@yielded/agent-platform-cloudflare 0.0.0-stage → 0.1.0-beta.167

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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Alarm-B0ODr1NE.mjs +1738 -0
  3. package/dist/Alarm-B0ODr1NE.mjs.map +1 -0
  4. package/dist/Alarm.d.mts +267 -0
  5. package/dist/Alarm.mjs +2 -0
  6. package/dist/BrowserCredentials.d.mts +91 -0
  7. package/dist/BrowserCredentials.mjs +120 -0
  8. package/dist/BrowserCredentials.mjs.map +1 -0
  9. package/dist/BrowserRestCapture.d.mts +34 -0
  10. package/dist/BrowserRestCapture.mjs +238 -0
  11. package/dist/BrowserRestCapture.mjs.map +1 -0
  12. package/dist/BrowserRestCrawl.d.mts +16 -0
  13. package/dist/BrowserRestCrawl.mjs +244 -0
  14. package/dist/BrowserRestCrawl.mjs.map +1 -0
  15. package/dist/BrowserSession-B969Xg-2.d.mts +132 -0
  16. package/dist/BrowserSession-RfIzxGCH.mjs +515 -0
  17. package/dist/BrowserSession-RfIzxGCH.mjs.map +1 -0
  18. package/dist/BrowserSession.d.mts +3 -0
  19. package/dist/BrowserSession.mjs +4 -0
  20. package/dist/CloudflareAiGateway.d.mts +64 -0
  21. package/dist/CloudflareAiGateway.mjs +70 -0
  22. package/dist/CloudflareAiGateway.mjs.map +1 -0
  23. package/dist/CloudflareBindings.d.mts +115 -0
  24. package/dist/CloudflareBindings.mjs +113 -0
  25. package/dist/CloudflareBindings.mjs.map +1 -0
  26. package/dist/CloudflareBrowser-5an6m-Qb.mjs +495 -0
  27. package/dist/CloudflareBrowser-5an6m-Qb.mjs.map +1 -0
  28. package/dist/CloudflareBrowser-gu7gma0Y.d.mts +80 -0
  29. package/dist/CloudflareBrowser.d.mts +2 -0
  30. package/dist/CloudflareBrowser.mjs +2 -0
  31. package/dist/CloudflareCodeMode.d.mts +44 -0
  32. package/dist/CloudflareCodeMode.mjs +616 -0
  33. package/dist/CloudflareCodeMode.mjs.map +1 -0
  34. package/dist/CloudflareConfig-qDrRLofL.d.mts +100 -0
  35. package/dist/CloudflareConfig.d.mts +2 -0
  36. package/dist/CloudflareConfig.mjs +122 -0
  37. package/dist/CloudflareConfig.mjs.map +1 -0
  38. package/dist/CloudflareMemory.d.mts +142 -0
  39. package/dist/CloudflareMemory.mjs +202 -0
  40. package/dist/CloudflareMemory.mjs.map +1 -0
  41. package/dist/CloudflareScheduling.d.mts +52 -0
  42. package/dist/CloudflareScheduling.mjs +396 -0
  43. package/dist/CloudflareScheduling.mjs.map +1 -0
  44. package/dist/CloudflareSubscriptions.d.mts +87 -0
  45. package/dist/CloudflareSubscriptions.mjs +529 -0
  46. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  47. package/dist/CloudflareThreadClient.d.mts +1796 -0
  48. package/dist/CloudflareThreadClient.mjs +382 -0
  49. package/dist/CloudflareThreadClient.mjs.map +1 -0
  50. package/dist/InteractiveBrowser-BbG3kGA9.mjs +1934 -0
  51. package/dist/InteractiveBrowser-BbG3kGA9.mjs.map +1 -0
  52. package/dist/InteractiveBrowser-wV2Bti5e.d.mts +228 -0
  53. package/dist/InteractiveBrowser.d.mts +2 -0
  54. package/dist/InteractiveBrowser.mjs +2 -0
  55. package/dist/ThreadObject-BcfXbzSO.d.mts +867 -0
  56. package/dist/ThreadObject-DHjrnlX4.mjs +937 -0
  57. package/dist/ThreadObject-DHjrnlX4.mjs.map +1 -0
  58. package/dist/ThreadObject.d.mts +2 -0
  59. package/dist/ThreadObject.mjs +3 -0
  60. package/dist/WakeScheduler.d.mts +23 -0
  61. package/dist/WakeScheduler.mjs +57 -0
  62. package/dist/WakeScheduler.mjs.map +1 -0
  63. package/dist/boundary-BguazVkh.mjs +42 -0
  64. package/dist/boundary-BguazVkh.mjs.map +1 -0
  65. package/dist/index.d.mts +15 -0
  66. package/dist/index.mjs +15 -0
  67. package/dist/prepared-admission-DMX5p9dC.mjs +73 -0
  68. package/dist/prepared-admission-DMX5p9dC.mjs.map +1 -0
  69. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  70. package/package.json +1 -6
  71. package/src/Alarm.ts +2961 -0
  72. package/src/BrowserCredentials.ts +146 -0
  73. package/src/BrowserRestCapture.ts +477 -0
  74. package/src/BrowserRestCrawl.ts +540 -0
  75. package/src/BrowserSession.ts +667 -0
  76. package/src/CloudflareAiGateway.ts +170 -0
  77. package/src/CloudflareBindings.ts +199 -0
  78. package/src/CloudflareBrowser.ts +15 -0
  79. package/src/CloudflareCodeMode.ts +878 -0
  80. package/src/CloudflareConfig.ts +138 -0
  81. package/src/CloudflareMemory.ts +420 -0
  82. package/src/CloudflareScheduling.ts +745 -0
  83. package/src/CloudflareSubscriptions.ts +1044 -0
  84. package/src/CloudflareThreadClient.ts +809 -0
  85. package/src/InteractiveBrowser.ts +3275 -0
  86. package/src/ThreadObject.ts +1198 -0
  87. package/src/WakeScheduler.ts +91 -0
  88. package/src/index.ts +14 -0
  89. package/src/internal/boundary.ts +55 -0
  90. package/src/internal/browser-binding.ts +322 -0
  91. package/src/internal/browser-credentials.ts +362 -0
  92. package/src/internal/browser-failure.ts +92 -0
  93. package/src/internal/browser-file-selection.ts +126 -0
  94. package/src/internal/browser-quick-action.ts +857 -0
  95. package/src/internal/browser-readonly-live-view.ts +111 -0
  96. package/src/internal/browser-session-lifecycle.ts +193 -0
  97. package/src/internal/due-queue.ts +493 -0
  98. package/src/internal/layers.ts +1068 -0
  99. package/src/internal/message-delivery.ts +120 -0
  100. package/src/internal/prepared-admission.ts +116 -0
  101. package/src/internal/progress-wait.ts +121 -0
  102. package/src/internal/transport.ts +56 -0
  103. package/README.md +0 -3
