deepline 0.3.128 → 0.3.129

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 (128) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +34 -9
  2. package/dist/bundling-sources/sdk/src/http.ts +22 -0
  3. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  4. package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +22 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +265 -2
  6. package/dist/bundling-sources/shared_libs/play-runtime/bounded-batch-function.ts +682 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +21 -5
  8. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +5390 -1732
  9. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +107 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +7 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +229 -7
  12. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +25 -4
  13. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-capacity.ts +71 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +1 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +4 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +694 -28
  17. package/dist/bundling-sources/shared_libs/play-runtime/egress-bandwidth-probe.ts +167 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/execute-server-timing.ts +103 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-receipt-output-reference.ts +34 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-response-size.ts +27 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +66 -1
  22. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +425 -71
  23. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +210 -32
  24. package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +27 -13
  25. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +19 -6
  26. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +61 -2
  27. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +33 -1
  28. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +42 -1
  29. package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +11 -11
  30. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +49 -16
  31. package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +1653 -98
  32. package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +807 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/memory-pressure-signal.ts +138 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +85 -3
  35. package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +60 -2
  36. package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +46 -1
  37. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +2 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +1 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +70 -27
  42. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +56 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/adaptive-provider-capacity-controller.ts +192 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/epoch-aimd-rate-controller.ts +194 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/fixture-capacity-simulator.ts +220 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +214 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +43 -0
  48. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +30 -0
  49. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-rate-policy.ts +95 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-throttle-classifier.ts +40 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +1535 -11
  52. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +49 -2
  53. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +6 -1
  54. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +55 -0
  55. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/egress-probe.ts +221 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +339 -29
  57. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +28 -1
  58. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +192 -165
  59. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +6 -0
  60. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +90 -5
  61. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +281 -6
  62. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +27 -1
  63. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +19 -0
  64. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +108 -0
  65. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +40 -0
  66. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +11 -5
  67. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +30 -0
  68. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-compression.ts +276 -0
  69. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +723 -115
  70. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +98 -2
  71. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +3 -2
  72. package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +41 -4
  73. package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +448 -0
  74. package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +215 -0
  75. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +11 -0
  76. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +161 -14
  77. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-value.ts +184 -0
  78. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +607 -121
  79. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +4 -0
  80. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +21 -0
  81. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +15 -2
  82. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +71 -14
  83. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +2 -2
  84. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +288 -119
  85. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -6
  86. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +28 -3
  87. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/canonical-integration-operation-store.ts +179 -0
  88. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +80 -2
  89. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +31 -2
  90. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-store-contract.ts +104 -0
  91. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-reference.ts +47 -0
  92. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-retirement.ts +193 -0
  93. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-storage.ts +420 -0
  94. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +41 -0
  95. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +134 -3
  96. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +1657 -95
  97. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +885 -73
  98. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-completion-coalescer.ts +452 -0
  99. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +601 -42
  100. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +1 -0
  101. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +371 -0
  102. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +86 -7
  103. package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +153 -22
  104. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
  105. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +188 -38
  106. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +28 -13
  107. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +44 -1
  108. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +56 -4
  109. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +2 -0
  110. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +16 -10
  111. package/dist/bundling-sources/shared_libs/plays/dataset.ts +4 -1
  112. package/dist/cli/index.js +250 -38
  113. package/dist/cli/index.mjs +250 -38
  114. package/dist/{compiler-manifest-DfSnbCTW.d.mts → compiler-manifest-BIqRyj5m.d.mts} +1 -1
  115. package/dist/{compiler-manifest-DfSnbCTW.d.ts → compiler-manifest-BIqRyj5m.d.ts} +1 -1
  116. package/dist/index.d.mts +4 -2
  117. package/dist/index.d.ts +4 -2
  118. package/dist/index.js +231 -35
  119. package/dist/index.mjs +231 -35
  120. package/dist/install-integrity.json +31 -2
  121. package/dist/plays/bundle-play-file.d.mts +2 -2
  122. package/dist/plays/bundle-play-file.d.ts +2 -2
  123. package/dist/plays/bundle-play-file.mjs +65 -1
  124. package/dist/release.d.mts +1 -1
  125. package/dist/release.d.ts +1 -1
  126. package/dist/release.js +1 -1
  127. package/dist/release.mjs +1 -1
  128. package/package.json +1 -1
