@modelprofile.com/browser-runtime 4.3.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 +34 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.runtime.d.ts +43 -11
- package/dist_ts/classes.runtime.js +574 -570
- package/dist_ts/classes.runtimeownership.d.ts +3 -1
- package/dist_ts/classes.runtimeownership.js +27 -7
- package/dist_ts/index.d.ts +2 -2
- 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 +22 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.runtime.ts +620 -638
- package/ts/classes.runtimeownership.ts +23 -6
- package/ts/index.ts +5 -1
- 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,15 @@ 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>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface IBrowserRuntimeLeaseRenewalOptions {
|
|
146
|
+
signal?: AbortSignal;
|
|
147
|
+
/** Lifetime from successful reauthorization; defaults to the runtime capability lifetime. */
|
|
148
|
+
expiresInMs?: number;
|
|
140
149
|
}
|
|
141
150
|
|
|
142
151
|
interface IOperationRecord {
|
|
@@ -165,6 +174,8 @@ interface IQueuedOperationRecord {
|
|
|
165
174
|
reject(error: unknown): void;
|
|
166
175
|
state: 'queued' | 'starting' | 'active' | 'settled';
|
|
167
176
|
onQueuedAbort?: () => void;
|
|
177
|
+
/** Resource-owned cleanup, admitted only for this exact released participant. */
|
|
178
|
+
participantCleanup?: boolean;
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
interface IOutstandingFrameRecord {
|
|
@@ -172,6 +183,7 @@ interface IOutstandingFrameRecord {
|
|
|
172
183
|
timer?: ReturnType<typeof setTimeout>;
|
|
173
184
|
session: ILiveBrowserSessionLike;
|
|
174
185
|
incarnationGeneration: number;
|
|
186
|
+
producerAcknowledgement: Promise<TFrameAcknowledgementOutcome>;
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
type TFrameAcknowledgementOutcome =
|
|
@@ -202,6 +214,7 @@ interface IFrameRefreshRecord {
|
|
|
202
214
|
abandoned: boolean;
|
|
203
215
|
boundaryCandidate?: plugins.smartpuppeteer.ILiveBrowserFrameIdentity;
|
|
204
216
|
failure?: BrowserRuntimeError;
|
|
217
|
+
producerFailure?: Promise<boolean>;
|
|
205
218
|
}
|
|
206
219
|
|
|
207
220
|
interface IResourceSlot {
|
|
@@ -222,12 +235,15 @@ interface IResourceSlot {
|
|
|
222
235
|
proxy?: BrowserEgressProxy;
|
|
223
236
|
profileDirectory?: string;
|
|
224
237
|
unsubscribeSession?: () => void;
|
|
225
|
-
|
|
238
|
+
leases: Map<string, ILeaseRecord>;
|
|
226
239
|
operation?: IOperationRecord;
|
|
227
240
|
operationQueue: IQueuedOperationRecord[];
|
|
228
241
|
operationSchedulerRunning: boolean;
|
|
229
|
-
|
|
242
|
+
frameSubscriptions: Map<string, IFrameSubscriptionRecord>;
|
|
243
|
+
highestFrameSequence: number;
|
|
244
|
+
producerAcknowledgements: Set<Promise<TFrameAcknowledgementOutcome>>;
|
|
230
245
|
frameRefresh?: IFrameRefreshRecord;
|
|
246
|
+
frameFailure?: { session: ILiveBrowserSessionLike; promise: Promise<boolean> };
|
|
231
247
|
idleTimer?: ReturnType<typeof setTimeout>;
|
|
232
248
|
lifecycleTail: Promise<void>;
|
|
233
249
|
lifecycleOperationCount: number;
|
|
@@ -259,6 +275,8 @@ export class BrowserRuntime {
|
|
|
259
275
|
private readonly failedRevocations = new Set<TCapabilityRecord>();
|
|
260
276
|
private readonly capabilityMutex = new TransitionMutex();
|
|
261
277
|
private readonly cleanupOperations = new Set<Promise<void>>();
|
|
278
|
+
// Retain admission until the host callback actually settles, including after cancellation.
|
|
279
|
+
private readonly leaseRenewals = new Set<ILeaseRecord>();
|
|
262
280
|
private readonly framedPeers = new Set<BrowserRuntimeFramedServerPeer>();
|
|
263
281
|
private startPromise?: Promise<void>;
|
|
264
282
|
private stopPromise?: Promise<void>;
|
|
@@ -689,6 +707,10 @@ export class BrowserRuntime {
|
|
|
689
707
|
permanentlyFenced: false,
|
|
690
708
|
retirementPending: false,
|
|
691
709
|
attachmentFenceCount: 0,
|
|
710
|
+
leases: new Map(),
|
|
711
|
+
frameSubscriptions: new Map(),
|
|
712
|
+
highestFrameSequence: 0,
|
|
713
|
+
producerAcknowledgements: new Set(),
|
|
692
714
|
operationQueue: [],
|
|
693
715
|
operationSchedulerRunning: false,
|
|
694
716
|
lifecycleTail: Promise.resolve(),
|
|
@@ -831,7 +853,6 @@ export class BrowserRuntime {
|
|
|
831
853
|
const sessionId = requestArg.sessionId === undefined
|
|
832
854
|
? undefined
|
|
833
855
|
: this.validateQualifiedSessionId(requestArg.sessionId);
|
|
834
|
-
if (sessionId?.harnessId === 'codex') throw new BrowserRuntimeError('INVALID_INPUT');
|
|
835
856
|
if (
|
|
836
857
|
(role === 'agent' && !sessionId)
|
|
837
858
|
|| (role === 'human' && sessionId !== undefined)
|
|
@@ -839,7 +860,7 @@ export class BrowserRuntime {
|
|
|
839
860
|
|| sessionId?.harnessId !== 'flex'))
|
|
840
861
|
|| (source === 'mcp' && (scopeId !== undefined || channelId !== undefined
|
|
841
862
|
|| runId !== undefined
|
|
842
|
-
|| sessionId
|
|
863
|
+
|| sessionId === undefined))
|
|
843
864
|
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
844
865
|
|| runId !== undefined))
|
|
845
866
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -1016,9 +1037,7 @@ export class BrowserRuntime {
|
|
|
1016
1037
|
);
|
|
1017
1038
|
const slot = this.getRegisteredSlot(capability.projectId, capability.browserResourceId);
|
|
1018
1039
|
this.assertCapabilityStillAttached(capability, slot);
|
|
1019
|
-
return
|
|
1020
|
-
? this.acquireAgentLease(slot, capability, digest, optionsArg)
|
|
1021
|
-
: this.acquireHumanLease(slot, capability, digest, optionsArg);
|
|
1040
|
+
return this.acquireParticipantLease(slot, capability, digest, optionsArg);
|
|
1022
1041
|
}
|
|
1023
1042
|
|
|
1024
1043
|
public authorizeCapability(
|
|
@@ -1130,6 +1149,75 @@ export class BrowserRuntime {
|
|
|
1130
1149
|
return this.createLeaseAuthority(lease);
|
|
1131
1150
|
}
|
|
1132
1151
|
|
|
1152
|
+
/** @internal */
|
|
1153
|
+
public async renewLease(
|
|
1154
|
+
lease: ILeaseRecord,
|
|
1155
|
+
optionsArg: IBrowserRuntimeLeaseRenewalOptions = {},
|
|
1156
|
+
): Promise<number> {
|
|
1157
|
+
const options = validateExactKeys(optionsArg, ['signal', 'expiresInMs'], 'renewal');
|
|
1158
|
+
if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
|
|
1159
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1160
|
+
}
|
|
1161
|
+
const lifetimeMs = validateOptionalInteger(
|
|
1162
|
+
options.expiresInMs, 'expiresInMs', 1000,
|
|
1163
|
+
this.options.capabilityMaximumTtlMs, this.options.capabilityDefaultTtlMs,
|
|
1164
|
+
);
|
|
1165
|
+
const authority = this.getLeaseAuthority(lease);
|
|
1166
|
+
if (!this.isLeaseAuthorityCurrent(lease, authority)) throw new BrowserRuntimeError('FENCED');
|
|
1167
|
+
if (this.leaseRenewals.has(lease)) throw new BrowserRuntimeError('BUSY');
|
|
1168
|
+
if (this.leaseRenewals.size >= this.options.maxCapabilities) {
|
|
1169
|
+
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
1170
|
+
}
|
|
1171
|
+
const controller = new AbortController();
|
|
1172
|
+
const signal = AbortSignal.any([
|
|
1173
|
+
controller.signal, this.lifecycleController.signal, lease.controller.signal,
|
|
1174
|
+
...(optionsArg.signal ? [optionsArg.signal] : []),
|
|
1175
|
+
]);
|
|
1176
|
+
signal.throwIfAborted();
|
|
1177
|
+
const timeout = setTimeout(() => controller.abort(new BrowserRuntimeError('TIMEOUT')),
|
|
1178
|
+
Math.min(this.options.authorizationTimeoutMs, lease.capability.expiresAt - Date.now()));
|
|
1179
|
+
timeout.unref();
|
|
1180
|
+
const binding = this.describeCapabilityIdentity(lease.capability);
|
|
1181
|
+
if (binding.sessionId) Object.freeze(binding.sessionId);
|
|
1182
|
+
Object.freeze(binding);
|
|
1183
|
+
this.leaseRenewals.add(lease);
|
|
1184
|
+
const authorization = Promise.resolve().then(() => {
|
|
1185
|
+
signal.throwIfAborted();
|
|
1186
|
+
return this.options.authorizeCapability(binding, signal);
|
|
1187
|
+
});
|
|
1188
|
+
void authorization.then(
|
|
1189
|
+
() => this.leaseRenewals.delete(lease),
|
|
1190
|
+
() => this.leaseRenewals.delete(lease),
|
|
1191
|
+
);
|
|
1192
|
+
let abortListener: (() => void) | undefined;
|
|
1193
|
+
try {
|
|
1194
|
+
const authorized = await new Promise<boolean>((resolve, reject) => {
|
|
1195
|
+
abortListener = () => reject(signal.reason ?? new BrowserRuntimeError('ABORTED'));
|
|
1196
|
+
signal.addEventListener('abort', abortListener, { once: true });
|
|
1197
|
+
if (signal.aborted) abortListener();
|
|
1198
|
+
void authorization.then(resolve, () => reject(new BrowserRuntimeError('AUTHORIZATION_DENIED')));
|
|
1199
|
+
});
|
|
1200
|
+
signal.throwIfAborted();
|
|
1201
|
+
if (authorized !== true) throw new BrowserRuntimeError('AUTHORIZATION_DENIED');
|
|
1202
|
+
// Neither expiry, release, attachment change nor incarnation replacement may be revived.
|
|
1203
|
+
if (!this.isLeaseAuthorityCurrent(lease, authority)) throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
1204
|
+
const capability = lease.capability;
|
|
1205
|
+
const expiresAt = Date.now() + lifetimeMs;
|
|
1206
|
+
clearTimeout(capability.expiryTimer);
|
|
1207
|
+
capability.expiresAt = expiresAt;
|
|
1208
|
+
capability.expiryTimer = setTimeout(() => {
|
|
1209
|
+
if (capability.state === 'active') capability.state = 'expired';
|
|
1210
|
+
this.trackCleanup(this.revokeCapabilityRecord(capability));
|
|
1211
|
+
}, lifetimeMs);
|
|
1212
|
+
capability.expiryTimer.unref();
|
|
1213
|
+
return expiresAt;
|
|
1214
|
+
} finally {
|
|
1215
|
+
clearTimeout(timeout);
|
|
1216
|
+
if (abortListener) signal.removeEventListener('abort', abortListener);
|
|
1217
|
+
controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1133
1221
|
/** @internal */
|
|
1134
1222
|
public isLeaseAuthorityCurrent(
|
|
1135
1223
|
lease: ILeaseRecord,
|
|
@@ -1248,17 +1336,61 @@ export class BrowserRuntime {
|
|
|
1248
1336
|
lease: ILeaseRecord,
|
|
1249
1337
|
viewport: plugins.smartpuppeteer.ILiveBrowserViewport,
|
|
1250
1338
|
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
1251
|
-
): Promise<
|
|
1339
|
+
): Promise<IBrowserRuntimeViewportResult> {
|
|
1252
1340
|
this.requireHuman(lease);
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
'setViewport',
|
|
1256
|
-
'viewport',
|
|
1257
|
-
operationOptions,
|
|
1341
|
+
const preferredViewport = this.validatePreferredViewport(viewport);
|
|
1342
|
+
return this.runOperation(lease, 'setViewport', 'viewport', operationOptions,
|
|
1258
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();
|
|
1259
1390
|
await session.setViewport(viewport, { signal });
|
|
1260
|
-
|
|
1261
|
-
);
|
|
1391
|
+
}
|
|
1392
|
+
const state = session.getState();
|
|
1393
|
+
return { viewport: { ...state.viewport }, viewportRevision: state.viewportRevision };
|
|
1262
1394
|
}
|
|
1263
1395
|
|
|
1264
1396
|
/** @internal */
|
|
@@ -1269,7 +1401,7 @@ export class BrowserRuntime {
|
|
|
1269
1401
|
this.requireHuman(lease);
|
|
1270
1402
|
this.requireValidLease(lease);
|
|
1271
1403
|
const slot = lease.slot;
|
|
1272
|
-
const subscription = slot.
|
|
1404
|
+
const subscription = slot.frameSubscriptions.get(lease.leaseId);
|
|
1273
1405
|
if (!subscription || subscription.closed || subscription.lease !== lease) {
|
|
1274
1406
|
throw new BrowserRuntimeError('BUSY');
|
|
1275
1407
|
}
|
|
@@ -1308,17 +1440,161 @@ export class BrowserRuntime {
|
|
|
1308
1440
|
this.requireHuman(lease);
|
|
1309
1441
|
await this.runOperation(lease, action, 'raw-input', operationOptions, async (_signal, session) => {
|
|
1310
1442
|
if (action === 'dispatchMouse') {
|
|
1311
|
-
|
|
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);
|
|
1312
1465
|
} else if (action === 'dispatchWheel') {
|
|
1313
|
-
|
|
1466
|
+
const wheel = input as plugins.smartpuppeteer.ILiveBrowserWheelInput;
|
|
1467
|
+
await session.dispatchWheel({ ...wheel,
|
|
1468
|
+
modifiers: this.sharedModifiers(lease.slot, wheel.tabId, wheel.modifiers),
|
|
1469
|
+
});
|
|
1314
1470
|
} else if (action === 'dispatchKey') {
|
|
1315
|
-
|
|
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);
|
|
1316
1493
|
} else {
|
|
1317
1494
|
await session.insertText(input as plugins.smartpuppeteer.ILiveBrowserInsertTextInput);
|
|
1318
1495
|
}
|
|
1319
1496
|
});
|
|
1320
1497
|
}
|
|
1321
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
|
+
|
|
1322
1598
|
/** @internal */
|
|
1323
1599
|
public async subscribeLeaseEvents(
|
|
1324
1600
|
lease: ILeaseRecord,
|
|
@@ -1332,7 +1608,7 @@ export class BrowserRuntime {
|
|
|
1332
1608
|
let subscription: IFrameSubscriptionRecord;
|
|
1333
1609
|
try {
|
|
1334
1610
|
this.requireValidLease(lease);
|
|
1335
|
-
if (slot.
|
|
1611
|
+
if (slot.frameSubscriptions.has(lease.leaseId)) throw new BrowserRuntimeError('BUSY');
|
|
1336
1612
|
subscription = {
|
|
1337
1613
|
lease,
|
|
1338
1614
|
listener,
|
|
@@ -1340,7 +1616,7 @@ export class BrowserRuntime {
|
|
|
1340
1616
|
highestSequence: 0,
|
|
1341
1617
|
closed: false,
|
|
1342
1618
|
};
|
|
1343
|
-
slot.
|
|
1619
|
+
slot.frameSubscriptions.set(lease.leaseId, subscription);
|
|
1344
1620
|
this.pushToSubscription(subscription, {
|
|
1345
1621
|
type: 'state',
|
|
1346
1622
|
state: this.resourceState(slot.session!.getState()),
|
|
@@ -1359,45 +1635,13 @@ export class BrowserRuntime {
|
|
|
1359
1635
|
acknowledgementArg: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
1360
1636
|
): Promise<boolean> {
|
|
1361
1637
|
this.requireHuman(lease);
|
|
1362
|
-
const acknowledgement = this.validateFrameAcknowledgement(acknowledgementArg, true);
|
|
1363
|
-
const key = this.frameAcknowledgementKey(acknowledgement);
|
|
1364
|
-
const slot = lease.slot;
|
|
1365
|
-
const subscription = slot.frameSubscription;
|
|
1366
1638
|
this.requireValidLease(lease);
|
|
1367
|
-
|
|
1368
|
-
const
|
|
1369
|
-
if (!
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
slot,
|
|
1373
|
-
subscription,
|
|
1374
|
-
session,
|
|
1375
|
-
incarnationGeneration,
|
|
1376
|
-
);
|
|
1377
|
-
if (refresh?.admissionClosed) {
|
|
1378
|
-
subscription.outstanding.delete(key);
|
|
1379
|
-
if (outstanding.timer) clearTimeout(outstanding.timer);
|
|
1380
|
-
outstanding.timer = undefined;
|
|
1381
|
-
return false;
|
|
1382
|
-
}
|
|
1383
|
-
subscription.outstanding.delete(key);
|
|
1384
|
-
if (outstanding.timer) clearTimeout(outstanding.timer);
|
|
1385
|
-
outstanding.timer = undefined;
|
|
1386
|
-
const refreshOwned = Boolean(refresh && !refresh.abandoned);
|
|
1387
|
-
const outcome = await this.beginFrameAcknowledgement(
|
|
1388
|
-
session,
|
|
1389
|
-
outstanding.acknowledgement,
|
|
1390
|
-
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),
|
|
1391
1644
|
);
|
|
1392
|
-
if (outcome.status === 'fulfilled') return outcome.accepted;
|
|
1393
|
-
if (refreshOwned) return false;
|
|
1394
|
-
if (refresh && slot.frameRefresh === refresh) {
|
|
1395
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
1396
|
-
return false;
|
|
1397
|
-
}
|
|
1398
|
-
await this.options.beforeFrameFailureTermination?.();
|
|
1399
|
-
await this.handleFrameFailure(slot, session, incarnationGeneration, subscription.lease);
|
|
1400
|
-
return false;
|
|
1401
1645
|
}
|
|
1402
1646
|
|
|
1403
1647
|
/** @internal */
|
|
@@ -1581,19 +1825,21 @@ export class BrowserRuntime {
|
|
|
1581
1825
|
}
|
|
1582
1826
|
}
|
|
1583
1827
|
|
|
1584
|
-
private async
|
|
1828
|
+
private async acquireParticipantLease(
|
|
1585
1829
|
slot: IResourceSlot,
|
|
1586
1830
|
capability: TCapabilityRecord,
|
|
1587
1831
|
digest: plugins.Buffer,
|
|
1588
1832
|
acquisition: TAcquireBrowserRuntimeLeaseOptions,
|
|
1589
1833
|
): Promise<BrowserRuntimeLease> {
|
|
1590
|
-
|
|
1591
|
-
if (
|
|
1834
|
+
this.assertSlotAvailable(slot);
|
|
1835
|
+
if (capability.lease || capability.acquiring) throw new BrowserRuntimeError('BUSY');
|
|
1836
|
+
capability.acquiring = true;
|
|
1837
|
+
let release: (() => void) | undefined;
|
|
1592
1838
|
try {
|
|
1839
|
+
release = await slot.mutex.acquire();
|
|
1593
1840
|
this.assertSlotAvailable(slot);
|
|
1594
1841
|
this.resolveCapability(digest, this.validateExpectedCapabilityBinding(acquisition));
|
|
1595
1842
|
this.assertCapabilityStillAttached(capability, slot);
|
|
1596
|
-
if (slot.lease || slot.operation) throw new BrowserRuntimeError('BUSY');
|
|
1597
1843
|
await this.ensureSession(slot, acquisition.signal);
|
|
1598
1844
|
await this.options.beforeLeasePublication?.();
|
|
1599
1845
|
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
@@ -1603,84 +1849,11 @@ export class BrowserRuntime {
|
|
|
1603
1849
|
const lease = this.createLeaseRecord(slot, capability, digest);
|
|
1604
1850
|
return new BrowserRuntimeLease(this, lease);
|
|
1605
1851
|
} catch (error) {
|
|
1606
|
-
if (slot.session &&
|
|
1852
|
+
if (slot.session && slot.leases.size === 0) this.scheduleIdleTermination(slot);
|
|
1607
1853
|
throw error;
|
|
1608
1854
|
} finally {
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
private async acquireHumanLease(
|
|
1614
|
-
slot: IResourceSlot,
|
|
1615
|
-
capability: TCapabilityRecord,
|
|
1616
|
-
digest: plugins.Buffer,
|
|
1617
|
-
acquisition: TAcquireBrowserRuntimeLeaseOptions,
|
|
1618
|
-
): Promise<BrowserRuntimeLease> {
|
|
1619
|
-
const release = await slot.mutex.acquire();
|
|
1620
|
-
let generation = 0;
|
|
1621
|
-
let preemptedLease: ILeaseRecord | undefined;
|
|
1622
|
-
try {
|
|
1623
|
-
this.assertSlotAvailable(slot);
|
|
1624
|
-
this.resolveCapability(digest, this.validateExpectedCapabilityBinding(acquisition));
|
|
1625
|
-
this.assertCapabilityStillAttached(capability, slot);
|
|
1626
|
-
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
1627
|
-
if (slot.fencingGeneration !== undefined) throw new BrowserRuntimeError('BUSY');
|
|
1628
|
-
if (slot.lease?.role === 'human') throw new BrowserRuntimeError('BUSY');
|
|
1629
|
-
generation = ++slot.arbitrationGeneration;
|
|
1630
|
-
slot.fencingGeneration = generation;
|
|
1631
|
-
preemptedLease = slot.lease;
|
|
1632
|
-
if (preemptedLease) {
|
|
1633
|
-
preemptedLease.released = true;
|
|
1634
|
-
preemptedLease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1635
|
-
slot.operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1636
|
-
this.notifyFramedLeaseEnded(preemptedLease);
|
|
1637
|
-
}
|
|
1638
|
-
} finally {
|
|
1639
|
-
release();
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
try {
|
|
1643
|
-
if (preemptedLease) await this.quiesceSlot(slot);
|
|
1644
|
-
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
1645
|
-
const finalRelease = await slot.mutex.acquire();
|
|
1646
|
-
try {
|
|
1647
|
-
if (slot.arbitrationGeneration !== generation || slot.fencingGeneration !== generation) {
|
|
1648
|
-
throw new BrowserRuntimeError('BUSY');
|
|
1649
|
-
}
|
|
1650
|
-
if (preemptedLease) {
|
|
1651
|
-
if (preemptedLease.capability.lease === preemptedLease) {
|
|
1652
|
-
preemptedLease.capability.lease = undefined;
|
|
1653
|
-
}
|
|
1654
|
-
if (slot.lease === preemptedLease) slot.lease = undefined;
|
|
1655
|
-
}
|
|
1656
|
-
if (slot.permanentlyFenced) throw new BrowserRuntimeError('FENCED');
|
|
1657
|
-
slot.fencingGeneration = undefined;
|
|
1658
|
-
await this.ensureSession(slot, acquisition.signal);
|
|
1659
|
-
await this.options.beforeLeasePublication?.();
|
|
1660
|
-
this.throwIfAcquisitionAborted(acquisition.signal);
|
|
1661
|
-
this.assertSlotAvailable(slot);
|
|
1662
|
-
this.resolveCapability(digest, this.validateExpectedCapabilityBinding(acquisition));
|
|
1663
|
-
this.assertCapabilityStillAttached(capability, slot);
|
|
1664
|
-
const lease = this.createLeaseRecord(slot, capability, digest);
|
|
1665
|
-
return new BrowserRuntimeLease(this, lease);
|
|
1666
|
-
} finally {
|
|
1667
|
-
finalRelease();
|
|
1668
|
-
}
|
|
1669
|
-
} catch (error) {
|
|
1670
|
-
const cleanupRelease = await slot.mutex.acquire();
|
|
1671
|
-
try {
|
|
1672
|
-
if (preemptedLease) {
|
|
1673
|
-
if (preemptedLease.capability.lease === preemptedLease) {
|
|
1674
|
-
preemptedLease.capability.lease = undefined;
|
|
1675
|
-
}
|
|
1676
|
-
if (slot.lease === preemptedLease) slot.lease = undefined;
|
|
1677
|
-
}
|
|
1678
|
-
if (slot.fencingGeneration === generation) slot.fencingGeneration = undefined;
|
|
1679
|
-
if (slot.session && !slot.lease) this.scheduleIdleTermination(slot);
|
|
1680
|
-
} finally {
|
|
1681
|
-
cleanupRelease();
|
|
1682
|
-
}
|
|
1683
|
-
throw error;
|
|
1855
|
+
capability.acquiring = false;
|
|
1856
|
+
release?.();
|
|
1684
1857
|
}
|
|
1685
1858
|
}
|
|
1686
1859
|
|
|
@@ -1691,7 +1864,7 @@ export class BrowserRuntime {
|
|
|
1691
1864
|
): ILeaseRecord {
|
|
1692
1865
|
this.assertSlotAvailable(slot);
|
|
1693
1866
|
this.assertCapabilityStillAttached(capability, slot);
|
|
1694
|
-
if (capability.lease
|
|
1867
|
+
if (capability.lease) throw new BrowserRuntimeError('BUSY');
|
|
1695
1868
|
if (slot.idleTimer) {
|
|
1696
1869
|
clearTimeout(slot.idleTimer);
|
|
1697
1870
|
slot.idleTimer = undefined;
|
|
@@ -1711,8 +1884,10 @@ export class BrowserRuntime {
|
|
|
1711
1884
|
controller: new AbortController(),
|
|
1712
1885
|
authorityGeneration: slot.authorityGeneration,
|
|
1713
1886
|
released: false,
|
|
1887
|
+
heldKeys: new Map(),
|
|
1888
|
+
heldButtons: new Map(),
|
|
1714
1889
|
};
|
|
1715
|
-
slot.lease
|
|
1890
|
+
slot.leases.set(lease.leaseId, lease);
|
|
1716
1891
|
capability.lease = lease;
|
|
1717
1892
|
return lease;
|
|
1718
1893
|
}
|
|
@@ -1877,6 +2052,7 @@ export class BrowserRuntime {
|
|
|
1877
2052
|
operationOptions: IBrowserRuntimeOperationOptions,
|
|
1878
2053
|
execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<T>,
|
|
1879
2054
|
finalize?: () => Promise<void>,
|
|
2055
|
+
participantCleanup = false,
|
|
1880
2056
|
): Promise<T> {
|
|
1881
2057
|
if (!operationOptions || typeof operationOptions !== 'object') {
|
|
1882
2058
|
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -1891,16 +2067,21 @@ export class BrowserRuntime {
|
|
|
1891
2067
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1892
2068
|
if (operationOptions.signal?.aborted) throw new BrowserRuntimeError('ABORTED');
|
|
1893
2069
|
const slot = lease.slot;
|
|
1894
|
-
this.
|
|
2070
|
+
this.requireQueuedLease(lease, participantCleanup);
|
|
1895
2071
|
this.assertSlotAvailable(slot);
|
|
1896
|
-
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
|
+
)) {
|
|
1897
2078
|
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
1898
2079
|
}
|
|
1899
2080
|
const timeoutMs = operationOptions.timeoutMs === undefined
|
|
1900
2081
|
? this.options.operationTimeoutMs
|
|
1901
2082
|
: validateInteger(operationOptions.timeoutMs, 'timeoutMs', 100, 120_000);
|
|
1902
2083
|
const signal = AbortSignal.any([
|
|
1903
|
-
lease.controller.signal,
|
|
2084
|
+
...(participantCleanup ? [] : [lease.controller.signal]),
|
|
1904
2085
|
this.lifecycleController.signal,
|
|
1905
2086
|
operationOptions.signal ?? new AbortController().signal,
|
|
1906
2087
|
]);
|
|
@@ -1919,6 +2100,7 @@ export class BrowserRuntime {
|
|
|
1919
2100
|
resolve: (value) => resolve(value as T),
|
|
1920
2101
|
reject,
|
|
1921
2102
|
state: 'queued',
|
|
2103
|
+
participantCleanup,
|
|
1922
2104
|
};
|
|
1923
2105
|
const onQueuedAbort = (): void => {
|
|
1924
2106
|
if (queued.state !== 'queued' && queued.state !== 'starting') return;
|
|
@@ -1928,7 +2110,15 @@ export class BrowserRuntime {
|
|
|
1928
2110
|
}
|
|
1929
2111
|
queued.state = 'settled';
|
|
1930
2112
|
signal.removeEventListener('abort', onQueuedAbort);
|
|
1931
|
-
|
|
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
|
+
}
|
|
1932
2122
|
};
|
|
1933
2123
|
queued.onQueuedAbort = onQueuedAbort;
|
|
1934
2124
|
signal.addEventListener('abort', onQueuedAbort, { once: true });
|
|
@@ -1992,7 +2182,7 @@ export class BrowserRuntime {
|
|
|
1992
2182
|
try {
|
|
1993
2183
|
if (queued.state === 'settled') return undefined;
|
|
1994
2184
|
queued.signal.throwIfAborted();
|
|
1995
|
-
this.
|
|
2185
|
+
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
1996
2186
|
this.assertSlotAvailable(slot);
|
|
1997
2187
|
if (slot.operation) throw new BrowserRuntimeError('BUSY');
|
|
1998
2188
|
session = slot.session!;
|
|
@@ -2053,13 +2243,12 @@ export class BrowserRuntime {
|
|
|
2053
2243
|
let executionStarted = false;
|
|
2054
2244
|
let executionSettled = false;
|
|
2055
2245
|
try {
|
|
2056
|
-
await this.runBeforeOperation(operationIdentity, combinedSignal);
|
|
2246
|
+
if (!queued.participantCleanup) await this.runBeforeOperation(operationIdentity, combinedSignal);
|
|
2057
2247
|
const executionRelease = await slot.mutex.acquire();
|
|
2058
2248
|
try {
|
|
2059
2249
|
combinedSignal.throwIfAborted();
|
|
2060
|
-
this.
|
|
2250
|
+
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2061
2251
|
this.assertSlotAvailable(slot);
|
|
2062
|
-
this.assertCapabilityStillAttached(lease.capability, slot);
|
|
2063
2252
|
if (
|
|
2064
2253
|
slot.operation !== operation
|
|
2065
2254
|
|| slot.session !== session
|
|
@@ -2067,7 +2256,13 @@ export class BrowserRuntime {
|
|
|
2067
2256
|
|| slot.incarnationGeneration !== incarnationGeneration
|
|
2068
2257
|
) throw new BrowserRuntimeError('ABORTED');
|
|
2069
2258
|
executionStarted = true;
|
|
2070
|
-
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
|
+
})();
|
|
2071
2266
|
operation.promise = executionPromise.then(() => undefined, () => undefined).finally(() => {
|
|
2072
2267
|
executionSettled = true;
|
|
2073
2268
|
});
|
|
@@ -2085,8 +2280,7 @@ export class BrowserRuntime {
|
|
|
2085
2280
|
combinedSignal.removeEventListener('abort', onAbort);
|
|
2086
2281
|
}
|
|
2087
2282
|
if (combinedSignal.aborted) throw combinedSignal.reason;
|
|
2088
|
-
this.
|
|
2089
|
-
this.assertCapabilityStillAttached(lease.capability, slot);
|
|
2283
|
+
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2090
2284
|
if (slot.session !== session! || slot.arbitrationGeneration !== generation!) {
|
|
2091
2285
|
throw new BrowserRuntimeError('ABORTED');
|
|
2092
2286
|
}
|
|
@@ -2128,6 +2322,12 @@ export class BrowserRuntime {
|
|
|
2128
2322
|
failure = error;
|
|
2129
2323
|
}
|
|
2130
2324
|
}
|
|
2325
|
+
if (slot.frameFailure?.session === session) {
|
|
2326
|
+
try { await slot.frameFailure.promise; } catch (error) {
|
|
2327
|
+
failed = true;
|
|
2328
|
+
failure = error;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2131
2331
|
const errorCode = failed ? this.operationErrorCode(failure) : undefined;
|
|
2132
2332
|
await this.emitAudit(Object.freeze({
|
|
2133
2333
|
...operationIdentity,
|
|
@@ -2455,10 +2655,10 @@ export class BrowserRuntime {
|
|
|
2455
2655
|
): IFrameRefreshRecord {
|
|
2456
2656
|
if (
|
|
2457
2657
|
slot.frameRefresh
|
|
2458
|
-
|| slot.
|
|
2658
|
+
|| slot.frameSubscriptions.get(lease.leaseId) !== subscription
|
|
2459
2659
|
|| subscription.closed
|
|
2460
2660
|
|| subscription.lease !== lease
|
|
2461
|
-
|| slot.lease !== lease
|
|
2661
|
+
|| slot.leases.get(lease.leaseId) !== lease
|
|
2462
2662
|
|| slot.session !== session
|
|
2463
2663
|
) throw new BrowserRuntimeError('BUSY');
|
|
2464
2664
|
const state = session.getState();
|
|
@@ -2467,7 +2667,7 @@ export class BrowserRuntime {
|
|
|
2467
2667
|
!activeTab
|
|
2468
2668
|
|| !activeTab.active
|
|
2469
2669
|
|| activeTab.status !== 'open'
|
|
2470
|
-
||
|
|
2670
|
+
|| state.status !== 'running'
|
|
2471
2671
|
) throw new BrowserRuntimeError('BUSY');
|
|
2472
2672
|
const refresh: IFrameRefreshRecord = {
|
|
2473
2673
|
lease,
|
|
@@ -2477,7 +2677,7 @@ export class BrowserRuntime {
|
|
|
2477
2677
|
initialTabId: activeTab.id,
|
|
2478
2678
|
initialGeneration: activeTab.generation,
|
|
2479
2679
|
initialViewportRevision: state.viewportRevision,
|
|
2480
|
-
startingHighestSequence:
|
|
2680
|
+
startingHighestSequence: slot.highestFrameSequence,
|
|
2481
2681
|
pendingAcknowledgements: new Set(),
|
|
2482
2682
|
refreshStarted: false,
|
|
2483
2683
|
admissionClosed: false,
|
|
@@ -2527,17 +2727,17 @@ export class BrowserRuntime {
|
|
|
2527
2727
|
}
|
|
2528
2728
|
|
|
2529
2729
|
private async drainFrameRefreshOutstanding(refresh: IFrameRefreshRecord): Promise<void> {
|
|
2530
|
-
const
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
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');
|
|
2535
2740
|
}
|
|
2536
|
-
await Promise.all(outstanding.map((frame) => this.beginFrameAcknowledgement(
|
|
2537
|
-
frame.session,
|
|
2538
|
-
frame.acknowledgement,
|
|
2539
|
-
refresh,
|
|
2540
|
-
)));
|
|
2541
2741
|
}
|
|
2542
2742
|
|
|
2543
2743
|
private validateFrameRefreshBoundary(
|
|
@@ -2569,25 +2769,23 @@ export class BrowserRuntime {
|
|
|
2569
2769
|
refresh: IFrameRefreshRecord,
|
|
2570
2770
|
identity: plugins.smartpuppeteer.ILiveBrowserFrameIdentity,
|
|
2571
2771
|
): Promise<void> {
|
|
2572
|
-
const
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
));
|
|
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');
|
|
2589
2788
|
}
|
|
2590
|
-
await Promise.all(retirements);
|
|
2591
2789
|
}
|
|
2592
2790
|
|
|
2593
2791
|
private beginFrameAcknowledgement(
|
|
@@ -2621,14 +2819,14 @@ export class BrowserRuntime {
|
|
|
2621
2819
|
refresh: IFrameRefreshRecord,
|
|
2622
2820
|
): Promise<void> {
|
|
2623
2821
|
if (refresh.failure) {
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
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
|
+
}
|
|
2632
2830
|
if (!claimed) {
|
|
2633
2831
|
this.abandonFrameRefresh(slot, refresh);
|
|
2634
2832
|
throw new BrowserRuntimeError('ABORTED');
|
|
@@ -2643,7 +2841,7 @@ export class BrowserRuntime {
|
|
|
2643
2841
|
this.requireValidLease(refresh.lease);
|
|
2644
2842
|
current = slot.frameRefresh === refresh
|
|
2645
2843
|
&& !refresh.abandoned
|
|
2646
|
-
&& slot.
|
|
2844
|
+
&& slot.frameSubscriptions.get(refresh.lease.leaseId) === refresh.subscription
|
|
2647
2845
|
&& !refresh.subscription.closed
|
|
2648
2846
|
&& slot.session === refresh.session
|
|
2649
2847
|
&& slot.incarnationGeneration === refresh.incarnationGeneration;
|
|
@@ -2671,7 +2869,7 @@ export class BrowserRuntime {
|
|
|
2671
2869
|
refresh.abandoned = true;
|
|
2672
2870
|
slot.frameRefresh = undefined;
|
|
2673
2871
|
if (
|
|
2674
|
-
slot.
|
|
2872
|
+
slot.frameSubscriptions.get(refresh.lease.leaseId) === refresh.subscription
|
|
2675
2873
|
&& !refresh.subscription.closed
|
|
2676
2874
|
&& slot.session === refresh.session
|
|
2677
2875
|
&& slot.incarnationGeneration === refresh.incarnationGeneration
|
|
@@ -2689,274 +2887,140 @@ export class BrowserRuntime {
|
|
|
2689
2887
|
): void {
|
|
2690
2888
|
if (frame.timer || subscription.closed || !subscription.outstanding.has(key)) return;
|
|
2691
2889
|
const timer = setTimeout(() => {
|
|
2692
|
-
//
|
|
2693
|
-
|
|
2694
|
-
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));
|
|
2695
2892
|
}, this.options.frameAcknowledgementTimeoutMs);
|
|
2696
2893
|
timer.unref();
|
|
2697
2894
|
frame.timer = timer;
|
|
2698
2895
|
}
|
|
2699
2896
|
|
|
2700
|
-
private matchingFrameRefresh(
|
|
2701
|
-
slot: IResourceSlot,
|
|
2702
|
-
subscription: IFrameSubscriptionRecord,
|
|
2703
|
-
session: ILiveBrowserSessionLike,
|
|
2704
|
-
incarnationGeneration: number,
|
|
2705
|
-
): IFrameRefreshRecord | undefined {
|
|
2706
|
-
const refresh = slot.frameRefresh;
|
|
2707
|
-
return refresh
|
|
2708
|
-
&& !refresh.abandoned
|
|
2709
|
-
&& refresh.subscription === subscription
|
|
2710
|
-
&& refresh.lease === subscription.lease
|
|
2711
|
-
&& refresh.session === session
|
|
2712
|
-
&& refresh.incarnationGeneration === incarnationGeneration
|
|
2713
|
-
? refresh
|
|
2714
|
-
: undefined;
|
|
2715
|
-
}
|
|
2716
|
-
|
|
2717
2897
|
private handleSessionEvent(
|
|
2718
2898
|
slot: IResourceSlot,
|
|
2719
2899
|
session: ILiveBrowserSessionLike,
|
|
2720
2900
|
event: plugins.smartpuppeteer.TLiveBrowserEvent,
|
|
2721
2901
|
): void {
|
|
2722
|
-
if (slot.session && slot.session !== session)
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
sequence: event.frame.sequence,
|
|
2727
|
-
generation: event.frame.generation,
|
|
2728
|
-
viewportRevision: event.frame.viewportRevision,
|
|
2729
|
-
}));
|
|
2730
|
-
}
|
|
2731
|
-
return;
|
|
2732
|
-
}
|
|
2733
|
-
const subscription = slot.frameSubscription;
|
|
2734
|
-
const refreshForSession = slot.frameRefresh
|
|
2735
|
-
&& !slot.frameRefresh.abandoned
|
|
2736
|
-
&& slot.frameRefresh.session === session
|
|
2737
|
-
&& slot.frameRefresh.incarnationGeneration === slot.incarnationGeneration
|
|
2738
|
-
? slot.frameRefresh
|
|
2739
|
-
: 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;
|
|
2740
2906
|
if (event.type === 'frame') {
|
|
2741
2907
|
let acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest;
|
|
2742
2908
|
try {
|
|
2743
|
-
if (!(event.frame.data instanceof Uint8Array))
|
|
2744
|
-
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2745
|
-
}
|
|
2909
|
+
if (!(event.frame.data instanceof Uint8Array)) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2746
2910
|
acknowledgement = this.validateFrameAcknowledgement(event.frame);
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
|
|
2750
|
-
return;
|
|
2911
|
+
if (acknowledgement.sequence <= slot.highestFrameSequence) {
|
|
2912
|
+
throw new BrowserRuntimeError('FRAME_STREAM_FAILED');
|
|
2751
2913
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
slot,
|
|
2755
|
-
session,
|
|
2756
|
-
slot.incarnationGeneration,
|
|
2757
|
-
failedLease,
|
|
2758
|
-
).then(() => undefined));
|
|
2759
|
-
return;
|
|
2760
|
-
}
|
|
2761
|
-
if (!subscription || subscription.closed) {
|
|
2762
|
-
if (refreshForSession) {
|
|
2763
|
-
this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
|
|
2764
|
-
return;
|
|
2914
|
+
if (slot.producerAcknowledgements.size >= this.options.maxOutstandingFrames) {
|
|
2915
|
+
throw new BrowserRuntimeError('FRAME_STREAM_FAILED');
|
|
2765
2916
|
}
|
|
2766
|
-
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
2767
|
-
return;
|
|
2768
|
-
}
|
|
2769
|
-
const refresh = this.matchingFrameRefresh(
|
|
2770
|
-
slot,
|
|
2771
|
-
subscription,
|
|
2772
|
-
session,
|
|
2773
|
-
slot.incarnationGeneration,
|
|
2774
|
-
);
|
|
2775
|
-
if (refreshForSession && refreshForSession !== refresh) {
|
|
2776
|
-
this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
|
|
2777
|
-
return;
|
|
2778
|
-
}
|
|
2779
|
-
try {
|
|
2780
|
-
this.requireValidLease(subscription.lease);
|
|
2781
2917
|
} catch {
|
|
2782
|
-
if (refresh)
|
|
2783
|
-
|
|
2784
|
-
return;
|
|
2785
|
-
}
|
|
2786
|
-
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
2787
|
-
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));
|
|
2788
2920
|
return;
|
|
2789
2921
|
}
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
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) {
|
|
2793
2932
|
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2794
2933
|
return;
|
|
2795
2934
|
}
|
|
2796
|
-
this.
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
slot.incarnationGeneration,
|
|
2800
|
-
subscription.lease,
|
|
2801
|
-
).then(() => undefined));
|
|
2802
|
-
return;
|
|
2803
|
-
}
|
|
2804
|
-
subscription.highestSequence = acknowledgement.sequence;
|
|
2935
|
+
await this.options.beforeFrameFailureTermination?.();
|
|
2936
|
+
await this.handleFrameFailure(slot, session, incarnationGeneration);
|
|
2937
|
+
}));
|
|
2805
2938
|
if (event.frame.data.byteLength > this.options.maxFrameBytes) {
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
if (refresh) {
|
|
2809
|
-
this.failFrameRefresh(refresh, 'FRAME_TOO_LARGE');
|
|
2939
|
+
if (refresh) this.failFrameRefresh(refresh, 'FRAME_TOO_LARGE');
|
|
2940
|
+
for (const subscription of slot.frameSubscriptions.values()) {
|
|
2810
2941
|
this.pushToSubscription(subscription, {
|
|
2811
2942
|
type: 'error',
|
|
2812
|
-
error: { code: 'FRAME_TOO_LARGE', fatal: false, tabId:
|
|
2943
|
+
error: { code: 'FRAME_TOO_LARGE', fatal: false, tabId: acknowledgement.tabId },
|
|
2813
2944
|
});
|
|
2814
|
-
return;
|
|
2815
2945
|
}
|
|
2816
|
-
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
2817
|
-
this.pushToSubscription(subscription, {
|
|
2818
|
-
type: 'error',
|
|
2819
|
-
error: { code: 'FRAME_TOO_LARGE', fatal: false, tabId: event.frame.tabId },
|
|
2820
|
-
});
|
|
2821
2946
|
return;
|
|
2822
2947
|
}
|
|
2823
|
-
if (refresh?.admissionClosed)
|
|
2824
|
-
const refreshAdmission = refresh && !refresh.admissionClosed && !refresh.abandoned
|
|
2825
|
-
? refresh
|
|
2826
|
-
: undefined;
|
|
2827
|
-
if (refreshAdmission) {
|
|
2828
|
-
if (refreshAdmission.failure) return;
|
|
2948
|
+
if (refresh?.refreshStarted && !refresh.boundaryCandidate && !refresh.admissionClosed) {
|
|
2829
2949
|
if (
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
} else {
|
|
2837
|
-
if (refresh && subscription.outstanding.size >= this.options.maxOutstandingFrames) {
|
|
2838
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2839
|
-
}
|
|
2840
|
-
while (subscription.outstanding.size >= this.options.maxOutstandingFrames) {
|
|
2841
|
-
const oldestKey = subscription.outstanding.keys().next().value as string | undefined;
|
|
2842
|
-
if (!oldestKey) break;
|
|
2843
|
-
this.trackCleanup(
|
|
2844
|
-
this.retireOutstandingFrame(subscription, oldestKey, false).then(() => undefined),
|
|
2845
|
-
);
|
|
2846
|
-
}
|
|
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');
|
|
2847
2956
|
}
|
|
2848
|
-
const
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
if (!refreshAdmission) this.armFrameAcknowledgementTimer(subscription, key, outstanding);
|
|
2855
|
-
const delivered = this.pushToSubscription(subscription, { type: 'frame', frame: event.frame });
|
|
2856
|
-
if (
|
|
2857
|
-
delivered
|
|
2858
|
-
&& refreshAdmission
|
|
2859
|
-
&& refreshAdmission.refreshStarted
|
|
2860
|
-
&& !refreshAdmission.boundaryCandidate
|
|
2861
|
-
) {
|
|
2862
|
-
if (
|
|
2863
|
-
acknowledgement.tabId === refreshAdmission.initialTabId
|
|
2864
|
-
&& acknowledgement.sequence > refreshAdmission.startingHighestSequence
|
|
2865
|
-
&& acknowledgement.generation > refreshAdmission.initialGeneration
|
|
2866
|
-
&& acknowledgement.viewportRevision === refreshAdmission.initialViewportRevision
|
|
2867
|
-
) {
|
|
2868
|
-
refreshAdmission.boundaryCandidate = { ...acknowledgement };
|
|
2869
|
-
} else {
|
|
2870
|
-
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));
|
|
2871
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 });
|
|
2872
2971
|
}
|
|
2873
2972
|
return;
|
|
2874
2973
|
}
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
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'));
|
|
2894
2998
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
&& !subscription.closed
|
|
2899
|
-
&& subscription.lease === fatalLease
|
|
2900
|
-
? subscription
|
|
2901
|
-
: undefined;
|
|
2902
|
-
if (refreshForSession?.lease === fatalLease) {
|
|
2903
|
-
this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
|
|
2904
|
-
}
|
|
2905
|
-
this.invalidateCapabilityRecord(fatalLease.capability);
|
|
2906
|
-
try {
|
|
2907
|
-
fatalSubscription?.listener(runtimeEvent);
|
|
2908
|
-
} catch {
|
|
2909
|
-
// Fatal revocation already owns cleanup for a failing listener.
|
|
2910
|
-
} finally {
|
|
2911
|
-
this.trackCleanup(this.revokeCapabilityRecord(fatalLease.capability));
|
|
2912
|
-
}
|
|
2913
|
-
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. */ }
|
|
2914
3002
|
}
|
|
2915
|
-
|
|
2916
|
-
this.pushToSubscription(subscription, runtimeEvent);
|
|
3003
|
+
this.trackCleanup(failure.then(() => undefined));
|
|
2917
3004
|
return;
|
|
2918
3005
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
3006
|
+
for (const subscription of [...slot.frameSubscriptions.values()]) {
|
|
3007
|
+
this.pushToSubscription(subscription, runtimeEvent);
|
|
3008
|
+
}
|
|
2921
3009
|
}
|
|
2922
3010
|
|
|
2923
3011
|
private pushToSubscription(
|
|
2924
3012
|
subscription: IFrameSubscriptionRecord,
|
|
2925
3013
|
event: TBrowserRuntimeEvent,
|
|
2926
3014
|
): boolean {
|
|
3015
|
+
if (subscription.closed) return false;
|
|
2927
3016
|
try {
|
|
2928
3017
|
this.requireValidLease(subscription.lease);
|
|
2929
3018
|
subscription.listener(event);
|
|
2930
3019
|
return true;
|
|
2931
3020
|
} catch {
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
const refresh = session
|
|
2936
|
-
? this.matchingFrameRefresh(slot, subscription, session, incarnationGeneration)
|
|
2937
|
-
: undefined;
|
|
2938
|
-
if (event.type === 'frame') {
|
|
2939
|
-
const key = this.frameAcknowledgementKey(this.validateFrameAcknowledgement(event.frame));
|
|
2940
|
-
this.trackCleanup(this.retireOutstandingFrame(subscription, key, true).then(() => undefined));
|
|
2941
|
-
return false;
|
|
2942
|
-
}
|
|
2943
|
-
if (refresh) {
|
|
2944
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2945
|
-
return false;
|
|
2946
|
-
}
|
|
2947
|
-
if (!session) {
|
|
2948
|
-
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
2949
|
-
return false;
|
|
2950
|
-
}
|
|
2951
|
-
this.trackCleanup((async () => {
|
|
2952
|
-
await this.options.beforeFrameFailureTermination?.();
|
|
2953
|
-
await this.handleFrameFailure(
|
|
2954
|
-
slot,
|
|
2955
|
-
session,
|
|
2956
|
-
incarnationGeneration,
|
|
2957
|
-
subscription.lease,
|
|
2958
|
-
);
|
|
2959
|
-
})());
|
|
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));
|
|
2960
3024
|
return false;
|
|
2961
3025
|
}
|
|
2962
3026
|
}
|
|
@@ -2965,60 +3029,28 @@ export class BrowserRuntime {
|
|
|
2965
3029
|
if (subscription.closed) return;
|
|
2966
3030
|
subscription.closed = true;
|
|
2967
3031
|
const slot = subscription.lease.slot;
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
3032
|
+
if (slot.frameRefresh?.subscription === subscription) {
|
|
3033
|
+
this.abandonFrameRefresh(slot, slot.frameRefresh);
|
|
2971
3034
|
}
|
|
2972
|
-
if (slot.
|
|
2973
|
-
|
|
2974
|
-
await this.retireOutstandingFrame(subscription, key, false);
|
|
3035
|
+
if (slot.frameSubscriptions.get(subscription.lease.leaseId) === subscription) {
|
|
3036
|
+
slot.frameSubscriptions.delete(subscription.lease.leaseId);
|
|
2975
3037
|
}
|
|
3038
|
+
for (const frame of subscription.outstanding.values()) {
|
|
3039
|
+
if (frame.timer) clearTimeout(frame.timer);
|
|
3040
|
+
}
|
|
3041
|
+
subscription.outstanding.clear();
|
|
2976
3042
|
}
|
|
2977
3043
|
|
|
2978
3044
|
private async retireOutstandingFrame(
|
|
2979
3045
|
subscription: IFrameSubscriptionRecord,
|
|
2980
3046
|
key: string,
|
|
2981
|
-
failLease: boolean,
|
|
2982
3047
|
): Promise<boolean> {
|
|
2983
3048
|
const outstanding = subscription.outstanding.get(key);
|
|
2984
3049
|
if (!outstanding) return false;
|
|
2985
3050
|
subscription.outstanding.delete(key);
|
|
2986
3051
|
if (outstanding.timer) clearTimeout(outstanding.timer);
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
const refresh = this.matchingFrameRefresh(
|
|
2990
|
-
slot,
|
|
2991
|
-
subscription,
|
|
2992
|
-
outstanding.session,
|
|
2993
|
-
outstanding.incarnationGeneration,
|
|
2994
|
-
);
|
|
2995
|
-
if (refresh?.admissionClosed) {
|
|
2996
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
2997
|
-
return false;
|
|
2998
|
-
}
|
|
2999
|
-
if (refresh && failLease) this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
3000
|
-
const refreshOwned = Boolean(refresh && !refresh.abandoned);
|
|
3001
|
-
const outcome = await this.beginFrameAcknowledgement(
|
|
3002
|
-
outstanding.session,
|
|
3003
|
-
outstanding.acknowledgement,
|
|
3004
|
-
refreshOwned ? refresh : undefined,
|
|
3005
|
-
);
|
|
3006
|
-
if (outcome.status === 'fulfilled' && !failLease) return outcome.accepted;
|
|
3007
|
-
if (refreshOwned) return outcome.status === 'fulfilled' ? outcome.accepted : false;
|
|
3008
|
-
if (refresh && slot.frameRefresh === refresh) {
|
|
3009
|
-
if (outcome.status !== 'fulfilled') {
|
|
3010
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
3011
|
-
}
|
|
3012
|
-
return outcome.status === 'fulfilled' ? outcome.accepted : false;
|
|
3013
|
-
}
|
|
3014
|
-
await this.options.beforeFrameFailureTermination?.();
|
|
3015
|
-
await this.handleFrameFailure(
|
|
3016
|
-
slot,
|
|
3017
|
-
outstanding.session,
|
|
3018
|
-
outstanding.incarnationGeneration,
|
|
3019
|
-
subscription.lease,
|
|
3020
|
-
);
|
|
3021
|
-
return outcome.status === 'fulfilled' ? outcome.accepted : false;
|
|
3052
|
+
const outcome = await outstanding.producerAcknowledgement;
|
|
3053
|
+
return outcome.status === 'fulfilled' && outcome.accepted;
|
|
3022
3054
|
}
|
|
3023
3055
|
|
|
3024
3056
|
private validateFrameAcknowledgement(
|
|
@@ -3080,130 +3112,73 @@ export class BrowserRuntime {
|
|
|
3080
3112
|
}
|
|
3081
3113
|
}
|
|
3082
3114
|
|
|
3083
|
-
private async
|
|
3115
|
+
private async handleFrameFailure(
|
|
3084
3116
|
slot: IResourceSlot,
|
|
3085
3117
|
session: ILiveBrowserSessionLike,
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
): Promise<
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
const
|
|
3092
|
-
const
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
const refreshOwned = Boolean(refresh && !refresh.admissionClosed && !refresh.abandoned);
|
|
3097
|
-
const outcome = await this.beginFrameAcknowledgement(
|
|
3098
|
-
session,
|
|
3099
|
-
acknowledgement,
|
|
3100
|
-
refreshOwned ? refresh : undefined,
|
|
3101
|
-
);
|
|
3102
|
-
if (outcome.status === 'fulfilled' && !failLease) return;
|
|
3103
|
-
if (refreshOwned) return;
|
|
3104
|
-
if (refresh && slot.frameRefresh === refresh) {
|
|
3105
|
-
if (outcome.status !== 'fulfilled') {
|
|
3106
|
-
this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
|
|
3107
|
-
}
|
|
3108
|
-
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;
|
|
3109
3128
|
}
|
|
3110
|
-
await this.options.beforeFrameFailureTermination?.();
|
|
3111
|
-
await this.handleFrameFailure(slot, session, incarnationGeneration, failedLease);
|
|
3112
3129
|
}
|
|
3113
3130
|
|
|
3114
|
-
private async
|
|
3131
|
+
private async handleFrameFailureInternal(
|
|
3115
3132
|
slot: IResourceSlot,
|
|
3116
3133
|
session: ILiveBrowserSessionLike,
|
|
3117
3134
|
incarnationGeneration: number,
|
|
3118
|
-
expectedLease: ILeaseRecord | null,
|
|
3119
3135
|
expectedRefresh?: IFrameRefreshRecord,
|
|
3120
3136
|
): Promise<boolean> {
|
|
3121
|
-
let transitionGeneration = 0;
|
|
3122
|
-
let ownsTransitionFence = false;
|
|
3123
|
-
const lease = expectedLease ?? undefined;
|
|
3124
3137
|
const release = await slot.mutex.acquire();
|
|
3138
|
+
let generation: number;
|
|
3139
|
+
let leases: ILeaseRecord[];
|
|
3125
3140
|
try {
|
|
3126
|
-
const exactLeaseIsCurrent = expectedLease === null
|
|
3127
|
-
? slot.lease === undefined
|
|
3128
|
-
: slot.lease === expectedLease && expectedLease.capability.lease === expectedLease;
|
|
3129
|
-
let leaseAttachmentIsCurrent = true;
|
|
3130
|
-
if (lease) {
|
|
3131
|
-
try {
|
|
3132
|
-
this.assertCapabilityStillAttached(lease.capability, slot);
|
|
3133
|
-
} catch {
|
|
3134
|
-
leaseAttachmentIsCurrent = false;
|
|
3135
|
-
}
|
|
3136
|
-
}
|
|
3137
|
-
const borrowsLeaseReleaseFence = Boolean(
|
|
3138
|
-
lease
|
|
3139
|
-
&& exactLeaseIsCurrent
|
|
3140
|
-
&& lease.releaseGeneration !== undefined
|
|
3141
|
-
&& slot.fencingGeneration === lease.releaseGeneration,
|
|
3142
|
-
);
|
|
3143
3141
|
if (
|
|
3144
3142
|
slot.session !== session
|
|
3145
3143
|
|| slot.incarnationGeneration !== incarnationGeneration
|
|
3146
|
-
|| slot.permanentlyFenced
|
|
3147
|
-
|| slot.
|
|
3148
|
-
|| slot.
|
|
3149
|
-
|| slot.attachmentFenceCount > 0
|
|
3150
|
-
|| slot.attachmentRetryBinding
|
|
3151
|
-
|| !exactLeaseIsCurrent
|
|
3152
|
-
|| !leaseAttachmentIsCurrent
|
|
3153
|
-
|| (expectedRefresh !== undefined && slot.frameRefresh !== expectedRefresh)
|
|
3154
|
-
|| (slot.fencingGeneration !== undefined && !borrowsLeaseReleaseFence)
|
|
3144
|
+
|| slot.permanentlyFenced || slot.retirementPending
|
|
3145
|
+
|| slot.terminationFenceGeneration !== undefined || slot.fencingGeneration !== undefined
|
|
3146
|
+
|| (expectedRefresh && slot.frameRefresh !== expectedRefresh)
|
|
3155
3147
|
) return false;
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
if (borrowsLeaseReleaseFence) {
|
|
3163
|
-
transitionGeneration = slot.fencingGeneration!;
|
|
3164
|
-
} else {
|
|
3165
|
-
transitionGeneration = ++slot.arbitrationGeneration;
|
|
3166
|
-
slot.fencingGeneration = transitionGeneration;
|
|
3167
|
-
ownsTransitionFence = true;
|
|
3168
|
-
}
|
|
3169
|
-
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);
|
|
3170
3154
|
lease.released = true;
|
|
3171
3155
|
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3156
|
+
this.notifyFramedLeaseEnded(lease);
|
|
3172
3157
|
}
|
|
3173
|
-
|
|
3174
|
-
slot.operation?.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3175
|
-
}
|
|
3158
|
+
slot.operation?.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3176
3159
|
} finally {
|
|
3177
3160
|
release();
|
|
3178
3161
|
}
|
|
3179
|
-
if (lease) this.trackFrameCapabilityRevocation(lease);
|
|
3180
3162
|
await this.options.afterFrameFailureFence?.();
|
|
3181
|
-
|
|
3163
|
+
await Promise.all(leases.map((lease) => this.closeFrameSubscriptionIfLease(lease)));
|
|
3182
3164
|
const errors: unknown[] = [];
|
|
3183
3165
|
await this.quiesceSlot(slot).catch((error) => errors.push(error));
|
|
3184
3166
|
await this.terminateSlotSession(slot, session).catch((error) => errors.push(error));
|
|
3185
3167
|
const finalRelease = await slot.mutex.acquire();
|
|
3186
3168
|
try {
|
|
3187
|
-
if (
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
&& slot.session !== session
|
|
3192
|
-
) {
|
|
3193
|
-
if (ownsTransitionFence && lease && slot.lease === lease) slot.lease = undefined;
|
|
3194
|
-
if (ownsTransitionFence && lease && lease.capability.lease === lease) {
|
|
3195
|
-
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;
|
|
3196
3173
|
}
|
|
3197
|
-
|
|
3198
|
-
|
|
3174
|
+
slot.operation = undefined;
|
|
3175
|
+
slot.fencingGeneration = undefined;
|
|
3199
3176
|
}
|
|
3200
3177
|
} finally {
|
|
3201
3178
|
finalRelease();
|
|
3202
3179
|
}
|
|
3203
|
-
|
|
3204
|
-
if (errors.length
|
|
3205
|
-
throw new AggregateError(errors, 'Frame acknowledgement cleanup is incomplete');
|
|
3206
|
-
}
|
|
3180
|
+
for (const lease of leases) this.trackFrameCapabilityRevocation(lease);
|
|
3181
|
+
if (errors.length) throw new AggregateError(errors, 'Frame producer cleanup is incomplete');
|
|
3207
3182
|
return true;
|
|
3208
3183
|
}
|
|
3209
3184
|
|
|
@@ -3216,6 +3191,14 @@ export class BrowserRuntime {
|
|
|
3216
3191
|
this.trackCleanup(operation);
|
|
3217
3192
|
}
|
|
3218
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
|
+
|
|
3219
3202
|
private requireValidLease(lease: ILeaseRecord): void {
|
|
3220
3203
|
if (lease.released || lease.controller.signal.aborted) {
|
|
3221
3204
|
throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
@@ -3227,7 +3210,7 @@ export class BrowserRuntime {
|
|
|
3227
3210
|
if (
|
|
3228
3211
|
capability !== lease.capability
|
|
3229
3212
|
|| capability.lease !== lease
|
|
3230
|
-
|| lease.slot.lease !== lease
|
|
3213
|
+
|| lease.slot.leases.get(lease.leaseId) !== lease
|
|
3231
3214
|
|| !lease.slot.session
|
|
3232
3215
|
) {
|
|
3233
3216
|
throw new BrowserRuntimeError('CAPABILITY_REVOKED');
|
|
@@ -3350,57 +3333,58 @@ export class BrowserRuntime {
|
|
|
3350
3333
|
}
|
|
3351
3334
|
|
|
3352
3335
|
private async releaseLeaseRecordInternal(lease: ILeaseRecord): Promise<void> {
|
|
3353
|
-
if (lease.released && lease.slot.lease !== lease) return;
|
|
3354
3336
|
const slot = lease.slot;
|
|
3337
|
+
if (slot.leases.get(lease.leaseId) !== lease) return;
|
|
3355
3338
|
const release = await slot.mutex.acquire();
|
|
3356
|
-
|
|
3357
|
-
const generation = lease.releaseGeneration
|
|
3358
|
-
?? existingFence
|
|
3359
|
-
?? ++slot.arbitrationGeneration;
|
|
3360
|
-
lease.releaseGeneration = generation;
|
|
3361
|
-
lease.releaseOwnsFence ??= existingFence === undefined;
|
|
3339
|
+
let operation: IOperationRecord | undefined;
|
|
3362
3340
|
try {
|
|
3363
|
-
if (existingFence !== undefined) {
|
|
3364
|
-
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3365
|
-
} else {
|
|
3366
|
-
slot.fencingGeneration = generation;
|
|
3367
|
-
}
|
|
3368
3341
|
lease.released = true;
|
|
3369
3342
|
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
}
|
|
3343
|
+
operation = slot.operation?.lease === lease ? slot.operation : undefined;
|
|
3344
|
+
operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3373
3345
|
} finally {
|
|
3374
3346
|
release();
|
|
3375
3347
|
}
|
|
3376
3348
|
await this.closeFrameSubscriptionIfLease(lease);
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
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
|
+
}
|
|
3380
3357
|
}
|
|
3358
|
+
if (slot.frameRefresh?.lease === lease) this.abandonFrameRefresh(slot, slot.frameRefresh);
|
|
3381
3359
|
if (
|
|
3382
|
-
slot.permanentlyFenced
|
|
3383
|
-
&&
|
|
3384
|
-
|
|
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
|
+
}
|
|
3385
3374
|
const finalRelease = await slot.mutex.acquire();
|
|
3386
3375
|
try {
|
|
3387
3376
|
if (lease.capability.state === 'active') this.notifyFramedLeaseEnded(lease);
|
|
3388
|
-
|
|
3377
|
+
slot.leases.delete(lease.leaseId);
|
|
3389
3378
|
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
3390
|
-
if (
|
|
3391
|
-
slot.fencingGeneration = undefined;
|
|
3392
|
-
}
|
|
3393
|
-
lease.releaseGeneration = undefined;
|
|
3394
|
-
lease.releaseOwnsFence = undefined;
|
|
3395
|
-
this.scheduleIdleTermination(slot);
|
|
3379
|
+
if (slot.leases.size === 0) this.scheduleIdleTermination(slot);
|
|
3396
3380
|
} finally {
|
|
3397
3381
|
finalRelease();
|
|
3398
3382
|
}
|
|
3399
3383
|
}
|
|
3400
3384
|
|
|
3401
3385
|
private async closeFrameSubscriptionIfLease(lease: ILeaseRecord): Promise<void> {
|
|
3402
|
-
const subscription = lease.slot.
|
|
3403
|
-
if (subscription
|
|
3386
|
+
const subscription = lease.slot.frameSubscriptions.get(lease.leaseId);
|
|
3387
|
+
if (subscription) await this.closeFrameSubscription(subscription);
|
|
3404
3388
|
}
|
|
3405
3389
|
|
|
3406
3390
|
private async quiesceSlot(slot: IResourceSlot): Promise<void> {
|
|
@@ -3436,6 +3420,12 @@ export class BrowserRuntime {
|
|
|
3436
3420
|
return;
|
|
3437
3421
|
}
|
|
3438
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
|
+
}
|
|
3439
3429
|
const release = await slot.mutex.acquire();
|
|
3440
3430
|
try {
|
|
3441
3431
|
if (
|
|
@@ -3488,6 +3478,8 @@ export class BrowserRuntime {
|
|
|
3488
3478
|
slot.unsubscribeSession?.();
|
|
3489
3479
|
slot.unsubscribeSession = undefined;
|
|
3490
3480
|
slot.session = undefined;
|
|
3481
|
+
slot.highestFrameSequence = 0;
|
|
3482
|
+
slot.producerAcknowledgements.clear();
|
|
3491
3483
|
}
|
|
3492
3484
|
if (slot.proxy) {
|
|
3493
3485
|
await this.stopProxyBounded(slot.proxy).then(
|
|
@@ -3505,7 +3497,7 @@ export class BrowserRuntime {
|
|
|
3505
3497
|
}
|
|
3506
3498
|
|
|
3507
3499
|
private scheduleIdleTermination(slot: IResourceSlot): void {
|
|
3508
|
-
if (slot.terminationFenceGeneration !== undefined || slot.retirementPending) return;
|
|
3500
|
+
if (slot.leases.size > 0 || slot.terminationFenceGeneration !== undefined || slot.retirementPending) return;
|
|
3509
3501
|
if (slot.idleTimer) clearTimeout(slot.idleTimer);
|
|
3510
3502
|
const arbitrationGeneration = slot.arbitrationGeneration;
|
|
3511
3503
|
const incarnationGeneration = slot.incarnationGeneration;
|
|
@@ -3549,7 +3541,7 @@ export class BrowserRuntime {
|
|
|
3549
3541
|
): Promise<void> {
|
|
3550
3542
|
const release = await slot.mutex.acquire();
|
|
3551
3543
|
let generation = 0;
|
|
3552
|
-
let
|
|
3544
|
+
let leases: ILeaseRecord[];
|
|
3553
3545
|
try {
|
|
3554
3546
|
if (
|
|
3555
3547
|
(expected?.arbitrationGeneration !== undefined
|
|
@@ -3557,48 +3549,34 @@ export class BrowserRuntime {
|
|
|
3557
3549
|
|| (expected?.incarnationGeneration !== undefined
|
|
3558
3550
|
&& slot.incarnationGeneration !== expected.incarnationGeneration)
|
|
3559
3551
|
|| (expected?.session !== undefined && slot.session !== expected.session)
|
|
3560
|
-
|| (expected?.requireIdle && slot.
|
|
3552
|
+
|| (expected?.requireIdle && slot.leases.size > 0)
|
|
3561
3553
|
) return;
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
if (slot.lease) {
|
|
3572
|
-
slot.lease.released = true;
|
|
3573
|
-
slot.lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3574
|
-
if (!stopping && !attachmentFenceOwnsTransition) {
|
|
3575
|
-
this.notifyFramedLeaseEnded(slot.lease);
|
|
3576
|
-
}
|
|
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);
|
|
3577
3563
|
}
|
|
3578
3564
|
slot.operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3579
3565
|
} finally {
|
|
3580
3566
|
release();
|
|
3581
3567
|
}
|
|
3582
|
-
if (attachmentFenceOwnsTransition) {
|
|
3583
|
-
await this.quiesceSlot(slot);
|
|
3584
|
-
if (slot.lease) await this.closeFrameSubscriptionIfLease(slot.lease);
|
|
3585
|
-
await this.terminateSlotSession(slot, expected?.session);
|
|
3586
|
-
return;
|
|
3587
|
-
}
|
|
3588
|
-
if (generation === 0) return;
|
|
3589
3568
|
await this.quiesceSlot(slot);
|
|
3590
|
-
|
|
3569
|
+
await Promise.all(leases.map((lease) => this.closeFrameSubscriptionIfLease(lease)));
|
|
3591
3570
|
await this.terminateSlotSession(slot, expected?.session);
|
|
3592
3571
|
const finalRelease = await slot.mutex.acquire();
|
|
3593
3572
|
try {
|
|
3594
3573
|
if (slot.arbitrationGeneration !== generation || slot.fencingGeneration !== generation) {
|
|
3595
3574
|
throw new BrowserRuntimeError('BUSY');
|
|
3596
3575
|
}
|
|
3597
|
-
const lease
|
|
3598
|
-
|
|
3599
|
-
|
|
3576
|
+
for (const lease of leases) {
|
|
3577
|
+
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
3578
|
+
slot.leases.delete(lease.leaseId);
|
|
3600
3579
|
}
|
|
3601
|
-
slot.lease = undefined;
|
|
3602
3580
|
slot.operation = undefined;
|
|
3603
3581
|
slot.fencingGeneration = undefined;
|
|
3604
3582
|
} finally {
|
|
@@ -3642,7 +3620,6 @@ export class BrowserRuntime {
|
|
|
3642
3620
|
if (slot.retirementPending) throw new BrowserRuntimeError('RESOURCE_RETIRED');
|
|
3643
3621
|
if (slot.permanentlyFenced) throw new BrowserRuntimeError('FENCED');
|
|
3644
3622
|
if (slot.terminationFenceGeneration !== undefined) throw new BrowserRuntimeError('BUSY');
|
|
3645
|
-
if (slot.attachmentFenceCount > 0) throw new BrowserRuntimeError('BUSY');
|
|
3646
3623
|
if (slot.fencingGeneration !== undefined) throw new BrowserRuntimeError('BUSY');
|
|
3647
3624
|
}
|
|
3648
3625
|
|
|
@@ -3685,54 +3662,45 @@ export class BrowserRuntime {
|
|
|
3685
3662
|
if (binding.attachmentRevision < current.attachmentRevision) {
|
|
3686
3663
|
throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3687
3664
|
}
|
|
3688
|
-
if (
|
|
3689
|
-
binding
|
|
3690
|
-
&& !slot.attachmentRetryBinding
|
|
3691
|
-
) {
|
|
3692
|
-
if (!this.bindingsEqual(binding, current)) {
|
|
3693
|
-
throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3694
|
-
}
|
|
3665
|
+
if (binding.attachmentRevision === current.attachmentRevision && !slot.attachmentRetryBinding) {
|
|
3666
|
+
if (!this.bindingsEqual(binding, current)) throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3695
3667
|
return this.describeResource(slot);
|
|
3696
3668
|
}
|
|
3697
|
-
if (
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
) throw new BrowserRuntimeError('BUSY');
|
|
3669
|
+
if (slot.attachmentRetryBinding && !this.bindingsEqual(binding, slot.attachmentRetryBinding)) {
|
|
3670
|
+
throw new BrowserRuntimeError('BUSY');
|
|
3671
|
+
}
|
|
3701
3672
|
const release = await slot.mutex.acquire();
|
|
3702
|
-
let fenceGeneration = 0;
|
|
3703
3673
|
try {
|
|
3704
|
-
fenceGeneration = ++slot.arbitrationGeneration;
|
|
3705
|
-
slot.fencingGeneration = fenceGeneration;
|
|
3706
3674
|
slot.attachmentBinding = binding;
|
|
3707
3675
|
slot.attachmentRetryBinding = binding;
|
|
3708
|
-
slot.
|
|
3709
|
-
|
|
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
|
+
}
|
|
3710
3684
|
} finally {
|
|
3711
3685
|
release();
|
|
3712
3686
|
}
|
|
3713
3687
|
const peerResults = await Promise.allSettled(
|
|
3714
3688
|
this.framedPeersForResource(slot).map((peer) => peer.close()),
|
|
3715
3689
|
);
|
|
3716
|
-
const capabilities = this.capabilitiesForResource(slot);
|
|
3717
3690
|
const capabilityResults = await Promise.allSettled(
|
|
3718
|
-
|
|
3691
|
+
this.capabilitiesForResource(slot).filter((record) => record.role === 'agent')
|
|
3692
|
+
.map((record) => this.revokeCapabilityRecord(record)),
|
|
3719
3693
|
);
|
|
3720
3694
|
const errors = [...peerResults, ...capabilityResults]
|
|
3721
3695
|
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
3722
3696
|
.map((result) => result.reason);
|
|
3723
|
-
await this.quiesceSlot(slot).catch((error) => errors.push(error));
|
|
3724
3697
|
const finalRelease = await slot.mutex.acquire();
|
|
3725
3698
|
try {
|
|
3726
|
-
if (
|
|
3727
|
-
slot.fencingGeneration = undefined;
|
|
3728
|
-
slot.attachmentRetryBinding = undefined;
|
|
3729
|
-
}
|
|
3699
|
+
if (errors.length === 0) slot.attachmentRetryBinding = undefined;
|
|
3730
3700
|
} finally {
|
|
3731
3701
|
finalRelease();
|
|
3732
3702
|
}
|
|
3733
|
-
if (errors.length
|
|
3734
|
-
throw new AggregateError(errors, 'Attachment fencing is incomplete');
|
|
3735
|
-
}
|
|
3703
|
+
if (errors.length) throw new AggregateError(errors, 'Attachment fencing is incomplete');
|
|
3736
3704
|
return this.describeResource(slot);
|
|
3737
3705
|
}
|
|
3738
3706
|
|
|
@@ -3902,6 +3870,9 @@ export class BrowserRuntime {
|
|
|
3902
3870
|
|| binding.attachmentRevision !== expected.attachmentRevision
|
|
3903
3871
|
) throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3904
3872
|
if (expected.role === 'agent') {
|
|
3873
|
+
if (slot.attachmentFenceCount > 0 || slot.attachmentRetryBinding) {
|
|
3874
|
+
throw new BrowserRuntimeError('BUSY');
|
|
3875
|
+
}
|
|
3905
3876
|
if (!binding.sessionId || !this.sessionsEqual(binding.sessionId, expected.sessionId)) {
|
|
3906
3877
|
throw new BrowserRuntimeError('ATTACHMENT_CONFLICT');
|
|
3907
3878
|
}
|
|
@@ -3918,6 +3889,7 @@ export class BrowserRuntime {
|
|
|
3918
3889
|
capability.projectId !== slot.projectId
|
|
3919
3890
|
|| capability.browserResourceId !== slot.browserResourceId
|
|
3920
3891
|
) throw new BrowserRuntimeError('CAPABILITY_INVALID');
|
|
3892
|
+
if (capability.role === 'human') return;
|
|
3921
3893
|
try {
|
|
3922
3894
|
this.assertCapabilityBinding(slot, capability);
|
|
3923
3895
|
} catch {
|
|
@@ -3950,7 +3922,7 @@ export class BrowserRuntime {
|
|
|
3950
3922
|
|| sessionId?.harnessId !== 'flex'))
|
|
3951
3923
|
|| (source === 'mcp' && (scopeId !== undefined || channelId !== undefined
|
|
3952
3924
|
|| runId !== undefined
|
|
3953
|
-
|| sessionId
|
|
3925
|
+
|| sessionId === undefined))
|
|
3954
3926
|
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
3955
3927
|
|| runId !== undefined))
|
|
3956
3928
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -3984,7 +3956,8 @@ export class BrowserRuntime {
|
|
|
3984
3956
|
runId: runId!,
|
|
3985
3957
|
};
|
|
3986
3958
|
}
|
|
3987
|
-
if (role === 'agent' && source === 'mcp'
|
|
3959
|
+
if (role === 'agent' && source === 'mcp'
|
|
3960
|
+
&& sessionId !== undefined) {
|
|
3988
3961
|
return { ...base, role, source, sessionId };
|
|
3989
3962
|
}
|
|
3990
3963
|
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -4022,7 +3995,7 @@ export class BrowserRuntime {
|
|
|
4022
3995
|
if (
|
|
4023
3996
|
capability.role === 'agent'
|
|
4024
3997
|
&& capability.source === 'mcp'
|
|
4025
|
-
&& capability.sessionId
|
|
3998
|
+
&& capability.sessionId !== undefined
|
|
4026
3999
|
) {
|
|
4027
4000
|
return {
|
|
4028
4001
|
...base,
|
|
@@ -4128,6 +4101,15 @@ export class BrowserRuntimeLease {
|
|
|
4128
4101
|
return this.record.capability.capabilityId;
|
|
4129
4102
|
}
|
|
4130
4103
|
|
|
4104
|
+
public get expiresAt(): number {
|
|
4105
|
+
return this.record.capability.expiresAt;
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
/** Reauthorize this exact live lease without replacing its frame stream or browser incarnation. */
|
|
4109
|
+
public renew(options?: IBrowserRuntimeLeaseRenewalOptions): Promise<number> {
|
|
4110
|
+
return this.runtime.renewLease(this.record, options);
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4131
4113
|
public get role(): TBrowserActorRole {
|
|
4132
4114
|
return this.record.role;
|
|
4133
4115
|
}
|
|
@@ -4219,7 +4201,7 @@ export class BrowserRuntimeLease {
|
|
|
4219
4201
|
public setViewport(
|
|
4220
4202
|
viewport: plugins.smartpuppeteer.ILiveBrowserViewport,
|
|
4221
4203
|
operationOptions?: IBrowserRuntimeOperationOptions,
|
|
4222
|
-
): Promise<
|
|
4204
|
+
): Promise<IBrowserRuntimeViewportResult> {
|
|
4223
4205
|
return this.runtime.leaseSetViewport(this.record, viewport, operationOptions);
|
|
4224
4206
|
}
|
|
4225
4207
|
|