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