@modelprofile.com/browser-runtime 3.0.0 → 3.1.0

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