@modelprofile.com/browser-runtime 3.0.1 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -146,6 +146,7 @@ interface IQueuedOperationRecord {
146
146
  onOperationStarted?: (operationId: string) => void;
147
147
  signal: AbortSignal;
148
148
  execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<unknown>;
149
+ finalize?: () => Promise<void>;
149
150
  resolve(value: unknown): void;
150
151
  reject(error: unknown): void;
151
152
  state: 'queued' | 'starting' | 'active' | 'settled';
@@ -154,7 +155,7 @@ interface IQueuedOperationRecord {
154
155
 
155
156
  interface IOutstandingFrameRecord {
156
157
  acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest;
157
- timer: ReturnType<typeof setTimeout>;
158
+ timer?: ReturnType<typeof setTimeout>;
158
159
  session: ILiveBrowserSessionLike;
159
160
  incarnationGeneration: number;
160
161
  }
@@ -172,6 +173,23 @@ interface IFrameSubscriptionRecord {
172
173
  closed: boolean;
173
174
  }
174
175
 
176
+ interface IFrameRefreshRecord {
177
+ lease: ILeaseRecord;
178
+ subscription: IFrameSubscriptionRecord;
179
+ session: ILiveBrowserSessionLike;
180
+ incarnationGeneration: number;
181
+ initialTabId: string;
182
+ initialGeneration: number;
183
+ initialViewportRevision: number;
184
+ startingHighestSequence: number;
185
+ pendingAcknowledgements: Set<Promise<TFrameAcknowledgementOutcome>>;
186
+ refreshStarted: boolean;
187
+ admissionClosed: boolean;
188
+ abandoned: boolean;
189
+ boundaryCandidate?: plugins.smartpuppeteer.ILiveBrowserFrameIdentity;
190
+ failure?: BrowserRuntimeError;
191
+ }
192
+
175
193
  interface IResourceSlot {
176
194
  projectId: string;
177
195
  browserResourceId: string;
@@ -195,6 +213,7 @@ interface IResourceSlot {
195
213
  operationQueue: IQueuedOperationRecord[];
196
214
  operationSchedulerRunning: boolean;
197
215
  frameSubscription?: IFrameSubscriptionRecord;
216
+ frameRefresh?: IFrameRefreshRecord;
198
217
  idleTimer?: ReturnType<typeof setTimeout>;
199
218
  lifecycleTail: Promise<void>;
200
219
  lifecycleOperationCount: number;
@@ -959,11 +978,14 @@ export class BrowserRuntime {
959
978
 
960
979
  public async disconnectPeer(peerIdArg: string): Promise<void> {
961
980
  const peerId = this.validateIdentifier(peerIdArg, 'peerId');
962
- const matching = [...this.capabilitiesById.values()].filter((record) => (
963
- record.peerId === peerId && record.state === 'active'
964
- ));
981
+ const matching = new Set([
982
+ ...[...this.capabilitiesById.values()].filter((record) => (
983
+ record.peerId === peerId
984
+ )),
985
+ ...[...this.failedRevocations].filter((record) => record.peerId === peerId),
986
+ ]);
965
987
  const results = await Promise.allSettled(
966
- matching.map((record) => this.revokeCapabilityRecord(record, false)),
988
+ [...matching].map((record) => this.revokeCapabilityRecord(record, false)),
967
989
  );
968
990
  const errors = results
969
991
  .filter((result): result is PromiseRejectedResult => result.status === 'rejected')
@@ -1153,6 +1175,39 @@ export class BrowserRuntime {
1153
1175
  );
1154
1176
  }
1155
1177
 
1178
+ /** @internal */
1179
+ public async leaseRefreshFrameStream(
1180
+ lease: ILeaseRecord,
1181
+ operationOptions: IBrowserRuntimeOperationOptions = {},
1182
+ ): Promise<void> {
1183
+ this.requireHuman(lease);
1184
+ this.requireValidLease(lease);
1185
+ const slot = lease.slot;
1186
+ const subscription = slot.frameSubscription;
1187
+ if (!subscription || subscription.closed || subscription.lease !== lease) {
1188
+ throw new BrowserRuntimeError('BUSY');
1189
+ }
1190
+ let refresh: IFrameRefreshRecord | undefined;
1191
+ return this.runOperation(
1192
+ lease,
1193
+ 'refreshFrameStream',
1194
+ 'frame-stream',
1195
+ operationOptions,
1196
+ async (signal, session) => {
1197
+ refresh = this.beginFrameRefresh(
1198
+ slot,
1199
+ lease,
1200
+ subscription,
1201
+ session,
1202
+ );
1203
+ await this.executeFrameRefresh(refresh, signal);
1204
+ },
1205
+ async () => {
1206
+ if (refresh) await this.finalizeFrameRefresh(slot, refresh);
1207
+ },
1208
+ );
1209
+ }
1210
+
1156
1211
  /** @internal */
1157
1212
  public async leaseDispatchRawInput(
1158
1213
  lease: ILeaseRecord,
@@ -1191,7 +1246,7 @@ export class BrowserRuntime {
1191
1246
  let subscription: IFrameSubscriptionRecord;
1192
1247
  try {
1193
1248
  this.requireValidLease(lease);
1194
- if (slot.frameSubscription) throw new BrowserRuntimeError('BUSY');
1249
+ if (slot.frameSubscription || slot.frameRefresh) throw new BrowserRuntimeError('BUSY');
1195
1250
  subscription = {
1196
1251
  lease,
1197
1252
  listener,
@@ -1226,14 +1281,34 @@ export class BrowserRuntime {
1226
1281
  if (!subscription || subscription.lease !== lease) return false;
1227
1282
  const outstanding = subscription.outstanding.get(key);
1228
1283
  if (!outstanding) return false;
1229
- subscription.outstanding.delete(key);
1230
- clearTimeout(outstanding.timer);
1231
1284
  const { session, incarnationGeneration } = outstanding;
1232
- const outcome = await this.waitForFrameAcknowledgement(
1285
+ const refresh = this.matchingFrameRefresh(
1286
+ slot,
1287
+ subscription,
1288
+ session,
1289
+ incarnationGeneration,
1290
+ );
1291
+ if (refresh?.admissionClosed) {
1292
+ subscription.outstanding.delete(key);
1293
+ if (outstanding.timer) clearTimeout(outstanding.timer);
1294
+ outstanding.timer = undefined;
1295
+ return false;
1296
+ }
1297
+ subscription.outstanding.delete(key);
1298
+ if (outstanding.timer) clearTimeout(outstanding.timer);
1299
+ outstanding.timer = undefined;
1300
+ const refreshOwned = Boolean(refresh && !refresh.abandoned);
1301
+ const outcome = await this.beginFrameAcknowledgement(
1233
1302
  session,
1234
1303
  outstanding.acknowledgement,
1304
+ refreshOwned ? refresh : undefined,
1235
1305
  );
1236
1306
  if (outcome.status === 'fulfilled') return outcome.accepted;
1307
+ if (refreshOwned) return false;
1308
+ if (refresh && slot.frameRefresh === refresh) {
1309
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
1310
+ return false;
1311
+ }
1237
1312
  await this.options.beforeFrameFailureTermination?.();
1238
1313
  await this.handleFrameFailure(slot, session, incarnationGeneration, subscription.lease);
1239
1314
  return false;
@@ -1641,6 +1716,7 @@ export class BrowserRuntime {
1641
1716
  classification: TBrowserRuntimeOperationClassification,
1642
1717
  operationOptions: IBrowserRuntimeOperationOptions,
1643
1718
  execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<T>,
1719
+ finalize?: () => Promise<void>,
1644
1720
  ): Promise<T> {
1645
1721
  if (!operationOptions || typeof operationOptions !== 'object') {
1646
1722
  throw new BrowserRuntimeError('INVALID_INPUT');
@@ -1679,6 +1755,7 @@ export class BrowserRuntime {
1679
1755
  onOperationStarted: operationOptions.onOperationStarted,
1680
1756
  signal,
1681
1757
  execute: execute as IQueuedOperationRecord['execute'],
1758
+ finalize,
1682
1759
  resolve: (value) => resolve(value as T),
1683
1760
  reject,
1684
1761
  state: 'queued',
@@ -1814,6 +1891,7 @@ export class BrowserRuntime {
1814
1891
  let failed = false;
1815
1892
  let failure: unknown;
1816
1893
  let executionStarted = false;
1894
+ let executionSettled = false;
1817
1895
  try {
1818
1896
  await this.runBeforeOperation(operationIdentity, combinedSignal);
1819
1897
  const executionRelease = await slot.mutex.acquire();
@@ -1830,7 +1908,9 @@ export class BrowserRuntime {
1830
1908
  ) throw new BrowserRuntimeError('ABORTED');
1831
1909
  executionStarted = true;
1832
1910
  executionPromise = queued.execute(combinedSignal, session);
1833
- operation.promise = executionPromise.then(() => undefined, () => undefined);
1911
+ operation.promise = executionPromise.then(() => undefined, () => undefined).finally(() => {
1912
+ executionSettled = true;
1913
+ });
1834
1914
  } finally {
1835
1915
  executionRelease();
1836
1916
  }
@@ -1876,6 +1956,18 @@ export class BrowserRuntime {
1876
1956
  } else {
1877
1957
  await cleanupSlot();
1878
1958
  }
1959
+ if (
1960
+ queued.finalize
1961
+ && (!executionStarted || executionSettled)
1962
+ && slot.operation !== operation
1963
+ ) {
1964
+ try {
1965
+ await queued.finalize();
1966
+ } catch (error) {
1967
+ failed = true;
1968
+ failure = error;
1969
+ }
1970
+ }
1879
1971
  const errorCode = failed ? this.operationErrorCode(failure) : undefined;
1880
1972
  await this.emitAudit(Object.freeze({
1881
1973
  ...operationIdentity,
@@ -2194,6 +2286,271 @@ export class BrowserRuntime {
2194
2286
  };
2195
2287
  }
2196
2288
 
2289
+ private beginFrameRefresh(
2290
+ slot: IResourceSlot,
2291
+ lease: ILeaseRecord,
2292
+ subscription: IFrameSubscriptionRecord,
2293
+ session: ILiveBrowserSessionLike,
2294
+ ): IFrameRefreshRecord {
2295
+ if (
2296
+ slot.frameRefresh
2297
+ || slot.frameSubscription !== subscription
2298
+ || subscription.closed
2299
+ || subscription.lease !== lease
2300
+ || slot.lease !== lease
2301
+ || slot.session !== session
2302
+ ) throw new BrowserRuntimeError('BUSY');
2303
+ const state = session.getState();
2304
+ const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId);
2305
+ if (
2306
+ !activeTab
2307
+ || !activeTab.active
2308
+ || activeTab.status !== 'open'
2309
+ || !activeTab.streaming
2310
+ ) throw new BrowserRuntimeError('BUSY');
2311
+ const refresh: IFrameRefreshRecord = {
2312
+ lease,
2313
+ subscription,
2314
+ session,
2315
+ incarnationGeneration: slot.incarnationGeneration,
2316
+ initialTabId: activeTab.id,
2317
+ initialGeneration: activeTab.generation,
2318
+ initialViewportRevision: state.viewportRevision,
2319
+ startingHighestSequence: subscription.highestSequence,
2320
+ pendingAcknowledgements: new Set(),
2321
+ refreshStarted: false,
2322
+ admissionClosed: false,
2323
+ abandoned: false,
2324
+ };
2325
+ slot.frameRefresh = refresh;
2326
+ return refresh;
2327
+ }
2328
+
2329
+ private async executeFrameRefresh(
2330
+ refresh: IFrameRefreshRecord,
2331
+ signal: AbortSignal,
2332
+ ): Promise<void> {
2333
+ let operationError: unknown;
2334
+ try {
2335
+ await this.drainFrameRefreshOutstanding(refresh);
2336
+ if (refresh.failure) throw refresh.failure;
2337
+ signal.throwIfAborted();
2338
+ refresh.refreshStarted = true;
2339
+ const identity = this.validateFrameAcknowledgement(
2340
+ await refresh.session.refreshScreencast({ signal }),
2341
+ true,
2342
+ );
2343
+ this.validateFrameRefreshBoundary(refresh, identity);
2344
+ await this.reconcileFrameRefreshOutstanding(refresh, identity);
2345
+ if (refresh.failure) throw refresh.failure;
2346
+ } catch (error) {
2347
+ operationError = error;
2348
+ if (signal.aborted && refresh.refreshStarted) {
2349
+ try {
2350
+ const candidate = refresh.boundaryCandidate;
2351
+ if (!candidate) throw new BrowserRuntimeError('FRAME_STREAM_FAILED');
2352
+ this.validateFrameRefreshBoundary(refresh, candidate);
2353
+ await this.reconcileFrameRefreshOutstanding(refresh, candidate);
2354
+ } catch {
2355
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2356
+ }
2357
+ } else if (!signal.aborted && !refresh.failure) {
2358
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2359
+ }
2360
+ } finally {
2361
+ refresh.admissionClosed = true;
2362
+ await Promise.allSettled([...refresh.pendingAcknowledgements]);
2363
+ }
2364
+ if (refresh.failure) throw refresh.failure;
2365
+ if (operationError !== undefined) throw operationError;
2366
+ }
2367
+
2368
+ private async drainFrameRefreshOutstanding(refresh: IFrameRefreshRecord): Promise<void> {
2369
+ const outstanding = [...refresh.subscription.outstanding.values()];
2370
+ refresh.subscription.outstanding.clear();
2371
+ for (const frame of outstanding) {
2372
+ if (frame.timer) clearTimeout(frame.timer);
2373
+ frame.timer = undefined;
2374
+ }
2375
+ await Promise.all(outstanding.map((frame) => this.beginFrameAcknowledgement(
2376
+ frame.session,
2377
+ frame.acknowledgement,
2378
+ refresh,
2379
+ )));
2380
+ }
2381
+
2382
+ private validateFrameRefreshBoundary(
2383
+ refresh: IFrameRefreshRecord,
2384
+ identity: plugins.smartpuppeteer.ILiveBrowserFrameIdentity,
2385
+ ): void {
2386
+ const candidate = refresh.boundaryCandidate;
2387
+ const state = refresh.session.getState();
2388
+ const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId);
2389
+ if (
2390
+ !candidate
2391
+ || this.frameAcknowledgementKey(candidate) !== this.frameAcknowledgementKey(identity)
2392
+ || identity.tabId !== refresh.initialTabId
2393
+ || identity.sequence <= refresh.startingHighestSequence
2394
+ || identity.generation <= refresh.initialGeneration
2395
+ || identity.viewportRevision !== refresh.initialViewportRevision
2396
+ || state.activeTabId !== refresh.initialTabId
2397
+ || state.viewportRevision !== refresh.initialViewportRevision
2398
+ || !activeTab
2399
+ || !activeTab.active
2400
+ || activeTab.status !== 'open'
2401
+ || !activeTab.streaming
2402
+ || activeTab.generation !== identity.generation
2403
+ || activeTab.appliedViewportRevision !== identity.viewportRevision
2404
+ ) throw new BrowserRuntimeError('FRAME_STREAM_FAILED');
2405
+ }
2406
+
2407
+ private async reconcileFrameRefreshOutstanding(
2408
+ refresh: IFrameRefreshRecord,
2409
+ identity: plugins.smartpuppeteer.ILiveBrowserFrameIdentity,
2410
+ ): Promise<void> {
2411
+ const retirements: Array<Promise<TFrameAcknowledgementOutcome>> = [];
2412
+ for (const [key, frame] of refresh.subscription.outstanding) {
2413
+ const acknowledgement = frame.acknowledgement;
2414
+ if (
2415
+ acknowledgement.tabId === identity.tabId
2416
+ && acknowledgement.generation === identity.generation
2417
+ && acknowledgement.viewportRevision === identity.viewportRevision
2418
+ && acknowledgement.sequence >= identity.sequence
2419
+ ) continue;
2420
+ refresh.subscription.outstanding.delete(key);
2421
+ if (frame.timer) clearTimeout(frame.timer);
2422
+ frame.timer = undefined;
2423
+ retirements.push(this.beginFrameAcknowledgement(
2424
+ frame.session,
2425
+ acknowledgement,
2426
+ refresh,
2427
+ ));
2428
+ }
2429
+ await Promise.all(retirements);
2430
+ }
2431
+
2432
+ private beginFrameAcknowledgement(
2433
+ session: ILiveBrowserSessionLike,
2434
+ acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
2435
+ refresh?: IFrameRefreshRecord,
2436
+ ): Promise<TFrameAcknowledgementOutcome> {
2437
+ const operation = this.waitForFrameAcknowledgement(session, acknowledgement);
2438
+ if (!refresh || refresh.admissionClosed || refresh.abandoned) return operation;
2439
+ const tracked = operation.then((outcome) => {
2440
+ if (outcome.status !== 'fulfilled') this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2441
+ return outcome;
2442
+ });
2443
+ refresh.pendingAcknowledgements.add(tracked);
2444
+ void tracked.then(
2445
+ () => refresh.pendingAcknowledgements.delete(tracked),
2446
+ () => refresh.pendingAcknowledgements.delete(tracked),
2447
+ );
2448
+ return tracked;
2449
+ }
2450
+
2451
+ private failFrameRefresh(
2452
+ refresh: IFrameRefreshRecord,
2453
+ code: 'FRAME_STREAM_FAILED' | 'FRAME_TOO_LARGE',
2454
+ ): void {
2455
+ refresh.failure ??= new BrowserRuntimeError(code);
2456
+ }
2457
+
2458
+ private async finalizeFrameRefresh(
2459
+ slot: IResourceSlot,
2460
+ refresh: IFrameRefreshRecord,
2461
+ ): Promise<void> {
2462
+ if (refresh.failure) {
2463
+ await this.options.beforeFrameFailureTermination?.();
2464
+ const claimed = await this.handleFrameFailure(
2465
+ slot,
2466
+ refresh.session,
2467
+ refresh.incarnationGeneration,
2468
+ refresh.lease,
2469
+ refresh,
2470
+ );
2471
+ if (!claimed) {
2472
+ this.abandonFrameRefresh(slot, refresh);
2473
+ throw new BrowserRuntimeError('ABORTED');
2474
+ }
2475
+ throw refresh.failure;
2476
+ }
2477
+
2478
+ let current = false;
2479
+ const release = await slot.mutex.acquire();
2480
+ try {
2481
+ try {
2482
+ this.requireValidLease(refresh.lease);
2483
+ current = slot.frameRefresh === refresh
2484
+ && !refresh.abandoned
2485
+ && slot.frameSubscription === refresh.subscription
2486
+ && !refresh.subscription.closed
2487
+ && slot.session === refresh.session
2488
+ && slot.incarnationGeneration === refresh.incarnationGeneration;
2489
+ } catch {
2490
+ current = false;
2491
+ }
2492
+ if (current) {
2493
+ slot.frameRefresh = undefined;
2494
+ for (const [key, frame] of refresh.subscription.outstanding) {
2495
+ if (!frame.timer) this.armFrameAcknowledgementTimer(refresh.subscription, key, frame);
2496
+ }
2497
+ } else {
2498
+ this.abandonFrameRefresh(slot, refresh);
2499
+ }
2500
+ } finally {
2501
+ release();
2502
+ }
2503
+ if (!current) throw new BrowserRuntimeError('ABORTED');
2504
+ }
2505
+
2506
+ private abandonFrameRefresh(slot: IResourceSlot, expected?: IFrameRefreshRecord): void {
2507
+ const refresh = slot.frameRefresh;
2508
+ if (!refresh || (expected && refresh !== expected)) return;
2509
+ refresh.admissionClosed = true;
2510
+ refresh.abandoned = true;
2511
+ slot.frameRefresh = undefined;
2512
+ if (
2513
+ slot.frameSubscription === refresh.subscription
2514
+ && !refresh.subscription.closed
2515
+ && slot.session === refresh.session
2516
+ && slot.incarnationGeneration === refresh.incarnationGeneration
2517
+ ) {
2518
+ for (const [key, frame] of refresh.subscription.outstanding) {
2519
+ if (!frame.timer) this.armFrameAcknowledgementTimer(refresh.subscription, key, frame);
2520
+ }
2521
+ }
2522
+ }
2523
+
2524
+ private armFrameAcknowledgementTimer(
2525
+ subscription: IFrameSubscriptionRecord,
2526
+ key: string,
2527
+ frame: IOutstandingFrameRecord,
2528
+ ): void {
2529
+ if (frame.timer || subscription.closed || !subscription.outstanding.has(key)) return;
2530
+ const timer = setTimeout(() => {
2531
+ this.trackCleanup(this.retireOutstandingFrame(subscription, key, true).then(() => undefined));
2532
+ }, this.options.frameAcknowledgementTimeoutMs);
2533
+ timer.unref();
2534
+ frame.timer = timer;
2535
+ }
2536
+
2537
+ private matchingFrameRefresh(
2538
+ slot: IResourceSlot,
2539
+ subscription: IFrameSubscriptionRecord,
2540
+ session: ILiveBrowserSessionLike,
2541
+ incarnationGeneration: number,
2542
+ ): IFrameRefreshRecord | undefined {
2543
+ const refresh = slot.frameRefresh;
2544
+ return refresh
2545
+ && !refresh.abandoned
2546
+ && refresh.subscription === subscription
2547
+ && refresh.lease === subscription.lease
2548
+ && refresh.session === session
2549
+ && refresh.incarnationGeneration === incarnationGeneration
2550
+ ? refresh
2551
+ : undefined;
2552
+ }
2553
+
2197
2554
  private handleSessionEvent(
2198
2555
  slot: IResourceSlot,
2199
2556
  session: ILiveBrowserSessionLike,
@@ -2211,6 +2568,12 @@ export class BrowserRuntime {
2211
2568
  return;
2212
2569
  }
2213
2570
  const subscription = slot.frameSubscription;
2571
+ const refreshForSession = slot.frameRefresh
2572
+ && !slot.frameRefresh.abandoned
2573
+ && slot.frameRefresh.session === session
2574
+ && slot.frameRefresh.incarnationGeneration === slot.incarnationGeneration
2575
+ ? slot.frameRefresh
2576
+ : undefined;
2214
2577
  if (event.type === 'frame') {
2215
2578
  let acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest;
2216
2579
  try {
@@ -2219,38 +2582,72 @@ export class BrowserRuntime {
2219
2582
  }
2220
2583
  acknowledgement = this.validateFrameAcknowledgement(event.frame);
2221
2584
  } catch {
2585
+ if (refreshForSession) {
2586
+ this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
2587
+ return;
2588
+ }
2222
2589
  const failedLease = slot.session === session ? (slot.lease ?? null) : null;
2223
2590
  this.trackCleanup(this.handleFrameFailure(
2224
2591
  slot,
2225
2592
  session,
2226
2593
  slot.incarnationGeneration,
2227
2594
  failedLease,
2228
- ));
2595
+ ).then(() => undefined));
2229
2596
  return;
2230
2597
  }
2231
2598
  if (!subscription || subscription.closed) {
2599
+ if (refreshForSession) {
2600
+ this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
2601
+ return;
2602
+ }
2232
2603
  this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
2233
2604
  return;
2234
2605
  }
2606
+ const refresh = this.matchingFrameRefresh(
2607
+ slot,
2608
+ subscription,
2609
+ session,
2610
+ slot.incarnationGeneration,
2611
+ );
2612
+ if (refreshForSession && refreshForSession !== refresh) {
2613
+ this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
2614
+ return;
2615
+ }
2235
2616
  try {
2236
2617
  this.requireValidLease(subscription.lease);
2237
2618
  } catch {
2619
+ if (refresh) {
2620
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2621
+ return;
2622
+ }
2238
2623
  this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
2239
2624
  this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
2240
2625
  return;
2241
2626
  }
2242
2627
  const key = this.frameAcknowledgementKey(acknowledgement);
2243
2628
  if (acknowledgement.sequence <= subscription.highestSequence) {
2629
+ if (refresh) {
2630
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2631
+ return;
2632
+ }
2244
2633
  this.trackCleanup(this.handleFrameFailure(
2245
2634
  slot,
2246
2635
  session,
2247
2636
  slot.incarnationGeneration,
2248
2637
  subscription.lease,
2249
- ));
2638
+ ).then(() => undefined));
2250
2639
  return;
2251
2640
  }
2252
2641
  subscription.highestSequence = acknowledgement.sequence;
2253
2642
  if (event.frame.data.byteLength > this.options.maxFrameBytes) {
2643
+ if (refresh) {
2644
+ this.failFrameRefresh(refresh, 'FRAME_TOO_LARGE');
2645
+ this.pushToSubscription(subscription, {
2646
+ type: 'error',
2647
+ error: { code: 'FRAME_TOO_LARGE', fatal: true, tabId: event.frame.tabId },
2648
+ });
2649
+ return;
2650
+ }
2254
2651
  this.trackCleanup(this.acknowledgeFrameInBackground(
2255
2652
  slot,
2256
2653
  session,
@@ -2263,28 +2660,64 @@ export class BrowserRuntime {
2263
2660
  });
2264
2661
  return;
2265
2662
  }
2266
- while (subscription.outstanding.size >= this.options.maxOutstandingFrames) {
2267
- const oldestKey = subscription.outstanding.keys().next().value as string | undefined;
2268
- if (!oldestKey) break;
2269
- this.trackCleanup(
2270
- this.retireOutstandingFrame(subscription, oldestKey, false).then(() => undefined),
2271
- );
2663
+ if (refresh?.admissionClosed) return;
2664
+ const refreshAdmission = refresh && !refresh.admissionClosed && !refresh.abandoned
2665
+ ? refresh
2666
+ : undefined;
2667
+ if (refreshAdmission) {
2668
+ if (refreshAdmission.failure) return;
2669
+ if (
2670
+ subscription.outstanding.size + refreshAdmission.pendingAcknowledgements.size
2671
+ >= this.options.maxOutstandingFrames
2672
+ ) {
2673
+ this.failFrameRefresh(refreshAdmission, 'FRAME_STREAM_FAILED');
2674
+ return;
2675
+ }
2676
+ } else {
2677
+ if (refresh && subscription.outstanding.size >= this.options.maxOutstandingFrames) {
2678
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2679
+ }
2680
+ while (subscription.outstanding.size >= this.options.maxOutstandingFrames) {
2681
+ const oldestKey = subscription.outstanding.keys().next().value as string | undefined;
2682
+ if (!oldestKey) break;
2683
+ this.trackCleanup(
2684
+ this.retireOutstandingFrame(subscription, oldestKey, false).then(() => undefined),
2685
+ );
2686
+ }
2272
2687
  }
2273
- const timer = setTimeout(() => {
2274
- this.trackCleanup(this.retireOutstandingFrame(subscription, key, true).then(() => undefined));
2275
- }, this.options.frameAcknowledgementTimeoutMs);
2276
- timer.unref();
2277
- subscription.outstanding.set(key, {
2688
+ const outstanding: IOutstandingFrameRecord = {
2278
2689
  acknowledgement,
2279
- timer,
2280
2690
  session,
2281
2691
  incarnationGeneration: slot.incarnationGeneration,
2282
- });
2283
- this.pushToSubscription(subscription, { type: 'frame', frame: event.frame });
2692
+ };
2693
+ subscription.outstanding.set(key, outstanding);
2694
+ if (!refreshAdmission) this.armFrameAcknowledgementTimer(subscription, key, outstanding);
2695
+ const delivered = this.pushToSubscription(subscription, { type: 'frame', frame: event.frame });
2696
+ if (
2697
+ delivered
2698
+ && refreshAdmission
2699
+ && refreshAdmission.refreshStarted
2700
+ && !refreshAdmission.boundaryCandidate
2701
+ ) {
2702
+ if (
2703
+ acknowledgement.tabId === refreshAdmission.initialTabId
2704
+ && acknowledgement.sequence > refreshAdmission.startingHighestSequence
2705
+ && acknowledgement.generation > refreshAdmission.initialGeneration
2706
+ && acknowledgement.viewportRevision === refreshAdmission.initialViewportRevision
2707
+ ) {
2708
+ refreshAdmission.boundaryCandidate = { ...acknowledgement };
2709
+ } else {
2710
+ this.failFrameRefresh(refreshAdmission, 'FRAME_STREAM_FAILED');
2711
+ }
2712
+ }
2284
2713
  return;
2285
2714
  }
2286
2715
  if (event.type === 'error' && event.error.fatal && slot.lease) {
2287
- this.trackCleanup(this.revokeCapabilityRecord(slot.lease.capability));
2716
+ if (refreshForSession) {
2717
+ this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
2718
+ } else {
2719
+ this.trackCleanup(this.revokeCapabilityRecord(slot.lease.capability));
2720
+ }
2288
2721
  }
2289
2722
  if (!subscription || subscription.closed) return;
2290
2723
  if (event.type === 'state') {
@@ -2304,22 +2737,30 @@ export class BrowserRuntime {
2304
2737
  private pushToSubscription(
2305
2738
  subscription: IFrameSubscriptionRecord,
2306
2739
  event: TBrowserRuntimeEvent,
2307
- ): void {
2740
+ ): boolean {
2308
2741
  try {
2309
2742
  this.requireValidLease(subscription.lease);
2310
2743
  subscription.listener(event);
2744
+ return true;
2311
2745
  } catch {
2312
2746
  const slot = subscription.lease.slot;
2313
2747
  const session = slot.session;
2314
2748
  const incarnationGeneration = slot.incarnationGeneration;
2749
+ const refresh = session
2750
+ ? this.matchingFrameRefresh(slot, subscription, session, incarnationGeneration)
2751
+ : undefined;
2315
2752
  if (event.type === 'frame') {
2316
2753
  const key = this.frameAcknowledgementKey(this.validateFrameAcknowledgement(event.frame));
2317
2754
  this.trackCleanup(this.retireOutstandingFrame(subscription, key, true).then(() => undefined));
2318
- return;
2755
+ return false;
2756
+ }
2757
+ if (refresh) {
2758
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2759
+ return false;
2319
2760
  }
2320
2761
  if (!session) {
2321
2762
  this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
2322
- return;
2763
+ return false;
2323
2764
  }
2324
2765
  this.trackCleanup((async () => {
2325
2766
  await this.options.beforeFrameFailureTermination?.();
@@ -2330,6 +2771,7 @@ export class BrowserRuntime {
2330
2771
  subscription.lease,
2331
2772
  );
2332
2773
  })());
2774
+ return false;
2333
2775
  }
2334
2776
  }
2335
2777
 
@@ -2337,6 +2779,10 @@ export class BrowserRuntime {
2337
2779
  if (subscription.closed) return;
2338
2780
  subscription.closed = true;
2339
2781
  const slot = subscription.lease.slot;
2782
+ const refresh = slot.frameRefresh;
2783
+ if (refresh?.subscription === subscription && !refresh.abandoned) {
2784
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2785
+ }
2340
2786
  if (slot.frameSubscription === subscription) slot.frameSubscription = undefined;
2341
2787
  for (const key of [...subscription.outstanding.keys()]) {
2342
2788
  await this.retireOutstandingFrame(subscription, key, false);
@@ -2351,15 +2797,37 @@ export class BrowserRuntime {
2351
2797
  const outstanding = subscription.outstanding.get(key);
2352
2798
  if (!outstanding) return false;
2353
2799
  subscription.outstanding.delete(key);
2354
- clearTimeout(outstanding.timer);
2355
- const outcome = await this.waitForFrameAcknowledgement(
2800
+ if (outstanding.timer) clearTimeout(outstanding.timer);
2801
+ outstanding.timer = undefined;
2802
+ const slot = subscription.lease.slot;
2803
+ const refresh = this.matchingFrameRefresh(
2804
+ slot,
2805
+ subscription,
2806
+ outstanding.session,
2807
+ outstanding.incarnationGeneration,
2808
+ );
2809
+ if (refresh?.admissionClosed) {
2810
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2811
+ return false;
2812
+ }
2813
+ if (refresh && failLease) this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2814
+ const refreshOwned = Boolean(refresh && !refresh.abandoned);
2815
+ const outcome = await this.beginFrameAcknowledgement(
2356
2816
  outstanding.session,
2357
2817
  outstanding.acknowledgement,
2818
+ refreshOwned ? refresh : undefined,
2358
2819
  );
2359
2820
  if (outcome.status === 'fulfilled' && !failLease) return outcome.accepted;
2821
+ if (refreshOwned) return outcome.status === 'fulfilled' ? outcome.accepted : false;
2822
+ if (refresh && slot.frameRefresh === refresh) {
2823
+ if (outcome.status !== 'fulfilled') {
2824
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2825
+ }
2826
+ return outcome.status === 'fulfilled' ? outcome.accepted : false;
2827
+ }
2360
2828
  await this.options.beforeFrameFailureTermination?.();
2361
2829
  await this.handleFrameFailure(
2362
- subscription.lease.slot,
2830
+ slot,
2363
2831
  outstanding.session,
2364
2832
  outstanding.incarnationGeneration,
2365
2833
  subscription.lease,
@@ -2434,8 +2902,25 @@ export class BrowserRuntime {
2434
2902
  ): Promise<void> {
2435
2903
  const failedLease = slot.session === session ? (slot.lease ?? null) : null;
2436
2904
  const incarnationGeneration = slot.incarnationGeneration;
2437
- const outcome = await this.waitForFrameAcknowledgement(session, acknowledgement);
2905
+ const subscription = slot.frameSubscription;
2906
+ const refresh = subscription
2907
+ ? this.matchingFrameRefresh(slot, subscription, session, incarnationGeneration)
2908
+ : undefined;
2909
+ if (refresh && failLease) this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2910
+ const refreshOwned = Boolean(refresh && !refresh.admissionClosed && !refresh.abandoned);
2911
+ const outcome = await this.beginFrameAcknowledgement(
2912
+ session,
2913
+ acknowledgement,
2914
+ refreshOwned ? refresh : undefined,
2915
+ );
2438
2916
  if (outcome.status === 'fulfilled' && !failLease) return;
2917
+ if (refreshOwned) return;
2918
+ if (refresh && slot.frameRefresh === refresh) {
2919
+ if (outcome.status !== 'fulfilled') {
2920
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2921
+ }
2922
+ return;
2923
+ }
2439
2924
  await this.options.beforeFrameFailureTermination?.();
2440
2925
  await this.handleFrameFailure(slot, session, incarnationGeneration, failedLease);
2441
2926
  }
@@ -2445,7 +2930,8 @@ export class BrowserRuntime {
2445
2930
  session: ILiveBrowserSessionLike,
2446
2931
  incarnationGeneration: number,
2447
2932
  expectedLease: ILeaseRecord | null,
2448
- ): Promise<void> {
2933
+ expectedRefresh?: IFrameRefreshRecord,
2934
+ ): Promise<boolean> {
2449
2935
  let transitionGeneration = 0;
2450
2936
  let ownsTransitionFence = false;
2451
2937
  const lease = expectedLease ?? undefined;
@@ -2478,8 +2964,14 @@ export class BrowserRuntime {
2478
2964
  || slot.attachmentRetryBinding
2479
2965
  || !exactLeaseIsCurrent
2480
2966
  || !leaseAttachmentIsCurrent
2967
+ || (expectedRefresh !== undefined && slot.frameRefresh !== expectedRefresh)
2481
2968
  || (slot.fencingGeneration !== undefined && !borrowsLeaseReleaseFence)
2482
- ) return;
2969
+ ) return false;
2970
+ if (expectedRefresh) {
2971
+ expectedRefresh.admissionClosed = true;
2972
+ expectedRefresh.abandoned = true;
2973
+ slot.frameRefresh = undefined;
2974
+ }
2483
2975
  if (lease) this.invalidateCapabilityRecord(lease.capability);
2484
2976
  if (borrowsLeaseReleaseFence) {
2485
2977
  transitionGeneration = slot.fencingGeneration!;
@@ -2526,6 +3018,7 @@ export class BrowserRuntime {
2526
3018
  if (errors.length > 1) {
2527
3019
  throw new AggregateError(errors, 'Frame acknowledgement cleanup is incomplete');
2528
3020
  }
3021
+ return true;
2529
3022
  }
2530
3023
 
2531
3024
  private trackFrameCapabilityRevocation(lease: ILeaseRecord): void {
@@ -2696,6 +3189,9 @@ export class BrowserRuntime {
2696
3189
  }
2697
3190
  await this.closeFrameSubscriptionIfLease(lease);
2698
3191
  await this.quiesceSlot(slot);
3192
+ if (slot.frameRefresh?.lease === lease) {
3193
+ this.abandonFrameRefresh(slot, slot.frameRefresh);
3194
+ }
2699
3195
  if (
2700
3196
  slot.permanentlyFenced
2701
3197
  && (slot.session || slot.proxy || slot.profileDirectory)
@@ -2780,6 +3276,9 @@ export class BrowserRuntime {
2780
3276
  }
2781
3277
  const session = slot.session;
2782
3278
  if (expectedSession && session !== expectedSession) return;
3279
+ if (slot.frameRefresh && (!session || slot.frameRefresh.session === session)) {
3280
+ this.abandonFrameRefresh(slot, slot.frameRefresh);
3281
+ }
2783
3282
  if (session) {
2784
3283
  let termination: plugins.smartpuppeteer.ILiveBrowserTerminationResult;
2785
3284
  try {
@@ -3578,6 +4077,12 @@ export class BrowserRuntimeLease {
3578
4077
  return this.runtime.acknowledgeLeaseFrame(this.record, acknowledgement);
3579
4078
  }
3580
4079
 
4080
+ public refreshFrameStream(
4081
+ operationOptions?: IBrowserRuntimeOperationOptions,
4082
+ ): Promise<void> {
4083
+ return this.runtime.leaseRefreshFrameStream(this.record, operationOptions);
4084
+ }
4085
+
3581
4086
  public readArtifact(artifactId: string): Promise<Uint8Array> {
3582
4087
  return this.runtime.readLeaseArtifact(this.record, artifactId);
3583
4088
  }