diff options
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/util')
12 files changed, 996 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts new file mode 100644 index 0000000000..4d96d0cdf4 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import type {AwaitableIterable} from '../common/types.js'; + +/** + * @internal + */ +export class AsyncIterableUtil { + static async *map<T, U>( + iterable: AwaitableIterable<T>, + map: (item: T) => Promise<U> + ): AsyncIterable<U> { + for await (const value of iterable) { + yield await map(value); + } + } + + static async *flatMap<T, U>( + iterable: AwaitableIterable<T>, + map: (item: T) => AwaitableIterable<U> + ): AsyncIterable<U> { + for await (const value of iterable) { + yield* map(value); + } + } + + static async collect<T>(iterable: AwaitableIterable<T>): Promise<T[]> { + const result = []; + for await (const value of iterable) { + result.push(value); + } + return result; + } + + static async first<T>( + iterable: AwaitableIterable<T> + ): Promise<T | undefined> { + for await (const value of iterable) { + return value; + } + return; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.test.ts new file mode 100644 index 0000000000..b989e3a888 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.test.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {describe, it} from 'node:test'; + +import expect from 'expect'; +import sinon from 'sinon'; + +import {Deferred} from './Deferred.js'; + +describe('DeferredPromise', function () { + it('should catch errors', async () => { + // Async function before try/catch. + async function task() { + await new Promise(resolve => { + return setTimeout(resolve, 50); + }); + } + // Async function that fails. + function fails(): Deferred<void> { + const deferred = Deferred.create<void>(); + setTimeout(() => { + deferred.reject(new Error('test')); + }, 25); + return deferred; + } + + const expectedToFail = fails(); + await task(); + let caught = false; + try { + await expectedToFail.valueOrThrow(); + } catch (err) { + expect((err as Error).message).toEqual('test'); + caught = true; + } + expect(caught).toBeTruthy(); + }); + + it('Deferred.race should cancel timeout', async function () { + const clock = sinon.useFakeTimers(); + + try { + const deferred = Deferred.create<void>(); + const deferredTimeout = Deferred.create<void>({ + message: 'Race did not stop timer', + timeout: 100, + }); + + clock.tick(50); + + await Promise.all([ + Deferred.race([deferred, deferredTimeout]), + deferred.resolve(), + ]); + + clock.tick(150); + + expect(deferredTimeout.value()).toBeInstanceOf(Error); + expect(deferredTimeout.value()?.message).toContain('Timeout cleared'); + } finally { + clock.restore(); + } + }); +}); diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts new file mode 100644 index 0000000000..0dfb013bb3 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts @@ -0,0 +1,122 @@ +import {TimeoutError} from '../common/Errors.js'; + +/** + * @internal + */ +export interface DeferredOptions { + message: string; + timeout: number; +} + +/** + * Creates and returns a deferred object along with the resolve/reject functions. + * + * If the deferred has not been resolved/rejected within the `timeout` period, + * the deferred gets resolves with a timeout error. `timeout` has to be greater than 0 or + * it is ignored. + * + * @internal + */ +export class Deferred<T, V extends Error = Error> { + static create<R, X extends Error = Error>( + opts?: DeferredOptions + ): Deferred<R, X> { + return new Deferred<R, X>(opts); + } + + static async race<R>( + awaitables: Array<Promise<R> | Deferred<R>> + ): Promise<R> { + const deferredWithTimeout = new Set<Deferred<R>>(); + try { + const promises = awaitables.map(value => { + if (value instanceof Deferred) { + if (value.#timeoutId) { + deferredWithTimeout.add(value); + } + + return value.valueOrThrow(); + } + + return value; + }); + // eslint-disable-next-line no-restricted-syntax + return await Promise.race(promises); + } finally { + for (const deferred of deferredWithTimeout) { + // We need to stop the timeout else + // Node.JS will keep running the event loop till the + // timer executes + deferred.reject(new Error('Timeout cleared')); + } + } + } + + #isResolved = false; + #isRejected = false; + #value: T | V | TimeoutError | undefined; + // SAFETY: This is ensured by #taskPromise. + #resolve!: (value: void) => void; + #taskPromise = new Promise<void>(resolve => { + this.#resolve = resolve; + }); + #timeoutId: ReturnType<typeof setTimeout> | undefined; + #timeoutError: TimeoutError | undefined; + + constructor(opts?: DeferredOptions) { + if (opts && opts.timeout > 0) { + this.#timeoutError = new TimeoutError(opts.message); + this.#timeoutId = setTimeout(() => { + this.reject(this.#timeoutError!); + }, opts.timeout); + } + } + + #finish(value: T | V | TimeoutError) { + clearTimeout(this.#timeoutId); + this.#value = value; + this.#resolve(); + } + + resolve(value: T): void { + if (this.#isRejected || this.#isResolved) { + return; + } + this.#isResolved = true; + this.#finish(value); + } + + reject(error: V | TimeoutError): void { + if (this.#isRejected || this.#isResolved) { + return; + } + this.#isRejected = true; + this.#finish(error); + } + + resolved(): boolean { + return this.#isResolved; + } + + finished(): boolean { + return this.#isResolved || this.#isRejected; + } + + value(): T | V | TimeoutError | undefined { + return this.#value; + } + + #promise: Promise<T> | undefined; + valueOrThrow(): Promise<T> { + if (!this.#promise) { + this.#promise = (async () => { + await this.#taskPromise; + if (this.#isRejected) { + throw this.#value; + } + return this.#value as T; + })(); + } + return this.#promise; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts new file mode 100644 index 0000000000..d4ab3044ab --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts @@ -0,0 +1,66 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {ProtocolError} from '../common/Errors.js'; + +/** + * @internal + */ +export interface ErrorLike extends Error { + name: string; + message: string; +} + +/** + * @internal + */ +export function isErrorLike(obj: unknown): obj is ErrorLike { + return ( + typeof obj === 'object' && obj !== null && 'name' in obj && 'message' in obj + ); +} + +/** + * @internal + */ +export function isErrnoException(obj: unknown): obj is NodeJS.ErrnoException { + return ( + isErrorLike(obj) && + ('errno' in obj || 'code' in obj || 'path' in obj || 'syscall' in obj) + ); +} + +/** + * @internal + */ +export function rewriteError( + error: ProtocolError, + message: string, + originalMessage?: string +): Error { + error.message = message; + error.originalMessage = originalMessage ?? error.originalMessage; + return error; +} + +/** + * @internal + */ +export function createProtocolErrorMessage(object: { + error: {message: string; data: any; code: number}; +}): string { + let message = object.error.message; + // TODO: remove the type checks when we stop connecting to BiDi with a CDP + // client. + if ( + object.error && + typeof object.error === 'object' && + 'data' in object.error + ) { + message += ` ${object.error.data}`; + } + return message; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.test.ts new file mode 100644 index 0000000000..c6da4cdf27 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.test.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {describe, it} from 'node:test'; + +import expect from 'expect'; + +import {interpolateFunction} from './Function.js'; + +describe('Function', function () { + describe('interpolateFunction', function () { + it('should work', async () => { + const test = interpolateFunction( + () => { + const test = PLACEHOLDER('test') as () => number; + return test(); + }, + {test: `() => 5`} + ); + expect(test()).toBe(5); + }); + it('should work inlined', async () => { + const test = interpolateFunction( + () => { + // Note the parenthesis will be removed by the typescript compiler. + return (PLACEHOLDER('test') as () => number)(); + }, + {test: `() => 5`} + ); + expect(test()).toBe(5); + }); + }); +}); diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts new file mode 100644 index 0000000000..41db98830b --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +const createdFunctions = new Map<string, (...args: unknown[]) => unknown>(); + +/** + * Creates a function from a string. + * + * @internal + */ +export const createFunction = ( + functionValue: string +): ((...args: unknown[]) => unknown) => { + let fn = createdFunctions.get(functionValue); + if (fn) { + return fn; + } + fn = new Function(`return ${functionValue}`)() as ( + ...args: unknown[] + ) => unknown; + createdFunctions.set(functionValue, fn); + return fn; +}; + +/** + * @internal + */ +export function stringifyFunction(fn: (...args: never) => unknown): string { + let value = fn.toString(); + try { + new Function(`(${value})`); + } catch { + // This means we might have a function shorthand (e.g. `test(){}`). Let's + // try prefixing. + let prefix = 'function '; + if (value.startsWith('async ')) { + prefix = `async ${prefix}`; + value = value.substring('async '.length); + } + value = `${prefix}${value}`; + try { + new Function(`(${value})`); + } catch { + // We tried hard to serialize, but there's a weird beast here. + throw new Error('Passed function cannot be serialized!'); + } + } + return value; +} + +/** + * Replaces `PLACEHOLDER`s with the given replacements. + * + * All replacements must be valid JS code. + * + * @example + * + * ```ts + * interpolateFunction(() => PLACEHOLDER('test'), {test: 'void 0'}); + * // Equivalent to () => void 0 + * ``` + * + * @internal + */ +export const interpolateFunction = <T extends (...args: never[]) => unknown>( + fn: T, + replacements: Record<string, string> +): T => { + let value = stringifyFunction(fn); + for (const [name, jsValue] of Object.entries(replacements)) { + value = value.replace( + new RegExp(`PLACEHOLDER\\(\\s*(?:'${name}'|"${name}")\\s*\\)`, 'g'), + // Wrapping this ensures tersers that accidently inline PLACEHOLDER calls + // are still valid. Without, we may get calls like ()=>{...}() which is + // not valid. + `(${jsValue})` + ); + } + return createFunction(value) as unknown as T; +}; + +declare global { + /** + * Used for interpolation with {@link interpolateFunction}. + * + * @internal + */ + function PLACEHOLDER<T>(name: string): T; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts new file mode 100644 index 0000000000..9498bac306 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts @@ -0,0 +1,41 @@ +import {Deferred} from './Deferred.js'; +import {disposeSymbol} from './disposable.js'; + +/** + * @internal + */ +export class Mutex { + static Guard = class Guard { + #mutex: Mutex; + constructor(mutex: Mutex) { + this.#mutex = mutex; + } + [disposeSymbol](): void { + return this.#mutex.release(); + } + }; + + #locked = false; + #acquirers: Array<() => void> = []; + + // This is FIFO. + async acquire(): Promise<InstanceType<typeof Mutex.Guard>> { + if (!this.#locked) { + this.#locked = true; + return new Mutex.Guard(this); + } + const deferred = Deferred.create<void>(); + this.#acquirers.push(deferred.resolve.bind(deferred)); + await deferred.valueOrThrow(); + return new Mutex.Guard(this); + } + + release(): void { + const resolve = this.#acquirers.shift(); + if (!resolve) { + this.#locked = false; + return; + } + resolve(); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts new file mode 100644 index 0000000000..7800b3be40 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2020 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Asserts that the given value is truthy. + * @param value - some conditional statement + * @param message - the error message to throw if the value is not truthy. + * + * @internal + */ +export const assert: (value: unknown, message?: string) => asserts value = ( + value, + message +) => { + if (!value) { + throw new Error(message); + } +}; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts new file mode 100644 index 0000000000..4cdaf15d5b --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {describe, it} from 'node:test'; + +import expect from 'expect'; +import sinon from 'sinon'; + +import {invokeAtMostOnceForArguments} from './decorators.js'; + +describe('decorators', function () { + describe('invokeAtMostOnceForArguments', () => { + it('should delegate calls', () => { + const spy = sinon.spy(); + class Test { + @invokeAtMostOnceForArguments + test(obj1: object, obj2: object) { + spy(obj1, obj2); + } + } + const t = new Test(); + expect(spy.callCount).toBe(0); + const obj1 = {}; + const obj2 = {}; + t.test(obj1, obj2); + expect(spy.callCount).toBe(1); + }); + + it('should prevent repeated calls', () => { + const spy = sinon.spy(); + class Test { + @invokeAtMostOnceForArguments + test(obj1: object, obj2: object) { + spy(obj1, obj2); + } + } + const t = new Test(); + expect(spy.callCount).toBe(0); + const obj1 = {}; + const obj2 = {}; + t.test(obj1, obj2); + expect(spy.callCount).toBe(1); + expect(spy.lastCall.calledWith(obj1, obj2)).toBeTruthy(); + t.test(obj1, obj2); + expect(spy.callCount).toBe(1); + expect(spy.lastCall.calledWith(obj1, obj2)).toBeTruthy(); + const obj3 = {}; + t.test(obj1, obj3); + expect(spy.callCount).toBe(2); + expect(spy.lastCall.calledWith(obj1, obj3)).toBeTruthy(); + }); + + it('should throw an error for dynamic argumetns', () => { + class Test { + @invokeAtMostOnceForArguments + test(..._args: unknown[]) {} + } + const t = new Test(); + t.test({}); + expect(() => { + t.test({}, {}); + }).toThrow(); + }); + + it('should throw an error for non object arguments', () => { + class Test { + @invokeAtMostOnceForArguments + test(..._args: unknown[]) {} + } + const t = new Test(); + expect(() => { + t.test(1); + }).toThrow(); + }); + }); +}); diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts new file mode 100644 index 0000000000..af21c5fe29 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts @@ -0,0 +1,140 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Disposed, Moveable} from '../common/types.js'; + +import {asyncDisposeSymbol, disposeSymbol} from './disposable.js'; +import {Mutex} from './Mutex.js'; + +const instances = new WeakSet<object>(); + +export function moveable< + Class extends abstract new (...args: never[]) => Moveable, +>(Class: Class, _: ClassDecoratorContext<Class>): Class { + let hasDispose = false; + if (Class.prototype[disposeSymbol]) { + const dispose = Class.prototype[disposeSymbol]; + Class.prototype[disposeSymbol] = function (this: InstanceType<Class>) { + if (instances.has(this)) { + instances.delete(this); + return; + } + return dispose.call(this); + }; + hasDispose = true; + } + if (Class.prototype[asyncDisposeSymbol]) { + const asyncDispose = Class.prototype[asyncDisposeSymbol]; + Class.prototype[asyncDisposeSymbol] = function (this: InstanceType<Class>) { + if (instances.has(this)) { + instances.delete(this); + return; + } + return asyncDispose.call(this); + }; + hasDispose = true; + } + if (hasDispose) { + Class.prototype.move = function ( + this: InstanceType<Class> + ): InstanceType<Class> { + instances.add(this); + return this; + }; + } + return Class; +} + +export function throwIfDisposed<This extends Disposed>( + message: (value: This) => string = value => { + return `Attempted to use disposed ${value.constructor.name}.`; + } +) { + return (target: (this: This, ...args: any[]) => any, _: unknown) => { + return function (this: This, ...args: any[]): any { + if (this.disposed) { + throw new Error(message(this)); + } + return target.call(this, ...args); + }; + }; +} + +export function inertIfDisposed<This extends Disposed>( + target: (this: This, ...args: any[]) => any, + _: unknown +) { + return function (this: This, ...args: any[]): any { + if (this.disposed) { + return; + } + return target.call(this, ...args); + }; +} + +/** + * The decorator only invokes the target if the target has not been invoked with + * the same arguments before. The decorated method throws an error if it's + * invoked with a different number of elements: if you decorate a method, it + * should have the same number of arguments + * + * @internal + */ +export function invokeAtMostOnceForArguments( + target: (this: unknown, ...args: any[]) => any, + _: unknown +): typeof target { + const cache = new WeakMap(); + let cacheDepth = -1; + return function (this: unknown, ...args: unknown[]) { + if (cacheDepth === -1) { + cacheDepth = args.length; + } + if (cacheDepth !== args.length) { + throw new Error( + 'Memoized method was called with the wrong number of arguments' + ); + } + let freshArguments = false; + let cacheIterator = cache; + for (const arg of args) { + if (cacheIterator.has(arg as object)) { + cacheIterator = cacheIterator.get(arg as object)!; + } else { + freshArguments = true; + cacheIterator.set(arg as object, new WeakMap()); + cacheIterator = cacheIterator.get(arg as object)!; + } + } + if (!freshArguments) { + return; + } + return target.call(this, ...args); + }; +} + +export function guarded<T extends object>( + getKey = function (this: T): object { + return this; + } +) { + return ( + target: (this: T, ...args: any[]) => Promise<any>, + _: ClassMethodDecoratorContext<T> + ): typeof target => { + const mutexes = new WeakMap<object, Mutex>(); + return async function (...args) { + const key = getKey.call(this); + let mutex = mutexes.get(key); + if (!mutex) { + mutex = new Mutex(); + mutexes.set(key, mutex); + } + await using _ = await mutex.acquire(); + return await target.call(this, ...args); + }; + }; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/disposable.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/disposable.ts new file mode 100644 index 0000000000..a1848f3860 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/disposable.ts @@ -0,0 +1,275 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +declare global { + interface SymbolConstructor { + /** + * A method that is used to release resources held by an object. Called by + * the semantics of the `using` statement. + */ + readonly dispose: unique symbol; + + /** + * A method that is used to asynchronously release resources held by an + * object. Called by the semantics of the `await using` statement. + */ + readonly asyncDispose: unique symbol; + } + + interface Disposable { + [Symbol.dispose](): void; + } + + interface AsyncDisposable { + [Symbol.asyncDispose](): PromiseLike<void>; + } +} + +(Symbol as any).dispose ??= Symbol('dispose'); +(Symbol as any).asyncDispose ??= Symbol('asyncDispose'); + +/** + * @internal + */ +export const disposeSymbol: typeof Symbol.dispose = Symbol.dispose; + +/** + * @internal + */ +export const asyncDisposeSymbol: typeof Symbol.asyncDispose = + Symbol.asyncDispose; + +/** + * @internal + */ +export class DisposableStack { + #disposed = false; + #stack: Disposable[] = []; + + /** + * Returns a value indicating whether this stack has been disposed. + */ + get disposed(): boolean { + return this.#disposed; + } + + /** + * Disposes each resource in the stack in the reverse order that they were added. + */ + dispose(): void { + if (this.#disposed) { + return; + } + this.#disposed = true; + for (const resource of this.#stack.reverse()) { + resource[disposeSymbol](); + } + } + + /** + * Adds a disposable resource to the stack, returning the resource. + * + * @param value - The resource to add. `null` and `undefined` will not be added, + * but will be returned. + * @returns The provided `value`. + */ + use<T extends Disposable | null | undefined>(value: T): T { + if (value) { + this.#stack.push(value); + } + return value; + } + + /** + * Adds a value and associated disposal callback as a resource to the stack. + * + * @param value - The value to add. + * @param onDispose - The callback to use in place of a `[disposeSymbol]()` + * method. Will be invoked with `value` as the first parameter. + * @returns The provided `value`. + */ + adopt<T>(value: T, onDispose: (value: T) => void): T { + this.#stack.push({ + [disposeSymbol]() { + onDispose(value); + }, + }); + return value; + } + + /** + * Adds a callback to be invoked when the stack is disposed. + */ + defer(onDispose: () => void): void { + this.#stack.push({ + [disposeSymbol]() { + onDispose(); + }, + }); + } + + /** + * Move all resources out of this stack and into a new `DisposableStack`, and + * marks this stack as disposed. + * + * @example + * + * ```ts + * class C { + * #res1: Disposable; + * #res2: Disposable; + * #disposables: DisposableStack; + * constructor() { + * // stack will be disposed when exiting constructor for any reason + * using stack = new DisposableStack(); + * + * // get first resource + * this.#res1 = stack.use(getResource1()); + * + * // get second resource. If this fails, both `stack` and `#res1` will be disposed. + * this.#res2 = stack.use(getResource2()); + * + * // all operations succeeded, move resources out of `stack` so that + * // they aren't disposed when constructor exits + * this.#disposables = stack.move(); + * } + * + * [disposeSymbol]() { + * this.#disposables.dispose(); + * } + * } + * ``` + */ + move(): DisposableStack { + if (this.#disposed) { + throw new ReferenceError('a disposed stack can not use anything new'); // step 3 + } + const stack = new DisposableStack(); // step 4-5 + stack.#stack = this.#stack; + this.#disposed = true; + return stack; + } + + [disposeSymbol] = this.dispose; + + readonly [Symbol.toStringTag] = 'DisposableStack'; +} + +/** + * @internal + */ +export class AsyncDisposableStack { + #disposed = false; + #stack: AsyncDisposable[] = []; + + /** + * Returns a value indicating whether this stack has been disposed. + */ + get disposed(): boolean { + return this.#disposed; + } + + /** + * Disposes each resource in the stack in the reverse order that they were added. + */ + async dispose(): Promise<void> { + if (this.#disposed) { + return; + } + this.#disposed = true; + for (const resource of this.#stack.reverse()) { + await resource[asyncDisposeSymbol](); + } + } + + /** + * Adds a disposable resource to the stack, returning the resource. + * + * @param value - The resource to add. `null` and `undefined` will not be added, + * but will be returned. + * @returns The provided `value`. + */ + use<T extends AsyncDisposable | null | undefined>(value: T): T { + if (value) { + this.#stack.push(value); + } + return value; + } + + /** + * Adds a value and associated disposal callback as a resource to the stack. + * + * @param value - The value to add. + * @param onDispose - The callback to use in place of a `[disposeSymbol]()` + * method. Will be invoked with `value` as the first parameter. + * @returns The provided `value`. + */ + adopt<T>(value: T, onDispose: (value: T) => Promise<void>): T { + this.#stack.push({ + [asyncDisposeSymbol]() { + return onDispose(value); + }, + }); + return value; + } + + /** + * Adds a callback to be invoked when the stack is disposed. + */ + defer(onDispose: () => Promise<void>): void { + this.#stack.push({ + [asyncDisposeSymbol]() { + return onDispose(); + }, + }); + } + + /** + * Move all resources out of this stack and into a new `DisposableStack`, and + * marks this stack as disposed. + * + * @example + * + * ```ts + * class C { + * #res1: Disposable; + * #res2: Disposable; + * #disposables: DisposableStack; + * constructor() { + * // stack will be disposed when exiting constructor for any reason + * using stack = new DisposableStack(); + * + * // get first resource + * this.#res1 = stack.use(getResource1()); + * + * // get second resource. If this fails, both `stack` and `#res1` will be disposed. + * this.#res2 = stack.use(getResource2()); + * + * // all operations succeeded, move resources out of `stack` so that + * // they aren't disposed when constructor exits + * this.#disposables = stack.move(); + * } + * + * [disposeSymbol]() { + * this.#disposables.dispose(); + * } + * } + * ``` + */ + move(): AsyncDisposableStack { + if (this.#disposed) { + throw new ReferenceError('a disposed stack can not use anything new'); // step 3 + } + const stack = new AsyncDisposableStack(); // step 4-5 + stack.#stack = this.#stack; + this.#disposed = true; + return stack; + } + + [asyncDisposeSymbol] = this.dispose; + + readonly [Symbol.toStringTag] = 'AsyncDisposableStack'; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts new file mode 100644 index 0000000000..f55610da9e --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './assert.js'; +export * from './Deferred.js'; +export * from './ErrorLike.js'; +export * from './AsyncIterableUtil.js'; +export * from './disposable.js'; |