@modelprofile.com/browser-runtime 5.3.0 → 5.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +18 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.runtime.d.ts +11 -1
- package/dist_ts/classes.runtime.js +152 -67
- package/dist_ts/errors.d.ts +1 -1
- package/dist_ts/errors.js +3 -1
- package/dist_ts/interfaces.d.ts +3 -1
- package/package.json +2 -2
- package/readme.md +6 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.runtime.ts +127 -61
- package/ts/errors.ts +4 -0
- package/ts/interfaces.ts +4 -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' | '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' | '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
|
@@ -11,6 +11,8 @@ const publicMessages = {
|
|
|
11
11
|
FENCED: 'The browser resource is permanently fenced.',
|
|
12
12
|
FRAME_STREAM_FAILED: 'The browser frame stream failed.',
|
|
13
13
|
FRAME_TOO_LARGE: 'The browser frame exceeded its limit.',
|
|
14
|
+
DIALOG_PENDING: 'Respond to the website dialog before sending input.',
|
|
15
|
+
STALE_INPUT: 'The input belongs to a previous document or dialog.',
|
|
14
16
|
INVALID_INPUT: 'The request is invalid.',
|
|
15
17
|
LOCKED: 'The browser runtime is already locked.',
|
|
16
18
|
NOT_RUNNING: 'The browser runtime is not running.',
|
|
@@ -41,4 +43,4 @@ export const toBrowserRuntimeError = (error) => {
|
|
|
41
43
|
}
|
|
42
44
|
return new BrowserRuntimeError('ABORTED');
|
|
43
45
|
};
|
|
44
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
46
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvZXJyb3JzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQXdCQSxNQUFNLGNBQWMsR0FBNkM7SUFDL0QsT0FBTyxFQUFFLDRCQUE0QjtJQUNyQyxvQkFBb0IsRUFBRSxzQ0FBc0M7SUFDNUQsbUJBQW1CLEVBQUUsNkRBQTZEO0lBQ2xGLElBQUksRUFBRSwrQkFBK0I7SUFDckMsa0JBQWtCLEVBQUUsNkJBQTZCO0lBQ2pELGtCQUFrQixFQUFFLDRCQUE0QjtJQUNoRCxrQkFBa0IsRUFBRSxrQ0FBa0M7SUFDdEQsa0JBQWtCLEVBQUUsNkNBQTZDO0lBQ2pFLGFBQWEsRUFBRSwrQkFBK0I7SUFDOUMsTUFBTSxFQUFFLDZDQUE2QztJQUNyRCxtQkFBbUIsRUFBRSxrQ0FBa0M7SUFDdkQsZUFBZSxFQUFFLHVDQUF1QztJQUN4RCxjQUFjLEVBQUUscURBQXFEO0lBQ3JFLFdBQVcsRUFBRSxxREFBcUQ7SUFDbEUsYUFBYSxFQUFFLHlCQUF5QjtJQUN4QyxNQUFNLEVBQUUsd0NBQXdDO0lBQ2hELFdBQVcsRUFBRSxxQ0FBcUM7SUFDbEQsY0FBYyxFQUFFLHlDQUF5QztJQUN6RCxjQUFjLEVBQUUsdUNBQXVDO0lBQ3ZELGtCQUFrQixFQUFFLHlDQUF5QztJQUM3RCxnQkFBZ0IsRUFBRSx3Q0FBd0M7SUFDMUQsT0FBTyxFQUFFLDBCQUEwQjtDQUNwQyxDQUFDO0FBRUYsTUFBTSxPQUFPLG1CQUFvQixTQUFRLEtBQUs7SUFDNUIsSUFBSSxDQUEyQjtJQUUvQyxZQUFZLElBQThCLEVBQUUsZUFBd0I7UUFDbEUsS0FBSyxDQUFDLGVBQWUsSUFBSSxjQUFjLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztRQUMvQyxJQUFJLENBQUMsSUFBSSxHQUFHLHFCQUFxQixDQUFDO1FBQ2xDLElBQUksQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO0lBQ25CLENBQUM7SUFFRCxJQUFXLGFBQWE7UUFDdEIsT0FBTyxjQUFjLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ25DLENBQUM7Q0FDRjtBQUVELE1BQU0sQ0FBQyxNQUFNLHFCQUFxQixHQUFHLENBQUMsS0FBYyxFQUF1QixFQUFFO0lBQzNFLElBQUksS0FBSyxZQUFZLG1CQUFtQixFQUFFLENBQUM7UUFDekMsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0lBQ0QsSUFDRSxLQUFLLFlBQVksS0FBSztXQUNuQixDQUFDLEtBQUssQ0FBQyxJQUFJLEtBQUssWUFBWSxJQUFJLEtBQUssQ0FBQyxJQUFJLEtBQUssY0FBYyxDQUFDLEVBQ2pFLENBQUM7UUFDRCxPQUFPLElBQUksbUJBQW1CLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxjQUFjLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDeEYsQ0FBQztJQUNELE9BQU8sSUFBSSxtQkFBbUIsQ0FBQyxTQUFTLENBQUMsQ0FBQztBQUM1QyxDQUFDLENBQUMifQ==
|
package/dist_ts/interfaces.d.ts
CHANGED
|
@@ -101,7 +101,7 @@ export type TBrowserRuntimeOperationIdentity = TReadonlyBrowserCapabilityAuthori
|
|
|
101
101
|
readonly classification: TBrowserRuntimeOperationClassification;
|
|
102
102
|
readonly startedAt: number;
|
|
103
103
|
};
|
|
104
|
-
export type TBrowserRuntimeOperationClassification = 'raw-input' | 'frame-stream' | 'video-peer' | 'devtools' | 'viewport' | 'navigation' | 'tab' | 'agent-action';
|
|
104
|
+
export type TBrowserRuntimeOperationClassification = 'dialog' | 'raw-input' | 'frame-stream' | 'video-peer' | 'devtools' | 'viewport' | 'navigation' | 'tab' | 'agent-action';
|
|
105
105
|
/** The viewport accepted by the shared resource, which may be smaller than one viewer's request. */
|
|
106
106
|
export interface IBrowserRuntimeViewportResult {
|
|
107
107
|
viewport: plugins.smartpuppeteer.ILiveBrowserViewport;
|
|
@@ -120,6 +120,7 @@ export type TBrowserRuntimeAuditEvent = TBrowserRuntimeOperationIdentity & {
|
|
|
120
120
|
readonly errorCode?: string;
|
|
121
121
|
};
|
|
122
122
|
export interface ILiveBrowserSessionLike {
|
|
123
|
+
respondToDialog(input: plugins.smartpuppeteer.ILiveBrowserDialogResponse, options?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<void>;
|
|
123
124
|
openDevTools?(options: plugins.smartpuppeteer.ILiveBrowserDevToolsOptions, operationOptions?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<plugins.smartpuppeteer.ILiveBrowserDevToolsConnection>;
|
|
124
125
|
start(options?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<void>;
|
|
125
126
|
stop(): Promise<void>;
|
|
@@ -207,6 +208,7 @@ export interface IBrowserRuntimeOptions {
|
|
|
207
208
|
artifacts?: Omit<IBrowserArtifactStoreOptions, 'rootDirectory'>;
|
|
208
209
|
}
|
|
209
210
|
export interface IBrowserRuntimeTabState {
|
|
211
|
+
dialog?: plugins.smartpuppeteer.ILiveBrowserDialog;
|
|
210
212
|
id: string;
|
|
211
213
|
url: string;
|
|
212
214
|
title: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelprofile.com/browser-runtime",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.1",
|
|
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.12.0",
|
|
34
34
|
"ipaddr.js": "^2.5.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
package/readme.md
CHANGED
|
@@ -24,6 +24,10 @@ No browser debugging port or browser-wide target discovery is exposed. Pair this
|
|
|
24
24
|
connection with `DevToolsFrontend` from `@push.rocks/smartbrowser/web` for the official
|
|
25
25
|
Chrome DevTools UI.
|
|
26
26
|
|
|
27
|
+
## Page dialogs
|
|
28
|
+
|
|
29
|
+
`state.tabs[].dialog` exposes the exact pending website dialog. A human lease can call `respondToDialog({ tabId, dialogId, accept, promptText? }, options?)`. This response receives the same authorization, audit, cancellation, and incarnation checks as other operations while bypassing document serialization. Agent leases cannot send this human response. A pending dialog rejects raw input with `DIALOG_PENDING`, preserving held-input tracking until it closes. Before input enters a new document, the runtime releases native keys and buttons retained from the previous document. Obsolete input and dialog identities fail with `STALE_INPUT`; they do not imply a failed browser connection.
|
|
30
|
+
|
|
27
31
|
## Native video viewers
|
|
28
32
|
|
|
29
33
|
Human leases expose `openVideoPeer(options?)`, `answerVideoPeer(negotiationId, description, options?)`, `closeVideoPeer(options?)`, and `getVideoStatistics(options?)`. Each operation runs through the resource's bounded, authorized and audited operation queue. Peer IDs are private to the lease; offers expose the negotiation ID and exact tab/generation/viewport source identity. Agent leases cannot open media peers. Releasing or revoking a human lease closes only its peer, preserving other viewers and agent work. Failed peer cleanup remains owned for a subsequent release attempt.
|
|
@@ -158,11 +162,11 @@ by `maxCapabilities`. Hosts must stop scheduling renewal when their viewer disco
|
|
|
158
162
|
|
|
159
163
|
Agent actions are exactly `navigate`, `snapshot`, `screenshot`, `click`, `fill`, and `press`. Agents can read or delete screenshots created by their exact lease using `readArtifact()` and `deleteArtifact()`. Human leases additionally expose tab lifecycle, viewport, raw input, frame subscription/acknowledgement and refresh, and exact-resource artifact reads/deletes. JavaScript evaluation is not public.
|
|
160
164
|
|
|
161
|
-
All participants
|
|
165
|
+
All participants share a bounded resource queue. 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.
|
|
162
166
|
|
|
163
167
|
`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.
|
|
164
168
|
|
|
165
|
-
`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`, `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.
|
|
169
|
+
`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.
|
|
166
170
|
|
|
167
171
|
`lease.getAuthority()` returns an immutable process-local lease authority containing the runtime authority ID, exported `authorityGeneration`, incarnation generation, and complete capability binding. Every snapshot also binds the exact `capabilityId` and `leaseId`. `lease.isAuthorityCurrent(authority)` performs an exact synchronous revalidation suitable for a host-owned virtual stream. These values are not durable Controller state and do not replace attachment checks against the Controller database.
|
|
168
172
|
|
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.4.1',
|
|
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
|
@@ -262,7 +262,9 @@ interface IResourceSlot {
|
|
|
262
262
|
profileDirectory?: string;
|
|
263
263
|
unsubscribeSession?: () => void;
|
|
264
264
|
leases: Map<string, ILeaseRecord>;
|
|
265
|
-
|
|
265
|
+
operations: Set<IOperationRecord>;
|
|
266
|
+
activeQueuedOperations: Set<IQueuedOperationRecord>;
|
|
267
|
+
inputCleanup?: { session: ILiveBrowserSessionLike; incarnationGeneration: number; promise: Promise<void> };
|
|
266
268
|
operationQueue: IQueuedOperationRecord[];
|
|
267
269
|
operationSchedulerRunning: boolean;
|
|
268
270
|
frameSubscriptions: Map<string, IFrameSubscriptionRecord>;
|
|
@@ -743,6 +745,8 @@ export class BrowserRuntime {
|
|
|
743
745
|
frameSubscriptions: new Map(),
|
|
744
746
|
highestFrameSequence: 0,
|
|
745
747
|
producerAcknowledgements: new Set(),
|
|
748
|
+
operations: new Set(),
|
|
749
|
+
activeQueuedOperations: new Set(),
|
|
746
750
|
operationQueue: [],
|
|
747
751
|
operationSchedulerRunning: false,
|
|
748
752
|
lifecycleTail: Promise.resolve(),
|
|
@@ -1640,6 +1644,24 @@ export class BrowserRuntime {
|
|
|
1640
1644
|
);
|
|
1641
1645
|
}
|
|
1642
1646
|
|
|
1647
|
+
/** @internal */
|
|
1648
|
+
public async leaseRespondToDialog(lease: ILeaseRecord,
|
|
1649
|
+
input: plugins.smartpuppeteer.ILiveBrowserDialogResponse,
|
|
1650
|
+
options: IBrowserRuntimeOperationOptions = {}): Promise<void> {
|
|
1651
|
+
this.requireHuman(lease);
|
|
1652
|
+
const record = validateExactKeys(input, ['tabId', 'dialogId', 'accept', 'promptText'], 'dialog');
|
|
1653
|
+
const tabId = validateBoundedString(record.tabId, 'tabId', 1, 128);
|
|
1654
|
+
const dialogId = validateBoundedString(record.dialogId, 'dialogId', 1, 128);
|
|
1655
|
+
if (typeof record.accept !== 'boolean' || (record.promptText !== undefined
|
|
1656
|
+
&& (typeof record.promptText !== 'string' || record.promptText.length > 32768))) {
|
|
1657
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1658
|
+
}
|
|
1659
|
+
const response = { tabId, dialogId, accept: record.accept,
|
|
1660
|
+
...(record.promptText !== undefined ? { promptText: record.promptText as string } : {}) };
|
|
1661
|
+
return this.runOperation(lease, 'respondToDialog', 'dialog', options,
|
|
1662
|
+
async (signal, session) => session.respondToDialog(response, { signal }));
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1643
1665
|
/** @internal */
|
|
1644
1666
|
public async leaseDispatchRawInput(
|
|
1645
1667
|
lease: ILeaseRecord,
|
|
@@ -1722,7 +1744,7 @@ export class BrowserRuntime {
|
|
|
1722
1744
|
const tab = state.tabs.find((value) => value.id === state.activeTabId);
|
|
1723
1745
|
if (!tab || tab.status !== 'open' || tab.id !== input.tabId
|
|
1724
1746
|
|| tab.generation !== input.generation || state.viewportRevision !== input.viewportRevision
|
|
1725
|
-
) throw new BrowserRuntimeError('
|
|
1747
|
+
) throw new BrowserRuntimeError('STALE_INPUT');
|
|
1726
1748
|
}
|
|
1727
1749
|
|
|
1728
1750
|
private mouseOwnershipKey(input: plugins.smartpuppeteer.ILiveBrowserMouseInput): string {
|
|
@@ -1770,6 +1792,29 @@ export class BrowserRuntime {
|
|
|
1770
1792
|
return modifiers;
|
|
1771
1793
|
}
|
|
1772
1794
|
|
|
1795
|
+
private async releaseStaleParticipantInput(slot: IResourceSlot, session: ILiveBrowserSessionLike): Promise<void> {
|
|
1796
|
+
const incarnationGeneration = slot.incarnationGeneration;
|
|
1797
|
+
if (slot.session !== session) throw new BrowserRuntimeError('ABORTED');
|
|
1798
|
+
if (slot.inputCleanup?.session === session
|
|
1799
|
+
&& slot.inputCleanup.incarnationGeneration === incarnationGeneration) return slot.inputCleanup.promise;
|
|
1800
|
+
const state = session.getState();
|
|
1801
|
+
const tab = state.tabs.find(tab => tab.id === state.activeTabId);
|
|
1802
|
+
if (!tab?.streaming || tab.dialog) return;
|
|
1803
|
+
const affected = [...slot.leases.values()].filter(lease => [...lease.heldKeys.values(), ...lease.heldButtons.values()]
|
|
1804
|
+
.some(input => input.tabId !== tab.id || input.generation !== tab.generation
|
|
1805
|
+
|| input.viewportRevision !== state.viewportRevision));
|
|
1806
|
+
if (!affected.length) return;
|
|
1807
|
+
// The runtime owns native held input, including link navigation that bypasses its command queue.
|
|
1808
|
+
const cleanup = { session, incarnationGeneration, promise: (async () => {
|
|
1809
|
+
for (const lease of affected) {
|
|
1810
|
+
if (slot.session !== session || slot.incarnationGeneration !== incarnationGeneration) return;
|
|
1811
|
+
await this.releaseParticipantInput(lease, session);
|
|
1812
|
+
}
|
|
1813
|
+
})() };
|
|
1814
|
+
slot.inputCleanup = cleanup;
|
|
1815
|
+
try { await cleanup.promise; } finally { if (slot.inputCleanup === cleanup) slot.inputCleanup = undefined; }
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1773
1818
|
private async releaseAllParticipantInput(
|
|
1774
1819
|
slot: IResourceSlot, session: ILiveBrowserSessionLike,
|
|
1775
1820
|
): Promise<void> {
|
|
@@ -1924,10 +1969,9 @@ export class BrowserRuntime {
|
|
|
1924
1969
|
/** @internal */
|
|
1925
1970
|
public cancelLeaseOperation(lease: ILeaseRecord): boolean {
|
|
1926
1971
|
this.requireValidLease(lease);
|
|
1927
|
-
const
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
return true;
|
|
1972
|
+
const operations = [...lease.slot.operations].filter(operation => operation.lease === lease);
|
|
1973
|
+
for (const operation of operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1974
|
+
return operations.length > 0;
|
|
1931
1975
|
}
|
|
1932
1976
|
|
|
1933
1977
|
/** @internal */
|
|
@@ -2391,37 +2435,42 @@ export class BrowserRuntime {
|
|
|
2391
2435
|
private drainOperationQueue(slot: IResourceSlot): void {
|
|
2392
2436
|
if (slot.operationSchedulerRunning) return;
|
|
2393
2437
|
slot.operationSchedulerRunning = true;
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
const
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
queued.
|
|
2414
|
-
}
|
|
2438
|
+
void (async () => {
|
|
2439
|
+
try {
|
|
2440
|
+
while (slot.operationQueue.length > 0) {
|
|
2441
|
+
// Dialog replies must unblock a page operation already waiting on Chrome.
|
|
2442
|
+
const dialogIndex = slot.operationQueue.findIndex(entry => entry.classification === 'dialog');
|
|
2443
|
+
const index = dialogIndex >= 0 ? dialogIndex : 0;
|
|
2444
|
+
const queued = slot.operationQueue[index]!;
|
|
2445
|
+
const active = [...slot.activeQueuedOperations];
|
|
2446
|
+
if (queued.classification === 'dialog') {
|
|
2447
|
+
if (active.some(entry => entry.classification === 'dialog')) break;
|
|
2448
|
+
} else if (active.length > 0 && !(queued.action === 'dispatchWheel'
|
|
2449
|
+
&& active.length < 4 && active.every(entry => entry.action === 'dispatchWheel'))) break;
|
|
2450
|
+
slot.operationQueue.splice(index, 1);
|
|
2451
|
+
if (queued.state === 'settled') continue;
|
|
2452
|
+
queued.state = 'starting';
|
|
2453
|
+
slot.activeQueuedOperations.add(queued);
|
|
2454
|
+
let dispatched!: () => void;
|
|
2455
|
+
const admission = new Promise<void>(resolve => { dispatched = resolve; });
|
|
2456
|
+
void this.executeQueuedOperation(queued, dispatched).then(result => {
|
|
2457
|
+
if (queued.state !== 'settled') { queued.state = 'settled'; queued.resolve(result); }
|
|
2458
|
+
}, error => {
|
|
2459
|
+
if (queued.state !== 'settled') { queued.state = 'settled'; queued.reject(error); }
|
|
2460
|
+
}).finally(() => {
|
|
2461
|
+
dispatched();
|
|
2462
|
+
if (queued.onQueuedAbort) queued.signal.removeEventListener('abort', queued.onQueuedAbort);
|
|
2463
|
+
slot.activeQueuedOperations.delete(queued);
|
|
2464
|
+
this.drainOperationQueue(slot);
|
|
2465
|
+
});
|
|
2466
|
+
// Preserve authorization and native-send order without waiting for wheel ACKs.
|
|
2467
|
+
await admission;
|
|
2415
2468
|
}
|
|
2416
|
-
}
|
|
2417
|
-
})()
|
|
2418
|
-
slot.operationSchedulerRunning = false;
|
|
2419
|
-
if (slot.operationQueue.length > 0) this.drainOperationQueue(slot);
|
|
2420
|
-
});
|
|
2421
|
-
void scheduler.catch(() => undefined);
|
|
2469
|
+
} finally { slot.operationSchedulerRunning = false; }
|
|
2470
|
+
})();
|
|
2422
2471
|
}
|
|
2423
2472
|
|
|
2424
|
-
private async executeQueuedOperation(queued: IQueuedOperationRecord): Promise<unknown> {
|
|
2473
|
+
private async executeQueuedOperation(queued: IQueuedOperationRecord, dispatched: () => void): Promise<unknown> {
|
|
2425
2474
|
const { lease, action, classification } = queued;
|
|
2426
2475
|
const slot = lease.slot;
|
|
2427
2476
|
const release = await slot.mutex.acquire();
|
|
@@ -2441,7 +2490,12 @@ export class BrowserRuntime {
|
|
|
2441
2490
|
queued.signal.throwIfAborted();
|
|
2442
2491
|
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2443
2492
|
this.assertSlotAvailable(slot);
|
|
2444
|
-
|
|
2493
|
+
const active = [...slot.operations];
|
|
2494
|
+
if (classification === 'dialog' ? active.some(operation => operation.action === 'respondToDialog')
|
|
2495
|
+
: active.length > 0 && !(action === 'dispatchWheel' && active.length < 4
|
|
2496
|
+
&& active.every(operation => operation.action === 'dispatchWheel'))) {
|
|
2497
|
+
throw new BrowserRuntimeError('BUSY');
|
|
2498
|
+
}
|
|
2445
2499
|
session = slot.session!;
|
|
2446
2500
|
generation = slot.arbitrationGeneration;
|
|
2447
2501
|
incarnationGeneration = slot.incarnationGeneration;
|
|
@@ -2471,7 +2525,7 @@ export class BrowserRuntime {
|
|
|
2471
2525
|
incarnationGeneration,
|
|
2472
2526
|
promise: new Promise<void>((resolve) => { resolvePreflight = resolve; }),
|
|
2473
2527
|
};
|
|
2474
|
-
slot.operation
|
|
2528
|
+
slot.operations.add(operation);
|
|
2475
2529
|
queued.state = 'active';
|
|
2476
2530
|
if (queued.onQueuedAbort) {
|
|
2477
2531
|
queued.signal.removeEventListener('abort', queued.onQueuedAbort);
|
|
@@ -2507,7 +2561,7 @@ export class BrowserRuntime {
|
|
|
2507
2561
|
this.requireQueuedLease(lease, queued.participantCleanup);
|
|
2508
2562
|
this.assertSlotAvailable(slot);
|
|
2509
2563
|
if (
|
|
2510
|
-
slot.operation
|
|
2564
|
+
!slot.operations.has(operation)
|
|
2511
2565
|
|| slot.session !== session
|
|
2512
2566
|
|| slot.arbitrationGeneration !== generation
|
|
2513
2567
|
|| slot.incarnationGeneration !== incarnationGeneration
|
|
@@ -2517,8 +2571,11 @@ export class BrowserRuntime {
|
|
|
2517
2571
|
if (classification === 'navigation' || classification === 'tab') {
|
|
2518
2572
|
await this.releaseAllParticipantInput(slot, session);
|
|
2519
2573
|
}
|
|
2574
|
+
if (classification !== 'dialog') await this.releaseStaleParticipantInput(slot, session);
|
|
2520
2575
|
combinedSignal.throwIfAborted();
|
|
2521
|
-
|
|
2576
|
+
const result = queued.execute(combinedSignal, session);
|
|
2577
|
+
dispatched();
|
|
2578
|
+
return result;
|
|
2522
2579
|
})();
|
|
2523
2580
|
operation.promise = executionPromise.then(() => undefined, () => undefined).finally(() => {
|
|
2524
2581
|
executionSettled = true;
|
|
@@ -2548,10 +2605,11 @@ export class BrowserRuntime {
|
|
|
2548
2605
|
clearTimeout(timeout!);
|
|
2549
2606
|
queued.externalSignal?.removeEventListener('abort', onExternalAbort!);
|
|
2550
2607
|
resolvePreflight();
|
|
2608
|
+
dispatched();
|
|
2551
2609
|
const cleanupSlot = async (): Promise<void> => {
|
|
2552
2610
|
const cleanupRelease = await slot.mutex.acquire();
|
|
2553
2611
|
try {
|
|
2554
|
-
|
|
2612
|
+
slot.operations.delete(operation);
|
|
2555
2613
|
} finally {
|
|
2556
2614
|
cleanupRelease();
|
|
2557
2615
|
}
|
|
@@ -2560,7 +2618,7 @@ export class BrowserRuntime {
|
|
|
2560
2618
|
if (combinedSignal!.aborted) {
|
|
2561
2619
|
await this.enforceOperationQuiescence(operation).catch(() => undefined);
|
|
2562
2620
|
}
|
|
2563
|
-
if (slot.operation
|
|
2621
|
+
if (slot.operations.has(operation)) {
|
|
2564
2622
|
const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
2565
2623
|
if (settled.settled) await cleanupSlot();
|
|
2566
2624
|
}
|
|
@@ -2570,7 +2628,7 @@ export class BrowserRuntime {
|
|
|
2570
2628
|
if (
|
|
2571
2629
|
queued.finalize
|
|
2572
2630
|
&& (!executionStarted || executionSettled)
|
|
2573
|
-
&& slot.operation
|
|
2631
|
+
&& !slot.operations.has(operation)
|
|
2574
2632
|
) {
|
|
2575
2633
|
try {
|
|
2576
2634
|
await queued.finalize();
|
|
@@ -2618,6 +2676,8 @@ export class BrowserRuntime {
|
|
|
2618
2676
|
);
|
|
2619
2677
|
}
|
|
2620
2678
|
if (errorArg instanceof BrowserRuntimeError) return errorArg;
|
|
2679
|
+
if (errorArg instanceof plugins.smartpuppeteer.LiveBrowserInputStaleError) return new BrowserRuntimeError('STALE_INPUT');
|
|
2680
|
+
if (errorArg instanceof plugins.smartpuppeteer.LiveBrowserDialogPendingError) return new BrowserRuntimeError('DIALOG_PENDING');
|
|
2621
2681
|
if (errorArg instanceof Error && errorArg.name === 'TimeoutError') {
|
|
2622
2682
|
return new BrowserRuntimeError('TIMEOUT');
|
|
2623
2683
|
}
|
|
@@ -2890,6 +2950,7 @@ export class BrowserRuntime {
|
|
|
2890
2950
|
generation: tab.generation,
|
|
2891
2951
|
appliedViewportRevision: tab.appliedViewportRevision,
|
|
2892
2952
|
streaming: tab.streaming,
|
|
2953
|
+
...(tab.dialog ? { dialog: { ...tab.dialog } } : {}),
|
|
2893
2954
|
})),
|
|
2894
2955
|
...(state.lastError
|
|
2895
2956
|
? {
|
|
@@ -3257,7 +3318,7 @@ export class BrowserRuntime {
|
|
|
3257
3318
|
lease.released = true;
|
|
3258
3319
|
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3259
3320
|
}
|
|
3260
|
-
slot.operation
|
|
3321
|
+
for (const operation of slot.operations) operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3261
3322
|
for (const subscription of subscribers) {
|
|
3262
3323
|
try { subscription.listener(runtimeEvent); } catch { /* Producer failure owns cleanup. */ }
|
|
3263
3324
|
}
|
|
@@ -3449,7 +3510,7 @@ export class BrowserRuntime {
|
|
|
3449
3510
|
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3450
3511
|
this.notifyFramedLeaseEnded(lease);
|
|
3451
3512
|
}
|
|
3452
|
-
slot.operation
|
|
3513
|
+
for (const operation of slot.operations) operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3453
3514
|
} finally {
|
|
3454
3515
|
release();
|
|
3455
3516
|
}
|
|
@@ -3465,7 +3526,7 @@ export class BrowserRuntime {
|
|
|
3465
3526
|
slot.leases.delete(lease.leaseId);
|
|
3466
3527
|
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
3467
3528
|
}
|
|
3468
|
-
slot.
|
|
3529
|
+
slot.operations.clear();
|
|
3469
3530
|
slot.fencingGeneration = undefined;
|
|
3470
3531
|
}
|
|
3471
3532
|
} finally {
|
|
@@ -3630,18 +3691,18 @@ export class BrowserRuntime {
|
|
|
3630
3691
|
const slot = lease.slot;
|
|
3631
3692
|
if (slot.leases.get(lease.leaseId) !== lease) return;
|
|
3632
3693
|
const release = await slot.mutex.acquire();
|
|
3633
|
-
let
|
|
3694
|
+
let operations: IOperationRecord[];
|
|
3634
3695
|
try {
|
|
3635
3696
|
lease.released = true;
|
|
3636
3697
|
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3637
|
-
|
|
3638
|
-
operation
|
|
3698
|
+
operations = [...slot.operations].filter(operation => operation.lease === lease);
|
|
3699
|
+
for (const operation of operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3639
3700
|
} finally {
|
|
3640
3701
|
release();
|
|
3641
3702
|
}
|
|
3642
3703
|
await this.closeFrameSubscriptionIfLease(lease);
|
|
3643
3704
|
if (lease.devTools) await this.closeLeaseDevTools(lease, lease.devTools);
|
|
3644
|
-
|
|
3705
|
+
for (const operation of operations) {
|
|
3645
3706
|
const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
3646
3707
|
if (!settled.settled) {
|
|
3647
3708
|
// An unquiescent operation belongs to the browser process, so its incarnation must end.
|
|
@@ -3689,13 +3750,12 @@ export class BrowserRuntime {
|
|
|
3689
3750
|
}
|
|
3690
3751
|
|
|
3691
3752
|
private async quiesceSlot(slot: IResourceSlot): Promise<void> {
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
|
|
3753
|
+
const operations = [...slot.operations];
|
|
3754
|
+
for (const operation of operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3755
|
+
const settled = await waitBounded(Promise.all(operations.map(operation => operation.promise)), this.options.quiescenceTimeoutMs);
|
|
3696
3756
|
if (settled.settled) return;
|
|
3697
3757
|
await this.terminateSlotSession(slot);
|
|
3698
|
-
|
|
3758
|
+
for (const operation of operations) slot.operations.delete(operation);
|
|
3699
3759
|
}
|
|
3700
3760
|
|
|
3701
3761
|
private async enforceOperationQuiescence(operation: IOperationRecord): Promise<void> {
|
|
@@ -3711,7 +3771,7 @@ export class BrowserRuntime {
|
|
|
3711
3771
|
} catch (error) {
|
|
3712
3772
|
cleanupError = error;
|
|
3713
3773
|
}
|
|
3714
|
-
if (operation.lease.slot.operation
|
|
3774
|
+
if (!operation.lease.slot.operations.has(operation)) {
|
|
3715
3775
|
if (cleanupError) throw cleanupError;
|
|
3716
3776
|
return;
|
|
3717
3777
|
}
|
|
@@ -3721,7 +3781,7 @@ export class BrowserRuntime {
|
|
|
3721
3781
|
return;
|
|
3722
3782
|
}
|
|
3723
3783
|
const slot = operation.lease.slot;
|
|
3724
|
-
if (slot.operation
|
|
3784
|
+
if (slot.operations.has(operation) && slot.session === operation.session
|
|
3725
3785
|
&& slot.incarnationGeneration === operation.incarnationGeneration) {
|
|
3726
3786
|
await this.terminateRegisteredResourceInternal(slot, false, {
|
|
3727
3787
|
session: operation.session, incarnationGeneration: operation.incarnationGeneration,
|
|
@@ -3730,13 +3790,13 @@ export class BrowserRuntime {
|
|
|
3730
3790
|
const release = await slot.mutex.acquire();
|
|
3731
3791
|
try {
|
|
3732
3792
|
if (
|
|
3733
|
-
slot.operation
|
|
3793
|
+
slot.operations.has(operation)
|
|
3734
3794
|
&& (
|
|
3735
3795
|
slot.permanentlyFenced
|
|
3736
3796
|
|| slot.session !== operation.session
|
|
3737
3797
|
|| slot.incarnationGeneration !== operation.incarnationGeneration
|
|
3738
3798
|
)
|
|
3739
|
-
) slot.operation
|
|
3799
|
+
) slot.operations.delete(operation);
|
|
3740
3800
|
} finally {
|
|
3741
3801
|
release();
|
|
3742
3802
|
}
|
|
@@ -3778,6 +3838,7 @@ export class BrowserRuntime {
|
|
|
3778
3838
|
}
|
|
3779
3839
|
slot.unsubscribeSession?.();
|
|
3780
3840
|
slot.unsubscribeSession = undefined;
|
|
3841
|
+
if (slot.inputCleanup?.session === session) slot.inputCleanup = undefined;
|
|
3781
3842
|
slot.session = undefined;
|
|
3782
3843
|
slot.highestFrameSequence = 0;
|
|
3783
3844
|
slot.producerAcknowledgements.clear();
|
|
@@ -3862,7 +3923,7 @@ export class BrowserRuntime {
|
|
|
3862
3923
|
lease.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3863
3924
|
if (!stopping) this.notifyFramedLeaseEnded(lease);
|
|
3864
3925
|
}
|
|
3865
|
-
slot.operation
|
|
3926
|
+
for (const operation of slot.operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
3866
3927
|
} finally {
|
|
3867
3928
|
release();
|
|
3868
3929
|
}
|
|
@@ -3878,7 +3939,7 @@ export class BrowserRuntime {
|
|
|
3878
3939
|
if (lease.capability.lease === lease) lease.capability.lease = undefined;
|
|
3879
3940
|
slot.leases.delete(lease.leaseId);
|
|
3880
3941
|
}
|
|
3881
|
-
slot.
|
|
3942
|
+
slot.operations.clear();
|
|
3882
3943
|
slot.fencingGeneration = undefined;
|
|
3883
3944
|
} finally {
|
|
3884
3945
|
finalRelease();
|
|
@@ -3979,8 +4040,8 @@ export class BrowserRuntime {
|
|
|
3979
4040
|
lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3980
4041
|
}
|
|
3981
4042
|
}
|
|
3982
|
-
|
|
3983
|
-
|
|
4043
|
+
for (const operation of slot.operations) {
|
|
4044
|
+
if (operation.lease.role === 'agent') operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
|
|
3984
4045
|
}
|
|
3985
4046
|
} finally {
|
|
3986
4047
|
release();
|
|
@@ -4506,6 +4567,11 @@ export class BrowserRuntimeLease {
|
|
|
4506
4567
|
return this.runtime.leaseSetViewport(this.record, viewport, operationOptions);
|
|
4507
4568
|
}
|
|
4508
4569
|
|
|
4570
|
+
public respondToDialog(input: plugins.smartpuppeteer.ILiveBrowserDialogResponse,
|
|
4571
|
+
options: IBrowserRuntimeOperationOptions = {}): Promise<void> {
|
|
4572
|
+
return this.runtime.leaseRespondToDialog(this.record, input, options);
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4509
4575
|
public dispatchMouse(
|
|
4510
4576
|
input: plugins.smartpuppeteer.ILiveBrowserMouseInput,
|
|
4511
4577
|
operationOptions?: IBrowserRuntimeOperationOptions,
|
package/ts/errors.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type TBrowserRuntimeErrorCode =
|
|
|
11
11
|
| 'FENCED'
|
|
12
12
|
| 'FRAME_STREAM_FAILED'
|
|
13
13
|
| 'FRAME_TOO_LARGE'
|
|
14
|
+
| 'DIALOG_PENDING'
|
|
15
|
+
| 'STALE_INPUT'
|
|
14
16
|
| 'INVALID_INPUT'
|
|
15
17
|
| 'LOCKED'
|
|
16
18
|
| 'NOT_RUNNING'
|
|
@@ -33,6 +35,8 @@ const publicMessages: Record<TBrowserRuntimeErrorCode, string> = {
|
|
|
33
35
|
FENCED: 'The browser resource is permanently fenced.',
|
|
34
36
|
FRAME_STREAM_FAILED: 'The browser frame stream failed.',
|
|
35
37
|
FRAME_TOO_LARGE: 'The browser frame exceeded its limit.',
|
|
38
|
+
DIALOG_PENDING: 'Respond to the website dialog before sending input.',
|
|
39
|
+
STALE_INPUT: 'The input belongs to a previous document or dialog.',
|
|
36
40
|
INVALID_INPUT: 'The request is invalid.',
|
|
37
41
|
LOCKED: 'The browser runtime is already locked.',
|
|
38
42
|
NOT_RUNNING: 'The browser runtime is not running.',
|
package/ts/interfaces.ts
CHANGED
|
@@ -131,6 +131,7 @@ export type TBrowserRuntimeOperationIdentity = TReadonlyBrowserCapabilityAuthori
|
|
|
131
131
|
};
|
|
132
132
|
|
|
133
133
|
export type TBrowserRuntimeOperationClassification =
|
|
134
|
+
| 'dialog'
|
|
134
135
|
| 'raw-input'
|
|
135
136
|
| 'frame-stream'
|
|
136
137
|
| 'video-peer'
|
|
@@ -161,6 +162,8 @@ export type TBrowserRuntimeAuditEvent = TBrowserRuntimeOperationIdentity & {
|
|
|
161
162
|
};
|
|
162
163
|
|
|
163
164
|
export interface ILiveBrowserSessionLike {
|
|
165
|
+
respondToDialog(input: plugins.smartpuppeteer.ILiveBrowserDialogResponse,
|
|
166
|
+
options?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<void>;
|
|
164
167
|
openDevTools?(options: plugins.smartpuppeteer.ILiveBrowserDevToolsOptions,
|
|
165
168
|
operationOptions?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<plugins.smartpuppeteer.ILiveBrowserDevToolsConnection>;
|
|
166
169
|
start(options?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<void>;
|
|
@@ -310,6 +313,7 @@ export interface IBrowserRuntimeOptions {
|
|
|
310
313
|
}
|
|
311
314
|
|
|
312
315
|
export interface IBrowserRuntimeTabState {
|
|
316
|
+
dialog?: plugins.smartpuppeteer.ILiveBrowserDialog;
|
|
313
317
|
id: string;
|
|
314
318
|
url: string;
|
|
315
319
|
title: string;
|