summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/puppeteer-core/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/util')
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts46
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.test.ts68
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Deferred.ts122
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts66
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Function.test.ts36
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts91
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/Mutex.ts41
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts21
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.test.ts79
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/decorators.ts140
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/disposable.ts275
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts11
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';