@push.rocks/smartpuppeteer 2.3.0 → 2.4.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.
@@ -3,6 +3,8 @@ export type TLiveBrowserStatus = 'stopped' | 'starting' | 'running' | 'stopping'
3
3
  export type TLiveBrowserImageFormat = 'jpeg' | 'png';
4
4
  export type TLiveBrowserTabStatus = 'open' | 'crashed';
5
5
  export type TLiveBrowserWaitUntil = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
6
+ export declare const liveBrowserDefaultMaxOutstandingFrames = 3;
7
+ export declare const liveBrowserMaxOutstandingFrames = 64;
6
8
  export interface ILiveBrowserViewport {
7
9
  width: number;
8
10
  height: number;
@@ -14,6 +16,7 @@ export interface ILiveBrowserScreencastOptions {
14
16
  maxWidth?: number;
15
17
  maxHeight?: number;
16
18
  everyNthFrame?: number;
19
+ maxOutstandingFrames?: number;
17
20
  }
18
21
  export interface ILiveBrowserSecurityOptions {
19
22
  denyDownloads?: boolean;
@@ -1,2 +1,3 @@
1
- export {};
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRwdXBwZXRlZXIuaW50ZXJmYWNlcy5saXZlYnJvd3Nlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0cHVwcGV0ZWVyLmludGVyZmFjZXMubGl2ZWJyb3dzZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiJ9
1
+ export const liveBrowserDefaultMaxOutstandingFrames = 3;
2
+ export const liveBrowserMaxOutstandingFrames = 64;
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRwdXBwZXRlZXIuaW50ZXJmYWNlcy5saXZlYnJvd3Nlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0cHVwcGV0ZWVyLmludGVyZmFjZXMubGl2ZWJyb3dzZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBV0EsTUFBTSxDQUFDLE1BQU0sc0NBQXNDLEdBQUcsQ0FBQyxDQUFDO0FBQ3hELE1BQU0sQ0FBQyxNQUFNLCtCQUErQixHQUFHLEVBQUUsQ0FBQyJ9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartpuppeteer",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "private": false,
5
5
  "description": "Provides simplified access to Puppeteer for automation and testing purposes.",
6
6
  "main": "dist_ts/index.js",
package/readme.hints.md CHANGED
@@ -3,7 +3,7 @@
3
3
  - `getEnvAwareBrowserInstance()` is the only Chromium launch path. Merge caller arguments before adding environment-required sandbox arguments, retain the pipe default, and do not run executable discovery when the caller selected a browser, channel, or executable.
4
4
  - `LiveBrowserSession` uses the browser's default context so all tabs and popups share one profile. Omitting both `launchOptions.userDataDir` and a `--user-data-dir` argument intentionally relies on Puppeteer's ephemeral profile lifecycle.
5
5
  - CDP is private to the live runtime. Public contracts contain transport-neutral values and `Uint8Array` image data, never `CDPSession`, raw CDP frame IDs, or base64 image strings.
6
- - Every published screencast frame has one private sequence-to-CDP acknowledgement entry. Public acknowledgement requires matching tab ID, sequence, generation, and viewport revision. Pending frames are bounded; drops and all stream invalidation paths must retire and acknowledge entries before detaching the CDP session.
6
+ - Every published screencast frame has one private sequence-to-CDP acknowledgement entry. Public acknowledgement requires matching tab ID, sequence, generation, and viewport revision. `screencast.maxOutstandingFrames` bounds those entries independently of any application transport window; overflow retires oldest-first. Drops and all stream invalidation paths must issue each CDP acknowledgement at most once and await in-flight acknowledgements before detaching the CDP session.
7
7
  - Activation, viewport changes, navigation, tab closure, snapshots, observations, semantic actions, and shutdown share one bounded operation scheduler. Raw input and frame acknowledgement remain direct, but must validate active tab, generation, and viewport revision. Repeated internal navigation/load state updates are coalesced per tab, and shutdown cancels queued work.
8
8
  - Retain the scheduler-owned launch `AbortController` for the full browser lifetime. Shutdown aborts both the active operation and Chromium itself so a non-signal-aware Puppeteer command or disabled protocol timeout cannot retain the browser ahead of queued cleanup.
9
9
  - Viewport revision starts at 1 and advances only after `Page.setViewport()` succeeds. Stop and flush the active screencast before applying a viewport or navigation mutation, then restart it with a new generation.
package/readme.md CHANGED
@@ -120,6 +120,8 @@ const session = new LiveBrowserSession({
120
120
  quality: 80,
121
121
  maxWidth: 1280,
122
122
  maxHeight: 720,
123
+ everyNthFrame: 1,
124
+ maxOutstandingFrames: 3,
123
125
  },
124
126
  launchOptions: {
125
127
  headless: true,
@@ -168,7 +170,9 @@ try {
168
170
  }
169
171
  ```
170
172
 
171
- Only the active tab is streamed. Frames carry a session-monotonic sequence, tab/CDP generation, viewport revision, viewport, MIME type, encoded dimensions, screencast metadata, and binary `Uint8Array` data. Every delivered frame must be acknowledged with all four identity fields. A delivered frame remains pending until it is acknowledged, dropped, or retired by the runtime; mismatched, duplicate, stale, retired, or operationally failed acknowledgements return `{ accepted: false }`. Operational acknowledgement failures also emit an `error` event whose code is `frame_acknowledgement_failed`. The runtime bounds pending frames and CDP-acknowledges an oldest frame when it must be dropped. Tab switches, navigation, resize, page cleanup, disconnect, and shutdown retire pending frames and acknowledge them while their CDP session remains available.
173
+ Only the active tab is streamed. Frames carry a session-monotonic sequence, tab/CDP generation, viewport revision, viewport, MIME type, encoded dimensions, screencast metadata, and binary `Uint8Array` data. Every delivered frame must be acknowledged with all four identity fields. A delivered frame remains pending until it is acknowledged, dropped, or retired by the runtime; mismatched, duplicate, stale, retired, or operationally failed acknowledgements return `{ accepted: false }`. Operational acknowledgement failures also emit an `error` event whose code is `frame_acknowledgement_failed`. `screencast.maxOutstandingFrames` is an integer from 1 through `liveBrowserMaxOutstandingFrames` (64) and defaults to `liveBrowserDefaultMaxOutstandingFrames` (3). At capacity, SmartPuppeteer retires and CDP-acknowledges the oldest pending frame before publishing the next one. Tab switches, navigation, resize, page cleanup, disconnect, and shutdown also retire pending frames and await their single CDP acknowledgement attempt before detaching the screencast session.
174
+
175
+ The SmartPuppeteer outstanding-frame bound covers only the private frame-to-`Page.screencastFrameAck` lifecycle. It is not a binary transport window, and `acknowledgeFrame()` is not an application transport acknowledgement. A higher-level runtime must maintain and bound its application frame window separately. Screencast `format` accepts `jpeg` or `png`, `quality` accepts integers from 0 through 100, `maxWidth` and `maxHeight` accept integers from 1 through 4096 subject to an 8,294,400-pixel combined ceiling, and `everyNthFrame` accepts integers from 1 through 100.
172
176
 
173
177
  The live API includes:
174
178
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartpuppeteer',
6
- version: '2.3.0',
6
+ version: '2.4.0',
7
7
  description: 'Provides simplified access to Puppeteer for automation and testing purposes.'
8
8
  }
@@ -1,4 +1,8 @@
1
1
  import { getEnvAwareBrowserInstance } from './smartpuppeteer.classes.smartpuppeteer.js';
2
+ import {
3
+ liveBrowserDefaultMaxOutstandingFrames,
4
+ liveBrowserMaxOutstandingFrames,
5
+ } from './smartpuppeteer.interfaces.livebrowser.js';
2
6
  import {
3
7
  delay,
4
8
  type IOwnedProcessIdentity,
@@ -59,7 +63,7 @@ const maxSelectorLength = 4096;
59
63
  const maxTextLength = 32768;
60
64
  const maxUrlLength = 16384;
61
65
  const maxTimeoutMs = 60000;
62
- const maxOutstandingFrames = 3;
66
+ const frameAcknowledgementTimeoutMs = 5000;
63
67
  const maxQueuedPublicOperations = 64;
64
68
  const maxQueuedInternalOperations = 128;
65
69
  const maxEvaluationScriptBytes = 262144;
@@ -143,12 +147,17 @@ interface IPrivateLiveBrowserTab {
143
147
  removeListeners: Array<() => void>;
144
148
  }
145
149
 
146
- interface IOutstandingFrame {
150
+ interface ICdpScreencastFrame {
147
151
  tabId: string;
148
152
  generation: number;
149
153
  viewportRevision: number;
150
154
  cdpSessionId: number;
151
155
  cdpSession: plugins.puppeteer.CDPSession;
156
+ acknowledgementPromise?: Promise<boolean>;
157
+ }
158
+
159
+ interface IOutstandingFrame extends ICdpScreencastFrame {
160
+ sequence: number;
152
161
  }
153
162
 
154
163
  interface IImageDimensions {
@@ -367,6 +376,8 @@ export class LiveBrowserSession {
367
376
  private readonly tabs = new Map<string, IPrivateLiveBrowserTab>();
368
377
  private readonly tabIdsByPage = new WeakMap<plugins.puppeteer.Page, string>();
369
378
  private readonly outstandingFrames = new Map<number, IOutstandingFrame>();
379
+ private readonly cdpFramesBeingAcknowledged = new Set<ICdpScreencastFrame>();
380
+ private readonly maxOutstandingFrames: number;
370
381
 
371
382
  private browser?: plugins.puppeteer.Browser;
372
383
  private browserContext?: plugins.puppeteer.BrowserContext;
@@ -392,6 +403,7 @@ export class LiveBrowserSession {
392
403
  private admittedPublicOperations = 0;
393
404
  private admittedInternalOperations = 0;
394
405
  private shutdownPromise?: Promise<void>;
406
+ private shutdownFrameDrainPromise?: Promise<void>;
395
407
  private status: 'stopped' | 'starting' | 'running' | 'stopping' = 'stopped';
396
408
  private activeTabId: string | null = null;
397
409
  private viewport: ILiveBrowserViewport = { ...defaultViewport };
@@ -418,6 +430,16 @@ export class LiveBrowserSession {
418
430
  if (optionsArg.launchOptions && 'signal' in optionsArg.launchOptions) {
419
431
  throw new Error('LiveBrowserSession owns launch cancellation; launchOptions.signal is unsupported');
420
432
  }
433
+ if (
434
+ optionsArg.screencast !== undefined
435
+ && (
436
+ !optionsArg.screencast
437
+ || typeof optionsArg.screencast !== 'object'
438
+ || Array.isArray(optionsArg.screencast)
439
+ )
440
+ ) {
441
+ throw new Error('screencast must be an object');
442
+ }
421
443
  validateOptionalBoolean(optionsArg.allowEvaluation, 'allowEvaluation');
422
444
  for (const name of [
423
445
  'denyDownloads',
@@ -477,6 +499,8 @@ export class LiveBrowserSession {
477
499
  };
478
500
  this.viewport = { ...viewport };
479
501
  this.validateScreencastOptions();
502
+ this.maxOutstandingFrames = this.options.screencast?.maxOutstandingFrames
503
+ ?? liveBrowserDefaultMaxOutstandingFrames;
480
504
  }
481
505
 
482
506
  public onEvent(listener: TLiveBrowserEventListener): () => void {
@@ -1747,15 +1771,27 @@ export class LiveBrowserSession {
1747
1771
  this.emitState();
1748
1772
  }
1749
1773
  const shutdownError = new Error('LiveBrowserSession is stopping');
1750
- if (
1774
+ const activeOperation = this.activeOperation;
1775
+ const shouldAbortActiveOperation = Boolean(
1751
1776
  abortActiveOperation
1752
- && this.activeOperation
1753
- && this.activeOperation.kind !== 'shutdown'
1754
- ) {
1755
- this.activeOperation.controller.abort(shutdownError);
1756
- }
1757
- if (this.browserLifetimeController && !this.browserLifetimeController.signal.aborted) {
1758
- this.browserLifetimeController.abort(shutdownError);
1777
+ && activeOperation
1778
+ && activeOperation.kind !== 'shutdown'
1779
+ );
1780
+ if (shouldAbortActiveOperation && activeOperation) {
1781
+ const browserLifetimeController = this.browserLifetimeController;
1782
+ const frameDrainPromise = (async () => {
1783
+ try {
1784
+ await this.retireOutstandingFrames(() => true);
1785
+ await this.waitForCdpFrameAcknowledgements();
1786
+ } finally {
1787
+ if (browserLifetimeController && !browserLifetimeController.signal.aborted) {
1788
+ browserLifetimeController.abort(shutdownError);
1789
+ }
1790
+ }
1791
+ })();
1792
+ this.shutdownFrameDrainPromise = frameDrainPromise;
1793
+ void frameDrainPromise.catch(() => {});
1794
+ activeOperation.controller.abort(shutdownError);
1759
1795
  }
1760
1796
  this.cancelQueuedOperations(shutdownError);
1761
1797
  }
@@ -2126,10 +2162,6 @@ export class LiveBrowserSession {
2126
2162
  this.lastError = { ...error };
2127
2163
  }
2128
2164
  this.emitState();
2129
- if (this.browserLifetimeController && !this.browserLifetimeController.signal.aborted) {
2130
- this.browserLifetimeController.abort(new Error('LiveBrowserSession is stopping'));
2131
- }
2132
-
2133
2165
  const browser = this.browser;
2134
2166
  if (browser && this.browserTargetCreatedListener) {
2135
2167
  browser.off('targetcreated', this.browserTargetCreatedListener);
@@ -2141,6 +2173,15 @@ export class LiveBrowserSession {
2141
2173
  this.browserDisconnectedListener = undefined;
2142
2174
 
2143
2175
  const shutdownErrors: unknown[] = [];
2176
+ const shutdownFrameDrainPromise = this.shutdownFrameDrainPromise;
2177
+ this.shutdownFrameDrainPromise = undefined;
2178
+ if (shutdownFrameDrainPromise) {
2179
+ try {
2180
+ await shutdownFrameDrainPromise;
2181
+ } catch (cleanupError) {
2182
+ shutdownErrors.push(cleanupError);
2183
+ }
2184
+ }
2144
2185
  try {
2145
2186
  await this.teardownProxySecurity();
2146
2187
  } catch (cleanupError) {
@@ -2173,6 +2214,10 @@ export class LiveBrowserSession {
2173
2214
  } catch (cleanupError) {
2174
2215
  shutdownErrors.push(cleanupError);
2175
2216
  }
2217
+ await this.waitForCdpFrameAcknowledgements();
2218
+ if (this.browserLifetimeController && !this.browserLifetimeController.signal.aborted) {
2219
+ this.browserLifetimeController.abort(new Error('LiveBrowserSession is stopping'));
2220
+ }
2176
2221
 
2177
2222
  if (browser) {
2178
2223
  try {
@@ -3321,6 +3366,8 @@ export class LiveBrowserSession {
3321
3366
  plugins.puppeteer.CDPSessionEvent.SessionDetached,
3322
3367
  cdpSessionDetachedListener,
3323
3368
  );
3369
+ await this.retireOutstandingFrames((frame) => frame.cdpSession === cdpSession);
3370
+ await this.waitForCdpFrameAcknowledgements(cdpSession);
3324
3371
  if (!cdpSession.detached) {
3325
3372
  try {
3326
3373
  await cdpSession.detach();
@@ -3365,6 +3412,7 @@ export class LiveBrowserSession {
3365
3412
  cdpSessionDetachedListener,
3366
3413
  );
3367
3414
  }
3415
+ await this.waitForCdpFrameAcknowledgements(cdpSession);
3368
3416
  if (!cdpSession.detached) {
3369
3417
  try {
3370
3418
  await cdpSession.detach();
@@ -3409,12 +3457,13 @@ export class LiveBrowserSession {
3409
3457
  const sequence = ++this.frameSequence;
3410
3458
  const outstandingFrame: IOutstandingFrame = {
3411
3459
  tabId: tab.id,
3460
+ sequence,
3412
3461
  generation,
3413
3462
  viewportRevision: this.viewportRevision,
3414
3463
  cdpSessionId: event.sessionId,
3415
3464
  cdpSession,
3416
3465
  };
3417
- while (this.outstandingFrames.size >= maxOutstandingFrames) {
3466
+ while (this.outstandingFrames.size >= this.maxOutstandingFrames) {
3418
3467
  const oldestFrameEntry = this.outstandingFrames.entries().next().value as
3419
3468
  | [number, IOutstandingFrame]
3420
3469
  | undefined;
@@ -3450,31 +3499,40 @@ export class LiveBrowserSession {
3450
3499
  this.emitEvent({ type: 'frame', frame });
3451
3500
  }
3452
3501
 
3453
- private async acknowledgeCdpFrame(frame: IOutstandingFrame): Promise<boolean> {
3454
- if (frame.cdpSession.detached) {
3455
- this.handleFrameAcknowledgementFailure(
3456
- frame,
3457
- new Error('CDP session detached before frame acknowledgement'),
3458
- );
3459
- return false;
3502
+ private acknowledgeCdpFrame(frame: ICdpScreencastFrame): Promise<boolean> {
3503
+ if (frame.acknowledgementPromise) {
3504
+ return frame.acknowledgementPromise;
3460
3505
  }
3461
- try {
3506
+ let resolveAcknowledgement!: (accepted: boolean) => void;
3507
+ const acknowledgementPromise = new Promise<boolean>((resolve) => {
3508
+ resolveAcknowledgement = resolve;
3509
+ });
3510
+ frame.acknowledgementPromise = acknowledgementPromise;
3511
+ this.cdpFramesBeingAcknowledged.add(frame);
3512
+ void (async (): Promise<boolean> => {
3513
+ if (frame.cdpSession.detached) {
3514
+ throw new Error('CDP session detached before frame acknowledgement');
3515
+ }
3462
3516
  await frame.cdpSession.send('Page.screencastFrameAck', {
3463
3517
  sessionId: frame.cdpSessionId,
3464
- });
3518
+ }, { timeout: frameAcknowledgementTimeoutMs });
3465
3519
  return true;
3466
- } catch (error) {
3520
+ })().then(resolveAcknowledgement, (error) => {
3467
3521
  this.handleFrameAcknowledgementFailure(frame, error);
3468
- return false;
3469
- }
3522
+ resolveAcknowledgement(false);
3523
+ });
3524
+ void acknowledgementPromise.then(() => {
3525
+ this.cdpFramesBeingAcknowledged.delete(frame);
3526
+ });
3527
+ return acknowledgementPromise;
3470
3528
  }
3471
3529
 
3472
- private acknowledgeCdpFrameInBackground(frame: IOutstandingFrame): void {
3530
+ private acknowledgeCdpFrameInBackground(frame: ICdpScreencastFrame): void {
3473
3531
  void this.acknowledgeCdpFrame(frame);
3474
3532
  }
3475
3533
 
3476
3534
  private handleFrameAcknowledgementFailure(
3477
- frame: IOutstandingFrame,
3535
+ frame: ICdpScreencastFrame,
3478
3536
  error: unknown,
3479
3537
  ): void {
3480
3538
  const tab = this.tabs.get(frame.tabId);
@@ -3514,6 +3572,15 @@ export class LiveBrowserSession {
3514
3572
  await Promise.all(acknowledgements);
3515
3573
  }
3516
3574
 
3575
+ private async waitForCdpFrameAcknowledgements(
3576
+ cdpSession?: plugins.puppeteer.CDPSession,
3577
+ ): Promise<void> {
3578
+ const acknowledgements = [...this.cdpFramesBeingAcknowledged]
3579
+ .filter((frame) => !cdpSession || frame.cdpSession === cdpSession)
3580
+ .map((frame) => frame.acknowledgementPromise!);
3581
+ await Promise.all(acknowledgements);
3582
+ }
3583
+
3517
3584
  private handlePossibleCdpDisconnection(
3518
3585
  tab: IPrivateLiveBrowserTab,
3519
3586
  cdpSession: plugins.puppeteer.CDPSession,
@@ -3972,5 +4039,13 @@ export class LiveBrowserSession {
3972
4039
  if (options.everyNthFrame !== undefined) {
3973
4040
  validateInteger(options.everyNthFrame, 'screencast.everyNthFrame', 1, 100);
3974
4041
  }
4042
+ if (options.maxOutstandingFrames !== undefined) {
4043
+ validateInteger(
4044
+ options.maxOutstandingFrames,
4045
+ 'screencast.maxOutstandingFrames',
4046
+ 1,
4047
+ liveBrowserMaxOutstandingFrames,
4048
+ );
4049
+ }
3975
4050
  }
3976
4051
  }
@@ -9,6 +9,9 @@ export type TLiveBrowserWaitUntil =
9
9
  | 'networkidle0'
10
10
  | 'networkidle2';
11
11
 
12
+ export const liveBrowserDefaultMaxOutstandingFrames = 3;
13
+ export const liveBrowserMaxOutstandingFrames = 64;
14
+
12
15
  export interface ILiveBrowserViewport {
13
16
  width: number;
14
17
  height: number;
@@ -21,6 +24,7 @@ export interface ILiveBrowserScreencastOptions {
21
24
  maxWidth?: number;
22
25
  maxHeight?: number;
23
26
  everyNthFrame?: number;
27
+ maxOutstandingFrames?: number;
24
28
  }
25
29
 
26
30
  export interface ILiveBrowserSecurityOptions {