agl 23.0.0 → 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/changelog.md +11 -0
  2. package/dist_serve/bundle.js +811 -808
  3. package/dist_serve/bundle.js.map +1 -0
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/classes.authstore.js +2 -2
  6. package/dist_ts/classes.browserresourcehost.d.ts +1 -0
  7. package/dist_ts/classes.browserresourcehost.js +7 -1
  8. package/dist_ts/classes.controller.d.ts +35 -5
  9. package/dist_ts/classes.controller.js +994 -229
  10. package/dist_ts/classes.validation.d.ts +9 -1
  11. package/dist_ts/classes.validation.js +62 -7
  12. package/dist_ts_interfaces/browsertransport.d.ts +72 -0
  13. package/dist_ts_interfaces/browsertransport.js +47 -0
  14. package/dist_ts_interfaces/index.d.ts +1 -0
  15. package/dist_ts_interfaces/index.js +2 -1
  16. package/dist_ts_interfaces/interfaces.d.ts +10 -6
  17. package/dist_ts_interfaces/interfaces.js +2 -3
  18. package/dist_ts_interfaces/requests.d.ts +20 -21
  19. package/package.json +5 -5
  20. package/readme.md +7 -3
  21. package/ts/00_commitinfo_data.ts +1 -1
  22. package/ts/classes.authstore.ts +1 -1
  23. package/ts/classes.browserresourcehost.ts +7 -0
  24. package/ts/classes.controller.ts +1367 -279
  25. package/ts/classes.validation.ts +77 -6
  26. package/ts_interfaces/browsertransport.ts +126 -0
  27. package/ts_interfaces/index.ts +1 -0
  28. package/ts_interfaces/interfaces.ts +11 -11
  29. package/ts_interfaces/requests.ts +23 -30
  30. package/ts_web/00_commitinfo_data.ts +1 -1
  31. package/ts_web/classes.browserviewtransport.ts +532 -0
  32. package/ts_web/classes.controllersocketclient.ts +6 -17
  33. package/ts_web/elements.harnesscontrollerapp.ts +450 -115
  34. package/ts_web/index.ts +1 -0
  35. package/ts_web/plugins.ts +2 -0