@@ -0,0 +1,493 @@
1
+ import { Effect, Exit, Scheduler, Schema } from "effect";
2
+ import { SqlClient } from "effect/sql/SqlClient";
3
+ import { SqlError, UnknownError } from "effect/sql/SqlError";
4
+
5
+ export const LaneId = Schema.NonEmptyString.check(Schema.isMaxLength(256));
6
+
7
+ export const HostLaneId = LaneId.check(
8
+ Schema.isPattern(/^(?!effect-agent:)/, {
9
+ message: "The effect-agent: namespace is reserved for framework maintenance lanes",
10
+ }),
11
+ );
12
+
13
+ export const Deadline = Schema.NullOr(Schema.Finite);
14
+ export const MinimumRetryMillis = 1_000;
15
+ export const MaximumNoProgressRearms = 8;
16
+ export const ParkedRetryMillis = 60 * 60_000;
17
+
18
+ /** Scheduling metadata only. Domain outboxes, claims and receipts remain authoritative. */
19
+ export class DueLane extends Schema.Class<DueLane>("CloudflareDueLane")({
20
+ id: LaneId,
21
+ revision: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
22
+ dueAt: Deadline,
23
+ stalls: Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 30 })),
24
+ progressKey: Schema.NullOr(Schema.NonEmptyString.check(Schema.isMaxLength(512))),
25
+ notBefore: Schema.Finite,
26
+ state: Schema.Literals(["idle", "pending", "parked"]),
27
+ reported: Schema.Literals([0, 1]),
28
+ }) {}
29
+
30
+ export const Native = "effect-agent:native";
31
+ export const Publication = "effect-agent:publication";
32
+ export const Projection = "effect-agent:projection";
33
+ export const Messages = "effect-agent:messages";
34
+ export const RecoveryEvents = "effect-agent:recovery-events";
35
+ export const Lifecycle = "effect-agent:lifecycle";
36
+
37
+ const decode = Schema.decodeUnknownSync(Schema.Array(DueLane));
38
+
39
+ const equivalent = Schema.toEquivalence(DueLane);
40
+
41
+ type Change = { readonly before: DueLane | undefined; readonly after: DueLane };
42
+ type Frame = {
43
+ readonly parent: Frame | undefined;
44
+ readonly sqlDepth: number | undefined;
45
+ readonly rows: ReadonlyArray<DueLane> | undefined;
46
+ readonly changes: Map<string, Change>;
47
+ readonly deadlines: Map<string, number>;
48
+ };
49
+ type View = {
50
+ rows: ReadonlyArray<DueLane> | undefined;
51
+ frame: Frame | undefined;
52
+ retained: number;
53
+ changes: Map<string, Change>;
54
+ deadlines: Map<string, number>;
55
+ };
56
+ const views = new WeakMap<DurableObjectStorage, View>();
57
+
58
+ /** Only the explicit source boundary may retain speculative rows across operations. */
59
+ export const buffering = (storage: DurableObjectStorage): boolean =>
60
+ views.get(storage)?.frame !== undefined;
61
+
62
+ export const ownsTransaction = (storage: DurableObjectStorage, depth: number): boolean =>
63
+ views.get(storage)?.frame?.sqlDepth === depth;
64
+
65
+ export const invalidate = (storage: DurableObjectStorage): void => {
66
+ const view = views.get(storage);
67
+
68
+ if (view !== undefined) view.rows = undefined;
69
+ };
70
+
71
+ /** Callers reserve the shared SQL connection through commit. Keep the complete view
72
+ * during a transaction, then retain at most 128 lanes between transactions. Intent
73
+ * flushes before source commit; claims and prearming commit before fallible work. */
74
+ export const make = (storage: DurableObjectStorage) => {
75
+ // Instrumentation may return a fresh SQL wrapper on every access.
76
+ const sql = storage.sql;
77
+ let view = views.get(storage);
78
+
79
+ if (view === undefined) {
80
+ view = {
81
+ rows: undefined,
82
+ frame: undefined,
83
+ retained: 0,
84
+ changes: new Map(),
85
+ deadlines: new Map(),
86
+ };
87
+ views.set(storage, view);
88
+ }
89
+ const current = view;
90
+
91
+ const trim = () => {
92
+ if (current.frame === undefined && current.retained === 0 && (current.rows?.length ?? 0) > 128)
93
+ current.rows = undefined;
94
+ };
95
+
96
+ /** Retain the pass's write-through view without deferring claims or source commits.
97
+ * Failed source transactions still invalidate it; large views are released at exit. */
98
+ const withView = <A, E, R>(body: Effect.Effect<A, E, R>) =>
99
+ Effect.acquireUseRelease(
100
+ Effect.sync(() => {
101
+ current.retained++;
102
+ }),
103
+ () => body,
104
+ () =>
105
+ Effect.sync(() => {
106
+ current.retained--;
107
+ trim();
108
+ }),
109
+ );
110
+
111
+ const begin = (sqlDepth?: number): Frame => {
112
+ const frame: Frame = {
113
+ parent: current.frame,
114
+ sqlDepth,
115
+ rows: current.rows,
116
+ changes: current.changes,
117
+ deadlines: current.deadlines,
118
+ };
119
+
120
+ current.frame = frame;
121
+ current.changes = new Map(current.changes);
122
+ current.deadlines = new Map(current.deadlines);
123
+
124
+ return frame;
125
+ };
126
+
127
+ const finish = (frame: Frame, success: boolean) => {
128
+ current.frame = frame.parent;
129
+ if (!success) {
130
+ // A child rollback must retain its parent's still-unflushed intent.
131
+ current.rows = frame.parent === undefined ? undefined : frame.rows;
132
+ current.changes = frame.changes;
133
+ current.deadlines = frame.deadlines;
134
+ }
135
+ if (frame.parent === undefined) {
136
+ current.changes.clear();
137
+ current.deadlines.clear();
138
+ trim();
139
+ }
140
+ };
141
+
142
+ // Nine bindings per lane stay below Durable Object SQLite's 100-parameter limit.
143
+ const write = (changes: ReadonlyArray<Change>) => {
144
+ const rows = decode(
145
+ sql
146
+ .exec(
147
+ `WITH changes (id, revision, dueAt, stalls, progressKey, notBefore, state, reported, expected_revision) AS (
148
+ VALUES ${changes.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ")}
149
+ ) INSERT INTO platform_cloudflare_due_queue
150
+ (id, revision, dueAt, stalls, progressKey, notBefore, state, reported)
151
+ SELECT id, revision, dueAt, stalls, progressKey, notBefore, state, reported FROM changes WHERE 1
152
+ ON CONFLICT(id) DO UPDATE SET revision = excluded.revision, dueAt = excluded.dueAt,
153
+ stalls = excluded.stalls, progressKey = excluded.progressKey,
154
+ notBefore = excluded.notBefore, state = excluded.state, reported = excluded.reported
155
+ WHERE platform_cloudflare_due_queue.revision = (
156
+ SELECT expected_revision FROM changes WHERE changes.id = excluded.id
157
+ ) RETURNING id, revision, dueAt, stalls, progressKey, notBefore, state, reported`,
158
+ ...changes.flatMap(({ before, after }) => [
159
+ after.id,
160
+ after.revision,
161
+ after.dueAt,
162
+ after.stalls,
163
+ after.progressKey,
164
+ after.notBefore,
165
+ after.state,
166
+ after.reported,
167
+ before?.revision ?? -1,
168
+ ]),
169
+ )
170
+ .toArray(),
171
+ );
172
+
173
+ // Partial revision acceptance must reject the enclosing source transaction.
174
+ if (rows.length !== changes.length)
175
+ throw new Error("Maintenance due revision changed during transaction");
176
+ };
177
+
178
+ const flush = (frame: Frame) => {
179
+ if (frame.parent !== undefined) return;
180
+
181
+ const changes = [...current.changes.values()].filter(
182
+ ({ before, after }) => before === undefined || !equivalent(before, after),
183
+ );
184
+
185
+ for (let offset = 0; offset < changes.length; offset += 11)
186
+ write(changes.slice(offset, offset + 11));
187
+ };
188
+
189
+ /** Own the flush inside the source transaction without changing the SQL service.
190
+ * Acquire its shared connection permit through Effect requirements. Cleanup follows
191
+ * native settlement, including commit rejection or interruption. */
192
+ const withTransaction = <A, E, R>(body: Effect.Effect<A, E, R>) =>
193
+ Effect.flatMap(SqlClient, (client) =>
194
+ Effect.flatMap(Effect.serviceOption(client.transactionService), (ambient) => {
195
+ if (ambient._tag === "Some" && !ownsTransaction(storage, ambient.value[1]))
196
+ return Effect.fail(
197
+ SqlError.make({
198
+ reason: UnknownError.make({
199
+ operation: "begin maintenance source transaction",
200
+ message:
201
+ "Use ThreadMutationGate.withTransaction at the outermost source transaction",
202
+ cause: new Error(
203
+ "Unwrapped SQL transaction cannot own buffered maintenance intent",
204
+ ),
205
+ }),
206
+ }),
207
+ );
208
+
209
+ return Effect.uninterruptibleMask((restore) =>
210
+ Effect.suspend(() => {
211
+ let frame: Frame | undefined;
212
+
213
+ return client
214
+ .withTransaction(
215
+ Effect.flatMap(Effect.serviceOption(client.transactionService), (transaction) =>
216
+ Effect.sync(() => {
217
+ if (transaction._tag === "None") throw new Error("SQL transaction unavailable");
218
+ frame = begin(transaction.value[1]);
219
+ }),
220
+ ).pipe(
221
+ Effect.andThen(restore(body)),
222
+ Effect.tap(() =>
223
+ Effect.try({
224
+ try: () => {
225
+ if (frame !== undefined) flush(frame);
226
+ },
227
+ catch: (cause) =>
228
+ SqlError.make({
229
+ reason: UnknownError.make({
230
+ cause,
231
+ operation: "flush maintenance due queue",
232
+ message: "Maintenance intent could not commit",
233
+ }),
234
+ }),
235
+ }),
236
+ ),
237
+ ),
238
+ )
239
+ .pipe(
240
+ Effect.onExit((exit) =>
241
+ Effect.sync(() => {
242
+ if (frame !== undefined) finish(frame, Exit.isSuccess(exit));
243
+ }),
244
+ ),
245
+ // A released permit can resume another transaction. Do not auto-yield
246
+ // between the driver's settlement and removing this transaction's frame.
247
+ Effect.provideService(Scheduler.PreventSchedulerYield, true),
248
+ );
249
+ }),
250
+ );
251
+ }),
252
+ );
253
+
254
+ const transaction = async <A>(
255
+ body: (transaction: DurableObjectTransaction) => Promise<A>,
256
+ ): Promise<A> => {
257
+ let frame: Frame | undefined;
258
+
259
+ try {
260
+ const result = await storage.transaction(async (transaction) => {
261
+ frame = begin();
262
+ const result = await body(transaction);
263
+
264
+ flush(frame);
265
+
266
+ return result;
267
+ });
268
+
269
+ if (frame !== undefined) finish(frame, true);
270
+
271
+ return result;
272
+ } catch (cause) {
273
+ if (frame !== undefined) finish(frame, false);
274
+ throw cause;
275
+ }
276
+ };
277
+
278
+ const initialize = () => {
279
+ sql.exec(`CREATE TABLE IF NOT EXISTS platform_cloudflare_due_queue (
280
+ id TEXT PRIMARY KEY, revision INTEGER NOT NULL, dueAt REAL, stalls INTEGER NOT NULL,
281
+ progressKey TEXT, notBefore REAL NOT NULL DEFAULT 0, state TEXT NOT NULL DEFAULT 'pending',
282
+ reported INTEGER NOT NULL DEFAULT 0
283
+ )`);
284
+
285
+ const columns = new Set(
286
+ sql
287
+ .exec("PRAGMA table_info(platform_cloudflare_due_queue)")
288
+ .toArray()
289
+ .map((row) => row.name),
290
+ );
291
+
292
+ if (!columns.has("reported"))
293
+ sql.exec(
294
+ "ALTER TABLE platform_cloudflare_due_queue ADD COLUMN reported INTEGER NOT NULL DEFAULT 0",
295
+ );
296
+ if (!columns.has("progressKey"))
297
+ sql.exec("ALTER TABLE platform_cloudflare_due_queue ADD COLUMN progressKey TEXT");
298
+ if (!columns.has("notBefore"))
299
+ sql.exec(
300
+ "ALTER TABLE platform_cloudflare_due_queue ADD COLUMN notBefore REAL NOT NULL DEFAULT 0",
301
+ );
302
+ if (!columns.has("state")) {
303
+ sql.exec(
304
+ "ALTER TABLE platform_cloudflare_due_queue ADD COLUMN state TEXT NOT NULL DEFAULT 'pending'",
305
+ );
306
+ sql.exec("UPDATE platform_cloudflare_due_queue SET state = 'idle' WHERE dueAt IS NULL");
307
+ }
308
+ };
309
+
310
+ /** Keep dormant rows so delete/reinsert cannot reuse a revision held by an older wave. */
311
+ const read = () => {
312
+ if (current.rows !== undefined) return current.rows;
313
+
314
+ const rows = decode(
315
+ sql
316
+ .exec(
317
+ "SELECT id, revision, dueAt, stalls, progressKey, notBefore, state, reported FROM platform_cloudflare_due_queue",
318
+ )
319
+ .toArray(),
320
+ );
321
+
322
+ if (current.frame !== undefined || current.retained > 0 || rows.length <= 128)
323
+ current.rows = rows;
324
+
325
+ return rows;
326
+ };
327
+
328
+ const set = (after: DueLane) => {
329
+ const rows = read();
330
+ const before = rows.find((row) => row.id === after.id);
331
+
332
+ if (before !== undefined && equivalent(before, after)) return before;
333
+ const previous = current.changes.get(after.id);
334
+ const change = { before: previous === undefined ? before : previous.before, after };
335
+
336
+ if (current.frame === undefined) write([change]);
337
+ else current.changes.set(after.id, change);
338
+ current.rows = [...rows.filter((row) => row.id !== after.id), after];
339
+ trim();
340
+
341
+ return after;
342
+ };
343
+
344
+ const dirty = (id: string, dueAt: number, progress = false, progressKey?: string) => {
345
+ Schema.decodeSync(LaneId)(id);
346
+ Schema.decodeSync(Schema.Finite)(dueAt);
347
+ const row = read().find((row) => row.id === id);
348
+ const earliest = Math.min(current.deadlines.get(id) ?? dueAt, dueAt);
349
+
350
+ if (current.frame !== undefined) current.deadlines.set(id, earliest);
351
+
352
+ return set(
353
+ DueLane.make({
354
+ id,
355
+ revision: row === undefined ? 1 : row.revision + Number(progress),
356
+ dueAt:
357
+ row === undefined || progress
358
+ ? earliest
359
+ : Math.max(row.notBefore, Math.min(row.dueAt ?? Infinity, earliest)),
360
+ state:
361
+ progress || (row?.stalls ?? 0) < MaximumNoProgressRearms
362
+ ? "pending"
363
+ : (row?.state ?? "pending"),
364
+ stalls: progress ? 0 : (row?.stalls ?? 0),
365
+ notBefore: progress ? 0 : (row?.notBefore ?? 0),
366
+ reported: progress ? 0 : (row?.reported ?? 0),
367
+ progressKey: progressKey ?? row?.progressKey ?? null,
368
+ }),
369
+ );
370
+ };
371
+
372
+ /** A strictly increasing source cursor; old or repeated notices cannot renew a budget. */
373
+ const progress = (id: string, dueAt: number, cursor: bigint) => {
374
+ Schema.decodeSync(Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(0n)))(cursor);
375
+ const previous = read().find((row) => row.id === id)?.progressKey;
376
+
377
+ if (previous !== undefined && previous !== null && cursor <= BigInt(previous)) return false;
378
+ dirty(id, dueAt, true, String(cursor));
379
+
380
+ return true;
381
+ };
382
+
383
+ /** One initial native discovery wave; host lanes always require explicit enrollment. */
384
+ const register = (id: string) => {
385
+ if (read().some((row) => row.id === id)) return;
386
+ set(
387
+ DueLane.make({
388
+ id,
389
+ revision: 0,
390
+ dueAt: 0,
391
+ stalls: 0,
392
+ progressKey: null,
393
+ notBefore: 0,
394
+ state: "pending",
395
+ reported: 0,
396
+ }),
397
+ );
398
+ };
399
+
400
+ /** This transaction must commit before fallible work: never defer a debit across a wave. */
401
+ const claim = (lane: DueLane, nowMillis: number, native = false) => {
402
+ const row = read().find((row) => row.id === lane.id);
403
+
404
+ if (
405
+ row === undefined ||
406
+ row.revision !== lane.revision ||
407
+ row.dueAt === null ||
408
+ !(row.dueAt <= nowMillis || (native && row.notBefore <= nowMillis))
409
+ )
410
+ return;
411
+ const stalls = Math.min(row.stalls + 1, MaximumNoProgressRearms);
412
+
413
+ const notBefore =
414
+ nowMillis +
415
+ (stalls >= MaximumNoProgressRearms
416
+ ? ParkedRetryMillis
417
+ : Math.min(60_000, MinimumRetryMillis * 2 ** row.stalls));
418
+
419
+ return set(
420
+ DueLane.make({
421
+ ...row,
422
+ stalls,
423
+ notBefore,
424
+ dueAt: notBefore,
425
+ state: stalls >= MaximumNoProgressRearms ? "parked" : "pending",
426
+ }),
427
+ );
428
+ };
429
+
430
+ const complete = (lane: DueLane, dueAt: number | null) => {
431
+ Schema.decodeSync(Deadline)(dueAt);
432
+ const row = read().find((row) => row.id === lane.id);
433
+
434
+ if (row === undefined || row.revision !== lane.revision) return;
435
+
436
+ const after = DueLane.make({
437
+ ...row,
438
+ dueAt: dueAt === null ? null : Math.max(dueAt, row.notBefore),
439
+ state: dueAt === null ? "idle" : row.stalls >= MaximumNoProgressRearms ? "parked" : "pending",
440
+ stalls: dueAt === null ? 0 : row.stalls,
441
+ notBefore: dueAt === null ? 0 : row.notBefore,
442
+ reported: dueAt === null ? 0 : row.reported,
443
+ });
444
+
445
+ return set(DueLane.make({ ...after, revision: row.revision + 1 }));
446
+ };
447
+
448
+ /** Native checkpoints already fence their own dirty/processed generation. */
449
+ const checkpointNative = (dueAt: number | null) => {
450
+ Schema.decodeSync(Deadline)(dueAt);
451
+ const rows = read();
452
+ const row = rows.find((row) => row.id === Native);
453
+
454
+ if (row === undefined) return;
455
+ const publication = rows.find((row) => row.id === Publication);
456
+
457
+ set(
458
+ DueLane.make({
459
+ ...row,
460
+ dueAt:
461
+ dueAt === null || publication?.state === "parked" ? null : Math.max(dueAt, row.notBefore),
462
+ state:
463
+ dueAt === null ? "idle" : row.stalls >= MaximumNoProgressRearms ? "parked" : "pending",
464
+ stalls: dueAt === null ? 0 : row.stalls,
465
+ notBefore: dueAt === null ? 0 : row.notBefore,
466
+ reported: dueAt === null ? 0 : row.reported,
467
+ }),
468
+ );
469
+ };
470
+
471
+ const takeParkedReports = () =>
472
+ read()
473
+ .filter((row) => row.state === "parked" && row.reported === 0)
474
+ .map((row) => set(DueLane.make({ ...row, reported: 1 })));
475
+
476
+ return {
477
+ initialize,
478
+ withView,
479
+ withTransaction,
480
+ transaction,
481
+ read,
482
+ dirty,
483
+ progress,
484
+ register,
485
+ claim,
486
+ complete,
487
+ checkpointNative,
488
+ takeParkedReports,
489
+ };
490
+ };
491
+
492
+ export const next = (rows: ReadonlyArray<DueLane>) =>
493
+ rows.reduce((next, row) => Math.min(next, row.dueAt ?? Infinity), Infinity);