UNPKG

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