@@ -0,0 +1,532 @@
1
+ import * as plugins from './plugins.js';
2
+ import * as interfaces from '../ts_interfaces/index.js';
3
+
4
+ const browserOperationTimeoutMs = 35_000;
5
+ const browserTransportStopTimeoutMs = 1_000;
6
+ const textEncoder = new TextEncoder();
7
+
8
+ interface IPendingBrowserOperation {
9
+ resolve(resultArg: interfaces.TControllerBrowserOperationResult): void;
10
+ reject(reasonArg: unknown): void;
11
+ abortSignal?: AbortSignal;
12
+ abortListener?: () => void;
13
+ timeout: ReturnType<typeof setTimeout>;
14
+ }
15
+
16
+ export interface IControllerBrowserViewTransportClientOptions {
17
+ streamGeneration: number;
18
+ events: plugins.typedrequestInterfaces.TVirtualStream<'receive'>;
19
+ operations: plugins.typedrequestInterfaces.TVirtualStream<'send'>;
20
+ onEvent(eventArg: interfaces.TControllerBrowserViewEvent): void;
21
+ onFailure(errorArg: unknown, recoverableArg: boolean): void;
22
+ }
23
+
24
+ export class ControllerBrowserOperationError extends Error {
25
+ constructor(
26
+ public readonly code: string,
27
+ messageArg: string,
28
+ ) {
29
+ super(messageArg);
30
+ this.name = 'ControllerBrowserOperationError';
31
+ }
32
+ }
33
+
34
+ type TObject = Record<string, unknown>;
35
+
36
+ const assertObject = (valueArg: unknown, nameArg: string): TObject => {
37
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
38
+ throw new interfaces.ControllerBrowserTransportProtocolError(`${nameArg} must be an object.`);
39
+ }
40
+ const prototype = Object.getPrototypeOf(valueArg);
41
+ if (prototype !== Object.prototype && prototype !== null) {
42
+ throw new interfaces.ControllerBrowserTransportProtocolError(`${nameArg} must be a plain object.`);
43
+ }
44
+ return valueArg as TObject;
45
+ };
46
+
47
+ const assertKeys = (
48
+ valueArg: TObject,
49
+ requiredArg: readonly string[],
50
+ optionalArg: readonly string[] = [],
51
+ ): void => {
52
+ const allowed = new Set([...requiredArg, ...optionalArg]);
53
+ if (
54
+ !requiredArg.every((keyArg) => Object.prototype.hasOwnProperty.call(valueArg, keyArg))
55
+ || Object.keys(valueArg).some((keyArg) => !allowed.has(keyArg))
56
+ ) throw new interfaces.ControllerBrowserTransportProtocolError('Browser packet keys are invalid.');
57
+ };
58
+
59
+ const assertBoundedString = (valueArg: unknown, nameArg: string, maximumBytesArg = 512): string => {
60
+ if (
61
+ typeof valueArg !== 'string'
62
+ || valueArg.length === 0
63
+ || textEncoder.encode(valueArg).byteLength > maximumBytesArg
64
+ ) throw new interfaces.ControllerBrowserTransportProtocolError(`${nameArg} is invalid.`);
65
+ return valueArg;
66
+ };
67
+
68
+ const assertSafeInteger = (valueArg: unknown, nameArg: string, minimumArg = 0): number => {
69
+ if (!Number.isSafeInteger(valueArg) || (valueArg as number) < minimumArg) {
70
+ throw new interfaces.ControllerBrowserTransportProtocolError(`${nameArg} is invalid.`);
71
+ }
72
+ return valueArg as number;
73
+ };
74
+
75
+ const assertFiniteNumber = (valueArg: unknown, nameArg: string): number => {
76
+ if (typeof valueArg !== 'number' || !Number.isFinite(valueArg)) {
77
+ throw new interfaces.ControllerBrowserTransportProtocolError(`${nameArg} is invalid.`);
78
+ }
79
+ return valueArg;
80
+ };
81
+
82
+ const assertBrowserViewport = (valueArg: unknown): interfaces.IControllerBrowserViewport => {
83
+ const value = assertObject(valueArg, 'frame.viewport');
84
+ assertKeys(value, ['width', 'height', 'deviceScaleFactor']);
85
+ const width = assertSafeInteger(value.width, 'viewport.width', 1);
86
+ const height = assertSafeInteger(value.height, 'viewport.height', 1);
87
+ const deviceScaleFactor = assertFiniteNumber(value.deviceScaleFactor, 'viewport.deviceScaleFactor');
88
+ if (
89
+ width > 4096
90
+ || height > 4096
91
+ || deviceScaleFactor < 0.25
92
+ || deviceScaleFactor > 3
93
+ || Math.ceil(width * deviceScaleFactor) * Math.ceil(height * deviceScaleFactor) > 8_294_400
94
+ ) throw new interfaces.ControllerBrowserTransportProtocolError('frame.viewport is out of bounds.');
95
+ return { width, height, deviceScaleFactor };
96
+ };
97
+
98
+ const assertBrowserError = (
99
+ valueArg: unknown,
100
+ fatalRequiredArg = true,
101
+ ): { code: string; message: string; fatal: boolean; tabId?: string } => {
102
+ const value = assertObject(valueArg, 'browser error');
103
+ assertKeys(value, fatalRequiredArg ? ['code', 'message', 'fatal'] : ['code', 'message'], ['tabId']);
104
+ if (fatalRequiredArg && typeof value.fatal !== 'boolean') {
105
+ throw new interfaces.ControllerBrowserTransportProtocolError('browser error fatal flag is invalid.');
106
+ }
107
+ const tabId = value.tabId === undefined
108
+ ? undefined
109
+ : assertBoundedString(value.tabId, 'browser error tabId');
110
+ return {
111
+ code: assertBoundedString(value.code, 'browser error code', 128),
112
+ message: assertBoundedString(value.message, 'browser error message', 512),
113
+ fatal: fatalRequiredArg ? value.fatal as boolean : false,
114
+ ...(tabId === undefined ? {} : { tabId }),
115
+ };
116
+ };
117
+
118
+ const assertBrowserState = (valueArg: unknown): interfaces.IControllerBrowserViewState => {
119
+ const value = assertObject(valueArg, 'browser state');
120
+ assertKeys(
121
+ value,
122
+ ['revision', 'status', 'activeTabId', 'viewportRevision', 'viewport', 'tabs'],
123
+ ['lastError'],
124
+ );
125
+ if (!['stopped', 'starting', 'running', 'stopping'].includes(String(value.status))) {
126
+ throw new interfaces.ControllerBrowserTransportProtocolError('browser state status is invalid.');
127
+ }
128
+ if (value.activeTabId !== null && typeof value.activeTabId !== 'string') {
129
+ throw new interfaces.ControllerBrowserTransportProtocolError('browser state activeTabId is invalid.');
130
+ }
131
+ if (!Array.isArray(value.tabs) || value.tabs.length > 64) {
132
+ throw new interfaces.ControllerBrowserTransportProtocolError('browser state tabs are invalid.');
133
+ }
134
+ const tabs = value.tabs.map((tabArg): interfaces.IControllerBrowserTabState => {
135
+ const tab = assertObject(tabArg, 'browser tab');
136
+ assertKeys(tab, [
137
+ 'id', 'url', 'title', 'active', 'status', 'generation', 'appliedViewportRevision', 'streaming',
138
+ ]);
139
+ if (
140
+ typeof tab.active !== 'boolean'
141
+ || typeof tab.streaming !== 'boolean'
142
+ || !['open', 'crashed'].includes(String(tab.status))
143
+ ) throw new interfaces.ControllerBrowserTransportProtocolError('browser tab state is invalid.');
144
+ return {
145
+ id: assertBoundedString(tab.id, 'browser tab id'),
146
+ url: typeof tab.url === 'string' && textEncoder.encode(tab.url).byteLength <= 16 * 1024
147
+ ? tab.url
148
+ : (() => { throw new interfaces.ControllerBrowserTransportProtocolError('browser tab URL is invalid.'); })(),
149
+ title: typeof tab.title === 'string' && textEncoder.encode(tab.title).byteLength <= 4 * 1024
150
+ ? tab.title
151
+ : (() => { throw new interfaces.ControllerBrowserTransportProtocolError('browser tab title is invalid.'); })(),
152
+ active: tab.active,
153
+ status: tab.status as 'open' | 'crashed',
154
+ generation: assertSafeInteger(tab.generation, 'browser tab generation'),
155
+ appliedViewportRevision: assertSafeInteger(
156
+ tab.appliedViewportRevision,
157
+ 'browser tab viewport revision',
158
+ ),
159
+ streaming: tab.streaming,
160
+ };
161
+ });
162
+ const lastError = value.lastError === undefined
163
+ ? undefined
164
+ : assertBrowserError(value.lastError);
165
+ return {
166
+ revision: assertSafeInteger(value.revision, 'browser state revision', 1),
167
+ status: value.status as interfaces.IControllerBrowserViewState['status'],
168
+ activeTabId: value.activeTabId as string | null,
169
+ viewportRevision: assertSafeInteger(value.viewportRevision, 'browser state viewport revision'),
170
+ viewport: assertBrowserViewport(value.viewport),
171
+ tabs,
172
+ ...(lastError === undefined ? {} : { lastError }),
173
+ };
174
+ };
175
+
176
+ const assertBrowserFrameMetadata = (
177
+ valueArg: unknown,
178
+ ): interfaces.IControllerBrowserFrameMetadata => {
179
+ const value = assertObject(valueArg, 'browser frame');
180
+ assertKeys(value, [
181
+ 'tabId', 'sequence', 'generation', 'viewportRevision', 'viewport', 'format', 'mimeType',
182
+ 'width', 'height', 'metadata', 'dataByteLength',
183
+ ]);
184
+ if (
185
+ (value.format !== 'jpeg' && value.format !== 'png')
186
+ || (value.mimeType !== 'image/jpeg' && value.mimeType !== 'image/png')
187
+ || (value.format === 'jpeg' && value.mimeType !== 'image/jpeg')
188
+ || (value.format === 'png' && value.mimeType !== 'image/png')
189
+ ) throw new interfaces.ControllerBrowserTransportProtocolError('browser frame format is invalid.');
190
+ const metadata = assertObject(value.metadata, 'browser frame metadata');
191
+ assertKeys(metadata, [
192
+ 'offsetTop', 'pageScaleFactor', 'deviceWidth', 'deviceHeight', 'scrollOffsetX', 'scrollOffsetY',
193
+ ], ['timestamp']);
194
+ const dataByteLength = assertSafeInteger(value.dataByteLength, 'frame.dataByteLength', 1);
195
+ if (dataByteLength > interfaces.controllerBrowserTransportMaximumFrameBytes) {
196
+ throw new interfaces.ControllerBrowserTransportProtocolError('browser frame exceeds the byte limit.');
197
+ }
198
+ return {
199
+ tabId: assertBoundedString(value.tabId, 'frame.tabId'),
200
+ sequence: assertSafeInteger(value.sequence, 'frame.sequence', 1),
201
+ generation: assertSafeInteger(value.generation, 'frame.generation'),
202
+ viewportRevision: assertSafeInteger(value.viewportRevision, 'frame.viewportRevision'),
203
+ viewport: assertBrowserViewport(value.viewport),
204
+ format: value.format,
205
+ mimeType: value.mimeType,
206
+ width: assertSafeInteger(value.width, 'frame.width', 1),
207
+ height: assertSafeInteger(value.height, 'frame.height', 1),
208
+ metadata: {
209
+ offsetTop: assertFiniteNumber(metadata.offsetTop, 'frame.metadata.offsetTop'),
210
+ pageScaleFactor: assertFiniteNumber(metadata.pageScaleFactor, 'frame.metadata.pageScaleFactor'),
211
+ deviceWidth: assertFiniteNumber(metadata.deviceWidth, 'frame.metadata.deviceWidth'),
212
+ deviceHeight: assertFiniteNumber(metadata.deviceHeight, 'frame.metadata.deviceHeight'),
213
+ scrollOffsetX: assertFiniteNumber(metadata.scrollOffsetX, 'frame.metadata.scrollOffsetX'),
214
+ scrollOffsetY: assertFiniteNumber(metadata.scrollOffsetY, 'frame.metadata.scrollOffsetY'),
215
+ ...(metadata.timestamp === undefined
216
+ ? {}
217
+ : { timestamp: assertFiniteNumber(metadata.timestamp, 'frame.metadata.timestamp') }),
218
+ },
219
+ dataByteLength,
220
+ };
221
+ };
222
+
223
+ const assertServerPacket = (valueArg: unknown): interfaces.TControllerBrowserServerPacket => {
224
+ const value = assertObject(valueArg, 'browser server packet');
225
+ if (value.protocol !== interfaces.controllerBrowserTransportProtocol) {
226
+ throw new interfaces.ControllerBrowserTransportProtocolError('browser packet protocol is invalid.');
227
+ }
228
+ const streamGeneration = assertSafeInteger(value.streamGeneration, 'streamGeneration', 1);
229
+ if (value.type === 'state') {
230
+ assertKeys(value, ['protocol', 'streamGeneration', 'type', 'state']);
231
+ return {
232
+ protocol: interfaces.controllerBrowserTransportProtocol,
233
+ streamGeneration,
234
+ type: 'state',
235
+ state: assertBrowserState(value.state),
236
+ };
237
+ }
238
+ if (value.type === 'error') {
239
+ assertKeys(value, ['protocol', 'streamGeneration', 'type', 'error']);
240
+ return {
241
+ protocol: interfaces.controllerBrowserTransportProtocol,
242
+ streamGeneration,
243
+ type: 'error',
244
+ error: assertBrowserError(value.error),
245
+ };
246
+ }
247
+ if (value.type === 'frame') {
248
+ assertKeys(value, ['protocol', 'streamGeneration', 'type', 'frame']);
249
+ return {
250
+ protocol: interfaces.controllerBrowserTransportProtocol,
251
+ streamGeneration,
252
+ type: 'frame',
253
+ frame: assertBrowserFrameMetadata(value.frame),
254
+ };
255
+ }
256
+ if (value.type === 'operationResult') {
257
+ assertKeys(value, ['protocol', 'streamGeneration', 'type', 'operationId', 'result']);
258
+ const result = assertObject(value.result, 'browser operation result');
259
+ if (result.status === 'succeeded') {
260
+ assertKeys(result, ['status'], ['accepted', 'state']);
261
+ if (result.accepted !== undefined && typeof result.accepted !== 'boolean') {
262
+ throw new interfaces.ControllerBrowserTransportProtocolError('operation acceptance is invalid.');
263
+ }
264
+ return {
265
+ protocol: interfaces.controllerBrowserTransportProtocol,
266
+ streamGeneration,
267
+ type: 'operationResult',
268
+ operationId: assertSafeInteger(value.operationId, 'operationId', 1),
269
+ result: {
270
+ status: 'succeeded',
271
+ ...(result.accepted === undefined ? {} : { accepted: result.accepted }),
272
+ ...(result.state === undefined ? {} : { state: assertBrowserState(result.state) }),
273
+ },
274
+ };
275
+ }
276
+ if (result.status === 'failed') {
277
+ assertKeys(result, ['status', 'error']);
278
+ const error = assertBrowserError(result.error, false);
279
+ return {
280
+ protocol: interfaces.controllerBrowserTransportProtocol,
281
+ streamGeneration,
282
+ type: 'operationResult',
283
+ operationId: assertSafeInteger(value.operationId, 'operationId', 1),
284
+ result: { status: 'failed', error: { code: error.code, message: error.message } },
285
+ };
286
+ }
287
+ if (result.status === 'cancelled' || result.status === 'superseded') {
288
+ assertKeys(result, ['status']);
289
+ return {
290
+ protocol: interfaces.controllerBrowserTransportProtocol,
291
+ streamGeneration,
292
+ type: 'operationResult',
293
+ operationId: assertSafeInteger(value.operationId, 'operationId', 1),
294
+ result: { status: result.status },
295
+ };
296
+ }
297
+ }
298
+ throw new interfaces.ControllerBrowserTransportProtocolError('browser server packet type is invalid.');
299
+ };
300
+
301
+ export class ControllerBrowserViewTransportClient {
302
+ private lifecycle: 'new' | 'opening' | 'active' | 'failed' | 'stopped' = 'new';
303
+ private nextOperationId = 0;
304
+ private readonly pendingOperations = new Map<number, IPendingBrowserOperation>();
305
+ private startTask?: Promise<void>;
306
+ private receiveTask?: Promise<void>;
307
+ private stopTask?: Promise<void>;
308
+ private failureReported = false;
309
+
310
+ constructor(private readonly options: IControllerBrowserViewTransportClientOptions) {}
311
+
312
+ public get streamGeneration(): number {
313
+ return this.options.streamGeneration;
314
+ }
315
+
316
+ public start(): Promise<void> {
317
+ if (this.lifecycle === 'active') return Promise.resolve();
318
+ if (this.startTask) return this.startTask;
319
+ if (this.lifecycle !== 'new') {
320
+ return Promise.reject(new Error('The browser transport cannot be started.'));
321
+ }
322
+ this.lifecycle = 'opening';
323
+ const receiveTask = this.receiveEvents();
324
+ this.receiveTask = receiveTask;
325
+ void receiveTask.catch((errorArg) => this.fail(
326
+ errorArg,
327
+ !(errorArg instanceof interfaces.ControllerBrowserTransportProtocolError),
328
+ ));
329
+ const startTask = Promise.all([
330
+ this.options.events.opened,
331
+ this.options.operations.opened,
332
+ ]).then(() => {
333
+ if (this.lifecycle !== 'opening') throw new Error('The browser transport stopped while opening.');
334
+ this.lifecycle = 'active';
335
+ }).catch((errorArg) => {
336
+ this.fail(errorArg, true);
337
+ throw errorArg;
338
+ });
339
+ this.startTask = startTask;
340
+ return startTask;
341
+ }
342
+
343
+ public async operate(
344
+ operationArg: interfaces.TControllerBrowserViewOperation,
345
+ signalArg?: AbortSignal,
346
+ ): Promise<interfaces.TControllerBrowserOperationResult> {
347
+ if (this.lifecycle !== 'active') throw new Error('The browser transport is unavailable.');
348
+ signalArg?.throwIfAborted();
349
+ if (
350
+ this.pendingOperations.size
351
+ >= interfaces.controllerBrowserTransportMaximumPendingOperations
352
+ ) throw new Error('The browser operation capacity was reached.');
353
+ const operationId = ++this.nextOperationId;
354
+ const packet = interfaces.encodeControllerBrowserTransportPacket({
355
+ protocol: interfaces.controllerBrowserTransportProtocol,
356
+ streamGeneration: this.options.streamGeneration,
357
+ type: 'operation',
358
+ operationId,
359
+ operation: operationArg,
360
+ });
361
+ const result = new Promise<interfaces.TControllerBrowserOperationResult>((resolve, reject) => {
362
+ const timeout = setTimeout(() => {
363
+ this.cancelPendingOperation(
364
+ operationId,
365
+ new DOMException('The browser operation timed out.', 'TimeoutError'),
366
+ );
367
+ }, browserOperationTimeoutMs);
368
+ const pending: IPendingBrowserOperation = { resolve, reject, timeout };
369
+ if (signalArg) {
370
+ pending.abortSignal = signalArg;
371
+ pending.abortListener = () => this.cancelPendingOperation(
372
+ operationId,
373
+ signalArg.reason ?? new DOMException('The operation was aborted.', 'AbortError'),
374
+ );
375
+ signalArg.addEventListener('abort', pending.abortListener, { once: true });
376
+ }
377
+ this.pendingOperations.set(operationId, pending);
378
+ });
379
+ let sendTask: Promise<void>;
380
+ try {
381
+ sendTask = this.options.operations.send(packet);
382
+ } catch (errorArg) {
383
+ this.rejectPendingOperation(operationId, errorArg);
384
+ this.fail(errorArg, true);
385
+ return result;
386
+ }
387
+ void sendTask.catch((errorArg) => {
388
+ this.rejectPendingOperation(operationId, errorArg);
389
+ this.fail(errorArg, true);
390
+ });
391
+ return result;
392
+ }
393
+
394
+ public stop(reasonArg: unknown = new Error('The browser transport stopped.')): Promise<void> {
395
+ if (this.stopTask) return this.stopTask;
396
+ this.lifecycle = 'stopped';
397
+ this.rejectAllPending(reasonArg);
398
+ let timeout: ReturnType<typeof setTimeout> | undefined;
399
+ const settlement = Promise.allSettled([
400
+ Promise.resolve().then(() => this.options.events.abort(reasonArg)),
401
+ Promise.resolve().then(() => this.options.operations.abort(reasonArg)),
402
+ this.startTask,
403
+ this.receiveTask,
404
+ this.options.events.closed,
405
+ this.options.operations.closed,
406
+ ].filter((candidateArg): candidateArg is Promise<void> => candidateArg !== undefined))
407
+ .then(() => undefined);
408
+ const boundedStop = Promise.race([
409
+ settlement,
410
+ new Promise<void>((resolve) => {
411
+ timeout = setTimeout(resolve, browserTransportStopTimeoutMs);
412
+ }),
413
+ ]).finally(() => {
414
+ if (timeout) clearTimeout(timeout);
415
+ });
416
+ this.stopTask = boundedStop;
417
+ return boundedStop;
418
+ }
419
+
420
+ private async receiveEvents(): Promise<void> {
421
+ while (this.lifecycle !== 'failed' && this.lifecycle !== 'stopped') {
422
+ const chunk = await this.options.events.receive();
423
+ if (chunk === undefined) {
424
+ if (this.isStopped()) return;
425
+ await this.options.events.accept();
426
+ throw new Error('The browser event stream closed unexpectedly.');
427
+ }
428
+ const packet = assertServerPacket(
429
+ interfaces.decodeControllerBrowserTransportPacket(chunk),
430
+ );
431
+ if (packet.streamGeneration !== this.options.streamGeneration) {
432
+ throw new interfaces.ControllerBrowserTransportProtocolError(
433
+ 'The browser packet belongs to a stale stream generation.',
434
+ );
435
+ }
436
+ if (packet.type === 'operationResult') {
437
+ this.settlePendingOperation(packet.operationId, packet.result);
438
+ continue;
439
+ }
440
+ if (packet.type === 'state') {
441
+ this.options.onEvent({ type: 'state', state: packet.state });
442
+ continue;
443
+ }
444
+ if (packet.type === 'error') {
445
+ this.options.onEvent({ type: 'error', error: packet.error });
446
+ if (packet.error.fatal) {
447
+ this.fail(new Error(packet.error.message), false);
448
+ return;
449
+ }
450
+ continue;
451
+ }
452
+ const data = await this.options.events.receive();
453
+ if (data === undefined || data.byteLength !== packet.frame.dataByteLength) {
454
+ throw new interfaces.ControllerBrowserTransportProtocolError(
455
+ 'The browser frame payload length does not match its metadata.',
456
+ );
457
+ }
458
+ const { dataByteLength: _dataByteLength, ...frame } = packet.frame;
459
+ const browserFrame: interfaces.IControllerBrowserFrame = {
460
+ ...frame,
461
+ data: Uint8Array.from(data),
462
+ };
463
+ this.options.onEvent({ type: 'frame', frame: browserFrame });
464
+ }
465
+ }
466
+
467
+ private cancelPendingOperation(operationIdArg: number, reasonArg: unknown): void {
468
+ if (!this.rejectPendingOperation(operationIdArg, reasonArg)) return;
469
+ const packet = interfaces.encodeControllerBrowserTransportPacket({
470
+ protocol: interfaces.controllerBrowserTransportProtocol,
471
+ streamGeneration: this.options.streamGeneration,
472
+ type: 'cancel',
473
+ operationId: operationIdArg,
474
+ });
475
+ void Promise.resolve().then(() => this.options.operations.send(packet))
476
+ .catch((errorArg) => this.fail(errorArg, true));
477
+ }
478
+
479
+ private settlePendingOperation(
480
+ operationIdArg: number,
481
+ resultArg: interfaces.TControllerBrowserOperationResult,
482
+ ): void {
483
+ const pending = this.takePendingOperation(operationIdArg);
484
+ if (!pending) return;
485
+ if (resultArg.status === 'failed') {
486
+ pending.reject(new ControllerBrowserOperationError(resultArg.error.code, resultArg.error.message));
487
+ } else if (resultArg.status === 'cancelled') {
488
+ pending.reject(new DOMException('The browser operation was cancelled.', 'AbortError'));
489
+ } else {
490
+ pending.resolve(resultArg);
491
+ }
492
+ }
493
+
494
+ private rejectPendingOperation(operationIdArg: number, reasonArg: unknown): boolean {
495
+ const pending = this.takePendingOperation(operationIdArg);
496
+ if (!pending) return false;
497
+ pending.reject(reasonArg);
498
+ return true;
499
+ }
500
+
501
+ private takePendingOperation(operationIdArg: number): IPendingBrowserOperation | undefined {
502
+ const pending = this.pendingOperations.get(operationIdArg);
503
+ if (!pending) return undefined;
504
+ this.pendingOperations.delete(operationIdArg);
505
+ clearTimeout(pending.timeout);
506
+ if (pending.abortSignal && pending.abortListener) {
507
+ pending.abortSignal.removeEventListener('abort', pending.abortListener);
508
+ }
509
+ return pending;
510
+ }
511
+
512
+ private rejectAllPending(reasonArg: unknown): void {
513
+ for (const operationId of [...this.pendingOperations.keys()]) {
514
+ this.rejectPendingOperation(operationId, reasonArg);
515
+ }
516
+ }
517
+
518
+ private fail(errorArg: unknown, recoverableArg: boolean): void {
519
+ if (this.lifecycle === 'failed' || this.lifecycle === 'stopped') return;
520
+ this.lifecycle = 'failed';
521
+ this.rejectAllPending(errorArg);
522
+ void this.options.events.abort(errorArg).catch(() => undefined);
523
+ void this.options.operations.abort(errorArg).catch(() => undefined);
524
+ if (this.failureReported) return;
525
+ this.failureReported = true;
526
+ this.options.onFailure(errorArg, recoverableArg);
527
+ }
528
+
529
+ private isStopped(): boolean {
530
+ return this.lifecycle === 'stopped';
531
+ }
532
+ }
@@ -279,18 +279,11 @@ export interface IControllerSocketClientOptions {
279
279
  onTerminalOutput?: (
280
280
  output: interfaces.IReq_ControllerTerminalOutput['request'],
281
281
  ) => void | Promise<void>;
282
- onBrowserViewEvent?: (
283
- event: TControllerBrowserViewEventMessage,
284
- ) => void | Promise<void>;
285
282
  /** Fires when the transport fail-closes the connection (e.g. frame limits). */
286
283
  onTransportClose?: (closeCode: number, reason: string) => void;
284
+ onReconnectExhausted?: (attempts: number, endpoint: string) => void;
287
285
  }
