@modelprofile.com/browser-runtime 5.2.1 → 5.4.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/changelog.md +17 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.runtime.d.ts +31 -1
- package/dist_ts/classes.runtime.js +330 -67
- package/dist_ts/errors.d.ts +1 -1
- package/dist_ts/errors.js +3 -1
- package/dist_ts/interfaces.d.ts +6 -1
- package/package.json +2 -2
- package/readme.md +28 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.runtime.ts +296 -61
- package/ts/errors.ts +4 -0
- package/ts/interfaces.ts +9 -0
package/ts/classes.runtime.ts
CHANGED
|
@@ -105,6 +105,7 @@ interface INormalizedRuntimeOptions {
|
|
|
105
105
|
frameAcknowledgementTimeoutMs: number;
|
|
106
106
|
screencast: INormalizedScreencastOptions;
|
|
107
107
|
video: Required<plugins.smartpuppeteer.ILiveVideoOptions>;
|
|
108
|
+
devTools: boolean;
|
|
108
109
|
egress: NonNullable<IBrowserRuntimeOptions['egress']>;
|
|
109
110
|
ownership?: IBrowserRuntimeTestingOptions['ownership'];
|
|
110
111
|
beforeLeasePublication?: IBrowserRuntimeTestingOptions['beforeLeasePublication'];
|
|
@@ -144,10 +145,25 @@ interface ILeaseRecord {
|
|
|
144
145
|
videoPeerId: string;
|
|
145
146
|
videoPeerMayExist: boolean;
|
|
146
147
|
frameCaptureUsed: boolean;
|
|
148
|
+
devTools?: IDevToolsRecord;
|
|
147
149
|
heldKeys: Map<string, plugins.smartpuppeteer.ILiveBrowserKeyInput>;
|
|
148
150
|
heldButtons: Map<string, plugins.smartpuppeteer.ILiveBrowserMouseInput>;
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
interface IDevToolsRecord {
|
|
154
|
+
authority: TBrowserRuntimeLeaseAuthority;
|
|
155
|
+
session: ILiveBrowserSessionLike;
|
|
156
|
+
controller: AbortController;
|
|
157
|
+
connection?: plugins.smartpuppeteer.ILiveBrowserDevToolsConnection;
|
|
158
|
+
opening?: Promise<plugins.smartpuppeteer.ILiveBrowserDevToolsConnection>;
|
|
159
|
+
closePromise?: Promise<void>;
|
|
160
|
+
pending: Set<Promise<void>>;
|
|
161
|
+
commands: number;
|
|
162
|
+
interrupts: number;
|
|
163
|
+
closed: boolean;
|
|
164
|
+
removeAbortListener(): void;
|
|
165
|
+
}
|
|
166
|
+
|
|
151
167
|
export interface IBrowserRuntimeLeaseRenewalOptions {
|
|
152
168
|
signal?: AbortSignal;
|
|
153
169
|
/** Lifetime from successful reauthorization; defaults to the runtime capability lifetime. */
|
|
@@ -246,7 +262,9 @@ interface IResourceSlot {
|
|
|
246
262
|
profileDirectory?: string;
|
|
247
263
|
unsubscribeSession?: () => void;
|
|
248
264
|
leases: Map<string, ILeaseRecord>;
|
|
249
|
-
|
|
265
|
+
operations: Set<IOperationRecord>;
|
|
266
|
+
activeQueuedOperations: Set<IQueuedOperationRecord>;
|
|
267
|
+
inputCleanup?: { session: ILiveBrowserSessionLike; incarnationGeneration: number; promise: Promise<void> };
|
|
250
268
|
operationQueue: IQueuedOperationRecord[];
|
|
251
269
|
operationSchedulerRunning: boolean;
|
|
252
270
|
frameSubscriptions: Map<string, IFrameSubscriptionRecord>;
|
|
@@ -318,6 +336,9 @@ export class BrowserRuntime {
|
|
|
318
336
|
if (options.beforeOperation !== undefined && typeof options.beforeOperation !== 'function') {
|
|
319
337
|
throw new BrowserRuntimeError('INVALID_INPUT', 'beforeOperation must be a function');
|
|
320
338
|
}
|
|
339
|
+
if (options.devTools !== undefined && typeof options.devTools !== 'boolean') {
|
|
340
|
+
throw new BrowserRuntimeError('INVALID_INPUT', 'devTools must be a boolean');
|
|
341
|
+
}
|
|
321
342
|
const testingOptions = browserRuntimeTesting in options
|
|
322
343
|
&& options[browserRuntimeTesting] === true
|
|
323
344
|
? options as IBrowserRuntimeTestingOptions
|
|
@@ -570,6 +591,7 @@ export class BrowserRuntime {
|
|
|
570
591
|
),
|
|
571
592
|
screencast,
|
|
572
593
|
video: plugins.smartpuppeteer.normalizeLiveVideoOptions(options.video ?? {}),
|
|
594
|
+
devTools: options.devTools ?? false,
|
|
573
595
|
egress: options.egress ?? {},
|
|
574
596
|
ownership: testingOptions?.ownership,
|
|
575
597
|
beforeLeasePublication: testingOptions?.beforeLeasePublication,
|
|
@@ -723,6 +745,8 @@ export class BrowserRuntime {
|
|
|
723
745
|
frameSubscriptions: new Map(),
|
|
724
746
|
highestFrameSequence: 0,
|
|
725
747
|
producerAcknowledgements: new Set(),
|
|
748
|
+
operations: new Set(),
|
|
749
|
+
activeQueuedOperations: new Set(),
|
|
726
750
|
operationQueue: [],
|
|
727
751
|
operationSchedulerRunning: false,
|
|
728
752
|
lifecycleTail: Promise.resolve(),
|
|
@@ -1374,6 +1398,148 @@ export class BrowserRuntime {
|
|
|
1374
1398
|
});
|
|
1375
1399
|
}
|
|
1376
1400
|
|
|
1401
|
+
/** @internal */
|
|
1402
|
+
public async leaseOpenDevTools(lease: ILeaseRecord, options: plugins.smartpuppeteer.ILiveBrowserDevToolsOptions,
|
|
1403
|
+
operationOptions: IBrowserRuntimeOperationOptions = {}): Promise<plugins.smartpuppeteer.ILiveBrowserDevToolsConnection> {
|
|
1404
|
+
this.requireHuman(lease);
|
|
1405
|
+
if (!this.options.devTools) throw new BrowserRuntimeError('CAPABILITY_INVALID', 'DevTools is not enabled.');
|
|
1406
|
+
if (!options || typeof options.onMessage !== 'function' || typeof options.onClose !== 'function') {
|
|
1407
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1408
|
+
}
|
|
1409
|
+
const tabId = validateBoundedString(options.tabId, 'tabId', 1, 128);
|
|
1410
|
+
let created: IDevToolsRecord | undefined;
|
|
1411
|
+
try {
|
|
1412
|
+
return await this.runOperation(lease, 'openDevTools', 'devtools', operationOptions, async (signal, session) => {
|
|
1413
|
+
if (!session.openDevTools) throw new BrowserRuntimeError('CAPABILITY_INVALID', 'The browser adapter does not support DevTools.');
|
|
1414
|
+
if (lease.devTools) await this.closeLeaseDevTools(lease, lease.devTools);
|
|
1415
|
+
const controller = new AbortController();
|
|
1416
|
+
const record: IDevToolsRecord = {
|
|
1417
|
+
authority: this.createLeaseAuthority(lease), session, controller,
|
|
1418
|
+
pending: new Set(), commands: 0, interrupts: 0, closed: false,
|
|
1419
|
+
removeAbortListener: () => lease.controller.signal.removeEventListener('abort', abort),
|
|
1420
|
+
};
|
|
1421
|
+
const abort = (): void => { this.trackCleanup(this.closeLeaseDevTools(lease, record)); };
|
|
1422
|
+
lease.controller.signal.addEventListener('abort', abort, { once: true });
|
|
1423
|
+
created = record;
|
|
1424
|
+
lease.devTools = record;
|
|
1425
|
+
record.opening = session.openDevTools({ tabId,
|
|
1426
|
+
onMessage: async message => {
|
|
1427
|
+
this.requireDevToolsAuthority(lease, record);
|
|
1428
|
+
await options.onMessage(message);
|
|
1429
|
+
this.requireDevToolsAuthority(lease, record);
|
|
1430
|
+
},
|
|
1431
|
+
onClose: reason => {
|
|
1432
|
+
record.closed = true;
|
|
1433
|
+
controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1434
|
+
record.removeAbortListener();
|
|
1435
|
+
options.onClose(reason);
|
|
1436
|
+
},
|
|
1437
|
+
}, { signal });
|
|
1438
|
+
const connection = await record.opening;
|
|
1439
|
+
record.connection = connection;
|
|
1440
|
+
try {
|
|
1441
|
+
signal.throwIfAborted();
|
|
1442
|
+
this.requireDevToolsAuthority(lease, record);
|
|
1443
|
+
} catch (error) { await this.closeLeaseDevTools(lease, record); throw error; }
|
|
1444
|
+
return {
|
|
1445
|
+
tabId: connection.tabId,
|
|
1446
|
+
browserVersion: connection.browserVersion,
|
|
1447
|
+
send: message => this.sendLeaseDevTools(lease, record, message),
|
|
1448
|
+
close: () => this.closeLeaseDevTools(lease, record),
|
|
1449
|
+
};
|
|
1450
|
+
});
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (created) await this.closeLeaseDevTools(lease, created);
|
|
1453
|
+
throw error;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
private requireDevToolsAuthority(lease: ILeaseRecord, record: IDevToolsRecord): void {
|
|
1458
|
+
this.requireHuman(lease);
|
|
1459
|
+
if (record.closed || record.controller.signal.aborted || lease.devTools !== record
|
|
1460
|
+
|| lease.slot.session !== record.session || !this.isLeaseAuthorityCurrent(lease, record.authority)) {
|
|
1461
|
+
throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
private async sendLeaseDevTools(lease: ILeaseRecord, record: IDevToolsRecord, message: string): Promise<void> {
|
|
1466
|
+
this.requireDevToolsAuthority(lease, record);
|
|
1467
|
+
const limits = plugins.smartpuppeteer.liveBrowserDevToolsLimits;
|
|
1468
|
+
if (typeof message !== 'string' || plugins.Buffer.byteLength(message) > limits.commandBytes) {
|
|
1469
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1470
|
+
}
|
|
1471
|
+
let command: { method?: unknown };
|
|
1472
|
+
try { command = JSON.parse(message); } catch { throw new BrowserRuntimeError('INVALID_INPUT'); }
|
|
1473
|
+
if (!command || typeof command.method !== 'string' || !/^[A-Za-z]+\.[A-Za-z]+$/.test(command.method)) {
|
|
1474
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1475
|
+
}
|
|
1476
|
+
const action = command.method;
|
|
1477
|
+
const interrupt = limits.interruptMethods.includes(action);
|
|
1478
|
+
if (interrupt ? record.interrupts >= limits.interruptCommands : record.commands >= limits.commands) {
|
|
1479
|
+
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
1480
|
+
}
|
|
1481
|
+
if (interrupt) record.interrupts++; else record.commands++;
|
|
1482
|
+
const startedAt = Date.now();
|
|
1483
|
+
const operation: IOperationRecord = {
|
|
1484
|
+
operationId: randomId(), lease, controller: record.controller, action, startedAt,
|
|
1485
|
+
session: record.session, incarnationGeneration: record.authority.incarnationGeneration,
|
|
1486
|
+
promise: Promise.resolve(),
|
|
1487
|
+
};
|
|
1488
|
+
const identity = this.createOperationIdentity(lease, operation, action, 'devtools', startedAt);
|
|
1489
|
+
const task = (async () => {
|
|
1490
|
+
let failed = false;
|
|
1491
|
+
let failure: unknown;
|
|
1492
|
+
try {
|
|
1493
|
+
await this.runBeforeOperation(identity, record.controller.signal);
|
|
1494
|
+
this.requireDevToolsAuthority(lease, record);
|
|
1495
|
+
await record.connection!.send(message);
|
|
1496
|
+
this.requireDevToolsAuthority(lease, record);
|
|
1497
|
+
} catch (error) { failed = true; failure = error; }
|
|
1498
|
+
finally {
|
|
1499
|
+
try {
|
|
1500
|
+
await this.emitAudit(Object.freeze({ ...identity, phase: failed ? 'failed' : 'completed',
|
|
1501
|
+
finishedAt: Date.now(), ...(failed ? { errorCode: this.operationErrorCode(failure) } : {}) }));
|
|
1502
|
+
} finally {
|
|
1503
|
+
if (interrupt) record.interrupts--; else record.commands--;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if (failed) throw failure;
|
|
1507
|
+
})();
|
|
1508
|
+
record.pending.add(task);
|
|
1509
|
+
void task.then(() => record.pending.delete(task), () => record.pending.delete(task));
|
|
1510
|
+
return task;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
private closeLeaseDevTools(lease: ILeaseRecord, record: IDevToolsRecord): Promise<void> {
|
|
1514
|
+
if (record.closePromise) return record.closePromise;
|
|
1515
|
+
record.closed = true;
|
|
1516
|
+
record.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1517
|
+
record.removeAbortListener();
|
|
1518
|
+
const closing = (async () => {
|
|
1519
|
+
try {
|
|
1520
|
+
const detached = await waitBounded((async () => {
|
|
1521
|
+
// Opening may still be completing when lease revocation arrives.
|
|
1522
|
+
await Promise.resolve();
|
|
1523
|
+
const connection = record.connection ?? await record.opening?.catch(() => undefined);
|
|
1524
|
+
await connection?.close();
|
|
1525
|
+
})(), this.options.quiescenceTimeoutMs + 3500);
|
|
1526
|
+
if (!detached.settled) throw new BrowserRuntimeError('TIMEOUT');
|
|
1527
|
+
const pending = await waitBounded(Promise.allSettled([...record.pending]),
|
|
1528
|
+
this.options.quiescenceTimeoutMs + this.options.auditTimeoutMs + this.options.beforeOperationTimeoutMs);
|
|
1529
|
+
if (!pending.settled) throw new BrowserRuntimeError('TIMEOUT');
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
await this.terminateRegisteredResourceInternal(lease.slot, false, {
|
|
1532
|
+
session: record.session, incarnationGeneration: record.authority.incarnationGeneration,
|
|
1533
|
+
});
|
|
1534
|
+
throw error;
|
|
1535
|
+
} finally {
|
|
1536
|
+
if (lease.devTools === record) lease.devTools = undefined;
|
|
1537
|
+
}
|
|
1538
|
+
})();
|
|
1539
|
+
record.closePromise = closing;
|
|
1540
|
+
return closing;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1377
1543
|
/** @internal */
|
|
1378
1544
|
public async leaseAnswerVideoPeer(lease: ILeaseRecord, negotiationId: string,
|
|
1379
1545
|
description: plugins.smartpuppeteer.ILiveVideoDescription, options: IBrowserRuntimeOperationOptions = {}): Promise<void> {
|
|
@@ -1478,6 +1644,24 @@ export class BrowserRuntime {
|
|
|
1478
1644
|
);
|
|
1479
1645
|
}
|
|
1480
1646
|
|
|
1647
|
+
/** @internal */
|
|
1648
|
+
public async leaseRespondToDialog(lease: ILeaseRecord,
|
|
1649
|
+
input: plugins.smartpuppeteer.ILiveBrowserDialogResponse,
|
|
1650
|
+
options: IBrowserRuntimeOperationOptions = {}): Promise<void> {
|
|
1651
|
+
this.requireHuman(lease);
|
|
1652
|
+
const record = validateExactKeys(input, ['tabId', 'dialogId', 'accept', 'promptText'], 'dialog');
|
|
1653
|
+
const tabId = validateBoundedString(record.tabId, 'tabId', 1, 128);
|
|
1654
|
+
const dialogId = validateBoundedString(record.dialogId, 'dialogId', 1, 128);
|
|
1655
|
+
if (typeof record.accept !== 'boolean' || (record.promptText !== undefined
|
|
1656
|
+
&& (typeof record.promptText !== 'string' || record.promptText.length > 32768))) {
|
|
1657
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1658
|
+
}
|
|
1659
|
+
const response = { tabId, dialogId, accept: record.accept,
|
|
1660
|
+
...(record.promptText !== undefined ? { promptText: record.promptText as string } : {}) };
|
|
1661
|
+
return this.runOperation(lease, 'respondToDialog', 'dialog', options,
|
|
1662
|
+
async (signal, session) => session.respondToDialog(response, { signal }));
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1481
1665
|
/** @internal */
|
|
1482
1666
|
public async leaseDispatchRawInput(
|
|
1483
1667
|
lease: ILeaseRecord,
|
|
@@ -1560,7 +1744,7 @@ export class BrowserRuntime {
|
|
|
1560
1744
|
const tab = state.tabs.find((value) => value.id === state.activeTabId);
|
|
1561
1745
|
if (!tab || tab.status !== 'open' || tab.id !== input.tabId
|
|
1562
1746
|
|| tab.generation !== input.generation || state.viewportRevision !== input.viewportRevision
|
|
1563
|
-
) throw new BrowserRuntimeError('
|
|
1747
|
+
) throw new BrowserRuntimeError('STALE_INPUT');
|
|
1564
1748
|
}
|
|
1565
1749
|
|
|
1566
1750
|
private mouseOwnershipKey(input: plugins.smartpuppeteer.ILiveBrowserMouseInput): string {
|
|
@@ -1608,6 +1792,29 @@ export class BrowserRuntime {
|
|
|
1608
1792
|
return modifiers;
|
|
1609
1793
|
}
|
|
1610
1794
|
|
|
1795
|
+
private async releaseStaleParticipantInput(slot: IResourceSlot, session: ILiveBrowserSessionLike): Promise<void> {
|
|
1796
|
+
const incarnationGeneration = slot.incarnationGeneration;
|
|
1797
|
+
if (slot.session !== session) throw new BrowserRuntimeError('ABORTED');
|
|
1798
|
+
if (slot.inputCleanup?.session === session
|
|
1799
|
+
&& slot.inputCleanup.incarnationGeneration === incarnationGeneration) return slot.inputCleanup.promise;
|
|
1800
|
+
const state = session.getState();
|
|
1801
|
+
const tab = state.tabs.find(tab => tab.id === state.activeTabId);
|
|
1802
|
+
if (!tab?.streaming || tab.dialog) return;
|
|
1803
|
+
const affected = [...slot.leases.values()].filter(lease => [...lease.heldKeys.values(), ...lease.heldButtons.values()]
|
|
1804
|
+
.some(input => input.tabId !== tab.id || input.generation !== tab.generation
|
|
1805
|
+
|| input.viewportRevision !== state.viewportRevision));
|
|
1806
|
+
if (!affected.length) return;
|
|
1807
|
+
// The runtime owns native held input, including link navigation that bypasses its command queue.
|
|
1808
|
+
const cleanup = { session, incarnationGeneration, promise: (async () => {
|
|
1809
|
+
for (const lease of affected) {
|
|
1810
|
+
if (slot.session !== session || slot.incarnationGeneration !== incarnationGeneration) return;
|
|
1811
|
+
await this.releaseParticipantInput(lease, session);
|
|
1812
|
+
}
|
|
1813
|
+
})() };
|
|
1814
|
+
slot.inputCleanup = cleanup;
|
|
1815
|
+
try { await cleanup.promise; } finally { if (slot.inputCleanup === cleanup) slot.inputCleanup = undefined; }
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1611
1818
|
private async releaseAllParticipantInput(
|
|
1612
1819
|
slot: IResourceSlot, session: ILiveBrowserSessionLike,
|
|
1613
1820
|
): Promise<void> {
|
|
@@ -1762,10 +1969,9 @@ export class BrowserRuntime {
|
|
|
1762
1969
|
/** @internal */
|
|
1763
1970
|
public cancelLeaseOperation(lease: ILeaseRecord): boolean {
|
|
1764
1971
|
this.requireValidLease(lease);
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
return true;
|
|
1972
|
+
const operations = [...lease.slot.operations].filter(operation => operation.lease === lease);
|
|
1973
|
+
for (const operation of operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1974
|
+
return operations.length > 0;
|
|
1769
1975
|
}
|
|
1770
1976
|
|
|
1771
1977
|
/** @internal */
|
|
@@ -2013,6 +2219,7 @@ export class BrowserRuntime {
|
|
|
2013
2219
|
usePipe: true,
|
|
2014
2220
|
allowEvaluation: true,
|
|
2015
2221
|
video: this.options.video,
|
|
2222
|
+
allowDevTools: this.options.devTools,
|
|
2016
2223
|
screencast: {
|
|
2017
2224
|
enabled: false, maxOutstandingFrames: this.options.maxOutstandingFrames,
|
|
2018
2225
|
quality: this.options.screencast.quality,
|
|
@@ -2228,37 +2435,42 @@ export class BrowserRuntime {
|
|
|
2228
2435
|
private drainOperationQueue(slot: IResourceSlot): void {
|
|
2229
2436
|
if (slot.operationSchedulerRunning) return;
|
|
2230
2437
|
slot.operationSchedulerRunning = true;
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
const
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
queued.
|
|
2251
|
-
}
|
|
2438
|
+
void (async () => {
|
|
2439
|
+
try {
|
|
2440
|
+
while (slot.operationQueue.length > 0) {
|
|
2441
|
+
// Dialog replies must unblock a page operation already waiting on Chrome.
|
|
2442
|
+
const dialogIndex = slot.operationQueue.findIndex(entry => entry.classification === 'dialog');
|
|
2443
|
+
const index = dialogIndex >= 0 ? dialogIndex : 0;
|
|
2444
|
+
const queued = slot.operationQueue[index]!;
|
|
2445
|
+
const active = [...slot.activeQueuedOperations];
|
|
2446
|
+
if (queued.classification === 'dialog') {
|
|
2447
|
+
if (active.some(entry => entry.classification === 'dialog')) break;
|
|
2448
|
+
} else if (active.length > 0 && !(queued.action === 'dispatchWheel'
|
|
2449
|
+
&& active.length < 4 && active.every(entry => entry.action === 'dispatchWheel'))) break;
|
|
2450
|
+
slot.operationQueue.splice(index, 1);
|
|
2451
|
+
if (queued.state === 'settled') continue;
|
|
2452
|
+
queued.state = 'starting';
|
|
2453
|
+
slot.activeQueuedOperations.add(queued);
|
|
2454
|
+
let dispatched!: () => void;
|
|
2455
|
+
const admission = new Promise<void>(resolve => { dispatched = resolve; });
|
|
2456
|
+
void this.executeQueuedOperation(queued, dispatched).then(result => {
|
|
2457
|
+
if (queued.state !== 'settled') { queued.state = 'settled'; queued.resolve(result); }
|
|
2458
|
+
}, error => {
|
|
2459
|
+
if (queued.state !== 'settled') { queued.state = 'settled'; queued.reject(error); }
|
|
2460
|
+
}).finally(() => {
|
|
2461
|
+
dispatched();
|
|
2462
|
+
if (queued.onQueuedAbort) queued.signal.removeEventListener('abort', queued.onQueuedAbort);
|
|
2463
|
+
slot.activeQueuedOperations.delete(queued);
|
|
2464
|
+
this.drainOperationQueue(slot);
|
|
2465
|
+
});
|
|
2466
|
+
// Preserve authorization and native-send order without waiting for wheel ACKs.
|
|
2467
|
+
await admission;
|
|
2252
2468
|
}
|
|
2253
|
-
}
|
|
2254
|
-
})()
|
|
2255
|
-
slot.operationSchedulerRunning = false;
|
|
2256
|
-
if (slot.operationQueue.length > 0) this.drainOperationQueue(slot);
|
|
2257
|
-
});
|
|
2258
|
-
void scheduler.catch(() => undefined);
|
|
2469
|
+
} finally { slot.operationSchedulerRunning = false; }
|
|
2470
|
+
})();
|
|
2259
2471
|
}
|
|
2260
2472
|
|
|
2261
|
-
private async executeQueuedOperation(queued: IQueuedOperationRecord): Promise<unknown> {
|
|
2473
|
+
private async executeQueuedOperation(queued: IQueuedOperationRecord, dispatched: () => void): Promise<unknown> {
|
|
2262
2474
|
const { lease, action, classification } = queued;
|
|
2263
2475
|
const slot = lease.slot;
|
|
2264
2476
|
const release = await slot.mutex.acquire();
|
|
@@ -2278,7 +2490,12 @@ export class BrowserRuntime {
|
|
|
2278
2490
|
queued.signal.throwIfAborted();
|
|
2279
2491
|
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2280
2492
|
this.assertSlotAvailable(slot);
|
|
2281
|
-
|
|
2493
|
+
const active = [...slot.operations];
|
|
2494
|
+
if (classification === 'dialog' ? active.some(operation => operation.action === 'respondToDialog')
|
|
2495
|
+
: active.length > 0 && !(action === 'dispatchWheel' && active.length < 4
|
|
2496
|
+
&& active.every(operation => operation.action === 'dispatchWheel'))) {
|
|
2497
|
+
throw new BrowserRuntimeError('BUSY');
|
|
2498
|
+
}
|
|
2282
2499
|
session = slot.session!;
|
|
2283
2500
|
generation = slot.arbitrationGeneration;
|
|
2284
2501
|
incarnationGeneration = slot.incarnationGeneration;
|
|
@@ -2308,7 +2525,7 @@ export class BrowserRuntime {
|
|
|
2308
2525
|
incarnationGeneration,
|
|
2309
2526
|
promise: new Promise<void>((resolve) => { resolvePreflight = resolve; }),
|
|
2310
2527
|
};
|
|
2311
|
-
slot.operation
|
|
2528
|
+
slot.operations.add(operation);
|
|
2312
2529
|
queued.state = 'active';
|
|
2313
2530
|
if (queued.onQueuedAbort) {
|
|
2314
2531
|
queued.signal.removeEventListener('abort', queued.onQueuedAbort);
|
|
@@ -2344,7 +2561,7 @@ export class BrowserRuntime {
|
|
|
2344
2561
|
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2345
2562
|
this.assertSlotAvailable(slot);
|
|
2346
2563
|
if (
|
|
2347
|
-
slot.operation
|
|
2564
|
+
!slot.operations.has(operation)
|
|
2348
2565
|
|| slot.session !== session
|
|
2349
2566
|
|| slot.arbitrationGeneration !== generation
|
|
2350
2567
|
|| slot.incarnationGeneration !== incarnationGeneration
|
|
@@ -2354,8 +2571,11 @@ export class BrowserRuntime {
|
|
|
2354
2571
|
if (classification === 'navigation' || classification === 'tab') {
|
|
2355
2572
|
await this.releaseAllParticipantInput(slot, session);
|
|
2356
2573
|
}
|
|
2574
|
+
if (classification !== 'dialog') await this.releaseStaleParticipantInput(slot, session);
|
|
2357
2575
|
combinedSignal.throwIfAborted();
|
|
2358
|
-
|
|
2576
|
+
const result = queued.execute(combinedSignal, session);
|
|
2577
|
+
dispatched();
|
|
2578
|
+
return result;
|
|
2359
2579
|
})();
|
|
2360
2580
|
operation.promise = executionPromise.then(() => undefined, () => undefined).finally(() => {
|
|
2361
2581
|
executionSettled = true;
|
|
@@ -2385,10 +2605,11 @@ export class BrowserRuntime {
|
|
|
2385
2605
|
clearTimeout(timeout!);
|
|
2386
2606
|
queued.externalSignal?.removeEventListener('abort', onExternalAbort!);
|
|
2387
2607
|
resolvePreflight();
|
|
2608
|
+
dispatched();
|
|
2388
2609
|
const cleanupSlot = async (): Promise<void> => {
|
|
2389
2610
|
const cleanupRelease = await slot.mutex.acquire();
|
|
2390
2611
|
try {
|
|
2391
|
-
|
|
2612
|
+
slot.operations.delete(operation);
|
|
2392
2613
|
} finally {
|
|
2393
2614
|
cleanupRelease();
|
|
2394
2615
|
}
|
|
@@ -2397,7 +2618,7 @@ export class BrowserRuntime {
|
|
|
2397
2618
|
if (combinedSignal!.aborted) {
|
|
2398
2619
|
await this.enforceOperationQuiescence(operation).catch(() => undefined);
|
|
2399
2620
|
}
|
|
2400
|
-
if (slot.operation
|
|
2621
|
+
if (slot.operations.has(operation)) {
|
|
2401
2622
|
const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
2402
2623
|
if (settled.settled) await cleanupSlot();
|
|
2403
2624
|
}
|
|
@@ -2407,7 +2628,7 @@ export class BrowserRuntime {
|
|
|
2407
2628
|
if (
|
|
2408
2629
|
queued.finalize
|
|
2409
2630
|
&& (!executionStarted || executionSettled)
|
|
2410
|
-
&& slot.operation
|
|
2631
|
+
&& !slot.operations.has(operation)
|
|
2411
2632
|
) {
|
|
2412
2633
|
try {
|
|
2413
2634
|
await queued.finalize();
|
|
@@ -2455,6 +2676,8 @@ export class BrowserRuntime {
|
|
|
2455
2676
|
);
|
|
2456
2677
|
}
|
|
2457
2678
|
if (errorArg instanceof BrowserRuntimeError) return errorArg;
|
|
2679
|
+
if (errorArg instanceof plugins.smartpuppeteer.LiveBrowserInputStaleError) return new BrowserRuntimeError('STALE_INPUT');
|
|
2680
|
+
if (errorArg instanceof plugins.smartpuppeteer.LiveBrowserDialogPendingError) return new BrowserRuntimeError('DIALOG_PENDING');
|
|
2458
2681
|
if (errorArg instanceof Error && errorArg.name === 'TimeoutError') {
|
|
2459
2682
|
return new BrowserRuntimeError('TIMEOUT');
|
|
2460
2683
|
}
|
|
@@ -2727,6 +2950,7 @@ export class BrowserRuntime {
|
|
|
2727
2950
|
generation: tab.generation,
|
|
2728
2951
|
appliedViewportRevision: tab.appliedViewportRevision,
|
|
2729
2952
|
streaming: tab.streaming,
|
|
2953
|
+
...(tab.dialog ? { dialog: { ...tab.dialog } } : {}),
|
|
2730
2954
|
})),
|
|
2731
2955
|
...(state.lastError
|
|
2732
2956
|
? {
|
|
@@ -3094,7 +3318,7 @@ export class BrowserRuntime {
|
|
|
3094
3318
|
lease.released = true;
|
|
3095
3319
|
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3096
3320
|
}
|
|
3097
|
-
slot.operation
|
|
3321
|
+
for (const operation of slot.operations) operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3098
3322
|
for (const subscription of subscribers) {
|
|
3099
3323
|
try { subscription.listener(runtimeEvent); } catch { /* Producer failure owns cleanup. */ }
|
|
3100
3324
|
}
|
|
@@ -3286,7 +3510,7 @@ export class BrowserRuntime {
|
|
|
3286
3510
|
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3287
3511
|
this.notifyFramedLeaseEnded(lease);
|
|
3288
3512
|
}
|
|
3289
|
-
slot.operation
|
|
3513
|
+
for (const operation of slot.operations) operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3290
3514
|
} finally {
|
|
3291
3515
|
release();
|
|
3292
3516
|
}
|
|
@@ -3302,7 +3526,7 @@ export class BrowserRuntime {
|
|
|
3302
3526
|
slot.leases.delete(lease.leaseId);
|
|
3303
3527
|
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
3304
3528
|
}
|
|
3305
|
-
slot.
|
|
3529
|
+
slot.operations.clear();
|
|
3306
3530
|
slot.fencingGeneration = undefined;
|
|
3307
3531
|
}
|
|
3308
3532
|
} finally {
|
|
@@ -3467,17 +3691,18 @@ export class BrowserRuntime {
|
|
|
3467
3691
|
const slot = lease.slot;
|
|
3468
3692
|
if (slot.leases.get(lease.leaseId) !== lease) return;
|
|
3469
3693
|
const release = await slot.mutex.acquire();
|
|
3470
|
-
let
|
|
3694
|
+
let operations: IOperationRecord[];
|
|
3471
3695
|
try {
|
|
3472
3696
|
lease.released = true;
|
|
3473
3697
|
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3474
|
-
|
|
3475
|
-
operation
|
|
3698
|
+
operations = [...slot.operations].filter(operation => operation.lease === lease);
|
|
3699
|
+
for (const operation of operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3476
3700
|
} finally {
|
|
3477
3701
|
release();
|
|
3478
3702
|
}
|
|
3479
3703
|
await this.closeFrameSubscriptionIfLease(lease);
|
|
3480
|
-
if (
|
|
3704
|
+
if (lease.devTools) await this.closeLeaseDevTools(lease, lease.devTools);
|
|
3705
|
+
for (const operation of operations) {
|
|
3481
3706
|
const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
3482
3707
|
if (!settled.settled) {
|
|
3483
3708
|
// An unquiescent operation belongs to the browser process, so its incarnation must end.
|
|
@@ -3525,13 +3750,12 @@ export class BrowserRuntime {
|
|
|
3525
3750
|
}
|
|
3526
3751
|
|
|
3527
3752
|
private async quiesceSlot(slot: IResourceSlot): Promise<void> {
|
|
3528
|
-
const
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
3753
|
+
const operations = [...slot.operations];
|
|
3754
|
+
for (const operation of operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3755
|
+
const settled = await waitBounded(Promise.all(operations.map(operation => operation.promise)), this.options.quiescenceTimeoutMs);
|
|
3532
3756
|
if (settled.settled) return;
|
|
3533
3757
|
await this.terminateSlotSession(slot);
|
|
3534
|
-
|
|
3758
|
+
for (const operation of operations) slot.operations.delete(operation);
|
|
3535
3759
|
}
|
|
3536
3760
|
|
|
3537
3761
|
private async enforceOperationQuiescence(operation: IOperationRecord): Promise<void> {
|
|
@@ -3547,7 +3771,7 @@ export class BrowserRuntime {
|
|
|
3547
3771
|
} catch (error) {
|
|
3548
3772
|
cleanupError = error;
|
|
3549
3773
|
}
|
|
3550
|
-
if (operation.lease.slot.operation
|
|
3774
|
+
if (!operation.lease.slot.operations.has(operation)) {
|
|
3551
3775
|
if (cleanupError) throw cleanupError;
|
|
3552
3776
|
return;
|
|
3553
3777
|
}
|
|
@@ -3557,7 +3781,7 @@ export class BrowserRuntime {
|
|
|
3557
3781
|
return;
|
|
3558
3782
|
}
|
|
3559
3783
|
const slot = operation.lease.slot;
|
|
3560
|
-
if (slot.operation
|
|
3784
|
+
if (slot.operations.has(operation) && slot.session === operation.session
|
|
3561
3785
|
&& slot.incarnationGeneration === operation.incarnationGeneration) {
|
|
3562
3786
|
await this.terminateRegisteredResourceInternal(slot, false, {
|
|
3563
3787
|
session: operation.session, incarnationGeneration: operation.incarnationGeneration,
|
|
@@ -3566,13 +3790,13 @@ export class BrowserRuntime {
|
|
|
3566
3790
|
const release = await slot.mutex.acquire();
|
|
3567
3791
|
try {
|
|
3568
3792
|
if (
|
|
3569
|
-
slot.operation
|
|
3793
|
+
slot.operations.has(operation)
|
|
3570
3794
|
&& (
|
|
3571
3795
|
slot.permanentlyFenced
|
|
3572
3796
|
|| slot.session !== operation.session
|
|
3573
3797
|
|| slot.incarnationGeneration !== operation.incarnationGeneration
|
|
3574
3798
|
)
|
|
3575
|
-
) slot.operation
|
|
3799
|
+
) slot.operations.delete(operation);
|
|
3576
3800
|
} finally {
|
|
3577
3801
|
release();
|
|
3578
3802
|
}
|
|
@@ -3614,6 +3838,7 @@ export class BrowserRuntime {
|
|
|
3614
3838
|
}
|
|
3615
3839
|
slot.unsubscribeSession?.();
|
|
3616
3840
|
slot.unsubscribeSession = undefined;
|
|
3841
|
+
if (slot.inputCleanup?.session === session) slot.inputCleanup = undefined;
|
|
3617
3842
|
slot.session = undefined;
|
|
3618
3843
|
slot.highestFrameSequence = 0;
|
|
3619
3844
|
slot.producerAcknowledgements.clear();
|
|
@@ -3698,7 +3923,7 @@ export class BrowserRuntime {
|
|
|
3698
3923
|
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3699
3924
|
if (!stopping) this.notifyFramedLeaseEnded(lease);
|
|
3700
3925
|
}
|
|
3701
|
-
slot.operation
|
|
3926
|
+
for (const operation of slot.operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3702
3927
|
} finally {
|
|
3703
3928
|
release();
|
|
3704
3929
|
}
|
|
@@ -3714,7 +3939,7 @@ export class BrowserRuntime {
|
|
|
3714
3939
|
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
3715
3940
|
slot.leases.delete(lease.leaseId);
|
|
3716
3941
|
}
|
|
3717
|
-
slot.
|
|
3942
|
+
slot.operations.clear();
|
|
3718
3943
|
slot.fencingGeneration = undefined;
|
|
3719
3944
|
} finally {
|
|
3720
3945
|
finalRelease();
|
|
@@ -3815,8 +4040,8 @@ export class BrowserRuntime {
|
|
|
3815
4040
|
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3816
4041
|
}
|
|
3817
4042
|
}
|
|
3818
|
-
|
|
3819
|
-
|
|
4043
|
+
for (const operation of slot.operations) {
|
|
4044
|
+
if (operation.lease.role === 'agent') operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3820
4045
|
}
|
|
3821
4046
|
} finally {
|
|
3822
4047
|
release();
|
|
@@ -4342,6 +4567,11 @@ export class BrowserRuntimeLease {
|
|
|
4342
4567
|
return this.runtime.leaseSetViewport(this.record, viewport, operationOptions);
|
|
4343
4568
|
}
|
|
4344
4569
|
|
|
4570
|
+
public respondToDialog(input: plugins.smartpuppeteer.ILiveBrowserDialogResponse,
|
|
4571
|
+
options: IBrowserRuntimeOperationOptions = {}): Promise<void> {
|
|
4572
|
+
return this.runtime.leaseRespondToDialog(this.record, input, options);
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4345
4575
|
public dispatchMouse(
|
|
4346
4576
|
input: plugins.smartpuppeteer.ILiveBrowserMouseInput,
|
|
4347
4577
|
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
@@ -4381,6 +4611,11 @@ export class BrowserRuntimeLease {
|
|
|
4381
4611
|
return this.runtime.leaseOpenVideoPeer(this.record, options);
|
|
4382
4612
|
}
|
|
4383
4613
|
|
|
4614
|
+
public openDevTools(options: plugins.smartpuppeteer.ILiveBrowserDevToolsOptions,
|
|
4615
|
+
operationOptions?: IBrowserRuntimeOperationOptions): Promise<plugins.smartpuppeteer.ILiveBrowserDevToolsConnection> {
|
|
4616
|
+
return this.runtime.leaseOpenDevTools(this.record, options, operationOptions);
|
|
4617
|
+
}
|
|
4618
|
+
|
|
4384
4619
|
public answerVideoPeer(negotiationId: string, description: plugins.smartpuppeteer.ILiveVideoDescription,
|
|
4385
4620
|
options?: IBrowserRuntimeOperationOptions): Promise<void> {
|
|
4386
4621
|
return this.runtime.leaseAnswerVideoPeer(this.record, negotiationId, description, options);
|
package/ts/errors.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type TBrowserRuntimeErrorCode =
|
|
|
11
11
|
| 'FENCED'
|
|
12
12
|
| 'FRAME_STREAM_FAILED'
|
|
13
13
|
| 'FRAME_TOO_LARGE'
|
|
14
|
+
| 'DIALOG_PENDING'
|
|
15
|
+
| 'STALE_INPUT'
|
|
14
16
|
| 'INVALID_INPUT'
|
|
15
17
|
| 'LOCKED'
|
|
16
18
|
| 'NOT_RUNNING'
|
|
@@ -33,6 +35,8 @@ const publicMessages: Record<TBrowserRuntimeErrorCode, string> = {
|
|
|
33
35
|
FENCED: 'The browser resource is permanently fenced.',
|
|
34
36
|
FRAME_STREAM_FAILED: 'The browser frame stream failed.',
|
|
35
37
|
FRAME_TOO_LARGE: 'The browser frame exceeded its limit.',
|
|
38
|
+
DIALOG_PENDING: 'Respond to the website dialog before sending input.',
|
|
39
|
+
STALE_INPUT: 'The input belongs to a previous document or dialog.',
|
|
36
40
|
INVALID_INPUT: 'The request is invalid.',
|
|
37
41
|
LOCKED: 'The browser runtime is already locked.',
|
|
38
42
|
NOT_RUNNING: 'The browser runtime is not running.',
|