@modelprofile.com/browser-runtime 4.4.0 → 5.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/changelog.md +24 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.runtime.d.ts +32 -11
- package/dist_ts/classes.runtime.js +498 -570
- package/dist_ts/classes.runtimeownership.d.ts +3 -1
- package/dist_ts/classes.runtimeownership.js +27 -7
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/interfaces.d.ts +8 -3
- package/dist_ts/mcp.js +5 -3
- package/package.json +2 -2
- package/readme.hints.md +13 -10
- package/readme.md +12 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.runtime.ts +534 -638
- package/ts/classes.runtimeownership.ts +23 -6
- package/ts/index.ts +1 -0
- package/ts/interfaces.ts +9 -3
- package/ts/mcp.ts +5 -2
package/ts/classes.runtime.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
IBrowserRuntimeOptions,
|
|
29
29
|
IBrowserResourceRegistration,
|
|
30
30
|
IBrowserRuntimeState,
|
|
31
|
+
IBrowserRuntimeViewportResult,
|
|
31
32
|
IBrowserScreenshotResult,
|
|
32
33
|
ICreateBrowserResourceRequest,
|
|
33
34
|
TQualifiedBrowserSessionId,
|
|
@@ -116,6 +117,7 @@ type TCapabilityRecord = TBrowserCapabilityDescriptor & {
|
|
|
116
117
|
expiryTimer: ReturnType<typeof setTimeout>;
|
|
117
118
|
disconnect?: (signal: AbortSignal) => Promise<void> | void;
|
|
118
119
|
lease?: ILeaseRecord;
|
|
120
|
+
acquiring?: boolean;
|
|
119
121
|
revocationPromise?: Promise<void>;
|
|
120
122
|
revocationInvokeDisconnect?: boolean;
|
|
121
123
|
revocationComplete?: boolean;
|
|
@@ -135,8 +137,9 @@ interface ILeaseRecord {
|
|
|
135
137
|
authorityGeneration: number;
|
|
136
138
|
released: boolean;
|
|
137
139
|
releasePromise?: Promise<void>;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
preferredViewport?: plugins.smartpuppeteer.ILiveBrowserViewport;
|
|
141
|
+
heldKeys: Map<string, plugins.smartpuppeteer.ILiveBrowserKeyInput>;
|
|
142
|
+
heldButtons: Map<string, plugins.smartpuppeteer.ILiveBrowserMouseInput>;
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
export interface IBrowserRuntimeLeaseRenewalOptions {
|
|
@@ -171,6 +174,8 @@ interface IQueuedOperationRecord {
|
|
|
171
174
|
reject(error: unknown): void;
|
|
172
175
|
state: 'queued' | 'starting' | 'active' | 'settled';
|
|
173
176
|
onQueuedAbort?: () => void;
|
|
177
|
+
/** Resource-owned cleanup, admitted only for this exact released participant. */
|
|
178
|
+
participantCleanup?: boolean;
|
|
174
179
|
}
|
|
175
180
|
|
|
176
181
|
interface IOutstandingFrameRecord {
|
|
@@ -178,6 +183,7 @@ interface IOutstandingFrameRecord {
|
|
|
178
183
|
timer?: ReturnType<typeof setTimeout>;
|
|
179
184
|
session: ILiveBrowserSessionLike;
|
|
180
185
|
incarnationGeneration: number;
|
|
186
|
+
producerAcknowledgement: Promise<TFrameAcknowledgementOutcome>;
|
|
181
187
|
}
|
|
182
188
|
|
|
183
189
|
type TFrameAcknowledgementOutcome =
|
|
@@ -208,6 +214,7 @@ interface IFrameRefreshRecord {
|
|
|
208
214
|
abandoned: boolean;
|
|
209
215
|
boundaryCandidate?: plugins.smartpuppeteer.ILiveBrowserFrameIdentity;
|
|
210
216
|
failure?: BrowserRuntimeError;
|
|
217
|
+
producerFailure?: Promise<boolean>;
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
interface IResourceSlot {
|
|
@@ -228,12 +235,15 @@ interface IResourceSlot {
|
|
|
228
235
|
proxy?: BrowserEgressProxy;
|
|
229
236
|
profileDirectory?: string;
|
|
230
237
|
unsubscribeSession?: () => void;
|
|
231
|
-
|
|
238
|
+
leases: Map<string, ILeaseRecord>;
|
|
232
239
|
operation?: IOperationRecord;
|
|
233
240
|
operationQueue: IQueuedOperationRecord[];
|
|
234
241
|
operationSchedulerRunning: boolean;
|
|
235
|
-
|
|
242
|
+
frameSubscriptions: Map<string, IFrameSubscriptionRecord>;
|
|
243
|
+
highestFrameSequence: number;
|
|
244
|
+
producerAcknowledgements: Set<Promise<TFrameAcknowledgementOutcome>>;
|
|
236
245
|
frameRefresh?: IFrameRefreshRecord;
|
|
246
|
+
frameFailure?: { session: ILiveBrowserSessionLike; promise: Promise<boolean> };
|
|
237
247
|
idleTimer?: ReturnType<typeof setTimeout>;
|
|
238
248
|
lifecycleTail: Promise<void>;
|
|
239
249
|
lifecycleOperationCount: number;
|
|
@@ -697,6 +707,10 @@ export class BrowserRuntime {
|
|
|
697
707
|
permanentlyFenced: false,
|
|
698
708
|
retirementPending: false,
|
|
699
709
|
attachmentFenceCount: 0,
|
|
710
|
+
leases: new Map(),
|
|
711
|
+
frameSubscriptions: new Map(),
|
|
712
|
+
highestFrameSequence: 0,
|
|
713
|
+
producerAcknowledgements: new Set(),
|
|
700
714
|
operationQueue: [],
|
|
701
715
|
operationSchedulerRunning: false,
|
|
702
716
|
lifecycleTail: Promise.resolve(),
|
|
@@ -839,7 +853,6 @@ export class BrowserRuntime {
|
|
|
839
853
|
const sessionId = requestArg.sessionId === undefined
|
|
840
854
|
? undefined
|
|
841
855
|
: this.validateQualifiedSessionId(requestArg.sessionId);
|
|
842
|
-
if (sessionId?.harnessId === 'codex') throw new BrowserRuntimeError('INVALID_INPUT');
|
|
843
856
|
if (
|
|
844
857
|
(role === 'agent' && !sessionId)
|
|
845
858
|
|| (role === 'human' && sessionId !== undefined)
|
|
@@ -847,7 +860,7 @@ export class BrowserRuntime {
|
|
|
847
860
|
|| sessionId?.harnessId !== 'flex'))
|
|
848
861
|
|| (source === 'mcp' && (scopeId !== undefined || channelId !== undefined
|
|
849
862
|
|| runId !== undefined
|
|
850
|
-
|| sessionId
|
|
863
|
+
|| sessionId === undefined))
|
|
851
864
|
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
852
865
|
|| runId !== undefined))
|
|
853
866
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -1024,9 +1037,7 @@ export class BrowserRuntime {
|
|
|
1024
1037
|
);
|
|
1025
1038
|
const slot = this.getRegisteredSlot(capability.projectId, capability.browserResourceId);
|
|
1026
1039
|
this.assertCapabilityStillAttached(capability, slot);
|
|
1027
|
-
return
|
|
1028
|
-
? this.acquireAgentLease(slot, capability, digest, optionsArg)
|
|
1029
|
-
: this.acquireHumanLease(slot, capability, digest, optionsArg);
|
|
1040
|
+
return this.acquireParticipantLease(slot, capability, digest, optionsArg);
|
|
1030
1041
|
}
|
|
1031
1042
|
|
|
1032
1043
|
public authorizeCapability(
|
|
@@ -1325,17 +1336,61 @@ export class BrowserRuntime {
|
|
|
1325
1336
|
lease: ILeaseRecord,
|
|
1326
1337
|
viewport: plugins.smartpuppeteer.ILiveBrowserViewport,
|
|
1327
1338
|
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
1328
|
-
): Promise<
|
|
1339
|
+
): Promise<IBrowserRuntimeViewportResult> {
|
|
1329
1340
|
this.requireHuman(lease);
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
'setViewport',
|
|
1333
|
-
'viewport',
|
|
1334
|
-
operationOptions,
|
|
1341
|
+
const preferredViewport = this.validatePreferredViewport(viewport);
|
|
1342
|
+
return this.runOperation(lease, 'setViewport', 'viewport', operationOptions,
|
|
1335
1343
|
async (signal, session) => {
|
|
1344
|
+
const previous = lease.preferredViewport;
|
|
1345
|
+
lease.preferredViewport = preferredViewport;
|
|
1346
|
+
try {
|
|
1347
|
+
return await this.applySharedViewport(lease.slot, session, signal);
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
lease.preferredViewport = previous;
|
|
1350
|
+
throw error;
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
private validatePreferredViewport(
|
|
1356
|
+
value: plugins.smartpuppeteer.ILiveBrowserViewport,
|
|
1357
|
+
): plugins.smartpuppeteer.ILiveBrowserViewport {
|
|
1358
|
+
const record = validateExactKeys(value, ['width', 'height', 'deviceScaleFactor'], 'viewport');
|
|
1359
|
+
const width = validateInteger(record.width, 'viewport.width', 1, 4096);
|
|
1360
|
+
const height = validateInteger(record.height, 'viewport.height', 1, 4096);
|
|
1361
|
+
const deviceScaleFactor = record.deviceScaleFactor;
|
|
1362
|
+
if (typeof deviceScaleFactor !== 'number' || !Number.isFinite(deviceScaleFactor)
|
|
1363
|
+
|| deviceScaleFactor < 0.25 || deviceScaleFactor > 3
|
|
1364
|
+
|| Math.ceil(width * deviceScaleFactor) * Math.ceil(height * deviceScaleFactor)
|
|
1365
|
+
> maxScreencastPixelArea
|
|
1366
|
+
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1367
|
+
return { width, height, deviceScaleFactor };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
private async applySharedViewport(
|
|
1371
|
+
slot: IResourceSlot,
|
|
1372
|
+
session: ILiveBrowserSessionLike,
|
|
1373
|
+
signal: AbortSignal,
|
|
1374
|
+
): Promise<IBrowserRuntimeViewportResult> {
|
|
1375
|
+
const preferences = [...slot.leases.values()]
|
|
1376
|
+
.filter((lease) => lease.role === 'human' && !lease.released && lease.preferredViewport)
|
|
1377
|
+
.map((lease) => lease.preferredViewport!);
|
|
1378
|
+
const current = session.getState();
|
|
1379
|
+
const viewport = preferences.length ? {
|
|
1380
|
+
width: Math.min(...preferences.map((value) => value.width)),
|
|
1381
|
+
height: Math.min(...preferences.map((value) => value.height)),
|
|
1382
|
+
deviceScaleFactor: Math.min(...preferences.map((value) => value.deviceScaleFactor)),
|
|
1383
|
+
} : current.viewport;
|
|
1384
|
+
if (
|
|
1385
|
+
viewport.width !== current.viewport.width || viewport.height !== current.viewport.height
|
|
1386
|
+
|| viewport.deviceScaleFactor !== current.viewport.deviceScaleFactor
|
|
1387
|
+
) {
|
|
1388
|
+
await this.releaseAllParticipantInput(slot, session);
|
|
1389
|
+
signal.throwIfAborted();
|
|
1336
1390
|
await session.setViewport(viewport, { signal });
|
|
1337
|
-
|
|
1338
|
-
);
|
|
1391
|
+
}
|
|
1392
|
+
const state = session.getState();
|
|
1393
|
+
return { viewport: { ...state.viewport }, viewportRevision: state.viewportRevision };
|
|
1339
1394
|
}
|
|
1340
1395
|
|
|
1341
1396
|
/** @internal */
|
|
@@ -1346,7 +1401,7 @@ export class BrowserRuntime {
|
|
|
1346
1401
|
this.requireHuman(lease);
|
|
1347
1402
|
this.requireValidLease(lease);
|
|
1348
1403
|
const slot = lease.slot;
|
|
1349
|
-
const subscription = slot.
|
|
1404
|
+
const subscription = slot.frameSubscriptions.get(lease.leaseId);
|
|
1350
1405
|
if (!subscription || subscription.closed || subscription.lease !== lease) {
|
|
1351
1406
|
throw new BrowserRuntimeError('BUSY');
|
|
1352
1407
|
}
|
|
@@ -1385,17 +1440,161 @@ export class BrowserRuntime {
|
|
|
1385
1440
|
this.requireHuman(lease);
|
|
1386
1441
|
await this.runOperation(lease, action, 'raw-input', operationOptions, async (_signal, session) => {
|
|
1387
1442
|
if (action === 'dispatchMouse') {
|
|
1388
|
-
|
|
1443
|
+
const mouse = input as plugins.smartpuppeteer.ILiveBrowserMouseInput;
|
|
1444
|
+
if (mouse.type === 'down') {
|
|
1445
|
+
this.validateHeldInputTarget(session, mouse);
|
|
1446
|
+
if (!['none', 'left', 'middle', 'right', 'back', 'forward'].includes(mouse.button ?? 'left')
|
|
1447
|
+
|| !Number.isFinite(mouse.x) || !Number.isFinite(mouse.y) || mouse.x < 0 || mouse.y < 0
|
|
1448
|
+
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1449
|
+
}
|
|
1450
|
+
const key = this.mouseOwnershipKey(mouse);
|
|
1451
|
+
if (mouse.type === 'up' && this.otherParticipantHolds(lease, 'heldButtons', key)) {
|
|
1452
|
+
lease.heldButtons.delete(key);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (mouse.type === 'down' && lease.heldButtons.size >= 5 && !lease.heldButtons.has(key)) {
|
|
1456
|
+
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
1457
|
+
}
|
|
1458
|
+
if (mouse.type === 'down') lease.heldButtons.set(key, { ...mouse });
|
|
1459
|
+
await session.dispatchMouse({ ...mouse,
|
|
1460
|
+
buttons: this.sharedMouseButtons(lease.slot, mouse.tabId,
|
|
1461
|
+
mouse.type === 'up' ? { lease, key } : undefined),
|
|
1462
|
+
modifiers: this.sharedModifiers(lease.slot, mouse.tabId, mouse.modifiers),
|
|
1463
|
+
});
|
|
1464
|
+
if (mouse.type === 'up') lease.heldButtons.delete(key);
|
|
1389
1465
|
} else if (action === 'dispatchWheel') {
|
|
1390
|
-
|
|
1466
|
+
const wheel = input as plugins.smartpuppeteer.ILiveBrowserWheelInput;
|
|
1467
|
+
await session.dispatchWheel({ ...wheel,
|
|
1468
|
+
modifiers: this.sharedModifiers(lease.slot, wheel.tabId, wheel.modifiers),
|
|
1469
|
+
});
|
|
1391
1470
|
} else if (action === 'dispatchKey') {
|
|
1392
|
-
|
|
1471
|
+
const keyInput = input as plugins.smartpuppeteer.ILiveBrowserKeyInput;
|
|
1472
|
+
if (keyInput.type === 'down') {
|
|
1473
|
+
this.validateHeldInputTarget(session, keyInput);
|
|
1474
|
+
if (typeof keyInput.key !== 'string' || keyInput.key.length < 1 || keyInput.key.length > 64
|
|
1475
|
+
|| (keyInput.code !== undefined && (typeof keyInput.code !== 'string'
|
|
1476
|
+
|| keyInput.code.length < 1 || keyInput.code.length > 64))
|
|
1477
|
+
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1478
|
+
}
|
|
1479
|
+
const key = this.keyOwnershipKey(keyInput);
|
|
1480
|
+
if (keyInput.type === 'up' && this.otherParticipantHolds(lease, 'heldKeys', key)) {
|
|
1481
|
+
lease.heldKeys.delete(key);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (keyInput.type === 'down' && lease.heldKeys.size >= 64 && !lease.heldKeys.has(key)) {
|
|
1485
|
+
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
1486
|
+
}
|
|
1487
|
+
if (keyInput.type === 'down') lease.heldKeys.set(key, { ...keyInput });
|
|
1488
|
+
await session.dispatchKey({ ...keyInput,
|
|
1489
|
+
modifiers: this.sharedModifiers(lease.slot, keyInput.tabId, keyInput.modifiers,
|
|
1490
|
+
keyInput.type === 'up' ? { lease, key } : undefined),
|
|
1491
|
+
});
|
|
1492
|
+
if (keyInput.type === 'up') lease.heldKeys.delete(key);
|
|
1393
1493
|
} else {
|
|
1394
1494
|
await session.insertText(input as plugins.smartpuppeteer.ILiveBrowserInsertTextInput);
|
|
1395
1495
|
}
|
|
1396
1496
|
});
|
|
1397
1497
|
}
|
|
1398
1498
|
|
|
1499
|
+
private keyOwnershipKey(input: plugins.smartpuppeteer.ILiveBrowserKeyInput): string {
|
|
1500
|
+
return JSON.stringify([input.tabId, input.code ?? input.key]);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
private validateHeldInputTarget(
|
|
1504
|
+
session: ILiveBrowserSessionLike,
|
|
1505
|
+
input: plugins.smartpuppeteer.ILiveBrowserInputBase,
|
|
1506
|
+
): void {
|
|
1507
|
+
const state = session.getState();
|
|
1508
|
+
const tab = state.tabs.find((value) => value.id === state.activeTabId);
|
|
1509
|
+
if (!tab || tab.status !== 'open' || tab.id !== input.tabId
|
|
1510
|
+
|| tab.generation !== input.generation || state.viewportRevision !== input.viewportRevision
|
|
1511
|
+
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
private mouseOwnershipKey(input: plugins.smartpuppeteer.ILiveBrowserMouseInput): string {
|
|
1515
|
+
return JSON.stringify([input.tabId, input.button ?? 'left']);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
private otherParticipantHolds(
|
|
1519
|
+
lease: ILeaseRecord, kind: 'heldKeys' | 'heldButtons', key: string,
|
|
1520
|
+
): boolean {
|
|
1521
|
+
return [...lease.slot.leases.values()].some((other) => other !== lease && other[kind].has(key));
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
private sharedMouseButtons(
|
|
1525
|
+
slot: IResourceSlot,
|
|
1526
|
+
tabId: string,
|
|
1527
|
+
excluding?: { lease: ILeaseRecord; key: string },
|
|
1528
|
+
): number {
|
|
1529
|
+
const masks = { left: 1, right: 2, middle: 4, back: 8, forward: 16, none: 0 };
|
|
1530
|
+
let buttons = 0;
|
|
1531
|
+
for (const lease of slot.leases.values()) {
|
|
1532
|
+
for (const [key, input] of lease.heldButtons) {
|
|
1533
|
+
if (input.tabId !== tabId || (excluding?.lease === lease && excluding.key === key)) continue;
|
|
1534
|
+
buttons |= masks[input.button ?? 'left'];
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return buttons;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
private sharedModifiers(
|
|
1541
|
+
slot: IResourceSlot,
|
|
1542
|
+
tabId: string,
|
|
1543
|
+
supplied?: plugins.smartpuppeteer.ILiveBrowserModifierState,
|
|
1544
|
+
excluding?: { lease: ILeaseRecord; key: string },
|
|
1545
|
+
): plugins.smartpuppeteer.ILiveBrowserModifierState {
|
|
1546
|
+
const modifiers = { ...supplied };
|
|
1547
|
+
for (const lease of slot.leases.values()) {
|
|
1548
|
+
for (const [key, input] of lease.heldKeys) {
|
|
1549
|
+
if (input.tabId !== tabId || (excluding?.lease === lease && excluding.key === key)) continue;
|
|
1550
|
+
if (input.key === 'Shift') modifiers.shift = true;
|
|
1551
|
+
else if (input.key === 'Control') modifiers.control = true;
|
|
1552
|
+
else if (input.key === 'Alt') modifiers.alt = true;
|
|
1553
|
+
else if (input.key === 'Meta') modifiers.meta = true;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return modifiers;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
private async releaseAllParticipantInput(
|
|
1560
|
+
slot: IResourceSlot, session: ILiveBrowserSessionLike,
|
|
1561
|
+
): Promise<void> {
|
|
1562
|
+
for (const lease of slot.leases.values()) await this.releaseParticipantInput(lease, session);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
private async releaseParticipantInput(
|
|
1566
|
+
lease: ILeaseRecord, session: ILiveBrowserSessionLike,
|
|
1567
|
+
): Promise<void> {
|
|
1568
|
+
if (!lease.heldKeys.size && !lease.heldButtons.size) return;
|
|
1569
|
+
let state = session.getState();
|
|
1570
|
+
let tab = state.tabs.find((value) => value.id === state.activeTabId);
|
|
1571
|
+
if (tab?.status === 'open' && !tab.streaming) {
|
|
1572
|
+
await session.refreshScreencast();
|
|
1573
|
+
state = session.getState();
|
|
1574
|
+
tab = state.tabs.find((value) => value.id === state.activeTabId);
|
|
1575
|
+
}
|
|
1576
|
+
const target = tab?.status === 'open'
|
|
1577
|
+
? { tabId: tab.id, generation: tab.generation, viewportRevision: state.viewportRevision }
|
|
1578
|
+
: undefined;
|
|
1579
|
+
for (const [key, input] of lease.heldKeys) {
|
|
1580
|
+
if (!this.otherParticipantHolds(lease, 'heldKeys', key) && target?.tabId === input.tabId) {
|
|
1581
|
+
await session.dispatchKey({ ...target, type: 'up', key: input.key, code: input.code,
|
|
1582
|
+
modifiers: this.sharedModifiers(lease.slot, input.tabId, undefined, { lease, key }) });
|
|
1583
|
+
}
|
|
1584
|
+
lease.heldKeys.delete(key);
|
|
1585
|
+
}
|
|
1586
|
+
for (const [key, input] of lease.heldButtons) {
|
|
1587
|
+
if (!this.otherParticipantHolds(lease, 'heldButtons', key) && target?.tabId === input.tabId) {
|
|
1588
|
+
await session.dispatchMouse({ ...target, type: 'up', button: input.button,
|
|
1589
|
+
buttons: this.sharedMouseButtons(lease.slot, input.tabId, { lease, key }),
|
|
1590
|
+
x: Math.min(input.x, state.viewport.width - 1),
|
|
1591
|
+
y: Math.min(input.y, state.viewport.height - 1),
|
|
1592
|
+
modifiers: this.sharedModifiers(lease.slot, input.tabId) });
|
|
1593
|
+
}
|
|
1594
|
+
lease.heldButtons.delete(key);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1399
1598
|
/** @internal */
|
|
1400
1599
|
public async subscribeLeaseEvents(
|
|
1401
1600
|
lease: ILeaseRecord,
|
|
@@ -1409,7 +1608,7 @@ export class BrowserRuntime {
|
|
|
1409
1608
|
let subscription: IFrameSubscriptionRecord;
|
|
1410
1609
|
try {
|
|
1411
1610
|
this.requireValidLease(lease);
|
|
1412
|
-
if (slot.
|
|
1611
|
+
if (slot.frameSubscriptions.has(lease.leaseId)) throw new BrowserRuntimeError('BUSY');
|
|
1413
1612
|
subscription = {
|
|
1414
1613
|
lease,
|
|
1415
1614
|
listener,
|
|
@@ -1417,7 +1616,7 @@ export class BrowserRuntime {
|
|
|
1417
1616
|
highestSequence: 0,
|
|
1418
1617
|
closed: false,
|
|
1419
1618
|
};
|
|
1420
|
-
slot.
|
|
1619
|
+
slot.frameSubscriptions.set(lease.leaseId, subscription);
|
|
1421
1620
|
this.pushToSubscription(subscription, {
|
|
1422
1621
|
type: 'state',
|
|
1423
1622
|
state: this.resourceState(slot.session!.getState()),
|
|
@@ -1436,45 +1635,13 @@ export class BrowserRuntime {
|
|
|
1436
1635
|
acknowledgementArg: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
1437
1636
|
): Promise<boolean> {
|
|
1438
1637
|
this.requireHuman(lease);
|
|
1439
|
-
const acknowledgement = this.validateFrameAcknowledgement(acknowledgementArg, true);
|
|
1440
|
-
const key = this.frameAcknowledgementKey(acknowledgement);
|
|
1441
|
-
const slot = lease.slot;
|
|
1442
|
-
const subscription = slot.frameSubscription;
|
|
1443
1638
|
this.requireValidLease(lease);
|
|
1444
|
-
|
|
1445
|
-
const
|
|
1446
|
-
if (!
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
slot,
|
|
1450
|
-
subscription,
|
|
1451
|
-
session,
|
|
1452
|
-
incarnationGeneration,
|
|
1453
|
-
);
|
|
1454
|
-
if (refresh?.admissionClosed) {
|
|
1455
|
-
subscription.outstanding.delete(key);
|
|
1456
|
-
if (outstanding.timer) clearTimeout(outstanding.timer);
|
|
1457
|
-
outstanding.timer = undefined;
|
|
1458
|
-
return false;
|
|
1459
|
-
}
|
|
1460
|
-
subscription.outstanding.delete(key);
|
|
1461
|
-
if (outstanding.timer) clearTimeout(outstanding.timer);
|
|
1462
|
-
outstanding.timer = undefined;
|
|
1463
|
-
const refreshOwned = Boolean(refresh && !refresh.abandoned);
|
|
1464
|
-
const outcome = await this.beginFrameAcknowledgement(
|
|
1465
|
-
session,
|
|
1466
|
-
outstanding.acknowledgement,
|
|
1467
|
-
refreshOwned ? refresh : undefined,
|
|
1639
|
+
const acknowledgement = this.validateFrameAcknowledgement(acknowledgementArg, true);
|
|
1640
|
+
const subscription = lease.slot.frameSubscriptions.get(lease.leaseId);
|
|
1641
|
+
if (!subscription) return false;
|
|
1642
|
+
return this.retireOutstandingFrame(
|
|
1643
|
+
subscription, this.frameAcknowledgementKey(acknowledgement),
|
|
1468
1644
|
);
|
|
1469
|
-
if (outcome.status === 'fulfilled') return outcome.accepted;
|
|
1470
|
-
if (refreshOwned) return false;
|
|
1471
|
-
if (refresh && slot.frameRefresh === refresh) {
|
|
1472
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
1473
|
-
return false;
|
|
1474
|
-
}
|
|
1475
|
-
await this.options.beforeFrameFailureTermination?.();
|
|
1476
|
-
await this.handleFrameFailure(slot, session, incarnationGeneration, subscription.lease);
|
|
1477
|
-
return false;
|
|
1478
1645
|
}
|
|
1479
1646
|
|
|
1480
1647
|
/** @internal */
|
|
@@ -1658,19 +1825,21 @@ export class BrowserRuntime {
|
|
|
1658
1825
|
}
|
|
1659
1826
|
}
|
|
1660
1827
|
|
|
1661
|
-
private async
|
|
1828
|
+
private async acquireParticipantLease(
|
|
1662
1829
|
slot: IResourceSlot,
|
|
1663
1830
|
capability: TCapabilityRecord,
|
|
1664
1831
|
digest: plugins.Buffer,
|
|
1665
1832
|
acquisition: TAcquireBrowserRuntimeLeaseOptions,
|
|
1666
1833
|
): Promise<BrowserRuntimeLease> {
|
|
1667
|
-
|
|
1668
|
-
if (
|
|
1834
|
+
this.assertSlotAvailable(slot);
|
|
1835
|
+
if (capability.lease || capability.acquiring) throw new BrowserRuntimeError('BUSY');
|
|
1836
|
+
capability.acquiring = true;
|
|
1837
|
+
let release: (() => void) | undefined;
|
|
1669
1838
|
try {
|
|
1839
|
+
release = await slot.mutex.acquire();
|
|
1670
1840
|
this.assertSlotAvailable(slot);
|
|
1671
1841
|
this.resolveCapability(digest, this.validateExpectedCapabilityBinding(acquisition));
|
|
1672
1842
|
this.assertCapabilityStillAttached(capability, slot);
|
|
1673
|
-
if (slot.lease || slot.operation) throw new BrowserRuntimeError('BUSY');
|
|
1674
1843
|
await this.ensureSession(slot, acquisition.signal);
|
|
1675
1844
|
await this.options.beforeLeasePublication?.();
|
|
1676
1845
|
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
@@ -1680,84 +1849,11 @@ export class BrowserRuntime {
|
|
|
1680
1849
|
const lease = this.createLeaseRecord(slot, capability, digest);
|
|
1681
1850
|
return new BrowserRuntimeLease(this, lease);
|
|
1682
1851
|
} catch (error) {
|
|
1683
|
-
if (slot.session &&
|
|
1852
|
+
if (slot.session && slot.leases.size === 0) this.scheduleIdleTermination(slot);
|
|
1684
1853
|
throw error;
|
|
1685
1854
|
} finally {
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
private async acquireHumanLease(
|
|
1691
|
-
slot: IResourceSlot,
|
|
1692
|
-
capability: TCapabilityRecord,
|
|
1693
|
-
digest: plugins.Buffer,
|
|
1694
|
-
acquisition: TAcquireBrowserRuntimeLeaseOptions,
|
|
1695
|
-
): Promise<BrowserRuntimeLease> {
|
|
1696
|
-
const release = await slot.mutex.acquire();
|
|
1697
|
-
let generation = 0;
|
|
1698
|
-
let preemptedLease: ILeaseRecord | undefined;
|
|
1699
|
-
try {
|
|
1700
|
-
this.assertSlotAvailable(slot);
|
|
1701
|
-
this.resolveCapability(digest, this.validateExpectedCapabilityBinding(acquisition));
|
|
1702
|
-
this.assertCapabilityStillAttached(capability, slot);
|
|
1703
|
-
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
1704
|
-
if (slot.fencingGeneration !== undefined) throw new BrowserRuntimeError('BUSY');
|
|
1705
|
-
if (slot.lease?.role === 'human') throw new BrowserRuntimeError('BUSY');
|
|
1706
|
-
generation = ++slot.arbitrationGeneration;
|
|
1707
|
-
slot.fencingGeneration = generation;
|
|
1708
|
-
preemptedLease = slot.lease;
|
|
1709
|
-
if (preemptedLease) {
|
|
1710
|
-
preemptedLease.released = true;
|
|
1711
|
-
preemptedLease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1712
|
-
slot.operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1713
|
-
this.notifyFramedLeaseEnded(preemptedLease);
|
|
1714
|
-
}
|
|
1715
|
-
} finally {
|
|
1716
|
-
release();
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
try {
|
|
1720
|
-
if (preemptedLease) await this.quiesceSlot(slot);
|
|
1721
|
-
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
1722
|
-
const finalRelease = await slot.mutex.acquire();
|
|
1723
|
-
try {
|
|
1724
|
-
if (slot.arbitrationGeneration !== generation || slot.fencingGeneration !== generation) {
|
|
1725
|
-
throw new BrowserRuntimeError('BUSY');
|
|
1726
|
-
}
|
|
1727
|
-
if (preemptedLease) {
|
|
1728
|
-
if (preemptedLease.capability.lease === preemptedLease) {
|
|
1729
|
-
preemptedLease.capability.lease = undefined;
|
|
1730
|
-
}
|
|
1731
|
-
if (slot.lease === preemptedLease) slot.lease = undefined;
|
|
1732
|
-
}
|
|
1733
|
-
if (slot.permanentlyFenced) throw new BrowserRuntimeError('FENCED');
|
|
1734
|
-
slot.fencingGeneration = undefined;
|
|
1735
|
-
await this.ensureSession(slot, acquisition.signal);
|
|
1736
|
-
await this.options.beforeLeasePublication?.();
|
|
1737
|
-
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
1738
|
-
this.assertSlotAvailable(slot);
|
|
1739
|
-
this.resolveCapability(digest, this.validateExpectedCapabilityBinding(acquisition));
|
|
1740
|
-
this.assertCapabilityStillAttached(capability, slot);
|
|
1741
|
-
const lease = this.createLeaseRecord(slot, capability, digest);
|
|
1742
|
-
return new BrowserRuntimeLease(this, lease);
|
|
1743
|
-
} finally {
|
|
1744
|
-
finalRelease();
|
|
1745
|
-
}
|
|
1746
|
-
} catch (error) {
|
|
1747
|
-
const cleanupRelease = await slot.mutex.acquire();
|
|
1748
|
-
try {
|
|
1749
|
-
if (preemptedLease) {
|
|
1750
|
-
if (preemptedLease.capability.lease === preemptedLease) {
|
|
1751
|
-
preemptedLease.capability.lease = undefined;
|
|
1752
|
-
}
|
|
1753
|
-
if (slot.lease === preemptedLease) slot.lease = undefined;
|
|
1754
|
-
}
|
|
1755
|
-
if (slot.fencingGeneration === generation) slot.fencingGeneration = undefined;
|
|
1756
|
-
if (slot.session && !slot.lease) this.scheduleIdleTermination(slot);
|
|
1757
|
-
} finally {
|
|
1758
|
-
cleanupRelease();
|
|
1759
|
-
}
|
|
1760
|
-
throw error;
|
|
1855
|
+
capability.acquiring = false;
|
|
1856
|
+
release?.();
|
|
1761
1857
|
}
|
|
1762
1858
|
}
|
|
1763
1859
|
|
|
@@ -1768,7 +1864,7 @@ export class BrowserRuntime {
|
|
|
1768
1864
|
): ILeaseRecord {
|
|
1769
1865
|
this.assertSlotAvailable(slot);
|
|
1770
1866
|
this.assertCapabilityStillAttached(capability, slot);
|
|
1771
|
-
if (capability.lease
|
|
1867
|
+
if (capability.lease) throw new BrowserRuntimeError('BUSY');
|
|
1772
1868
|
if (slot.idleTimer) {
|
|
1773
1869
|
clearTimeout(slot.idleTimer);
|
|
1774
1870
|
slot.idleTimer = undefined;
|
|
@@ -1788,8 +1884,10 @@ export class BrowserRuntime {
|
|
|
1788
1884
|
controller: new AbortController(),
|
|
1789
1885
|
authorityGeneration: slot.authorityGeneration,
|
|
1790
1886
|
released: false,
|
|
1887
|
+
heldKeys: new Map(),
|
|
1888
|
+
heldButtons: new Map(),
|
|
1791
1889
|
};
|
|
1792
|
-
slot.lease
|
|
1890
|
+
slot.leases.set(lease.leaseId, lease);
|
|
1793
1891
|
capability.lease = lease;
|
|
1794
1892
|
return lease;
|
|
1795
1893
|
}
|
|
@@ -1954,6 +2052,7 @@ export class BrowserRuntime {
|
|
|
1954
2052
|
operationOptions: IBrowserRuntimeOperationOptions,
|
|
1955
2053
|
execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<T>,
|
|
1956
2054
|
finalize?: () => Promise<void>,
|
|
2055
|
+
participantCleanup = false,
|
|
1957
2056
|
): Promise<T> {
|
|
1958
2057
|
if (!operationOptions || typeof operationOptions !== 'object') {
|
|
1959
2058
|
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -1968,16 +2067,21 @@ export class BrowserRuntime {
|
|
|
1968
2067
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1969
2068
|
if (operationOptions.signal?.aborted) throw new BrowserRuntimeError('ABORTED');
|
|
1970
2069
|
const slot = lease.slot;
|
|
1971
|
-
this.
|
|
2070
|
+
this.requireQueuedLease(lease, participantCleanup);
|
|
1972
2071
|
this.assertSlotAvailable(slot);
|
|
1973
|
-
if (
|
|
2072
|
+
if (!participantCleanup && (
|
|
2073
|
+
slot.operationQueue.filter((entry) => entry.lease === lease).length
|
|
2074
|
+
>= this.options.maxQueuedOperationsPerLease
|
|
2075
|
+
|| slot.operationQueue.length >= this.options.maxQueuedOperationsPerLease
|
|
2076
|
+
* this.options.maxCapabilitiesPerResource
|
|
2077
|
+
)) {
|
|
1974
2078
|
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
1975
2079
|
}
|
|
1976
2080
|
const timeoutMs = operationOptions.timeoutMs === undefined
|
|
1977
2081
|
? this.options.operationTimeoutMs
|
|
1978
2082
|
: validateInteger(operationOptions.timeoutMs, 'timeoutMs', 100, 120_000);
|
|
1979
2083
|
const signal = AbortSignal.any([
|
|
1980
|
-
lease.controller.signal,
|
|
2084
|
+
...(participantCleanup ? [] : [lease.controller.signal]),
|
|
1981
2085
|
this.lifecycleController.signal,
|
|
1982
2086
|
operationOptions.signal ?? new AbortController().signal,
|
|
1983
2087
|
]);
|
|
@@ -1996,6 +2100,7 @@ export class BrowserRuntime {
|
|
|
1996
2100
|
resolve: (value) => resolve(value as T),
|
|
1997
2101
|
reject,
|
|
1998
2102
|
state: 'queued',
|
|
2103
|
+
participantCleanup,
|
|
1999
2104
|
};
|
|
2000
2105
|
const onQueuedAbort = (): void => {
|
|
2001
2106
|
if (queued.state !== 'queued' && queued.state !== 'starting') return;
|
|
@@ -2005,7 +2110,15 @@ export class BrowserRuntime {
|
|
|
2005
2110
|
}
|
|
2006
2111
|
queued.state = 'settled';
|
|
2007
2112
|
signal.removeEventListener('abort', onQueuedAbort);
|
|
2008
|
-
|
|
2113
|
+
const error = this.queuedOperationAbortError(queued);
|
|
2114
|
+
const frameFailure = slot.frameFailure;
|
|
2115
|
+
if (frameFailure && frameFailure.session === slot.session) {
|
|
2116
|
+
// Stop admission immediately, but expose the terminal result only
|
|
2117
|
+
// after cleanup of the failed producer's incarnation has settled.
|
|
2118
|
+
void frameFailure.promise.then(() => reject(error), reject);
|
|
2119
|
+
} else {
|
|
2120
|
+
reject(error);
|
|
2121
|
+
}
|
|
2009
2122
|
};
|
|
2010
2123
|
queued.onQueuedAbort = onQueuedAbort;
|
|
2011
2124
|
signal.addEventListener('abort', onQueuedAbort, { once: true });
|
|
@@ -2069,7 +2182,7 @@ export class BrowserRuntime {
|
|
|
2069
2182
|
try {
|
|
2070
2183
|
if (queued.state === 'settled') return undefined;
|
|
2071
2184
|
queued.signal.throwIfAborted();
|
|
2072
|
-
this.
|
|
2185
|
+
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2073
2186
|
this.assertSlotAvailable(slot);
|
|
2074
2187
|
if (slot.operation) throw new BrowserRuntimeError('BUSY');
|
|
2075
2188
|
session = slot.session!;
|
|
@@ -2130,13 +2243,12 @@ export class BrowserRuntime {
|
|
|
2130
2243
|
let executionStarted = false;
|
|
2131
2244
|
let executionSettled = false;
|
|
2132
2245
|
try {
|
|
2133
|
-
await this.runBeforeOperation(operationIdentity, combinedSignal);
|
|
2246
|
+
if (!queued.participantCleanup) await this.runBeforeOperation(operationIdentity, combinedSignal);
|
|
2134
2247
|
const executionRelease = await slot.mutex.acquire();
|
|
2135
2248
|
try {
|
|
2136
2249
|
combinedSignal.throwIfAborted();
|
|
2137
|
-
this.
|
|
2250
|
+
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2138
2251
|
this.assertSlotAvailable(slot);
|
|
2139
|
-
this.assertCapabilityStillAttached(lease.capability, slot);
|
|
2140
2252
|
if (
|
|
2141
2253
|
slot.operation !== operation
|
|
2142
2254
|
|| slot.session !== session
|
|
@@ -2144,7 +2256,13 @@ export class BrowserRuntime {
|
|
|
2144
2256
|
|| slot.incarnationGeneration !== incarnationGeneration
|
|
2145
2257
|
) throw new BrowserRuntimeError('ABORTED');
|
|
2146
2258
|
executionStarted = true;
|
|
2147
|
-
executionPromise =
|
|
2259
|
+
executionPromise = (async () => {
|
|
2260
|
+
if (classification === 'navigation' || classification === 'tab') {
|
|
2261
|
+
await this.releaseAllParticipantInput(slot, session);
|
|
2262
|
+
}
|
|
2263
|
+
combinedSignal.throwIfAborted();
|
|
2264
|
+
return queued.execute(combinedSignal, session);
|
|
2265
|
+
})();
|
|
2148
2266
|
operation.promise = executionPromise.then(() => undefined, () => undefined).finally(() => {
|
|
2149
2267
|
executionSettled = true;
|
|
2150
2268
|
});
|
|
@@ -2162,8 +2280,7 @@ export class BrowserRuntime {
|
|
|
2162
2280
|
combinedSignal.removeEventListener('abort', onAbort);
|
|
2163
2281
|
}
|
|
2164
2282
|
if (combinedSignal.aborted) throw combinedSignal.reason;
|
|
2165
|
-
this.
|
|
2166
|
-
this.assertCapabilityStillAttached(lease.capability, slot);
|
|
2283
|
+
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2167
2284
|
if (slot.session !== session! || slot.arbitrationGeneration !== generation!) {
|
|
2168
2285
|
throw new BrowserRuntimeError('ABORTED');
|
|
2169
2286
|
}
|
|
@@ -2205,6 +2322,12 @@ export class BrowserRuntime {
|
|
|
2205
2322
|
failure = error;
|
|
2206
2323
|
}
|
|
2207
2324
|
}
|
|
2325
|
+
if (slot.frameFailure?.session === session) {
|
|
2326
|
+
try { await slot.frameFailure.promise; } catch (error) {
|
|
2327
|
+
failed = true;
|
|
2328
|
+
failure = error;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2208
2331
|
const errorCode = failed ? this.operationErrorCode(failure) : undefined;
|
|
2209
2332
|
await this.emitAudit(Object.freeze({
|
|
2210
2333
|
...operationIdentity,
|
|
@@ -2532,10 +2655,10 @@ export class BrowserRuntime {
|
|
|
2532
2655
|
): IFrameRefreshRecord {
|
|
2533
2656
|
if (
|
|
2534
2657
|
slot.frameRefresh
|
|
2535
|
-
|| slot.
|
|
2658
|
+
|| slot.frameSubscriptions.get(lease.leaseId) !== subscription
|
|
2536
2659
|
|| subscription.closed
|
|
2537
2660
|
|| subscription.lease !== lease
|
|
2538
|
-
|| slot.lease !== lease
|
|
2661
|
+
|| slot.leases.get(lease.leaseId) !== lease
|
|
2539
2662
|
|| slot.session !== session
|
|
2540
2663
|
) throw new BrowserRuntimeError('BUSY');
|
|
2541
2664
|
const state = session.getState();
|
|
@@ -2544,7 +2667,7 @@ export class BrowserRuntime {
|
|
|
2544
2667
|
!activeTab
|
|
2545
2668
|
|| !activeTab.active
|
|
2546
2669
|
|| activeTab.status !== 'open'
|
|
2547
|
-
||
|
|
2670
|
+
|| state.status !== 'running'
|
|
2548
2671
|
) throw new BrowserRuntimeError('BUSY');
|
|
2549
2672
|
const refresh: IFrameRefreshRecord = {
|
|
2550
2673
|
lease,
|
|
@@ -2554,7 +2677,7 @@ export class BrowserRuntime {
|
|
|
2554
2677
|
initialTabId: activeTab.id,
|
|
2555
2678
|
initialGeneration: activeTab.generation,
|
|
2556
2679
|
initialViewportRevision: state.viewportRevision,
|
|
2557
|
-
startingHighestSequence:
|
|
2680
|
+
startingHighestSequence: slot.highestFrameSequence,
|
|
2558
2681
|
pendingAcknowledgements: new Set(),
|
|
2559
2682
|
refreshStarted: false,
|
|
2560
2683
|
admissionClosed: false,
|
|
@@ -2604,17 +2727,17 @@ export class BrowserRuntime {
|
|
|
2604
2727
|
}
|
|
2605
2728
|
|
|
2606
2729
|
private async drainFrameRefreshOutstanding(refresh: IFrameRefreshRecord): Promise<void> {
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2730
|
+
const slot = refresh.lease.slot;
|
|
2731
|
+
for (const subscription of slot.frameSubscriptions.values()) {
|
|
2732
|
+
for (const frame of subscription.outstanding.values()) {
|
|
2733
|
+
if (frame.timer) clearTimeout(frame.timer);
|
|
2734
|
+
}
|
|
2735
|
+
subscription.outstanding.clear();
|
|
2736
|
+
}
|
|
2737
|
+
const outcomes = await Promise.all([...slot.producerAcknowledgements]);
|
|
2738
|
+
if (outcomes.some((outcome) => outcome.status !== 'fulfilled')) {
|
|
2739
|
+
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2612
2740
|
}
|
|
2613
|
-
await Promise.all(outstanding.map((frame) => this.beginFrameAcknowledgement(
|
|
2614
|
-
frame.session,
|
|
2615
|
-
frame.acknowledgement,
|
|
2616
|
-
refresh,
|
|
2617
|
-
)));
|
|
2618
2741
|
}
|
|
2619
2742
|
|
|
2620
2743
|
private validateFrameRefreshBoundary(
|
|
@@ -2646,25 +2769,23 @@ export class BrowserRuntime {
|
|
|
2646
2769
|
refresh: IFrameRefreshRecord,
|
|
2647
2770
|
identity: plugins.smartpuppeteer.ILiveBrowserFrameIdentity,
|
|
2648
2771
|
): Promise<void> {
|
|
2649
|
-
const
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
));
|
|
2772
|
+
for (const subscription of refresh.lease.slot.frameSubscriptions.values()) {
|
|
2773
|
+
for (const [key, frame] of subscription.outstanding) {
|
|
2774
|
+
const acknowledgement = frame.acknowledgement;
|
|
2775
|
+
if (
|
|
2776
|
+
acknowledgement.tabId === identity.tabId
|
|
2777
|
+
&& acknowledgement.generation === identity.generation
|
|
2778
|
+
&& acknowledgement.viewportRevision === identity.viewportRevision
|
|
2779
|
+
&& acknowledgement.sequence >= identity.sequence
|
|
2780
|
+
) continue;
|
|
2781
|
+
subscription.outstanding.delete(key);
|
|
2782
|
+
if (frame.timer) clearTimeout(frame.timer);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
const outcomes = await Promise.all([...refresh.lease.slot.producerAcknowledgements]);
|
|
2786
|
+
if (outcomes.some((outcome) => outcome.status !== 'fulfilled')) {
|
|
2787
|
+
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2666
2788
|
}
|
|
2667
|
-
await Promise.all(retirements);
|
|
2668
2789
|
}
|
|
2669
2790
|
|
|
2670
2791
|
private beginFrameAcknowledgement(
|
|
@@ -2698,14 +2819,14 @@ export class BrowserRuntime {
|
|
|
2698
2819
|
refresh: IFrameRefreshRecord,
|
|
2699
2820
|
): Promise<void> {
|
|
2700
2821
|
if (refresh.failure) {
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2822
|
+
let claimed: boolean;
|
|
2823
|
+
if (refresh.producerFailure) claimed = await refresh.producerFailure;
|
|
2824
|
+
else {
|
|
2825
|
+
await this.options.beforeFrameFailureTermination?.();
|
|
2826
|
+
claimed = await this.handleFrameFailure(
|
|
2827
|
+
slot, refresh.session, refresh.incarnationGeneration, refresh,
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2709
2830
|
if (!claimed) {
|
|
2710
2831
|
this.abandonFrameRefresh(slot, refresh);
|
|
2711
2832
|
throw new BrowserRuntimeError('ABORTED');
|
|
@@ -2720,7 +2841,7 @@ export class BrowserRuntime {
|
|
|
2720
2841
|
this.requireValidLease(refresh.lease);
|
|
2721
2842
|
current = slot.frameRefresh === refresh
|
|
2722
2843
|
&& !refresh.abandoned
|
|
2723
|
-
&& slot.
|
|
2844
|
+
&& slot.frameSubscriptions.get(refresh.lease.leaseId) === refresh.subscription
|
|
2724
2845
|
&& !refresh.subscription.closed
|
|
2725
2846
|
&& slot.session === refresh.session
|
|
2726
2847
|
&& slot.incarnationGeneration === refresh.incarnationGeneration;
|
|
@@ -2748,7 +2869,7 @@ export class BrowserRuntime {
|
|
|
2748
2869
|
refresh.abandoned = true;
|
|
2749
2870
|
slot.frameRefresh = undefined;
|
|
2750
2871
|
if (
|
|
2751
|
-
slot.
|
|
2872
|
+
slot.frameSubscriptions.get(refresh.lease.leaseId) === refresh.subscription
|
|
2752
2873
|
&& !refresh.subscription.closed
|
|
2753
2874
|
&& slot.session === refresh.session
|
|
2754
2875
|
&& slot.incarnationGeneration === refresh.incarnationGeneration
|
|
@@ -2766,274 +2887,140 @@ export class BrowserRuntime {
|
|
|
2766
2887
|
): void {
|
|
2767
2888
|
if (frame.timer || subscription.closed || !subscription.outstanding.has(key)) return;
|
|
2768
2889
|
const timer = setTimeout(() => {
|
|
2769
|
-
//
|
|
2770
|
-
|
|
2771
|
-
this.trackCleanup(this.retireOutstandingFrame(subscription, key, false).then(() => undefined));
|
|
2890
|
+
// Viewer lag retires only its local window; producer acknowledgement already started.
|
|
2891
|
+
this.trackCleanup(this.retireOutstandingFrame(subscription, key).then(() => undefined));
|
|
2772
2892
|
}, this.options.frameAcknowledgementTimeoutMs);
|
|
2773
2893
|
timer.unref();
|
|
2774
2894
|
frame.timer = timer;
|
|
2775
2895
|
}
|
|
2776
2896
|
|
|
2777
|
-
private matchingFrameRefresh(
|
|
2778
|
-
slot: IResourceSlot,
|
|
2779
|
-
subscription: IFrameSubscriptionRecord,
|
|
2780
|
-
session: ILiveBrowserSessionLike,
|
|
2781
|
-
incarnationGeneration: number,
|
|
2782
|
-
): IFrameRefreshRecord | undefined {
|
|
2783
|
-
const refresh = slot.frameRefresh;
|
|
2784
|
-
return refresh
|
|
2785
|
-
&& !refresh.abandoned
|
|
2786
|
-
&& refresh.subscription === subscription
|
|
2787
|
-
&& refresh.lease === subscription.lease
|
|
2788
|
-
&& refresh.session === session
|
|
2789
|
-
&& refresh.incarnationGeneration === incarnationGeneration
|
|
2790
|
-
? refresh
|
|
2791
|
-
: undefined;
|
|
2792
|
-
}
|
|
2793
|
-
|
|
2794
2897
|
private handleSessionEvent(
|
|
2795
2898
|
slot: IResourceSlot,
|
|
2796
2899
|
session: ILiveBrowserSessionLike,
|
|
2797
2900
|
event: plugins.smartpuppeteer.TLiveBrowserEvent,
|
|
2798
2901
|
): void {
|
|
2799
|
-
if (slot.session && slot.session !== session)
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
sequence: event.frame.sequence,
|
|
2804
|
-
generation: event.frame.generation,
|
|
2805
|
-
viewportRevision: event.frame.viewportRevision,
|
|
2806
|
-
}));
|
|
2807
|
-
}
|
|
2808
|
-
return;
|
|
2809
|
-
}
|
|
2810
|
-
const subscription = slot.frameSubscription;
|
|
2811
|
-
const refreshForSession = slot.frameRefresh
|
|
2812
|
-
&& !slot.frameRefresh.abandoned
|
|
2813
|
-
&& slot.frameRefresh.session === session
|
|
2814
|
-
&& slot.frameRefresh.incarnationGeneration === slot.incarnationGeneration
|
|
2815
|
-
? slot.frameRefresh
|
|
2816
|
-
: undefined;
|
|
2902
|
+
if (slot.session && slot.session !== session) return;
|
|
2903
|
+
const incarnationGeneration = slot.incarnationGeneration;
|
|
2904
|
+
const refresh = slot.frameRefresh?.session === session && !slot.frameRefresh.abandoned
|
|
2905
|
+
? slot.frameRefresh : undefined;
|
|
2817
2906
|
if (event.type === 'frame') {
|
|
2818
2907
|
let acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest;
|
|
2819
2908
|
try {
|
|
2820
|
-
if (!(event.frame.data instanceof Uint8Array))
|
|
2821
|
-
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2822
|
-
}
|
|
2909
|
+
if (!(event.frame.data instanceof Uint8Array)) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2823
2910
|
acknowledgement = this.validateFrameAcknowledgement(event.frame);
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
|
|
2827
|
-
return;
|
|
2911
|
+
if (acknowledgement.sequence <= slot.highestFrameSequence) {
|
|
2912
|
+
throw new BrowserRuntimeError('FRAME_STREAM_FAILED');
|
|
2828
2913
|
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
slot,
|
|
2832
|
-
session,
|
|
2833
|
-
slot.incarnationGeneration,
|
|
2834
|
-
failedLease,
|
|
2835
|
-
).then(() => undefined));
|
|
2836
|
-
return;
|
|
2837
|
-
}
|
|
2838
|
-
if (!subscription || subscription.closed) {
|
|
2839
|
-
if (refreshForSession) {
|
|
2840
|
-
this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
|
|
2841
|
-
return;
|
|
2914
|
+
if (slot.producerAcknowledgements.size >= this.options.maxOutstandingFrames) {
|
|
2915
|
+
throw new BrowserRuntimeError('FRAME_STREAM_FAILED');
|
|
2842
2916
|
}
|
|
2843
|
-
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
2844
|
-
return;
|
|
2845
|
-
}
|
|
2846
|
-
const refresh = this.matchingFrameRefresh(
|
|
2847
|
-
slot,
|
|
2848
|
-
subscription,
|
|
2849
|
-
session,
|
|
2850
|
-
slot.incarnationGeneration,
|
|
2851
|
-
);
|
|
2852
|
-
if (refreshForSession && refreshForSession !== refresh) {
|
|
2853
|
-
this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
|
|
2854
|
-
return;
|
|
2855
|
-
}
|
|
2856
|
-
try {
|
|
2857
|
-
this.requireValidLease(subscription.lease);
|
|
2858
2917
|
} catch {
|
|
2859
|
-
if (refresh)
|
|
2860
|
-
|
|
2861
|
-
return;
|
|
2862
|
-
}
|
|
2863
|
-
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
2864
|
-
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
2918
|
+
if (refresh) this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2919
|
+
else this.trackCleanup(this.handleFrameFailure(slot, session, incarnationGeneration).then(() => undefined));
|
|
2865
2920
|
return;
|
|
2866
2921
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2922
|
+
slot.highestFrameSequence = acknowledgement.sequence;
|
|
2923
|
+
// The producer has one bounded ACK, independent of how quickly any viewer paints.
|
|
2924
|
+
const producerAcknowledgement = this.beginFrameAcknowledgement(
|
|
2925
|
+
session, acknowledgement, refresh,
|
|
2926
|
+
);
|
|
2927
|
+
slot.producerAcknowledgements.add(producerAcknowledgement);
|
|
2928
|
+
this.trackCleanup(producerAcknowledgement.then(async (outcome) => {
|
|
2929
|
+
slot.producerAcknowledgements.delete(producerAcknowledgement);
|
|
2930
|
+
if (outcome.status === 'fulfilled') return;
|
|
2931
|
+
if (refresh && slot.frameRefresh === refresh && !refresh.abandoned) {
|
|
2870
2932
|
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2871
2933
|
return;
|
|
2872
2934
|
}
|
|
2873
|
-
this.
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
slot.incarnationGeneration,
|
|
2877
|
-
subscription.lease,
|
|
2878
|
-
).then(() => undefined));
|
|
2879
|
-
return;
|
|
2880
|
-
}
|
|
2881
|
-
subscription.highestSequence = acknowledgement.sequence;
|
|
2935
|
+
await this.options.beforeFrameFailureTermination?.();
|
|
2936
|
+
await this.handleFrameFailure(slot, session, incarnationGeneration);
|
|
2937
|
+
}));
|
|
2882
2938
|
if (event.frame.data.byteLength > this.options.maxFrameBytes) {
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
if (refresh) {
|
|
2886
|
-
this.failFrameRefresh(refresh, 'FRAME_TOO_LARGE');
|
|
2939
|
+
if (refresh) this.failFrameRefresh(refresh, 'FRAME_TOO_LARGE');
|
|
2940
|
+
for (const subscription of slot.frameSubscriptions.values()) {
|
|
2887
2941
|
this.pushToSubscription(subscription, {
|
|
2888
2942
|
type: 'error',
|
|
2889
|
-
error: { code: 'FRAME_TOO_LARGE', fatal: false, tabId:
|
|
2943
|
+
error: { code: 'FRAME_TOO_LARGE', fatal: false, tabId: acknowledgement.tabId },
|
|
2890
2944
|
});
|
|
2891
|
-
return;
|
|
2892
2945
|
}
|
|
2893
|
-
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
2894
|
-
this.pushToSubscription(subscription, {
|
|
2895
|
-
type: 'error',
|
|
2896
|
-
error: { code: 'FRAME_TOO_LARGE', fatal: false, tabId: event.frame.tabId },
|
|
2897
|
-
});
|
|
2898
2946
|
return;
|
|
2899
2947
|
}
|
|
2900
|
-
if (refresh?.admissionClosed)
|
|
2901
|
-
const refreshAdmission = refresh && !refresh.admissionClosed && !refresh.abandoned
|
|
2902
|
-
? refresh
|
|
2903
|
-
: undefined;
|
|
2904
|
-
if (refreshAdmission) {
|
|
2905
|
-
if (refreshAdmission.failure) return;
|
|
2948
|
+
if (refresh?.refreshStarted && !refresh.boundaryCandidate && !refresh.admissionClosed) {
|
|
2906
2949
|
if (
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
} else {
|
|
2914
|
-
if (refresh && subscription.outstanding.size >= this.options.maxOutstandingFrames) {
|
|
2915
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2916
|
-
}
|
|
2917
|
-
while (subscription.outstanding.size >= this.options.maxOutstandingFrames) {
|
|
2918
|
-
const oldestKey = subscription.outstanding.keys().next().value as string | undefined;
|
|
2919
|
-
if (!oldestKey) break;
|
|
2920
|
-
this.trackCleanup(
|
|
2921
|
-
this.retireOutstandingFrame(subscription, oldestKey, false).then(() => undefined),
|
|
2922
|
-
);
|
|
2923
|
-
}
|
|
2950
|
+
acknowledgement.tabId === refresh.initialTabId
|
|
2951
|
+
&& acknowledgement.sequence > refresh.startingHighestSequence
|
|
2952
|
+
&& acknowledgement.generation > refresh.initialGeneration
|
|
2953
|
+
&& acknowledgement.viewportRevision === refresh.initialViewportRevision
|
|
2954
|
+
) refresh.boundaryCandidate = { ...acknowledgement };
|
|
2955
|
+
else this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2924
2956
|
}
|
|
2925
|
-
const
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
if (!refreshAdmission) this.armFrameAcknowledgementTimer(subscription, key, outstanding);
|
|
2932
|
-
const delivered = this.pushToSubscription(subscription, { type: 'frame', frame: event.frame });
|
|
2933
|
-
if (
|
|
2934
|
-
delivered
|
|
2935
|
-
&& refreshAdmission
|
|
2936
|
-
&& refreshAdmission.refreshStarted
|
|
2937
|
-
&& !refreshAdmission.boundaryCandidate
|
|
2938
|
-
) {
|
|
2939
|
-
if (
|
|
2940
|
-
acknowledgement.tabId === refreshAdmission.initialTabId
|
|
2941
|
-
&& acknowledgement.sequence > refreshAdmission.startingHighestSequence
|
|
2942
|
-
&& acknowledgement.generation > refreshAdmission.initialGeneration
|
|
2943
|
-
&& acknowledgement.viewportRevision === refreshAdmission.initialViewportRevision
|
|
2944
|
-
) {
|
|
2945
|
-
refreshAdmission.boundaryCandidate = { ...acknowledgement };
|
|
2946
|
-
} else {
|
|
2947
|
-
this.failFrameRefresh(refreshAdmission, 'FRAME_STREAM_FAILED');
|
|
2957
|
+
const key = this.frameAcknowledgementKey(acknowledgement);
|
|
2958
|
+
for (const subscription of [...slot.frameSubscriptions.values()]) {
|
|
2959
|
+
if (subscription.closed) continue;
|
|
2960
|
+
while (subscription.outstanding.size >= this.options.maxOutstandingFrames) {
|
|
2961
|
+
const oldestKey = subscription.outstanding.keys().next().value!;
|
|
2962
|
+
this.trackCleanup(this.retireOutstandingFrame(subscription, oldestKey).then(() => undefined));
|
|
2948
2963
|
}
|
|
2964
|
+
const outstanding: IOutstandingFrameRecord = {
|
|
2965
|
+
acknowledgement, session, incarnationGeneration, producerAcknowledgement,
|
|
2966
|
+
};
|
|
2967
|
+
subscription.highestSequence = acknowledgement.sequence;
|
|
2968
|
+
subscription.outstanding.set(key, outstanding);
|
|
2969
|
+
this.armFrameAcknowledgementTimer(subscription, key, outstanding);
|
|
2970
|
+
this.pushToSubscription(subscription, { type: 'frame', frame: event.frame });
|
|
2949
2971
|
}
|
|
2950
2972
|
return;
|
|
2951
2973
|
}
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2974
|
+
const runtimeEvent: TBrowserRuntimeEvent = event.type === 'state'
|
|
2975
|
+
? { type: 'state', state: this.resourceState(event.state) }
|
|
2976
|
+
: {
|
|
2977
|
+
type: 'error',
|
|
2978
|
+
error: {
|
|
2979
|
+
code: truncateString(event.error.code, 128),
|
|
2980
|
+
fatal: event.error.fatal,
|
|
2981
|
+
message: truncateString(event.error.message, 2048),
|
|
2982
|
+
...(event.error.tabId ? { tabId: truncateString(event.error.tabId, 128) } : {}),
|
|
2983
|
+
},
|
|
2984
|
+
};
|
|
2985
|
+
if (event.type === 'error' && event.error.fatal) {
|
|
2986
|
+
const subscribers = [...slot.frameSubscriptions.values()].filter((subscription) => (
|
|
2987
|
+
!subscription.closed && !subscription.lease.released
|
|
2988
|
+
));
|
|
2989
|
+
// Publish the cleanup promise before synchronous revocation wakes queued callers.
|
|
2990
|
+
if (refresh) this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2991
|
+
const failure = this.handleFrameFailure(slot, session, incarnationGeneration);
|
|
2992
|
+
if (refresh) refresh.producerFailure = failure;
|
|
2993
|
+
for (const lease of slot.leases.values()) {
|
|
2994
|
+
if (lease.released) continue;
|
|
2995
|
+
this.invalidateCapabilityRecord(lease.capability);
|
|
2996
|
+
lease.released = true;
|
|
2997
|
+
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
2971
2998
|
}
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
&& !subscription.closed
|
|
2976
|
-
&& subscription.lease === fatalLease
|
|
2977
|
-
? subscription
|
|
2978
|
-
: undefined;
|
|
2979
|
-
if (refreshForSession?.lease === fatalLease) {
|
|
2980
|
-
this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
|
|
2981
|
-
}
|
|
2982
|
-
this.invalidateCapabilityRecord(fatalLease.capability);
|
|
2983
|
-
try {
|
|
2984
|
-
fatalSubscription?.listener(runtimeEvent);
|
|
2985
|
-
} catch {
|
|
2986
|
-
// Fatal revocation already owns cleanup for a failing listener.
|
|
2987
|
-
} finally {
|
|
2988
|
-
this.trackCleanup(this.revokeCapabilityRecord(fatalLease.capability));
|
|
2989
|
-
}
|
|
2990
|
-
return;
|
|
2999
|
+
slot.operation?.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3000
|
+
for (const subscription of subscribers) {
|
|
3001
|
+
try { subscription.listener(runtimeEvent); } catch { /* Producer failure owns cleanup. */ }
|
|
2991
3002
|
}
|
|
2992
|
-
|
|
2993
|
-
this.pushToSubscription(subscription, runtimeEvent);
|
|
3003
|
+
this.trackCleanup(failure.then(() => undefined));
|
|
2994
3004
|
return;
|
|
2995
3005
|
}
|
|
2996
|
-
|
|
2997
|
-
|
|
3006
|
+
for (const subscription of [...slot.frameSubscriptions.values()]) {
|
|
3007
|
+
this.pushToSubscription(subscription, runtimeEvent);
|
|
3008
|
+
}
|
|
2998
3009
|
}
|
|
2999
3010
|
|
|
3000
3011
|
private pushToSubscription(
|
|
3001
3012
|
subscription: IFrameSubscriptionRecord,
|
|
3002
3013
|
event: TBrowserRuntimeEvent,
|
|
3003
3014
|
): boolean {
|
|
3015
|
+
if (subscription.closed) return false;
|
|
3004
3016
|
try {
|
|
3005
3017
|
this.requireValidLease(subscription.lease);
|
|
3006
3018
|
subscription.listener(event);
|
|
3007
3019
|
return true;
|
|
3008
3020
|
} catch {
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
const refresh = session
|
|
3013
|
-
? this.matchingFrameRefresh(slot, subscription, session, incarnationGeneration)
|
|
3014
|
-
: undefined;
|
|
3015
|
-
if (event.type === 'frame') {
|
|
3016
|
-
const key = this.frameAcknowledgementKey(this.validateFrameAcknowledgement(event.frame));
|
|
3017
|
-
this.trackCleanup(this.retireOutstandingFrame(subscription, key, true).then(() => undefined));
|
|
3018
|
-
return false;
|
|
3019
|
-
}
|
|
3020
|
-
if (refresh) {
|
|
3021
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
3022
|
-
return false;
|
|
3023
|
-
}
|
|
3024
|
-
if (!session) {
|
|
3025
|
-
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
3026
|
-
return false;
|
|
3027
|
-
}
|
|
3028
|
-
this.trackCleanup((async () => {
|
|
3029
|
-
await this.options.beforeFrameFailureTermination?.();
|
|
3030
|
-
await this.handleFrameFailure(
|
|
3031
|
-
slot,
|
|
3032
|
-
session,
|
|
3033
|
-
incarnationGeneration,
|
|
3034
|
-
subscription.lease,
|
|
3035
|
-
);
|
|
3036
|
-
})());
|
|
3021
|
+
// A failed participant transport cannot revoke another participant or kill the producer.
|
|
3022
|
+
this.trackCleanup(this.closeFrameSubscription(subscription));
|
|
3023
|
+
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
3037
3024
|
return false;
|
|
3038
3025
|
}
|
|
3039
3026
|
}
|
|
@@ -3042,60 +3029,28 @@ export class BrowserRuntime {
|
|
|
3042
3029
|
if (subscription.closed) return;
|
|
3043
3030
|
subscription.closed = true;
|
|
3044
3031
|
const slot = subscription.lease.slot;
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3032
|
+
if (slot.frameRefresh?.subscription === subscription) {
|
|
3033
|
+
this.abandonFrameRefresh(slot, slot.frameRefresh);
|
|
3034
|
+
}
|
|
3035
|
+
if (slot.frameSubscriptions.get(subscription.lease.leaseId) === subscription) {
|
|
3036
|
+
slot.frameSubscriptions.delete(subscription.lease.leaseId);
|
|
3048
3037
|
}
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
await this.retireOutstandingFrame(subscription, key, false);
|
|
3038
|
+
for (const frame of subscription.outstanding.values()) {
|
|
3039
|
+
if (frame.timer) clearTimeout(frame.timer);
|
|
3052
3040
|
}
|
|
3041
|
+
subscription.outstanding.clear();
|
|
3053
3042
|
}
|
|
3054
3043
|
|
|
3055
3044
|
private async retireOutstandingFrame(
|
|
3056
3045
|
subscription: IFrameSubscriptionRecord,
|
|
3057
3046
|
key: string,
|
|
3058
|
-
failLease: boolean,
|
|
3059
3047
|
): Promise<boolean> {
|
|
3060
3048
|
const outstanding = subscription.outstanding.get(key);
|
|
3061
3049
|
if (!outstanding) return false;
|
|
3062
3050
|
subscription.outstanding.delete(key);
|
|
3063
3051
|
if (outstanding.timer) clearTimeout(outstanding.timer);
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
const refresh = this.matchingFrameRefresh(
|
|
3067
|
-
slot,
|
|
3068
|
-
subscription,
|
|
3069
|
-
outstanding.session,
|
|
3070
|
-
outstanding.incarnationGeneration,
|
|
3071
|
-
);
|
|
3072
|
-
if (refresh?.admissionClosed) {
|
|
3073
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
3074
|
-
return false;
|
|
3075
|
-
}
|
|
3076
|
-
if (refresh && failLease) this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
3077
|
-
const refreshOwned = Boolean(refresh && !refresh.abandoned);
|
|
3078
|
-
const outcome = await this.beginFrameAcknowledgement(
|
|
3079
|
-
outstanding.session,
|
|
3080
|
-
outstanding.acknowledgement,
|
|
3081
|
-
refreshOwned ? refresh : undefined,
|
|
3082
|
-
);
|
|
3083
|
-
if (outcome.status === 'fulfilled' && !failLease) return outcome.accepted;
|
|
3084
|
-
if (refreshOwned) return outcome.status === 'fulfilled' ? outcome.accepted : false;
|
|
3085
|
-
if (refresh && slot.frameRefresh === refresh) {
|
|
3086
|
-
if (outcome.status !== 'fulfilled') {
|
|
3087
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
3088
|
-
}
|
|
3089
|
-
return outcome.status === 'fulfilled' ? outcome.accepted : false;
|
|
3090
|
-
}
|
|
3091
|
-
await this.options.beforeFrameFailureTermination?.();
|
|
3092
|
-
await this.handleFrameFailure(
|
|
3093
|
-
slot,
|
|
3094
|
-
outstanding.session,
|
|
3095
|
-
outstanding.incarnationGeneration,
|
|
3096
|
-
subscription.lease,
|
|
3097
|
-
);
|
|
3098
|
-
return outcome.status === 'fulfilled' ? outcome.accepted : false;
|
|
3052
|
+
const outcome = await outstanding.producerAcknowledgement;
|
|
3053
|
+
return outcome.status === 'fulfilled' && outcome.accepted;
|
|
3099
3054
|
}
|
|
3100
3055
|
|
|
3101
3056
|
private validateFrameAcknowledgement(
|
|
@@ -3157,130 +3112,73 @@ export class BrowserRuntime {
|
|
|
3157
3112
|
}
|
|
3158
3113
|
}
|
|
3159
3114
|
|
|
3160
|
-
private async
|
|
3115
|
+
private async handleFrameFailure(
|
|
3161
3116
|
slot: IResourceSlot,
|
|
3162
3117
|
session: ILiveBrowserSessionLike,
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
): Promise<
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
const
|
|
3169
|
-
const
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
const refreshOwned = Boolean(refresh && !refresh.admissionClosed && !refresh.abandoned);
|
|
3174
|
-
const outcome = await this.beginFrameAcknowledgement(
|
|
3175
|
-
session,
|
|
3176
|
-
acknowledgement,
|
|
3177
|
-
refreshOwned ? refresh : undefined,
|
|
3178
|
-
);
|
|
3179
|
-
if (outcome.status === 'fulfilled' && !failLease) return;
|
|
3180
|
-
if (refreshOwned) return;
|
|
3181
|
-
if (refresh && slot.frameRefresh === refresh) {
|
|
3182
|
-
if (outcome.status !== 'fulfilled') {
|
|
3183
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
3184
|
-
}
|
|
3185
|
-
return;
|
|
3118
|
+
incarnationGeneration: number,
|
|
3119
|
+
expectedRefresh?: IFrameRefreshRecord,
|
|
3120
|
+
): Promise<boolean> {
|
|
3121
|
+
if (slot.session !== session || slot.incarnationGeneration !== incarnationGeneration) return false;
|
|
3122
|
+
if (slot.frameFailure?.session === session) return slot.frameFailure.promise;
|
|
3123
|
+
const promise = this.handleFrameFailureInternal(slot, session, incarnationGeneration, expectedRefresh);
|
|
3124
|
+
const failure = { session, promise };
|
|
3125
|
+
slot.frameFailure = failure;
|
|
3126
|
+
try { return await promise; } finally {
|
|
3127
|
+
if (slot.frameFailure === failure) slot.frameFailure = undefined;
|
|
3186
3128
|
}
|
|
3187
|
-
await this.options.beforeFrameFailureTermination?.();
|
|
3188
|
-
await this.handleFrameFailure(slot, session, incarnationGeneration, failedLease);
|
|
3189
3129
|
}
|
|
3190
3130
|
|
|
3191
|
-
private async
|
|
3131
|
+
private async handleFrameFailureInternal(
|
|
3192
3132
|
slot: IResourceSlot,
|
|
3193
3133
|
session: ILiveBrowserSessionLike,
|
|
3194
3134
|
incarnationGeneration: number,
|
|
3195
|
-
expectedLease: ILeaseRecord | null,
|
|
3196
3135
|
expectedRefresh?: IFrameRefreshRecord,
|
|
3197
3136
|
): Promise<boolean> {
|
|
3198
|
-
let transitionGeneration = 0;
|
|
3199
|
-
let ownsTransitionFence = false;
|
|
3200
|
-
const lease = expectedLease ?? undefined;
|
|
3201
3137
|
const release = await slot.mutex.acquire();
|
|
3138
|
+
let generation: number;
|
|
3139
|
+
let leases: ILeaseRecord[];
|
|
3202
3140
|
try {
|
|
3203
|
-
const exactLeaseIsCurrent = expectedLease === null
|
|
3204
|
-
? slot.lease === undefined
|
|
3205
|
-
: slot.lease === expectedLease && expectedLease.capability.lease === expectedLease;
|
|
3206
|
-
let leaseAttachmentIsCurrent = true;
|
|
3207
|
-
if (lease) {
|
|
3208
|
-
try {
|
|
3209
|
-
this.assertCapabilityStillAttached(lease.capability, slot);
|
|
3210
|
-
} catch {
|
|
3211
|
-
leaseAttachmentIsCurrent = false;
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
const borrowsLeaseReleaseFence = Boolean(
|
|
3215
|
-
lease
|
|
3216
|
-
&& exactLeaseIsCurrent
|
|
3217
|
-
&& lease.releaseGeneration !== undefined
|
|
3218
|
-
&& slot.fencingGeneration === lease.releaseGeneration,
|
|
3219
|
-
);
|
|
3220
3141
|
if (
|
|
3221
3142
|
slot.session !== session
|
|
3222
3143
|
|| slot.incarnationGeneration !== incarnationGeneration
|
|
3223
|
-
|| slot.permanentlyFenced
|
|
3224
|
-
|| slot.
|
|
3225
|
-
|| slot.
|
|
3226
|
-
|| slot.attachmentFenceCount > 0
|
|
3227
|
-
|| slot.attachmentRetryBinding
|
|
3228
|
-
|| !exactLeaseIsCurrent
|
|
3229
|
-
|| !leaseAttachmentIsCurrent
|
|
3230
|
-
|| (expectedRefresh !== undefined && slot.frameRefresh !== expectedRefresh)
|
|
3231
|
-
|| (slot.fencingGeneration !== undefined && !borrowsLeaseReleaseFence)
|
|
3144
|
+
|| slot.permanentlyFenced || slot.retirementPending
|
|
3145
|
+
|| slot.terminationFenceGeneration !== undefined || slot.fencingGeneration !== undefined
|
|
3146
|
+
|| (expectedRefresh && slot.frameRefresh !== expectedRefresh)
|
|
3232
3147
|
) return false;
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
if (borrowsLeaseReleaseFence) {
|
|
3240
|
-
transitionGeneration = slot.fencingGeneration!;
|
|
3241
|
-
} else {
|
|
3242
|
-
transitionGeneration = ++slot.arbitrationGeneration;
|
|
3243
|
-
slot.fencingGeneration = transitionGeneration;
|
|
3244
|
-
ownsTransitionFence = true;
|
|
3245
|
-
}
|
|
3246
|
-
if (lease) {
|
|
3148
|
+
generation = ++slot.arbitrationGeneration;
|
|
3149
|
+
slot.fencingGeneration = generation;
|
|
3150
|
+
this.abandonFrameRefresh(slot);
|
|
3151
|
+
leases = [...slot.leases.values()];
|
|
3152
|
+
for (const lease of leases) {
|
|
3153
|
+
this.invalidateCapabilityRecord(lease.capability);
|
|
3247
3154
|
lease.released = true;
|
|
3248
3155
|
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3156
|
+
this.notifyFramedLeaseEnded(lease);
|
|
3249
3157
|
}
|
|
3250
|
-
|
|
3251
|
-
slot.operation?.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3252
|
-
}
|
|
3158
|
+
slot.operation?.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3253
3159
|
} finally {
|
|
3254
3160
|
release();
|
|
3255
3161
|
}
|
|
3256
|
-
if (lease) this.trackFrameCapabilityRevocation(lease);
|
|
3257
3162
|
await this.options.afterFrameFailureFence?.();
|
|
3258
|
-
|
|
3163
|
+
await Promise.all(leases.map((lease) => this.closeFrameSubscriptionIfLease(lease)));
|
|
3259
3164
|
const errors: unknown[] = [];
|
|
3260
3165
|
await this.quiesceSlot(slot).catch((error) => errors.push(error));
|
|
3261
3166
|
await this.terminateSlotSession(slot, session).catch((error) => errors.push(error));
|
|
3262
3167
|
const finalRelease = await slot.mutex.acquire();
|
|
3263
3168
|
try {
|
|
3264
|
-
if (
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
&& slot.session !== session
|
|
3269
|
-
) {
|
|
3270
|
-
if (ownsTransitionFence && lease && slot.lease === lease) slot.lease = undefined;
|
|
3271
|
-
if (ownsTransitionFence && lease && lease.capability.lease === lease) {
|
|
3272
|
-
lease.capability.lease = undefined;
|
|
3169
|
+
if (slot.fencingGeneration === generation && errors.length === 0) {
|
|
3170
|
+
for (const lease of leases) {
|
|
3171
|
+
slot.leases.delete(lease.leaseId);
|
|
3172
|
+
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
3273
3173
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3174
|
+
slot.operation = undefined;
|
|
3175
|
+
slot.fencingGeneration = undefined;
|
|
3276
3176
|
}
|
|
3277
3177
|
} finally {
|
|
3278
3178
|
finalRelease();
|
|
3279
3179
|
}
|
|
3280
|
-
|
|
3281
|
-
if (errors.length
|
|
3282
|
-
throw new AggregateError(errors, 'Frame acknowledgement cleanup is incomplete');
|
|
3283
|
-
}
|
|
3180
|
+
for (const lease of leases) this.trackFrameCapabilityRevocation(lease);
|
|
3181
|
+
if (errors.length) throw new AggregateError(errors, 'Frame producer cleanup is incomplete');
|
|
3284
3182
|
return true;
|
|
3285
3183
|
}
|
|
3286
3184
|
|
|
@@ -3293,6 +3191,14 @@ export class BrowserRuntime {
|
|
|
3293
3191
|
this.trackCleanup(operation);
|
|
3294
3192
|
}
|
|
3295
3193
|
|
|
3194
|
+
private requireQueuedLease(lease: ILeaseRecord, participantCleanup = false): void {
|
|
3195
|
+
if (!participantCleanup) return this.requireValidLease(lease);
|
|
3196
|
+
if (
|
|
3197
|
+
!lease.released || lease.slot.leases.get(lease.leaseId) !== lease
|
|
3198
|
+
|| lease.capability.lease !== lease || !lease.slot.session
|
|
3199
|
+
) throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3296
3202
|
private requireValidLease(lease: ILeaseRecord): void {
|
|
3297
3203
|
if (lease.released || lease.controller.signal.aborted) {
|
|
3298
3204
|
throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
@@ -3304,7 +3210,7 @@ export class BrowserRuntime {
|
|
|
3304
3210
|
if (
|
|
3305
3211
|
capability !== lease.capability
|
|
3306
3212
|
|| capability.lease !== lease
|
|
3307
|
-
|| lease.slot.lease !== lease
|
|
3213
|
+
|| lease.slot.leases.get(lease.leaseId) !== lease
|
|
3308
3214
|
|| !lease.slot.session
|
|
3309
3215
|
) {
|
|
3310
3216
|
throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
@@ -3427,57 +3333,58 @@ export class BrowserRuntime {
|
|
|
3427
3333
|
}
|
|
3428
3334
|
|
|
3429
3335
|
private async releaseLeaseRecordInternal(lease: ILeaseRecord): Promise<void> {
|
|
3430
|
-
if (lease.released && lease.slot.lease !== lease) return;
|
|
3431
3336
|
const slot = lease.slot;
|
|
3337
|
+
if (slot.leases.get(lease.leaseId) !== lease) return;
|
|
3432
3338
|
const release = await slot.mutex.acquire();
|
|
3433
|
-
|
|
3434
|
-
const generation = lease.releaseGeneration
|
|
3435
|
-
?? existingFence
|
|
3436
|
-
?? ++slot.arbitrationGeneration;
|
|
3437
|
-
lease.releaseGeneration = generation;
|
|
3438
|
-
lease.releaseOwnsFence ??= existingFence === undefined;
|
|
3339
|
+
let operation: IOperationRecord | undefined;
|
|
3439
3340
|
try {
|
|
3440
|
-
if (existingFence !== undefined) {
|
|
3441
|
-
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3442
|
-
} else {
|
|
3443
|
-
slot.fencingGeneration = generation;
|
|
3444
|
-
}
|
|
3445
3341
|
lease.released = true;
|
|
3446
3342
|
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
}
|
|
3343
|
+
operation = slot.operation?.lease === lease ? slot.operation : undefined;
|
|
3344
|
+
operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3450
3345
|
} finally {
|
|
3451
3346
|
release();
|
|
3452
3347
|
}
|
|
3453
3348
|
await this.closeFrameSubscriptionIfLease(lease);
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3349
|
+
if (operation) {
|
|
3350
|
+
const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
3351
|
+
if (!settled.settled) {
|
|
3352
|
+
// An unquiescent operation belongs to the browser process, so its incarnation must end.
|
|
3353
|
+
await this.terminateRegisteredResourceInternal(slot, false, {
|
|
3354
|
+
session: operation.session, incarnationGeneration: operation.incarnationGeneration,
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3457
3357
|
}
|
|
3358
|
+
if (slot.frameRefresh?.lease === lease) this.abandonFrameRefresh(slot, slot.frameRefresh);
|
|
3458
3359
|
if (
|
|
3459
|
-
slot.permanentlyFenced
|
|
3460
|
-
&&
|
|
3461
|
-
|
|
3360
|
+
this.lifecycleState === 'running' && slot.session && !slot.permanentlyFenced && !slot.retirementPending
|
|
3361
|
+
&& slot.fencingGeneration === undefined && slot.terminationFenceGeneration === undefined
|
|
3362
|
+
&& (lease.preferredViewport || lease.heldKeys.size || lease.heldButtons.size)
|
|
3363
|
+
) {
|
|
3364
|
+
await this.runOperation(lease, 'releaseParticipant', 'viewport', {},
|
|
3365
|
+
async (signal, session) => {
|
|
3366
|
+
await this.releaseParticipantInput(lease, session);
|
|
3367
|
+
await this.applySharedViewport(slot, session, signal);
|
|
3368
|
+
lease.preferredViewport = undefined;
|
|
3369
|
+
}, undefined, true);
|
|
3370
|
+
}
|
|
3371
|
+
if (slot.permanentlyFenced && (slot.session || slot.proxy || slot.profileDirectory)) {
|
|
3372
|
+
await this.terminateSlotSession(slot);
|
|
3373
|
+
}
|
|
3462
3374
|
const finalRelease = await slot.mutex.acquire();
|
|
3463
3375
|
try {
|
|
3464
3376
|
if (lease.capability.state === 'active') this.notifyFramedLeaseEnded(lease);
|
|
3465
|
-
|
|
3377
|
+
slot.leases.delete(lease.leaseId);
|
|
3466
3378
|
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
3467
|
-
if (
|
|
3468
|
-
slot.fencingGeneration = undefined;
|
|
3469
|
-
}
|
|
3470
|
-
lease.releaseGeneration = undefined;
|
|
3471
|
-
lease.releaseOwnsFence = undefined;
|
|
3472
|
-
this.scheduleIdleTermination(slot);
|
|
3379
|
+
if (slot.leases.size === 0) this.scheduleIdleTermination(slot);
|
|
3473
3380
|
} finally {
|
|
3474
3381
|
finalRelease();
|
|
3475
3382
|
}
|
|
3476
3383
|
}
|
|
3477
3384
|
|
|
3478
3385
|
private async closeFrameSubscriptionIfLease(lease: ILeaseRecord): Promise<void> {
|
|
3479
|
-
const subscription = lease.slot.
|
|
3480
|
-
if (subscription
|
|
3386
|
+
const subscription = lease.slot.frameSubscriptions.get(lease.leaseId);
|
|
3387
|
+
if (subscription) await this.closeFrameSubscription(subscription);
|
|
3481
3388
|
}
|
|
3482
3389
|
|
|
3483
3390
|
private async quiesceSlot(slot: IResourceSlot): Promise<void> {
|
|
@@ -3513,6 +3420,12 @@ export class BrowserRuntime {
|
|
|
3513
3420
|
return;
|
|
3514
3421
|
}
|
|
3515
3422
|
const slot = operation.lease.slot;
|
|
3423
|
+
if (slot.operation === operation && slot.session === operation.session
|
|
3424
|
+
&& slot.incarnationGeneration === operation.incarnationGeneration) {
|
|
3425
|
+
await this.terminateRegisteredResourceInternal(slot, false, {
|
|
3426
|
+
session: operation.session, incarnationGeneration: operation.incarnationGeneration,
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3516
3429
|
const release = await slot.mutex.acquire();
|
|
3517
3430
|
try {
|
|
3518
3431
|
if (
|
|
@@ -3565,6 +3478,8 @@ export class BrowserRuntime {
|
|
|
3565
3478
|
slot.unsubscribeSession?.();
|
|
3566
3479
|
slot.unsubscribeSession = undefined;
|
|
3567
3480
|
slot.session = undefined;
|
|
3481
|
+
slot.highestFrameSequence = 0;
|
|
3482
|
+
slot.producerAcknowledgements.clear();
|
|
3568
3483
|
}
|
|
3569
3484
|
if (slot.proxy) {
|
|
3570
3485
|
await this.stopProxyBounded(slot.proxy).then(
|
|
@@ -3582,7 +3497,7 @@ export class BrowserRuntime {
|
|
|
3582
3497
|
}
|
|
3583
3498
|
|
|
3584
3499
|
private scheduleIdleTermination(slot: IResourceSlot): void {
|
|
3585
|
-
if (slot.terminationFenceGeneration !== undefined || slot.retirementPending) return;
|
|
3500
|
+
if (slot.leases.size > 0 || slot.terminationFenceGeneration !== undefined || slot.retirementPending) return;
|
|
3586
3501
|
if (slot.idleTimer) clearTimeout(slot.idleTimer);
|
|
3587
3502
|
const arbitrationGeneration = slot.arbitrationGeneration;
|
|
3588
3503
|
const incarnationGeneration = slot.incarnationGeneration;
|
|
@@ -3626,7 +3541,7 @@ export class BrowserRuntime {
|
|
|
3626
3541
|
): Promise<void> {
|
|
3627
3542
|
const release = await slot.mutex.acquire();
|
|
3628
3543
|
let generation = 0;
|
|
3629
|
-
let
|
|
3544
|
+
let leases: ILeaseRecord[];
|
|
3630
3545
|
try {
|
|
3631
3546
|
if (
|
|
3632
3547
|
(expected?.arbitrationGeneration !== undefined
|
|
@@ -3634,48 +3549,34 @@ export class BrowserRuntime {
|
|
|
3634
3549
|
|| (expected?.incarnationGeneration !== undefined
|
|
3635
3550
|
&& slot.incarnationGeneration !== expected.incarnationGeneration)
|
|
3636
3551
|
|| (expected?.session !== undefined && slot.session !== expected.session)
|
|
3637
|
-
|| (expected?.requireIdle && slot.
|
|
3552
|
+
|| (expected?.requireIdle && slot.leases.size > 0)
|
|
3638
3553
|
) return;
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
if (slot.lease) {
|
|
3649
|
-
slot.lease.released = true;
|
|
3650
|
-
slot.lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3651
|
-
if (!stopping && !attachmentFenceOwnsTransition) {
|
|
3652
|
-
this.notifyFramedLeaseEnded(slot.lease);
|
|
3653
|
-
}
|
|
3554
|
+
generation = ++slot.arbitrationGeneration;
|
|
3555
|
+
slot.fencingGeneration = generation;
|
|
3556
|
+
if (slot.idleTimer) clearTimeout(slot.idleTimer);
|
|
3557
|
+
slot.idleTimer = undefined;
|
|
3558
|
+
leases = [...slot.leases.values()];
|
|
3559
|
+
for (const lease of leases) {
|
|
3560
|
+
lease.released = true;
|
|
3561
|
+
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3562
|
+
if (!stopping) this.notifyFramedLeaseEnded(lease);
|
|
3654
3563
|
}
|
|
3655
3564
|
slot.operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3656
3565
|
} finally {
|
|
3657
3566
|
release();
|
|
3658
3567
|
}
|
|
3659
|
-
if (attachmentFenceOwnsTransition) {
|
|
3660
|
-
await this.quiesceSlot(slot);
|
|
3661
|
-
if (slot.lease) await this.closeFrameSubscriptionIfLease(slot.lease);
|
|
3662
|
-
await this.terminateSlotSession(slot, expected?.session);
|
|
3663
|
-
return;
|
|
3664
|
-
}
|
|
3665
|
-
if (generation === 0) return;
|
|
3666
3568
|
await this.quiesceSlot(slot);
|
|
3667
|
-
|
|
3569
|
+
await Promise.all(leases.map((lease) => this.closeFrameSubscriptionIfLease(lease)));
|
|
3668
3570
|
await this.terminateSlotSession(slot, expected?.session);
|
|
3669
3571
|
const finalRelease = await slot.mutex.acquire();
|
|
3670
3572
|
try {
|
|
3671
3573
|
if (slot.arbitrationGeneration !== generation || slot.fencingGeneration !== generation) {
|
|
3672
3574
|
throw new BrowserRuntimeError('BUSY');
|
|
3673
3575
|
}
|
|
3674
|
-
const lease
|
|
3675
|
-
|
|
3676
|
-
|
|
3576
|
+
for (const lease of leases) {
|
|
3577
|
+
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
3578
|
+
slot.leases.delete(lease.leaseId);
|
|
3677
3579
|
}
|
|
3678
|
-
slot.lease = undefined;
|
|
3679
3580
|
slot.operation = undefined;
|
|
3680
3581
|
slot.fencingGeneration = undefined;
|
|
3681
3582
|
} finally {
|
|
@@ -3719,7 +3620,6 @@ export class BrowserRuntime {
|
|
|
3719
3620
|
if (slot.retirementPending) throw new BrowserRuntimeError('RESOURCE_RETIRED');
|
|
3720
3621
|
if (slot.permanentlyFenced) throw new BrowserRuntimeError('FENCED');
|
|
3721
3622
|
if (slot.terminationFenceGeneration !== undefined) throw new BrowserRuntimeError('BUSY');
|
|
3722
|
-
if (slot.attachmentFenceCount > 0) throw new BrowserRuntimeError('BUSY');
|
|
3723
3623
|
if (slot.fencingGeneration !== undefined) throw new BrowserRuntimeError('BUSY');
|
|
3724
3624
|
}
|
|
3725
3625
|
|
|
@@ -3762,54 +3662,45 @@ export class BrowserRuntime {
|
|
|
3762
3662
|
if (binding.attachmentRevision < current.attachmentRevision) {
|
|
3763
3663
|
throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3764
3664
|
}
|
|
3765
|
-
if (
|
|
3766
|
-
binding
|
|
3767
|
-
&& !slot.attachmentRetryBinding
|
|
3768
|
-
) {
|
|
3769
|
-
if (!this.bindingsEqual(binding, current)) {
|
|
3770
|
-
throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3771
|
-
}
|
|
3665
|
+
if (binding.attachmentRevision === current.attachmentRevision && !slot.attachmentRetryBinding) {
|
|
3666
|
+
if (!this.bindingsEqual(binding, current)) throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3772
3667
|
return this.describeResource(slot);
|
|
3773
3668
|
}
|
|
3774
|
-
if (
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
) throw new BrowserRuntimeError('BUSY');
|
|
3669
|
+
if (slot.attachmentRetryBinding && !this.bindingsEqual(binding, slot.attachmentRetryBinding)) {
|
|
3670
|
+
throw new BrowserRuntimeError('BUSY');
|
|
3671
|
+
}
|
|
3778
3672
|
const release = await slot.mutex.acquire();
|
|
3779
|
-
let fenceGeneration = 0;
|
|
3780
3673
|
try {
|
|
3781
|
-
fenceGeneration = ++slot.arbitrationGeneration;
|
|
3782
|
-
slot.fencingGeneration = fenceGeneration;
|
|
3783
3674
|
slot.attachmentBinding = binding;
|
|
3784
3675
|
slot.attachmentRetryBinding = binding;
|
|
3785
|
-
slot.
|
|
3786
|
-
|
|
3676
|
+
for (const lease of slot.leases.values()) {
|
|
3677
|
+
if (lease.role === 'agent') {
|
|
3678
|
+
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
if (slot.operation?.lease.role === 'agent') {
|
|
3682
|
+
slot.operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3683
|
+
}
|
|
3787
3684
|
} finally {
|
|
3788
3685
|
release();
|
|
3789
3686
|
}
|
|
3790
3687
|
const peerResults = await Promise.allSettled(
|
|
3791
3688
|
this.framedPeersForResource(slot).map((peer) => peer.close()),
|
|
3792
3689
|
);
|
|
3793
|
-
const capabilities = this.capabilitiesForResource(slot);
|
|
3794
3690
|
const capabilityResults = await Promise.allSettled(
|
|
3795
|
-
|
|
3691
|
+
this.capabilitiesForResource(slot).filter((record) => record.role === 'agent')
|
|
3692
|
+
.map((record) => this.revokeCapabilityRecord(record)),
|
|
3796
3693
|
);
|
|
3797
3694
|
const errors = [...peerResults, ...capabilityResults]
|
|
3798
3695
|
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
3799
3696
|
.map((result) => result.reason);
|
|
3800
|
-
await this.quiesceSlot(slot).catch((error) => errors.push(error));
|
|
3801
3697
|
const finalRelease = await slot.mutex.acquire();
|
|
3802
3698
|
try {
|
|
3803
|
-
if (
|
|
3804
|
-
slot.fencingGeneration = undefined;
|
|
3805
|
-
slot.attachmentRetryBinding = undefined;
|
|
3806
|
-
}
|
|
3699
|
+
if (errors.length === 0) slot.attachmentRetryBinding = undefined;
|
|
3807
3700
|
} finally {
|
|
3808
3701
|
finalRelease();
|
|
3809
3702
|
}
|
|
3810
|
-
if (errors.length
|
|
3811
|
-
throw new AggregateError(errors, 'Attachment fencing is incomplete');
|
|
3812
|
-
}
|
|
3703
|
+
if (errors.length) throw new AggregateError(errors, 'Attachment fencing is incomplete');
|
|
3813
3704
|
return this.describeResource(slot);
|
|
3814
3705
|
}
|
|
3815
3706
|
|
|
@@ -3979,6 +3870,9 @@ export class BrowserRuntime {
|
|
|
3979
3870
|
|| binding.attachmentRevision !== expected.attachmentRevision
|
|
3980
3871
|
) throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3981
3872
|
if (expected.role === 'agent') {
|
|
3873
|
+
if (slot.attachmentFenceCount > 0 || slot.attachmentRetryBinding) {
|
|
3874
|
+
throw new BrowserRuntimeError('BUSY');
|
|
3875
|
+
}
|
|
3982
3876
|
if (!binding.sessionId || !this.sessionsEqual(binding.sessionId, expected.sessionId)) {
|
|
3983
3877
|
throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3984
3878
|
}
|
|
@@ -3995,6 +3889,7 @@ export class BrowserRuntime {
|
|
|
3995
3889
|
capability.projectId !== slot.projectId
|
|
3996
3890
|
|| capability.browserResourceId !== slot.browserResourceId
|
|
3997
3891
|
) throw new BrowserRuntimeError('CAPABILITY_INVALID');
|
|
3892
|
+
if (capability.role === 'human') return;
|
|
3998
3893
|
try {
|
|
3999
3894
|
this.assertCapabilityBinding(slot, capability);
|
|
4000
3895
|
} catch {
|
|
@@ -4027,7 +3922,7 @@ export class BrowserRuntime {
|
|
|
4027
3922
|
|| sessionId?.harnessId !== 'flex'))
|
|
4028
3923
|
|| (source === 'mcp' && (scopeId !== undefined || channelId !== undefined
|
|
4029
3924
|
|| runId !== undefined
|
|
4030
|
-
|| sessionId
|
|
3925
|
+
|| sessionId === undefined))
|
|
4031
3926
|
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
4032
3927
|
|| runId !== undefined))
|
|
4033
3928
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -4061,7 +3956,8 @@ export class BrowserRuntime {
|
|
|
4061
3956
|
runId: runId!,
|
|
4062
3957
|
};
|
|
4063
3958
|
}
|
|
4064
|
-
if (role === 'agent' && source === 'mcp'
|
|
3959
|
+
if (role === 'agent' && source === 'mcp'
|
|
3960
|
+
&& sessionId !== undefined) {
|
|
4065
3961
|
return { ...base, role, source, sessionId };
|
|
4066
3962
|
}
|
|
4067
3963
|
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -4099,7 +3995,7 @@ export class BrowserRuntime {
|
|
|
4099
3995
|
if (
|
|
4100
3996
|
capability.role === 'agent'
|
|
4101
3997
|
&& capability.source === 'mcp'
|
|
4102
|
-
&& capability.sessionId
|
|
3998
|
+
&& capability.sessionId !== undefined
|
|
4103
3999
|
) {
|
|
4104
4000
|
return {
|
|
4105
4001
|
...base,
|
|
@@ -4305,7 +4201,7 @@ export class BrowserRuntimeLease {
|
|
|
4305
4201
|
public setViewport(
|
|
4306
4202
|
viewport: plugins.smartpuppeteer.ILiveBrowserViewport,
|
|
4307
4203
|
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
4308
|
-
): Promise<
|
|
4204
|
+
): Promise<IBrowserRuntimeViewportResult> {
|
|
4309
4205
|
return this.runtime.leaseSetViewport(this.record, viewport, operationOptions);
|
|
4310
4206
|
}
|
|
4311
4207
|
|