CallbackRegistry.ts (4117B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import {Deferred} from '../util/Deferred.js'; 8 import {rewriteError} from '../util/ErrorLike.js'; 9 import {createIncrementalIdGenerator} from '../util/incremental-id-generator.js'; 10 11 import {ProtocolError, TargetCloseError} from './Errors.js'; 12 import {debugError} from './util.js'; 13 14 const idGenerator = createIncrementalIdGenerator(); 15 16 /** 17 * Manages callbacks and their IDs for the protocol request/response communication. 18 * 19 * @internal 20 */ 21 export class CallbackRegistry { 22 #callbacks = new Map<number, Callback>(); 23 #idGenerator = idGenerator; 24 25 create( 26 label: string, 27 timeout: number | undefined, 28 request: (id: number) => void, 29 ): Promise<unknown> { 30 const callback = new Callback(this.#idGenerator(), label, timeout); 31 this.#callbacks.set(callback.id, callback); 32 try { 33 request(callback.id); 34 } catch (error) { 35 // We still throw sync errors synchronously and clean up the scheduled 36 // callback. 37 callback.promise.catch(debugError).finally(() => { 38 this.#callbacks.delete(callback.id); 39 }); 40 callback.reject(error as Error); 41 throw error; 42 } 43 // Must only have sync code up until here. 44 return callback.promise.finally(() => { 45 this.#callbacks.delete(callback.id); 46 }); 47 } 48 49 reject(id: number, message: string, originalMessage?: string): void { 50 const callback = this.#callbacks.get(id); 51 if (!callback) { 52 return; 53 } 54 this._reject(callback, message, originalMessage); 55 } 56 57 rejectRaw(id: number, error: object): void { 58 const callback = this.#callbacks.get(id); 59 if (!callback) { 60 return; 61 } 62 callback.reject(error as any); 63 } 64 65 _reject( 66 callback: Callback, 67 errorMessage: string | ProtocolError, 68 originalMessage?: string, 69 ): void { 70 let error: ProtocolError; 71 let message: string; 72 if (errorMessage instanceof ProtocolError) { 73 error = errorMessage; 74 error.cause = callback.error; 75 message = errorMessage.message; 76 } else { 77 error = callback.error; 78 message = errorMessage; 79 } 80 81 callback.reject( 82 rewriteError( 83 error, 84 `Protocol error (${callback.label}): ${message}`, 85 originalMessage, 86 ), 87 ); 88 } 89 90 resolve(id: number, value: unknown): void { 91 const callback = this.#callbacks.get(id); 92 if (!callback) { 93 return; 94 } 95 callback.resolve(value); 96 } 97 98 clear(): void { 99 for (const callback of this.#callbacks.values()) { 100 // TODO: probably we can accept error messages as params. 101 this._reject(callback, new TargetCloseError('Target closed')); 102 } 103 this.#callbacks.clear(); 104 } 105 106 /** 107 * @internal 108 */ 109 getPendingProtocolErrors(): Error[] { 110 const result: Error[] = []; 111 for (const callback of this.#callbacks.values()) { 112 result.push( 113 new Error( 114 `${callback.label} timed out. Trace: ${callback.error.stack}`, 115 ), 116 ); 117 } 118 return result; 119 } 120 } 121 /** 122 * @internal 123 */ 124 125 export class Callback { 126 #id: number; 127 #error = new ProtocolError(); 128 #deferred = Deferred.create<unknown>(); 129 #timer?: ReturnType<typeof setTimeout>; 130 #label: string; 131 132 constructor(id: number, label: string, timeout?: number) { 133 this.#id = id; 134 this.#label = label; 135 if (timeout) { 136 this.#timer = setTimeout(() => { 137 this.#deferred.reject( 138 rewriteError( 139 this.#error, 140 `${label} timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.`, 141 ), 142 ); 143 }, timeout); 144 } 145 } 146 147 resolve(value: unknown): void { 148 clearTimeout(this.#timer); 149 this.#deferred.resolve(value); 150 } 151 152 reject(error: Error): void { 153 clearTimeout(this.#timer); 154 this.#deferred.reject(error); 155 } 156 157 get id(): number { 158 return this.#id; 159 } 160 161 get promise(): Promise<unknown> { 162 return this.#deferred.valueOrThrow(); 163 } 164 165 get error(): ProtocolError { 166 return this.#error; 167 } 168 169 get label(): string { 170 return this.#label; 171 } 172 }