@@ -0,0 +1,682 @@
1
+ /**
2
+ * A small, host-neutral batching primitive for ordinary async code.
3
+ *
4
+ * `submit()` returns one promise per value. Compatible values share one call to
5
+ * `execute()`, whose result positions settle the original promises. The
6
+ * primitive deliberately owns only bounded grouping, timing, cancellation, and
7
+ * result correlation. Durable identity, admission, retries, and persistence
8
+ * remain with the caller's executor, where their domain-specific semantics
9
+ * already live.
10
+ *
11
+ * Every input is counted from acceptance until its physical batch settles,
12
+ * including inputs already handed to `execute()`. That makes the configured
13
+ * retained item/byte/unit budgets a real memory bound rather than a queue-only
14
+ * illusion. A full budget rejects before retaining another closure or input;
15
+ * there is no hidden unbounded waiter list.
16
+ */
17
+
18
+ type PendingState = 'queued' | 'ready' | 'active' | 'settled';
19
+
20
+ type Pending<Value, Result, Key> = {
21
+ key: Key;
22
+ keyId: string;
23
+ value: Value;
24
+ bytes: number;
25
+ units: number;
26
+ enqueuedAt: number;
27
+ deadlineAt: number | null;
28
+ signal: AbortSignal | null;
29
+ onAbort: (() => void) | null;
30
+ state: PendingState;
31
+ settled: boolean;
32
+ resolve: (result: Result) => void;
33
+ reject: (error: unknown) => void;
34
+ };
35
+
36
+ type OpenGroup<Value, Result, Key> = {
37
+ key: Key;
38
+ keyId: string;
39
+ items: Array<Pending<Value, Result, Key>>;
40
+ bytes: number;
41
+ units: number;
42
+ timer: ReturnType<typeof setTimeout> | null;
43
+ };
44
+
45
+ type ReadyBatch<Value, Result, Key> = Omit<
46
+ OpenGroup<Value, Result, Key>,
47
+ 'timer'
48
+ >;
49
+
50
+ export class BoundedBatchInputTooLargeError extends Error {
51
+ constructor(
52
+ readonly bytes: number,
53
+ readonly units: number,
54
+ readonly maxBatchBytes: number,
55
+ readonly maxBatchUnits: number,
56
+ ) {
57
+ super(
58
+ `Batch input exceeds one physical batch (bytes ${bytes}/${maxBatchBytes}, units ${units}/${maxBatchUnits}).`,
59
+ );
60
+ this.name = 'BoundedBatchInputTooLargeError';
61
+ }
62
+ }
63
+
64
+ export class BoundedBatchCapacityError extends Error {
65
+ constructor(
66
+ readonly limit: 'items' | 'bytes' | 'units' | 'groups',
67
+ readonly current: number,
68
+ readonly maximum: number,
69
+ ) {
70
+ super(
71
+ `Bounded batch ${limit} capacity is full (${current}/${maximum}); retry after a physical batch settles.`,
72
+ );
73
+ this.name = 'BoundedBatchCapacityError';
74
+ }
75
+ }
76
+
77
+ export class BoundedBatchDeadlineError extends Error {
78
+ constructor(readonly waitedMs: number) {
79
+ super(`Bounded batch item exceeded its ${waitedMs}ms queue deadline.`);
80
+ this.name = 'BoundedBatchDeadlineError';
81
+ }
82
+ }
83
+
84
+ export class BoundedBatchResultCountError extends Error {
85
+ constructor(
86
+ readonly expected: number,
87
+ readonly received: number,
88
+ ) {
89
+ super(
90
+ `Bounded batch executor returned ${received} results for ${expected} inputs.`,
91
+ );
92
+ this.name = 'BoundedBatchResultCountError';
93
+ }
94
+ }
95
+
96
+ export class BoundedBatchClosedError extends Error {
97
+ constructor() {
98
+ super('Bounded batch function is closed.');
99
+ this.name = 'BoundedBatchClosedError';
100
+ }
101
+ }
102
+
103
+ export type BoundedBatchStats = {
104
+ openGroups: number;
105
+ readyBatches: number;
106
+ queuedItems: number;
107
+ activeItems: number;
108
+ inFlightBatches: number;
109
+ retainedItems: number;
110
+ retainedBytes: number;
111
+ retainedUnits: number;
112
+ };
113
+
114
+ function positiveInteger(value: number, name: string): number {
115
+ if (!Number.isSafeInteger(value) || value < 1) {
116
+ throw new Error(`Bounded batch requires ${name} to be a positive integer.`);
117
+ }
118
+ return value;
119
+ }
120
+
121
+ function nonNegativeInteger(value: number, name: string): number {
122
+ if (!Number.isSafeInteger(value) || value < 0) {
123
+ throw new Error(
124
+ `Bounded batch requires ${name} to be a non-negative safe integer.`,
125
+ );
126
+ }
127
+ return value;
128
+ }
129
+
130
+ function abortReason(signal: AbortSignal): unknown {
131
+ return (
132
+ signal.reason ??
133
+ new DOMException('The operation was aborted.', 'AbortError')
134
+ );
135
+ }
136
+
137
+ /**
138
+ * Batch compatible ordinary async function calls under one explicit resource
139
+ * budget. `keyId` must represent every fact that makes a shared execution
140
+ * unsafe (tenant, auth, wire format, provider policy, and so on).
141
+ */
142
+ export class BoundedBatchFunction<Value, Result, Key> {
143
+ readonly #execute: (input: {
144
+ key: Key;
145
+ values: readonly Value[];
146
+ signal: AbortSignal;
147
+ }) => Promise<readonly Result[]>;
148
+ readonly #keyId: (key: Key) => string;
149
+ readonly #estimateBytes: (value: Value) => number;
150
+ readonly #units: (value: Value) => number;
151
+ readonly #maxBatchItems: number;
152
+ readonly #maxBatchBytes: number;
153
+ readonly #maxBatchBytesForKey: (key: Key) => number;
154
+ readonly #maxBatchUnits: number;
155
+ readonly #maxRetainedItems: number;
156
+ readonly #maxRetainedBytes: number;
157
+ readonly #maxRetainedUnits: number;
158
+ readonly #maxOpenGroups: number;
159
+ readonly #maxInFlightBatches: number;
160
+ readonly #flushMs: number;
161
+ readonly #now: () => number;
162
+ readonly #closedController = new AbortController();
163
+ readonly #groups = new Map<string, OpenGroup<Value, Result, Key>>();
164
+ readonly #ready: Array<ReadyBatch<Value, Result, Key>> = [];
165
+ readonly #inFlight = new Set<Promise<void>>();
166
+ #deadlineTimer: ReturnType<typeof setTimeout> | null = null;
167
+ #closed = false;
168
+ #closeReason: unknown = null;
169
+ #retainedItems = 0;
170
+ #retainedBytes = 0;
171
+ #retainedUnits = 0;
172
+ #activeItems = 0;
173
+
174
+ constructor(input: {
175
+ keyId: (key: Key) => string;
176
+ execute: (input: {
177
+ key: Key;
178
+ values: readonly Value[];
179
+ signal: AbortSignal;
180
+ }) => Promise<readonly Result[]>;
181
+ estimateBytes?: (value: Value) => number;
182
+ units?: (value: Value) => number;
183
+ maxBatchItems: number;
184
+ maxBatchBytes: number;
185
+ /** Optional compatibility-key-specific physical byte ceiling. */
186
+ maxBatchBytesForKey?: (key: Key) => number;
187
+ maxBatchUnits?: number;
188
+ maxRetainedItems: number;
189
+ maxRetainedBytes: number;
190
+ maxRetainedUnits?: number;
191
+ maxOpenGroups?: number;
192
+ maxInFlightBatches?: number;
193
+ flushMs?: number;
194
+ now?: () => number;
195
+ }) {
196
+ this.#keyId = input.keyId;
197
+ this.#execute = input.execute;
198
+ this.#estimateBytes = input.estimateBytes ?? (() => 0);
199
+ this.#units = input.units ?? (() => 1);
200
+ this.#maxBatchItems = positiveInteger(input.maxBatchItems, 'maxBatchItems');
201
+ this.#maxBatchBytes = positiveInteger(input.maxBatchBytes, 'maxBatchBytes');
202
+ this.#maxBatchBytesForKey =
203
+ input.maxBatchBytesForKey ?? (() => this.#maxBatchBytes);
204
+ this.#maxBatchUnits = positiveInteger(
205
+ input.maxBatchUnits ?? input.maxBatchItems,
206
+ 'maxBatchUnits',
207
+ );
208
+ this.#maxRetainedItems = positiveInteger(
209
+ input.maxRetainedItems,
210
+ 'maxRetainedItems',
211
+ );
212
+ this.#maxRetainedBytes = positiveInteger(
213
+ input.maxRetainedBytes,
214
+ 'maxRetainedBytes',
215
+ );
216
+ this.#maxRetainedUnits = positiveInteger(
217
+ input.maxRetainedUnits ?? input.maxRetainedItems,
218
+ 'maxRetainedUnits',
219
+ );
220
+ this.#maxOpenGroups = positiveInteger(
221
+ input.maxOpenGroups ?? input.maxRetainedItems,
222
+ 'maxOpenGroups',
223
+ );
224
+ this.#maxInFlightBatches = positiveInteger(
225
+ input.maxInFlightBatches ?? 1,
226
+ 'maxInFlightBatches',
227
+ );
228
+ this.#flushMs = Math.max(0, Math.floor(input.flushMs ?? 0));
229
+ this.#now = input.now ?? Date.now;
230
+ }
231
+
232
+ /**
233
+ * Submit one logical value. `maxWaitMs` applies only before the physical
234
+ * call starts; after dispatch an individual caller may abort its wait but
235
+ * cannot cancel shared work that can still settle sibling callers safely.
236
+ */
237
+ submit(input: {
238
+ key: Key;
239
+ value: Value;
240
+ signal?: AbortSignal;
241
+ maxWaitMs?: number;
242
+ }): Promise<Result> {
243
+ if (this.#closed) {
244
+ return Promise.reject(this.#closeReason ?? new BoundedBatchClosedError());
245
+ }
246
+ this.#expireQueued();
247
+ if (input.signal?.aborted) return Promise.reject(abortReason(input.signal));
248
+
249
+ const keyId = this.#keyId(input.key);
250
+ if (!keyId) {
251
+ return Promise.reject(
252
+ new Error('Bounded batch keyId must not be empty.'),
253
+ );
254
+ }
255
+ const bytes = nonNegativeInteger(
256
+ Math.ceil(this.#estimateBytes(input.value)),
257
+ 'estimated input bytes',
258
+ );
259
+ const units = positiveInteger(this.#units(input.value), 'input units');
260
+ const maxBatchBytes = positiveInteger(
261
+ this.#maxBatchBytesForKey(input.key),
262
+ 'maxBatchBytesForKey result',
263
+ );
264
+ if (maxBatchBytes > this.#maxBatchBytes) {
265
+ return Promise.reject(
266
+ new Error(
267
+ 'Bounded batch key-specific byte limit exceeds maxBatchBytes.',
268
+ ),
269
+ );
270
+ }
271
+ if (bytes > maxBatchBytes || units > this.#maxBatchUnits) {
272
+ return Promise.reject(
273
+ new BoundedBatchInputTooLargeError(
274
+ bytes,
275
+ units,
276
+ maxBatchBytes,
277
+ this.#maxBatchUnits,
278
+ ),
279
+ );
280
+ }
281
+ if (this.#retainedItems >= this.#maxRetainedItems) {
282
+ return Promise.reject(
283
+ new BoundedBatchCapacityError(
284
+ 'items',
285
+ this.#retainedItems,
286
+ this.#maxRetainedItems,
287
+ ),
288
+ );
289
+ }
290
+ if (this.#retainedBytes + bytes > this.#maxRetainedBytes) {
291
+ return Promise.reject(
292
+ new BoundedBatchCapacityError(
293
+ 'bytes',
294
+ this.#retainedBytes,
295
+ this.#maxRetainedBytes,
296
+ ),
297
+ );
298
+ }
299
+ if (this.#retainedUnits + units > this.#maxRetainedUnits) {
300
+ return Promise.reject(
301
+ new BoundedBatchCapacityError(
302
+ 'units',
303
+ this.#retainedUnits,
304
+ this.#maxRetainedUnits,
305
+ ),
306
+ );
307
+ }
308
+
309
+ const existing = this.#groups.get(keyId);
310
+ if (!existing && this.#groups.size >= this.#maxOpenGroups) {
311
+ return Promise.reject(
312
+ new BoundedBatchCapacityError(
313
+ 'groups',
314
+ this.#groups.size,
315
+ this.#maxOpenGroups,
316
+ ),
317
+ );
318
+ }
319
+
320
+ const maxWaitMs =
321
+ input.maxWaitMs === undefined
322
+ ? null
323
+ : nonNegativeInteger(Math.floor(input.maxWaitMs), 'maxWaitMs');
324
+ const now = this.#now();
325
+ return new Promise<Result>((resolve, reject) => {
326
+ const pending: Pending<Value, Result, Key> = {
327
+ key: input.key,
328
+ keyId,
329
+ value: input.value,
330
+ bytes,
331
+ units,
332
+ enqueuedAt: now,
333
+ deadlineAt: maxWaitMs === null ? null : now + maxWaitMs,
334
+ signal: input.signal ?? null,
335
+ onAbort: null,
336
+ state: 'queued',
337
+ settled: false,
338
+ resolve,
339
+ reject,
340
+ };
341
+ pending.onAbort = () => this.#abortPending(pending);
342
+ pending.signal?.addEventListener('abort', pending.onAbort, {
343
+ once: true,
344
+ });
345
+
346
+ // Seal before accepting the new value: a batch is never allowed to grow
347
+ // past a physical bound while the next compatible value waits behind it.
348
+ const open = this.#groups.get(keyId);
349
+ if (
350
+ open &&
351
+ (open.items.length >= this.#maxBatchItems ||
352
+ open.bytes + bytes > maxBatchBytes ||
353
+ open.units + units > this.#maxBatchUnits)
354
+ ) {
355
+ this.#sealGroup(keyId);
356
+ }
357
+ const group = this.#groups.get(keyId) ?? {
358
+ key: input.key,
359
+ keyId,
360
+ items: [],
361
+ bytes: 0,
362
+ units: 0,
363
+ timer: null,
364
+ };
365
+ group.items.push(pending);
366
+ group.bytes += bytes;
367
+ group.units += units;
368
+ this.#groups.set(keyId, group);
369
+ this.#retain(pending);
370
+
371
+ if (
372
+ group.items.length >= this.#maxBatchItems ||
373
+ group.bytes >= maxBatchBytes ||
374
+ group.units >= this.#maxBatchUnits
375
+ ) {
376
+ this.#sealGroup(keyId);
377
+ } else {
378
+ this.#scheduleGroupFlush(group);
379
+ }
380
+ this.#scheduleDeadlineTimer();
381
+ this.#pump();
382
+ });
383
+ }
384
+
385
+ /** Seal every open window and wait until all physical calls settle. */
386
+ async drain(): Promise<void> {
387
+ for (const keyId of [...this.#groups.keys()]) this.#sealGroup(keyId);
388
+ this.#pump();
389
+ while (this.#ready.length > 0 || this.#inFlight.size > 0) {
390
+ if (this.#inFlight.size === 0) {
391
+ this.#pump();
392
+ continue;
393
+ }
394
+ await Promise.allSettled([...this.#inFlight]);
395
+ this.#pump();
396
+ }
397
+ }
398
+
399
+ /**
400
+ * A graceful close drains accepted work. A forced close rejects subscribers,
401
+ * aborts the executor signal, and still waits for it to release retained
402
+ * inputs before returning.
403
+ */
404
+ async close(reason?: unknown): Promise<void> {
405
+ if (!this.#closed) {
406
+ this.#closed = true;
407
+ this.#closeReason = reason ?? new BoundedBatchClosedError();
408
+ if (reason !== undefined) {
409
+ this.#closedController.abort(reason);
410
+ for (const group of this.#groups.values())
411
+ this.#rejectContainer(group, reason);
412
+ this.#groups.clear();
413
+ for (const batch of this.#ready) this.#rejectContainer(batch, reason);
414
+ this.#ready.length = 0;
415
+ this.#clearDeadlineTimer();
416
+ }
417
+ }
418
+ if (reason === undefined) {
419
+ await this.drain();
420
+ return;
421
+ }
422
+ while (this.#inFlight.size > 0) {
423
+ await Promise.allSettled([...this.#inFlight]);
424
+ }
425
+ }
426
+
427
+ getStats(): BoundedBatchStats {
428
+ const queuedItems =
429
+ [...this.#groups.values()].reduce(
430
+ (total, group) => total + group.items.length,
431
+ 0,
432
+ ) + this.#ready.reduce((total, batch) => total + batch.items.length, 0);
433
+ return {
434
+ openGroups: this.#groups.size,
435
+ readyBatches: this.#ready.length,
436
+ queuedItems,
437
+ activeItems: this.#activeItems,
438
+ inFlightBatches: this.#inFlight.size,
439
+ retainedItems: this.#retainedItems,
440
+ retainedBytes: this.#retainedBytes,
441
+ retainedUnits: this.#retainedUnits,
442
+ };
443
+ }
444
+
445
+ #retain(pending: Pending<Value, Result, Key>): void {
446
+ this.#retainedItems += 1;
447
+ this.#retainedBytes += pending.bytes;
448
+ this.#retainedUnits += pending.units;
449
+ }
450
+
451
+ #release(pending: Pending<Value, Result, Key>): void {
452
+ this.#retainedItems -= 1;
453
+ this.#retainedBytes -= pending.bytes;
454
+ this.#retainedUnits -= pending.units;
455
+ }
456
+
457
+ #scheduleGroupFlush(group: OpenGroup<Value, Result, Key>): void {
458
+ if (group.timer !== null) return;
459
+ group.timer = setTimeout(() => {
460
+ group.timer = null;
461
+ this.#sealGroup(group.keyId);
462
+ this.#pump();
463
+ }, this.#flushMs);
464
+ group.timer.unref?.();
465
+ }
466
+
467
+ #sealGroup(keyId: string): void {
468
+ const group = this.#groups.get(keyId);
469
+ if (!group) return;
470
+ this.#groups.delete(keyId);
471
+ if (group.timer !== null) clearTimeout(group.timer);
472
+ group.timer = null;
473
+ if (group.items.length === 0) return;
474
+ for (const pending of group.items) pending.state = 'ready';
475
+ this.#ready.push({
476
+ key: group.key,
477
+ keyId: group.keyId,
478
+ items: group.items,
479
+ bytes: group.bytes,
480
+ units: group.units,
481
+ });
482
+ this.#scheduleDeadlineTimer();
483
+ }
484
+
485
+ #pump(): void {
486
+ this.#expireQueued();
487
+ while (
488
+ this.#ready.length > 0 &&
489
+ this.#inFlight.size < this.#maxInFlightBatches
490
+ ) {
491
+ const batch = this.#ready.shift()!;
492
+ const items = batch.items.filter((pending) => pending.state === 'ready');
493
+ if (items.length === 0) continue;
494
+ for (const pending of items) pending.state = 'active';
495
+ this.#activeItems += items.length;
496
+ const active = this.#runBatch({ ...batch, items }).finally(() => {
497
+ this.#inFlight.delete(active);
498
+ this.#pump();
499
+ });
500
+ this.#inFlight.add(active);
501
+ }
502
+ this.#scheduleDeadlineTimer();
503
+ }
504
+
505
+ async #runBatch(batch: ReadyBatch<Value, Result, Key>): Promise<void> {
506
+ try {
507
+ const results = await this.#execute({
508
+ key: batch.key,
509
+ values: batch.items.map((pending) => pending.value),
510
+ signal: this.#closedController.signal,
511
+ });
512
+ if (results.length !== batch.items.length) {
513
+ throw new BoundedBatchResultCountError(
514
+ batch.items.length,
515
+ results.length,
516
+ );
517
+ }
518
+ for (let index = 0; index < batch.items.length; index += 1) {
519
+ const pending = batch.items[index]!;
520
+ if (!pending.settled) {
521
+ pending.settled = true;
522
+ pending.resolve(results[index]!);
523
+ }
524
+ }
525
+ } catch (error) {
526
+ for (const pending of batch.items) {
527
+ if (!pending.settled) {
528
+ pending.settled = true;
529
+ pending.reject(error);
530
+ }
531
+ }
532
+ } finally {
533
+ for (const pending of batch.items) {
534
+ pending.state = 'settled';
535
+ pending.signal?.removeEventListener('abort', pending.onAbort!);
536
+ this.#release(pending);
537
+ }
538
+ this.#activeItems -= batch.items.length;
539
+ }
540
+ }
541
+
542
+ #abortPending(pending: Pending<Value, Result, Key>): void {
543
+ if (pending.settled) return;
544
+ const error = abortReason(pending.signal!);
545
+ if (pending.state === 'active') {
546
+ // The operation may own durable effects for sibling callers. Detach this
547
+ // subscriber only; retained input is released when the shared batch ends.
548
+ pending.settled = true;
549
+ pending.reject(error);
550
+ return;
551
+ }
552
+ if (pending.state === 'queued') {
553
+ const group = this.#groups.get(pending.keyId);
554
+ if (group) this.#removeQueued(group, pending, error);
555
+ } else if (pending.state === 'ready') {
556
+ const batch = this.#ready.find((candidate) =>
557
+ candidate.items.includes(pending),
558
+ );
559
+ if (batch) this.#removeQueued(batch, pending, error);
560
+ }
561
+ this.#scheduleDeadlineTimer();
562
+ }
563
+
564
+ #removeQueued(
565
+ container: OpenGroup<Value, Result, Key> | ReadyBatch<Value, Result, Key>,
566
+ pending: Pending<Value, Result, Key>,
567
+ error: unknown,
568
+ ): void {
569
+ const index = container.items.indexOf(pending);
570
+ if (index < 0 || pending.settled) return;
571
+ container.items.splice(index, 1);
572
+ container.bytes -= pending.bytes;
573
+ container.units -= pending.units;
574
+ pending.state = 'settled';
575
+ pending.settled = true;
576
+ pending.signal?.removeEventListener('abort', pending.onAbort!);
577
+ this.#release(pending);
578
+ pending.reject(error);
579
+ if (container.items.length === 0 && 'timer' in container) {
580
+ if (container.timer !== null) clearTimeout(container.timer);
581
+ this.#groups.delete(container.keyId);
582
+ } else if (container.items.length === 0) {
583
+ const readyIndex = this.#ready.indexOf(container);
584
+ if (readyIndex >= 0) this.#ready.splice(readyIndex, 1);
585
+ }
586
+ }
587
+
588
+ #rejectContainer(
589
+ container: OpenGroup<Value, Result, Key> | ReadyBatch<Value, Result, Key>,
590
+ error: unknown,
591
+ ): void {
592
+ for (const pending of [...container.items]) {
593
+ if (!pending.settled) {
594
+ pending.state = 'settled';
595
+ pending.settled = true;
596
+ pending.signal?.removeEventListener('abort', pending.onAbort!);
597
+ this.#release(pending);
598
+ pending.reject(error);
599
+ }
600
+ }
601
+ container.items.length = 0;
602
+ if ('timer' in container && container.timer !== null) {
603
+ clearTimeout(container.timer);
604
+ container.timer = null;
605
+ }
606
+ }
607
+
608
+ #expireQueued(): void {
609
+ const now = this.#now();
610
+ const expired = (pending: Pending<Value, Result, Key>) =>
611
+ pending.deadlineAt !== null && pending.deadlineAt <= now;
612
+ for (const group of this.#groups.values()) {
613
+ for (const pending of [...group.items]) {
614
+ if (expired(pending)) {
615
+ this.#removeQueued(
616
+ group,
617
+ pending,
618
+ new BoundedBatchDeadlineError(
619
+ Math.max(0, now - pending.enqueuedAt),
620
+ ),
621
+ );
622
+ }
623
+ }
624
+ }
625
+ for (const batch of this.#ready) {
626
+ for (const pending of [...batch.items]) {
627
+ if (expired(pending)) {
628
+ this.#removeQueued(
629
+ batch,
630
+ pending,
631
+ new BoundedBatchDeadlineError(
632
+ Math.max(0, now - pending.enqueuedAt),
633
+ ),
634
+ );
635
+ }
636
+ }
637
+ }
638
+ for (let index = this.#ready.length - 1; index >= 0; index -= 1) {
639
+ if (this.#ready[index]!.items.length === 0) this.#ready.splice(index, 1);
640
+ }
641
+ }
642
+
643
+ #scheduleDeadlineTimer(): void {
644
+ this.#clearDeadlineTimer();
645
+ let nearest: number | null = null;
646
+ for (const group of this.#groups.values()) {
647
+ for (const pending of group.items) {
648
+ if (pending.deadlineAt !== null) {
649
+ nearest =
650
+ nearest === null
651
+ ? pending.deadlineAt
652
+ : Math.min(nearest, pending.deadlineAt);
653
+ }
654
+ }
655
+ }
656
+ for (const batch of this.#ready) {
657
+ for (const pending of batch.items) {
658
+ if (pending.deadlineAt !== null) {
659
+ nearest =
660
+ nearest === null
661
+ ? pending.deadlineAt
662
+ : Math.min(nearest, pending.deadlineAt);
663
+ }
664
+ }
665
+ }
666
+ if (nearest === null) return;
667
+ this.#deadlineTimer = setTimeout(
668
+ () => {
669
+ this.#deadlineTimer = null;
670
+ this.#expireQueued();
671
+ this.#scheduleDeadlineTimer();
672
+ },
673
+ Math.max(0, nearest - this.#now()),
674
+ );
675
+ this.#deadlineTimer.unref?.();
676
+ }
677
+
678
+ #clearDeadlineTimer(): void {
679
+ if (this.#deadlineTimer !== null) clearTimeout(this.#deadlineTimer);
680
+ this.#deadlineTimer = null;
681
+ }
682
+ }