codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.4
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 +42 -0
- package/README.md +43 -0
- package/contracts/v1/fixtures/backend-capabilities.json +40 -1
- package/contracts/v1/fixtures/backend-compatibility.json +21 -0
- package/contracts/v1/fixtures/backend-version.json +6 -1
- package/contracts/v1/fixtures/download-blocked-by-browser.json +22 -0
- package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
- package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
- package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
- package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
- package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
- package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
- package/contracts/v1/fixtures/operation-action.json +13 -0
- package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
- package/contracts/v1/fixtures/operation-blocker.json +10 -0
- package/contracts/v1/fixtures/operation-collect-request.json +17 -0
- package/contracts/v1/fixtures/operation-collect-result.json +42 -0
- package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
- package/contracts/v1/fixtures/operation-control-request.json +18 -0
- package/contracts/v1/fixtures/operation-control-result.json +31 -0
- package/contracts/v1/fixtures/operation-event.json +18 -0
- package/contracts/v1/fixtures/operation-handle.json +10 -0
- package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
- package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
- package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
- package/contracts/v1/fixtures/operation-receipt.json +32 -0
- package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
- package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
- package/contracts/v1/fixtures/operation-request.json +33 -0
- package/contracts/v1/fixtures/operation-state.json +144 -0
- package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
- package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
- package/contracts/v1/fixtures/operation-submit-result.json +16 -0
- package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
- package/contracts/v1/manifest.json +185 -1
- package/contracts/v1/parity-suite.json +106 -8
- package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
- package/contracts/v1/schemas/backend-request.schema.json +8 -2
- package/contracts/v1/schemas/capabilities.schema.json +81 -1
- package/contracts/v1/schemas/manifest.schema.json +57 -0
- package/contracts/v1/schemas/operation-action.schema.json +155 -0
- package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
- package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
- package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
- package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
- package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
- package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
- package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
- package/contracts/v1/schemas/operation-event.schema.json +756 -0
- package/contracts/v1/schemas/operation-handle.schema.json +55 -0
- package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
- package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
- package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
- package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
- package/contracts/v1/schemas/operation-request.schema.json +158 -0
- package/contracts/v1/schemas/operation-state.schema.json +691 -0
- package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
- package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
- package/contracts/v1/surface-drift-policy.json +50 -0
- package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
- package/dist/codex-chatgpt-control-backend.mjs +45882 -9361
- package/dist/codex-chatgpt-control-journal.mjs +5547 -0
- package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
- package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
- package/dist/codex-chatgpt-control.bundle.mjs +48392 -10260
- package/dist/src/backend/client.d.ts +103 -2
- package/dist/src/backend/client.js +1436 -97
- package/dist/src/backend/compatibility.d.ts +12 -0
- package/dist/src/backend/compatibility.js +208 -0
- package/dist/src/backend/protocol.d.ts +84 -1
- package/dist/src/backend/protocol.js +31 -5
- package/dist/src/backend/runtime-identity.d.ts +10 -0
- package/dist/src/backend/runtime-identity.js +141 -0
- package/dist/src/backend/session.d.ts +10 -2
- package/dist/src/backend/session.js +478 -7
- package/dist/src/backend/stdio-server.d.ts +4 -1
- package/dist/src/backend/stdio-server.js +240 -28
- package/dist/src/browser/active-composer-file-input.d.ts +9 -0
- package/dist/src/browser/active-composer-file-input.js +33 -0
- package/dist/src/browser/attach.d.ts +4 -1
- package/dist/src/browser/attach.js +136 -28
- package/dist/src/browser/chatgpt-url.d.ts +2 -0
- package/dist/src/browser/chatgpt-url.js +9 -0
- package/dist/src/browser/downloads.d.ts +17 -1
- package/dist/src/browser/downloads.js +150 -28
- package/dist/src/browser/page-state.js +6 -0
- package/dist/src/client.d.ts +43 -0
- package/dist/src/client.js +1279 -59
- package/dist/src/commands/artifacts.js +6 -4
- package/dist/src/commands/chat-popover.d.ts +37 -0
- package/dist/src/commands/chat-popover.js +537 -0
- package/dist/src/commands/configuration.d.ts +5 -0
- package/dist/src/commands/configuration.js +134 -21
- package/dist/src/commands/context.js +10 -3
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +27 -4
- package/dist/src/commands/experience.d.ts +5 -0
- package/dist/src/commands/experience.js +104 -21
- package/dist/src/commands/files.js +14 -33
- package/dist/src/commands/modes.js +85 -21
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +669 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +19 -6
- package/dist/src/commands/work.js +31 -0
- package/dist/src/dom/composer-text.d.ts +6 -0
- package/dist/src/dom/composer-text.js +85 -0
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +9 -0
- package/dist/src/operations/artifact-output.d.ts +69 -0
- package/dist/src/operations/artifact-output.js +1748 -0
- package/dist/src/operations/artifact-stream.d.ts +19 -0
- package/dist/src/operations/artifact-stream.js +35 -0
- package/dist/src/operations/artifact-transfer.d.ts +149 -0
- package/dist/src/operations/artifact-transfer.js +1312 -0
- package/dist/src/operations/browser-adapter.d.ts +167 -0
- package/dist/src/operations/browser-adapter.js +2103 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1256 -0
- package/dist/src/operations/browser-target.d.ts +105 -0
- package/dist/src/operations/browser-target.js +531 -0
- package/dist/src/operations/canonical.d.ts +4 -0
- package/dist/src/operations/canonical.js +412 -0
- package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
- package/dist/src/operations/chatgpt-runtime.js +1232 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1145 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -0
- package/dist/src/operations/configuration-routing.d.ts +4 -0
- package/dist/src/operations/configuration-routing.js +14 -0
- package/dist/src/operations/control.d.ts +372 -0
- package/dist/src/operations/control.js +1498 -0
- package/dist/src/operations/file-identity.d.ts +33 -0
- package/dist/src/operations/file-identity.js +153 -0
- package/dist/src/operations/handle.d.ts +19 -0
- package/dist/src/operations/handle.js +727 -0
- package/dist/src/operations/index.d.ts +25 -0
- package/dist/src/operations/index.js +25 -0
- package/dist/src/operations/journal-authority.d.ts +12 -0
- package/dist/src/operations/journal-authority.js +1 -0
- package/dist/src/operations/journal-rpc-client.d.ts +12 -0
- package/dist/src/operations/journal-rpc-client.js +225 -0
- package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
- package/dist/src/operations/journal-rpc-protocol.js +214 -0
- package/dist/src/operations/journal-rpc-server.d.ts +15 -0
- package/dist/src/operations/journal-rpc-server.js +272 -0
- package/dist/src/operations/journal.d.ts +143 -0
- package/dist/src/operations/journal.js +1957 -0
- package/dist/src/operations/production-attachments.d.ts +118 -0
- package/dist/src/operations/production-attachments.js +1035 -0
- package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
- package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
- package/dist/src/operations/production-chatgpt-attachments.d.ts +80 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1544 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +856 -0
- package/dist/src/operations/production-work-steer.d.ts +203 -0
- package/dist/src/operations/production-work-steer.js +1218 -0
- package/dist/src/operations/recovery.d.ts +80 -0
- package/dist/src/operations/recovery.js +174 -0
- package/dist/src/operations/runtime-adapter.d.ts +126 -0
- package/dist/src/operations/runtime-adapter.js +802 -0
- package/dist/src/operations/send-once.d.ts +224 -0
- package/dist/src/operations/send-once.js +1081 -0
- package/dist/src/operations/service.d.ts +286 -0
- package/dist/src/operations/service.js +2831 -0
- package/dist/src/operations/staging.d.ts +136 -0
- package/dist/src/operations/staging.js +630 -0
- package/dist/src/operations/state-machine.d.ts +29 -0
- package/dist/src/operations/state-machine.js +2047 -0
- package/dist/src/operations/submission.d.ts +423 -0
- package/dist/src/operations/submission.js +1676 -0
- package/dist/src/operations/transactional-chat-power.d.ts +17 -0
- package/dist/src/operations/transactional-chat-power.js +212 -0
- package/dist/src/operations/turn-ownership.d.ts +202 -0
- package/dist/src/operations/turn-ownership.js +700 -0
- package/dist/src/operations/types.d.ts +454 -0
- package/dist/src/operations/types.js +20 -0
- package/dist/src/operations/wire-requests.d.ts +21 -0
- package/dist/src/operations/wire-requests.js +396 -0
- package/dist/src/operations/wire-results.d.ts +97 -0
- package/dist/src/operations/wire-results.js +818 -0
- package/dist/src/runner/responses.d.ts +3 -1
- package/dist/src/runner/responses.js +19 -1
- package/dist/src/runner/result.js +227 -55
- package/dist/src/runner/types.d.ts +12 -0
- package/dist/src/runtime/command-routing.d.ts +149 -0
- package/dist/src/runtime/command-routing.js +431 -0
- package/dist/src/runtime/coordinated-browser.d.ts +24 -0
- package/dist/src/runtime/coordinated-browser.js +315 -0
- package/dist/src/runtime/coordinated-page.d.ts +41 -0
- package/dist/src/runtime/coordinated-page.js +651 -0
- package/dist/src/runtime/operation-context.d.ts +142 -0
- package/dist/src/runtime/operation-context.js +410 -0
- package/dist/src/runtime/runtime-session.d.ts +95 -0
- package/dist/src/runtime/runtime-session.js +314 -0
- package/dist/src/runtime/tab-coordinator.d.ts +200 -0
- package/dist/src/runtime/tab-coordinator.js +1200 -0
- package/dist/src/runtime/value-boundaries.d.ts +18 -0
- package/dist/src/runtime/value-boundaries.js +49 -0
- package/dist/src/safety/blockers.js +8 -1
- package/dist/src/safety/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -1
- package/dist/src/scripts/capture-surface-profile.js +7 -2
- package/dist/src/scripts/journal-server.d.ts +2 -0
- package/dist/src/scripts/journal-server.js +37 -0
- package/dist/src/scripts/live-smoke/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
- package/dist/src/scripts/live-smoke/scenarios.js +147 -30
- package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
- package/dist/src/scripts/live-smoke/transactional.js +254 -0
- package/dist/src/scripts/live-smoke/types.d.ts +3 -0
- package/dist/src/scripts/release-canary-module.js +17 -4
- package/dist/src/types.d.ts +38 -1
- package/package.json +8 -3
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/2026-09-06-journal-service.md +140 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +176 -0
- package/references/python-parity.md +137 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
const INVALID_ID_VALUES = new Set(["unknown", "undefined", "null", "n/a", "na"]);
|
|
4
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
5
|
+
function validateStableId(label, value) {
|
|
6
|
+
if (typeof value !== "string") {
|
|
7
|
+
throw new InvalidResourceKeyError(`${label} must be a non-empty stable string`);
|
|
8
|
+
}
|
|
9
|
+
const normalized = value.trim();
|
|
10
|
+
if (normalized.length === 0 ||
|
|
11
|
+
normalized.length > 512 ||
|
|
12
|
+
INVALID_ID_VALUES.has(normalized.toLowerCase()) ||
|
|
13
|
+
/[\u0000-\u001f\u007f]/u.test(normalized)) {
|
|
14
|
+
throw new InvalidResourceKeyError(`${label} must be a known stable identifier`);
|
|
15
|
+
}
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
function validateOwner(owner) {
|
|
19
|
+
if (!owner || typeof owner !== "object") {
|
|
20
|
+
throw new InvalidCoordinatorRequestError("owner metadata is required");
|
|
21
|
+
}
|
|
22
|
+
const backendSessionId = validateStableId("owner.backendSessionId", owner.backendSessionId);
|
|
23
|
+
const ownerId = owner.ownerId === undefined ? undefined : validateStableId("owner.ownerId", owner.ownerId);
|
|
24
|
+
const operationId = owner.operationId === undefined ? undefined : validateStableId("owner.operationId", owner.operationId);
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
backendSessionId,
|
|
27
|
+
...(ownerId === undefined ? {} : { ownerId }),
|
|
28
|
+
...(operationId === undefined ? {} : { operationId })
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function encodeKeyPart(value) {
|
|
32
|
+
return encodeURIComponent(value);
|
|
33
|
+
}
|
|
34
|
+
export function createBrowserResourceKey(providerOrIdentity, browserId) {
|
|
35
|
+
if (providerOrIdentity === null ||
|
|
36
|
+
typeof providerOrIdentity !== "string" && typeof providerOrIdentity !== "object") {
|
|
37
|
+
throw new InvalidResourceKeyError("browser identity must contain stable providerId and browserId");
|
|
38
|
+
}
|
|
39
|
+
const providerId = typeof providerOrIdentity === "string" ? providerOrIdentity : providerOrIdentity.providerId;
|
|
40
|
+
const resolvedBrowserId = typeof providerOrIdentity === "string" ? browserId : providerOrIdentity.browserId;
|
|
41
|
+
const provider = validateStableId("providerId", providerId);
|
|
42
|
+
const browser = validateStableId("browserId", resolvedBrowserId);
|
|
43
|
+
return `browser:${encodeKeyPart(provider)}:${encodeKeyPart(browser)}`;
|
|
44
|
+
}
|
|
45
|
+
export function createTabResourceKey(providerOrIdentity, browserId, tabId) {
|
|
46
|
+
if (providerOrIdentity === null ||
|
|
47
|
+
typeof providerOrIdentity !== "string" && typeof providerOrIdentity !== "object") {
|
|
48
|
+
throw new InvalidResourceKeyError("tab identity must contain stable providerId, browserId, and tabId");
|
|
49
|
+
}
|
|
50
|
+
const providerId = typeof providerOrIdentity === "string" ? providerOrIdentity : providerOrIdentity.providerId;
|
|
51
|
+
const resolvedBrowserId = typeof providerOrIdentity === "string" ? browserId : providerOrIdentity.browserId;
|
|
52
|
+
const resolvedTabId = typeof providerOrIdentity === "string" ? tabId : providerOrIdentity.tabId;
|
|
53
|
+
const provider = validateStableId("providerId", providerId);
|
|
54
|
+
const browser = validateStableId("browserId", resolvedBrowserId);
|
|
55
|
+
const tab = validateStableId("tabId", resolvedTabId);
|
|
56
|
+
return `tab:${encodeKeyPart(provider)}:${encodeKeyPart(browser)}:${encodeKeyPart(tab)}`;
|
|
57
|
+
}
|
|
58
|
+
export class CoordinatorError extends Error {
|
|
59
|
+
code;
|
|
60
|
+
diagnostics;
|
|
61
|
+
constructor(code, message, diagnostics) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "CoordinatorError";
|
|
64
|
+
this.code = code;
|
|
65
|
+
// Errors can be returned while an in-flight callback remains quarantined.
|
|
66
|
+
// Never expose the actor's live timing object to a caller that could
|
|
67
|
+
// mutate scheduler state before that callback settles.
|
|
68
|
+
if (diagnostics !== undefined)
|
|
69
|
+
this.diagnostics = freezeCoordinatorDiagnostics(diagnostics);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class InvalidResourceKeyError extends CoordinatorError {
|
|
73
|
+
constructor(message) {
|
|
74
|
+
super("invalid_resource_key", message);
|
|
75
|
+
this.name = "InvalidResourceKeyError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export class InvalidCoordinatorRequestError extends CoordinatorError {
|
|
79
|
+
constructor(message) {
|
|
80
|
+
super("invalid_request", message);
|
|
81
|
+
this.name = "InvalidCoordinatorRequestError";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export class CoordinatorQueueFullError extends CoordinatorError {
|
|
85
|
+
constructor(diagnostics) {
|
|
86
|
+
super("queue_full", `The ${diagnostics.resourceKind} coordinator queue is full (${diagnostics.queueDepth} pending requests)`, diagnostics);
|
|
87
|
+
this.name = "CoordinatorQueueFullError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export class CoordinatorAbortedError extends CoordinatorError {
|
|
91
|
+
phase;
|
|
92
|
+
constructor(phase, diagnostics) {
|
|
93
|
+
super("aborted", `Coordinator request was aborted while ${phase}`, diagnostics);
|
|
94
|
+
this.name = "CoordinatorAbortedError";
|
|
95
|
+
this.phase = phase;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export class CoordinatorDeadlineExceededError extends CoordinatorError {
|
|
99
|
+
phase;
|
|
100
|
+
constructor(phase, diagnostics) {
|
|
101
|
+
super("deadline_exceeded", `Coordinator deadline exceeded while ${phase}`, diagnostics);
|
|
102
|
+
this.name = "CoordinatorDeadlineExceededError";
|
|
103
|
+
this.phase = phase;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export class ReentrantAcquisitionError extends CoordinatorError {
|
|
107
|
+
resourceKind;
|
|
108
|
+
resourceKey;
|
|
109
|
+
constructor(context) {
|
|
110
|
+
super("reentrant_acquisition", `Re-entrant ${context.resourceKind} acquisition rejected for ${context.resourceKey}; use a short callback and release the actor before reacquiring`, context.timing);
|
|
111
|
+
this.name = "ReentrantAcquisitionError";
|
|
112
|
+
this.resourceKind = context.resourceKind;
|
|
113
|
+
this.resourceKey = context.resourceKey;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const acquisitionContexts = new AsyncLocalStorage();
|
|
117
|
+
const activeAcquisitionTokens = new Set();
|
|
118
|
+
function cloneTiming(timing) {
|
|
119
|
+
return {
|
|
120
|
+
...timing,
|
|
121
|
+
owner: { ...timing.owner }
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function cloneBrowserGateDiagnostics(diagnostics) {
|
|
125
|
+
return {
|
|
126
|
+
...diagnostics,
|
|
127
|
+
...(diagnostics.activeExclusiveOwner === undefined ? {} : {
|
|
128
|
+
activeExclusiveOwner: { ...diagnostics.activeExclusiveOwner }
|
|
129
|
+
})
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function cloneQueueDiagnostics(diagnostics) {
|
|
133
|
+
return {
|
|
134
|
+
...diagnostics,
|
|
135
|
+
...(diagnostics.activeOwner === undefined ? {} : { activeOwner: { ...diagnostics.activeOwner } }),
|
|
136
|
+
...(diagnostics.lastCompleted === undefined ? {} : { lastCompleted: cloneTiming(diagnostics.lastCompleted) }),
|
|
137
|
+
...(diagnostics.lastRejected === undefined ? {} : { lastRejected: cloneTiming(diagnostics.lastRejected) }),
|
|
138
|
+
...(diagnostics.quarantinedUntilSettled === undefined ? {} : {
|
|
139
|
+
quarantinedUntilSettled: cloneTiming(diagnostics.quarantinedUntilSettled)
|
|
140
|
+
}),
|
|
141
|
+
...(diagnostics.browserGate === undefined ? {} : {
|
|
142
|
+
browserGate: cloneBrowserGateDiagnostics(diagnostics.browserGate)
|
|
143
|
+
})
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function freezeTiming(timing) {
|
|
147
|
+
return Object.freeze({
|
|
148
|
+
...timing,
|
|
149
|
+
owner: Object.freeze({ ...timing.owner })
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function freezeCoordinatorDiagnostics(diagnostics) {
|
|
153
|
+
if ("requestId" in diagnostics)
|
|
154
|
+
return freezeTiming(diagnostics);
|
|
155
|
+
return Object.freeze({
|
|
156
|
+
...diagnostics,
|
|
157
|
+
...(diagnostics.activeOwner === undefined ? {} : {
|
|
158
|
+
activeOwner: Object.freeze({ ...diagnostics.activeOwner })
|
|
159
|
+
}),
|
|
160
|
+
...(diagnostics.lastCompleted === undefined ? {} : {
|
|
161
|
+
lastCompleted: freezeTiming(diagnostics.lastCompleted)
|
|
162
|
+
}),
|
|
163
|
+
...(diagnostics.lastRejected === undefined ? {} : {
|
|
164
|
+
lastRejected: freezeTiming(diagnostics.lastRejected)
|
|
165
|
+
}),
|
|
166
|
+
...(diagnostics.quarantinedUntilSettled === undefined ? {} : {
|
|
167
|
+
quarantinedUntilSettled: freezeTiming(diagnostics.quarantinedUntilSettled)
|
|
168
|
+
}),
|
|
169
|
+
...(diagnostics.browserGate === undefined ? {} : {
|
|
170
|
+
browserGate: Object.freeze({
|
|
171
|
+
...diagnostics.browserGate,
|
|
172
|
+
...(diagnostics.browserGate.activeExclusiveOwner === undefined ? {} : {
|
|
173
|
+
activeExclusiveOwner: Object.freeze({ ...diagnostics.browserGate.activeExclusiveOwner })
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function validateResourceKey(kind, value) {
|
|
180
|
+
if (typeof value !== "string") {
|
|
181
|
+
throw new InvalidResourceKeyError(`${kind} resource key must be created by the coordinator key factory`);
|
|
182
|
+
}
|
|
183
|
+
parseResourceIdentity(kind, value);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Decode only the canonical keys produced by the factories above. In
|
|
187
|
+
* particular, do not derive a browser key by splitting a caller-provided tab
|
|
188
|
+
* key and interpolating strings: the encoded parts must round-trip exactly
|
|
189
|
+
* through the stable-id validators first.
|
|
190
|
+
*/
|
|
191
|
+
function parseResourceIdentity(kind, value) {
|
|
192
|
+
const parts = value.split(":");
|
|
193
|
+
const expectedParts = kind === "browser" ? 3 : 4;
|
|
194
|
+
if (parts.length !== expectedParts || parts[0] !== kind) {
|
|
195
|
+
throw new InvalidResourceKeyError(`${kind} resource key must be created by the coordinator key factory`);
|
|
196
|
+
}
|
|
197
|
+
const decode = (encoded, label) => {
|
|
198
|
+
if (encoded.length === 0) {
|
|
199
|
+
throw new InvalidResourceKeyError(`${kind} resource key must be created by the coordinator key factory`);
|
|
200
|
+
}
|
|
201
|
+
let decoded;
|
|
202
|
+
try {
|
|
203
|
+
decoded = decodeURIComponent(encoded);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
throw new InvalidResourceKeyError(`${kind} resource key must be created by the coordinator key factory`);
|
|
207
|
+
}
|
|
208
|
+
// Reject alternate encodings (for example %62 for b) so this parser is a
|
|
209
|
+
// canonical identity boundary rather than permissive string guesswork.
|
|
210
|
+
if (encodeKeyPart(decoded) !== encoded) {
|
|
211
|
+
throw new InvalidResourceKeyError(`${kind} resource key must be created by the coordinator key factory`);
|
|
212
|
+
}
|
|
213
|
+
return validateStableId(label, decoded);
|
|
214
|
+
};
|
|
215
|
+
const providerId = decode(parts[1], "providerId");
|
|
216
|
+
const browserId = decode(parts[2], "browserId");
|
|
217
|
+
if (kind === "browser")
|
|
218
|
+
return { providerId, browserId };
|
|
219
|
+
return { providerId, browserId, tabId: decode(parts[3], "tabId") };
|
|
220
|
+
}
|
|
221
|
+
function browserKeyForResource(kind, resourceKey) {
|
|
222
|
+
const identity = parseResourceIdentity(kind, resourceKey);
|
|
223
|
+
return createBrowserResourceKey(identity.providerId, identity.browserId);
|
|
224
|
+
}
|
|
225
|
+
function validatePriority(priority) {
|
|
226
|
+
if (priority === undefined)
|
|
227
|
+
return "read";
|
|
228
|
+
if (priority !== "read" && priority !== "mutation" && priority !== "control") {
|
|
229
|
+
throw new InvalidCoordinatorRequestError(`Unsupported coordinator priority: ${String(priority)}`);
|
|
230
|
+
}
|
|
231
|
+
return priority;
|
|
232
|
+
}
|
|
233
|
+
function validatePositiveInteger(label, value) {
|
|
234
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
235
|
+
throw new InvalidCoordinatorRequestError(`${label} must be a positive integer`);
|
|
236
|
+
}
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* One gate per provider/browser. Browser acquisitions are exclusive. Tab
|
|
241
|
+
* transactions are shared leases, but only while no exclusive browser waiter
|
|
242
|
+
* exists. This gives the browser actor a bounded writer turn even when tab
|
|
243
|
+
* actors for many different tabs are active concurrently.
|
|
244
|
+
*/
|
|
245
|
+
class BrowserGate {
|
|
246
|
+
resourceKey;
|
|
247
|
+
maxQueueSize;
|
|
248
|
+
maxConsecutiveExclusives;
|
|
249
|
+
/** Reserved but not yet acquired waiters; Set gives O(1) removal. */
|
|
250
|
+
queued = new Set();
|
|
251
|
+
/** Started shared waiters are the only shared entries a drain may grant. */
|
|
252
|
+
startedShared = new Set();
|
|
253
|
+
/** Min-heap by reservation sequence; stale cancelled entries are removed lazily. */
|
|
254
|
+
startedExclusiveHeap = [];
|
|
255
|
+
activeShared = new Set();
|
|
256
|
+
activeExclusive;
|
|
257
|
+
queuedExclusiveCount = 0;
|
|
258
|
+
queuedSharedCount = 0;
|
|
259
|
+
sequence = 0;
|
|
260
|
+
drainScheduled = false;
|
|
261
|
+
rejectedCount = 0;
|
|
262
|
+
consecutiveExclusive = 0;
|
|
263
|
+
/**
|
|
264
|
+
* Shared admissions are reserved when their tab actor accepts a request,
|
|
265
|
+
* not only when that actor reaches its execution turn. Without this
|
|
266
|
+
* counter a gate could look idle while another tab still had queued work
|
|
267
|
+
* holding a closure over the old gate, allowing a replacement gate to be
|
|
268
|
+
* created for the same browser.
|
|
269
|
+
*/
|
|
270
|
+
acceptedSharedReservations = 0;
|
|
271
|
+
pendingSharedReservations = 0;
|
|
272
|
+
constructor(resourceKey, maxQueueSize, maxConsecutiveExclusives) {
|
|
273
|
+
this.resourceKey = resourceKey;
|
|
274
|
+
this.maxQueueSize = maxQueueSize;
|
|
275
|
+
this.maxConsecutiveExclusives = maxConsecutiveExclusives;
|
|
276
|
+
}
|
|
277
|
+
createExclusiveAdmission() {
|
|
278
|
+
let waiter;
|
|
279
|
+
return {
|
|
280
|
+
onAccepted: () => {
|
|
281
|
+
if (waiter !== undefined) {
|
|
282
|
+
throw new InvalidCoordinatorRequestError("browser admission was accepted more than once");
|
|
283
|
+
}
|
|
284
|
+
waiter = this.reserve("exclusive");
|
|
285
|
+
},
|
|
286
|
+
onStarted: (context) => {
|
|
287
|
+
if (waiter === undefined) {
|
|
288
|
+
return Promise.reject(new InvalidCoordinatorRequestError("browser admission was not accepted"));
|
|
289
|
+
}
|
|
290
|
+
return this.start(waiter, context);
|
|
291
|
+
},
|
|
292
|
+
onAbandoned: () => {
|
|
293
|
+
if (waiter !== undefined)
|
|
294
|
+
this.cancel(waiter);
|
|
295
|
+
waiter = undefined;
|
|
296
|
+
},
|
|
297
|
+
onSettled: () => {
|
|
298
|
+
if (waiter !== undefined)
|
|
299
|
+
this.settle(waiter);
|
|
300
|
+
waiter = undefined;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
createSharedAdmission() {
|
|
305
|
+
let waiter;
|
|
306
|
+
let accepted = false;
|
|
307
|
+
let started = false;
|
|
308
|
+
return {
|
|
309
|
+
onAccepted: () => {
|
|
310
|
+
if (accepted) {
|
|
311
|
+
throw new InvalidCoordinatorRequestError("tab admission was accepted more than once");
|
|
312
|
+
}
|
|
313
|
+
this.assertReservationCapacity();
|
|
314
|
+
accepted = true;
|
|
315
|
+
started = false;
|
|
316
|
+
this.acceptedSharedReservations += 1;
|
|
317
|
+
this.pendingSharedReservations += 1;
|
|
318
|
+
},
|
|
319
|
+
onStarted: (context) => {
|
|
320
|
+
if (!accepted || waiter !== undefined) {
|
|
321
|
+
return Promise.reject(new InvalidCoordinatorRequestError("tab admission was accepted more than once"));
|
|
322
|
+
}
|
|
323
|
+
// Convert the already-counted pending reservation into a concrete gate
|
|
324
|
+
// waiter without briefly double-counting it against the browser-wide
|
|
325
|
+
// queue bound. Restore the reservation if allocation fails so the
|
|
326
|
+
// actor's settlement path can release it exactly once.
|
|
327
|
+
this.pendingSharedReservations -= 1;
|
|
328
|
+
try {
|
|
329
|
+
waiter = this.reserve("shared");
|
|
330
|
+
started = true;
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
this.pendingSharedReservations += 1;
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
return this.start(waiter, context);
|
|
337
|
+
},
|
|
338
|
+
onAbandoned: () => {
|
|
339
|
+
if (waiter !== undefined)
|
|
340
|
+
this.cancel(waiter);
|
|
341
|
+
waiter = undefined;
|
|
342
|
+
if (accepted) {
|
|
343
|
+
this.acceptedSharedReservations -= 1;
|
|
344
|
+
if (!started)
|
|
345
|
+
this.pendingSharedReservations -= 1;
|
|
346
|
+
}
|
|
347
|
+
accepted = false;
|
|
348
|
+
started = false;
|
|
349
|
+
},
|
|
350
|
+
onSettled: () => {
|
|
351
|
+
if (waiter !== undefined)
|
|
352
|
+
this.settle(waiter);
|
|
353
|
+
waiter = undefined;
|
|
354
|
+
if (accepted) {
|
|
355
|
+
this.acceptedSharedReservations -= 1;
|
|
356
|
+
if (!started)
|
|
357
|
+
this.pendingSharedReservations -= 1;
|
|
358
|
+
}
|
|
359
|
+
accepted = false;
|
|
360
|
+
started = false;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
isIdle() {
|
|
365
|
+
return this.queued.size === 0 &&
|
|
366
|
+
this.activeExclusive === undefined &&
|
|
367
|
+
this.activeShared.size === 0 &&
|
|
368
|
+
this.acceptedSharedReservations === 0;
|
|
369
|
+
}
|
|
370
|
+
snapshot() {
|
|
371
|
+
return {
|
|
372
|
+
resourceKind: "browser",
|
|
373
|
+
resourceKey: this.resourceKey,
|
|
374
|
+
queueDepth: this.queued.size + this.pendingSharedReservations,
|
|
375
|
+
active: this.activeExclusive !== undefined || this.activeShared.size > 0,
|
|
376
|
+
activeSharedCount: this.activeShared.size,
|
|
377
|
+
queuedExclusiveCount: this.queuedExclusiveCount,
|
|
378
|
+
queuedSharedCount: this.queuedSharedCount + this.pendingSharedReservations,
|
|
379
|
+
rejectedCount: this.rejectedCount,
|
|
380
|
+
...(this.activeExclusive === undefined ? {} : {
|
|
381
|
+
activeExclusiveRequestId: this.activeExclusive.requestId,
|
|
382
|
+
...(this.activeExclusive.context === undefined ? {} : {
|
|
383
|
+
activeExclusiveOwner: { ...this.activeExclusive.context.owner }
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
queueSnapshot() {
|
|
389
|
+
return {
|
|
390
|
+
resourceKind: "browser",
|
|
391
|
+
resourceKey: this.resourceKey,
|
|
392
|
+
queueDepth: this.queued.size + this.pendingSharedReservations,
|
|
393
|
+
active: this.activeExclusive !== undefined || this.activeShared.size > 0,
|
|
394
|
+
...(this.activeExclusive === undefined ? {} : { activeRequestId: this.activeExclusive.requestId }),
|
|
395
|
+
completedCount: 0,
|
|
396
|
+
rejectedCount: this.rejectedCount
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
reserve(kind) {
|
|
400
|
+
this.assertReservationCapacity();
|
|
401
|
+
const waiter = {
|
|
402
|
+
requestId: randomUUID(),
|
|
403
|
+
sequence: ++this.sequence,
|
|
404
|
+
kind,
|
|
405
|
+
started: false,
|
|
406
|
+
acquired: false,
|
|
407
|
+
settled: false
|
|
408
|
+
};
|
|
409
|
+
this.queued.add(waiter);
|
|
410
|
+
if (kind === "exclusive")
|
|
411
|
+
this.queuedExclusiveCount += 1;
|
|
412
|
+
else
|
|
413
|
+
this.queuedSharedCount += 1;
|
|
414
|
+
return waiter;
|
|
415
|
+
}
|
|
416
|
+
assertReservationCapacity() {
|
|
417
|
+
if (this.queued.size + this.pendingSharedReservations < this.maxQueueSize)
|
|
418
|
+
return;
|
|
419
|
+
this.rejectedCount += 1;
|
|
420
|
+
throw new CoordinatorQueueFullError(this.queueSnapshot());
|
|
421
|
+
}
|
|
422
|
+
start(waiter, context) {
|
|
423
|
+
if (waiter.settled) {
|
|
424
|
+
return Promise.reject(new InvalidCoordinatorRequestError("coordinator admission is no longer active"));
|
|
425
|
+
}
|
|
426
|
+
if (waiter.started) {
|
|
427
|
+
return Promise.reject(new InvalidCoordinatorRequestError("coordinator admission was started more than once"));
|
|
428
|
+
}
|
|
429
|
+
waiter.started = true;
|
|
430
|
+
waiter.context = context;
|
|
431
|
+
if (waiter.kind === "exclusive")
|
|
432
|
+
this.pushStartedExclusive(waiter);
|
|
433
|
+
else
|
|
434
|
+
this.startedShared.add(waiter);
|
|
435
|
+
waiter.promise = new Promise((resolve, reject) => {
|
|
436
|
+
waiter.resolve = resolve;
|
|
437
|
+
waiter.reject = reject;
|
|
438
|
+
});
|
|
439
|
+
const onAbort = () => {
|
|
440
|
+
if (waiter.acquired || waiter.settled)
|
|
441
|
+
return;
|
|
442
|
+
const reason = context.signal.reason;
|
|
443
|
+
const error = reason instanceof CoordinatorDeadlineExceededError || reason instanceof CoordinatorAbortedError
|
|
444
|
+
? reason
|
|
445
|
+
: new CoordinatorAbortedError("in_flight", context.timing);
|
|
446
|
+
this.cancel(waiter, error);
|
|
447
|
+
};
|
|
448
|
+
waiter.abortListener = onAbort;
|
|
449
|
+
context.signal.addEventListener("abort", onAbort, { once: true });
|
|
450
|
+
if (context.signal.aborted)
|
|
451
|
+
onAbort();
|
|
452
|
+
this.scheduleDrain();
|
|
453
|
+
return waiter.promise;
|
|
454
|
+
}
|
|
455
|
+
hasExclusiveWaiter() {
|
|
456
|
+
return this.queuedExclusiveCount > 0;
|
|
457
|
+
}
|
|
458
|
+
scheduleDrain() {
|
|
459
|
+
if (this.drainScheduled)
|
|
460
|
+
return;
|
|
461
|
+
this.drainScheduled = true;
|
|
462
|
+
queueMicrotask(() => {
|
|
463
|
+
this.drainScheduled = false;
|
|
464
|
+
this.drain();
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
drain() {
|
|
468
|
+
if (this.activeExclusive !== undefined)
|
|
469
|
+
return;
|
|
470
|
+
// A queued exclusive request freezes new shared leases until every
|
|
471
|
+
// already-active tab lease has released. This is the writer gate that
|
|
472
|
+
// prevents a stream of other tabs from starving browser-level controls.
|
|
473
|
+
if (this.activeShared.size > 0 && this.hasExclusiveWaiter())
|
|
474
|
+
return;
|
|
475
|
+
const exclusive = this.nextStartedExclusive();
|
|
476
|
+
if (this.startedShared.size > 0 &&
|
|
477
|
+
this.consecutiveExclusive >= this.maxConsecutiveExclusives) {
|
|
478
|
+
// A browser-exclusive waiter has writer preference so new tab work
|
|
479
|
+
// cannot continually extend an existing shared turn. Bound that
|
|
480
|
+
// preference as well: after the configured number of exclusive turns,
|
|
481
|
+
// admit the already queued shared batch. New shared reservations remain
|
|
482
|
+
// blocked while the batch is active because hasExclusiveWaiter() is
|
|
483
|
+
// still true.
|
|
484
|
+
for (const waiter of [...this.startedShared])
|
|
485
|
+
this.grant(waiter);
|
|
486
|
+
this.consecutiveExclusive = 0;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (exclusive !== undefined && this.activeShared.size === 0) {
|
|
490
|
+
this.grant(exclusive);
|
|
491
|
+
this.consecutiveExclusive += 1;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// A reserved browser request may not have reached its ResourceActor
|
|
495
|
+
// execution turn yet. It still blocks readers, so that handoff cannot
|
|
496
|
+
// be overtaken by a tab started in the meantime.
|
|
497
|
+
if (this.hasExclusiveWaiter())
|
|
498
|
+
return;
|
|
499
|
+
for (const waiter of [...this.startedShared])
|
|
500
|
+
this.grant(waiter);
|
|
501
|
+
}
|
|
502
|
+
grant(waiter) {
|
|
503
|
+
if (!this.queued.has(waiter) || waiter.settled || !waiter.started)
|
|
504
|
+
return;
|
|
505
|
+
this.removeQueued(waiter);
|
|
506
|
+
waiter.acquired = true;
|
|
507
|
+
if (waiter.kind === "exclusive")
|
|
508
|
+
this.activeExclusive = waiter;
|
|
509
|
+
else
|
|
510
|
+
this.activeShared.add(waiter);
|
|
511
|
+
this.detachAbortListener(waiter);
|
|
512
|
+
waiter.resolve?.(() => this.release(waiter));
|
|
513
|
+
}
|
|
514
|
+
release(waiter) {
|
|
515
|
+
if (waiter.settled)
|
|
516
|
+
return;
|
|
517
|
+
if (!waiter.acquired) {
|
|
518
|
+
this.cancel(waiter);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
waiter.acquired = false;
|
|
522
|
+
waiter.settled = true;
|
|
523
|
+
if (waiter.kind === "exclusive") {
|
|
524
|
+
if (this.activeExclusive === waiter)
|
|
525
|
+
delete this.activeExclusive;
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
this.activeShared.delete(waiter);
|
|
529
|
+
}
|
|
530
|
+
this.detachAbortListener(waiter);
|
|
531
|
+
this.scheduleDrain();
|
|
532
|
+
}
|
|
533
|
+
settle(waiter) {
|
|
534
|
+
if (waiter.settled)
|
|
535
|
+
return;
|
|
536
|
+
if (waiter.acquired) {
|
|
537
|
+
this.release(waiter);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
this.cancel(waiter);
|
|
541
|
+
}
|
|
542
|
+
cancel(waiter, reason) {
|
|
543
|
+
if (waiter.settled || waiter.acquired)
|
|
544
|
+
return;
|
|
545
|
+
this.removeQueued(waiter);
|
|
546
|
+
waiter.settled = true;
|
|
547
|
+
this.detachAbortListener(waiter);
|
|
548
|
+
if (waiter.started) {
|
|
549
|
+
waiter.reject?.(reason ?? new CoordinatorAbortedError("in_flight", waiter.context.timing));
|
|
550
|
+
}
|
|
551
|
+
this.scheduleDrain();
|
|
552
|
+
}
|
|
553
|
+
removeQueued(waiter) {
|
|
554
|
+
if (!this.queued.delete(waiter))
|
|
555
|
+
return;
|
|
556
|
+
if (waiter.kind === "exclusive")
|
|
557
|
+
this.queuedExclusiveCount -= 1;
|
|
558
|
+
else {
|
|
559
|
+
this.queuedSharedCount -= 1;
|
|
560
|
+
this.startedShared.delete(waiter);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
pushStartedExclusive(waiter) {
|
|
564
|
+
const heap = this.startedExclusiveHeap;
|
|
565
|
+
heap.push(waiter);
|
|
566
|
+
let index = heap.length - 1;
|
|
567
|
+
while (index > 0) {
|
|
568
|
+
const parent = Math.floor((index - 1) / 2);
|
|
569
|
+
if (heap[parent].sequence <= waiter.sequence)
|
|
570
|
+
break;
|
|
571
|
+
heap[index] = heap[parent];
|
|
572
|
+
index = parent;
|
|
573
|
+
}
|
|
574
|
+
heap[index] = waiter;
|
|
575
|
+
}
|
|
576
|
+
nextStartedExclusive() {
|
|
577
|
+
const heap = this.startedExclusiveHeap;
|
|
578
|
+
while (heap.length > 0 && !this.queued.has(heap[0]))
|
|
579
|
+
this.popStartedExclusive();
|
|
580
|
+
return heap[0];
|
|
581
|
+
}
|
|
582
|
+
popStartedExclusive() {
|
|
583
|
+
const heap = this.startedExclusiveHeap;
|
|
584
|
+
const first = heap[0];
|
|
585
|
+
const last = heap.pop();
|
|
586
|
+
if (first === undefined || last === undefined || heap.length === 0)
|
|
587
|
+
return first;
|
|
588
|
+
let index = 0;
|
|
589
|
+
while (true) {
|
|
590
|
+
const left = index * 2 + 1;
|
|
591
|
+
const right = left + 1;
|
|
592
|
+
if (left >= heap.length)
|
|
593
|
+
break;
|
|
594
|
+
const child = right < heap.length && heap[right].sequence < heap[left].sequence ? right : left;
|
|
595
|
+
if (heap[child].sequence >= last.sequence)
|
|
596
|
+
break;
|
|
597
|
+
heap[index] = heap[child];
|
|
598
|
+
index = child;
|
|
599
|
+
}
|
|
600
|
+
heap[index] = last;
|
|
601
|
+
return first;
|
|
602
|
+
}
|
|
603
|
+
detachAbortListener(waiter) {
|
|
604
|
+
if (waiter.abortListener !== undefined && waiter.context !== undefined) {
|
|
605
|
+
waiter.context.signal.removeEventListener("abort", waiter.abortListener);
|
|
606
|
+
delete waiter.abortListener;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
class ResourceActor {
|
|
611
|
+
options;
|
|
612
|
+
onIdle;
|
|
613
|
+
queue = [];
|
|
614
|
+
active;
|
|
615
|
+
sequence = 0;
|
|
616
|
+
drainScheduled = false;
|
|
617
|
+
consecutive = { read: 0, mutation: 0, control: 0 };
|
|
618
|
+
completedCount;
|
|
619
|
+
rejectedCount;
|
|
620
|
+
lastCompleted;
|
|
621
|
+
lastRejected;
|
|
622
|
+
quarantinedUntilSettled;
|
|
623
|
+
constructor(options, initialDiagnostics, onIdle) {
|
|
624
|
+
this.options = options;
|
|
625
|
+
this.onIdle = onIdle;
|
|
626
|
+
this.completedCount = initialDiagnostics?.completedCount ?? 0;
|
|
627
|
+
this.rejectedCount = initialDiagnostics?.rejectedCount ?? 0;
|
|
628
|
+
if (initialDiagnostics?.lastCompleted !== undefined) {
|
|
629
|
+
this.lastCompleted = cloneTiming(initialDiagnostics.lastCompleted);
|
|
630
|
+
}
|
|
631
|
+
if (initialDiagnostics?.lastRejected !== undefined) {
|
|
632
|
+
this.lastRejected = cloneTiming(initialDiagnostics.lastRejected);
|
|
633
|
+
}
|
|
634
|
+
if (initialDiagnostics?.quarantinedUntilSettled !== undefined) {
|
|
635
|
+
this.quarantinedUntilSettled = cloneTiming(initialDiagnostics.quarantinedUntilSettled);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
isIdle() {
|
|
639
|
+
return this.active === undefined && this.queue.length === 0 && this.quarantinedUntilSettled === undefined;
|
|
640
|
+
}
|
|
641
|
+
notifyIfIdle() {
|
|
642
|
+
if (this.isIdle())
|
|
643
|
+
this.onIdle?.(this);
|
|
644
|
+
}
|
|
645
|
+
snapshot() {
|
|
646
|
+
return {
|
|
647
|
+
resourceKind: this.options.resourceKind,
|
|
648
|
+
resourceKey: this.options.resourceKey,
|
|
649
|
+
queueDepth: this.queue.length,
|
|
650
|
+
active: this.active !== undefined,
|
|
651
|
+
...(this.active === undefined ? {} : {
|
|
652
|
+
activeRequestId: this.active.requestId,
|
|
653
|
+
activeOwner: this.active.owner
|
|
654
|
+
}),
|
|
655
|
+
completedCount: this.completedCount,
|
|
656
|
+
rejectedCount: this.rejectedCount,
|
|
657
|
+
...(this.lastCompleted === undefined ? {} : { lastCompleted: cloneTiming(this.lastCompleted) }),
|
|
658
|
+
...(this.lastRejected === undefined ? {} : { lastRejected: cloneTiming(this.lastRejected) }),
|
|
659
|
+
...(this.quarantinedUntilSettled === undefined ? {} : {
|
|
660
|
+
quarantinedUntilSettled: cloneTiming(this.quarantinedUntilSettled)
|
|
661
|
+
})
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
enqueue(request, externalSignal) {
|
|
665
|
+
if (this.queue.length >= this.options.maxQueueSize) {
|
|
666
|
+
this.rejectedCount += 1;
|
|
667
|
+
request.timing.outcome = "rejected";
|
|
668
|
+
request.timing.settledAt = this.options.now();
|
|
669
|
+
request.timing.totalMs = request.timing.settledAt - request.timing.enqueuedAt;
|
|
670
|
+
this.lastRejected = request.timing;
|
|
671
|
+
this.notifyIfIdle();
|
|
672
|
+
return Promise.reject(new CoordinatorQueueFullError(this.snapshot()));
|
|
673
|
+
}
|
|
674
|
+
return new Promise((resolve, reject) => {
|
|
675
|
+
const pending = {
|
|
676
|
+
...request,
|
|
677
|
+
...(externalSignal === undefined ? {} : { externalSignal }),
|
|
678
|
+
sequence: ++this.sequence,
|
|
679
|
+
resolve: (value) => resolve(value),
|
|
680
|
+
reject,
|
|
681
|
+
started: false,
|
|
682
|
+
settled: false,
|
|
683
|
+
callerSettled: false,
|
|
684
|
+
cancelled: false
|
|
685
|
+
};
|
|
686
|
+
if (externalSignal !== undefined) {
|
|
687
|
+
const onAbort = () => this.handleAbort(pending, externalSignal.reason);
|
|
688
|
+
pending.abortListener = onAbort;
|
|
689
|
+
externalSignal.addEventListener("abort", onAbort, { once: true });
|
|
690
|
+
if (externalSignal.aborted) {
|
|
691
|
+
this.handleAbort(pending, externalSignal.reason);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (pending.deadlineAt !== undefined && pending.deadlineAt <= this.options.now()) {
|
|
696
|
+
this.handleDeadline(pending);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
pending.admission.onAccepted();
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
this.clearDeadline(pending);
|
|
704
|
+
this.detachAbortListener(pending);
|
|
705
|
+
pending.settled = true;
|
|
706
|
+
pending.callerSettled = true;
|
|
707
|
+
pending.timing.outcome = "rejected";
|
|
708
|
+
pending.timing.settledAt = this.options.now();
|
|
709
|
+
pending.timing.totalMs = pending.timing.settledAt - pending.timing.enqueuedAt;
|
|
710
|
+
pending.admission.onAbandoned();
|
|
711
|
+
this.rejectedCount += 1;
|
|
712
|
+
this.lastRejected = pending.timing;
|
|
713
|
+
pending.reject(error);
|
|
714
|
+
this.notifyIfIdle();
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
this.queue.push(pending);
|
|
718
|
+
this.armDeadline(pending);
|
|
719
|
+
this.scheduleDrain();
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
armDeadline(request) {
|
|
723
|
+
if (request.deadlineAt === undefined)
|
|
724
|
+
return;
|
|
725
|
+
const tick = () => {
|
|
726
|
+
if (request.settled || request.cancelled)
|
|
727
|
+
return;
|
|
728
|
+
const remaining = request.deadlineAt - this.options.now();
|
|
729
|
+
if (remaining <= 0) {
|
|
730
|
+
this.handleDeadline(request);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
request.deadlineTimer = setTimeout(tick, Math.min(remaining, MAX_TIMER_DELAY_MS));
|
|
734
|
+
};
|
|
735
|
+
tick();
|
|
736
|
+
}
|
|
737
|
+
clearDeadline(request) {
|
|
738
|
+
if (request.deadlineTimer !== undefined) {
|
|
739
|
+
clearTimeout(request.deadlineTimer);
|
|
740
|
+
delete request.deadlineTimer;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
detachAbortListener(request) {
|
|
744
|
+
if (request.abortListener !== undefined && request.externalSignal !== undefined) {
|
|
745
|
+
request.externalSignal.removeEventListener("abort", request.abortListener);
|
|
746
|
+
delete request.abortListener;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
handleAbort(request, reason) {
|
|
750
|
+
if (request.settled)
|
|
751
|
+
return;
|
|
752
|
+
if (!request.started) {
|
|
753
|
+
request.cancelled = true;
|
|
754
|
+
request.timing.queuedCancellation = true;
|
|
755
|
+
this.removeQueued(request);
|
|
756
|
+
request.admission.onAbandoned();
|
|
757
|
+
request.settled = true;
|
|
758
|
+
request.callerSettled = true;
|
|
759
|
+
request.timing.outcome = "rejected";
|
|
760
|
+
request.timing.settledAt = this.options.now();
|
|
761
|
+
request.timing.totalMs = request.timing.settledAt - request.timing.enqueuedAt;
|
|
762
|
+
this.rejectedCount += 1;
|
|
763
|
+
this.lastRejected = request.timing;
|
|
764
|
+
request.reject(new CoordinatorAbortedError("queued", request.timing));
|
|
765
|
+
this.scheduleDrain();
|
|
766
|
+
this.notifyIfIdle();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
request.timing.aborted = true;
|
|
770
|
+
request.timing.quarantinedUntilSettled = true;
|
|
771
|
+
this.quarantinedUntilSettled = request.timing;
|
|
772
|
+
request.inFlightError ??= new CoordinatorAbortedError("in_flight", request.timing);
|
|
773
|
+
if (!request.callerSettled) {
|
|
774
|
+
request.callerSettled = true;
|
|
775
|
+
request.reject(request.inFlightError);
|
|
776
|
+
}
|
|
777
|
+
request.controller.abort(reason ?? new Error("The caller aborted the coordinator request"));
|
|
778
|
+
}
|
|
779
|
+
handleDeadline(request) {
|
|
780
|
+
if (request.settled)
|
|
781
|
+
return;
|
|
782
|
+
if (!request.started) {
|
|
783
|
+
request.cancelled = true;
|
|
784
|
+
request.timing.queuedDeadlineExceeded = true;
|
|
785
|
+
this.removeQueued(request);
|
|
786
|
+
request.admission.onAbandoned();
|
|
787
|
+
request.settled = true;
|
|
788
|
+
request.callerSettled = true;
|
|
789
|
+
request.timing.outcome = "rejected";
|
|
790
|
+
request.timing.settledAt = this.options.now();
|
|
791
|
+
request.timing.totalMs = request.timing.settledAt - request.timing.enqueuedAt;
|
|
792
|
+
this.rejectedCount += 1;
|
|
793
|
+
this.lastRejected = request.timing;
|
|
794
|
+
request.reject(new CoordinatorDeadlineExceededError("queued", request.timing));
|
|
795
|
+
this.scheduleDrain();
|
|
796
|
+
this.notifyIfIdle();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
request.timing.deadlineExceededInFlight = true;
|
|
800
|
+
request.timing.quarantinedUntilSettled = true;
|
|
801
|
+
this.quarantinedUntilSettled = request.timing;
|
|
802
|
+
request.inFlightError ??= new CoordinatorDeadlineExceededError("in_flight", request.timing);
|
|
803
|
+
if (!request.callerSettled) {
|
|
804
|
+
request.callerSettled = true;
|
|
805
|
+
request.reject(request.inFlightError);
|
|
806
|
+
}
|
|
807
|
+
request.controller.abort(new CoordinatorDeadlineExceededError("in_flight", request.timing));
|
|
808
|
+
}
|
|
809
|
+
removeQueued(request) {
|
|
810
|
+
const index = this.queue.indexOf(request);
|
|
811
|
+
if (index >= 0)
|
|
812
|
+
this.queue.splice(index, 1);
|
|
813
|
+
this.clearDeadline(request);
|
|
814
|
+
this.detachAbortListener(request);
|
|
815
|
+
}
|
|
816
|
+
scheduleDrain() {
|
|
817
|
+
if (this.drainScheduled || this.active !== undefined)
|
|
818
|
+
return;
|
|
819
|
+
this.drainScheduled = true;
|
|
820
|
+
queueMicrotask(() => {
|
|
821
|
+
this.drainScheduled = false;
|
|
822
|
+
this.drain();
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
drain() {
|
|
826
|
+
if (this.active !== undefined)
|
|
827
|
+
return;
|
|
828
|
+
let request = this.selectNext();
|
|
829
|
+
while (request !== undefined && request.cancelled)
|
|
830
|
+
request = this.selectNext();
|
|
831
|
+
if (request === undefined)
|
|
832
|
+
return;
|
|
833
|
+
request.started = true;
|
|
834
|
+
request.timing.startedAt = this.options.now();
|
|
835
|
+
request.timing.queueDelayMs = request.timing.startedAt - request.timing.enqueuedAt;
|
|
836
|
+
// Keep the deadline timer and external abort listener attached while the
|
|
837
|
+
// callback is in flight. The actor remains active until it settles.
|
|
838
|
+
this.active = request;
|
|
839
|
+
const acquisitionToken = randomUUID();
|
|
840
|
+
const context = Object.freeze({
|
|
841
|
+
resourceKind: this.options.resourceKind,
|
|
842
|
+
resourceKey: this.options.resourceKey,
|
|
843
|
+
acquisitionToken,
|
|
844
|
+
owner: request.owner,
|
|
845
|
+
priority: request.priority,
|
|
846
|
+
signal: request.controller.signal,
|
|
847
|
+
timing: freezeTiming(request.timing)
|
|
848
|
+
});
|
|
849
|
+
request.context = context;
|
|
850
|
+
void this.execute(request, context);
|
|
851
|
+
}
|
|
852
|
+
async execute(request, context) {
|
|
853
|
+
activeAcquisitionTokens.add(context.acquisitionToken);
|
|
854
|
+
let admissionLease;
|
|
855
|
+
try {
|
|
856
|
+
admissionLease = await acquisitionContexts.run(context, () => request.admission.onStarted(context));
|
|
857
|
+
request.timing.admittedAt = this.options.now();
|
|
858
|
+
request.timing.admissionDelayMs = request.timing.admittedAt - (request.timing.startedAt ?? request.timing.admittedAt);
|
|
859
|
+
const value = await acquisitionContexts.run(context, () => request.callback(context));
|
|
860
|
+
if (request.inFlightError !== undefined) {
|
|
861
|
+
throw request.inFlightError;
|
|
862
|
+
}
|
|
863
|
+
if (!request.callerSettled) {
|
|
864
|
+
request.callerSettled = true;
|
|
865
|
+
request.resolve(value);
|
|
866
|
+
}
|
|
867
|
+
request.timing.outcome = "fulfilled";
|
|
868
|
+
}
|
|
869
|
+
catch (error) {
|
|
870
|
+
if (!request.callerSettled) {
|
|
871
|
+
request.callerSettled = true;
|
|
872
|
+
request.reject(request.inFlightError ?? error);
|
|
873
|
+
}
|
|
874
|
+
request.timing.outcome = "rejected";
|
|
875
|
+
}
|
|
876
|
+
finally {
|
|
877
|
+
admissionLease?.();
|
|
878
|
+
request.admission.onSettled();
|
|
879
|
+
activeAcquisitionTokens.delete(context.acquisitionToken);
|
|
880
|
+
request.settled = true;
|
|
881
|
+
this.clearDeadline(request);
|
|
882
|
+
this.detachAbortListener(request);
|
|
883
|
+
request.timing.settledAt = this.options.now();
|
|
884
|
+
request.timing.executionMs = request.timing.settledAt - (request.timing.admittedAt ?? request.timing.startedAt ?? request.timing.settledAt);
|
|
885
|
+
request.timing.totalMs = request.timing.settledAt - request.timing.enqueuedAt;
|
|
886
|
+
if (request.timing.outcome === "fulfilled") {
|
|
887
|
+
this.completedCount += 1;
|
|
888
|
+
this.lastCompleted = request.timing;
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
this.rejectedCount += 1;
|
|
892
|
+
this.lastRejected = request.timing;
|
|
893
|
+
}
|
|
894
|
+
if (this.quarantinedUntilSettled === request.timing)
|
|
895
|
+
delete this.quarantinedUntilSettled;
|
|
896
|
+
delete this.active;
|
|
897
|
+
this.scheduleDrain();
|
|
898
|
+
this.notifyIfIdle();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
selectNext() {
|
|
902
|
+
this.removeExpiredQueued();
|
|
903
|
+
if (this.queue.length === 0)
|
|
904
|
+
return undefined;
|
|
905
|
+
const now = this.options.now();
|
|
906
|
+
const aged = this.queue
|
|
907
|
+
.filter((request) => now - request.enqueuedAt >= this.options.maxWaitMs)
|
|
908
|
+
.sort((left, right) => left.sequence - right.sequence)[0];
|
|
909
|
+
if (aged !== undefined) {
|
|
910
|
+
this.queue.splice(this.queue.indexOf(aged), 1);
|
|
911
|
+
this.recordSelection(aged.priority);
|
|
912
|
+
return aged;
|
|
913
|
+
}
|
|
914
|
+
const controls = this.queue.filter((request) => request.priority === "control");
|
|
915
|
+
const mutations = this.queue.filter((request) => request.priority === "mutation");
|
|
916
|
+
const reads = this.queue.filter((request) => request.priority === "read");
|
|
917
|
+
let selected;
|
|
918
|
+
if (controls.length > 0 &&
|
|
919
|
+
(mutations.length === 0 || this.consecutive.control < this.options.maxConsecutiveControls)) {
|
|
920
|
+
selected = controls[0];
|
|
921
|
+
}
|
|
922
|
+
else if (mutations.length > 0 &&
|
|
923
|
+
(reads.length === 0 || (this.consecutive.read >= this.options.maxConsecutiveReads &&
|
|
924
|
+
this.consecutive.mutation < this.options.maxConsecutiveMutations))) {
|
|
925
|
+
selected = mutations[0];
|
|
926
|
+
}
|
|
927
|
+
else if (reads.length > 0) {
|
|
928
|
+
selected = reads[0];
|
|
929
|
+
}
|
|
930
|
+
else if (mutations.length > 0) {
|
|
931
|
+
selected = mutations[0];
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
selected = controls[0];
|
|
935
|
+
}
|
|
936
|
+
if (selected === undefined)
|
|
937
|
+
return undefined;
|
|
938
|
+
this.queue.splice(this.queue.indexOf(selected), 1);
|
|
939
|
+
this.recordSelection(selected.priority);
|
|
940
|
+
return selected;
|
|
941
|
+
}
|
|
942
|
+
recordSelection(priority) {
|
|
943
|
+
for (const candidate of ["read", "mutation", "control"]) {
|
|
944
|
+
this.consecutive[candidate] = candidate === priority ? this.consecutive[candidate] + 1 : 0;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
removeExpiredQueued() {
|
|
948
|
+
const now = this.options.now();
|
|
949
|
+
for (const request of [...this.queue]) {
|
|
950
|
+
if (request.deadlineAt !== undefined && request.deadlineAt <= now)
|
|
951
|
+
this.handleDeadline(request);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function makeDeadline(options, now) {
|
|
956
|
+
if (options.deadlineAt !== undefined && options.timeoutMs !== undefined) {
|
|
957
|
+
throw new InvalidCoordinatorRequestError("Use deadlineAt or timeoutMs, not both");
|
|
958
|
+
}
|
|
959
|
+
if (options.deadlineAt !== undefined) {
|
|
960
|
+
if (!Number.isFinite(options.deadlineAt))
|
|
961
|
+
throw new InvalidCoordinatorRequestError("deadlineAt must be finite");
|
|
962
|
+
return options.deadlineAt;
|
|
963
|
+
}
|
|
964
|
+
if (options.timeoutMs !== undefined) {
|
|
965
|
+
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0) {
|
|
966
|
+
throw new InvalidCoordinatorRequestError("timeoutMs must be a non-negative finite number");
|
|
967
|
+
}
|
|
968
|
+
return now + options.timeoutMs;
|
|
969
|
+
}
|
|
970
|
+
return undefined;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Process-local actors for short browser acquisition and tab transactions.
|
|
974
|
+
*
|
|
975
|
+
* The class intentionally coordinates only cooperating callers in this
|
|
976
|
+
* process. It does not advertise provider-level or cross-process tab
|
|
977
|
+
* concurrency; a provider claim/fencing capability must be integrated before
|
|
978
|
+
* those guarantees can be made. Callback code should perform one short
|
|
979
|
+
* browser operation. Polling, sleeps, journal I/O, hashing, and report work
|
|
980
|
+
* belong outside this API so no scheduler actor is held by those waits.
|
|
981
|
+
*/
|
|
982
|
+
export class ProcessTabCoordinator {
|
|
983
|
+
browserActors = new Map();
|
|
984
|
+
tabActors = new Map();
|
|
985
|
+
browserGates = new Map();
|
|
986
|
+
idleDiagnostics = new Map();
|
|
987
|
+
options;
|
|
988
|
+
constructor(options = {}) {
|
|
989
|
+
this.options = {
|
|
990
|
+
maxQueueSize: validatePositiveInteger("maxQueueSize", options.maxQueueSize ?? 64),
|
|
991
|
+
maxConsecutiveReads: validatePositiveInteger("maxConsecutiveReads", options.maxConsecutiveReads ?? 4),
|
|
992
|
+
maxConsecutiveMutations: validatePositiveInteger("maxConsecutiveMutations", options.maxConsecutiveMutations ?? 4),
|
|
993
|
+
maxConsecutiveControls: validatePositiveInteger("maxConsecutiveControls", options.maxConsecutiveControls ?? 4),
|
|
994
|
+
maxWaitMs: (() => {
|
|
995
|
+
const value = options.maxWaitMs ?? 1_000;
|
|
996
|
+
if (!Number.isFinite(value) || value < 0)
|
|
997
|
+
throw new InvalidCoordinatorRequestError("maxWaitMs must be non-negative");
|
|
998
|
+
return value;
|
|
999
|
+
})(),
|
|
1000
|
+
maxConsecutiveBrowserExclusives: validatePositiveInteger("maxConsecutiveBrowserExclusives", options.maxConsecutiveBrowserExclusives ?? 4),
|
|
1001
|
+
maxIdleDiagnostics: validatePositiveInteger("maxIdleDiagnostics", options.maxIdleDiagnostics ?? 256),
|
|
1002
|
+
now: options.now ?? (() => Date.now())
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
withBrowserAcquisition(resourceKey, options, callback) {
|
|
1006
|
+
return this.enqueue("browser", resourceKey, options, callback);
|
|
1007
|
+
}
|
|
1008
|
+
withTabTransaction(resourceKey, options, callback) {
|
|
1009
|
+
return this.enqueue("tab", resourceKey, options, callback);
|
|
1010
|
+
}
|
|
1011
|
+
getBrowserDiagnostics(resourceKey) {
|
|
1012
|
+
validateResourceKey("browser", resourceKey);
|
|
1013
|
+
const actor = this.browserActors.get(resourceKey);
|
|
1014
|
+
const diagnostics = actor?.snapshot()
|
|
1015
|
+
?? this.idleDiagnostics.get(this.diagnosticsKey("browser", resourceKey))
|
|
1016
|
+
?? this.emptyDiagnostics("browser", resourceKey);
|
|
1017
|
+
const gate = this.browserGates.get(resourceKey);
|
|
1018
|
+
return cloneQueueDiagnostics(gate === undefined ? diagnostics : { ...diagnostics, browserGate: gate.snapshot() });
|
|
1019
|
+
}
|
|
1020
|
+
getTabDiagnostics(resourceKey) {
|
|
1021
|
+
validateResourceKey("tab", resourceKey);
|
|
1022
|
+
const diagnostics = this.tabActors.get(resourceKey)?.snapshot()
|
|
1023
|
+
?? this.idleDiagnostics.get(this.diagnosticsKey("tab", resourceKey))
|
|
1024
|
+
?? this.emptyDiagnostics("tab", resourceKey);
|
|
1025
|
+
const gate = this.browserGates.get(browserKeyForResource("tab", resourceKey));
|
|
1026
|
+
return cloneQueueDiagnostics(gate === undefined ? diagnostics : { ...diagnostics, browserGate: gate.snapshot() });
|
|
1027
|
+
}
|
|
1028
|
+
getActor(kind, key) {
|
|
1029
|
+
const actors = kind === "browser" ? this.browserActors : this.tabActors;
|
|
1030
|
+
let actor = actors.get(key);
|
|
1031
|
+
if (actor === undefined) {
|
|
1032
|
+
actor = this.createActor(kind, key);
|
|
1033
|
+
actors.set(key, actor);
|
|
1034
|
+
}
|
|
1035
|
+
return actor;
|
|
1036
|
+
}
|
|
1037
|
+
createActor(kind, key) {
|
|
1038
|
+
const actors = kind === "browser" ? this.browserActors : this.tabActors;
|
|
1039
|
+
const diagnosticsKey = this.diagnosticsKey(kind, key);
|
|
1040
|
+
const initialDiagnostics = this.idleDiagnostics.get(diagnosticsKey);
|
|
1041
|
+
let actor;
|
|
1042
|
+
actor = new ResourceActor({
|
|
1043
|
+
resourceKind: kind,
|
|
1044
|
+
resourceKey: key,
|
|
1045
|
+
maxQueueSize: this.options.maxQueueSize,
|
|
1046
|
+
maxConsecutiveReads: this.options.maxConsecutiveReads,
|
|
1047
|
+
maxConsecutiveMutations: this.options.maxConsecutiveMutations,
|
|
1048
|
+
maxConsecutiveControls: this.options.maxConsecutiveControls,
|
|
1049
|
+
maxWaitMs: this.options.maxWaitMs,
|
|
1050
|
+
now: this.options.now
|
|
1051
|
+
}, initialDiagnostics, (idleActor) => this.onActorIdle(kind, key, idleActor));
|
|
1052
|
+
// The callback runs only after a later asynchronous request turn, but
|
|
1053
|
+
// retaining this identity check makes cleanup safe if a caller creates a
|
|
1054
|
+
// replacement actor after an idle notification has been queued.
|
|
1055
|
+
if (actors.get(key) === undefined)
|
|
1056
|
+
this.idleDiagnostics.delete(diagnosticsKey);
|
|
1057
|
+
return actor;
|
|
1058
|
+
}
|
|
1059
|
+
diagnosticsKey(kind, key) {
|
|
1060
|
+
return `${kind}:${key}`;
|
|
1061
|
+
}
|
|
1062
|
+
emptyDiagnostics(kind, key) {
|
|
1063
|
+
return {
|
|
1064
|
+
resourceKind: kind,
|
|
1065
|
+
resourceKey: key,
|
|
1066
|
+
queueDepth: 0,
|
|
1067
|
+
active: false,
|
|
1068
|
+
completedCount: 0,
|
|
1069
|
+
rejectedCount: 0
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
onActorIdle(kind, key, actor) {
|
|
1073
|
+
const actors = kind === "browser" ? this.browserActors : this.tabActors;
|
|
1074
|
+
if (actors.get(key) !== actor || !actor.isIdle())
|
|
1075
|
+
return;
|
|
1076
|
+
const browserKey = kind === "browser" ? key : browserKeyForResource("tab", key);
|
|
1077
|
+
// Keep a browser actor discoverable while its parent gate still carries a
|
|
1078
|
+
// shared or exclusive lease. Otherwise a diagnostics lookup could
|
|
1079
|
+
// create a replacement actor during an in-flight sibling-tab operation.
|
|
1080
|
+
if (kind === "browser") {
|
|
1081
|
+
const gate = this.browserGates.get(browserKey);
|
|
1082
|
+
if (gate !== undefined && !gate.isIdle())
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
this.idleDiagnostics.delete(this.diagnosticsKey(kind, key));
|
|
1086
|
+
this.idleDiagnostics.set(this.diagnosticsKey(kind, key), cloneQueueDiagnostics(actor.snapshot()));
|
|
1087
|
+
while (this.idleDiagnostics.size > this.options.maxIdleDiagnostics) {
|
|
1088
|
+
const oldest = this.idleDiagnostics.keys().next().value;
|
|
1089
|
+
if (oldest === undefined)
|
|
1090
|
+
break;
|
|
1091
|
+
this.idleDiagnostics.delete(oldest);
|
|
1092
|
+
}
|
|
1093
|
+
actors.delete(key);
|
|
1094
|
+
this.maybeCleanupGate(browserKey);
|
|
1095
|
+
}
|
|
1096
|
+
getBrowserGate(kind, resourceKey) {
|
|
1097
|
+
const browserKey = browserKeyForResource(kind, resourceKey);
|
|
1098
|
+
let gate = this.browserGates.get(browserKey);
|
|
1099
|
+
if (gate === undefined) {
|
|
1100
|
+
gate = new BrowserGate(browserKey, this.options.maxQueueSize, this.options.maxConsecutiveBrowserExclusives);
|
|
1101
|
+
this.browserGates.set(browserKey, gate);
|
|
1102
|
+
}
|
|
1103
|
+
return gate;
|
|
1104
|
+
}
|
|
1105
|
+
maybeCleanupGate(browserKey) {
|
|
1106
|
+
const gate = this.browserGates.get(browserKey);
|
|
1107
|
+
if (gate === undefined || !gate.isIdle())
|
|
1108
|
+
return;
|
|
1109
|
+
const browserActor = this.browserActors.get(browserKey);
|
|
1110
|
+
if (browserActor !== undefined && !browserActor.isIdle())
|
|
1111
|
+
return;
|
|
1112
|
+
if (browserActor !== undefined) {
|
|
1113
|
+
// The browser actor may have completed before the last tab lease. Its
|
|
1114
|
+
// idle callback intentionally deferred eviction so the parent gate
|
|
1115
|
+
// remained visible; finish that deterministic handoff now.
|
|
1116
|
+
this.onActorIdle("browser", browserKey, browserActor);
|
|
1117
|
+
if (this.browserActors.has(browserKey))
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
this.browserGates.delete(browserKey);
|
|
1121
|
+
}
|
|
1122
|
+
enqueue(kind, resourceKey, requestOptions, callback) {
|
|
1123
|
+
if (typeof callback !== "function")
|
|
1124
|
+
throw new InvalidCoordinatorRequestError("callback is required");
|
|
1125
|
+
validateResourceKey(kind, resourceKey);
|
|
1126
|
+
const stored = acquisitionContexts.getStore();
|
|
1127
|
+
const current = stored !== undefined && activeAcquisitionTokens.has(stored.acquisitionToken) ? stored : undefined;
|
|
1128
|
+
const explicit = requestOptions.acquisitionContext;
|
|
1129
|
+
if (explicit !== undefined && !activeAcquisitionTokens.has(explicit.acquisitionToken)) {
|
|
1130
|
+
return Promise.reject(new InvalidCoordinatorRequestError("acquisitionContext is stale or does not belong to an active coordinator callback"));
|
|
1131
|
+
}
|
|
1132
|
+
if (current !== undefined && explicit !== undefined && current.acquisitionToken !== explicit.acquisitionToken) {
|
|
1133
|
+
return Promise.reject(new InvalidCoordinatorRequestError("acquisitionContext cannot override the active async coordinator context"));
|
|
1134
|
+
}
|
|
1135
|
+
const parent = current ?? explicit;
|
|
1136
|
+
if (parent !== undefined) {
|
|
1137
|
+
// Nested acquisition of any coordinator actor can deadlock when another
|
|
1138
|
+
// caller acquires the same resources in the opposite order. Callers must
|
|
1139
|
+
// finish one short browser transaction before requesting another.
|
|
1140
|
+
return Promise.reject(new ReentrantAcquisitionError(parent));
|
|
1141
|
+
}
|
|
1142
|
+
const owner = validateOwner(requestOptions.owner);
|
|
1143
|
+
const priority = validatePriority(requestOptions.priority);
|
|
1144
|
+
const now = this.options.now();
|
|
1145
|
+
const deadlineAt = makeDeadline(requestOptions, now);
|
|
1146
|
+
const browserKey = browserKeyForResource(kind, resourceKey);
|
|
1147
|
+
const gate = this.getBrowserGate(kind, resourceKey);
|
|
1148
|
+
const admission = kind === "browser"
|
|
1149
|
+
? gate.createExclusiveAdmission()
|
|
1150
|
+
: gate.createSharedAdmission();
|
|
1151
|
+
const timing = {
|
|
1152
|
+
requestId: randomUUID(),
|
|
1153
|
+
resourceKind: kind,
|
|
1154
|
+
resourceKey,
|
|
1155
|
+
priority,
|
|
1156
|
+
owner,
|
|
1157
|
+
...(requestOptions.label === undefined ? {} : { label: requestOptions.label }),
|
|
1158
|
+
enqueuedAt: now,
|
|
1159
|
+
...(deadlineAt === undefined ? {} : { deadlineAt })
|
|
1160
|
+
};
|
|
1161
|
+
const controller = new AbortController();
|
|
1162
|
+
const request = {
|
|
1163
|
+
requestId: timing.requestId,
|
|
1164
|
+
priority,
|
|
1165
|
+
owner,
|
|
1166
|
+
...(requestOptions.label === undefined ? {} : { label: requestOptions.label }),
|
|
1167
|
+
enqueuedAt: now,
|
|
1168
|
+
...(deadlineAt === undefined ? {} : { deadlineAt }),
|
|
1169
|
+
callback,
|
|
1170
|
+
timing,
|
|
1171
|
+
controller,
|
|
1172
|
+
admission
|
|
1173
|
+
};
|
|
1174
|
+
const actor = this.getActor(kind, resourceKey);
|
|
1175
|
+
const result = actor.enqueue(request, requestOptions.signal);
|
|
1176
|
+
// Queue-full and already-aborted requests may never call admission's
|
|
1177
|
+
// accepted hook. Remove a gate created solely for that rejected request,
|
|
1178
|
+
// while leaving any active/queued parent work untouched.
|
|
1179
|
+
this.maybeCleanupGate(browserKey);
|
|
1180
|
+
return result;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
/** Explicit factory to make process/runtime ownership visible at call sites. */
|
|
1184
|
+
export function createProcessTabCoordinator(options) {
|
|
1185
|
+
return new ProcessTabCoordinator(options);
|
|
1186
|
+
}
|
|
1187
|
+
let defaultProcessCoordinator;
|
|
1188
|
+
/**
|
|
1189
|
+
* Return the lifecycle-wide coordinator used by default SDK/runtime services.
|
|
1190
|
+
*
|
|
1191
|
+
* Constructing a coordinator per client would make each queue internally
|
|
1192
|
+
* correct while allowing two clients in the same backend process to overlap
|
|
1193
|
+
* on the same tab. Callers that need deterministic test limits may still
|
|
1194
|
+
* inject an explicitly constructed coordinator; production integration should
|
|
1195
|
+
* use this shared instance.
|
|
1196
|
+
*/
|
|
1197
|
+
export function getProcessTabCoordinator() {
|
|
1198
|
+
defaultProcessCoordinator ??= new ProcessTabCoordinator();
|
|
1199
|
+
return defaultProcessCoordinator;
|
|
1200
|
+
}
|