288
286
 
289
- export type TControllerBrowserViewEventMessage =
290
- plugins.typedrequest.TReverseVirtualStreamDirections<
291
- interfaces.IReq_ControllerBrowserViewEvent['request']
292
- >;
293
-
294
287
  export interface IControllerSocketRequestOptions
295
288
  extends plugins.typedrequest.ITypedRequestFireOptions {
296
289
  abortSignal?: AbortSignal;
@@ -502,15 +495,6 @@ export class ControllerSocketClient {
502
495
  },
503
496
  ),
504
497
  );
505
- this.typedRouter.addTypedHandler(
506
- new plugins.typedrequest.TypedHandler<interfaces.IReq_ControllerBrowserViewEvent>(
507
- 'controller.browser.view.event',
508
- async (eventArg) => {
509
- await this.options.onBrowserViewEvent?.(eventArg);
510
- return {};
511
- },
512
- ),
513
- );
514
498
  }
515
499
 
516
500
  public get status(): plugins.typedsocket.TConnectionStatus {
@@ -608,6 +592,11 @@ export class ControllerSocketClient {
608
592
  diagnosticArg.closeCode,
609
593
  typeof diagnosticArg.reason === 'string' ? diagnosticArg.reason : '',
610
594
  );
595
+ } else if (diagnosticArg.kind === 'reconnectExhausted') {
596
+ this.options.onReconnectExhausted?.(
597
+ diagnosticArg.attempts,
598
+ diagnosticArg.endpoint,
599
+ );
611
600
  }
612
601
  });
613
602