chore: remove the usages of raw target closed message constant (#27669)

This commit is contained in:
Pavel Feldman 2023-10-17 15:35:41 -07:00 committed by GitHub
parent 5262e5ab35
commit 091f6883f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 74 additions and 54 deletions

View File

@ -281,7 +281,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
const waiter = Waiter.createForEvent(this, event); const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`); waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.AndroidDevice.Close) if (event !== Events.AndroidDevice.Close)
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new TargetClosedError()); waiter.rejectOnEvent(this, Events.AndroidDevice.Close, () => new TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate as any); const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose(); waiter.dispose();
return result; return result;

View File

@ -40,6 +40,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
// Used from @playwright/test fixtures. // Used from @playwright/test fixtures.
_connectHeaders?: HeadersArray; _connectHeaders?: HeadersArray;
_closeReason: string | undefined;
static from(browser: channels.BrowserChannel): Browser { static from(browser: channels.BrowserChannel): Browser {
return (browser as any)._object; return (browser as any)._object;
@ -131,6 +132,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
} }
async close(options: { reason?: string } = {}): Promise<void> { async close(options: { reason?: string } = {}): Promise<void> {
this._closeReason = options.reason;
try { try {
if (this._shouldCloseConnectionOnClose) if (this._shouldCloseConnectionOnClose)
this._connection.close(); this._connection.close();

View File

@ -64,6 +64,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
readonly _isChromium: boolean; readonly _isChromium: boolean;
private _harRecorders = new Map<string, { path: string, content: 'embed' | 'attach' | 'omit' | undefined }>(); private _harRecorders = new Map<string, { path: string, content: 'embed' | 'attach' | 'omit' | undefined }>();
private _closeWasCalled = false; private _closeWasCalled = false;
private _closeReason: string | undefined;
static from(context: channels.BrowserContextChannel): BrowserContext { static from(context: channels.BrowserContextChannel): BrowserContext {
return (context as any)._object; return (context as any)._object;
@ -337,6 +338,10 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._channel.setNetworkInterceptionPatterns({ patterns }); await this._channel.setNetworkInterceptionPatterns({ patterns });
} }
_effectiveCloseReason(): string | undefined {
return this._closeReason || this._browser?._closeReason;
}
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> { async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
return this._wrapApiCall(async () => { return this._wrapApiCall(async () => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
@ -344,7 +349,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
const waiter = Waiter.createForEvent(this, event); const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`); waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.BrowserContext.Close) if (event !== Events.BrowserContext.Close)
waiter.rejectOnEvent(this, Events.BrowserContext.Close, new TargetClosedError()); waiter.rejectOnEvent(this, Events.BrowserContext.Close, () => new TargetClosedError(this._effectiveCloseReason()));
const result = await waiter.waitForEvent(this, event, predicate as any); const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose(); waiter.dispose();
return result; return result;
@ -386,6 +391,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
async close(options: { reason?: string } = {}): Promise<void> { async close(options: { reason?: string } = {}): Promise<void> {
if (this._closeWasCalled) if (this._closeWasCalled)
return; return;
this._closeReason = options.reason;
this._closeWasCalled = true; this._closeWasCalled = true;
await this._wrapApiCall(async () => { await this._wrapApiCall(async () => {
await this._browserType?._willCloseContext(this); await this._browserType?._willCloseContext(this);

View File

@ -184,9 +184,7 @@ export class Connection extends EventEmitter {
} }
close(cause?: Error) { close(cause?: Error) {
this._closedError = new TargetClosedError(); this._closedError = new TargetClosedError(cause?.toString());
if (cause)
rewriteErrorMessage(this._closedError, this._closedError.message + '\nCaused by: ' + cause.toString());
for (const callback of this._callbacks.values()) for (const callback of this._callbacks.values())
callback.reject(this._closedError); callback.reject(this._closedError);
this._callbacks.clear(); this._callbacks.clear();

View File

@ -121,7 +121,7 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
const waiter = Waiter.createForEvent(this, event); const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`); waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== Events.ElectronApplication.Close) if (event !== Events.ElectronApplication.Close)
waiter.rejectOnEvent(this, Events.ElectronApplication.Close, new TargetClosedError()); waiter.rejectOnEvent(this, Events.ElectronApplication.Close, () => new TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate as any); const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose(); waiter.dispose();
return result; return result;

View File

@ -36,7 +36,6 @@ import { urlMatches } from '../utils/network';
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import type * as structs from '../../types/structs'; import type * as structs from '../../types/structs';
import { debugLogger } from '../common/debugLogger'; import { debugLogger } from '../common/debugLogger';
import { TargetClosedError } from '../common/errors';
export type WaitForNavigationOptions = { export type WaitForNavigationOptions = {
timeout?: number, timeout?: number,
@ -105,8 +104,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
private _setupNavigationWaiter(options: { timeout?: number }): Waiter { private _setupNavigationWaiter(options: { timeout?: number }): Waiter {
const waiter = new Waiter(this._page!, ''); const waiter = new Waiter(this._page!, '');
if (this._page!.isClosed()) if (this._page!.isClosed())
waiter.rejectImmediately(new TargetClosedError()); waiter.rejectImmediately(this._page!._closeErrorWithReason());
waiter.rejectOnEvent(this._page!, Events.Page.Close, new TargetClosedError()); waiter.rejectOnEvent(this._page!, Events.Page.Close, () => this._page!._closeErrorWithReason());
waiter.rejectOnEvent(this._page!, Events.Page.Crash, new Error('Navigation failed because page crashed!')); waiter.rejectOnEvent(this._page!, Events.Page.Crash, new Error('Navigation failed because page crashed!'));
waiter.rejectOnEvent<Frame>(this._page!, Events.Page.FrameDetached, new Error('Navigating frame was detached!'), frame => frame === this); waiter.rejectOnEvent<Frame>(this._page!, Events.Page.FrameDetached, new Error('Navigating frame was detached!'), frame => frame === this);
const timeout = this._page!._timeoutSettings.navigationTimeout(options); const timeout = this._page!._timeoutSettings.navigationTimeout(options);

View File

@ -34,7 +34,6 @@ import { MultiMap } from '../utils/multimap';
import { APIResponse } from './fetch'; import { APIResponse } from './fetch';
import type { Serializable } from '../../types/structs'; import type { Serializable } from '../../types/structs';
import type { BrowserContext } from './browserContext'; import type { BrowserContext } from './browserContext';
import { TargetClosedError } from '../common/errors';
export type NetworkCookie = { export type NetworkCookie = {
name: string, name: string,
@ -611,7 +610,7 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel> implement
waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error')); waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error'));
if (event !== Events.WebSocket.Close) if (event !== Events.WebSocket.Close)
waiter.rejectOnEvent(this, Events.WebSocket.Close, new Error('Socket closed')); waiter.rejectOnEvent(this, Events.WebSocket.Close, new Error('Socket closed'));
waiter.rejectOnEvent(this._page, Events.Page.Close, new TargetClosedError()); waiter.rejectOnEvent(this._page, Events.Page.Close, () => this._page._closeErrorWithReason());
const result = await waiter.waitForEvent(this, event, predicate as any); const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose(); waiter.dispose();
return result; return result;

View File

@ -19,7 +19,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import type * as structs from '../../types/structs'; import type * as structs from '../../types/structs';
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import { isTargetClosedError, TargetClosedError, kTargetClosedErrorMessage } from '../common/errors'; import { isTargetClosedError, TargetClosedError } from '../common/errors';
import { urlMatches } from '../utils/network'; import { urlMatches } from '../utils/network';
import { TimeoutSettings } from '../common/timeoutSettings'; import { TimeoutSettings } from '../common/timeoutSettings';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
@ -93,6 +93,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
readonly _timeoutSettings: TimeoutSettings; readonly _timeoutSettings: TimeoutSettings;
private _video: Video | null = null; private _video: Video | null = null;
readonly _opener: Page | null; readonly _opener: Page | null;
private _closeReason: string | undefined;
static from(page: channels.PageChannel): Page { static from(page: channels.PageChannel): Page {
return (page as any)._object; return (page as any)._object;
@ -140,8 +141,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
this.coverage = new Coverage(this._channel); this.coverage = new Coverage(this._channel);
this.once(Events.Page.Close, () => this._closedOrCrashedScope.close(kTargetClosedErrorMessage)); this.once(Events.Page.Close, () => this._closedOrCrashedScope.close(this._closeErrorWithReason()));
this.once(Events.Page.Crash, () => this._closedOrCrashedScope.close(kTargetClosedErrorMessage)); this.once(Events.Page.Crash, () => this._closedOrCrashedScope.close(new TargetClosedError()));
this._setEventToSubscriptionMapping(new Map<string, channels.PageUpdateSubscriptionParams['event']>([ this._setEventToSubscriptionMapping(new Map<string, channels.PageUpdateSubscriptionParams['event']>([
[Events.Page.Console, 'console'], [Events.Page.Console, 'console'],
@ -387,6 +388,10 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return this._waitForEvent(event, optionsOrPredicate, `waiting for event "${event}"`); return this._waitForEvent(event, optionsOrPredicate, `waiting for event "${event}"`);
} }
_closeErrorWithReason(): TargetClosedError {
return new TargetClosedError(this._closeReason || this._browserContext._effectiveCloseReason());
}
private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> { private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> {
return this._wrapApiCall(async () => { return this._wrapApiCall(async () => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
@ -398,7 +403,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
if (event !== Events.Page.Crash) if (event !== Events.Page.Crash)
waiter.rejectOnEvent(this, Events.Page.Crash, new Error('Page crashed')); waiter.rejectOnEvent(this, Events.Page.Crash, new Error('Page crashed'));
if (event !== Events.Page.Close) if (event !== Events.Page.Close)
waiter.rejectOnEvent(this, Events.Page.Close, new TargetClosedError()); waiter.rejectOnEvent(this, Events.Page.Close, () => this._closeErrorWithReason());
const result = await waiter.waitForEvent(this, event, predicate as any); const result = await waiter.waitForEvent(this, event, predicate as any);
waiter.dispose(); waiter.dispose();
return result; return result;
@ -513,7 +518,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
await this._channel.bringToFront(); await this._channel.bringToFront();
} }
async close(options: { runBeforeUnload?: boolean } = { runBeforeUnload: undefined }) { async close(options: { runBeforeUnload?: boolean, reason?: string } = {}) {
this._closeReason = options.reason;
try { try {
if (this._ownedContext) if (this._ownedContext)
await this._ownedContext.close(); await this._ownedContext.close();

View File

@ -50,9 +50,9 @@ export class Waiter {
return this.waitForPromise(promise, dispose); return this.waitForPromise(promise, dispose);
} }
rejectOnEvent<T = void>(emitter: EventEmitter, event: string, error: Error, predicate?: (arg: T) => boolean | Promise<boolean>) { rejectOnEvent<T = void>(emitter: EventEmitter, event: string, error: Error | (() => Error), predicate?: (arg: T) => boolean | Promise<boolean>) {
const { promise, dispose } = waitForEvent(emitter, event, predicate); const { promise, dispose } = waitForEvent(emitter, event, predicate);
this._rejectOn(promise.then(() => { throw error; }), dispose); this._rejectOn(promise.then(() => { throw (typeof error === 'function' ? error() : error); }), dispose);
} }
rejectOnTimeout(timeout: number, message: string) { rejectOnTimeout(timeout: number, message: string) {

View File

@ -23,7 +23,7 @@ import type { BrowserContext } from './browserContext';
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import type * as structs from '../../types/structs'; import type * as structs from '../../types/structs';
import { LongStandingScope } from '../utils'; import { LongStandingScope } from '../utils';
import { kTargetClosedErrorMessage } from '../common/errors'; import { TargetClosedError } from '../common/errors';
export class Worker extends ChannelOwner<channels.WorkerChannel> implements api.Worker { export class Worker extends ChannelOwner<channels.WorkerChannel> implements api.Worker {
_page: Page | undefined; // Set for web workers. _page: Page | undefined; // Set for web workers.
@ -43,7 +43,7 @@ export class Worker extends ChannelOwner<channels.WorkerChannel> implements api.
this._context._serviceWorkers.delete(this); this._context._serviceWorkers.delete(this);
this.emit(Events.Worker.Close, this); this.emit(Events.Worker.Close, this);
}); });
this.once(Events.Worker.Close, () => this._closedScope.close(kTargetClosedErrorMessage)); this.once(Events.Worker.Close, () => this._closedScope.close(this._page?._closeErrorWithReason() || new TargetClosedError()));
} }
url(): string { url(): string {

View File

@ -25,16 +25,13 @@ class CustomError extends Error {
export class TimeoutError extends CustomError {} export class TimeoutError extends CustomError {}
export const kTargetClosedErrorMessage = 'Target page, context or browser has been closed';
export const kTargetCrashedErrorMessage = 'Target crashed';
export class TargetClosedError extends Error { export class TargetClosedError extends Error {
constructor() { constructor(cause?: string, logs?: string) {
super(kTargetClosedErrorMessage); super((cause || 'Target page, context or browser has been closed') + (logs || ''));
this.name = this.constructor.name; this.name = this.constructor.name;
} }
} }
export function isTargetClosedError(error: Error) { export function isTargetClosedError(error: Error) {
return error instanceof TargetClosedError || error.message.includes(kTargetClosedErrorMessage); return error instanceof TargetClosedError || error.name === 'TargetClosedError';
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { TimeoutError } from '../common/errors'; import { TargetClosedError, TimeoutError } from '../common/errors';
import type { SerializedError, SerializedValue } from '@protocol/channels'; import type { SerializedError, SerializedValue } from '@protocol/channels';
export function serializeError(e: any): SerializedError { export function serializeError(e: any): SerializedError {
@ -34,6 +34,11 @@ export function parseError(error: SerializedError): Error {
e.stack = error.error.stack || ''; e.stack = error.error.stack || '';
return e; return e;
} }
if (error.error.name === 'TargetClosedError') {
const e = new TargetClosedError(error.error.message);
e.stack = error.error.stack || '';
return e;
}
const e = new Error(error.error.message); const e = new Error(error.error.message);
e.stack = error.error.stack || ''; e.stack = error.error.stack || '';
e.name = error.error.name; e.name = error.error.name;

View File

@ -19,7 +19,7 @@ import type * as channels from '@protocol/channels';
import { serializeError } from '../../protocol/serializers'; import { serializeError } from '../../protocol/serializers';
import { findValidator, ValidationError, createMetadataValidator, type ValidatorContext } from '../../protocol/validator'; import { findValidator, ValidationError, createMetadataValidator, type ValidatorContext } from '../../protocol/validator';
import { assert, isUnderTest, monotonicTime, rewriteErrorMessage } from '../../utils'; import { assert, isUnderTest, monotonicTime, rewriteErrorMessage } from '../../utils';
import { TargetClosedError, isTargetClosedError, kTargetClosedErrorMessage, kTargetCrashedErrorMessage } from '../../common/errors'; import { TargetClosedError, isTargetClosedError } from '../../common/errors';
import type { CallMetadata } from '../instrumentation'; import type { CallMetadata } from '../instrumentation';
import { SdkObject } from '../instrumentation'; import { SdkObject } from '../instrumentation';
import type { PlaywrightDispatcher } from './playwrightDispatcher'; import type { PlaywrightDispatcher } from './playwrightDispatcher';
@ -330,15 +330,17 @@ export class DispatcherConnection {
const validator = findValidator(dispatcher._type, method, 'Result'); const validator = findValidator(dispatcher._type, method, 'Result');
callMetadata.result = validator(result, '', { tChannelImpl: this._tChannelImplToWire.bind(this), binary: this._isLocal ? 'buffer' : 'toBase64' }); callMetadata.result = validator(result, '', { tChannelImpl: this._tChannelImplToWire.bind(this), binary: this._isLocal ? 'buffer' : 'toBase64' });
} catch (e) { } catch (e) {
if (isTargetClosedError(e) && sdkObject) if (isTargetClosedError(e) && sdkObject) {
rewriteErrorMessage(e, closeReason(sdkObject)); const reason = closeReason(sdkObject);
if (isProtocolError(e)) { if (reason)
rewriteErrorMessage(e, reason);
} else if (isProtocolError(e)) {
if (e.type === 'closed') { if (e.type === 'closed') {
const closedReason = sdkObject ? closeReason(sdkObject) : kTargetClosedErrorMessage; const reason = sdkObject ? closeReason(sdkObject) : undefined;
rewriteErrorMessage(e, closedReason + e.browserLogMessage()); e = new TargetClosedError(reason, e.browserLogMessage());
} else if (e.type === 'crashed') {
rewriteErrorMessage(e, 'Target crashed ' + e.browserLogMessage());
} }
if (e.type === 'crashed')
rewriteErrorMessage(e, kTargetCrashedErrorMessage + e.browserLogMessage());
} }
callMetadata.error = serializeError(e); callMetadata.error = serializeError(e);
} finally { } finally {
@ -357,8 +359,8 @@ export class DispatcherConnection {
} }
} }
function closeReason(sdkObject: SdkObject) { function closeReason(sdkObject: SdkObject): string | undefined {
return sdkObject.attribution.page?._closeReason || return sdkObject.attribution.page?._closeReason ||
sdkObject.attribution.context?._closeReason || sdkObject.attribution.context?._closeReason ||
sdkObject.attribution.browser?._closeReason || kTargetClosedErrorMessage; sdkObject.attribution.browser?._closeReason;
} }

View File

@ -1532,7 +1532,7 @@ export class Frame extends SdkObject {
_onDetached() { _onDetached() {
this._stopNetworkIdleTimer(); this._stopNetworkIdleTimer();
this._detachedScope.close('Frame was detached'); this._detachedScope.close(new Error('Frame was detached'));
for (const data of this._contextData.values()) { for (const data of this._contextData.values()) {
if (data.context) if (data.context)
data.context.contextDestroyed('Frame was detached'); data.context.contextDestroyed('Frame was detached');

View File

@ -73,7 +73,7 @@ export class ExecutionContext extends SdkObject {
} }
contextDestroyed(reason: string) { contextDestroyed(reason: string) {
this._contextDestroyedScope.close(reason); this._contextDestroyedScope.close(new Error(reason));
} }
async _raceAgainstContextDestroyed<T>(promise: Promise<T>): Promise<T> { async _raceAgainstContextDestroyed<T>(promise: Promise<T>): Promise<T> {

View File

@ -43,7 +43,7 @@ import type { TimeoutOptions } from '../common/types';
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser'; import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers'; import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers';
import type { SerializedValue } from './isomorphic/utilityScriptSerializers'; import type { SerializedValue } from './isomorphic/utilityScriptSerializers';
import { kTargetClosedErrorMessage } from '../common/errors'; import { TargetClosedError } from '../common/errors';
export interface PageDelegate { export interface PageDelegate {
readonly rawMouse: input.RawMouse; readonly rawMouse: input.RawMouse;
@ -277,7 +277,7 @@ export class Page extends SdkObject {
this.emit(Page.Events.Close); this.emit(Page.Events.Close);
this._closedPromise.resolve(); this._closedPromise.resolve();
this.instrumentation.onPageClose(this); this.instrumentation.onPageClose(this);
this.openScope.close(kTargetClosedErrorMessage); this.openScope.close(new TargetClosedError());
} }
_didCrash() { _didCrash() {
@ -286,7 +286,7 @@ export class Page extends SdkObject {
this.emit(Page.Events.Crash); this.emit(Page.Events.Crash);
this._crashed = true; this._crashed = true;
this.instrumentation.onPageClose(this); this.instrumentation.onPageClose(this);
this.openScope.close('Page crashed'); this.openScope.close(new Error('Page crashed'));
} }
async _onFileChooserOpened(handle: dom.ElementHandle) { async _onFileChooserOpened(handle: dom.ElementHandle) {
@ -733,7 +733,7 @@ export class Worker extends SdkObject {
if (this._existingExecutionContext) if (this._existingExecutionContext)
this._existingExecutionContext.contextDestroyed('Worker was closed'); this._existingExecutionContext.contextDestroyed('Worker was closed');
this.emit(Worker.Events.Close, this); this.emit(Worker.Events.Close, this);
this.openScope.close('Worker closed'); this.openScope.close(new Error('Worker closed'));
} }
async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise<any> { async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {

View File

@ -58,7 +58,7 @@ export class ManualPromise<T = void> extends Promise<T> {
export class LongStandingScope { export class LongStandingScope {
private _terminateError: Error | undefined; private _terminateError: Error | undefined;
private _terminateErrorMessage: string | undefined; private _closeError: Error | undefined;
private _terminatePromises = new Map<ManualPromise<Error>, string[]>(); private _terminatePromises = new Map<ManualPromise<Error>, string[]>();
private _isClosed = false; private _isClosed = false;
@ -69,14 +69,11 @@ export class LongStandingScope {
p.resolve(error); p.resolve(error);
} }
close(errorMessage: string) { close(error: Error) {
this._isClosed = true; this._isClosed = true;
this._terminateErrorMessage = errorMessage; this._closeError = error;
for (const [p, frames] of this._terminatePromises) { for (const [p, frames] of this._terminatePromises)
const error = new Error(errorMessage); p.resolve(cloneError(error, frames));
error.stack = [error.name + ':' + errorMessage, ...frames].join('\n');
p.resolve(error);
}
} }
isClosed() { isClosed() {
@ -97,11 +94,12 @@ export class LongStandingScope {
private async _race(promises: Promise<any>[], safe: boolean, defaultValue?: any): Promise<any> { private async _race(promises: Promise<any>[], safe: boolean, defaultValue?: any): Promise<any> {
const terminatePromise = new ManualPromise<Error>(); const terminatePromise = new ManualPromise<Error>();
const frames = captureRawStack();
if (this._terminateError) if (this._terminateError)
terminatePromise.resolve(this._terminateError); terminatePromise.resolve(this._terminateError);
if (this._terminateErrorMessage) if (this._closeError)
terminatePromise.resolve(new Error(this._terminateErrorMessage)); terminatePromise.resolve(cloneError(this._closeError, frames));
this._terminatePromises.set(terminatePromise, captureRawStack()); this._terminatePromises.set(terminatePromise, frames);
try { try {
return await Promise.race([ return await Promise.race([
terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)), terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)),
@ -112,3 +110,11 @@ export class LongStandingScope {
} }
} }
} }
function cloneError(error: Error, frames: string[]) {
const clone = new Error();
clone.name = error.name;
clone.message = error.message;
clone.stack = [error.name + ':' + error.message, ...frames].join('\n');
return clone;
}