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.
Files changed (231) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +43 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +40 -1
  4. package/contracts/v1/fixtures/backend-compatibility.json +21 -0
  5. package/contracts/v1/fixtures/backend-version.json +6 -1
  6. package/contracts/v1/fixtures/download-blocked-by-browser.json +22 -0
  7. package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
  8. package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
  9. package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
  10. package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
  11. package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
  12. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  13. package/contracts/v1/fixtures/operation-action.json +13 -0
  14. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  15. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  16. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  17. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  18. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  19. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  20. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  21. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  22. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  23. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  24. package/contracts/v1/fixtures/operation-event.json +18 -0
  25. package/contracts/v1/fixtures/operation-handle.json +10 -0
  26. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  27. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  28. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  29. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  30. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  31. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  32. package/contracts/v1/fixtures/operation-request.json +33 -0
  33. package/contracts/v1/fixtures/operation-state.json +144 -0
  34. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  35. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  36. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  37. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  38. package/contracts/v1/manifest.json +185 -1
  39. package/contracts/v1/parity-suite.json +106 -8
  40. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  41. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  42. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  43. package/contracts/v1/schemas/manifest.schema.json +57 -0
  44. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  45. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  46. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  47. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  48. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  49. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  50. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  51. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  52. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  53. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  54. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  55. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  56. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  57. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  58. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  59. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  60. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  61. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  62. package/contracts/v1/surface-drift-policy.json +50 -0
  63. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  64. package/dist/codex-chatgpt-control-backend.mjs +45882 -9361
  65. package/dist/codex-chatgpt-control-journal.mjs +5547 -0
  66. package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
  67. package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
  68. package/dist/codex-chatgpt-control.bundle.mjs +48392 -10260
  69. package/dist/src/backend/client.d.ts +103 -2
  70. package/dist/src/backend/client.js +1436 -97
  71. package/dist/src/backend/compatibility.d.ts +12 -0
  72. package/dist/src/backend/compatibility.js +208 -0
  73. package/dist/src/backend/protocol.d.ts +84 -1
  74. package/dist/src/backend/protocol.js +31 -5
  75. package/dist/src/backend/runtime-identity.d.ts +10 -0
  76. package/dist/src/backend/runtime-identity.js +141 -0
  77. package/dist/src/backend/session.d.ts +10 -2
  78. package/dist/src/backend/session.js +478 -7
  79. package/dist/src/backend/stdio-server.d.ts +4 -1
  80. package/dist/src/backend/stdio-server.js +240 -28
  81. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  82. package/dist/src/browser/active-composer-file-input.js +33 -0
  83. package/dist/src/browser/attach.d.ts +4 -1
  84. package/dist/src/browser/attach.js +136 -28
  85. package/dist/src/browser/chatgpt-url.d.ts +2 -0
  86. package/dist/src/browser/chatgpt-url.js +9 -0
  87. package/dist/src/browser/downloads.d.ts +17 -1
  88. package/dist/src/browser/downloads.js +150 -28
  89. package/dist/src/browser/page-state.js +6 -0
  90. package/dist/src/client.d.ts +43 -0
  91. package/dist/src/client.js +1279 -59
  92. package/dist/src/commands/artifacts.js +6 -4
  93. package/dist/src/commands/chat-popover.d.ts +37 -0
  94. package/dist/src/commands/chat-popover.js +537 -0
  95. package/dist/src/commands/configuration.d.ts +5 -0
  96. package/dist/src/commands/configuration.js +134 -21
  97. package/dist/src/commands/context.js +10 -3
  98. package/dist/src/commands/doctor.d.ts +1 -1
  99. package/dist/src/commands/doctor.js +27 -4
  100. package/dist/src/commands/experience.d.ts +5 -0
  101. package/dist/src/commands/experience.js +104 -21
  102. package/dist/src/commands/files.js +14 -33
  103. package/dist/src/commands/modes.js +85 -21
  104. package/dist/src/commands/power-discovery.d.ts +130 -0
  105. package/dist/src/commands/power-discovery.js +669 -0
  106. package/dist/src/commands/project-sources.js +325 -31
  107. package/dist/src/commands/sequence.js +59 -32
  108. package/dist/src/commands/session.js +19 -6
  109. package/dist/src/commands/work.js +31 -0
  110. package/dist/src/dom/composer-text.d.ts +6 -0
  111. package/dist/src/dom/composer-text.js +85 -0
  112. package/dist/src/errors.d.ts +9 -0
  113. package/dist/src/errors.js +22 -0
  114. package/dist/src/index.d.ts +6 -0
  115. package/dist/src/index.js +9 -0
  116. package/dist/src/operations/artifact-output.d.ts +69 -0
  117. package/dist/src/operations/artifact-output.js +1748 -0
  118. package/dist/src/operations/artifact-stream.d.ts +19 -0
  119. package/dist/src/operations/artifact-stream.js +35 -0
  120. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  121. package/dist/src/operations/artifact-transfer.js +1312 -0
  122. package/dist/src/operations/browser-adapter.d.ts +167 -0
  123. package/dist/src/operations/browser-adapter.js +2103 -0
  124. package/dist/src/operations/browser-observation.d.ts +110 -0
  125. package/dist/src/operations/browser-observation.js +1256 -0
  126. package/dist/src/operations/browser-target.d.ts +105 -0
  127. package/dist/src/operations/browser-target.js +531 -0
  128. package/dist/src/operations/canonical.d.ts +4 -0
  129. package/dist/src/operations/canonical.js +412 -0
  130. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  131. package/dist/src/operations/chatgpt-runtime.js +1232 -0
  132. package/dist/src/operations/client.d.ts +138 -0
  133. package/dist/src/operations/client.js +1145 -0
  134. package/dist/src/operations/collector.d.ts +195 -0
  135. package/dist/src/operations/collector.js +1039 -0
  136. package/dist/src/operations/configuration-routing.d.ts +4 -0
  137. package/dist/src/operations/configuration-routing.js +14 -0
  138. package/dist/src/operations/control.d.ts +372 -0
  139. package/dist/src/operations/control.js +1498 -0
  140. package/dist/src/operations/file-identity.d.ts +33 -0
  141. package/dist/src/operations/file-identity.js +153 -0
  142. package/dist/src/operations/handle.d.ts +19 -0
  143. package/dist/src/operations/handle.js +727 -0
  144. package/dist/src/operations/index.d.ts +25 -0
  145. package/dist/src/operations/index.js +25 -0
  146. package/dist/src/operations/journal-authority.d.ts +12 -0
  147. package/dist/src/operations/journal-authority.js +1 -0
  148. package/dist/src/operations/journal-rpc-client.d.ts +12 -0
  149. package/dist/src/operations/journal-rpc-client.js +225 -0
  150. package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
  151. package/dist/src/operations/journal-rpc-protocol.js +214 -0
  152. package/dist/src/operations/journal-rpc-server.d.ts +15 -0
  153. package/dist/src/operations/journal-rpc-server.js +272 -0
  154. package/dist/src/operations/journal.d.ts +143 -0
  155. package/dist/src/operations/journal.js +1957 -0
  156. package/dist/src/operations/production-attachments.d.ts +118 -0
  157. package/dist/src/operations/production-attachments.js +1035 -0
  158. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  159. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  160. package/dist/src/operations/production-chatgpt-attachments.d.ts +80 -0
  161. package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
  162. package/dist/src/operations/production-configuration.d.ts +42 -0
  163. package/dist/src/operations/production-configuration.js +1544 -0
  164. package/dist/src/operations/production-primitives.d.ts +53 -0
  165. package/dist/src/operations/production-primitives.js +856 -0
  166. package/dist/src/operations/production-work-steer.d.ts +203 -0
  167. package/dist/src/operations/production-work-steer.js +1218 -0
  168. package/dist/src/operations/recovery.d.ts +80 -0
  169. package/dist/src/operations/recovery.js +174 -0
  170. package/dist/src/operations/runtime-adapter.d.ts +126 -0
  171. package/dist/src/operations/runtime-adapter.js +802 -0
  172. package/dist/src/operations/send-once.d.ts +224 -0
  173. package/dist/src/operations/send-once.js +1081 -0
  174. package/dist/src/operations/service.d.ts +286 -0
  175. package/dist/src/operations/service.js +2831 -0
  176. package/dist/src/operations/staging.d.ts +136 -0
  177. package/dist/src/operations/staging.js +630 -0
  178. package/dist/src/operations/state-machine.d.ts +29 -0
  179. package/dist/src/operations/state-machine.js +2047 -0
  180. package/dist/src/operations/submission.d.ts +423 -0
  181. package/dist/src/operations/submission.js +1676 -0
  182. package/dist/src/operations/transactional-chat-power.d.ts +17 -0
  183. package/dist/src/operations/transactional-chat-power.js +212 -0
  184. package/dist/src/operations/turn-ownership.d.ts +202 -0
  185. package/dist/src/operations/turn-ownership.js +700 -0
  186. package/dist/src/operations/types.d.ts +454 -0
  187. package/dist/src/operations/types.js +20 -0
  188. package/dist/src/operations/wire-requests.d.ts +21 -0
  189. package/dist/src/operations/wire-requests.js +396 -0
  190. package/dist/src/operations/wire-results.d.ts +97 -0
  191. package/dist/src/operations/wire-results.js +818 -0
  192. package/dist/src/runner/responses.d.ts +3 -1
  193. package/dist/src/runner/responses.js +19 -1
  194. package/dist/src/runner/result.js +227 -55
  195. package/dist/src/runner/types.d.ts +12 -0
  196. package/dist/src/runtime/command-routing.d.ts +149 -0
  197. package/dist/src/runtime/command-routing.js +431 -0
  198. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  199. package/dist/src/runtime/coordinated-browser.js +315 -0
  200. package/dist/src/runtime/coordinated-page.d.ts +41 -0
  201. package/dist/src/runtime/coordinated-page.js +651 -0
  202. package/dist/src/runtime/operation-context.d.ts +142 -0
  203. package/dist/src/runtime/operation-context.js +410 -0
  204. package/dist/src/runtime/runtime-session.d.ts +95 -0
  205. package/dist/src/runtime/runtime-session.js +314 -0
  206. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  207. package/dist/src/runtime/tab-coordinator.js +1200 -0
  208. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  209. package/dist/src/runtime/value-boundaries.js +49 -0
  210. package/dist/src/safety/blockers.js +8 -1
  211. package/dist/src/safety/untrusted-output.js +2 -1
  212. package/dist/src/scripts/backend-server.js +4 -1
  213. package/dist/src/scripts/capture-surface-profile.js +7 -2
  214. package/dist/src/scripts/journal-server.d.ts +2 -0
  215. package/dist/src/scripts/journal-server.js +37 -0
  216. package/dist/src/scripts/live-smoke/harness.js +79 -7
  217. package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
  218. package/dist/src/scripts/live-smoke/scenarios.js +147 -30
  219. package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
  220. package/dist/src/scripts/live-smoke/transactional.js +254 -0
  221. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  222. package/dist/src/scripts/release-canary-module.js +17 -4
  223. package/dist/src/types.d.ts +38 -1
  224. package/package.json +8 -3
  225. package/references/2026-08-16-transactional-operations.md +459 -0
  226. package/references/2026-09-06-journal-service.md +140 -0
  227. package/references/agents-runner.md +34 -0
  228. package/references/backend-protocol.md +176 -0
  229. package/references/python-parity.md +137 -0
  230. package/references/responses-adapter.md +19 -0
  231. 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
+ }