@iterativeflow/conformance 2.0.0-alpha.1 → 2.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/index.d.mts +21 -5
- package/dist/index.mjs +804 -120
- package/package.json +10 -3
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @iterativeflow/conformance
|
|
2
|
+
|
|
3
|
+
The backend-authoring kit for [iterativeflow](https://github.com/ahmedrowaihi/iterativeflow)
|
|
4
|
+
v2. To add a substrate, implement the four ports (store/queue/timer/wakeup) from
|
|
5
|
+
`@iterativeflow/core/backend` and prove them against these shared suites — the
|
|
6
|
+
same tests the built-in memory, Postgres, and DynamoDB backends pass. A real
|
|
7
|
+
serialization or consistency regression can't hide behind a backend's own tests.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -D @iterativeflow/conformance @iterativeflow/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { storeConformance, engineConformance } from "@iterativeflow/conformance";
|
|
15
|
+
import { createMyBackend } from "./backend";
|
|
16
|
+
|
|
17
|
+
storeConformance("my-backend", () => createMyBackend());
|
|
18
|
+
engineConformance("my-backend", () => createMyBackend());
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Each suite registers `describe`/`it` blocks (vitest), so call them at the top
|
|
22
|
+
level of a test file.
|
|
23
|
+
|
|
24
|
+
## Suites
|
|
25
|
+
|
|
26
|
+
- **Port suites** — `storeConformance`, `queueConformance`, `timerConformance`,
|
|
27
|
+
`wakeupConformance`: each port's contract in isolation.
|
|
28
|
+
- `outboxConformance` — the atomic multi-port checkpoint write.
|
|
29
|
+
- `signalConformance`, `reconcileConformance`, `cronConformance` — the durable
|
|
30
|
+
behaviors built on the ports.
|
|
31
|
+
- `engineConformance` — the composed engine (retry/dead-letter, signal resume,
|
|
32
|
+
cancel cascade, fan-out, drift) end-to-end on your backend.
|
|
33
|
+
|
|
34
|
+
Every suite takes `(label, makeBackend)` where `makeBackend` returns a fresh
|
|
35
|
+
`Backend` (or a `Promise` of one) per case.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Backend
|
|
1
|
+
import { Backend } from "@iterativeflow/core";
|
|
2
|
+
import { Backend as Backend$1, Queue, Store, Timer, Wakeup } from "@iterativeflow/core/backend";
|
|
2
3
|
//#region src/store.d.ts
|
|
3
4
|
/**
|
|
4
5
|
* The Store contract as executable invariants. EVERY backend (in-memory, Postgres,
|
|
@@ -39,7 +40,7 @@ declare const wakeupConformance: (label: string, makeWakeup: () => Wakeup | Prom
|
|
|
39
40
|
* satisfy this; it is the property that makes crash recovery correct rather than just
|
|
40
41
|
* plausible.
|
|
41
42
|
*/
|
|
42
|
-
declare const outboxConformance: (label: string, makeBackend: () => Backend | Promise<Backend>) => void;
|
|
43
|
+
declare const outboxConformance: (label: string, makeBackend: () => Backend$1 | Promise<Backend$1>) => void;
|
|
43
44
|
//#endregion
|
|
44
45
|
//#region src/signal.d.ts
|
|
45
46
|
/**
|
|
@@ -47,7 +48,7 @@ declare const outboxConformance: (label: string, makeBackend: () => Backend | Pr
|
|
|
47
48
|
* wakes the run (re-enqueue), is idempotent on its key, and is consumed exactly once by a
|
|
48
49
|
* checkpoint's `consumeSignals` outbox.
|
|
49
50
|
*/
|
|
50
|
-
declare const signalConformance: (label: string, makeBackend: () => Backend | Promise<Backend>) => void;
|
|
51
|
+
declare const signalConformance: (label: string, makeBackend: () => Backend$1 | Promise<Backend$1>) => void;
|
|
51
52
|
//#endregion
|
|
52
53
|
//#region src/reconcile.d.ts
|
|
53
54
|
/**
|
|
@@ -56,7 +57,7 @@ declare const signalConformance: (label: string, makeBackend: () => Backend | Pr
|
|
|
56
57
|
* pending timer, OR an `awaiting_signal` parent whose child already finished (lost wake). A
|
|
57
58
|
* run that is queued, timing, terminal, or legitimately waiting on an external signal is NOT.
|
|
58
59
|
*/
|
|
59
|
-
declare const reconcileConformance: (label: string, makeBackend: () => Backend | Promise<Backend>) => void;
|
|
60
|
+
declare const reconcileConformance: (label: string, makeBackend: () => Backend$1 | Promise<Backend$1>) => void;
|
|
60
61
|
//#endregion
|
|
61
62
|
//#region src/cron.d.ts
|
|
62
63
|
/**
|
|
@@ -66,4 +67,19 @@ declare const reconcileConformance: (label: string, makeBackend: () => Backend |
|
|
|
66
67
|
*/
|
|
67
68
|
declare const cronConformance: (label: string, makeStore: () => Store | Promise<Store>) => void;
|
|
68
69
|
//#endregion
|
|
69
|
-
|
|
70
|
+
//#region src/engine.d.ts
|
|
71
|
+
/**
|
|
72
|
+
* Engine-behavior conformance: the composed durable behaviors (retry/dead-letter, signal
|
|
73
|
+
* resume, cancel cascade, drift) run against every backend, not just memory — so a real
|
|
74
|
+
* serialization/consistency regression can't hide behind the port-conformance suites.
|
|
75
|
+
*/
|
|
76
|
+
declare const engineConformance: (label: string, makeBackend: () => Backend | Promise<Backend>) => void;
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/claim-filter.d.ts
|
|
79
|
+
/**
|
|
80
|
+
* `ClaimOpts.names` as executable invariants: a sharded worker leases only runs whose flow `name`
|
|
81
|
+
* it registered. Needs the store (to create named runs), so it takes the whole backend.
|
|
82
|
+
*/
|
|
83
|
+
declare const claimFilterConformance: (label: string, makeBackend: () => Backend$1 | Promise<Backend$1>) => void;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { claimFilterConformance, cronConformance, engineConformance, outboxConformance, queueConformance, reconcileConformance, signalConformance, storeConformance, timerConformance, wakeupConformance };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { builder, cancelRun, defineFlow, reconcile, registry, signalRun, signalType, submit, tickOnce } from "@iterativeflow/core";
|
|
3
|
+
import { isTerminal } from "@iterativeflow/core/backend";
|
|
2
4
|
//#region src/store.ts
|
|
3
5
|
/**
|
|
4
6
|
* The Store contract as executable invariants. EVERY backend (in-memory, Postgres,
|
|
@@ -75,6 +77,22 @@ const storeConformance = (label, makeStore) => {
|
|
|
75
77
|
expect(results[1].created).toBe(true);
|
|
76
78
|
expect(results[1].runId).not.toBe(first.runId);
|
|
77
79
|
});
|
|
80
|
+
it("startManyRuns dedups a key that repeats WITHIN the batch — one run, not a throw", async () => {
|
|
81
|
+
const results = await (await makeStore()).startManyRuns([{
|
|
82
|
+
name: "f",
|
|
83
|
+
version: 1,
|
|
84
|
+
input: {},
|
|
85
|
+
idempotencyKey: "same"
|
|
86
|
+
}, {
|
|
87
|
+
name: "f",
|
|
88
|
+
version: 1,
|
|
89
|
+
input: {},
|
|
90
|
+
idempotencyKey: "same"
|
|
91
|
+
}]);
|
|
92
|
+
expect(results).toHaveLength(2);
|
|
93
|
+
expect(results[0].runId).toBe(results[1].runId);
|
|
94
|
+
expect([results[0].created, results[1].created].filter(Boolean)).toHaveLength(1);
|
|
95
|
+
});
|
|
78
96
|
it("startRun is idempotent on the idempotency key — no duplicate run", async () => {
|
|
79
97
|
const s = await makeStore();
|
|
80
98
|
const a = await s.startRun({
|
|
@@ -111,6 +129,73 @@ const storeConformance = (label, makeStore) => {
|
|
|
111
129
|
it("loadRun returns undefined for an unknown run", async () => {
|
|
112
130
|
expect(await (await makeStore()).loadRun("00000000-0000-0000-0000-000000000000")).toBeUndefined();
|
|
113
131
|
});
|
|
132
|
+
it("loadRunRow returns just the run row (no step/signal maps), undefined if gone", async () => {
|
|
133
|
+
const s = await makeStore();
|
|
134
|
+
const { runId } = await s.startRun({
|
|
135
|
+
name: "f",
|
|
136
|
+
version: 1,
|
|
137
|
+
input: { a: 1 }
|
|
138
|
+
});
|
|
139
|
+
await s.checkpointStep({
|
|
140
|
+
runId,
|
|
141
|
+
cursorKey: "a",
|
|
142
|
+
status: "ok",
|
|
143
|
+
result: 1,
|
|
144
|
+
attempts: 1
|
|
145
|
+
});
|
|
146
|
+
const row = await s.loadRunRow(runId);
|
|
147
|
+
expect(row).toMatchObject({
|
|
148
|
+
id: runId,
|
|
149
|
+
status: "pending"
|
|
150
|
+
});
|
|
151
|
+
expect(row).not.toHaveProperty("steps");
|
|
152
|
+
expect(row).not.toHaveProperty("signals");
|
|
153
|
+
expect(await s.loadRunRow("00000000-0000-0000-0000-000000000000")).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
it("loadRunRows returns rows aligned to the input ids, undefined where gone", async () => {
|
|
156
|
+
const s = await makeStore();
|
|
157
|
+
const a = await s.startRun({
|
|
158
|
+
name: "f",
|
|
159
|
+
version: 1,
|
|
160
|
+
input: { n: 1 }
|
|
161
|
+
});
|
|
162
|
+
const b = await s.startRun({
|
|
163
|
+
name: "f",
|
|
164
|
+
version: 1,
|
|
165
|
+
input: { n: 2 }
|
|
166
|
+
});
|
|
167
|
+
const rows = await s.loadRunRows([
|
|
168
|
+
a.runId,
|
|
169
|
+
"00000000-0000-0000-0000-000000000000",
|
|
170
|
+
b.runId
|
|
171
|
+
]);
|
|
172
|
+
expect(rows).toHaveLength(3);
|
|
173
|
+
expect(rows[0]?.id).toBe(a.runId);
|
|
174
|
+
expect(rows[1]).toBeUndefined();
|
|
175
|
+
expect(rows[2]?.id).toBe(b.runId);
|
|
176
|
+
expect(await s.loadRunRows([])).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
it("arriveAtJoin decrements the armed join countdown, and sentinels a gone parent", async () => {
|
|
179
|
+
const s = await makeStore();
|
|
180
|
+
const { runId } = await s.startRun({
|
|
181
|
+
name: "p",
|
|
182
|
+
version: 1,
|
|
183
|
+
input: {}
|
|
184
|
+
});
|
|
185
|
+
await s.checkpointStep({
|
|
186
|
+
runId,
|
|
187
|
+
cursorKey: "s0",
|
|
188
|
+
status: "ok",
|
|
189
|
+
result: [],
|
|
190
|
+
attempts: 1
|
|
191
|
+
}, { joinTarget: {
|
|
192
|
+
runId,
|
|
193
|
+
count: 2
|
|
194
|
+
} });
|
|
195
|
+
expect(await s.arriveAtJoin(runId)).toBe(1);
|
|
196
|
+
expect(await s.arriveAtJoin(runId)).toBe(0);
|
|
197
|
+
expect(await s.arriveAtJoin("00000000-0000-0000-0000-000000000000")).toBeUndefined();
|
|
198
|
+
});
|
|
114
199
|
it("checkpointStep persists a completed step into the memo", async () => {
|
|
115
200
|
const s = await makeStore();
|
|
116
201
|
const { runId } = await s.startRun({
|
|
@@ -131,6 +216,23 @@ const storeConformance = (label, makeStore) => {
|
|
|
131
216
|
attempts: 1
|
|
132
217
|
});
|
|
133
218
|
});
|
|
219
|
+
it("checkpointStep round-trips the shape tag (drift-guard evidence)", async () => {
|
|
220
|
+
const s = await makeStore();
|
|
221
|
+
const { runId } = await s.startRun({
|
|
222
|
+
name: "f",
|
|
223
|
+
version: 1,
|
|
224
|
+
input: {}
|
|
225
|
+
});
|
|
226
|
+
await s.checkpointStep({
|
|
227
|
+
runId,
|
|
228
|
+
cursorKey: "a",
|
|
229
|
+
status: "ok",
|
|
230
|
+
result: 1,
|
|
231
|
+
attempts: 1,
|
|
232
|
+
shape: "step:charge"
|
|
233
|
+
});
|
|
234
|
+
expect((await s.loadRun(runId))?.steps.get("a")?.shape).toBe("step:charge");
|
|
235
|
+
});
|
|
134
236
|
it("checkpointStep is first-writer-wins — a second write does NOT overwrite (exactly-once memo)", async () => {
|
|
135
237
|
const s = await makeStore();
|
|
136
238
|
const { runId } = await s.startRun({
|
|
@@ -167,6 +269,22 @@ const storeConformance = (label, makeStore) => {
|
|
|
167
269
|
expect(await s.markRunning(runId)).toBe(2);
|
|
168
270
|
expect((await s.loadRun(runId))?.run.attempts).toBe(2);
|
|
169
271
|
});
|
|
272
|
+
it("a forward-progress suspend zeroes the dispatch counter; retrying keeps it", async () => {
|
|
273
|
+
const s = await makeStore();
|
|
274
|
+
const { runId } = await s.startRun({
|
|
275
|
+
name: "f",
|
|
276
|
+
version: 1,
|
|
277
|
+
input: {}
|
|
278
|
+
});
|
|
279
|
+
await s.markRunning(runId);
|
|
280
|
+
await s.markRunning(runId);
|
|
281
|
+
await s.suspendRun(runId, "retrying");
|
|
282
|
+
expect((await s.loadRunRow(runId))?.attempts).toBe(2);
|
|
283
|
+
await s.markRunning(runId);
|
|
284
|
+
await s.suspendRun(runId, "sleeping");
|
|
285
|
+
expect((await s.loadRunRow(runId))?.attempts).toBe(0);
|
|
286
|
+
expect(await s.markRunning(runId)).toBe(1);
|
|
287
|
+
});
|
|
170
288
|
it("markRunning never resurrects a terminal run (a late re-dispatch can't undo a cancel)", async () => {
|
|
171
289
|
const s = await makeStore();
|
|
172
290
|
const { runId } = await s.startRun({
|
|
@@ -207,56 +325,69 @@ const storeConformance = (label, makeStore) => {
|
|
|
207
325
|
});
|
|
208
326
|
expect((await s.loadRun(runId))?.run.status).toBe("canceled");
|
|
209
327
|
});
|
|
210
|
-
it("
|
|
328
|
+
it("deleteRunsOlderThan prunes terminal runs (with steps + signals) before the cutoff, spares live ones", async () => {
|
|
211
329
|
const s = await makeStore();
|
|
212
|
-
const
|
|
330
|
+
const old = /* @__PURE__ */ new Date("2020-01-01T00:00:00.000Z");
|
|
331
|
+
const doomed = await s.startRun({
|
|
213
332
|
name: "f",
|
|
214
333
|
version: 1,
|
|
215
|
-
input: {}
|
|
334
|
+
input: {},
|
|
335
|
+
createdAt: old
|
|
216
336
|
});
|
|
217
337
|
await s.checkpointStep({
|
|
218
|
-
runId,
|
|
338
|
+
runId: doomed.runId,
|
|
219
339
|
cursorKey: "a",
|
|
220
|
-
status: "
|
|
221
|
-
|
|
222
|
-
code: "BOOM",
|
|
223
|
-
message: "boom"
|
|
224
|
-
},
|
|
340
|
+
status: "ok",
|
|
341
|
+
result: 1,
|
|
225
342
|
attempts: 1
|
|
226
343
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
344
|
+
await s.postSignal(doomed.runId, "go", { ok: true });
|
|
345
|
+
await s.markTerminal(doomed.runId, {
|
|
346
|
+
status: "done",
|
|
347
|
+
output: 1
|
|
348
|
+
});
|
|
349
|
+
const live = await s.startRun({
|
|
350
|
+
name: "f",
|
|
351
|
+
version: 1,
|
|
352
|
+
input: {},
|
|
353
|
+
createdAt: old
|
|
233
354
|
});
|
|
355
|
+
expect((await s.loadRunRow(live.runId))?.createdAt?.getTime()).toBe(old.getTime());
|
|
356
|
+
expect(await s.deleteRunsOlderThan(/* @__PURE__ */ new Date("2020-06-01T00:00:00Z"), 100)).toBe(1);
|
|
357
|
+
expect(await s.loadRun(doomed.runId)).toBeUndefined();
|
|
358
|
+
expect((await s.loadRun(live.runId))?.run.status).toBe("pending");
|
|
234
359
|
});
|
|
235
|
-
it("
|
|
360
|
+
it("deleteRunsOlderThan honours the cutoff and the per-sweep limit", async () => {
|
|
236
361
|
const s = await makeStore();
|
|
237
|
-
const
|
|
362
|
+
const old = /* @__PURE__ */ new Date("2020-01-01T00:00:00.000Z");
|
|
363
|
+
const a = await s.startRun({
|
|
238
364
|
name: "f",
|
|
239
365
|
version: 1,
|
|
240
|
-
input: {}
|
|
366
|
+
input: {},
|
|
367
|
+
createdAt: old
|
|
241
368
|
});
|
|
242
|
-
await s.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
369
|
+
const b = await s.startRun({
|
|
370
|
+
name: "f",
|
|
371
|
+
version: 1,
|
|
372
|
+
input: {},
|
|
373
|
+
createdAt: old
|
|
374
|
+
});
|
|
375
|
+
await s.markTerminal(a.runId, {
|
|
376
|
+
status: "done",
|
|
377
|
+
output: 1
|
|
378
|
+
});
|
|
379
|
+
await s.markTerminal(b.runId, {
|
|
380
|
+
status: "failed",
|
|
246
381
|
error: {
|
|
247
|
-
code: "
|
|
248
|
-
message: "
|
|
249
|
-
}
|
|
250
|
-
attempts: 1
|
|
382
|
+
code: "X",
|
|
383
|
+
message: "x"
|
|
384
|
+
}
|
|
251
385
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
attempts: 2
|
|
258
|
-
})).status).toBe("failed_terminal");
|
|
259
|
-
expect((await s.loadRun(runId))?.steps.get("a")?.status).toBe("failed_terminal");
|
|
386
|
+
const after = /* @__PURE__ */ new Date("2020-06-01T00:00:00Z");
|
|
387
|
+
expect(await s.deleteRunsOlderThan(/* @__PURE__ */ new Date("2019-01-01T00:00:00Z"), 100)).toBe(0);
|
|
388
|
+
expect(await s.deleteRunsOlderThan(after, 1)).toBe(1);
|
|
389
|
+
expect(await s.deleteRunsOlderThan(after, 1)).toBe(1);
|
|
390
|
+
expect(await s.deleteRunsOlderThan(after, 1)).toBe(0);
|
|
260
391
|
});
|
|
261
392
|
it("checkpointStep on an unknown run is rejected (no orphan step)", async () => {
|
|
262
393
|
await expect((await makeStore()).checkpointStep({
|
|
@@ -477,7 +608,7 @@ const queueConformance = (label, makeQueue) => {
|
|
|
477
608
|
const q = await makeQueue();
|
|
478
609
|
await q.enqueue("r1");
|
|
479
610
|
const leases = await q.claim({
|
|
480
|
-
|
|
611
|
+
limit: 10,
|
|
481
612
|
leaseMs: 1e3,
|
|
482
613
|
now: at(0)
|
|
483
614
|
});
|
|
@@ -485,16 +616,41 @@ const queueConformance = (label, makeQueue) => {
|
|
|
485
616
|
expect(leases[0].token).toBeTruthy();
|
|
486
617
|
expect(leases[0].expiresAt.getTime()).toBe(at(1e3).getTime());
|
|
487
618
|
});
|
|
619
|
+
it("depth reports backlog, in-flight, and the oldest-claimable age", async () => {
|
|
620
|
+
const q = await makeQueue();
|
|
621
|
+
expect(await q.depth(at(0))).toEqual({
|
|
622
|
+
claimable: 0,
|
|
623
|
+
leased: 0,
|
|
624
|
+
oldestClaimableAgeMs: null
|
|
625
|
+
});
|
|
626
|
+
await q.enqueue("r1", { runAt: at(0) });
|
|
627
|
+
await q.enqueue("r2", { runAt: at(5e3) });
|
|
628
|
+
expect(await q.depth(at(1e3))).toEqual({
|
|
629
|
+
claimable: 1,
|
|
630
|
+
leased: 0,
|
|
631
|
+
oldestClaimableAgeMs: 1e3
|
|
632
|
+
});
|
|
633
|
+
expect((await q.claim({
|
|
634
|
+
limit: 10,
|
|
635
|
+
leaseMs: 1e3,
|
|
636
|
+
now: at(1e3)
|
|
637
|
+
})).map((l) => l.runId)).toEqual(["r1"]);
|
|
638
|
+
expect(await q.depth(at(1e3))).toEqual({
|
|
639
|
+
claimable: 0,
|
|
640
|
+
leased: 1,
|
|
641
|
+
oldestClaimableAgeMs: null
|
|
642
|
+
});
|
|
643
|
+
});
|
|
488
644
|
it("a future runAt is not claimable until due", async () => {
|
|
489
645
|
const q = await makeQueue();
|
|
490
646
|
await q.enqueue("r1", { runAt: at(5e3) });
|
|
491
647
|
expect(await q.claim({
|
|
492
|
-
|
|
648
|
+
limit: 10,
|
|
493
649
|
leaseMs: 1e3,
|
|
494
650
|
now: at(0)
|
|
495
651
|
})).toEqual([]);
|
|
496
652
|
expect((await q.claim({
|
|
497
|
-
|
|
653
|
+
limit: 10,
|
|
498
654
|
leaseMs: 1e3,
|
|
499
655
|
now: at(5e3)
|
|
500
656
|
})).map((l) => l.runId)).toEqual(["r1"]);
|
|
@@ -503,12 +659,12 @@ const queueConformance = (label, makeQueue) => {
|
|
|
503
659
|
const q = await makeQueue();
|
|
504
660
|
await q.enqueue("r1");
|
|
505
661
|
expect(await q.claim({
|
|
506
|
-
|
|
662
|
+
limit: 10,
|
|
507
663
|
leaseMs: 1e3,
|
|
508
664
|
now: at(0)
|
|
509
665
|
})).toHaveLength(1);
|
|
510
666
|
expect(await q.claim({
|
|
511
|
-
|
|
667
|
+
limit: 10,
|
|
512
668
|
leaseMs: 1e3,
|
|
513
669
|
now: at(500)
|
|
514
670
|
})).toEqual([]);
|
|
@@ -517,12 +673,12 @@ const queueConformance = (label, makeQueue) => {
|
|
|
517
673
|
const q = await makeQueue();
|
|
518
674
|
await q.enqueue("r1");
|
|
519
675
|
const first = await q.claim({
|
|
520
|
-
|
|
676
|
+
limit: 10,
|
|
521
677
|
leaseMs: 1e3,
|
|
522
678
|
now: at(0)
|
|
523
679
|
});
|
|
524
680
|
const reclaim = await q.claim({
|
|
525
|
-
|
|
681
|
+
limit: 10,
|
|
526
682
|
leaseMs: 1e3,
|
|
527
683
|
now: at(1001)
|
|
528
684
|
});
|
|
@@ -533,7 +689,7 @@ const queueConformance = (label, makeQueue) => {
|
|
|
533
689
|
const q = await makeQueue();
|
|
534
690
|
await q.enqueue("r1");
|
|
535
691
|
const [lease] = await q.claim({
|
|
536
|
-
|
|
692
|
+
limit: 10,
|
|
537
693
|
leaseMs: 1e3,
|
|
538
694
|
now: at(0)
|
|
539
695
|
});
|
|
@@ -542,12 +698,12 @@ const queueConformance = (label, makeQueue) => {
|
|
|
542
698
|
now: at(500)
|
|
543
699
|
});
|
|
544
700
|
expect(await q.claim({
|
|
545
|
-
|
|
701
|
+
limit: 10,
|
|
546
702
|
leaseMs: 1e3,
|
|
547
703
|
now: at(1001)
|
|
548
704
|
})).toEqual([]);
|
|
549
705
|
expect((await q.claim({
|
|
550
|
-
|
|
706
|
+
limit: 10,
|
|
551
707
|
leaseMs: 1e3,
|
|
552
708
|
now: at(1501)
|
|
553
709
|
})).map((l) => l.runId)).toEqual(["r1"]);
|
|
@@ -556,12 +712,12 @@ const queueConformance = (label, makeQueue) => {
|
|
|
556
712
|
const q = await makeQueue();
|
|
557
713
|
await q.enqueue("r1");
|
|
558
714
|
const [stale] = await q.claim({
|
|
559
|
-
|
|
715
|
+
limit: 10,
|
|
560
716
|
leaseMs: 1e3,
|
|
561
717
|
now: at(0)
|
|
562
718
|
});
|
|
563
719
|
await q.claim({
|
|
564
|
-
|
|
720
|
+
limit: 10,
|
|
565
721
|
leaseMs: 1e3,
|
|
566
722
|
now: at(1001)
|
|
567
723
|
});
|
|
@@ -574,13 +730,13 @@ const queueConformance = (label, makeQueue) => {
|
|
|
574
730
|
const q = await makeQueue();
|
|
575
731
|
await q.enqueue("r1");
|
|
576
732
|
const [lease] = await q.claim({
|
|
577
|
-
|
|
733
|
+
limit: 10,
|
|
578
734
|
leaseMs: 1e3,
|
|
579
735
|
now: at(0)
|
|
580
736
|
});
|
|
581
737
|
await q.ack(lease, { now: at(1) });
|
|
582
738
|
expect(await q.claim({
|
|
583
|
-
|
|
739
|
+
limit: 10,
|
|
584
740
|
leaseMs: 1e3,
|
|
585
741
|
now: at(2e3)
|
|
586
742
|
})).toEqual([]);
|
|
@@ -589,18 +745,18 @@ const queueConformance = (label, makeQueue) => {
|
|
|
589
745
|
const q = await makeQueue();
|
|
590
746
|
await q.enqueue("r1");
|
|
591
747
|
const [stale] = await q.claim({
|
|
592
|
-
|
|
748
|
+
limit: 10,
|
|
593
749
|
leaseMs: 1e3,
|
|
594
750
|
now: at(0)
|
|
595
751
|
});
|
|
596
752
|
await q.claim({
|
|
597
|
-
|
|
753
|
+
limit: 10,
|
|
598
754
|
leaseMs: 1e3,
|
|
599
755
|
now: at(1001)
|
|
600
756
|
});
|
|
601
757
|
await q.ack(stale, { now: at(1002) });
|
|
602
758
|
expect((await q.claim({
|
|
603
|
-
|
|
759
|
+
limit: 10,
|
|
604
760
|
leaseMs: 1e3,
|
|
605
761
|
now: at(2002)
|
|
606
762
|
})).map((l) => l.runId)).toEqual(["r1"]);
|
|
@@ -609,14 +765,14 @@ const queueConformance = (label, makeQueue) => {
|
|
|
609
765
|
const q = await makeQueue();
|
|
610
766
|
await q.enqueue("r1");
|
|
611
767
|
const [lease] = await q.claim({
|
|
612
|
-
|
|
768
|
+
limit: 10,
|
|
613
769
|
leaseMs: 1e3,
|
|
614
770
|
now: at(0)
|
|
615
771
|
});
|
|
616
772
|
await q.enqueue("r1");
|
|
617
773
|
await q.ack(lease, { now: at(1) });
|
|
618
774
|
expect((await q.claim({
|
|
619
|
-
|
|
775
|
+
limit: 10,
|
|
620
776
|
leaseMs: 1e3,
|
|
621
777
|
now: at(2)
|
|
622
778
|
})).map((l) => l.runId)).toEqual(["r1"]);
|
|
@@ -625,13 +781,13 @@ const queueConformance = (label, makeQueue) => {
|
|
|
625
781
|
const q = await makeQueue();
|
|
626
782
|
await q.enqueue("r1");
|
|
627
783
|
const [lease] = await q.claim({
|
|
628
|
-
|
|
784
|
+
limit: 10,
|
|
629
785
|
leaseMs: 1e3,
|
|
630
786
|
now: at(0)
|
|
631
787
|
});
|
|
632
788
|
await q.ack(lease, { now: at(1001) });
|
|
633
789
|
expect((await q.claim({
|
|
634
|
-
|
|
790
|
+
limit: 10,
|
|
635
791
|
leaseMs: 1e3,
|
|
636
792
|
now: at(1002)
|
|
637
793
|
})).map((l) => l.runId)).toEqual(["r1"]);
|
|
@@ -640,7 +796,7 @@ const queueConformance = (label, makeQueue) => {
|
|
|
640
796
|
const q = await makeQueue();
|
|
641
797
|
await q.enqueue("r1");
|
|
642
798
|
const [lease] = await q.claim({
|
|
643
|
-
|
|
799
|
+
limit: 10,
|
|
644
800
|
leaseMs: 1e3,
|
|
645
801
|
now: at(0)
|
|
646
802
|
});
|
|
@@ -653,59 +809,22 @@ const queueConformance = (label, makeQueue) => {
|
|
|
653
809
|
const q = await makeQueue();
|
|
654
810
|
await q.enqueue("r1");
|
|
655
811
|
await q.claim({
|
|
656
|
-
|
|
812
|
+
limit: 10,
|
|
657
813
|
leaseMs: 1e3,
|
|
658
814
|
now: at(0)
|
|
659
815
|
});
|
|
660
816
|
await q.enqueue("r1", { priority: 1 });
|
|
661
817
|
expect(await q.claim({
|
|
662
|
-
|
|
818
|
+
limit: 10,
|
|
663
819
|
leaseMs: 1e3,
|
|
664
820
|
now: at(500)
|
|
665
821
|
})).toEqual([]);
|
|
666
822
|
});
|
|
667
|
-
it("
|
|
668
|
-
const q = await makeQueue();
|
|
669
|
-
await q.enqueue("r1");
|
|
670
|
-
const [lease] = await q.claim({
|
|
671
|
-
max: 10,
|
|
672
|
-
leaseMs: 1e3,
|
|
673
|
-
now: at(0)
|
|
674
|
-
});
|
|
675
|
-
await q.release(lease, { now: at(100) });
|
|
676
|
-
const again = await q.claim({
|
|
677
|
-
max: 10,
|
|
678
|
-
leaseMs: 1e3,
|
|
679
|
-
now: at(200)
|
|
680
|
-
});
|
|
681
|
-
expect(again.map((l) => l.runId)).toEqual(["r1"]);
|
|
682
|
-
expect(again[0].token).not.toBe(lease.token);
|
|
683
|
-
});
|
|
684
|
-
it("a stale release is a no-op — it can't unseat the current owner", async () => {
|
|
685
|
-
const q = await makeQueue();
|
|
686
|
-
await q.enqueue("r1");
|
|
687
|
-
const [stale] = await q.claim({
|
|
688
|
-
max: 10,
|
|
689
|
-
leaseMs: 1e3,
|
|
690
|
-
now: at(0)
|
|
691
|
-
});
|
|
692
|
-
await q.claim({
|
|
693
|
-
max: 10,
|
|
694
|
-
leaseMs: 1e3,
|
|
695
|
-
now: at(1001)
|
|
696
|
-
});
|
|
697
|
-
await q.release(stale, { now: at(1002) });
|
|
698
|
-
expect(await q.claim({
|
|
699
|
-
max: 10,
|
|
700
|
-
leaseMs: 1e3,
|
|
701
|
-
now: at(1003)
|
|
702
|
-
})).toEqual([]);
|
|
703
|
-
});
|
|
704
|
-
it("claim honours the batch max", async () => {
|
|
823
|
+
it("claim honours the batch limit", async () => {
|
|
705
824
|
const q = await makeQueue();
|
|
706
825
|
for (let i = 0; i < 5; i++) await q.enqueue(`r${i}`);
|
|
707
826
|
expect(await q.claim({
|
|
708
|
-
|
|
827
|
+
limit: 2,
|
|
709
828
|
leaseMs: 1e3,
|
|
710
829
|
now: at(0)
|
|
711
830
|
})).toHaveLength(2);
|
|
@@ -715,7 +834,7 @@ const queueConformance = (label, makeQueue) => {
|
|
|
715
834
|
await q.enqueue("low", { priority: 10 });
|
|
716
835
|
await q.enqueue("high", { priority: 1 });
|
|
717
836
|
const [first] = await q.claim({
|
|
718
|
-
|
|
837
|
+
limit: 1,
|
|
719
838
|
leaseMs: 1e3,
|
|
720
839
|
now: at(0)
|
|
721
840
|
});
|
|
@@ -726,7 +845,7 @@ const queueConformance = (label, makeQueue) => {
|
|
|
726
845
|
await q.enqueue("r1");
|
|
727
846
|
await q.enqueue("r1", { priority: 5 });
|
|
728
847
|
expect((await q.claim({
|
|
729
|
-
|
|
848
|
+
limit: 10,
|
|
730
849
|
leaseMs: 1e3,
|
|
731
850
|
now: at(0)
|
|
732
851
|
})).map((l) => l.runId)).toEqual(["r1"]);
|
|
@@ -746,11 +865,11 @@ const timerConformance = (label, makeTimer) => {
|
|
|
746
865
|
await tm.schedule("r1", at(1e3));
|
|
747
866
|
expect(await tm.dueBatch({
|
|
748
867
|
now: at(999),
|
|
749
|
-
|
|
868
|
+
limit: 10
|
|
750
869
|
})).toEqual([]);
|
|
751
870
|
expect(await tm.dueBatch({
|
|
752
871
|
now: at(1e3),
|
|
753
|
-
|
|
872
|
+
limit: 10
|
|
754
873
|
})).toEqual(["r1"]);
|
|
755
874
|
});
|
|
756
875
|
it("dueBatch fires exactly once — a consumed timer is not returned again", async () => {
|
|
@@ -758,11 +877,11 @@ const timerConformance = (label, makeTimer) => {
|
|
|
758
877
|
await tm.schedule("r1", at(0));
|
|
759
878
|
expect(await tm.dueBatch({
|
|
760
879
|
now: at(1),
|
|
761
|
-
|
|
880
|
+
limit: 10
|
|
762
881
|
})).toEqual(["r1"]);
|
|
763
882
|
expect(await tm.dueBatch({
|
|
764
883
|
now: at(2),
|
|
765
|
-
|
|
884
|
+
limit: 10
|
|
766
885
|
})).toEqual([]);
|
|
767
886
|
});
|
|
768
887
|
it("reschedule is an upsert — the latest fireAt wins", async () => {
|
|
@@ -771,11 +890,11 @@ const timerConformance = (label, makeTimer) => {
|
|
|
771
890
|
await tm.schedule("r1", at(5e3));
|
|
772
891
|
expect(await tm.dueBatch({
|
|
773
892
|
now: at(2e3),
|
|
774
|
-
|
|
893
|
+
limit: 10
|
|
775
894
|
})).toEqual([]);
|
|
776
895
|
expect(await tm.dueBatch({
|
|
777
896
|
now: at(5e3),
|
|
778
|
-
|
|
897
|
+
limit: 10
|
|
779
898
|
})).toEqual(["r1"]);
|
|
780
899
|
});
|
|
781
900
|
it("reschedule EARLIER also wins — upsert, not a LEAST(fire_at) merge", async () => {
|
|
@@ -784,7 +903,7 @@ const timerConformance = (label, makeTimer) => {
|
|
|
784
903
|
await tm.schedule("r1", at(1e3));
|
|
785
904
|
expect(await tm.dueBatch({
|
|
786
905
|
now: at(1e3),
|
|
787
|
-
|
|
906
|
+
limit: 10
|
|
788
907
|
})).toEqual(["r1"]);
|
|
789
908
|
});
|
|
790
909
|
it("cancel removes a pending timer", async () => {
|
|
@@ -793,19 +912,40 @@ const timerConformance = (label, makeTimer) => {
|
|
|
793
912
|
await tm.cancel("r1");
|
|
794
913
|
expect(await tm.dueBatch({
|
|
795
914
|
now: at(2e3),
|
|
796
|
-
|
|
915
|
+
limit: 10
|
|
797
916
|
})).toEqual([]);
|
|
798
917
|
});
|
|
799
|
-
it("dueBatch honours
|
|
918
|
+
it("dueBatch honours limit and returns earliest-first", async () => {
|
|
800
919
|
const tm = await makeTimer();
|
|
801
920
|
await tm.schedule("late", at(300));
|
|
802
921
|
await tm.schedule("early", at(100));
|
|
803
922
|
await tm.schedule("mid", at(200));
|
|
804
923
|
expect(await tm.dueBatch({
|
|
805
924
|
now: at(1e3),
|
|
806
|
-
|
|
925
|
+
limit: 2
|
|
807
926
|
})).toEqual(["early", "mid"]);
|
|
808
927
|
});
|
|
928
|
+
it("nextDueAt is null when nothing is pending", async () => {
|
|
929
|
+
expect(await (await makeTimer()).nextDueAt(at(0))).toBeNull();
|
|
930
|
+
});
|
|
931
|
+
it("nextDueAt returns the earliest timer due strictly after now", async () => {
|
|
932
|
+
const tm = await makeTimer();
|
|
933
|
+
await tm.schedule("late", at(3e3));
|
|
934
|
+
await tm.schedule("early", at(1e3));
|
|
935
|
+
await tm.schedule("mid", at(2e3));
|
|
936
|
+
expect(await tm.nextDueAt(at(500))).toEqual(at(1e3));
|
|
937
|
+
expect(await tm.nextDueAt(at(1e3))).toEqual(at(2e3));
|
|
938
|
+
expect(await tm.nextDueAt(at(3e3))).toBeNull();
|
|
939
|
+
});
|
|
940
|
+
it("nextDueAt reflects reschedule and cancel", async () => {
|
|
941
|
+
const tm = await makeTimer();
|
|
942
|
+
await tm.schedule("r1", at(5e3));
|
|
943
|
+
expect(await tm.nextDueAt(at(0))).toEqual(at(5e3));
|
|
944
|
+
await tm.schedule("r1", at(1e3));
|
|
945
|
+
expect(await tm.nextDueAt(at(0))).toEqual(at(1e3));
|
|
946
|
+
await tm.cancel("r1");
|
|
947
|
+
expect(await tm.nextDueAt(at(0))).toBeNull();
|
|
948
|
+
});
|
|
809
949
|
});
|
|
810
950
|
};
|
|
811
951
|
//#endregion
|
|
@@ -893,7 +1033,7 @@ const outboxConformance = (label, makeBackend) => {
|
|
|
893
1033
|
attempts: 1
|
|
894
1034
|
}, { enqueue: [{ runId }] });
|
|
895
1035
|
expect((await queue.claim({
|
|
896
|
-
|
|
1036
|
+
limit: 10,
|
|
897
1037
|
leaseMs: 1e3,
|
|
898
1038
|
now: at(0)
|
|
899
1039
|
})).map((l) => l.runId)).toEqual([runId]);
|
|
@@ -922,7 +1062,7 @@ const outboxConformance = (label, makeBackend) => {
|
|
|
922
1062
|
}] });
|
|
923
1063
|
expect((await store.loadRun(childId))?.run.input).toEqual({ n: 1 });
|
|
924
1064
|
expect((await queue.claim({
|
|
925
|
-
|
|
1065
|
+
limit: 10,
|
|
926
1066
|
leaseMs: 1e3,
|
|
927
1067
|
now: at(0)
|
|
928
1068
|
})).map((l) => l.runId)).toContain(childId);
|
|
@@ -958,7 +1098,7 @@ const outboxConformance = (label, makeBackend) => {
|
|
|
958
1098
|
attempts: 2
|
|
959
1099
|
}, spawn);
|
|
960
1100
|
expect((await queue.claim({
|
|
961
|
-
|
|
1101
|
+
limit: 10,
|
|
962
1102
|
leaseMs: 1e3,
|
|
963
1103
|
now: at(0)
|
|
964
1104
|
})).filter((l) => l.runId === childId)).toHaveLength(1);
|
|
@@ -978,11 +1118,11 @@ const outboxConformance = (label, makeBackend) => {
|
|
|
978
1118
|
expect((await store.loadRun(runId))?.run.status).toBe("sleeping");
|
|
979
1119
|
expect(await timer.dueBatch({
|
|
980
1120
|
now: at(4999),
|
|
981
|
-
|
|
1121
|
+
limit: 10
|
|
982
1122
|
})).toEqual([]);
|
|
983
1123
|
expect(await timer.dueBatch({
|
|
984
1124
|
now: at(5e3),
|
|
985
|
-
|
|
1125
|
+
limit: 10
|
|
986
1126
|
})).toEqual([runId]);
|
|
987
1127
|
});
|
|
988
1128
|
it("suspendRun is a no-op on a terminal run — no zombie wake timer", async () => {
|
|
@@ -1003,7 +1143,7 @@ const outboxConformance = (label, makeBackend) => {
|
|
|
1003
1143
|
expect((await store.loadRun(runId))?.run.status).toBe("done");
|
|
1004
1144
|
expect(await timer.dueBatch({
|
|
1005
1145
|
now: at(5e3),
|
|
1006
|
-
|
|
1146
|
+
limit: 10
|
|
1007
1147
|
})).toEqual([]);
|
|
1008
1148
|
});
|
|
1009
1149
|
it("markTerminal wakes a suspended parent atomically (enqueue in the same write)", async () => {
|
|
@@ -1024,7 +1164,7 @@ const outboxConformance = (label, makeBackend) => {
|
|
|
1024
1164
|
output: 7
|
|
1025
1165
|
}, { enqueue: [{ runId: parent.runId }] });
|
|
1026
1166
|
expect((await queue.claim({
|
|
1027
|
-
|
|
1167
|
+
limit: 10,
|
|
1028
1168
|
leaseMs: 1e3,
|
|
1029
1169
|
now: at(0)
|
|
1030
1170
|
})).map((l) => l.runId)).toContain(parent.runId);
|
|
@@ -1047,7 +1187,7 @@ const outboxConformance = (label, makeBackend) => {
|
|
|
1047
1187
|
}, { cancelTimers: [runId] });
|
|
1048
1188
|
expect(await timer.dueBatch({
|
|
1049
1189
|
now: at(5e3),
|
|
1050
|
-
|
|
1190
|
+
limit: 10
|
|
1051
1191
|
})).toEqual([]);
|
|
1052
1192
|
});
|
|
1053
1193
|
it("a canceled run's markTerminal is sticky AND skips its outbox", async () => {
|
|
@@ -1064,7 +1204,7 @@ const outboxConformance = (label, makeBackend) => {
|
|
|
1064
1204
|
}, { enqueue: [{ runId }] });
|
|
1065
1205
|
expect((await store.loadRun(runId))?.run.status).toBe("canceled");
|
|
1066
1206
|
expect(await queue.claim({
|
|
1067
|
-
|
|
1207
|
+
limit: 10,
|
|
1068
1208
|
leaseMs: 1e3,
|
|
1069
1209
|
now: at(0)
|
|
1070
1210
|
})).toEqual([]);
|
|
@@ -1096,7 +1236,7 @@ const signalConformance = (label, makeBackend) => {
|
|
|
1096
1236
|
payload: { ok: true }
|
|
1097
1237
|
}]);
|
|
1098
1238
|
expect((await queue.claim({
|
|
1099
|
-
|
|
1239
|
+
limit: 10,
|
|
1100
1240
|
leaseMs: 1e3,
|
|
1101
1241
|
now: at(0)
|
|
1102
1242
|
})).map((l) => l.runId)).toEqual([runId]);
|
|
@@ -1135,6 +1275,37 @@ const signalConformance = (label, makeBackend) => {
|
|
|
1135
1275
|
expect(after?.signals.map((s) => s.name)).toEqual(["b"]);
|
|
1136
1276
|
expect(after?.steps.get("wait")?.result).toBe("first");
|
|
1137
1277
|
});
|
|
1278
|
+
it("requireVersion commits while the job version is unchanged, refuses once a racing signal moves it", async () => {
|
|
1279
|
+
const { store, queue } = await makeBackend();
|
|
1280
|
+
const { runId } = await store.startRun({
|
|
1281
|
+
name: "f",
|
|
1282
|
+
version: 1,
|
|
1283
|
+
input: {}
|
|
1284
|
+
});
|
|
1285
|
+
await queue.enqueue(runId);
|
|
1286
|
+
const [lease] = await queue.claim({
|
|
1287
|
+
limit: 1,
|
|
1288
|
+
leaseMs: 1e3,
|
|
1289
|
+
now: at(0)
|
|
1290
|
+
});
|
|
1291
|
+
expect((await store.checkpointStep({
|
|
1292
|
+
runId,
|
|
1293
|
+
cursorKey: "res",
|
|
1294
|
+
status: "ok",
|
|
1295
|
+
result: { received: false },
|
|
1296
|
+
attempts: 1
|
|
1297
|
+
}, { requireVersion: lease.version })).committed).not.toBe(false);
|
|
1298
|
+
expect((await store.loadRun(runId))?.steps.get("res")?.result).toEqual({ received: false });
|
|
1299
|
+
await store.postSignal(runId, "go", "payload");
|
|
1300
|
+
expect((await store.checkpointStep({
|
|
1301
|
+
runId,
|
|
1302
|
+
cursorKey: "res2",
|
|
1303
|
+
status: "ok",
|
|
1304
|
+
result: { received: false },
|
|
1305
|
+
attempts: 1
|
|
1306
|
+
}, { requireVersion: lease.version })).committed).toBe(false);
|
|
1307
|
+
expect((await store.loadRun(runId))?.steps.get("res2")).toBeUndefined();
|
|
1308
|
+
});
|
|
1138
1309
|
it("distinct signals to a run preserve delivery order in the inbox", async () => {
|
|
1139
1310
|
const { store } = await makeBackend();
|
|
1140
1311
|
const { runId } = await store.startRun({
|
|
@@ -1228,6 +1399,61 @@ const reconcileConformance = (label, makeBackend) => {
|
|
|
1228
1399
|
});
|
|
1229
1400
|
expect(await store.orphanedRuns(10)).toContain(parent.runId);
|
|
1230
1401
|
});
|
|
1402
|
+
it("a fan-out parent with a still-running child is NOT an orphan (join not resolved)", async () => {
|
|
1403
|
+
const { store } = await makeBackend();
|
|
1404
|
+
const parent = await store.startRun({
|
|
1405
|
+
name: "p",
|
|
1406
|
+
version: 1,
|
|
1407
|
+
input: {}
|
|
1408
|
+
});
|
|
1409
|
+
const kids = await store.startManyRuns([{
|
|
1410
|
+
name: "c",
|
|
1411
|
+
version: 1,
|
|
1412
|
+
input: {},
|
|
1413
|
+
parentRunId: parent.runId
|
|
1414
|
+
}, {
|
|
1415
|
+
name: "c",
|
|
1416
|
+
version: 1,
|
|
1417
|
+
input: {},
|
|
1418
|
+
parentRunId: parent.runId
|
|
1419
|
+
}]);
|
|
1420
|
+
await store.markRunning(parent.runId);
|
|
1421
|
+
await store.suspendRun(parent.runId, "awaiting_child");
|
|
1422
|
+
await store.markTerminal(kids[0].runId, {
|
|
1423
|
+
status: "done",
|
|
1424
|
+
output: 1
|
|
1425
|
+
});
|
|
1426
|
+
expect(await store.orphanedRuns(10)).not.toContain(parent.runId);
|
|
1427
|
+
});
|
|
1428
|
+
it("a fan-out parent with a FAILED child IS an orphan even while siblings run (fast-fail)", async () => {
|
|
1429
|
+
const { store } = await makeBackend();
|
|
1430
|
+
const parent = await store.startRun({
|
|
1431
|
+
name: "p",
|
|
1432
|
+
version: 1,
|
|
1433
|
+
input: {}
|
|
1434
|
+
});
|
|
1435
|
+
const kids = await store.startManyRuns([{
|
|
1436
|
+
name: "c",
|
|
1437
|
+
version: 1,
|
|
1438
|
+
input: {},
|
|
1439
|
+
parentRunId: parent.runId
|
|
1440
|
+
}, {
|
|
1441
|
+
name: "c",
|
|
1442
|
+
version: 1,
|
|
1443
|
+
input: {},
|
|
1444
|
+
parentRunId: parent.runId
|
|
1445
|
+
}]);
|
|
1446
|
+
await store.markRunning(parent.runId);
|
|
1447
|
+
await store.suspendRun(parent.runId, "awaiting_child");
|
|
1448
|
+
await store.markTerminal(kids[0].runId, {
|
|
1449
|
+
status: "failed",
|
|
1450
|
+
error: {
|
|
1451
|
+
code: "X",
|
|
1452
|
+
message: "boom"
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
expect(await store.orphanedRuns(10)).toContain(parent.runId);
|
|
1456
|
+
});
|
|
1231
1457
|
it("a terminal run is never an orphan", async () => {
|
|
1232
1458
|
const { store } = await makeBackend();
|
|
1233
1459
|
const { runId } = await store.startRun({
|
|
@@ -1300,4 +1526,462 @@ const cronConformance = (label, makeStore) => {
|
|
|
1300
1526
|
});
|
|
1301
1527
|
};
|
|
1302
1528
|
//#endregion
|
|
1303
|
-
|
|
1529
|
+
//#region src/engine.ts
|
|
1530
|
+
/**
|
|
1531
|
+
* Engine-behavior conformance: the composed durable behaviors (retry/dead-letter, signal
|
|
1532
|
+
* resume, cancel cascade, drift) run against every backend, not just memory — so a real
|
|
1533
|
+
* serialization/consistency regression can't hide behind the port-conformance suites.
|
|
1534
|
+
*/
|
|
1535
|
+
const engineConformance = (label, makeBackend) => {
|
|
1536
|
+
describe(`engine conformance (${label})`, () => {
|
|
1537
|
+
const base = {
|
|
1538
|
+
batchMax: 16,
|
|
1539
|
+
leaseMs: 6e5
|
|
1540
|
+
};
|
|
1541
|
+
const drive = async (backend, flows, runId, retry) => {
|
|
1542
|
+
let clock = /* @__PURE__ */ new Date("2030-01-01T00:00:00Z");
|
|
1543
|
+
const now = () => clock;
|
|
1544
|
+
for (let i = 0; i < 200; i++) {
|
|
1545
|
+
await tickOnce(backend, flows, {
|
|
1546
|
+
...base,
|
|
1547
|
+
now,
|
|
1548
|
+
retry
|
|
1549
|
+
});
|
|
1550
|
+
const run = await backend.store.loadRunRow(runId);
|
|
1551
|
+
if (run && isTerminal(run.status)) return run;
|
|
1552
|
+
clock = new Date(clock.getTime() + 6e4);
|
|
1553
|
+
}
|
|
1554
|
+
throw new Error("run did not settle");
|
|
1555
|
+
};
|
|
1556
|
+
it("a run dispatched far more than maxAttempts times (many sleeps) completes", async () => {
|
|
1557
|
+
const backend = await makeBackend();
|
|
1558
|
+
const flow = defineFlow({
|
|
1559
|
+
name: "poller",
|
|
1560
|
+
version: 1,
|
|
1561
|
+
run: async (ctx) => {
|
|
1562
|
+
for (let i = 0; i < 6; i++) await ctx.sleep(1e3);
|
|
1563
|
+
return "settled";
|
|
1564
|
+
}
|
|
1565
|
+
});
|
|
1566
|
+
const runId = await submit(backend, flow, {});
|
|
1567
|
+
expect(await drive(backend, registry([flow]), runId, {
|
|
1568
|
+
maxAttempts: 3,
|
|
1569
|
+
baseDelayMs: 1,
|
|
1570
|
+
maxDelayMs: 1
|
|
1571
|
+
})).toMatchObject({
|
|
1572
|
+
status: "done",
|
|
1573
|
+
output: "settled"
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
it("runs a builder flow with a durable sleep to completion", async () => {
|
|
1577
|
+
const backend = await makeBackend();
|
|
1578
|
+
const flow = builder(`${label}-sleep`, 1).step("doubled", (acc) => acc.input.x * 2).step("nap", async (_acc, ctx) => {
|
|
1579
|
+
await ctx.sleep(5e3);
|
|
1580
|
+
return "rested";
|
|
1581
|
+
}).output((acc) => ({
|
|
1582
|
+
doubled: acc.doubled,
|
|
1583
|
+
nap: acc.nap
|
|
1584
|
+
}));
|
|
1585
|
+
const runId = await submit(backend, flow, { x: 21 });
|
|
1586
|
+
expect(await drive(backend, registry([flow]), runId)).toMatchObject({
|
|
1587
|
+
status: "done",
|
|
1588
|
+
output: {
|
|
1589
|
+
doubled: 42,
|
|
1590
|
+
nap: "rested"
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
});
|
|
1594
|
+
it("a genuinely failing step still dead-letters after its retry budget", async () => {
|
|
1595
|
+
const backend = await makeBackend();
|
|
1596
|
+
const flow = defineFlow({
|
|
1597
|
+
name: "always-throws",
|
|
1598
|
+
version: 1,
|
|
1599
|
+
run: async (ctx) => ctx.step("boom", () => {
|
|
1600
|
+
throw new Error("nope");
|
|
1601
|
+
})
|
|
1602
|
+
});
|
|
1603
|
+
const runId = await submit(backend, flow, {});
|
|
1604
|
+
expect((await drive(backend, registry([flow]), runId, {
|
|
1605
|
+
maxAttempts: 2,
|
|
1606
|
+
baseDelayMs: 1,
|
|
1607
|
+
maxDelayMs: 1
|
|
1608
|
+
})).status).toBe("failed");
|
|
1609
|
+
});
|
|
1610
|
+
it("dead-letters a poison-pill (uncatchable-crash) run once attempts exceed the cap", async () => {
|
|
1611
|
+
const backend = await makeBackend();
|
|
1612
|
+
const flow = defineFlow({
|
|
1613
|
+
name: "poison",
|
|
1614
|
+
version: 1,
|
|
1615
|
+
run: async () => 1
|
|
1616
|
+
});
|
|
1617
|
+
const runId = await submit(backend, flow, {});
|
|
1618
|
+
for (let i = 0; i < 5; i++) await backend.store.markRunning(runId);
|
|
1619
|
+
await tickOnce(backend, registry([flow]), {
|
|
1620
|
+
...base,
|
|
1621
|
+
now: () => /* @__PURE__ */ new Date("2030-01-01T00:00:00Z"),
|
|
1622
|
+
retry: {
|
|
1623
|
+
maxAttempts: 3,
|
|
1624
|
+
baseDelayMs: 1,
|
|
1625
|
+
maxDelayMs: 1
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
const run = await backend.store.loadRunRow(runId);
|
|
1629
|
+
expect(run?.status).toBe("failed");
|
|
1630
|
+
expect(run?.error?.code).toBe("RUN_ATTEMPTS_EXHAUSTED");
|
|
1631
|
+
});
|
|
1632
|
+
it("awaits an external signal, parks, and resumes with its payload", async () => {
|
|
1633
|
+
const backend = await makeBackend();
|
|
1634
|
+
const flow = defineFlow({
|
|
1635
|
+
name: "approve",
|
|
1636
|
+
version: 1,
|
|
1637
|
+
signals: { go: signalType() },
|
|
1638
|
+
run: async (ctx) => {
|
|
1639
|
+
return (await ctx.signal("go")).ok ? "shipped" : "held";
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
const flows = registry([flow]);
|
|
1643
|
+
const runId = await submit(backend, flow, {});
|
|
1644
|
+
await tickOnce(backend, flows, {
|
|
1645
|
+
...base,
|
|
1646
|
+
now: () => /* @__PURE__ */ new Date("2030-01-01T00:00:00Z")
|
|
1647
|
+
});
|
|
1648
|
+
expect((await backend.store.loadRunRow(runId))?.status).toBe("awaiting_signal");
|
|
1649
|
+
await signalRun(backend, runId, "go", { ok: true });
|
|
1650
|
+
expect(await drive(backend, flows, runId)).toMatchObject({
|
|
1651
|
+
status: "done",
|
|
1652
|
+
output: "shipped"
|
|
1653
|
+
});
|
|
1654
|
+
});
|
|
1655
|
+
it("cancels a run and cascades to grandchildren", async () => {
|
|
1656
|
+
const backend = await makeBackend();
|
|
1657
|
+
const grandchild = defineFlow({
|
|
1658
|
+
name: "gc",
|
|
1659
|
+
version: 1,
|
|
1660
|
+
run: async (ctx) => {
|
|
1661
|
+
await ctx.sleep(1e7);
|
|
1662
|
+
return "gc";
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
const child = defineFlow({
|
|
1666
|
+
name: "c",
|
|
1667
|
+
version: 1,
|
|
1668
|
+
run: async (ctx) => ctx.invoke(grandchild, {})
|
|
1669
|
+
});
|
|
1670
|
+
const parent = defineFlow({
|
|
1671
|
+
name: "p",
|
|
1672
|
+
version: 1,
|
|
1673
|
+
run: async (ctx) => ctx.invoke(child, {})
|
|
1674
|
+
});
|
|
1675
|
+
const flows = registry([
|
|
1676
|
+
parent,
|
|
1677
|
+
child,
|
|
1678
|
+
grandchild
|
|
1679
|
+
]);
|
|
1680
|
+
const parentId = await submit(backend, parent, {});
|
|
1681
|
+
let clock = /* @__PURE__ */ new Date("2030-01-01T00:00:00Z");
|
|
1682
|
+
for (let i = 0; i < 10; i++) {
|
|
1683
|
+
await tickOnce(backend, flows, {
|
|
1684
|
+
...base,
|
|
1685
|
+
now: () => clock
|
|
1686
|
+
});
|
|
1687
|
+
clock = new Date(clock.getTime() + 1e3);
|
|
1688
|
+
}
|
|
1689
|
+
await cancelRun(backend, parentId);
|
|
1690
|
+
const kids = await backend.store.childrenOf(parentId);
|
|
1691
|
+
expect(kids.length).toBeGreaterThan(0);
|
|
1692
|
+
const childId = kids[0].id;
|
|
1693
|
+
const grandkids = await backend.store.childrenOf(childId);
|
|
1694
|
+
expect((await backend.store.loadRunRow(parentId))?.status).toBe("canceled");
|
|
1695
|
+
expect((await backend.store.loadRunRow(childId))?.status).toBe("canceled");
|
|
1696
|
+
if (grandkids[0]) expect((await backend.store.loadRunRow(grandkids[0].id))?.status).toBe("canceled");
|
|
1697
|
+
});
|
|
1698
|
+
it("a child of an already-terminated parent cancels itself on dispatch (crash-safe cascade)", async () => {
|
|
1699
|
+
const backend = await makeBackend();
|
|
1700
|
+
const orphan = defineFlow({
|
|
1701
|
+
name: "orphan",
|
|
1702
|
+
version: 1,
|
|
1703
|
+
run: async (ctx) => {
|
|
1704
|
+
await ctx.sleep(1e3);
|
|
1705
|
+
return 1;
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
const { runId: parentId } = await backend.store.startRun({
|
|
1709
|
+
name: "p",
|
|
1710
|
+
version: 1,
|
|
1711
|
+
input: {}
|
|
1712
|
+
});
|
|
1713
|
+
await backend.store.markTerminal(parentId, {
|
|
1714
|
+
status: "failed",
|
|
1715
|
+
error: {
|
|
1716
|
+
code: "X",
|
|
1717
|
+
message: "parent died"
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
const { runId: childId } = await backend.store.startRun({
|
|
1721
|
+
name: "orphan",
|
|
1722
|
+
version: 1,
|
|
1723
|
+
input: {},
|
|
1724
|
+
parentRunId: parentId,
|
|
1725
|
+
parentCursorKey: "s0"
|
|
1726
|
+
});
|
|
1727
|
+
await backend.queue.enqueue(childId);
|
|
1728
|
+
await tickOnce(backend, registry([orphan]), {
|
|
1729
|
+
...base,
|
|
1730
|
+
now: () => /* @__PURE__ */ new Date("2030-01-01T00:00:00Z")
|
|
1731
|
+
});
|
|
1732
|
+
expect((await backend.store.loadRunRow(childId))?.status).toBe("canceled");
|
|
1733
|
+
});
|
|
1734
|
+
it("reconcile cancels a sleeping child left behind by a dead parent", async () => {
|
|
1735
|
+
const backend = await makeBackend();
|
|
1736
|
+
const now = () => /* @__PURE__ */ new Date("2030-01-01T00:00:00Z");
|
|
1737
|
+
const orphan = defineFlow({
|
|
1738
|
+
name: "orphan2",
|
|
1739
|
+
version: 1,
|
|
1740
|
+
run: async (ctx) => {
|
|
1741
|
+
await ctx.sleep(1e7);
|
|
1742
|
+
return 1;
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
const { runId: parentId } = await backend.store.startRun({
|
|
1746
|
+
name: "p",
|
|
1747
|
+
version: 1,
|
|
1748
|
+
input: {}
|
|
1749
|
+
});
|
|
1750
|
+
const { runId: childId } = await backend.store.startRun({
|
|
1751
|
+
name: "orphan2",
|
|
1752
|
+
version: 1,
|
|
1753
|
+
input: {},
|
|
1754
|
+
parentRunId: parentId,
|
|
1755
|
+
parentCursorKey: "s0"
|
|
1756
|
+
});
|
|
1757
|
+
await backend.queue.enqueue(childId);
|
|
1758
|
+
await tickOnce(backend, registry([orphan]), {
|
|
1759
|
+
...base,
|
|
1760
|
+
now
|
|
1761
|
+
});
|
|
1762
|
+
expect((await backend.store.loadRunRow(childId))?.status).toBe("sleeping");
|
|
1763
|
+
await backend.store.markTerminal(parentId, {
|
|
1764
|
+
status: "failed",
|
|
1765
|
+
error: {
|
|
1766
|
+
code: "X",
|
|
1767
|
+
message: "died"
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
await reconcile(backend, { limit: 16 });
|
|
1771
|
+
await tickOnce(backend, registry([orphan]), {
|
|
1772
|
+
...base,
|
|
1773
|
+
now
|
|
1774
|
+
});
|
|
1775
|
+
expect((await backend.store.loadRunRow(childId))?.status).toBe("canceled");
|
|
1776
|
+
});
|
|
1777
|
+
it("fans out children in parallel and joins their outputs in order", async () => {
|
|
1778
|
+
const backend = await makeBackend();
|
|
1779
|
+
const dbl = defineFlow({
|
|
1780
|
+
name: "dbl",
|
|
1781
|
+
version: 1,
|
|
1782
|
+
run: async (_ctx, n) => n * 2
|
|
1783
|
+
});
|
|
1784
|
+
const parent = defineFlow({
|
|
1785
|
+
name: "fan",
|
|
1786
|
+
version: 1,
|
|
1787
|
+
run: async (ctx) => ctx.invoke([
|
|
1788
|
+
{
|
|
1789
|
+
flow: dbl,
|
|
1790
|
+
input: 1
|
|
1791
|
+
},
|
|
1792
|
+
{
|
|
1793
|
+
flow: dbl,
|
|
1794
|
+
input: 2
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
flow: dbl,
|
|
1798
|
+
input: 3
|
|
1799
|
+
}
|
|
1800
|
+
])
|
|
1801
|
+
});
|
|
1802
|
+
const runId = await submit(backend, parent, {});
|
|
1803
|
+
expect(await drive(backend, registry([parent, dbl]), runId)).toMatchObject({
|
|
1804
|
+
status: "done",
|
|
1805
|
+
output: [
|
|
1806
|
+
2,
|
|
1807
|
+
4,
|
|
1808
|
+
6
|
|
1809
|
+
]
|
|
1810
|
+
});
|
|
1811
|
+
});
|
|
1812
|
+
it("rejects a fan-out beyond the cap", async () => {
|
|
1813
|
+
const backend = await makeBackend();
|
|
1814
|
+
const noop = defineFlow({
|
|
1815
|
+
name: "noop",
|
|
1816
|
+
version: 1,
|
|
1817
|
+
run: async () => 1
|
|
1818
|
+
});
|
|
1819
|
+
const parent = defineFlow({
|
|
1820
|
+
name: "toobig",
|
|
1821
|
+
version: 1,
|
|
1822
|
+
run: async (ctx) => ctx.invoke(Array.from({ length: 10001 }, () => ({
|
|
1823
|
+
flow: noop,
|
|
1824
|
+
input: {}
|
|
1825
|
+
})))
|
|
1826
|
+
});
|
|
1827
|
+
const runId = await submit(backend, parent, {});
|
|
1828
|
+
await tickOnce(backend, registry([parent, noop]), {
|
|
1829
|
+
...base,
|
|
1830
|
+
now: () => /* @__PURE__ */ new Date("2030-01-01T00:00:00Z"),
|
|
1831
|
+
retry: {
|
|
1832
|
+
maxAttempts: 1,
|
|
1833
|
+
baseDelayMs: 1,
|
|
1834
|
+
maxDelayMs: 1
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
expect((await backend.store.loadRunRow(runId))?.status).toBe("failed");
|
|
1838
|
+
});
|
|
1839
|
+
it("caps invoke recursion at the depth limit and persists each run's depth", async () => {
|
|
1840
|
+
const backend = await makeBackend();
|
|
1841
|
+
const recurse = defineFlow({
|
|
1842
|
+
name: "recurse",
|
|
1843
|
+
version: 1,
|
|
1844
|
+
policy: { maxDepth: 3 },
|
|
1845
|
+
run: async (ctx, n) => ctx.invoke(recurse, n + 1)
|
|
1846
|
+
});
|
|
1847
|
+
const runId = await submit(backend, recurse, 0);
|
|
1848
|
+
expect((await drive(backend, registry([recurse]), runId, {
|
|
1849
|
+
maxAttempts: 1,
|
|
1850
|
+
baseDelayMs: 1,
|
|
1851
|
+
maxDelayMs: 1
|
|
1852
|
+
})).status).toBe("failed");
|
|
1853
|
+
let cur = runId;
|
|
1854
|
+
let deepest = await backend.store.loadRunRow(cur);
|
|
1855
|
+
for (;;) {
|
|
1856
|
+
const kids = await backend.store.childrenOf(cur);
|
|
1857
|
+
if (kids.length === 0) break;
|
|
1858
|
+
deepest = kids[0];
|
|
1859
|
+
cur = kids[0].id;
|
|
1860
|
+
}
|
|
1861
|
+
expect(deepest?.depth).toBe(3);
|
|
1862
|
+
expect(deepest?.error?.message).toContain("depth");
|
|
1863
|
+
});
|
|
1864
|
+
it("fans out a batch larger than one spawn chunk (crosses chunk boundaries)", async () => {
|
|
1865
|
+
const backend = await makeBackend();
|
|
1866
|
+
const inc = defineFlow({
|
|
1867
|
+
name: "inc",
|
|
1868
|
+
version: 1,
|
|
1869
|
+
run: async (_ctx, n) => n + 1
|
|
1870
|
+
});
|
|
1871
|
+
const parent = defineFlow({
|
|
1872
|
+
name: "wide",
|
|
1873
|
+
version: 1,
|
|
1874
|
+
run: async (ctx) => ctx.invoke(Array.from({ length: 45 }, (_v, n) => ({
|
|
1875
|
+
flow: inc,
|
|
1876
|
+
input: n
|
|
1877
|
+
})))
|
|
1878
|
+
});
|
|
1879
|
+
const runId = await submit(backend, parent, {});
|
|
1880
|
+
const run = await drive(backend, registry([parent, inc]), runId);
|
|
1881
|
+
expect(run.status).toBe("done");
|
|
1882
|
+
expect(run.output).toEqual(Array.from({ length: 45 }, (_v, n) => n + 1));
|
|
1883
|
+
});
|
|
1884
|
+
it("fast-fails a fan-out and cancels the running siblings when one child fails", async () => {
|
|
1885
|
+
const backend = await makeBackend();
|
|
1886
|
+
const boom = defineFlow({
|
|
1887
|
+
name: "boom",
|
|
1888
|
+
version: 1,
|
|
1889
|
+
run: async () => {
|
|
1890
|
+
throw new Error("boom");
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
const slow = defineFlow({
|
|
1894
|
+
name: "slow",
|
|
1895
|
+
version: 1,
|
|
1896
|
+
run: async (ctx) => {
|
|
1897
|
+
await ctx.sleep(1e7);
|
|
1898
|
+
return 1;
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
const parent = defineFlow({
|
|
1902
|
+
name: "fanfail",
|
|
1903
|
+
version: 1,
|
|
1904
|
+
run: async (ctx) => ctx.invoke([{
|
|
1905
|
+
flow: boom,
|
|
1906
|
+
input: {}
|
|
1907
|
+
}, {
|
|
1908
|
+
flow: slow,
|
|
1909
|
+
input: {}
|
|
1910
|
+
}])
|
|
1911
|
+
});
|
|
1912
|
+
const runId = await submit(backend, parent, {});
|
|
1913
|
+
expect((await drive(backend, registry([
|
|
1914
|
+
parent,
|
|
1915
|
+
boom,
|
|
1916
|
+
slow
|
|
1917
|
+
]), runId, {
|
|
1918
|
+
maxAttempts: 1,
|
|
1919
|
+
baseDelayMs: 1,
|
|
1920
|
+
maxDelayMs: 1
|
|
1921
|
+
})).status).toBe("failed");
|
|
1922
|
+
const kids = await backend.store.childrenOf(runId);
|
|
1923
|
+
expect(kids).toHaveLength(2);
|
|
1924
|
+
expect(kids.every((k) => isTerminal(k.status))).toBe(true);
|
|
1925
|
+
expect(kids.some((k) => k.status === "canceled")).toBe(true);
|
|
1926
|
+
});
|
|
1927
|
+
});
|
|
1928
|
+
};
|
|
1929
|
+
//#endregion
|
|
1930
|
+
//#region src/claim-filter.ts
|
|
1931
|
+
/**
|
|
1932
|
+
* `ClaimOpts.names` as executable invariants: a sharded worker leases only runs whose flow `name`
|
|
1933
|
+
* it registered. Needs the store (to create named runs), so it takes the whole backend.
|
|
1934
|
+
*/
|
|
1935
|
+
const claimFilterConformance = (label, makeBackend) => {
|
|
1936
|
+
describe(`Claim flow-name filter — ${label}`, () => {
|
|
1937
|
+
it("leases only runs whose flow name is in the set", async () => {
|
|
1938
|
+
const { store, queue } = await makeBackend();
|
|
1939
|
+
const a = await store.startRun({
|
|
1940
|
+
name: "flow-a",
|
|
1941
|
+
version: 1,
|
|
1942
|
+
input: {}
|
|
1943
|
+
});
|
|
1944
|
+
const b = await store.startRun({
|
|
1945
|
+
name: "flow-b",
|
|
1946
|
+
version: 1,
|
|
1947
|
+
input: {}
|
|
1948
|
+
});
|
|
1949
|
+
await queue.enqueue(a.runId);
|
|
1950
|
+
await queue.enqueue(b.runId);
|
|
1951
|
+
expect((await queue.claim({
|
|
1952
|
+
limit: 10,
|
|
1953
|
+
leaseMs: 1e3,
|
|
1954
|
+
names: ["flow-a"],
|
|
1955
|
+
now: at(0)
|
|
1956
|
+
})).map((l) => l.runId)).toEqual([a.runId]);
|
|
1957
|
+
expect((await queue.claim({
|
|
1958
|
+
limit: 10,
|
|
1959
|
+
leaseMs: 1e3,
|
|
1960
|
+
names: ["flow-b"],
|
|
1961
|
+
now: at(0)
|
|
1962
|
+
})).map((l) => l.runId)).toEqual([b.runId]);
|
|
1963
|
+
});
|
|
1964
|
+
it("an empty set leases nothing; an omitted set leases everything", async () => {
|
|
1965
|
+
const { store, queue } = await makeBackend();
|
|
1966
|
+
const a = await store.startRun({
|
|
1967
|
+
name: "flow-a",
|
|
1968
|
+
version: 1,
|
|
1969
|
+
input: {}
|
|
1970
|
+
});
|
|
1971
|
+
await queue.enqueue(a.runId);
|
|
1972
|
+
expect(await queue.claim({
|
|
1973
|
+
limit: 10,
|
|
1974
|
+
leaseMs: 1e3,
|
|
1975
|
+
names: [],
|
|
1976
|
+
now: at(0)
|
|
1977
|
+
})).toEqual([]);
|
|
1978
|
+
expect((await queue.claim({
|
|
1979
|
+
limit: 10,
|
|
1980
|
+
leaseMs: 1e3,
|
|
1981
|
+
now: at(0)
|
|
1982
|
+
})).map((l) => l.runId)).toEqual([a.runId]);
|
|
1983
|
+
});
|
|
1984
|
+
});
|
|
1985
|
+
};
|
|
1986
|
+
//#endregion
|
|
1987
|
+
export { claimFilterConformance, cronConformance, engineConformance, outboxConformance, queueConformance, reconcileConformance, signalConformance, storeConformance, timerConformance, wakeupConformance };
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iterativeflow/conformance",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.10",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/ahmedrowaihi/iterativeflow.git",
|
|
8
|
+
"directory": "v2/packages/conformance"
|
|
9
|
+
},
|
|
4
10
|
"files": [
|
|
5
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
6
13
|
],
|
|
7
14
|
"type": "module",
|
|
8
15
|
"imports": {
|
|
@@ -21,7 +28,7 @@
|
|
|
21
28
|
"build": "tsdown"
|
|
22
29
|
},
|
|
23
30
|
"dependencies": {
|
|
24
|
-
"@iterativeflow/core": "2.0.0-alpha.
|
|
31
|
+
"@iterativeflow/core": "2.0.0-alpha.10"
|
|
25
32
|
},
|
|
26
33
|
"peerDependencies": {
|
|
27
34
|
"vitest": ">=3"
|