@modelprofile.com/browser-runtime 3.0.1 → 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,
@@ -1191,7 +1243,7 @@ export class BrowserRuntime {
1191
1243
  let subscription: IFrameSubscriptionRecord;
1192
1244
  try {
1193
1245
  this.requireValidLease(lease);
1194
- if (slot.frameSubscription) throw new BrowserRuntimeError('BUSY');
1246
+ if (slot.frameSubscription || slot.frameRefresh) throw new BrowserRuntimeError('BUSY');
1195
1247
  subscription = {
1196
1248
  lease,
1197
1249
  listener,
@@ -1226,14 +1278,34 @@ export class BrowserRuntime {
1226
1278
  if (!subscription || subscription.lease !== lease) return false;
1227
1279
  const outstanding = subscription.outstanding.get(key);
1228
1280
  if (!outstanding) return false;
1229
- subscription.outstanding.delete(key);
1230
- clearTimeout(outstanding.timer);
1231
1281
  const { session, incarnationGeneration } = outstanding;
1232
- 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(
1233
1299
  session,
1234
1300
  outstanding.acknowledgement,
1301
+ refreshOwned ? refresh : undefined,
1235
1302
  );
1236
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
+ }
1237
1309
  await this.options.beforeFrameFailureTermination?.();
1238
1310
  await this.handleFrameFailure(slot, session, incarnationGeneration, subscription.lease);
1239
1311
  return false;
@@ -1641,6 +1713,7 @@ export class BrowserRuntime {
1641
1713
  classification: TBrowserRuntimeOperationClassification,
1642
1714
  operationOptions: IBrowserRuntimeOperationOptions,
1643
1715
  execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<T>,
1716
+ finalize?: () => Promise<void>,
1644
1717
  ): Promise<T> {
1645
1718
  if (!operationOptions || typeof operationOptions !== 'object') {
1646
1719
  throw new BrowserRuntimeError('INVALID_INPUT');
@@ -1679,6 +1752,7 @@ export class BrowserRuntime {
1679
1752
  onOperationStarted: operationOptions.onOperationStarted,
1680
1753
  signal,
1681
1754
  execute: execute as IQueuedOperationRecord['execute'],
1755
+ finalize,
1682
1756
  resolve: (value) => resolve(value as T),
1683
1757
  reject,
1684
1758
  state: 'queued',
@@ -1814,6 +1888,7 @@ export class BrowserRuntime {
1814
1888
  let failed = false;
1815
1889
  let failure: unknown;
1816
1890
  let executionStarted = false;
1891
+ let executionSettled = false;
1817
1892
  try {
1818
1893
  await this.runBeforeOperation(operationIdentity, combinedSignal);
1819
1894
  const executionRelease = await slot.mutex.acquire();
@@ -1830,7 +1905,9 @@ export class BrowserRuntime {
1830
1905
  ) throw new BrowserRuntimeError('ABORTED');
1831
1906
  executionStarted = true;
1832
1907
  executionPromise = queued.execute(combinedSignal, session);
1833
- operation.promise = executionPromise.then(() => undefined, () => undefined);
1908
+ operation.promise = executionPromise.then(() => undefined, () => undefined).finally(() => {
1909
+ executionSettled = true;
1910
+ });
1834
1911
  } finally {
1835
1912
  executionRelease();
1836
1913
  }
@@ -1876,6 +1953,18 @@ export class BrowserRuntime {
1876
1953
  } else {
1877
1954
  await cleanupSlot();
1878
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
+ }
1879
1968
  const errorCode = failed ? this.operationErrorCode(failure) : undefined;
1880
1969
  await this.emitAudit(Object.freeze({
1881
1970
  ...operationIdentity,
@@ -2194,6 +2283,271 @@ export class BrowserRuntime {
2194
2283
  };
2195
2284
  }
2196
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
+
2197
2551
  private handleSessionEvent(
2198
2552
  slot: IResourceSlot,
2199
2553
  session: ILiveBrowserSessionLike,
@@ -2211,6 +2565,12 @@ export class BrowserRuntime {
2211
2565
  return;
2212
2566
  }
2213
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;
2214
2574
  if (event.type === 'frame') {
2215
2575
  let acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest;
2216
2576
  try {
@@ -2219,38 +2579,72 @@ export class BrowserRuntime {
2219
2579
  }
2220
2580
  acknowledgement = this.validateFrameAcknowledgement(event.frame);
2221
2581
  } catch {
2582
+ if (refreshForSession) {
2583
+ this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
2584
+ return;
2585
+ }
2222
2586
  const failedLease = slot.session === session ? (slot.lease ?? null) : null;
2223
2587
  this.trackCleanup(this.handleFrameFailure(
2224
2588
  slot,
2225
2589
  session,
2226
2590
  slot.incarnationGeneration,
2227
2591
  failedLease,
2228
- ));
2592
+ ).then(() => undefined));
2229
2593
  return;
2230
2594
  }
2231
2595
  if (!subscription || subscription.closed) {
2596
+ if (refreshForSession) {
2597
+ this.failFrameRefresh(refreshForSession, 'FRAME_STREAM_FAILED');
2598
+ return;
2599
+ }
2232
2600
  this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
2233
2601
  return;
2234
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
+ }
2235
2613
  try {
2236
2614
  this.requireValidLease(subscription.lease);
2237
2615
  } catch {
2616
+ if (refresh) {
2617
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2618
+ return;
2619
+ }
2238
2620
  this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
2239
2621
  this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
2240
2622
  return;
2241
2623
  }
2242
2624
  const key = this.frameAcknowledgementKey(acknowledgement);
2243
2625
  if (acknowledgement.sequence <= subscription.highestSequence) {
2626
+ if (refresh) {
2627
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2628
+ return;
2629
+ }
2244
2630
  this.trackCleanup(this.handleFrameFailure(
2245
2631
  slot,
2246
2632
  session,
2247
2633
  slot.incarnationGeneration,
2248
2634
  subscription.lease,
2249
- ));
2635
+ ).then(() => undefined));
2250
2636
  return;
2251
2637
  }
2252
2638
  subscription.highestSequence = acknowledgement.sequence;
2253
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
+ }
2254
2648
  this.trackCleanup(this.acknowledgeFrameInBackground(
2255
2649
  slot,
2256
2650
  session,
@@ -2263,28 +2657,64 @@ export class BrowserRuntime {
2263
2657
  });
2264
2658
  return;
2265
2659
  }
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
- );
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
+ }
2272
2684
  }
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, {
2685
+ const outstanding: IOutstandingFrameRecord = {
2278
2686
  acknowledgement,
2279
- timer,
2280
2687
  session,
2281
2688
  incarnationGeneration: slot.incarnationGeneration,
2282
- });
2283
- 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
+ }
2284
2710
  return;
2285
2711
  }
2286
2712
  if (event.type === 'error' && event.error.fatal && slot.lease) {
2287
- 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
+ }
2288
2718
  }
