@push.rocks/smartbrowser 3.0.0 → 4.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/classes.livebrowsercanvasrenderer.d.ts +25 -5
- package/dist_ts_web/classes.livebrowsercanvasrenderer.js +385 -136
- package/dist_ts_web/interfaces.livebrowsercanvas.d.ts +18 -6
- package/package.json +3 -3
- package/readme.hints.md +11 -6
- package/readme.md +19 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.livebrowsercanvasrenderer.ts +483 -149
- package/ts_web/interfaces.livebrowsercanvas.ts +34 -5
|
@@ -11,17 +11,26 @@ import type {
|
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
13
|
ILiveBrowserCanvasError,
|
|
14
|
+
ILiveBrowserCanvasOperationOptions,
|
|
14
15
|
ILiveBrowserCanvasRendererOptions,
|
|
15
|
-
TLiveBrowserCanvasErrorCode,
|
|
16
16
|
} from './interfaces.livebrowsercanvas.js';
|
|
17
17
|
|
|
18
18
|
const maxInputQueueLength = 128;
|
|
19
19
|
const maxPendingAcknowledgements = 16;
|
|
20
20
|
const maxInputReleaseAttempts = 2;
|
|
21
|
+
const maxFrameDimension = 12288;
|
|
22
|
+
const maxFramePixelArea = 8294400;
|
|
23
|
+
const maxFrameByteLength = (maxFramePixelArea * 4) + 1048576;
|
|
24
|
+
const maxViewportWidth = 4096;
|
|
25
|
+
const maxViewportHeight = 4096;
|
|
26
|
+
const maxDeviceScaleFactor = 3;
|
|
21
27
|
const coordinateEdgeInset = 0.001;
|
|
22
28
|
const defaultOperationTimeoutMs = 10000;
|
|
23
29
|
|
|
24
30
|
class LiveBrowserOperationTimeoutError extends Error {}
|
|
31
|
+
class LiveBrowserRunAbortedError extends Error {}
|
|
32
|
+
class LiveBrowserFrameProtocolError extends Error {}
|
|
33
|
+
class LiveBrowserFrameIntegrityError extends Error {}
|
|
25
34
|
|
|
26
35
|
interface IFrameWork {
|
|
27
36
|
frame: ILiveBrowserFrame;
|
|
@@ -39,7 +48,7 @@ type TInputCommandKind = 'key' | 'mouse' | 'mouseMove' | 'text' | 'wheel';
|
|
|
39
48
|
interface IInputCommand {
|
|
40
49
|
kind: TInputCommandKind;
|
|
41
50
|
runEpoch: number;
|
|
42
|
-
execute: () => Promise<void>;
|
|
51
|
+
execute: (optionsArg: ILiveBrowserCanvasOperationOptions) => Promise<void>;
|
|
43
52
|
onAttempt?: () => void;
|
|
44
53
|
onSuccess?: () => void;
|
|
45
54
|
resolve: () => void;
|
|
@@ -107,9 +116,11 @@ export class LiveBrowserCanvasRenderer {
|
|
|
107
116
|
private lifecycleTail: Promise<void> = Promise.resolve();
|
|
108
117
|
private runEpoch = 0;
|
|
109
118
|
private running = false;
|
|
119
|
+
private suspended = false;
|
|
110
120
|
private stopping = false;
|
|
111
|
-
private
|
|
112
|
-
private
|
|
121
|
+
private interruptionRequested = false;
|
|
122
|
+
private runController?: AbortController;
|
|
123
|
+
private operationControllers = new Set<AbortController>();
|
|
113
124
|
private acceptingFrames = false;
|
|
114
125
|
private state?: ILiveBrowserState;
|
|
115
126
|
private unsubscribe?: () => void;
|
|
@@ -124,6 +135,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
124
135
|
private queuedFrame?: IFrameWork;
|
|
125
136
|
private frameProcessingPromise?: Promise<void>;
|
|
126
137
|
private acknowledgementPromises = new Set<Promise<void>>();
|
|
138
|
+
private rawDecodeSettlementPromises = new Set<Promise<void>>();
|
|
127
139
|
|
|
128
140
|
private inputBlocked = true;
|
|
129
141
|
private inputCommands: IInputCommand[] = [];
|
|
@@ -174,46 +186,66 @@ export class LiveBrowserCanvasRenderer {
|
|
|
174
186
|
return this.running;
|
|
175
187
|
}
|
|
176
188
|
|
|
189
|
+
public get isSuspended(): boolean {
|
|
190
|
+
return this.suspended;
|
|
191
|
+
}
|
|
192
|
+
|
|
177
193
|
public start(): Promise<void> {
|
|
178
194
|
return this.enqueueLifecycle(async () => {
|
|
179
|
-
if (this.terminallyStopped) {
|
|
180
|
-
throw new Error(
|
|
181
|
-
'Live browser renderer cannot restart after a terminal operation failure; create a new renderer and client',
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
195
|
if (this.running) {
|
|
185
196
|
return;
|
|
186
197
|
}
|
|
198
|
+
if (this.suspended) {
|
|
199
|
+
throw new Error('Live browser renderer is suspended; call resume() to start a fresh run');
|
|
200
|
+
}
|
|
201
|
+
await this.startRun(false);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
187
204
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
this.
|
|
193
|
-
this.
|
|
194
|
-
|
|
205
|
+
public stop(): Promise<void> {
|
|
206
|
+
this.interruptRun(this.runEpoch);
|
|
207
|
+
return this.enqueueLifecycle(async () => {
|
|
208
|
+
const runEpoch = this.runEpoch;
|
|
209
|
+
this.interruptRun(runEpoch);
|
|
210
|
+
if (this.stopping) {
|
|
211
|
+
await this.finishRun(runEpoch, false);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!this.suspended) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.suspended = false;
|
|
218
|
+
this.restoreCanvasAndFocus();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
195
221
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
throw error;
|
|
222
|
+
/**
|
|
223
|
+
* Aborts the current transport generation without replaying input recovery.
|
|
224
|
+
*/
|
|
225
|
+
public suspend(): Promise<void> {
|
|
226
|
+
this.interruptRun(this.runEpoch);
|
|
227
|
+
return this.enqueueLifecycle(async () => {
|
|
228
|
+
const runEpoch = this.runEpoch;
|
|
229
|
+
this.interruptRun(runEpoch);
|
|
230
|
+
if (!this.stopping) {
|
|
231
|
+
return;
|
|
207
232
|
}
|
|
233
|
+
await this.finishRun(runEpoch, true);
|
|
208
234
|
});
|
|
209
235
|
}
|
|
210
236
|
|
|
211
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Starts a new transport generation that requires new state and a new frame.
|
|
239
|
+
*/
|
|
240
|
+
public resume(): Promise<void> {
|
|
212
241
|
return this.enqueueLifecycle(async () => {
|
|
213
|
-
if (
|
|
242
|
+
if (this.running) {
|
|
214
243
|
return;
|
|
215
244
|
}
|
|
216
|
-
|
|
245
|
+
if (!this.suspended) {
|
|
246
|
+
throw new Error('Live browser renderer is not suspended');
|
|
247
|
+
}
|
|
248
|
+
await this.startRun(true);
|
|
217
249
|
});
|
|
218
250
|
}
|
|
219
251
|
|
|
@@ -226,11 +258,11 @@ export class LiveBrowserCanvasRenderer {
|
|
|
226
258
|
await this.enqueueInputCommand({
|
|
227
259
|
kind: 'text',
|
|
228
260
|
runEpoch: this.runEpoch,
|
|
229
|
-
execute: async () => {
|
|
261
|
+
execute: async (optionsArg) => {
|
|
230
262
|
await this.client.insertText({
|
|
231
263
|
...inputIdentity,
|
|
232
264
|
text: textArg,
|
|
233
|
-
});
|
|
265
|
+
}, optionsArg);
|
|
234
266
|
},
|
|
235
267
|
});
|
|
236
268
|
}
|
|
@@ -240,7 +272,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
240
272
|
* remote CSS viewport. Intrinsic canvas dimensions are never used here.
|
|
241
273
|
*/
|
|
242
274
|
public syncViewport(): void {
|
|
243
|
-
if (!this.running || this.stopping || this.
|
|
275
|
+
if (!this.running || this.stopping || this.interruptionRequested || !this.resizeTarget) {
|
|
244
276
|
return;
|
|
245
277
|
}
|
|
246
278
|
const width = Math.floor(this.resizeTarget.clientWidth);
|
|
@@ -254,12 +286,76 @@ export class LiveBrowserCanvasRenderer {
|
|
|
254
286
|
return operation;
|
|
255
287
|
}
|
|
256
288
|
|
|
257
|
-
private async
|
|
289
|
+
private async startRun(resumingArg: boolean): Promise<void> {
|
|
290
|
+
await Promise.all([...this.rawDecodeSettlementPromises]);
|
|
291
|
+
const runEpoch = ++this.runEpoch;
|
|
292
|
+
this.runController = new AbortController();
|
|
293
|
+
this.running = true;
|
|
294
|
+
this.suspended = false;
|
|
295
|
+
this.stopping = false;
|
|
296
|
+
this.interruptionRequested = false;
|
|
297
|
+
this.acceptingFrames = true;
|
|
298
|
+
this.inputBlocked = true;
|
|
299
|
+
this.state = undefined;
|
|
300
|
+
this.displayedFrame = undefined;
|
|
301
|
+
this.highestFrameSequence = -1;
|
|
302
|
+
this.clearCanvas();
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
this.makeFocusTargetFocusable();
|
|
306
|
+
this.installDomListeners(runEpoch);
|
|
307
|
+
this.unsubscribe = this.client.onEvent((eventArg) => {
|
|
308
|
+
this.handleEvent(eventArg, runEpoch);
|
|
309
|
+
});
|
|
310
|
+
this.applyState(this.client.getState(), runEpoch);
|
|
311
|
+
this.installResizeObserver(runEpoch);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
this.interruptRun(runEpoch);
|
|
314
|
+
await this.finishRun(runEpoch, resumingArg);
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private interruptRun(runEpochArg: number): void {
|
|
320
|
+
if (
|
|
321
|
+
runEpochArg !== this.runEpoch
|
|
322
|
+
|| (!this.running && !this.stopping)
|
|
323
|
+
) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.running = false;
|
|
258
327
|
this.stopping = true;
|
|
328
|
+
this.interruptionRequested = true;
|
|
259
329
|
this.acceptingFrames = false;
|
|
260
330
|
this.inputBlocked = true;
|
|
261
331
|
this.pendingViewport = undefined;
|
|
332
|
+
this.queuedFrame = undefined;
|
|
333
|
+
this.displayedFrame = undefined;
|
|
334
|
+
this.resizeFence = undefined;
|
|
262
335
|
this.releasePointerCaptures();
|
|
336
|
+
this.clearCanvas();
|
|
337
|
+
|
|
338
|
+
const interruptionError = new LiveBrowserRunAbortedError(
|
|
339
|
+
`Live browser renderer run ${runEpochArg} was interrupted`,
|
|
340
|
+
);
|
|
341
|
+
this.runController?.abort(interruptionError);
|
|
342
|
+
for (const controller of this.operationControllers) {
|
|
343
|
+
controller.abort(interruptionError);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const abandonedCommands = this.inputCommands.splice(0);
|
|
347
|
+
for (const command of abandonedCommands) {
|
|
348
|
+
command.reject(interruptionError);
|
|
349
|
+
}
|
|
350
|
+
this.pressedKeys.clear();
|
|
351
|
+
this.pressedMouseButtons.clear();
|
|
352
|
+
this.inputOverflowRecoveryPending = false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private async finishRun(runEpochArg: number, suspendArg: boolean): Promise<void> {
|
|
356
|
+
if (runEpochArg !== this.runEpoch) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
263
359
|
|
|
264
360
|
try {
|
|
265
361
|
this.resizeObserver?.disconnect();
|
|
@@ -280,30 +376,38 @@ export class LiveBrowserCanvasRenderer {
|
|
|
280
376
|
}
|
|
281
377
|
}
|
|
282
378
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
this.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
await this.inputResetTail;
|
|
291
|
-
await this.waitForInputIdle();
|
|
292
|
-
await this.releasePressedInput(runEpochArg);
|
|
293
|
-
|
|
294
|
-
this.running = false;
|
|
295
|
-
await this.frameProcessingPromise;
|
|
296
|
-
await Promise.all([...this.acknowledgementPromises]);
|
|
379
|
+
await Promise.allSettled([
|
|
380
|
+
this.viewportProcessingPromise,
|
|
381
|
+
this.inputResetTail,
|
|
382
|
+
this.waitForInputIdle(),
|
|
383
|
+
this.frameProcessingPromise,
|
|
384
|
+
...this.acknowledgementPromises,
|
|
385
|
+
]);
|
|
297
386
|
} finally {
|
|
387
|
+
for (const controller of this.operationControllers) {
|
|
388
|
+
controller.abort(new LiveBrowserRunAbortedError(
|
|
389
|
+
`Live browser renderer run ${runEpochArg} finished`,
|
|
390
|
+
));
|
|
391
|
+
}
|
|
392
|
+
this.operationControllers.clear();
|
|
393
|
+
this.runController = undefined;
|
|
298
394
|
this.running = false;
|
|
395
|
+
this.suspended = suspendArg;
|
|
299
396
|
this.acceptingFrames = false;
|
|
300
397
|
this.state = undefined;
|
|
301
398
|
this.displayedFrame = undefined;
|
|
399
|
+
this.highestFrameSequence = -1;
|
|
400
|
+
this.queuedFrame = undefined;
|
|
401
|
+
this.frameProcessingPromise = undefined;
|
|
402
|
+
this.acknowledgementPromises.clear();
|
|
302
403
|
this.resizeFence = undefined;
|
|
303
404
|
this.pendingViewport = undefined;
|
|
304
405
|
this.inFlightViewport = undefined;
|
|
305
406
|
this.viewportProcessing = false;
|
|
407
|
+
this.viewportProcessingPromise = undefined;
|
|
306
408
|
this.inputResetPromise = undefined;
|
|
409
|
+
this.inputResetTail = Promise.resolve();
|
|
410
|
+
this.inputResetCount = 0;
|
|
307
411
|
this.inputBlocked = true;
|
|
308
412
|
this.pressedKeys.clear();
|
|
309
413
|
this.pressedMouseButtons.clear();
|
|
@@ -317,14 +421,18 @@ export class LiveBrowserCanvasRenderer {
|
|
|
317
421
|
for (const resolver of inputIdleResolvers) {
|
|
318
422
|
resolver();
|
|
319
423
|
}
|
|
320
|
-
this.
|
|
321
|
-
this.
|
|
322
|
-
this.restoreFocusTarget();
|
|
323
|
-
this.stopRequested = false;
|
|
424
|
+
this.restoreCanvasAndFocus();
|
|
425
|
+
this.interruptionRequested = false;
|
|
324
426
|
this.stopping = false;
|
|
325
427
|
}
|
|
326
428
|
}
|
|
327
429
|
|
|
430
|
+
private restoreCanvasAndFocus(): void {
|
|
431
|
+
this.canvas.width = this.initialCanvasWidth;
|
|
432
|
+
this.canvas.height = this.initialCanvasHeight;
|
|
433
|
+
this.restoreFocusTarget();
|
|
434
|
+
}
|
|
435
|
+
|
|
328
436
|
private installDomListeners(runEpochArg: number): void {
|
|
329
437
|
const listenerController = new AbortController();
|
|
330
438
|
const signal = listenerController.signal;
|
|
@@ -450,18 +558,34 @@ export class LiveBrowserCanvasRenderer {
|
|
|
450
558
|
}
|
|
451
559
|
|
|
452
560
|
private queueFrame(frameArg: ILiveBrowserFrame, runEpochArg: number): void {
|
|
561
|
+
if (!this.running || !this.acceptingFrames || runEpochArg !== this.runEpoch) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
this.validateFrame(frameArg);
|
|
567
|
+
if (frameArg.sequence <= this.highestFrameSequence) {
|
|
568
|
+
throw new LiveBrowserFrameProtocolError(
|
|
569
|
+
`frame.sequence ${frameArg.sequence} must be strictly greater than current run high-water ${this.highestFrameSequence}`,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
} catch (error) {
|
|
573
|
+
this.requestSuspend(runEpochArg);
|
|
574
|
+
this.reportError({
|
|
575
|
+
code: 'frame_render_failed',
|
|
576
|
+
message: `Rejected invalid live browser frame: ${getErrorMessage(error)}`,
|
|
577
|
+
cause: error,
|
|
578
|
+
frame: frameArg,
|
|
579
|
+
}, runEpochArg);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
this.highestFrameSequence = frameArg.sequence;
|
|
453
584
|
const frameWork: IFrameWork = {
|
|
454
585
|
frame: frameArg,
|
|
455
586
|
runEpoch: runEpochArg,
|
|
456
587
|
acknowledgementAttempted: false,
|
|
457
588
|
};
|
|
458
|
-
|
|
459
|
-
if (!this.running || !this.acceptingFrames || runEpochArg !== this.runEpoch) {
|
|
460
|
-
void this.acknowledgeFrame(frameWork);
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
this.highestFrameSequence = Math.max(this.highestFrameSequence, frameArg.sequence);
|
|
465
589
|
if (this.queuedFrame) {
|
|
466
590
|
const supersededFrame = this.queuedFrame;
|
|
467
591
|
this.queuedFrame = undefined;
|
|
@@ -498,6 +622,8 @@ export class LiveBrowserCanvasRenderer {
|
|
|
498
622
|
private async processFrame(frameWorkArg: IFrameWork): Promise<void> {
|
|
499
623
|
const { frame, runEpoch } = frameWorkArg;
|
|
500
624
|
let imageBitmap: ImageBitmap | undefined;
|
|
625
|
+
let terminalFailure = false;
|
|
626
|
+
let terminalError: unknown;
|
|
501
627
|
try {
|
|
502
628
|
if (!this.canRenderFrame(frame, runEpoch)) {
|
|
503
629
|
return;
|
|
@@ -507,16 +633,16 @@ export class LiveBrowserCanvasRenderer {
|
|
|
507
633
|
frameBytes.set(frame.data);
|
|
508
634
|
imageBitmap = await this.decodeFrame(new Blob([frameBytes.buffer], {
|
|
509
635
|
type: frame.mimeType,
|
|
510
|
-
}));
|
|
636
|
+
}), runEpoch);
|
|
511
637
|
|
|
512
|
-
if (!this.canRenderFrame(frame, runEpoch) || frame.sequence !== this.highestFrameSequence) {
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
638
|
if (imageBitmap.width !== frame.width || imageBitmap.height !== frame.height) {
|
|
516
|
-
throw new
|
|
639
|
+
throw new LiveBrowserFrameIntegrityError(
|
|
517
640
|
`decoded frame dimensions ${imageBitmap.width}x${imageBitmap.height} do not match ${frame.width}x${frame.height}`,
|
|
518
641
|
);
|
|
519
642
|
}
|
|
643
|
+
if (!this.canRenderFrame(frame, runEpoch) || frame.sequence !== this.highestFrameSequence) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
520
646
|
|
|
521
647
|
this.canvas.width = frame.width;
|
|
522
648
|
this.canvas.height = frame.height;
|
|
@@ -541,18 +667,23 @@ export class LiveBrowserCanvasRenderer {
|
|
|
541
667
|
this.updateInputAvailability();
|
|
542
668
|
this.callFrameRendered(frame, runEpoch);
|
|
543
669
|
} catch (error) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
message: `Could not render live browser frame: ${getErrorMessage(error)}`,
|
|
547
|
-
cause: error,
|
|
548
|
-
frame,
|
|
549
|
-
}, runEpoch);
|
|
550
|
-
if (error instanceof LiveBrowserOperationTimeoutError) {
|
|
551
|
-
this.requestStop(runEpoch);
|
|
670
|
+
if (error instanceof LiveBrowserRunAbortedError) {
|
|
671
|
+
return;
|
|
552
672
|
}
|
|
673
|
+
terminalFailure = true;
|
|
674
|
+
terminalError = error;
|
|
553
675
|
} finally {
|
|
554
676
|
imageBitmap?.close();
|
|
555
677
|
await this.acknowledgeFrame(frameWorkArg);
|
|
678
|
+
if (terminalFailure) {
|
|
679
|
+
this.requestSuspend(runEpoch);
|
|
680
|
+
this.reportError({
|
|
681
|
+
code: 'frame_render_failed',
|
|
682
|
+
message: `Could not render live browser frame: ${getErrorMessage(terminalError)}`,
|
|
683
|
+
cause: terminalError,
|
|
684
|
+
frame,
|
|
685
|
+
}, runEpoch);
|
|
686
|
+
}
|
|
556
687
|
}
|
|
557
688
|
}
|
|
558
689
|
|
|
@@ -592,28 +723,39 @@ export class LiveBrowserCanvasRenderer {
|
|
|
592
723
|
}
|
|
593
724
|
frameWorkArg.acknowledgementAttempted = true;
|
|
594
725
|
|
|
726
|
+
if (!this.isRunActive(frameWorkArg.runEpoch)) {
|
|
727
|
+
return Promise.resolve();
|
|
728
|
+
}
|
|
729
|
+
|
|
595
730
|
if (this.acknowledgementPromises.size >= maxPendingAcknowledgements) {
|
|
731
|
+
this.requestSuspend(frameWorkArg.runEpoch);
|
|
596
732
|
this.reportError({
|
|
597
733
|
code: 'frame_acknowledgement_failed',
|
|
598
734
|
message: `Live browser frame acknowledgement capacity of ${maxPendingAcknowledgements} was reached`,
|
|
599
735
|
}, frameWorkArg.runEpoch);
|
|
600
|
-
this.requestStop(frameWorkArg.runEpoch);
|
|
601
736
|
return Promise.resolve();
|
|
602
737
|
}
|
|
603
738
|
|
|
604
739
|
const acknowledgement = this.runClientOperation(
|
|
605
|
-
() => this.client.acknowledgeFrame(
|
|
740
|
+
(optionsArg) => this.client.acknowledgeFrame(
|
|
741
|
+
createFrameAcknowledgementRequest(frameWorkArg.frame),
|
|
742
|
+
optionsArg,
|
|
743
|
+
),
|
|
606
744
|
'frame acknowledgement',
|
|
745
|
+
frameWorkArg.runEpoch,
|
|
607
746
|
)
|
|
608
747
|
.then(() => undefined)
|
|
609
748
|
.catch((error) => {
|
|
749
|
+
if (error instanceof LiveBrowserRunAbortedError) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
this.requestSuspend(frameWorkArg.runEpoch);
|
|
610
753
|
this.reportError({
|
|
611
754
|
code: 'frame_acknowledgement_failed',
|
|
612
755
|
message: `Could not acknowledge live browser frame: ${getErrorMessage(error)}`,
|
|
613
756
|
cause: error,
|
|
614
757
|
frame: frameWorkArg.frame,
|
|
615
758
|
}, frameWorkArg.runEpoch);
|
|
616
|
-
this.requestStop(frameWorkArg.runEpoch);
|
|
617
759
|
});
|
|
618
760
|
let trackedAcknowledgement: Promise<void>;
|
|
619
761
|
trackedAcknowledgement = acknowledgement.finally(() => {
|
|
@@ -659,7 +801,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
659
801
|
void this.enqueueInputCommand({
|
|
660
802
|
kind: 'mouse',
|
|
661
803
|
runEpoch: runEpochArg,
|
|
662
|
-
execute: async () => this.client.dispatchMouse(input),
|
|
804
|
+
execute: async (optionsArg) => this.client.dispatchMouse(input, optionsArg),
|
|
663
805
|
onAttempt: () => this.pressedMouseButtons.set(button, input),
|
|
664
806
|
}).catch(() => undefined);
|
|
665
807
|
}
|
|
@@ -690,7 +832,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
690
832
|
void this.enqueueInputCommand({
|
|
691
833
|
kind: 'mouseMove',
|
|
692
834
|
runEpoch: runEpochArg,
|
|
693
|
-
execute: async () => this.client.dispatchMouse(input),
|
|
835
|
+
execute: async (optionsArg) => this.client.dispatchMouse(input, optionsArg),
|
|
694
836
|
}).catch(() => undefined);
|
|
695
837
|
}
|
|
696
838
|
|
|
@@ -723,7 +865,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
723
865
|
void this.enqueueInputCommand({
|
|
724
866
|
kind: 'mouse',
|
|
725
867
|
runEpoch: runEpochArg,
|
|
726
|
-
execute: async () => this.client.dispatchMouse(input),
|
|
868
|
+
execute: async (optionsArg) => this.client.dispatchMouse(input, optionsArg),
|
|
727
869
|
onSuccess: () => this.pressedMouseButtons.delete(button),
|
|
728
870
|
}).catch(() => undefined);
|
|
729
871
|
this.releasePointerCapture(eventArg.pointerId);
|
|
@@ -748,13 +890,13 @@ export class LiveBrowserCanvasRenderer {
|
|
|
748
890
|
void this.enqueueInputCommand({
|
|
749
891
|
kind: 'wheel',
|
|
750
892
|
runEpoch: runEpochArg,
|
|
751
|
-
execute: async () => this.client.dispatchWheel({
|
|
893
|
+
execute: async (optionsArg) => this.client.dispatchWheel({
|
|
752
894
|
...createInputIdentity(displayedFrame),
|
|
753
895
|
...coordinates,
|
|
754
896
|
deltaX: clampDelta(eventArg.deltaX * multiplier),
|
|
755
897
|
deltaY: clampDelta(eventArg.deltaY * multiplier),
|
|
756
898
|
modifiers: createModifiers(eventArg),
|
|
757
|
-
}),
|
|
899
|
+
}, optionsArg),
|
|
758
900
|
}).catch(() => undefined);
|
|
759
901
|
}
|
|
760
902
|
|
|
@@ -795,7 +937,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
795
937
|
void this.enqueueInputCommand({
|
|
796
938
|
kind: 'key',
|
|
797
939
|
runEpoch: runEpochArg,
|
|
798
|
-
execute: async () => this.client.dispatchKey(input),
|
|
940
|
+
execute: async (optionsArg) => this.client.dispatchKey(input, optionsArg),
|
|
799
941
|
onAttempt: typeArg === 'down'
|
|
800
942
|
? () => this.pressedKeys.set(keyIdentity, input)
|
|
801
943
|
: undefined,
|
|
@@ -916,34 +1058,44 @@ export class LiveBrowserCanvasRenderer {
|
|
|
916
1058
|
try {
|
|
917
1059
|
while (this.inputCommands.length > 0) {
|
|
918
1060
|
const command = this.inputCommands.shift()!;
|
|
919
|
-
if (command.runEpoch
|
|
920
|
-
command.
|
|
1061
|
+
if (!this.isRunActive(command.runEpoch)) {
|
|
1062
|
+
command.reject(new LiveBrowserRunAbortedError(
|
|
1063
|
+
`Live browser input belongs to inactive run ${command.runEpoch}`,
|
|
1064
|
+
));
|
|
921
1065
|
continue;
|
|
922
1066
|
}
|
|
923
1067
|
try {
|
|
924
1068
|
command.onAttempt?.();
|
|
925
|
-
await this.runClientOperation(command.execute, 'input dispatch');
|
|
1069
|
+
await this.runClientOperation(command.execute, 'input dispatch', command.runEpoch);
|
|
1070
|
+
if (!this.isRunActive(command.runEpoch)) {
|
|
1071
|
+
throw new LiveBrowserRunAbortedError(
|
|
1072
|
+
`Live browser input belongs to inactive run ${command.runEpoch}`,
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
926
1075
|
command.onSuccess?.();
|
|
927
1076
|
command.resolve();
|
|
928
1077
|
} catch (error) {
|
|
1078
|
+
command.reject(error);
|
|
1079
|
+
if (error instanceof LiveBrowserRunAbortedError) {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (error instanceof LiveBrowserOperationTimeoutError) {
|
|
1083
|
+
this.requestSuspend(command.runEpoch);
|
|
1084
|
+
}
|
|
929
1085
|
this.reportError({
|
|
930
1086
|
code: 'input_dispatch_failed',
|
|
931
1087
|
message: `Could not dispatch live browser input: ${getErrorMessage(error)}`,
|
|
932
1088
|
cause: error,
|
|
933
1089
|
}, command.runEpoch);
|
|
934
|
-
command.reject(error);
|
|
935
1090
|
const abandonedCommands = this.inputCommands.splice(0);
|
|
936
1091
|
for (const abandonedCommand of abandonedCommands) {
|
|
937
1092
|
abandonedCommand.reject(
|
|
938
1093
|
new Error('Live browser input was abandoned after a dispatch failure'),
|
|
939
1094
|
);
|
|
940
1095
|
}
|
|
941
|
-
if (!this.stopping) {
|
|
1096
|
+
if (!(error instanceof LiveBrowserOperationTimeoutError) && !this.stopping) {
|
|
942
1097
|
void this.scheduleInputReset(false, command.runEpoch);
|
|
943
1098
|
}
|
|
944
|
-
if (error instanceof LiveBrowserOperationTimeoutError) {
|
|
945
|
-
this.requestStop(command.runEpoch);
|
|
946
|
-
}
|
|
947
1099
|
}
|
|
948
1100
|
}
|
|
949
1101
|
} finally {
|
|
@@ -1017,13 +1169,13 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1017
1169
|
private async releasePressedInput(runEpochArg: number): Promise<void> {
|
|
1018
1170
|
for (const [keyIdentity, pressedKey] of [...this.pressedKeys.entries()]) {
|
|
1019
1171
|
const released = await this.tryInputRelease(
|
|
1020
|
-
() => this.client.dispatchKey({
|
|
1172
|
+
(optionsArg) => this.client.dispatchKey({
|
|
1021
1173
|
...pressedKey,
|
|
1022
1174
|
type: 'up',
|
|
1023
1175
|
text: undefined,
|
|
1024
1176
|
autoRepeat: false,
|
|
1025
1177
|
modifiers: {},
|
|
1026
|
-
}),
|
|
1178
|
+
}, optionsArg),
|
|
1027
1179
|
runEpochArg,
|
|
1028
1180
|
);
|
|
1029
1181
|
if (released && this.pressedKeys.get(keyIdentity) === pressedKey) {
|
|
@@ -1032,13 +1184,13 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1032
1184
|
}
|
|
1033
1185
|
for (const [button, pressedButton] of [...this.pressedMouseButtons.entries()]) {
|
|
1034
1186
|
const released = await this.tryInputRelease(
|
|
1035
|
-
() => this.client.dispatchMouse({
|
|
1187
|
+
(optionsArg) => this.client.dispatchMouse({
|
|
1036
1188
|
...pressedButton,
|
|
1037
1189
|
type: 'up',
|
|
1038
1190
|
buttons: 0,
|
|
1039
1191
|
clickCount: 1,
|
|
1040
1192
|
modifiers: {},
|
|
1041
|
-
}),
|
|
1193
|
+
}, optionsArg),
|
|
1042
1194
|
runEpochArg,
|
|
1043
1195
|
);
|
|
1044
1196
|
if (released && this.pressedMouseButtons.get(button) === pressedButton) {
|
|
@@ -1048,7 +1200,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1048
1200
|
}
|
|
1049
1201
|
|
|
1050
1202
|
private async tryInputRelease(
|
|
1051
|
-
operationArg: () => Promise<void>,
|
|
1203
|
+
operationArg: (optionsArg: ILiveBrowserCanvasOperationOptions) => Promise<void>,
|
|
1052
1204
|
runEpochArg: number,
|
|
1053
1205
|
): Promise<boolean> {
|
|
1054
1206
|
let lastError: unknown;
|
|
@@ -1057,14 +1209,18 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1057
1209
|
await this.runClientOperation(
|
|
1058
1210
|
operationArg,
|
|
1059
1211
|
'input release',
|
|
1212
|
+
runEpochArg,
|
|
1060
1213
|
);
|
|
1061
1214
|
return true;
|
|
1062
1215
|
} catch (error) {
|
|
1216
|
+
if (error instanceof LiveBrowserRunAbortedError) {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1063
1219
|
lastError = error;
|
|
1064
1220
|
}
|
|
1065
1221
|
}
|
|
1222
|
+
this.requestSuspend(runEpochArg);
|
|
1066
1223
|
this.reportInputReleaseError(lastError, runEpochArg);
|
|
1067
|
-
this.requestStop(runEpochArg);
|
|
1068
1224
|
return false;
|
|
1069
1225
|
}
|
|
1070
1226
|
|
|
@@ -1080,7 +1236,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1080
1236
|
if (
|
|
1081
1237
|
!this.running
|
|
1082
1238
|
|| this.stopping
|
|
1083
|
-
|| this.
|
|
1239
|
+
|| this.interruptionRequested
|
|
1084
1240
|
|| runEpochArg !== this.runEpoch
|
|
1085
1241
|
|| widthArg <= 0
|
|
1086
1242
|
|| heightArg <= 0
|
|
@@ -1175,8 +1331,9 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1175
1331
|
const previousMinimumRevision = this.resizeFence?.minimumViewportRevision ?? currentRevision;
|
|
1176
1332
|
try {
|
|
1177
1333
|
await this.runClientOperation(
|
|
1178
|
-
() => this.client.setViewport(viewport),
|
|
1334
|
+
(optionsArg) => this.client.setViewport(viewport, optionsArg),
|
|
1179
1335
|
'viewport update',
|
|
1336
|
+
runEpochArg,
|
|
1180
1337
|
);
|
|
1181
1338
|
if (!this.running || this.stopping || runEpochArg !== this.runEpoch) {
|
|
1182
1339
|
return;
|
|
@@ -1186,16 +1343,16 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1186
1343
|
minimumViewportRevision: Math.max(currentRevision, previousMinimumRevision) + 1,
|
|
1187
1344
|
};
|
|
1188
1345
|
} catch (error) {
|
|
1189
|
-
if (runEpochArg === this.runEpoch) {
|
|
1346
|
+
if (runEpochArg === this.runEpoch && !(error instanceof LiveBrowserRunAbortedError)) {
|
|
1190
1347
|
this.resizeFence = undefined;
|
|
1348
|
+
if (error instanceof LiveBrowserOperationTimeoutError) {
|
|
1349
|
+
this.requestSuspend(runEpochArg);
|
|
1350
|
+
}
|
|
1191
1351
|
this.reportError({
|
|
1192
1352
|
code: 'viewport_update_failed',
|
|
1193
1353
|
message: `Could not update live browser viewport: ${getErrorMessage(error)}`,
|
|
1194
1354
|
cause: error,
|
|
1195
1355
|
}, runEpochArg);
|
|
1196
|
-
if (error instanceof LiveBrowserOperationTimeoutError) {
|
|
1197
|
-
this.requestStop(runEpochArg);
|
|
1198
|
-
}
|
|
1199
1356
|
}
|
|
1200
1357
|
} finally {
|
|
1201
1358
|
if (this.inFlightViewport && viewportEquals(this.inFlightViewport, viewport)) {
|
|
@@ -1216,8 +1373,6 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1216
1373
|
&& !this.viewportProcessing
|
|
1217
1374
|
&& !this.resizeFence
|
|
1218
1375
|
&& this.inputResetCount === 0
|
|
1219
|
-
&& this.pressedKeys.size === 0
|
|
1220
|
-
&& this.pressedMouseButtons.size === 0
|
|
1221
1376
|
&& this.isDisplayedFrameCurrent()
|
|
1222
1377
|
);
|
|
1223
1378
|
}
|
|
@@ -1276,22 +1431,12 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1276
1431
|
}
|
|
1277
1432
|
}
|
|
1278
1433
|
|
|
1279
|
-
private
|
|
1280
|
-
if (
|
|
1281
|
-
runEpochArg !== this.runEpoch
|
|
1282
|
-
|| (!this.running && !this.stopping)
|
|
1283
|
-
) {
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
this.terminallyStopped = true;
|
|
1287
|
-
this.acceptingFrames = false;
|
|
1288
|
-
this.inputBlocked = true;
|
|
1289
|
-
this.pendingViewport = undefined;
|
|
1290
|
-
if (this.stopping || this.stopRequested) {
|
|
1434
|
+
private requestSuspend(runEpochArg: number): void {
|
|
1435
|
+
if (!this.isRunActive(runEpochArg) || this.interruptionRequested) {
|
|
1291
1436
|
return;
|
|
1292
1437
|
}
|
|
1293
|
-
this.
|
|
1294
|
-
void this.
|
|
1438
|
+
this.interruptRun(runEpochArg);
|
|
1439
|
+
void this.suspend();
|
|
1295
1440
|
}
|
|
1296
1441
|
|
|
1297
1442
|
private validateTimeout(timeoutArg: number, nameArg: string): number {
|
|
@@ -1302,68 +1447,257 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1302
1447
|
}
|
|
1303
1448
|
|
|
1304
1449
|
private runClientOperation<T>(
|
|
1305
|
-
operationArg: () => Promise<T>,
|
|
1450
|
+
operationArg: (optionsArg: ILiveBrowserCanvasOperationOptions) => Promise<T>,
|
|
1306
1451
|
operationNameArg: string,
|
|
1452
|
+
runEpochArg: number,
|
|
1307
1453
|
): Promise<T> {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
return this.withTimeout(operation, this.operationTimeoutMs, operationNameArg);
|
|
1454
|
+
return this.runAbortableOperation(
|
|
1455
|
+
(signalArg) => operationArg({ signal: signalArg }),
|
|
1456
|
+
this.operationTimeoutMs,
|
|
1457
|
+
operationNameArg,
|
|
1458
|
+
runEpochArg,
|
|
1459
|
+
);
|
|
1315
1460
|
}
|
|
1316
1461
|
|
|
1317
|
-
private
|
|
1318
|
-
operationArg: Promise<T>,
|
|
1462
|
+
private runAbortableOperation<T>(
|
|
1463
|
+
operationArg: (signalArg: AbortSignal) => Promise<T>,
|
|
1319
1464
|
timeoutMsArg: number,
|
|
1320
1465
|
operationNameArg: string,
|
|
1466
|
+
runEpochArg: number,
|
|
1321
1467
|
): Promise<T> {
|
|
1322
|
-
|
|
1468
|
+
const runController = this.runController;
|
|
1469
|
+
if (
|
|
1470
|
+
!runController
|
|
1471
|
+
|| !this.isRunActive(runEpochArg)
|
|
1472
|
+
|| runController.signal.aborted
|
|
1473
|
+
) {
|
|
1474
|
+
return Promise.reject(new LiveBrowserRunAbortedError(
|
|
1475
|
+
`Cannot start ${operationNameArg} for inactive run ${runEpochArg}`,
|
|
1476
|
+
));
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
const operationController = new AbortController();
|
|
1480
|
+
this.operationControllers.add(operationController);
|
|
1481
|
+
const abortFromRun = () => {
|
|
1482
|
+
const reason = runController.signal.reason instanceof Error
|
|
1483
|
+
? runController.signal.reason
|
|
1484
|
+
: new LiveBrowserRunAbortedError(
|
|
1485
|
+
`Live browser renderer run ${runEpochArg} was interrupted`,
|
|
1486
|
+
);
|
|
1487
|
+
operationController.abort(reason);
|
|
1488
|
+
};
|
|
1489
|
+
runController.signal.addEventListener('abort', abortFromRun, { once: true });
|
|
1490
|
+
const timeout = globalThis.setTimeout(() => {
|
|
1491
|
+
operationController.abort(new LiveBrowserOperationTimeoutError(
|
|
1492
|
+
`${operationNameArg} timed out after ${timeoutMsArg}ms`,
|
|
1493
|
+
));
|
|
1494
|
+
}, timeoutMsArg);
|
|
1495
|
+
|
|
1496
|
+
let abortOperation = () => undefined;
|
|
1497
|
+
const operation = new Promise<T>((resolve, reject) => {
|
|
1323
1498
|
let settled = false;
|
|
1324
|
-
const
|
|
1499
|
+
const settle = (callbackArg: () => void) => {
|
|
1325
1500
|
if (!settled) {
|
|
1326
1501
|
settled = true;
|
|
1327
|
-
|
|
1328
|
-
`${operationNameArg} timed out after ${timeoutMsArg}ms`,
|
|
1329
|
-
));
|
|
1502
|
+
callbackArg();
|
|
1330
1503
|
}
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1504
|
+
};
|
|
1505
|
+
abortOperation = () => {
|
|
1506
|
+
const reason = operationController.signal.reason instanceof Error
|
|
1507
|
+
? operationController.signal.reason
|
|
1508
|
+
: new LiveBrowserRunAbortedError(`${operationNameArg} was aborted`);
|
|
1509
|
+
settle(() => reject(reason));
|
|
1510
|
+
};
|
|
1511
|
+
operationController.signal.addEventListener('abort', abortOperation, { once: true });
|
|
1512
|
+
|
|
1513
|
+
let operationResult: Promise<T>;
|
|
1514
|
+
try {
|
|
1515
|
+
operationResult = operationArg(operationController.signal);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
settle(() => reject(error));
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
operationResult.then(
|
|
1333
1521
|
(valueArg) => {
|
|
1334
|
-
|
|
1335
|
-
settled = true;
|
|
1336
|
-
globalThis.clearTimeout(timeout);
|
|
1337
|
-
resolve(valueArg);
|
|
1338
|
-
}
|
|
1522
|
+
settle(() => resolve(valueArg));
|
|
1339
1523
|
},
|
|
1340
1524
|
(errorArg) => {
|
|
1341
|
-
|
|
1342
|
-
settled = true;
|
|
1343
|
-
globalThis.clearTimeout(timeout);
|
|
1344
|
-
reject(errorArg);
|
|
1345
|
-
}
|
|
1525
|
+
settle(() => reject(errorArg));
|
|
1346
1526
|
},
|
|
1347
1527
|
);
|
|
1528
|
+
}).finally(() => {
|
|
1529
|
+
globalThis.clearTimeout(timeout);
|
|
1530
|
+
runController.signal.removeEventListener('abort', abortFromRun);
|
|
1531
|
+
operationController.signal.removeEventListener('abort', abortOperation);
|
|
1532
|
+
this.operationControllers.delete(operationController);
|
|
1348
1533
|
});
|
|
1534
|
+
return operation;
|
|
1349
1535
|
}
|
|
1350
1536
|
|
|
1351
|
-
private async decodeFrame(blobArg: Blob): Promise<ImageBitmap> {
|
|
1537
|
+
private async decodeFrame(blobArg: Blob, runEpochArg: number): Promise<ImageBitmap> {
|
|
1352
1538
|
let decodeOperation: Promise<ImageBitmap>;
|
|
1353
1539
|
try {
|
|
1354
1540
|
decodeOperation = createImageBitmap(blobArg);
|
|
1355
1541
|
} catch (error) {
|
|
1356
1542
|
throw error;
|
|
1357
1543
|
}
|
|
1544
|
+
let rawDecodeSettlementPromise: Promise<void>;
|
|
1545
|
+
rawDecodeSettlementPromise = decodeOperation.then(
|
|
1546
|
+
() => undefined,
|
|
1547
|
+
() => undefined,
|
|
1548
|
+
).finally(() => {
|
|
1549
|
+
this.rawDecodeSettlementPromises.delete(rawDecodeSettlementPromise);
|
|
1550
|
+
});
|
|
1551
|
+
this.rawDecodeSettlementPromises.add(rawDecodeSettlementPromise);
|
|
1358
1552
|
try {
|
|
1359
|
-
return await this.
|
|
1360
|
-
decodeOperation,
|
|
1553
|
+
return await this.runAbortableOperation(
|
|
1554
|
+
async () => decodeOperation,
|
|
1361
1555
|
this.frameDecodeTimeoutMs,
|
|
1362
1556
|
'frame decode',
|
|
1557
|
+
runEpochArg,
|
|
1363
1558
|
);
|
|
1364
1559
|
} catch (error) {
|
|
1365
|
-
|
|
1366
|
-
|
|
1560
|
+
if (
|
|
1561
|
+
error instanceof LiveBrowserOperationTimeoutError
|
|
1562
|
+
|| error instanceof LiveBrowserRunAbortedError
|
|
1563
|
+
) {
|
|
1564
|
+
void decodeOperation.then((lateBitmapArg) => lateBitmapArg.close(), () => undefined);
|
|
1565
|
+
throw error;
|
|
1566
|
+
}
|
|
1567
|
+
throw new LiveBrowserFrameIntegrityError(
|
|
1568
|
+
`frame bytes could not be decoded: ${getErrorMessage(error)}`,
|
|
1569
|
+
{ cause: error },
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
private isRunActive(runEpochArg: number): boolean {
|
|
1575
|
+
return Boolean(
|
|
1576
|
+
this.running
|
|
1577
|
+
&& !this.stopping
|
|
1578
|
+
&& runEpochArg === this.runEpoch
|
|
1579
|
+
&& this.runController
|
|
1580
|
+
&& !this.runController.signal.aborted,
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
private validateFrame(frameArg: ILiveBrowserFrame): void {
|
|
1585
|
+
if (!frameArg || typeof frameArg !== 'object') {
|
|
1586
|
+
throw new LiveBrowserFrameProtocolError('frame must be an object');
|
|
1587
|
+
}
|
|
1588
|
+
if (
|
|
1589
|
+
typeof frameArg.tabId !== 'string'
|
|
1590
|
+
|| frameArg.tabId.length === 0
|
|
1591
|
+
|| frameArg.tabId.length > 4096
|
|
1592
|
+
) {
|
|
1593
|
+
throw new LiveBrowserFrameProtocolError('frame.tabId must be a non-empty bounded string');
|
|
1594
|
+
}
|
|
1595
|
+
this.validateProtocolInteger(frameArg.sequence, 'frame.sequence', 1);
|
|
1596
|
+
this.validateProtocolInteger(frameArg.generation, 'frame.generation', 1);
|
|
1597
|
+
this.validateProtocolInteger(frameArg.viewportRevision, 'frame.viewportRevision', 1);
|
|
1598
|
+
this.validateViewport(frameArg.viewport, 'frame.viewport');
|
|
1599
|
+
if (frameArg.format !== 'jpeg' && frameArg.format !== 'png') {
|
|
1600
|
+
throw new LiveBrowserFrameProtocolError('frame.format must be jpeg or png');
|
|
1601
|
+
}
|
|
1602
|
+
const expectedMimeType = frameArg.format === 'jpeg' ? 'image/jpeg' : 'image/png';
|
|
1603
|
+
if (frameArg.mimeType !== expectedMimeType) {
|
|
1604
|
+
throw new LiveBrowserFrameProtocolError(
|
|
1605
|
+
`frame.mimeType must be ${expectedMimeType} for ${frameArg.format}`,
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
this.validateProtocolInteger(frameArg.width, 'frame.width', 1, maxFrameDimension);
|
|
1609
|
+
this.validateProtocolInteger(frameArg.height, 'frame.height', 1, maxFrameDimension);
|
|
1610
|
+
if (frameArg.width * frameArg.height > maxFramePixelArea) {
|
|
1611
|
+
throw new LiveBrowserFrameProtocolError(
|
|
1612
|
+
`frame pixel area must not exceed ${maxFramePixelArea}`,
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
if (!(frameArg.data instanceof Uint8Array)) {
|
|
1616
|
+
throw new LiveBrowserFrameProtocolError('frame.data must be a Uint8Array');
|
|
1617
|
+
}
|
|
1618
|
+
if (frameArg.data.byteLength === 0 || frameArg.data.byteLength > maxFrameByteLength) {
|
|
1619
|
+
throw new LiveBrowserFrameProtocolError(
|
|
1620
|
+
`frame.data byte length must be between 1 and ${maxFrameByteLength}`,
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const metadata = frameArg.metadata;
|
|
1625
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
1626
|
+
throw new LiveBrowserFrameProtocolError('frame.metadata must be an object');
|
|
1627
|
+
}
|
|
1628
|
+
this.validateProtocolNumber(metadata.offsetTop, 'frame.metadata.offsetTop');
|
|
1629
|
+
this.validateProtocolNumber(
|
|
1630
|
+
metadata.pageScaleFactor,
|
|
1631
|
+
'frame.metadata.pageScaleFactor',
|
|
1632
|
+
true,
|
|
1633
|
+
);
|
|
1634
|
+
this.validateProtocolNumber(metadata.deviceWidth, 'frame.metadata.deviceWidth', true);
|
|
1635
|
+
this.validateProtocolNumber(metadata.deviceHeight, 'frame.metadata.deviceHeight', true);
|
|
1636
|
+
if (
|
|
1637
|
+
metadata.deviceWidth > maxFrameDimension
|
|
1638
|
+
|| metadata.deviceHeight > maxFrameDimension
|
|
1639
|
+
|| metadata.deviceWidth * metadata.deviceHeight > maxFramePixelArea
|
|
1640
|
+
) {
|
|
1641
|
+
throw new LiveBrowserFrameProtocolError(
|
|
1642
|
+
`frame metadata dimensions must fit within ${maxFramePixelArea} pixels`,
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
this.validateProtocolNumber(metadata.scrollOffsetX, 'frame.metadata.scrollOffsetX');
|
|
1646
|
+
this.validateProtocolNumber(metadata.scrollOffsetY, 'frame.metadata.scrollOffsetY');
|
|
1647
|
+
if (metadata.timestamp !== undefined) {
|
|
1648
|
+
this.validateProtocolNumber(metadata.timestamp, 'frame.metadata.timestamp');
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
private validateViewport(viewportArg: ILiveBrowserViewport, nameArg: string): void {
|
|
1653
|
+
if (!viewportArg || typeof viewportArg !== 'object') {
|
|
1654
|
+
throw new LiveBrowserFrameProtocolError(`${nameArg} must be an object`);
|
|
1655
|
+
}
|
|
1656
|
+
this.validateProtocolInteger(viewportArg.width, `${nameArg}.width`, 1, maxViewportWidth);
|
|
1657
|
+
this.validateProtocolInteger(viewportArg.height, `${nameArg}.height`, 1, maxViewportHeight);
|
|
1658
|
+
this.validateProtocolNumber(viewportArg.deviceScaleFactor, `${nameArg}.deviceScaleFactor`, true);
|
|
1659
|
+
if (viewportArg.deviceScaleFactor > maxDeviceScaleFactor) {
|
|
1660
|
+
throw new LiveBrowserFrameProtocolError(
|
|
1661
|
+
`${nameArg}.deviceScaleFactor must not exceed ${maxDeviceScaleFactor}`,
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
const physicalArea = viewportArg.width
|
|
1665
|
+
* viewportArg.height
|
|
1666
|
+
* viewportArg.deviceScaleFactor
|
|
1667
|
+
* viewportArg.deviceScaleFactor;
|
|
1668
|
+
if (physicalArea > maxFramePixelArea) {
|
|
1669
|
+
throw new LiveBrowserFrameProtocolError(
|
|
1670
|
+
`${nameArg} physical pixel area must not exceed ${maxFramePixelArea}`,
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
private validateProtocolInteger(
|
|
1676
|
+
valueArg: number,
|
|
1677
|
+
nameArg: string,
|
|
1678
|
+
minimumArg: number,
|
|
1679
|
+
maximumArg = Number.MAX_SAFE_INTEGER,
|
|
1680
|
+
): void {
|
|
1681
|
+
if (
|
|
1682
|
+
!Number.isSafeInteger(valueArg)
|
|
1683
|
+
|| valueArg < minimumArg
|
|
1684
|
+
|| valueArg > maximumArg
|
|
1685
|
+
) {
|
|
1686
|
+
throw new LiveBrowserFrameProtocolError(
|
|
1687
|
+
`${nameArg} must be an integer between ${minimumArg} and ${maximumArg}`,
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
private validateProtocolNumber(
|
|
1693
|
+
valueArg: number,
|
|
1694
|
+
nameArg: string,
|
|
1695
|
+
positiveArg = false,
|
|
1696
|
+
): void {
|
|
1697
|
+
if (!Number.isFinite(valueArg) || (positiveArg && valueArg <= 0)) {
|
|
1698
|
+
throw new LiveBrowserFrameProtocolError(
|
|
1699
|
+
`${nameArg} must be a ${positiveArg ? 'positive ' : ''}finite number`,
|
|
1700
|
+
);
|
|
1367
1701
|
}
|
|
1368
1702
|
}
|
|
1369
1703
|
|