@modelprofile.com/browser-runtime 5.5.1 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +15 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.runtime.d.ts +3 -0
- package/dist_ts/classes.runtime.js +52 -5
- package/dist_ts/errors.d.ts +1 -1
- package/dist_ts/errors.js +2 -1
- package/dist_ts/interfaces.d.ts +3 -0
- package/package.json +2 -2
- package/readme.md +16 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.runtime.ts +52 -3
- package/ts/errors.ts +2 -0
- package/ts/interfaces.ts +3 -0
package/dist_ts/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type TBrowserRuntimeErrorCode = 'ABORTED' | 'AUTHORIZATION_DENIED' | 'ATTACHMENT_CONFLICT' | 'BUSY' | 'CAPABILITY_EXPIRED' | 'CAPABILITY_INVALID' | 'CAPABILITY_REVOKED' | 'CONFINEMENT_FAILED' | 'EGRESS_DENIED' | 'FENCED' | 'FRAME_STREAM_FAILED' | 'FRAME_TOO_LARGE' | 'DIALOG_PENDING' | 'STALE_INPUT' | 'INVALID_INPUT' | 'LOCKED' | 'NOT_RUNNING' | 'PROTOCOL_ERROR' | 'QUOTA_EXCEEDED' | 'RESOURCE_NOT_FOUND' | 'RESOURCE_RETIRED' | 'TIMEOUT';
|
|
1
|
+
export type TBrowserRuntimeErrorCode = 'ABORTED' | 'AUTHORIZATION_DENIED' | 'ATTACHMENT_CONFLICT' | 'BUSY' | 'CAPABILITY_EXPIRED' | 'CAPABILITY_INVALID' | 'CAPABILITY_REVOKED' | 'CONFINEMENT_FAILED' | 'EGRESS_DENIED' | 'FENCED' | 'FRAME_STREAM_FAILED' | 'FRAME_TOO_LARGE' | 'DIALOG_PENDING' | 'STALE_INPUT' | 'SUPERSEDED' | 'INVALID_INPUT' | 'LOCKED' | 'NOT_RUNNING' | 'PROTOCOL_ERROR' | 'QUOTA_EXCEEDED' | 'RESOURCE_NOT_FOUND' | 'RESOURCE_RETIRED' | 'TIMEOUT';
|
|
2
2
|
export declare class BrowserRuntimeError extends Error {
|
|
3
3
|
readonly code: TBrowserRuntimeErrorCode;
|
|
4
4
|
constructor(code: TBrowserRuntimeErrorCode, internalMessage?: string);
|
package/dist_ts/errors.js
CHANGED
|
@@ -13,6 +13,7 @@ const publicMessages = {
|
|
|
13
13
|
FRAME_TOO_LARGE: 'The browser frame exceeded its limit.',
|
|
14
14
|
DIALOG_PENDING: 'Respond to the website dialog before sending input.',
|
|
15
15
|
STALE_INPUT: 'The input belongs to a previous document or dialog.',
|
|
16
|
+
SUPERSEDED: 'A newer hover input replaced this queued operation.',
|
|
16
17
|
INVALID_INPUT: 'The request is invalid.',
|
|
17
18
|
LOCKED: 'The browser runtime is already locked.',
|
|
18
19
|
NOT_RUNNING: 'The browser runtime is not running.',
|
|
@@ -43,4 +44,4 @@ export const toBrowserRuntimeError = (error) => {
|
|
|
43
44
|
}
|
|
44
45
|
return new BrowserRuntimeError('ABORTED');
|
|
45
46
|
};
|
|
46
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
47
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvZXJyb3JzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQXlCQSxNQUFNLGNBQWMsR0FBNkM7SUFDL0QsT0FBTyxFQUFFLDRCQUE0QjtJQUNyQyxvQkFBb0IsRUFBRSxzQ0FBc0M7SUFDNUQsbUJBQW1CLEVBQUUsNkRBQTZEO0lBQ2xGLElBQUksRUFBRSwrQkFBK0I7SUFDckMsa0JBQWtCLEVBQUUsNkJBQTZCO0lBQ2pELGtCQUFrQixFQUFFLDRCQUE0QjtJQUNoRCxrQkFBa0IsRUFBRSxrQ0FBa0M7SUFDdEQsa0JBQWtCLEVBQUUsNkNBQTZDO0lBQ2pFLGFBQWEsRUFBRSwrQkFBK0I7SUFDOUMsTUFBTSxFQUFFLDZDQUE2QztJQUNyRCxtQkFBbUIsRUFBRSxrQ0FBa0M7SUFDdkQsZUFBZSxFQUFFLHVDQUF1QztJQUN4RCxjQUFjLEVBQUUscURBQXFEO0lBQ3JFLFdBQVcsRUFBRSxxREFBcUQ7SUFDbEUsVUFBVSxFQUFFLHFEQUFxRDtJQUNqRSxhQUFhLEVBQUUseUJBQXlCO0lBQ3hDLE1BQU0sRUFBRSx3Q0FBd0M7SUFDaEQsV0FBVyxFQUFFLHFDQUFxQztJQUNsRCxjQUFjLEVBQUUseUNBQXlDO0lBQ3pELGNBQWMsRUFBRSx1Q0FBdUM7SUFDdkQsa0JBQWtCLEVBQUUseUNBQXlDO0lBQzdELGdCQUFnQixFQUFFLHdDQUF3QztJQUMxRCxPQUFPLEVBQUUsMEJBQTBCO0NBQ3BDLENBQUM7QUFFRixNQUFNLE9BQU8sbUJBQW9CLFNBQVEsS0FBSztJQUM1QixJQUFJLENBQTJCO0lBRS9DLFlBQVksSUFBOEIsRUFBRSxlQUF3QjtRQUNsRSxLQUFLLENBQUMsZUFBZSxJQUFJLGNBQWMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBQy9DLElBQUksQ0FBQyxJQUFJLEdBQUcscUJBQXFCLENBQUM7UUFDbEMsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7SUFDbkIsQ0FBQztJQUVELElBQVcsYUFBYTtRQUN0QixPQUFPLGNBQWMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDbkMsQ0FBQztDQUNGO0FBRUQsTUFBTSxDQUFDLE1BQU0scUJBQXFCLEdBQUcsQ0FBQyxLQUFjLEVBQXVCLEVBQUU7SUFDM0UsSUFBSSxLQUFLLFlBQVksbUJBQW1CLEVBQUUsQ0FBQztRQUN6QyxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUM7SUFDRCxJQUNFLEtBQUssWUFBWSxLQUFLO1dBQ25CLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxZQUFZLElBQUksS0FBSyxDQUFDLElBQUksS0FBSyxjQUFjLENBQUMsRUFDakUsQ0FBQztRQUNELE9BQU8sSUFBSSxtQkFBbUIsQ0FBQyxLQUFLLENBQUMsSUFBSSxLQUFLLGNBQWMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQztJQUN4RixDQUFDO0lBQ0QsT0FBTyxJQUFJLG1CQUFtQixDQUFDLFNBQVMsQ0FBQyxDQUFDO0FBQzVDLENBQUMsQ0FBQyJ9
|
package/dist_ts/interfaces.d.ts
CHANGED
|
@@ -225,6 +225,7 @@ export interface IBrowserRuntimeEventSubscriptionOptions {
|
|
|
225
225
|
export type TBrowserRuntimeVideoOffer = Omit<plugins.smartpuppeteer.ILiveVideoOffer, 'peerId'>;
|
|
226
226
|
export interface IBrowserRuntimeState {
|
|
227
227
|
videoAcceleration?: plugins.smartpuppeteer.ILiveVideoAcceleration;
|
|
228
|
+
videoSource?: plugins.smartpuppeteer.ILiveVideoSource;
|
|
228
229
|
status: 'stopped' | 'starting' | 'running' | 'stopping';
|
|
229
230
|
activeTabId: string | null;
|
|
230
231
|
viewportRevision: number;
|
|
@@ -336,6 +337,8 @@ export interface IBrowserRuntimeOperationOptions {
|
|
|
336
337
|
signal?: AbortSignal;
|
|
337
338
|
timeoutMs?: number;
|
|
338
339
|
onOperationStarted?(operationId: string): void;
|
|
340
|
+
/** Replace an adjacent, unstarted hover from this lease. Superseded calls reject with SUPERSEDED. */
|
|
341
|
+
coalesceHoverMoves?: boolean;
|
|
339
342
|
}
|
|
340
343
|
export interface IBrowserRuntimeFrameSubscription {
|
|
341
344
|
close(): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelprofile.com/browser-runtime",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Parent-owned, resource-centric Chromium runtime with revisioned attachment fencing, authenticated human and agent control, fail-closed egress, bounded artifacts, and Flex/MCP adapters.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@push.rocks/smartagent": "^6.0.0",
|
|
31
31
|
"@push.rocks/smartipc": "^2.6.0",
|
|
32
32
|
"@push.rocks/smartmcp": "^0.3.0",
|
|
33
|
-
"@push.rocks/smartpuppeteer": "^2.
|
|
33
|
+
"@push.rocks/smartpuppeteer": "^2.13.0",
|
|
34
34
|
"ipaddr.js": "^2.5.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
package/readme.md
CHANGED
|
@@ -34,6 +34,19 @@ Human leases expose `openVideoPeer(options?)`, `answerVideoPeer(negotiationId, d
|
|
|
34
34
|
|
|
35
35
|
Set `video` on `BrowserRuntime` for trusted host limits or ICE configuration. Defaults use direct connections with no external STUN/TURN service and automatic GPU support. The private capture extension is loaded without weakening ordinary page proxy, DNS, permission or WebRTC confinement. `state.videoAcceleration` and peer statistics report actual browser acceleration/encoder support.
|
|
36
36
|
|
|
37
|
+
`video: { backend: 'native', maxFrameRate: 30 }` selects SmartPuppeteer's Rust/NVIDIA
|
|
38
|
+
capture and NVENC sender. Chromium remains the default backend. Native viewers must
|
|
39
|
+
forward `state.videoSource` unchanged, including its RTP presentation fence and coded
|
|
40
|
+
frame alignment. The native viewport negotiates device scale 1 and fits the configured
|
|
41
|
+
capture limits while preserving the preferred aspect ratio. The returned viewport owns
|
|
42
|
+
input coordinates. Unsupported receiver codec limits fail negotiation explicitly.
|
|
43
|
+
|
|
44
|
+
Forward DOM pointer `event.timeStamp` as `dispatchMouse().timestampMs` to preserve the
|
|
45
|
+
occurrence spacing of a gesture delivered in a transport burst. Participant changes reset
|
|
46
|
+
the native timing history because viewer clocks have different origins; synthesized
|
|
47
|
+
button releases omit timestamps. Existing operation ordering and held-button ownership
|
|
48
|
+
remain authoritative.
|
|
49
|
+
|
|
37
50
|
Native viewers should call `lease.subscribeEvents(listener, { includeFrames: false })`. They receive state and errors without generating JPEG traffic. Existing image consumers retain the default `includeFrames: true`; capture starts for the first such subscriber and stops after the last closes. Agent snapshots remain independent of continuous capture. Image subscription cleanup is serialized, coalesced and retryable; a failed disable is retained until cleanup succeeds or its exact browser incarnation terminates.
|
|
38
51
|
|
|
39
52
|
## Issue Reporting and Security
|
|
@@ -164,6 +177,9 @@ Agent actions are exactly `navigate`, `snapshot`, `screenshot`, `click`, `fill`,
|
|
|
164
177
|
|
|
165
178
|
BrowserRuntime alone owns the bounded execution scheduler shared by all participants. Host transports submit operations in order without adding a second execution queue or performing asynchronous authorization ahead of admission. One statistics read may run independently of mutations; its preflight and completion cannot fence input, navigation, or dialog replies. Up to four wheel operations may run concurrently, with authorization and native-send admission kept in order. Other input, viewport, and semantic operations remain barriers behind preceding wheel work. Dialog replies use one interrupt slot so a paused document operation cannot block its own response. `maxQueuedOperationsPerLease` defaults to 128 and accepts 1 through 1,024; each participant has that bound, and the resource queue is bounded by its product with `maxCapabilitiesPerResource`. Overflow fails with `QUOTA_EXCEEDED`. Releasing a participant cancels only its queued and active work. Resource termination and shutdown cancel everyone. Started uncertain work must quiesce or the exact incarnation is terminated before operation settlement.
|
|
166
179
|
|
|
180
|
+
Interactive hosts can pass `{ coalesceHoverMoves: true }` to `humanLease.dispatchMouse()`. Only an adjacent, unstarted, unpressed move from the same lease, document, viewport and modifier state is replaced. The replaced promise rejects with `BrowserRuntimeError` code `SUPERSEDED`; hosts should report that terminal outcome without treating it as a failed connection. Drag paths, queued or held buttons, other participants, and discrete operations retain their order. The default preserves every submitted operation.
|
|
181
|
+
|
|
182
|
+
|
|
167
183
|
`setViewport()` stores the viewer's preferred width, height and device scale factor and returns `IBrowserRuntimeViewportResult` with the effective `viewport` and `viewportRevision`. The effective values are the componentwise minima across active viewers that have supplied a preference. Unchanged effective sizes do not restart the stream. Removing a viewer recomputes the viewport in the resource FIFO. Held keys and buttons are tracked per participant; leaving releases only inputs that no other participant holds. Viewport and tab/navigation transitions clear held input before changing the target. Resource-owned departure cleanup is bounded and audited as `releaseParticipant`; it does not call the external `beforeOperation` gate after the participant has lost access.
|
|
168
184
|
|
|
169
185
|
`beforeOperation` is an optional awaited fail-closed gate. It receives the complete immutable authority, operation/capability/lease IDs, action, classification, start time, and an `AbortSignal` after dequeue but before the browser side effect starts. Classifications are `raw-input`, `dialog`, `devtools`, `frame-stream`, `video-peer`, `viewport`, `navigation`, `tab`, and `agent-action`. Hosts that require durable attempt-before-side-effect auditing should persist the attempt there. Rejection denies the operation. The default `beforeOperationTimeoutMs` is 10,000 milliseconds and accepts values from 100 through 120,000; timeout fails with `TIMEOUT`. The terminal `audit` callback remains a best-effort `completed`/`failed` notification correlated by the same operation ID and classification and runs after bounded operation cleanup releases or fences the exact reservation.
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@modelprofile.com/browser-runtime',
|
|
6
|
-
version: '5.
|
|
6
|
+
version: '5.7.0',
|
|
7
7
|
description: 'Parent-owned, resource-centric Chromium runtime with revisioned attachment fencing, authenticated human and agent control, fail-closed egress, bounded artifacts, and Flex/MCP adapters.'
|
|
8
8
|
}
|
package/ts/classes.runtime.ts
CHANGED
|
@@ -172,6 +172,7 @@ export interface IBrowserRuntimeLeaseRenewalOptions {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
interface IOperationRecord {
|
|
175
|
+
hoverKey?: string;
|
|
175
176
|
operationId: string;
|
|
176
177
|
lease: ILeaseRecord;
|
|
177
178
|
controller: AbortController;
|
|
@@ -183,6 +184,7 @@ interface IOperationRecord {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
interface IQueuedOperationRecord {
|
|
187
|
+
hoverKey?: string;
|
|
186
188
|
operationId: string;
|
|
187
189
|
lease: ILeaseRecord;
|
|
188
190
|
action: string;
|
|
@@ -263,6 +265,7 @@ interface IResourceSlot {
|
|
|
263
265
|
profileDirectory?: string;
|
|
264
266
|
unsubscribeSession?: () => void;
|
|
265
267
|
leases: Map<string, ILeaseRecord>;
|
|
268
|
+
lastMouseTimestampLeaseId?: string;
|
|
266
269
|
operations: Set<IOperationRecord>;
|
|
267
270
|
inputCleanup?: { session: ILiveBrowserSessionLike; incarnationGeneration: number; promise: Promise<void> };
|
|
268
271
|
operationScheduler: BrowserOperationScheduler<IQueuedOperationRecord>;
|
|
@@ -1595,7 +1598,16 @@ export class BrowserRuntime {
|
|
|
1595
1598
|
width: Math.min(...preferences.map((value) => value.width)),
|
|
1596
1599
|
height: Math.min(...preferences.map((value) => value.height)),
|
|
1597
1600
|
deviceScaleFactor: Math.min(...preferences.map((value) => value.deviceScaleFactor)),
|
|
1598
|
-
} : current.viewport;
|
|
1601
|
+
} : { ...current.viewport };
|
|
1602
|
+
if (this.options.video.backend === 'native') {
|
|
1603
|
+
// A native desktop has physical pixels. Negotiate the accepted viewport
|
|
1604
|
+
// here so all viewers use the same layout and coordinate space.
|
|
1605
|
+
const scale = Math.min(1, this.options.video.maxWidth / viewport.width,
|
|
1606
|
+
this.options.video.maxHeight / viewport.height);
|
|
1607
|
+
viewport.width = Math.max(1, Math.floor(viewport.width * scale));
|
|
1608
|
+
viewport.height = Math.max(1, Math.floor(viewport.height * scale));
|
|
1609
|
+
viewport.deviceScaleFactor = 1;
|
|
1610
|
+
}
|
|
1599
1611
|
if (
|
|
1600
1612
|
viewport.width !== current.viewport.width || viewport.height !== current.viewport.height
|
|
1601
1613
|
|| viewport.deviceScaleFactor !== current.viewport.deviceScaleFactor
|
|
@@ -1671,6 +1683,21 @@ export class BrowserRuntime {
|
|
|
1671
1683
|
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
1672
1684
|
): Promise<void> {
|
|
1673
1685
|
this.requireHuman(lease);
|
|
1686
|
+
const mouse = action === 'dispatchMouse' ? input as plugins.smartpuppeteer.ILiveBrowserMouseInput : undefined;
|
|
1687
|
+
if (mouse?.timestampMs !== undefined && (!Number.isFinite(mouse.timestampMs) || mouse.timestampMs < 0)) {
|
|
1688
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1689
|
+
}
|
|
1690
|
+
// Drag paths and shared held input have observable intermediate positions.
|
|
1691
|
+
// Only explicit hover submissions can replace another pending hover.
|
|
1692
|
+
const hoverKey = operationOptions.coalesceHoverMoves && mouse?.type === 'move'
|
|
1693
|
+
&& (mouse.buttons ?? 0) === 0 && (mouse.button ?? 'none') === 'none'
|
|
1694
|
+
&& Number.isFinite(mouse.x) && Number.isFinite(mouse.y) && mouse.x >= 0 && mouse.y >= 0
|
|
1695
|
+
&& ![...lease.slot.leases.values()].some(participant => participant.heldButtons.size > 0)
|
|
1696
|
+
&& !lease.slot.operationScheduler.pending.some(operation => operation.action === 'dispatchMouse' && !operation.hoverKey)
|
|
1697
|
+
&& ![...lease.slot.operations].some(operation => operation.action === 'dispatchMouse' && !operation.hoverKey)
|
|
1698
|
+
? JSON.stringify([mouse.tabId, mouse.generation, mouse.viewportRevision,
|
|
1699
|
+
mouse.modifiers?.alt ?? false, mouse.modifiers?.control ?? false,
|
|
1700
|
+
mouse.modifiers?.meta ?? false, mouse.modifiers?.shift ?? false]) : undefined;
|
|
1674
1701
|
await this.runOperation(lease, action, 'raw-input', operationOptions, async (_signal, session) => {
|
|
1675
1702
|
if (action === 'dispatchMouse') {
|
|
1676
1703
|
const mouse = input as plugins.smartpuppeteer.ILiveBrowserMouseInput;
|
|
@@ -1689,11 +1716,16 @@ export class BrowserRuntime {
|
|
|
1689
1716
|
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
1690
1717
|
}
|
|
1691
1718
|
if (mouse.type === 'down') lease.heldButtons.set(key, { ...mouse });
|
|
1719
|
+
const previousClock = lease.slot.lastMouseTimestampLeaseId;
|
|
1720
|
+
// Viewer performance clocks have unrelated origins. An untimed event
|
|
1721
|
+
// resets native gesture pacing when another participant takes over.
|
|
1692
1722
|
await session.dispatchMouse({ ...mouse,
|
|
1723
|
+
timestampMs: previousClock && previousClock !== lease.leaseId ? undefined : mouse.timestampMs,
|
|
1693
1724
|
buttons: this.sharedMouseButtons(lease.slot, mouse.tabId,
|
|
1694
1725
|
mouse.type === 'up' ? { lease, key } : undefined),
|
|
1695
1726
|
modifiers: this.sharedModifiers(lease.slot, mouse.tabId, mouse.modifiers),
|
|
1696
1727
|
});
|
|
1728
|
+
lease.slot.lastMouseTimestampLeaseId = lease.leaseId;
|
|
1697
1729
|
if (mouse.type === 'up') lease.heldButtons.delete(key);
|
|
1698
1730
|
} else if (action === 'dispatchWheel') {
|
|
1699
1731
|
const wheel = input as plugins.smartpuppeteer.ILiveBrowserWheelInput;
|
|
@@ -1726,7 +1758,7 @@ export class BrowserRuntime {
|
|
|
1726
1758
|
} else {
|
|
1727
1759
|
await session.insertText(input as plugins.smartpuppeteer.ILiveBrowserInsertTextInput);
|
|
1728
1760
|
}
|
|
1729
|
-
});
|
|
1761
|
+
}, undefined, false, hoverKey);
|
|
1730
1762
|
}
|
|
1731
1763
|
|
|
1732
1764
|
private keyOwnershipKey(input: plugins.smartpuppeteer.ILiveBrowserKeyInput): string {
|
|
@@ -2351,6 +2383,7 @@ export class BrowserRuntime {
|
|
|
2351
2383
|
execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<T>,
|
|
2352
2384
|
finalize?: () => Promise<void>,
|
|
2353
2385
|
participantCleanup: 'participant' | 'subscription' | false = false,
|
|
2386
|
+
hoverKey?: string,
|
|
2354
2387
|
): Promise<T> {
|
|
2355
2388
|
if (!operationOptions || typeof operationOptions !== 'object') {
|
|
2356
2389
|
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
@@ -2364,10 +2397,15 @@ export class BrowserRuntime {
|
|
|
2364
2397
|
&& typeof operationOptions.onOperationStarted !== 'function'
|
|
2365
2398
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2366
2399
|
if (operationOptions.signal?.aborted) throw new BrowserRuntimeError('ABORTED');
|
|
2400
|
+
if (operationOptions.coalesceHoverMoves !== undefined
|
|
2401
|
+
&& typeof operationOptions.coalesceHoverMoves !== 'boolean') throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2367
2402
|
const slot = lease.slot;
|
|
2368
2403
|
this.requireQueuedLease(lease, participantCleanup);
|
|
2369
2404
|
this.assertSlotAvailable(slot);
|
|
2370
|
-
|
|
2405
|
+
const previousHover = slot.operationScheduler.pending.at(-1);
|
|
2406
|
+
const superseded = hoverKey !== undefined && previousHover?.state === 'queued'
|
|
2407
|
+
&& previousHover.lease === lease && previousHover.hoverKey === hoverKey ? previousHover : undefined;
|
|
2408
|
+
if (!participantCleanup && !superseded && (
|
|
2371
2409
|
slot.operationScheduler.pending.filter((entry) => entry.lease === lease).length
|
|
2372
2410
|
>= this.options.maxQueuedOperationsPerLease
|
|
2373
2411
|
|| slot.operationScheduler.pending.length >= this.options.maxQueuedOperationsPerLease
|
|
@@ -2385,6 +2423,7 @@ export class BrowserRuntime {
|
|
|
2385
2423
|
]);
|
|
2386
2424
|
return new Promise<T>((resolve, reject) => {
|
|
2387
2425
|
const queued: IQueuedOperationRecord = {
|
|
2426
|
+
hoverKey,
|
|
2388
2427
|
operationId: randomId(18),
|
|
2389
2428
|
lease,
|
|
2390
2429
|
action,
|
|
@@ -2423,6 +2462,14 @@ export class BrowserRuntime {
|
|
|
2423
2462
|
onQueuedAbort();
|
|
2424
2463
|
return;
|
|
2425
2464
|
}
|
|
2465
|
+
if (superseded) {
|
|
2466
|
+
// Replace atomically before draining; remove() would start the old tail's successor.
|
|
2467
|
+
const index = slot.operationScheduler.pending.indexOf(superseded);
|
|
2468
|
+
if (index >= 0) slot.operationScheduler.pending.splice(index, 1);
|
|
2469
|
+
superseded.state = 'settled';
|
|
2470
|
+
if (superseded.onQueuedAbort) superseded.signal.removeEventListener('abort', superseded.onQueuedAbort);
|
|
2471
|
+
superseded.reject(new BrowserRuntimeError('SUPERSEDED'));
|
|
2472
|
+
}
|
|
2426
2473
|
slot.operationScheduler.enqueue(queued);
|
|
2427
2474
|
});
|
|
2428
2475
|
}
|
|
@@ -2471,6 +2518,7 @@ export class BrowserRuntime {
|
|
|
2471
2518
|
timeoutController.signal,
|
|
2472
2519
|
]);
|
|
2473
2520
|
operation = {
|
|
2521
|
+
hoverKey: queued.hoverKey,
|
|
2474
2522
|
operationId: queued.operationId,
|
|
2475
2523
|
lease,
|
|
2476
2524
|
controller,
|
|
@@ -2890,6 +2938,7 @@ export class BrowserRuntime {
|
|
|
2890
2938
|
private resourceState(state: plugins.smartpuppeteer.ILiveBrowserState): IBrowserRuntimeState {
|
|
2891
2939
|
return {
|
|
2892
2940
|
...(state.videoAcceleration ? { videoAcceleration: { ...state.videoAcceleration } } : {}),
|
|
2941
|
+
...(state.videoSource ? { videoSource: structuredClone(state.videoSource) } : {}),
|
|
2893
2942
|
status: state.status,
|
|
2894
2943
|
activeTabId: state.activeTabId ? truncateString(state.activeTabId, 128) : null,
|
|
2895
2944
|
viewportRevision: state.viewportRevision,
|
package/ts/errors.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type TBrowserRuntimeErrorCode =
|
|
|
13
13
|
| 'FRAME_TOO_LARGE'
|
|
14
14
|
| 'DIALOG_PENDING'
|
|
15
15
|
| 'STALE_INPUT'
|
|
16
|
+
| 'SUPERSEDED'
|
|
16
17
|
| 'INVALID_INPUT'
|
|
17
18
|
| 'LOCKED'
|
|
18
19
|
| 'NOT_RUNNING'
|
|
@@ -37,6 +38,7 @@ const publicMessages: Record<TBrowserRuntimeErrorCode, string> = {
|
|
|
37
38
|
FRAME_TOO_LARGE: 'The browser frame exceeded its limit.',
|
|
38
39
|
DIALOG_PENDING: 'Respond to the website dialog before sending input.',
|
|
39
40
|
STALE_INPUT: 'The input belongs to a previous document or dialog.',
|
|
41
|
+
SUPERSEDED: 'A newer hover input replaced this queued operation.',
|
|
40
42
|
INVALID_INPUT: 'The request is invalid.',
|
|
41
43
|
LOCKED: 'The browser runtime is already locked.',
|
|
42
44
|
NOT_RUNNING: 'The browser runtime is not running.',
|
package/ts/interfaces.ts
CHANGED
|
@@ -333,6 +333,7 @@ export type TBrowserRuntimeVideoOffer = Omit<plugins.smartpuppeteer.ILiveVideoOf
|
|
|
333
333
|
|
|
334
334
|
export interface IBrowserRuntimeState {
|
|
335
335
|
videoAcceleration?: plugins.smartpuppeteer.ILiveVideoAcceleration;
|
|
336
|
+
videoSource?: plugins.smartpuppeteer.ILiveVideoSource;
|
|
336
337
|
status: 'stopped' | 'starting' | 'running' | 'stopping';
|
|
337
338
|
activeTabId: string | null;
|
|
338
339
|
viewportRevision: number;
|
|
@@ -460,6 +461,8 @@ export interface IBrowserRuntimeOperationOptions {
|
|
|
460
461
|
signal?: AbortSignal;
|
|
461
462
|
timeoutMs?: number;
|
|
462
463
|
onOperationStarted?(operationId: string): void;
|
|
464
|
+
/** Replace an adjacent, unstarted hover from this lease. Superseded calls reject with SUPERSEDED. */
|
|
465
|
+
coalesceHoverMoves?: boolean;
|
|
463
466
|
}
|
|
464
467
|
|
|
465
468
|
export interface IBrowserRuntimeFrameSubscription {
|