2289
2719
  if (!subscription || subscription.closed) return;
2290
2720
  if (event.type === 'state') {
@@ -2304,22 +2734,30 @@ export class BrowserRuntime {
2304
2734
  private pushToSubscription(
2305
2735
  subscription: IFrameSubscriptionRecord,
2306
2736
  event: TBrowserRuntimeEvent,
2307
- ): void {
2737
+ ): boolean {
2308
2738
  try {
2309
2739
  this.requireValidLease(subscription.lease);
2310
2740
  subscription.listener(event);
2741
+ return true;
2311
2742
  } catch {
2312
2743
  const slot = subscription.lease.slot;
2313
2744
  const session = slot.session;
2314
2745
  const incarnationGeneration = slot.incarnationGeneration;
2746
+ const refresh = session
2747
+ ? this.matchingFrameRefresh(slot, subscription, session, incarnationGeneration)
2748
+ : undefined;
2315
2749
  if (event.type === 'frame') {
2316
2750
  const key = this.frameAcknowledgementKey(this.validateFrameAcknowledgement(event.frame));
2317
2751
  this.trackCleanup(this.retireOutstandingFrame(subscription, key, true).then(() => undefined));
2318
- return;
2752
+ return false;
2753
+ }
2754
+ if (refresh) {
2755
+ this.failFrameRefresh(refresh, 'FRAME_STREAM_FAILED');
2756
+ return false;
2319
2757
  }
2320
2758
  if (!session) {
2321
2759
  this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
2322
- return;
2760
+ return false;
2323
2761
  }
2324
2762
  this.trackCleanup((async () => {
2325
2763
  await this.options.beforeFrameFailureTermination?.();
@@ -2330,6 +2768,7 @@ export class BrowserRuntime {
2330
2768
  subscription.lease,
2331
2769
  );
2332
2770
  })());
2771
+ return false;
2333
2772
  }
2334
2773
  }
2335
2774
 
@@ -2337,6 +2776,10 @@ export class BrowserRuntime {
2337
2776
  if (subscription.closed) return;
2338
2777
  subscription.closed = true;
2339
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
+ }
2340
2783
  if (slot.frameSubscription === subscription) slot.frameSubscription = undefined;
2341
2784
  for (const key of [...subscription.outstanding.keys()]) {
2342
2785
  await this.retireOutstandingFrame(subscription, key, false);
@@ -2351,15 +2794,37 @@ export class BrowserRuntime {
2351
2794
  const outstanding = subscription.outstanding.get(key);
2352
2795
  if (!outstanding) return false;
2353
2796
  subscription.outstanding.delete(key);
2354
- clearTimeout(outstanding.timer);
2355
- 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(
2356
2813
  outstanding.session,
2357
2814
  outstanding.acknowledgement,
2815
+ refreshOwned ? refresh : undefined,
2358
2816
  );
2359
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
+ }
2360
2825
  await this.options.beforeFrameFailureTermination?.();
2361
2826
  await this.handleFrameFailure(
2362
- subscription.lease.slot,
2827
+ slot,
2363
2828
  outstanding.session,
2364
2829
  outstanding.incarnationGeneration,
2365
2830
  subscription.lease,
@@ -2434,8 +2899,25 @@ export class BrowserRuntime {
2434
2899
  ): Promise<void> {
2435
2900
  const failedLease = slot.session === session ? (slot.lease ?? null) : null;
2436
2901
  const incarnationGeneration = slot.incarnationGeneration;
2437
- 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
+ );
2438
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
+ }
2439
2921
  await this.options.beforeFrameFailureTermination?.();
2440
2922
  await this.handleFrameFailure(slot, session, incarnationGeneration, failedLease);
2441
2923
  }
@@ -2445,7 +2927,8 @@ export class BrowserRuntime {
2445
2927
  session: ILiveBrowserSessionLike,
2446
2928
  incarnationGeneration: number,
2447
2929
  expectedLease: ILeaseRecord | null,
2448
- ): Promise<void> {
2930
+ expectedRefresh?: IFrameRefreshRecord,
2931
+ ): Promise<boolean> {
2449
2932
  let transitionGeneration = 0;
2450
2933
  let ownsTransitionFence = false;
2451
2934
  const lease = expectedLease ?? undefined;
@@ -2478,8 +2961,14 @@ export class BrowserRuntime {
2478
2961
  || slot.attachmentRetryBinding
2479
2962
  || !exactLeaseIsCurrent
2480
2963
  || !leaseAttachmentIsCurrent
2964
+ || (expectedRefresh !== undefined && slot.frameRefresh !== expectedRefresh)
2481
2965
  || (slot.fencingGeneration !== undefined && !borrowsLeaseReleaseFence)
2482
- ) return;
2966
+ ) return false;
2967
+ if (expectedRefresh) {
2968
+ expectedRefresh.admissionClosed = true;
2969
+ expectedRefresh.abandoned = true;
2970
+ slot.frameRefresh = undefined;
2971
+ }
2483
2972
  if (lease) this.invalidateCapabilityRecord(lease.capability);
2484
2973
  if (borrowsLeaseReleaseFence) {
2485
2974
  transitionGeneration = slot.fencingGeneration!;
@@ -2526,6 +3015,7 @@ export class BrowserRuntime {
2526
3015
  if (errors.length > 1) {
2527
3016
  throw new AggregateError(errors, 'Frame acknowledgement cleanup is incomplete');
2528
3017
  }
3018
+ return true;
2529
3019
  }
2530
3020
 
2531
3021
  private trackFrameCapabilityRevocation(lease: ILeaseRecord): void {
@@ -2696,6 +3186,9 @@ export class BrowserRuntime {
2696
3186
  }
2697
3187
  await this.closeFrameSubscriptionIfLease(lease);
2698
3188
  await this.quiesceSlot(slot);
3189
+ if (slot.frameRefresh?.lease === lease) {
3190
+ this.abandonFrameRefresh(slot, slot.frameRefresh);
3191
+ }
2699
3192
  if (
2700
3193
  slot.permanentlyFenced
2701
3194
  && (slot.session || slot.proxy || slot.profileDirectory)
@@ -2780,6 +3273,9 @@ export class BrowserRuntime {
2780
3273
  }
2781
3274
  const session = slot.session;
2782
3275
  if (expectedSession && session !== expectedSession) return;
3276
+ if (slot.frameRefresh && (!session || slot.frameRefresh.session === session)) {
3277
+ this.abandonFrameRefresh(slot, slot.frameRefresh);
3278
+ }
2783
3279
  if (session) {
2784
3280
  let termination: plugins.smartpuppeteer.ILiveBrowserTerminationResult;
2785
3281
  try {
@@ -3578,6 +4074,12 @@ export class BrowserRuntimeLease {
3578
4074
  return this.runtime.acknowledgeLeaseFrame(this.record, acknowledgement);
3579
4075
  }
3580
4076
 
4077
+ public refreshFrameStream(
4078
+ operationOptions?: IBrowserRuntimeOperationOptions,
4079
+ ): Promise<void> {
4080
+ return this.runtime.leaseRefreshFrameStream(this.record, operationOptions);
4081
+ }
4082
+
3581
4083
  public readArtifact(artifactId: string): Promise<Uint8Array> {
3582
4084
  return this.runtime.readLeaseArtifact(this.record, artifactId);
3583
4085
  }