@modelprofile.com/browser-runtime 4.4.0 → 5.1.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 +33 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.artifactstore.d.ts +4 -3
- package/dist_ts/classes.artifactstore.js +19 -5
- package/dist_ts/classes.runtime.d.ts +32 -11
- package/dist_ts/classes.runtime.js +509 -575
- 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 +14 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.artifactstore.ts +19 -1
- package/ts/classes.runtime.ts +545 -641
- 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 */
|
|
@@ -1482,23 +1649,30 @@ export class BrowserRuntime {
|
|
|
1482
1649
|
lease: ILeaseRecord,
|
|
1483
1650
|
artifactId: string,
|
|
1484
1651
|
): Promise<Uint8Array> {
|
|
1485
|
-
this.requireHuman(lease);
|
|
1486
1652
|
this.requireValidLease(lease);
|
|
1487
|
-
|
|
1653
|
+
const bytes = await this.requireArtifactStore().read(
|
|
1488
1654
|
lease.capability.projectId,
|
|
1489
1655
|
lease.capability.browserResourceId,
|
|
1490
1656
|
artifactId,
|
|
1657
|
+
lease.role === 'agent' ? lease.leaseId : undefined,
|
|
1491
1658
|
);
|
|
1659
|
+
try {
|
|
1660
|
+
this.requireValidLease(lease);
|
|
1661
|
+
return bytes;
|
|
1662
|
+
} catch (errorArg) {
|
|
1663
|
+
bytes.fill(0);
|
|
1664
|
+
throw errorArg;
|
|
1665
|
+
}
|
|
1492
1666
|
}
|
|
1493
1667
|
|
|
1494
1668
|
/** @internal */
|
|
1495
1669
|
public async deleteLeaseArtifact(lease: ILeaseRecord, artifactId: string): Promise<void> {
|
|
1496
|
-
this.requireHuman(lease);
|
|
1497
1670
|
this.requireValidLease(lease);
|
|
1498
1671
|
await this.requireArtifactStore().delete(
|
|
1499
1672
|
lease.capability.projectId,
|
|
1500
1673
|
lease.capability.browserResourceId,
|
|
1501
1674
|
artifactId,
|
|
1675
|
+
lease.role === 'agent' ? lease.leaseId : undefined,
|
|
1502
1676
|
);
|
|
1503
1677
|
}
|
|
1504
1678
|
|
|
@@ -1658,19 +1832,21 @@ export class BrowserRuntime {
|
|
|
1658
1832
|
}
|
|
1659
1833
|
}
|
|
1660
1834
|
|
|
1661
|
-
private async
|
|
1835
|
+
private async acquireParticipantLease(
|
|
1662
1836
|
slot: IResourceSlot,
|
|
1663
1837
|
capability: TCapabilityRecord,
|
|
1664
1838
|
digest: plugins.Buffer,
|
|
1665
1839
|
acquisition: TAcquireBrowserRuntimeLeaseOptions,
|
|
1666
1840
|
): Promise<BrowserRuntimeLease> {
|
|
1667
|
-
|
|
1668
|
-
if (
|
|
1841
|
+
this.assertSlotAvailable(slot);
|
|
1842
|
+
if (capability.lease || capability.acquiring) throw new BrowserRuntimeError('BUSY');
|
|
1843
|
+
capability.acquiring = true;
|
|
1844
|
+
let release: (() => void) | undefined;
|
|
1669
1845
|
try {
|
|
1846
|
+
release = await slot.mutex.acquire();
|
|
1670
1847
|
this.assertSlotAvailable(slot);
|
|
1671
1848
|
this.resolveCapability(digest, this.validateExpectedCapabilityBinding(acquisition));
|
|
1672
1849
|
this.assertCapabilityStillAttached(capability, slot);
|
|
1673
|
-
if (slot.lease || slot.operation) throw new BrowserRuntimeError('BUSY');
|
|
1674
1850
|
await this.ensureSession(slot, acquisition.signal);
|
|
1675
1851
|
await this.options.beforeLeasePublication?.();
|
|
1676
1852
|
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
@@ -1680,84 +1856,11 @@ export class BrowserRuntime {
|
|
|
1680
1856
|
const lease = this.createLeaseRecord(slot, capability, digest);
|
|
1681
1857
|
return new BrowserRuntimeLease(this, lease);
|
|
1682
1858
|
} catch (error) {
|
|
1683
|
-
if (slot.session &&
|
|
1859
|
+
if (slot.session && slot.leases.size === 0) this.scheduleIdleTermination(slot);
|
|
1684
1860
|
throw error;
|
|
1685
1861
|
} 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;
|
|
1862
|
+
capability.acquiring = false;
|
|
1863
|
+
release?.();
|
|
1761
1864
|
}
|
|
1762
1865
|
}
|
|
1763
1866
|
|
|
@@ -1768,7 +1871,7 @@ export class BrowserRuntime {
|
|
|
1768
1871
|
): ILeaseRecord {
|
|
1769
1872
|
this.assertSlotAvailable(slot);
|
|
1770
1873
|
this.assertCapabilityStillAttached(capability, slot);
|
|
1771
|
-
if (capability.lease
|
|
1874
|
+
if (capability.lease) throw new BrowserRuntimeError('BUSY');
|
|
1772
1875
|
if (slot.idleTimer) {
|
|
1773
1876
|
clearTimeout(slot.idleTimer);
|
|
1774
1877
|
slot.idleTimer = undefined;
|
|
@@ -1788,8 +1891,10 @@ export class BrowserRuntime {
|
|
|
1788
1891
|
controller: new AbortController(),
|
|
1789
1892
|
authorityGeneration: slot.authorityGeneration,
|
|
1790
1893
|
released: false,
|
|
1894
|
+
heldKeys: new Map(),
|
|
1895
|
+
heldButtons: new Map(),
|
|
1791
1896
|
};
|
|
1792
|
-
slot.lease
|
|
1897
|
+
slot.leases.set(lease.leaseId, lease);
|
|
1793
1898
|
capability.lease = lease;
|
|
1794
1899
|
return lease;
|
|
1795
1900
|
}
|
|
@@ -1954,6 +2059,7 @@ export class BrowserRuntime {
|
|
|
1954
2059
|
operationOptions: IBrowserRuntimeOperationOptions,
|
|
1955
2060
|
execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<T>,
|
|
1956
2061
|
finalize?: () => Promise<void>,
|
|
2062
|
+
participantCleanup = false,
|
|
1957
2063
|
): Promise<T> {
|
|
1958
2064
|
if (!operationOptions || typeof operationOptions !== 'object') {
|
|
1959
2065
|
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -1968,16 +2074,21 @@ export class BrowserRuntime {
|
|
|
1968
2074
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1969
2075
|
if (operationOptions.signal?.aborted) throw new BrowserRuntimeError('ABORTED');
|
|
1970
2076
|
const slot = lease.slot;
|
|
1971
|
-
this.
|
|
2077
|
+
this.requireQueuedLease(lease, participantCleanup);
|
|
1972
2078
|
this.assertSlotAvailable(slot);
|
|
1973
|
-
if (
|
|
2079
|
+
if (!participantCleanup && (
|
|
2080
|
+
slot.operationQueue.filter((entry) => entry.lease === lease).length
|
|
2081
|
+
>= this.options.maxQueuedOperationsPerLease
|
|
2082
|
+
|| slot.operationQueue.length >= this.options.maxQueuedOperationsPerLease
|
|
2083
|
+
* this.options.maxCapabilitiesPerResource
|
|
2084
|
+
)) {
|
|
1974
2085
|
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
1975
2086
|
}
|
|
1976
2087
|
const timeoutMs = operationOptions.timeoutMs === undefined
|
|
1977
2088
|
? this.options.operationTimeoutMs
|
|
1978
2089
|
: validateInteger(operationOptions.timeoutMs, 'timeoutMs', 100, 120_000);
|
|
1979
2090
|
const signal = AbortSignal.any([
|
|
1980
|
-
lease.controller.signal,
|
|
2091
|
+
...(participantCleanup ? [] : [lease.controller.signal]),
|
|
1981
2092
|
this.lifecycleController.signal,
|
|
1982
2093
|
operationOptions.signal ?? new AbortController().signal,
|
|
1983
2094
|
]);
|
|
@@ -1996,6 +2107,7 @@ export class BrowserRuntime {
|
|
|
1996
2107
|
resolve: (value) => resolve(value as T),
|
|
1997
2108
|
reject,
|
|
1998
2109
|
state: 'queued',
|
|
2110
|
+
participantCleanup,
|
|
1999
2111
|
};
|
|
2000
2112
|
const onQueuedAbort = (): void => {
|
|
2001
2113
|
if (queued.state !== 'queued' && queued.state !== 'starting') return;
|
|
@@ -2005,7 +2117,15 @@ export class BrowserRuntime {
|
|
|
2005
2117
|
}
|
|
2006
2118
|
queued.state = 'settled';
|
|
2007
2119
|
signal.removeEventListener('abort', onQueuedAbort);
|
|
2008
|
-
|
|
2120
|
+
const error = this.queuedOperationAbortError(queued);
|
|
2121
|
+
const frameFailure = slot.frameFailure;
|
|
2122
|
+
if (frameFailure && frameFailure.session === slot.session) {
|
|
2123
|
+
// Stop admission immediately, but expose the terminal result only
|
|
2124
|
+
// after cleanup of the failed producer's incarnation has settled.
|
|
2125
|
+
void frameFailure.promise.then(() => reject(error), reject);
|
|
2126
|
+
} else {
|
|
2127
|
+
reject(error);
|
|
2128
|
+
}
|
|
2009
2129
|
};
|
|
2010
2130
|
queued.onQueuedAbort = onQueuedAbort;
|
|
2011
2131
|
signal.addEventListener('abort', onQueuedAbort, { once: true });
|
|
@@ -2069,7 +2189,7 @@ export class BrowserRuntime {
|
|
|
2069
2189
|
try {
|
|
2070
2190
|
if (queued.state === 'settled') return undefined;
|
|
2071
2191
|
queued.signal.throwIfAborted();
|
|
2072
|
-
this.
|
|
2192
|
+
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2073
2193
|
this.assertSlotAvailable(slot);
|
|
2074
2194
|
if (slot.operation) throw new BrowserRuntimeError('BUSY');
|
|
2075
2195
|
session = slot.session!;
|
|
@@ -2130,13 +2250,12 @@ export class BrowserRuntime {
|
|
|
2130
2250
|
let executionStarted = false;
|
|
2131
2251
|
let executionSettled = false;
|
|
2132
2252
|
try {
|
|
2133
|
-
await this.runBeforeOperation(operationIdentity, combinedSignal);
|
|
2253
|
+
if (!queued.participantCleanup) await this.runBeforeOperation(operationIdentity, combinedSignal);
|
|
2134
2254
|
const executionRelease = await slot.mutex.acquire();
|
|
2135
2255
|
try {
|
|
2136
2256
|
combinedSignal.throwIfAborted();
|
|
2137
|
-
this.
|
|
2257
|
+
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2138
2258
|
this.assertSlotAvailable(slot);
|
|
2139
|
-
this.assertCapabilityStillAttached(lease.capability, slot);
|
|
2140
2259
|
if (
|
|
2141
2260
|
slot.operation !== operation
|
|
2142
2261
|
|| slot.session !== session
|
|
@@ -2144,7 +2263,13 @@ export class BrowserRuntime {
|
|
|
2144
2263
|
|| slot.incarnationGeneration !== incarnationGeneration
|
|
2145
2264
|
) throw new BrowserRuntimeError('ABORTED');
|
|
2146
2265
|
executionStarted = true;
|
|
2147
|
-
executionPromise =
|
|
2266
|
+
executionPromise = (async () => {
|
|
2267
|
+
if (classification === 'navigation' || classification === 'tab') {
|
|
2268
|
+
await this.releaseAllParticipantInput(slot, session);
|
|
2269
|
+
}
|
|
2270
|
+
combinedSignal.throwIfAborted();
|
|
2271
|
+
return queued.execute(combinedSignal, session);
|
|
2272
|
+
})();
|
|
2148
2273
|
operation.promise = executionPromise.then(() => undefined, () => undefined).finally(() => {
|
|
2149
2274
|
executionSettled = true;
|
|
2150
2275
|
});
|
|
@@ -2162,8 +2287,7 @@ export class BrowserRuntime {
|
|
|
2162
2287
|
combinedSignal.removeEventListener('abort', onAbort);
|
|
2163
2288
|
}
|
|
2164
2289
|
if (combinedSignal.aborted) throw combinedSignal.reason;
|
|
2165
|
-
this.
|
|
2166
|
-
this.assertCapabilityStillAttached(lease.capability, slot);
|
|
2290
|
+
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2167
2291
|
if (slot.session !== session! || slot.arbitrationGeneration !== generation!) {
|
|
2168
2292
|
throw new BrowserRuntimeError('ABORTED');
|
|
2169
2293
|
}
|
|
@@ -2205,6 +2329,12 @@ export class BrowserRuntime {
|
|
|
2205
2329
|
failure = error;
|
|
2206
2330
|
}
|
|
2207
2331
|
}
|
|
2332
|
+
if (slot.frameFailure?.session === session) {
|
|
2333
|
+
try { await slot.frameFailure.promise; } catch (error) {
|
|
2334
|
+
failed = true;
|
|
2335
|
+
failure = error;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2208
2338
|
const errorCode = failed ? this.operationErrorCode(failure) : undefined;
|
|
2209
2339
|
await this.emitAudit(Object.freeze({
|
|
2210
2340
|
...operationIdentity,
|
|
@@ -2441,6 +2571,7 @@ export class BrowserRuntime {
|
|
|
2441
2571
|
lease.capability.browserResourceId,
|
|
2442
2572
|
snapshot.mimeType,
|
|
2443
2573
|
snapshot.data,
|
|
2574
|
+
lease.leaseId,
|
|
2444
2575
|
);
|
|
2445
2576
|
return { action: 'screenshot', artifact } satisfies IBrowserScreenshotResult;
|
|
2446
2577
|
} finally {
|
|
@@ -2532,10 +2663,10 @@ export class BrowserRuntime {
|
|
|
2532
2663
|
): IFrameRefreshRecord {
|
|
2533
2664
|
if (
|
|
2534
2665
|
slot.frameRefresh
|
|
2535
|
-
|| slot.
|
|
2666
|
+
|| slot.frameSubscriptions.get(lease.leaseId) !== subscription
|
|
2536
2667
|
|| subscription.closed
|
|
2537
2668
|
|| subscription.lease !== lease
|
|
2538
|
-
|| slot.lease !== lease
|
|
2669
|
+
|| slot.leases.get(lease.leaseId) !== lease
|
|
2539
2670
|
|| slot.session !== session
|
|
2540
2671
|
) throw new BrowserRuntimeError('BUSY');
|
|
2541
2672
|
const state = session.getState();
|
|
@@ -2544,7 +2675,7 @@ export class BrowserRuntime {
|
|
|
2544
2675
|
!activeTab
|
|
2545
2676
|
|| !activeTab.active
|
|
2546
2677
|
|| activeTab.status !== 'open'
|
|
2547
|
-
||
|
|
2678
|
+
|| state.status !== 'running'
|
|
2548
2679
|
) throw new BrowserRuntimeError('BUSY');
|
|
2549
2680
|
const refresh: IFrameRefreshRecord = {
|
|
2550
2681
|
lease,
|
|
@@ -2554,7 +2685,7 @@ export class BrowserRuntime {
|
|
|
2554
2685
|
initialTabId: activeTab.id,
|
|
2555
2686
|
initialGeneration: activeTab.generation,
|
|
2556
2687
|
initialViewportRevision: state.viewportRevision,
|
|
2557
|
-
startingHighestSequence:
|
|
2688
|
+
startingHighestSequence: slot.highestFrameSequence,
|
|
2558
2689
|
pendingAcknowledgements: new Set(),
|
|
2559
2690
|
refreshStarted: false,
|
|
2560
2691
|
admissionClosed: false,
|
|
@@ -2604,17 +2735,17 @@ export class BrowserRuntime {
|
|
|
2604
2735
|
}
|
|
2605
2736
|
|
|
2606
2737
|
private async drainFrameRefreshOutstanding(refresh: IFrameRefreshRecord): Promise<void> {
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2738
|
+
const slot = refresh.lease.slot;
|
|
2739
|
+
for (const subscription of slot.frameSubscriptions.values()) {
|
|
2740
|
+
for (const frame of subscription.outstanding.values()) {
|
|
2741
|
+
if (frame.timer) clearTimeout(frame.timer);
|
|
2742
|
+
}
|
|
2743
|
+
subscription.outstanding.clear();
|
|
2744
|
+
}
|
|
2745
|
+
const outcomes = await Promise.all([...slot.producerAcknowledgements]);
|
|
2746
|
+
if (outcomes.some((outcome) => outcome.status !== 'fulfilled')) {
|
|
2747
|
+
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2612
2748
|
}
|
|
2613
|
-
await Promise.all(outstanding.map((frame) => this.beginFrameAcknowledgement(
|
|
2614
|
-
frame.session,
|
|
2615
|
-
frame.acknowledgement,
|
|
2616
|
-
refresh,
|
|
2617
|
-
)));
|
|
2618
2749
|
}
|
|
2619
2750
|
|
|
2620
2751
|
private validateFrameRefreshBoundary(
|
|
@@ -2646,25 +2777,23 @@ export class BrowserRuntime {
|
|
|
2646
2777
|
refresh: IFrameRefreshRecord,
|
|
2647
2778
|
identity: plugins.smartpuppeteer.ILiveBrowserFrameIdentity,
|
|
2648
2779
|
): Promise<void> {
|
|
2649
|
-
const
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
));
|
|
2780
|
+
for (const subscription of refresh.lease.slot.frameSubscriptions.values()) {
|
|
2781
|
+
for (const [key, frame] of subscription.outstanding) {
|
|
2782
|
+
const acknowledgement = frame.acknowledgement;
|
|
2783
|
+
if (
|
|
2784
|
+
acknowledgement.tabId === identity.tabId
|
|
2785
|
+
&& acknowledgement.generation === identity.generation
|
|
2786
|
+
&& acknowledgement.viewportRevision === identity.viewportRevision
|
|
2787
|
+
&& acknowledgement.sequence >= identity.sequence
|
|
2788
|
+
) continue;
|
|
2789
|
+
subscription.outstanding.delete(key);
|
|
2790
|
+
if (frame.timer) clearTimeout(frame.timer);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
const outcomes = await Promise.all([...refresh.lease.slot.producerAcknowledgements]);
|
|
2794
|
+
if (outcomes.some((outcome) => outcome.status !== 'fulfilled')) {
|
|
2795
|
+
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2666
2796
|
}
|
|
2667
|
-
await Promise.all(retirements);
|
|
2668
2797
|
}
|
|
2669
2798
|
|
|
2670
2799
|
private beginFrameAcknowledgement(
|
|
@@ -2698,14 +2827,14 @@ export class BrowserRuntime {
|
|
|
2698
2827
|
refresh: IFrameRefreshRecord,
|
|
2699
2828
|
): Promise<void> {
|
|
2700
2829
|
if (refresh.failure) {
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2830
|
+
let claimed: boolean;
|
|
2831
|
+
if (refresh.producerFailure) claimed = await refresh.producerFailure;
|
|
2832
|
+
else {
|
|
2833
|
+
await this.options.beforeFrameFailureTermination?.();
|
|
2834
|
+
claimed = await this.handleFrameFailure(
|
|
2835
|
+
slot, refresh.session, refresh.incarnationGeneration, refresh,
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2709
2838
|
if (!claimed) {
|
|
2710
2839
|
this.abandonFrameRefresh(slot, refresh);
|
|
2711
2840
|
throw new BrowserRuntimeError('ABORTED');
|
|
@@ -2720,7 +2849,7 @@ export class BrowserRuntime {
|
|
|
2720
2849
|
this.requireValidLease(refresh.lease);
|
|
2721
2850
|
current = slot.frameRefresh === refresh
|
|
2722
2851
|
&& !refresh.abandoned
|
|
2723
|
-
&& slot.
|
|
2852
|
+
&& slot.frameSubscriptions.get(refresh.lease.leaseId) === refresh.subscription
|
|
2724
2853
|
&& !refresh.subscription.closed
|
|
2725
2854
|
&& slot.session === refresh.session
|
|
2726
2855
|
&& slot.incarnationGeneration === refresh.incarnationGeneration;
|
|
@@ -2748,7 +2877,7 @@ export class BrowserRuntime {
|
|
|
2748
2877
|
refresh.abandoned = true;
|
|
2749
2878
|
slot.frameRefresh = undefined;
|
|
2750
2879
|
if (
|
|
2751
|
-
slot.
|
|
2880
|
+
slot.frameSubscriptions.get(refresh.lease.leaseId) === refresh.subscription
|
|
2752
2881
|
&& !refresh.subscription.closed
|
|
2753
2882
|
&& slot.session === refresh.session
|
|
2754
2883
|
&& slot.incarnationGeneration === refresh.incarnationGeneration
|
|
@@ -2766,274 +2895,140 @@ export class BrowserRuntime {
|
|
|
2766
2895
|
): void {
|
|
2767
2896
|
if (frame.timer || subscription.closed || !subscription.outstanding.has(key)) return;
|
|
2768
2897
|
const timer = setTimeout(() => {
|
|
2769
|
-
//
|
|
2770
|
-
|
|
2771
|
-
this.trackCleanup(this.retireOutstandingFrame(subscription, key, false).then(() => undefined));
|
|
2898
|
+
// Viewer lag retires only its local window; producer acknowledgement already started.
|
|
2899
|
+
this.trackCleanup(this.retireOutstandingFrame(subscription, key).then(() => undefined));
|
|
2772
2900
|
}, this.options.frameAcknowledgementTimeoutMs);
|
|
2773
2901
|
timer.unref();
|
|
2774
2902
|
frame.timer = timer;
|
|
2775
2903
|
}
|
|
2776
2904
|
|
|
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
2905
|
private handleSessionEvent(
|
|
2795
2906
|
slot: IResourceSlot,
|
|
2796
2907
|
session: ILiveBrowserSessionLike,
|
|
2797
2908
|
event: plugins.smartpuppeteer.TLiveBrowserEvent,
|
|
2798
2909
|
): 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;
|
|
2910
|
+
if (slot.session && slot.session !== session) return;
|
|
2911
|
+
const incarnationGeneration = slot.incarnationGeneration;
|
|
2912
|
+
const refresh = slot.frameRefresh?.session === session && !slot.frameRefresh.abandoned
|
|
2913
|
+
? slot.frameRefresh : undefined;
|
|
2817
2914
|
if (event.type === 'frame') {
|
|
2818
2915
|
let acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest;
|
|
2819
2916
|
try {
|
|
2820
|
-
if (!(event.frame.data instanceof Uint8Array))
|
|
2821
|
-
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2822
|
-
}
|
|
2917
|
+
if (!(event.frame.data instanceof Uint8Array)) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2823
2918
|
acknowledgement = this.validateFrameAcknowledgement(event.frame);
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
|
|
2827
|
-
return;
|
|
2919
|
+
if (acknowledgement.sequence <= slot.highestFrameSequence) {
|
|
2920
|
+
throw new BrowserRuntimeError('FRAME_STREAM_FAILED');
|
|
2828
2921
|
}
|
|
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;
|
|
2922
|
+
if (slot.producerAcknowledgements.size >= this.options.maxOutstandingFrames) {
|
|
2923
|
+
throw new BrowserRuntimeError('FRAME_STREAM_FAILED');
|
|
2842
2924
|
}
|
|
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
2925
|
} catch {
|
|
2859
|
-
if (refresh)
|
|
2860
|
-
|
|
2861
|
-
return;
|
|
2862
|
-
}
|
|
2863
|
-
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
2864
|
-
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
2926
|
+
if (refresh) this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2927
|
+
else this.trackCleanup(this.handleFrameFailure(slot, session, incarnationGeneration).then(() => undefined));
|
|
2865
2928
|
return;
|
|
2866
2929
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2930
|
+
slot.highestFrameSequence = acknowledgement.sequence;
|
|
2931
|
+
// The producer has one bounded ACK, independent of how quickly any viewer paints.
|
|
2932
|
+
const producerAcknowledgement = this.beginFrameAcknowledgement(
|
|
2933
|
+
session, acknowledgement, refresh,
|
|
2934
|
+
);
|
|
2935
|
+
slot.producerAcknowledgements.add(producerAcknowledgement);
|
|
2936
|
+
this.trackCleanup(producerAcknowledgement.then(async (outcome) => {
|
|
2937
|
+
slot.producerAcknowledgements.delete(producerAcknowledgement);
|
|
2938
|
+
if (outcome.status === 'fulfilled') return;
|
|
2939
|
+
if (refresh && slot.frameRefresh === refresh && !refresh.abandoned) {
|
|
2870
2940
|
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2871
2941
|
return;
|
|
2872
2942
|
}
|
|
2873
|
-
this.
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
slot.incarnationGeneration,
|
|
2877
|
-
subscription.lease,
|
|
2878
|
-
).then(() => undefined));
|
|
2879
|
-
return;
|
|
2880
|
-
}
|
|
2881
|
-
subscription.highestSequence = acknowledgement.sequence;
|
|
2943
|
+
await this.options.beforeFrameFailureTermination?.();
|
|
2944
|
+
await this.handleFrameFailure(slot, session, incarnationGeneration);
|
|
2945
|
+
}));
|
|
2882
2946
|
if (event.frame.data.byteLength > this.options.maxFrameBytes) {
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
if (refresh) {
|
|
2886
|
-
this.failFrameRefresh(refresh, 'FRAME_TOO_LARGE');
|
|
2947
|
+
if (refresh) this.failFrameRefresh(refresh, 'FRAME_TOO_LARGE');
|
|
2948
|
+
for (const subscription of slot.frameSubscriptions.values()) {
|
|
2887
2949
|
this.pushToSubscription(subscription, {
|
|
2888
2950
|
type: 'error',
|
|
2889
|
-
error: { code: 'FRAME_TOO_LARGE', fatal: false, tabId:
|
|
2951
|
+
error: { code: 'FRAME_TOO_LARGE', fatal: false, tabId: acknowledgement.tabId },
|
|
2890
2952
|
});
|
|
2891
|
-
return;
|
|
2892
2953
|
}
|
|
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
2954
|
return;
|
|
2899
2955
|
}
|
|
2900
|
-
if (refresh?.admissionClosed)
|
|
2901
|
-
const refreshAdmission = refresh && !refresh.admissionClosed && !refresh.abandoned
|
|
2902
|
-
? refresh
|
|
2903
|
-
: undefined;
|
|
2904
|
-
if (refreshAdmission) {
|
|
2905
|
-
if (refreshAdmission.failure) return;
|
|
2956
|
+
if (refresh?.refreshStarted && !refresh.boundaryCandidate && !refresh.admissionClosed) {
|
|
2906
2957
|
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
|
-
}
|
|
2958
|
+
acknowledgement.tabId === refresh.initialTabId
|
|
2959
|
+
&& acknowledgement.sequence > refresh.startingHighestSequence
|
|
2960
|
+
&& acknowledgement.generation > refresh.initialGeneration
|
|
2961
|
+
&& acknowledgement.viewportRevision === refresh.initialViewportRevision
|
|
2962
|
+
) refresh.boundaryCandidate = { ...acknowledgement };
|
|
2963
|
+
else this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2924
2964
|
}
|
|
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');
|
|
2965
|
+
const key = this.frameAcknowledgementKey(acknowledgement);
|
|
2966
|
+
for (const subscription of [...slot.frameSubscriptions.values()]) {
|
|
2967
|
+
if (subscription.closed) continue;
|
|
2968
|
+
while (subscription.outstanding.size >= this.options.maxOutstandingFrames) {
|
|
2969
|
+
const oldestKey = subscription.outstanding.keys().next().value!;
|
|
2970
|
+
this.trackCleanup(this.retireOutstandingFrame(subscription, oldestKey).then(() => undefined));
|
|
2948
2971
|
}
|
|
2972
|
+
const outstanding: IOutstandingFrameRecord = {
|
|
2973
|
+
acknowledgement, session, incarnationGeneration, producerAcknowledgement,
|
|
2974
|
+
};
|
|
2975
|
+
subscription.highestSequence = acknowledgement.sequence;
|
|
2976
|
+
subscription.outstanding.set(key, outstanding);
|
|
2977
|
+
this.armFrameAcknowledgementTimer(subscription, key, outstanding);
|
|
2978
|
+
this.pushToSubscription(subscription, { type: 'frame', frame: event.frame });
|
|
2949
2979
|
}
|
|
2950
2980
|
return;
|
|
2951
2981
|
}
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2982
|
+
const runtimeEvent: TBrowserRuntimeEvent = event.type === 'state'
|
|
2983
|
+
? { type: 'state', state: this.resourceState(event.state) }
|
|
2984
|
+
: {
|
|
2985
|
+
type: 'error',
|
|
2986
|
+
error: {
|
|
2987
|
+
code: truncateString(event.error.code, 128),
|
|
2988
|
+
fatal: event.error.fatal,
|
|
2989
|
+
message: truncateString(event.error.message, 2048),
|
|
2990
|
+
...(event.error.tabId ? { tabId: truncateString(event.error.tabId, 128) } : {}),
|
|
2991
|
+
},
|
|
2992
|
+
};
|
|
2993
|
+
if (event.type === 'error' && event.error.fatal) {
|
|
2994
|
+
const subscribers = [...slot.frameSubscriptions.values()].filter((subscription) => (
|
|
2995
|
+
!subscription.closed && !subscription.lease.released
|
|
2996
|
+
));
|
|
2997
|
+
// Publish the cleanup promise before synchronous revocation wakes queued callers.
|
|
2998
|
+
if (refresh) this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2999
|
+
const failure = this.handleFrameFailure(slot, session, incarnationGeneration);
|
|
3000
|
+
if (refresh) refresh.producerFailure = failure;
|
|
3001
|
+
for (const lease of slot.leases.values()) {
|
|
3002
|
+
if (lease.released) continue;
|
|
3003
|
+
this.invalidateCapabilityRecord(lease.capability);
|
|
3004
|
+
lease.released = true;
|
|
3005
|
+
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
2971
3006
|
}
|
|
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;
|
|
3007
|
+
slot.operation?.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3008
|
+
for (const subscription of subscribers) {
|
|
3009
|
+
try { subscription.listener(runtimeEvent); } catch { /* Producer failure owns cleanup. */ }
|
|
2991
3010
|
}
|
|
2992
|
-
|
|
2993
|
-
this.pushToSubscription(subscription, runtimeEvent);
|
|
3011
|
+
this.trackCleanup(failure.then(() => undefined));
|
|
2994
3012
|
return;
|
|
2995
3013
|
}
|
|
2996
|
-
|
|
2997
|
-
|
|
3014
|
+
for (const subscription of [...slot.frameSubscriptions.values()]) {
|
|
3015
|
+
this.pushToSubscription(subscription, runtimeEvent);
|
|
3016
|
+
}
|
|
2998
3017
|
}
|
|
2999
3018
|
|
|
3000
3019
|
private pushToSubscription(
|
|
3001
3020
|
subscription: IFrameSubscriptionRecord,
|
|
3002
3021
|
event: TBrowserRuntimeEvent,
|
|
3003
3022
|
): boolean {
|
|
3023
|
+
if (subscription.closed) return false;
|
|
3004
3024
|
try {
|
|
3005
3025
|
this.requireValidLease(subscription.lease);
|
|
3006
3026
|
subscription.listener(event);
|
|
3007
3027
|
return true;
|
|
3008
3028
|
} 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
|
-
})());
|
|
3029
|
+
// A failed participant transport cannot revoke another participant or kill the producer.
|
|
3030
|
+
this.trackCleanup(this.closeFrameSubscription(subscription));
|
|
3031
|
+
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
3037
3032
|
return false;
|
|
3038
3033
|
}
|
|
3039
3034
|
}
|
|
@@ -3042,60 +3037,28 @@ export class BrowserRuntime {
|
|
|
3042
3037
|
if (subscription.closed) return;
|
|
3043
3038
|
subscription.closed = true;
|
|
3044
3039
|
const slot = subscription.lease.slot;
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
3040
|
+
if (slot.frameRefresh?.subscription === subscription) {
|
|
3041
|
+
this.abandonFrameRefresh(slot, slot.frameRefresh);
|
|
3048
3042
|
}
|
|
3049
|
-
if (slot.
|
|
3050
|
-
|
|
3051
|
-
|
|
3043
|
+
if (slot.frameSubscriptions.get(subscription.lease.leaseId) === subscription) {
|
|
3044
|
+
slot.frameSubscriptions.delete(subscription.lease.leaseId);
|
|
3045
|
+
}
|
|
3046
|
+
for (const frame of subscription.outstanding.values()) {
|
|
3047
|
+
if (frame.timer) clearTimeout(frame.timer);
|
|
3052
3048
|
}
|
|
3049
|
+
subscription.outstanding.clear();
|
|
3053
3050
|
}
|
|
3054
3051
|
|
|
3055
3052
|
private async retireOutstandingFrame(
|
|
3056
3053
|
subscription: IFrameSubscriptionRecord,
|
|
3057
3054
|
key: string,
|
|
3058
|
-
failLease: boolean,
|
|
3059
3055
|
): Promise<boolean> {
|
|
3060
3056
|
const outstanding = subscription.outstanding.get(key);
|
|
3061
3057
|
if (!outstanding) return false;
|
|
3062
3058
|
subscription.outstanding.delete(key);
|
|
3063
3059
|
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;
|
|
3060
|
+
const outcome = await outstanding.producerAcknowledgement;
|
|
3061
|
+
return outcome.status === 'fulfilled' && outcome.accepted;
|
|
3099
3062
|
}
|
|
3100
3063
|
|
|
3101
3064
|
private validateFrameAcknowledgement(
|
|
@@ -3157,130 +3120,73 @@ export class BrowserRuntime {
|
|
|
3157
3120
|
}
|
|
3158
3121
|
}
|
|
3159
3122
|
|
|
3160
|
-
private async
|
|
3123
|
+
private async handleFrameFailure(
|
|
3161
3124
|
slot: IResourceSlot,
|
|
3162
3125
|
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;
|
|
3126
|
+
incarnationGeneration: number,
|
|
3127
|
+
expectedRefresh?: IFrameRefreshRecord,
|
|
3128
|
+
): Promise<boolean> {
|
|
3129
|
+
if (slot.session !== session || slot.incarnationGeneration !== incarnationGeneration) return false;
|
|
3130
|
+
if (slot.frameFailure?.session === session) return slot.frameFailure.promise;
|
|
3131
|
+
const promise = this.handleFrameFailureInternal(slot, session, incarnationGeneration, expectedRefresh);
|
|
3132
|
+
const failure = { session, promise };
|
|
3133
|
+
slot.frameFailure = failure;
|
|
3134
|
+
try { return await promise; } finally {
|
|
3135
|
+
if (slot.frameFailure === failure) slot.frameFailure = undefined;
|
|
3186
3136
|
}
|
|
3187
|
-
await this.options.beforeFrameFailureTermination?.();
|
|
3188
|
-
await this.handleFrameFailure(slot, session, incarnationGeneration, failedLease);
|
|
3189
3137
|
}
|
|
3190
3138
|
|
|
3191
|
-
private async
|
|
3139
|
+
private async handleFrameFailureInternal(
|
|
3192
3140
|
slot: IResourceSlot,
|
|
3193
3141
|
session: ILiveBrowserSessionLike,
|
|
3194
3142
|
incarnationGeneration: number,
|
|
3195
|
-
expectedLease: ILeaseRecord | null,
|
|
3196
3143
|
expectedRefresh?: IFrameRefreshRecord,
|
|
3197
3144
|
): Promise<boolean> {
|
|
3198
|
-
let transitionGeneration = 0;
|
|
3199
|
-
let ownsTransitionFence = false;
|
|
3200
|
-
const lease = expectedLease ?? undefined;
|
|
3201
3145
|
const release = await slot.mutex.acquire();
|
|
3146
|
+
let generation: number;
|
|
3147
|
+
let leases: ILeaseRecord[];
|
|
3202
3148
|
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
3149
|
if (
|
|
3221
3150
|
slot.session !== session
|
|
3222
3151
|
|| 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)
|
|
3152
|
+
|| slot.permanentlyFenced || slot.retirementPending
|
|
3153
|
+
|| slot.terminationFenceGeneration !== undefined || slot.fencingGeneration !== undefined
|
|
3154
|
+
|| (expectedRefresh && slot.frameRefresh !== expectedRefresh)
|
|
3232
3155
|
) 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) {
|
|
3156
|
+
generation = ++slot.arbitrationGeneration;
|
|
3157
|
+
slot.fencingGeneration = generation;
|
|
3158
|
+
this.abandonFrameRefresh(slot);
|
|
3159
|
+
leases = [...slot.leases.values()];
|
|
3160
|
+
for (const lease of leases) {
|
|
3161
|
+
this.invalidateCapabilityRecord(lease.capability);
|
|
3247
3162
|
lease.released = true;
|
|
3248
3163
|
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3164
|
+
this.notifyFramedLeaseEnded(lease);
|
|
3249
3165
|
}
|
|
3250
|
-
|
|
3251
|
-
slot.operation?.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3252
|
-
}
|
|
3166
|
+
slot.operation?.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3253
3167
|
} finally {
|
|
3254
3168
|
release();
|
|
3255
3169
|
}
|
|
3256
|
-
if (lease) this.trackFrameCapabilityRevocation(lease);
|
|
3257
3170
|
await this.options.afterFrameFailureFence?.();
|
|
3258
|
-
|
|
3171
|
+
await Promise.all(leases.map((lease) => this.closeFrameSubscriptionIfLease(lease)));
|
|
3259
3172
|
const errors: unknown[] = [];
|
|
3260
3173
|
await this.quiesceSlot(slot).catch((error) => errors.push(error));
|
|
3261
3174
|
await this.terminateSlotSession(slot, session).catch((error) => errors.push(error));
|
|
3262
3175
|
const finalRelease = await slot.mutex.acquire();
|
|
3263
3176
|
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;
|
|
3177
|
+
if (slot.fencingGeneration === generation && errors.length === 0) {
|
|
3178
|
+
for (const lease of leases) {
|
|
3179
|
+
slot.leases.delete(lease.leaseId);
|
|
3180
|
+
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
3273
3181
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3182
|
+
slot.operation = undefined;
|
|
3183
|
+
slot.fencingGeneration = undefined;
|
|
3276
3184
|
}
|
|
3277
3185
|
} finally {
|
|
3278
3186
|
finalRelease();
|
|
3279
3187
|
}
|
|
3280
|
-
|
|
3281
|
-
if (errors.length
|
|
3282
|
-
throw new AggregateError(errors, 'Frame acknowledgement cleanup is incomplete');
|
|
3283
|
-
}
|
|
3188
|
+
for (const lease of leases) this.trackFrameCapabilityRevocation(lease);
|
|
3189
|
+
if (errors.length) throw new AggregateError(errors, 'Frame producer cleanup is incomplete');
|
|
3284
3190
|
return true;
|
|
3285
3191
|
}
|
|
3286
3192
|
|
|
@@ -3293,6 +3199,14 @@ export class BrowserRuntime {
|
|
|
3293
3199
|
this.trackCleanup(operation);
|
|
3294
3200
|
}
|
|
3295
3201
|
|
|
3202
|
+
private requireQueuedLease(lease: ILeaseRecord, participantCleanup = false): void {
|
|
3203
|
+
if (!participantCleanup) return this.requireValidLease(lease);
|
|
3204
|
+
if (
|
|
3205
|
+
!lease.released || lease.slot.leases.get(lease.leaseId) !== lease
|
|
3206
|
+
|| lease.capability.lease !== lease || !lease.slot.session
|
|
3207
|
+
) throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3296
3210
|
private requireValidLease(lease: ILeaseRecord): void {
|
|
3297
3211
|
if (lease.released || lease.controller.signal.aborted) {
|
|
3298
3212
|
throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
@@ -3304,7 +3218,7 @@ export class BrowserRuntime {
|
|
|
3304
3218
|
if (
|
|
3305
3219
|
capability !== lease.capability
|
|
3306
3220
|
|| capability.lease !== lease
|
|
3307
|
-
|| lease.slot.lease !== lease
|
|
3221
|
+
|| lease.slot.leases.get(lease.leaseId) !== lease
|
|
3308
3222
|
|| !lease.slot.session
|
|
3309
3223
|
) {
|
|
3310
3224
|
throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
@@ -3427,57 +3341,58 @@ export class BrowserRuntime {
|
|
|
3427
3341
|
}
|
|
3428
3342
|
|
|
3429
3343
|
private async releaseLeaseRecordInternal(lease: ILeaseRecord): Promise<void> {
|
|
3430
|
-
if (lease.released && lease.slot.lease !== lease) return;
|
|
3431
3344
|
const slot = lease.slot;
|
|
3345
|
+
if (slot.leases.get(lease.leaseId) !== lease) return;
|
|
3432
3346
|
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;
|
|
3347
|
+
let operation: IOperationRecord | undefined;
|
|
3439
3348
|
try {
|
|
3440
|
-
if (existingFence !== undefined) {
|
|
3441
|
-
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3442
|
-
} else {
|
|
3443
|
-
slot.fencingGeneration = generation;
|
|
3444
|
-
}
|
|
3445
3349
|
lease.released = true;
|
|
3446
3350
|
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
}
|
|
3351
|
+
operation = slot.operation?.lease === lease ? slot.operation : undefined;
|
|
3352
|
+
operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3450
3353
|
} finally {
|
|
3451
3354
|
release();
|
|
3452
3355
|
}
|
|
3453
3356
|
await this.closeFrameSubscriptionIfLease(lease);
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3357
|
+
if (operation) {
|
|
3358
|
+
const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
3359
|
+
if (!settled.settled) {
|
|
3360
|
+
// An unquiescent operation belongs to the browser process, so its incarnation must end.
|
|
3361
|
+
await this.terminateRegisteredResourceInternal(slot, false, {
|
|
3362
|
+
session: operation.session, incarnationGeneration: operation.incarnationGeneration,
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3457
3365
|
}
|
|
3366
|
+
if (slot.frameRefresh?.lease === lease) this.abandonFrameRefresh(slot, slot.frameRefresh);
|
|
3458
3367
|
if (
|
|
3459
|
-
slot.permanentlyFenced
|
|
3460
|
-
&&
|
|
3461
|
-
|
|
3368
|
+
this.lifecycleState === 'running' && slot.session && !slot.permanentlyFenced && !slot.retirementPending
|
|
3369
|
+
&& slot.fencingGeneration === undefined && slot.terminationFenceGeneration === undefined
|
|
3370
|
+
&& (lease.preferredViewport || lease.heldKeys.size || lease.heldButtons.size)
|
|
3371
|
+
) {
|
|
3372
|
+
await this.runOperation(lease, 'releaseParticipant', 'viewport', {},
|
|
3373
|
+
async (signal, session) => {
|
|
3374
|
+
await this.releaseParticipantInput(lease, session);
|
|
3375
|
+
await this.applySharedViewport(slot, session, signal);
|
|
3376
|
+
lease.preferredViewport = undefined;
|
|
3377
|
+
}, undefined, true);
|
|
3378
|
+
}
|
|
3379
|
+
if (slot.permanentlyFenced && (slot.session || slot.proxy || slot.profileDirectory)) {
|
|
3380
|
+
await this.terminateSlotSession(slot);
|
|
3381
|
+
}
|
|
3462
3382
|
const finalRelease = await slot.mutex.acquire();
|
|
3463
3383
|
try {
|
|
3464
3384
|
if (lease.capability.state === 'active') this.notifyFramedLeaseEnded(lease);
|
|
3465
|
-
|
|
3385
|
+
slot.leases.delete(lease.leaseId);
|
|
3466
3386
|
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);
|
|
3387
|
+
if (slot.leases.size === 0) this.scheduleIdleTermination(slot);
|
|
3473
3388
|
} finally {
|
|
3474
3389
|
finalRelease();
|
|
3475
3390
|
}
|
|
3476
3391
|
}
|
|
3477
3392
|
|
|
3478
3393
|
private async closeFrameSubscriptionIfLease(lease: ILeaseRecord): Promise<void> {
|
|
3479
|
-
const subscription = lease.slot.
|
|
3480
|
-
if (subscription
|
|
3394
|
+
const subscription = lease.slot.frameSubscriptions.get(lease.leaseId);
|
|
3395
|
+
if (subscription) await this.closeFrameSubscription(subscription);
|
|
3481
3396
|
}
|
|
3482
3397
|
|
|
3483
3398
|
private async quiesceSlot(slot: IResourceSlot): Promise<void> {
|
|
@@ -3513,6 +3428,12 @@ export class BrowserRuntime {
|
|
|
3513
3428
|
return;
|
|
3514
3429
|
}
|
|
3515
3430
|
const slot = operation.lease.slot;
|
|
3431
|
+
if (slot.operation === operation && slot.session === operation.session
|
|
3432
|
+
&& slot.incarnationGeneration === operation.incarnationGeneration) {
|
|
3433
|
+
await this.terminateRegisteredResourceInternal(slot, false, {
|
|
3434
|
+
session: operation.session, incarnationGeneration: operation.incarnationGeneration,
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3516
3437
|
const release = await slot.mutex.acquire();
|
|
3517
3438
|
try {
|
|
3518
3439
|
if (
|
|
@@ -3565,6 +3486,8 @@ export class BrowserRuntime {
|
|
|
3565
3486
|
slot.unsubscribeSession?.();
|
|
3566
3487
|
slot.unsubscribeSession = undefined;
|
|
3567
3488
|
slot.session = undefined;
|
|
3489
|
+
slot.highestFrameSequence = 0;
|
|
3490
|
+
slot.producerAcknowledgements.clear();
|
|
3568
3491
|
}
|
|
3569
3492
|
if (slot.proxy) {
|
|
3570
3493
|
await this.stopProxyBounded(slot.proxy).then(
|
|
@@ -3582,7 +3505,7 @@ export class BrowserRuntime {
|
|
|
3582
3505
|
}
|
|
3583
3506
|
|
|
3584
3507
|
private scheduleIdleTermination(slot: IResourceSlot): void {
|
|
3585
|
-
if (slot.terminationFenceGeneration !== undefined || slot.retirementPending) return;
|
|
3508
|
+
if (slot.leases.size > 0 || slot.terminationFenceGeneration !== undefined || slot.retirementPending) return;
|
|
3586
3509
|
if (slot.idleTimer) clearTimeout(slot.idleTimer);
|
|
3587
3510
|
const arbitrationGeneration = slot.arbitrationGeneration;
|
|
3588
3511
|
const incarnationGeneration = slot.incarnationGeneration;
|
|
@@ -3626,7 +3549,7 @@ export class BrowserRuntime {
|
|
|
3626
3549
|
): Promise<void> {
|
|
3627
3550
|
const release = await slot.mutex.acquire();
|
|
3628
3551
|
let generation = 0;
|
|
3629
|
-
let
|
|
3552
|
+
let leases: ILeaseRecord[];
|
|
3630
3553
|
try {
|
|
3631
3554
|
if (
|
|
3632
3555
|
(expected?.arbitrationGeneration !== undefined
|
|
@@ -3634,48 +3557,34 @@ export class BrowserRuntime {
|
|
|
3634
3557
|
|| (expected?.incarnationGeneration !== undefined
|
|
3635
3558
|
&& slot.incarnationGeneration !== expected.incarnationGeneration)
|
|
3636
3559
|
|| (expected?.session !== undefined && slot.session !== expected.session)
|
|
3637
|
-
|| (expected?.requireIdle && slot.
|
|
3560
|
+
|| (expected?.requireIdle && slot.leases.size > 0)
|
|
3638
3561
|
) 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
|
-
}
|
|
3562
|
+
generation = ++slot.arbitrationGeneration;
|
|
3563
|
+
slot.fencingGeneration = generation;
|
|
3564
|
+
if (slot.idleTimer) clearTimeout(slot.idleTimer);
|
|
3565
|
+
slot.idleTimer = undefined;
|
|
3566
|
+
leases = [...slot.leases.values()];
|
|
3567
|
+
for (const lease of leases) {
|
|
3568
|
+
lease.released = true;
|
|
3569
|
+
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3570
|
+
if (!stopping) this.notifyFramedLeaseEnded(lease);
|
|
3654
3571
|
}
|
|
3655
3572
|
slot.operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3656
3573
|
} finally {
|
|
3657
3574
|
release();
|
|
3658
3575
|
}
|
|
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
3576
|
await this.quiesceSlot(slot);
|
|
3667
|
-
|
|
3577
|
+
await Promise.all(leases.map((lease) => this.closeFrameSubscriptionIfLease(lease)));
|
|
3668
3578
|
await this.terminateSlotSession(slot, expected?.session);
|
|
3669
3579
|
const finalRelease = await slot.mutex.acquire();
|
|
3670
3580
|
try {
|
|
3671
3581
|
if (slot.arbitrationGeneration !== generation || slot.fencingGeneration !== generation) {
|
|
3672
3582
|
throw new BrowserRuntimeError('BUSY');
|
|
3673
3583
|
}
|
|
3674
|
-
const lease
|
|
3675
|
-
|
|
3676
|
-
|
|
3584
|
+
for (const lease of leases) {
|
|
3585
|
+
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
3586
|
+
slot.leases.delete(lease.leaseId);
|
|
3677
3587
|
}
|
|
3678
|
-
slot.lease = undefined;
|
|
3679
3588
|
slot.operation = undefined;
|
|
3680
3589
|
slot.fencingGeneration = undefined;
|
|
3681
3590
|
} finally {
|
|
@@ -3719,7 +3628,6 @@ export class BrowserRuntime {
|
|
|
3719
3628
|
if (slot.retirementPending) throw new BrowserRuntimeError('RESOURCE_RETIRED');
|
|
3720
3629
|
if (slot.permanentlyFenced) throw new BrowserRuntimeError('FENCED');
|
|
3721
3630
|
if (slot.terminationFenceGeneration !== undefined) throw new BrowserRuntimeError('BUSY');
|
|
3722
|
-
if (slot.attachmentFenceCount > 0) throw new BrowserRuntimeError('BUSY');
|
|
3723
3631
|
if (slot.fencingGeneration !== undefined) throw new BrowserRuntimeError('BUSY');
|
|
3724
3632
|
}
|
|
3725
3633
|
|
|
@@ -3762,54 +3670,45 @@ export class BrowserRuntime {
|
|
|
3762
3670
|
if (binding.attachmentRevision < current.attachmentRevision) {
|
|
3763
3671
|
throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3764
3672
|
}
|
|
3765
|
-
if (
|
|
3766
|
-
binding
|
|
3767
|
-
&& !slot.attachmentRetryBinding
|
|
3768
|
-
) {
|
|
3769
|
-
if (!this.bindingsEqual(binding, current)) {
|
|
3770
|
-
throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3771
|
-
}
|
|
3673
|
+
if (binding.attachmentRevision === current.attachmentRevision && !slot.attachmentRetryBinding) {
|
|
3674
|
+
if (!this.bindingsEqual(binding, current)) throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3772
3675
|
return this.describeResource(slot);
|
|
3773
3676
|
}
|
|
3774
|
-
if (
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
) throw new BrowserRuntimeError('BUSY');
|
|
3677
|
+
if (slot.attachmentRetryBinding && !this.bindingsEqual(binding, slot.attachmentRetryBinding)) {
|
|
3678
|
+
throw new BrowserRuntimeError('BUSY');
|
|
3679
|
+
}
|
|
3778
3680
|
const release = await slot.mutex.acquire();
|
|
3779
|
-
let fenceGeneration = 0;
|
|
3780
3681
|
try {
|
|
3781
|
-
fenceGeneration = ++slot.arbitrationGeneration;
|
|
3782
|
-
slot.fencingGeneration = fenceGeneration;
|
|
3783
3682
|
slot.attachmentBinding = binding;
|
|
3784
3683
|
slot.attachmentRetryBinding = binding;
|
|
3785
|
-
slot.
|
|
3786
|
-
|
|
3684
|
+
for (const lease of slot.leases.values()) {
|
|
3685
|
+
if (lease.role === 'agent') {
|
|
3686
|
+
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
if (slot.operation?.lease.role === 'agent') {
|
|
3690
|
+
slot.operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3691
|
+
}
|
|
3787
3692
|
} finally {
|
|
3788
3693
|
release();
|
|
3789
3694
|
}
|
|
3790
3695
|
const peerResults = await Promise.allSettled(
|
|
3791
3696
|
this.framedPeersForResource(slot).map((peer) => peer.close()),
|
|
3792
3697
|
);
|
|
3793
|
-
const capabilities = this.capabilitiesForResource(slot);
|
|
3794
3698
|
const capabilityResults = await Promise.allSettled(
|
|
3795
|
-
|
|
3699
|
+
this.capabilitiesForResource(slot).filter((record) => record.role === 'agent')
|
|
3700
|
+
.map((record) => this.revokeCapabilityRecord(record)),
|
|
3796
3701
|
);
|
|
3797
3702
|
const errors = [...peerResults, ...capabilityResults]
|
|
3798
3703
|
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
3799
3704
|
.map((result) => result.reason);
|
|
3800
|
-
await this.quiesceSlot(slot).catch((error) => errors.push(error));
|
|
3801
3705
|
const finalRelease = await slot.mutex.acquire();
|
|
3802
3706
|
try {
|
|
3803
|
-
if (
|
|
3804
|
-
slot.fencingGeneration = undefined;
|
|
3805
|
-
slot.attachmentRetryBinding = undefined;
|
|
3806
|
-
}
|
|
3707
|
+
if (errors.length === 0) slot.attachmentRetryBinding = undefined;
|
|
3807
3708
|
} finally {
|
|
3808
3709
|
finalRelease();
|
|
3809
3710
|
}
|
|
3810
|
-
if (errors.length
|
|
3811
|
-
throw new AggregateError(errors, 'Attachment fencing is incomplete');
|
|
3812
|
-
}
|
|
3711
|
+
if (errors.length) throw new AggregateError(errors, 'Attachment fencing is incomplete');
|
|
3813
3712
|
return this.describeResource(slot);
|
|
3814
3713
|
}
|
|
3815
3714
|
|
|
@@ -3979,6 +3878,9 @@ export class BrowserRuntime {
|
|
|
3979
3878
|
|| binding.attachmentRevision !== expected.attachmentRevision
|
|
3980
3879
|
) throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3981
3880
|
if (expected.role === 'agent') {
|
|
3881
|
+
if (slot.attachmentFenceCount > 0 || slot.attachmentRetryBinding) {
|
|
3882
|
+
throw new BrowserRuntimeError('BUSY');
|
|
3883
|
+
}
|
|
3982
3884
|
if (!binding.sessionId || !this.sessionsEqual(binding.sessionId, expected.sessionId)) {
|
|
3983
3885
|
throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3984
3886
|
}
|
|
@@ -3995,6 +3897,7 @@ export class BrowserRuntime {
|
|
|
3995
3897
|
capability.projectId !== slot.projectId
|
|
3996
3898
|
|| capability.browserResourceId !== slot.browserResourceId
|
|
3997
3899
|
) throw new BrowserRuntimeError('CAPABILITY_INVALID');
|
|
3900
|
+
if (capability.role === 'human') return;
|
|
3998
3901
|
try {
|
|
3999
3902
|
this.assertCapabilityBinding(slot, capability);
|
|
4000
3903
|
} catch {
|
|
@@ -4027,7 +3930,7 @@ export class BrowserRuntime {
|
|
|
4027
3930
|
|| sessionId?.harnessId !== 'flex'))
|
|
4028
3931
|
|| (source === 'mcp' && (scopeId !== undefined || channelId !== undefined
|
|
4029
3932
|
|| runId !== undefined
|
|
4030
|
-
|| sessionId
|
|
3933
|
+
|| sessionId === undefined))
|
|
4031
3934
|
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
4032
3935
|
|| runId !== undefined))
|
|
4033
3936
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -4061,7 +3964,8 @@ export class BrowserRuntime {
|
|
|
4061
3964
|
runId: runId!,
|
|
4062
3965
|
};
|
|
4063
3966
|
}
|
|
4064
|
-
if (role === 'agent' && source === 'mcp'
|
|
3967
|
+
if (role === 'agent' && source === 'mcp'
|
|
3968
|
+
&& sessionId !== undefined) {
|
|
4065
3969
|
return { ...base, role, source, sessionId };
|
|
4066
3970
|
}
|
|
4067
3971
|
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -4099,7 +4003,7 @@ export class BrowserRuntime {
|
|
|
4099
4003
|
if (
|
|
4100
4004
|
capability.role === 'agent'
|
|
4101
4005
|
&& capability.source === 'mcp'
|
|
4102
|
-
&& capability.sessionId
|
|
4006
|
+
&& capability.sessionId !== undefined
|
|
4103
4007
|
) {
|
|
4104
4008
|
return {
|
|
4105
4009
|
...base,
|
|
@@ -4305,7 +4209,7 @@ export class BrowserRuntimeLease {
|
|
|
4305
4209
|
public setViewport(
|
|
4306
4210
|
viewport: plugins.smartpuppeteer.ILiveBrowserViewport,
|
|
4307
4211
|
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
4308
|
-
): Promise<
|
|
4212
|
+
): Promise<IBrowserRuntimeViewportResult> {
|
|
4309
4213
|
return this.runtime.leaseSetViewport(this.record, viewport, operationOptions);
|
|
4310
4214
|
}
|
|
4311
4215
|
|