@yieldstar/runtime-conformance 0.0.0-stage → 0.6.0

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/dist/index.js ADDED
@@ -0,0 +1,3835 @@
1
+ // src/index.ts
2
+ import {
3
+ DEFAULT_LEASE_DURATION as DEFAULT_LEASE_DURATION3,
4
+ defineStore as defineStore2
5
+ } from "@yieldstar/core";
6
+ import { describe as describe3, expect as expect3, test as test3 } from "vitest";
7
+
8
+ // src/effect.ts
9
+ import {
10
+ DEFAULT_LEASE_DURATION,
11
+ EffectClientInvariantError,
12
+ redeliveryDelay,
13
+ registerErrorClass
14
+ } from "@yieldstar/core";
15
+ import { describe, expect, test } from "vitest";
16
+ var START = 1e6;
17
+ var LEASE = DEFAULT_LEASE_DURATION;
18
+ var REDELIVER_AFTER = LEASE + redeliveryDelay(1);
19
+ var OPERATION = "charge-v1";
20
+
21
+ class CardDeclinedError extends Error {
22
+ code = "CARD_DECLINED";
23
+ declineCode;
24
+ constructor(declineCode) {
25
+ super(`Card declined: ${declineCode}`);
26
+ this.name = "ConformanceCardDeclinedError";
27
+ this.declineCode = declineCode;
28
+ }
29
+ }
30
+ registerErrorClass("ConformanceCardDeclinedError", CardDeclinedError);
31
+ function registerEffectClientConformance({
32
+ name,
33
+ create
34
+ }) {
35
+ async function setup() {
36
+ const clock = { now: START };
37
+ const harness = await create({ clock: () => clock.now });
38
+ const peer = await harness.createPeer?.() ?? harness.runtime;
39
+ return {
40
+ runtime: harness.runtime,
41
+ peer,
42
+ harness,
43
+ clock,
44
+ advance(ms) {
45
+ clock.now += ms;
46
+ }
47
+ };
48
+ }
49
+ function it(title, fn) {
50
+ test(title, async (testContext) => {
51
+ const ctx = await setup();
52
+ try {
53
+ await fn(ctx, testContext);
54
+ } finally {
55
+ await ctx.harness.dispose?.();
56
+ }
57
+ });
58
+ }
59
+ function itAfterRestart(title, fn) {
60
+ test(title, async (testContext) => {
61
+ const ctx = await setup();
62
+ try {
63
+ if (!ctx.harness.restart) {
64
+ testContext.skip();
65
+ return;
66
+ }
67
+ await fn({ ...ctx, restart: async () => ctx.harness.restart() });
68
+ } finally {
69
+ await ctx.harness.dispose?.();
70
+ }
71
+ });
72
+ }
73
+ function claim(runtime) {
74
+ return runtime.executions.claimDue({ limit: 100, duration: LEASE });
75
+ }
76
+ async function claimOne(runtime, executionId) {
77
+ const leases = await claim(runtime);
78
+ expect(leases.map((lease) => lease.executionId)).toEqual([executionId]);
79
+ return leases[0];
80
+ }
81
+ async function claimedIds(runtime) {
82
+ return (await claim(runtime)).map((lease) => lease.executionId).sort();
83
+ }
84
+ async function admitSuspended(runtime, executionId) {
85
+ const admitted = await runtime.executions.admit({
86
+ executionId,
87
+ workflowId: "charge-order",
88
+ params: { executionId },
89
+ context: new Map
90
+ });
91
+ expect(admitted).toEqual({ status: "accepted", executionId });
92
+ const lease = await claimOne(runtime, executionId);
93
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
94
+ return executionId;
95
+ }
96
+ async function reclaim(ctx, executionId) {
97
+ const stale = await claimOne(ctx.runtime, executionId);
98
+ ctx.advance(REDELIVER_AFTER);
99
+ const current = await claimOne(ctx.peer, executionId);
100
+ expect(current.token).toBeGreaterThan(stale.token);
101
+ return { stale, current };
102
+ }
103
+ function begin(runtime, effectId, options = {}) {
104
+ return runtime.effects.begin({
105
+ effectId,
106
+ operation: options.operation ?? OPERATION,
107
+ input: "input" in options ? options.input : { amount: 4999, currency: "usd" },
108
+ executionId: options.executionId ?? "order-42",
109
+ stepKey: options.stepKey ?? "charge"
110
+ });
111
+ }
112
+ function settle(runtime, effectId, result, operation = OPERATION) {
113
+ return runtime.effects.settle({ effectId, operation, result });
114
+ }
115
+ function watch(runtime, effectId, executionId, stepKey = "charge") {
116
+ return runtime.effects.watch({ effectId, executionId, stepKey });
117
+ }
118
+ function recordStep(runtime, lease, options = {}) {
119
+ return runtime.heap.writeStep({
120
+ lease,
121
+ stepKey: options.stepKey ?? "charge",
122
+ stepAttempt: options.attempt ?? 1,
123
+ done: options.done ?? true,
124
+ stepResponseJson: JSON.stringify({ paymentId: "pay_1" })
125
+ });
126
+ }
127
+ async function expectInvariant(promise) {
128
+ const error = await promise.then(() => {
129
+ throw new Error("expected an EffectClientInvariantError");
130
+ }, (err) => err);
131
+ expect(error).toBeInstanceOf(EffectClientInvariantError);
132
+ expect(error.code).toBe("EFFECT_RUNTIME_INVARIANT");
133
+ }
134
+ describe(`${name} effect client conformance`, () => {
135
+ describe("begin", () => {
136
+ it("creates one pending effect and returns its current state to repeated matching calls", async ({
137
+ runtime,
138
+ peer
139
+ }) => {
140
+ expect(await runtime.effects.getEffect("pay:42")).toBeUndefined();
141
+ expect(await begin(runtime, "pay:42")).toEqual({ status: "new" });
142
+ expect(await runtime.effects.getEffect("pay:42")).toEqual({
143
+ effectId: "pay:42",
144
+ operation: OPERATION,
145
+ input: { amount: 4999, currency: "usd" },
146
+ status: "pending",
147
+ conflicts: []
148
+ });
149
+ expect(await begin(runtime, "pay:42")).toEqual({ status: "pending" });
150
+ expect(await begin(peer, "pay:42")).toEqual({ status: "pending" });
151
+ expect(await begin(runtime, "pay:42", { executionId: "support", stepKey: "check" })).toEqual({ status: "pending" });
152
+ await settle(runtime, "pay:42", { paymentId: "pay_1" });
153
+ expect(await begin(peer, "pay:42")).toEqual({
154
+ status: "settled",
155
+ result: { paymentId: "pay_1" }
156
+ });
157
+ });
158
+ it("creates one effect for concurrent begins with one effect ID", async ({
159
+ runtime,
160
+ peer
161
+ }) => {
162
+ const results = await Promise.all([
163
+ begin(runtime, "pay:race"),
164
+ begin(peer, "pay:race"),
165
+ begin(runtime, "pay:race")
166
+ ]);
167
+ expect(results.map((result) => result.status).sort()).toEqual([
168
+ "new",
169
+ "pending",
170
+ "pending"
171
+ ]);
172
+ expect(await runtime.effects.getEffect("pay:race")).toMatchObject({
173
+ status: "pending"
174
+ });
175
+ });
176
+ it("matches input whose keys are in a different order", async ({ runtime }) => {
177
+ await begin(runtime, "pay:keys", { input: { a: 1, b: { c: 2, d: 3 } } });
178
+ expect(await begin(runtime, "pay:keys", { input: { b: { d: 3, c: 2 }, a: 1 } })).toEqual({ status: "pending" });
179
+ });
180
+ it("rejects a changed operation or input without changing the effect", async ({
181
+ runtime,
182
+ peer
183
+ }) => {
184
+ const mismatch = {
185
+ status: "rejected",
186
+ code: "EFFECT_REQUEST_MISMATCH"
187
+ };
188
+ await begin(runtime, "pay:changed");
189
+ const before = await runtime.effects.getEffect("pay:changed");
190
+ expect(await begin(runtime, "pay:changed", { operation: "charge-v2" })).toEqual(mismatch);
191
+ expect(await begin(peer, "pay:changed", { input: { amount: 5999, currency: "usd" } })).toEqual(mismatch);
192
+ expect(await begin(runtime, "pay:changed", {
193
+ input: { amount: 4999, currency: "usd", note: undefined }
194
+ })).toEqual(mismatch);
195
+ expect(await runtime.effects.getEffect("pay:changed")).toEqual(before);
196
+ await settle(runtime, "pay:changed", { paymentId: "pay_1" });
197
+ const settled = await runtime.effects.getEffect("pay:changed");
198
+ expect(await begin(runtime, "pay:changed", { operation: "charge-v2" })).toEqual(mismatch);
199
+ expect(await runtime.effects.getEffect("pay:changed")).toEqual(settled);
200
+ });
201
+ test.each([
202
+ ["an empty effect ID", { effectId: "" }],
203
+ ["an empty operation", { operation: "" }],
204
+ ["array input", { input: [] }],
205
+ ["a Date in the input", { input: { at: new Date(0) } }],
206
+ ["a non-finite number in the input", { input: { amount: Number.NaN } }],
207
+ ["an empty execution ID", { executionId: "" }],
208
+ ["an empty step key", { stepKey: "" }]
209
+ ])("throws EffectClientInvariantError for %s and creates nothing", async (_label, override) => {
210
+ const ctx = await setup();
211
+ try {
212
+ const request = {
213
+ effectId: "pay:invalid",
214
+ operation: OPERATION,
215
+ input: { amount: 4999 },
216
+ executionId: "order-42",
217
+ stepKey: "charge",
218
+ ...override
219
+ };
220
+ await expectInvariant(ctx.runtime.effects.begin(request));
221
+ expect(await ctx.runtime.effects.getEffect("pay:invalid")).toBeUndefined();
222
+ } finally {
223
+ await ctx.harness.dispose?.();
224
+ }
225
+ });
226
+ });
227
+ describe("settle", () => {
228
+ it("settles a pending effect and returns its result", async ({ runtime }) => {
229
+ await begin(runtime, "pay:settle");
230
+ expect(await settle(runtime, "pay:settle", { paymentId: "pay_1" })).toEqual({
231
+ status: "settled",
232
+ result: { paymentId: "pay_1" }
233
+ });
234
+ expect(await runtime.effects.getEffect("pay:settle")).toEqual({
235
+ effectId: "pay:settle",
236
+ operation: OPERATION,
237
+ input: { amount: 4999, currency: "usd" },
238
+ status: "settled",
239
+ result: { paymentId: "pay_1" },
240
+ conflicts: []
241
+ });
242
+ });
243
+ it("keeps one settled result when equal results settle concurrently", async ({
244
+ runtime,
245
+ peer
246
+ }) => {
247
+ await begin(runtime, "pay:equal");
248
+ const results = await Promise.all([
249
+ settle(runtime, "pay:equal", { paymentId: "pay_1", fee: 30 }),
250
+ settle(peer, "pay:equal", { fee: 30, paymentId: "pay_1" }),
251
+ settle(runtime, "pay:equal", { paymentId: "pay_1", fee: 30 })
252
+ ]);
253
+ for (const result of results) {
254
+ expect(result).toEqual({
255
+ status: "settled",
256
+ result: { paymentId: "pay_1", fee: 30 }
257
+ });
258
+ }
259
+ expect(await runtime.effects.getEffect("pay:equal")).toMatchObject({
260
+ status: "settled",
261
+ result: { paymentId: "pay_1", fee: 30 },
262
+ conflicts: []
263
+ });
264
+ });
265
+ it("keeps the first result and one conflict report when a different result settles", async ({
266
+ runtime,
267
+ peer,
268
+ clock,
269
+ advance
270
+ }) => {
271
+ await begin(runtime, "pay:conflict");
272
+ await settle(runtime, "pay:conflict", { paymentId: "pay_1" });
273
+ advance(10);
274
+ const reportedAt = clock.now;
275
+ const conflict = {
276
+ status: "conflict",
277
+ result: { paymentId: "pay_1" },
278
+ conflictingResult: { paymentId: "pay_2" }
279
+ };
280
+ expect(await settle(peer, "pay:conflict", { paymentId: "pay_2" })).toEqual(conflict);
281
+ advance(10);
282
+ expect(await settle(runtime, "pay:conflict", { paymentId: "pay_2" })).toEqual(conflict);
283
+ expect(await settle(runtime, "pay:conflict", { paymentId: "pay_1" })).toEqual({
284
+ status: "settled",
285
+ result: { paymentId: "pay_1" }
286
+ });
287
+ expect(await begin(runtime, "pay:conflict")).toEqual({
288
+ status: "settled",
289
+ result: { paymentId: "pay_1" }
290
+ });
291
+ expect(await runtime.effects.getEffect("pay:conflict")).toMatchObject({
292
+ status: "settled",
293
+ result: { paymentId: "pay_1" },
294
+ conflicts: [
295
+ {
296
+ result: { paymentId: "pay_1" },
297
+ conflictingResult: { paymentId: "pay_2" },
298
+ reportedAt
299
+ }
300
+ ]
301
+ });
302
+ });
303
+ it("settles once when different results settle concurrently", async ({
304
+ runtime,
305
+ peer
306
+ }) => {
307
+ await begin(runtime, "pay:race");
308
+ const results = await Promise.all([
309
+ settle(runtime, "pay:race", "first"),
310
+ settle(peer, "pay:race", "second")
311
+ ]);
312
+ const settled = results.filter((result) => result.status === "settled");
313
+ const conflicts = results.filter((result) => result.status === "conflict");
314
+ expect(settled).toHaveLength(1);
315
+ expect(conflicts).toHaveLength(1);
316
+ const winner = settled[0].result;
317
+ const loser = winner === "first" ? "second" : "first";
318
+ expect(conflicts[0]).toEqual({
319
+ status: "conflict",
320
+ result: winner,
321
+ conflictingResult: loser
322
+ });
323
+ const record = await runtime.effects.getEffect("pay:race");
324
+ expect(record?.result).toBe(winner);
325
+ expect(record?.conflicts).toEqual([
326
+ { result: winner, conflictingResult: loser, reportedAt: START }
327
+ ]);
328
+ });
329
+ it("stores undefined, errors and error-shaped objects without changing their type", async ({
330
+ runtime
331
+ }) => {
332
+ await begin(runtime, "delete:1");
333
+ expect(await settle(runtime, "delete:1", undefined)).toEqual({
334
+ status: "settled",
335
+ result: undefined
336
+ });
337
+ const deleted = await runtime.effects.getEffect("delete:1");
338
+ expect(deleted?.status).toBe("settled");
339
+ expect("result" in deleted).toBe(true);
340
+ expect(deleted?.result).toBeUndefined();
341
+ expect(await begin(runtime, "delete:1")).toEqual({
342
+ status: "settled",
343
+ result: undefined
344
+ });
345
+ const result = {
346
+ declined: new CardDeclinedError("insufficient_funds"),
347
+ report: { name: "Error", message: "not an error" },
348
+ tagged: { $type: "undefined" },
349
+ missing: undefined
350
+ };
351
+ await begin(runtime, "pay:values", {
352
+ input: { amount: 4999, note: undefined, shaped: { name: "E", message: "m" } }
353
+ });
354
+ await settle(runtime, "pay:values", result);
355
+ const begun = await begin(runtime, "pay:values", {
356
+ input: { amount: 4999, note: undefined, shaped: { name: "E", message: "m" } }
357
+ });
358
+ const record = await runtime.effects.getEffect("pay:values");
359
+ for (const stored of [
360
+ begun.result,
361
+ record.result
362
+ ]) {
363
+ expect(stored.declined).toBeInstanceOf(CardDeclinedError);
364
+ expect(stored.declined.message).toBe("Card declined: insufficient_funds");
365
+ expect(stored.declined.code).toBe("CARD_DECLINED");
366
+ expect(stored.declined.declineCode).toBe("insufficient_funds");
367
+ expect(stored.report).not.toBeInstanceOf(Error);
368
+ expect(stored.report).toEqual({ name: "Error", message: "not an error" });
369
+ expect(stored.tagged).toEqual({ $type: "undefined" });
370
+ expect(Object.keys(stored)).toContain("missing");
371
+ }
372
+ expect(record.input.shaped).not.toBeInstanceOf(Error);
373
+ expect(Object.keys(record.input)).toContain("note");
374
+ });
375
+ it("throws EffectClientInvariantError for an unknown effect, a changed operation or an invalid result", async ({
376
+ runtime
377
+ }) => {
378
+ await expectInvariant(settle(runtime, "pay:unknown", "result"));
379
+ expect(await runtime.effects.getEffect("pay:unknown")).toBeUndefined();
380
+ await begin(runtime, "pay:invariant");
381
+ await expectInvariant(settle(runtime, "pay:invariant", "result", "charge-v2"));
382
+ await expectInvariant(settle(runtime, "pay:invariant", { at: new Date(0) }));
383
+ expect(await runtime.effects.getEffect("pay:invariant")).toMatchObject({
384
+ status: "pending"
385
+ });
386
+ });
387
+ });
388
+ describe("watch and wakes", () => {
389
+ it("wakes every watching execution when the settlement commits", async ({
390
+ runtime,
391
+ peer
392
+ }) => {
393
+ await admitSuspended(runtime, "order-42");
394
+ await admitSuspended(runtime, "support");
395
+ await begin(runtime, "pay:watched");
396
+ await watch(runtime, "pay:watched", "order-42");
397
+ await watch(peer, "pay:watched", "support", "check");
398
+ expect(await claim(runtime)).toEqual([]);
399
+ await settle(peer, "pay:watched", { paymentId: "pay_1" });
400
+ expect(await claimedIds(runtime)).toEqual(["order-42", "support"]);
401
+ });
402
+ it("registers nothing when a watch fails before it commits, and a repeat succeeds as a first call", async ({ runtime, harness }, testContext) => {
403
+ if (!harness.failEffectWatches) {
404
+ testContext.skip();
405
+ return;
406
+ }
407
+ await admitSuspended(runtime, "order-42");
408
+ await begin(runtime, "pay:failed");
409
+ await begin(runtime, "pay:retried");
410
+ const restore = await harness.failEffectWatches();
411
+ try {
412
+ await expect(watch(runtime, "pay:failed", "order-42")).rejects.toThrow();
413
+ await expect(watch(runtime, "pay:retried", "order-42")).rejects.toThrow();
414
+ } finally {
415
+ await restore();
416
+ }
417
+ await settle(runtime, "pay:failed", "done");
418
+ expect(await claim(runtime)).toEqual([]);
419
+ await watch(runtime, "pay:retried", "order-42");
420
+ expect(await claim(runtime)).toEqual([]);
421
+ await settle(runtime, "pay:retried", "done");
422
+ expect(await claimedIds(runtime)).toEqual(["order-42"]);
423
+ });
424
+ it("keeps one registration when a watch is repeated after a lost response", async ({
425
+ runtime,
426
+ peer
427
+ }) => {
428
+ await admitSuspended(runtime, "order-42");
429
+ await begin(runtime, "pay:repeated");
430
+ await watch(runtime, "pay:repeated", "order-42");
431
+ await watch(peer, "pay:repeated", "order-42");
432
+ await watch(runtime, "pay:repeated", "order-42");
433
+ await runtime.executions.requestWake({
434
+ executionId: "order-42",
435
+ wakeAt: START
436
+ });
437
+ const lease = await claimOne(runtime, "order-42");
438
+ await recordStep(runtime, lease);
439
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
440
+ await settle(runtime, "pay:repeated", "done");
441
+ expect(await claim(runtime)).toEqual([]);
442
+ });
443
+ it("wakes the execution at once when the effect is already settled, on every watch", async ({
444
+ runtime
445
+ }) => {
446
+ await admitSuspended(runtime, "order-42");
447
+ await begin(runtime, "pay:settled");
448
+ await settle(runtime, "pay:settled", "done");
449
+ await watch(runtime, "pay:settled", "order-42");
450
+ const lease = await claimOne(runtime, "order-42");
451
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
452
+ expect(await claim(runtime)).toEqual([]);
453
+ await watch(runtime, "pay:settled", "order-42");
454
+ expect(await claimedIds(runtime)).toEqual(["order-42"]);
455
+ });
456
+ it("wakes the watching execution when settlement races with watch", async ({
457
+ runtime,
458
+ peer
459
+ }) => {
460
+ for (let i = 0;i < 10; i++) {
461
+ const executionId = `racer-${i}`;
462
+ const effectId = `pay:race-${i}`;
463
+ await admitSuspended(runtime, executionId);
464
+ await begin(runtime, effectId);
465
+ const watching = watch(i % 2 ? runtime : peer, effectId, executionId);
466
+ const settling = settle(i % 2 ? peer : runtime, effectId, "done");
467
+ await Promise.all(i % 4 < 2 ? [watching, settling] : [settling, watching]);
468
+ expect(await claimedIds(runtime)).toEqual([executionId]);
469
+ }
470
+ });
471
+ it("deletes a waiter in the write that records its step's result, so a later settlement does not wake", async ({
472
+ runtime
473
+ }) => {
474
+ await admitSuspended(runtime, "order-42");
475
+ await begin(runtime, "pay:recorded");
476
+ await begin(runtime, "pay:other");
477
+ await watch(runtime, "pay:recorded", "order-42", "charge");
478
+ await watch(runtime, "pay:other", "order-42", "refund");
479
+ await runtime.executions.requestWake({ executionId: "order-42", wakeAt: START });
480
+ const unfinished = await claimOne(runtime, "order-42");
481
+ await recordStep(runtime, unfinished, { attempt: 1, done: false });
482
+ await runtime.executions.acknowledge({
483
+ lease: unfinished,
484
+ outcome: { status: "suspended" }
485
+ });
486
+ await runtime.executions.requestWake({ executionId: "order-42", wakeAt: START });
487
+ const lease = await claimOne(runtime, "order-42");
488
+ await recordStep(runtime, lease, { attempt: 2 });
489
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
490
+ await settle(runtime, "pay:recorded", "done");
491
+ expect(await claim(runtime)).toEqual([]);
492
+ await settle(runtime, "pay:other", "done");
493
+ expect(await claimedIds(runtime)).toEqual(["order-42"]);
494
+ });
495
+ it("keeps the waiter when a stale holder's step result is rejected", async (ctx) => {
496
+ const { runtime } = ctx;
497
+ await admitSuspended(runtime, "order-42");
498
+ await begin(runtime, "pay:stale");
499
+ await watch(runtime, "pay:stale", "order-42");
500
+ await runtime.executions.requestWake({ executionId: "order-42", wakeAt: START });
501
+ const { stale, current } = await reclaim(ctx, "order-42");
502
+ expect(await recordStep(runtime, stale)).toEqual({ status: "lost" });
503
+ await ctx.peer.executions.acknowledge({
504
+ lease: current,
505
+ outcome: { status: "suspended" }
506
+ });
507
+ await settle(runtime, "pay:stale", "done");
508
+ expect(await claimedIds(runtime)).toEqual(["order-42"]);
509
+ });
510
+ it("settles without waking a terminal execution", async ({ runtime }) => {
511
+ await admitSuspended(runtime, "order-42");
512
+ await begin(runtime, "pay:terminal");
513
+ await watch(runtime, "pay:terminal", "order-42");
514
+ await runtime.executions.requestWake({ executionId: "order-42", wakeAt: START });
515
+ const lease = await claimOne(runtime, "order-42");
516
+ await runtime.executions.acknowledge({
517
+ lease,
518
+ outcome: { status: "completed", result: null }
519
+ });
520
+ expect(await settle(runtime, "pay:terminal", "done")).toEqual({
521
+ status: "settled",
522
+ result: "done"
523
+ });
524
+ expect(await claim(runtime)).toEqual([]);
525
+ expect(await runtime.executions.get("order-42")).toMatchObject({
526
+ status: "completed"
527
+ });
528
+ });
529
+ it("throws EffectClientInvariantError when watching an unknown effect", async ({
530
+ runtime
531
+ }) => {
532
+ await admitSuspended(runtime, "order-42");
533
+ await expectInvariant(watch(runtime, "pay:unknown", "order-42"));
534
+ await begin(runtime, "pay:unknown");
535
+ await settle(runtime, "pay:unknown", "done");
536
+ expect(await claim(runtime)).toEqual([]);
537
+ });
538
+ });
539
+ describe("getEffect", () => {
540
+ it("returns the first result and the conflict report after a conflicting settlement", async ({
541
+ runtime,
542
+ peer
543
+ }) => {
544
+ await begin(runtime, "pay:report");
545
+ await settle(runtime, "pay:report", { paymentId: "pay_1" });
546
+ await settle(runtime, "pay:report", { paymentId: "pay_2" });
547
+ await settle(runtime, "pay:report", { paymentId: "pay_3" });
548
+ expect(await peer.effects.getEffect("pay:report")).toEqual({
549
+ effectId: "pay:report",
550
+ operation: OPERATION,
551
+ input: { amount: 4999, currency: "usd" },
552
+ status: "settled",
553
+ result: { paymentId: "pay_1" },
554
+ conflicts: [
555
+ {
556
+ result: { paymentId: "pay_1" },
557
+ conflictingResult: { paymentId: "pay_2" },
558
+ reportedAt: START
559
+ },
560
+ {
561
+ result: { paymentId: "pay_1" },
562
+ conflictingResult: { paymentId: "pay_3" },
563
+ reportedAt: START
564
+ }
565
+ ]
566
+ });
567
+ });
568
+ it("returns a copy that callers cannot use to change the record", async ({
569
+ runtime
570
+ }) => {
571
+ const input = { amount: 4999, nested: { currency: "usd" } };
572
+ await begin(runtime, "pay:copy", { input });
573
+ input.nested.currency = "eur";
574
+ const record = await runtime.effects.getEffect("pay:copy");
575
+ record.input.nested.currency = "gbp";
576
+ expect((await runtime.effects.getEffect("pay:copy")).input).toEqual({
577
+ amount: 4999,
578
+ nested: { currency: "usd" }
579
+ });
580
+ });
581
+ });
582
+ describe("durability", () => {
583
+ itAfterRestart("resumes delivery of stored wakes after a restart", async ({
584
+ runtime,
585
+ restart
586
+ }) => {
587
+ await admitSuspended(runtime, "order-42");
588
+ await begin(runtime, "pay:wake");
589
+ await watch(runtime, "pay:wake", "order-42");
590
+ await settle(runtime, "pay:wake", "done");
591
+ const restarted = await restart();
592
+ expect(await claimedIds(restarted)).toEqual(["order-42"]);
593
+ });
594
+ itAfterRestart("keeps effects, conflict reports and waiters across a restart", async ({
595
+ runtime,
596
+ restart
597
+ }) => {
598
+ await admitSuspended(runtime, "order-42");
599
+ await begin(runtime, "pay:pending");
600
+ await watch(runtime, "pay:pending", "order-42");
601
+ await begin(runtime, "pay:conflict");
602
+ await settle(runtime, "pay:conflict", "first");
603
+ await settle(runtime, "pay:conflict", "second");
604
+ const conflicted = await runtime.effects.getEffect("pay:conflict");
605
+ const restarted = await restart();
606
+ expect(await restarted.effects.getEffect("pay:conflict")).toEqual(conflicted);
607
+ expect(await begin(restarted, "pay:pending")).toEqual({ status: "pending" });
608
+ expect(await claim(restarted)).toEqual([]);
609
+ await settle(restarted, "pay:pending", "done");
610
+ expect(await claimedIds(restarted)).toEqual(["order-42"]);
611
+ });
612
+ });
613
+ });
614
+ }
615
+ // src/execution.ts
616
+ import {
617
+ DEFAULT_LEASE_DURATION as DEFAULT_LEASE_DURATION2,
618
+ defineStore,
619
+ durableValueCodec,
620
+ redeliveryDelay as redeliveryDelay2,
621
+ registerErrorClass as registerErrorClass2
622
+ } from "@yieldstar/core";
623
+ import { describe as describe2, expect as expect2, test as test2 } from "vitest";
624
+ var START2 = 1e6;
625
+ var LEASE2 = DEFAULT_LEASE_DURATION2;
626
+ var REDELIVER_AFTER2 = LEASE2 + redeliveryDelay2(1);
627
+ function schema() {
628
+ return {
629
+ "~standard": {
630
+ version: 1,
631
+ vendor: "yieldstar-execution-conformance",
632
+ validate(value) {
633
+ return { value };
634
+ }
635
+ }
636
+ };
637
+ }
638
+
639
+ class Point {
640
+ x;
641
+ constructor(x) {
642
+ this.x = x;
643
+ }
644
+ }
645
+
646
+ class PaymentDeclinedError extends Error {
647
+ code = "CARD_DECLINED";
648
+ constructor(message) {
649
+ super(message);
650
+ this.name = "PaymentDeclinedError";
651
+ }
652
+ }
653
+
654
+ class GatewayTimeoutError extends Error {
655
+ gateway;
656
+ constructor(gateway, options) {
657
+ super(`${gateway} timed out`, options);
658
+ this.name = "ConformanceGatewayTimeoutError";
659
+ this.gateway = gateway;
660
+ }
661
+ }
662
+ registerErrorClass2("ConformanceGatewayTimeoutError", GatewayTimeoutError);
663
+ function registerExecutionClientConformance({
664
+ name,
665
+ create
666
+ }) {
667
+ const Store = defineStore(`${name}-execution-conformance`, schema(), {
668
+ initial: () => ({ messages: [] })
669
+ });
670
+ async function setup() {
671
+ const clock = { now: START2 };
672
+ const harness = await create({
673
+ clock: () => clock.now
674
+ });
675
+ return {
676
+ runtime: harness.runtime,
677
+ harness,
678
+ clock,
679
+ advance(ms) {
680
+ clock.now += ms;
681
+ }
682
+ };
683
+ }
684
+ function it(title, fn) {
685
+ test2(title, async (testContext) => {
686
+ const ctx = await setup();
687
+ try {
688
+ await fn(ctx, testContext);
689
+ } finally {
690
+ await ctx.harness.dispose?.();
691
+ }
692
+ });
693
+ }
694
+ function itAfterRestart(title, fn) {
695
+ test2(title, async (testContext) => {
696
+ const ctx = await setup();
697
+ try {
698
+ if (!ctx.harness.restart) {
699
+ testContext.skip();
700
+ return;
701
+ }
702
+ await fn({ ...ctx, restart: async () => ctx.harness.restart() });
703
+ } finally {
704
+ await ctx.harness.dispose?.();
705
+ }
706
+ });
707
+ }
708
+ async function admit(runtime, executionId, workflowId = "workflow") {
709
+ const admitted = await runtime.executions.admit({
710
+ executionId,
711
+ workflowId,
712
+ params: { executionId },
713
+ context: new Map
714
+ });
715
+ expect2(admitted).toEqual({ status: "accepted", executionId });
716
+ return executionId;
717
+ }
718
+ function startFrom(runtime, callerExecutionId, stepKey, target = {}) {
719
+ return runtime.executions.start({
720
+ requestId: { executionId: callerExecutionId, stepKey },
721
+ workflowId: target.workflowId ?? "ship-order",
722
+ params: "params" in target ? target.params : { callerExecutionId, stepKey }
723
+ });
724
+ }
725
+ function acceptedId(result) {
726
+ if (result.status !== "accepted") {
727
+ throw new Error(`start rejected: ${result.code}: ${result.message}`);
728
+ }
729
+ return result.executionId;
730
+ }
731
+ function claim(runtime, limit = 100) {
732
+ return runtime.executions.claimDue({ limit, duration: LEASE2 });
733
+ }
734
+ async function claimOne(runtime, executionId) {
735
+ const leases = await claim(runtime);
736
+ expect2(leases.map((d) => d.executionId)).toEqual([executionId]);
737
+ return leases[0];
738
+ }
739
+ async function claimedIds(runtime) {
740
+ return (await claim(runtime)).map((d) => d.executionId).sort();
741
+ }
742
+ async function drain(runtime) {
743
+ const leases = await claim(runtime);
744
+ for (const lease of leases) {
745
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
746
+ }
747
+ return leases.map((d) => d.executionId).sort();
748
+ }
749
+ async function admitSuspended(runtime, executionId) {
750
+ await admit(runtime, executionId);
751
+ const lease = await claimOne(runtime, executionId);
752
+ expect2(await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } })).toEqual({ status: "suspended" });
753
+ return executionId;
754
+ }
755
+ async function admitCompleted(runtime, executionId, result) {
756
+ await admit(runtime, executionId);
757
+ const lease = await claimOne(runtime, executionId);
758
+ await runtime.executions.acknowledge({
759
+ lease,
760
+ outcome: { status: "completed", result }
761
+ });
762
+ return lease;
763
+ }
764
+ function waiter(executionId, instanceId, sinceVersion) {
765
+ return {
766
+ workflowId: "workflow",
767
+ executionId,
768
+ stepKey: "wait-for-message",
769
+ storeName: Store.name,
770
+ storeId: executionId,
771
+ instanceId,
772
+ sinceVersion,
773
+ readPaths: [["messages"]]
774
+ };
775
+ }
776
+ async function appendMessage(runtime, storeId, id) {
777
+ return runtime.stores.updateStore({
778
+ definition: Store,
779
+ id: storeId,
780
+ updater(draft) {
781
+ draft.messages.push({ id });
782
+ }
783
+ });
784
+ }
785
+ describe2(`${name} execution client conformance`, () => {
786
+ describe2("admission", () => {
787
+ it("reserves and returns an unused caller-supplied ID", async ({ runtime }) => {
788
+ expect2(await runtime.executions.admit({
789
+ executionId: "order-42",
790
+ workflowId: "charge-order",
791
+ params: { orderId: 42 },
792
+ context: new Map
793
+ })).toEqual({ status: "accepted", executionId: "order-42" });
794
+ expect2(await runtime.executions.get("order-42")).toMatchObject({
795
+ executionId: "order-42",
796
+ workflowId: "charge-order"
797
+ });
798
+ });
799
+ it("rejects an ID in use and leaves the existing record unchanged", async ({
800
+ runtime,
801
+ advance
802
+ }) => {
803
+ await runtime.executions.admit({
804
+ executionId: "order-42",
805
+ workflowId: "charge-order",
806
+ params: { orderId: 42 },
807
+ context: new Map([["tenant", "acme"]])
808
+ });
809
+ const before = await runtime.executions.get("order-42");
810
+ advance(10);
811
+ const conflict = await runtime.executions.admit({
812
+ executionId: "order-42",
813
+ workflowId: "refund-order",
814
+ params: { orderId: 43 },
815
+ context: new Map([["tenant", "other"]])
816
+ });
817
+ expect2(conflict).toMatchObject({
818
+ status: "rejected",
819
+ code: "EXECUTION_ID_CONFLICT"
820
+ });
821
+ expect2(conflict.status === "rejected" && typeof conflict.message).toBe("string");
822
+ const after = await runtime.executions.get("order-42");
823
+ expect2(after).toEqual(before);
824
+ expect2([...after.context.entries()]).toEqual([["tenant", "acme"]]);
825
+ expect2(await claimedIds(runtime)).toEqual(["order-42"]);
826
+ });
827
+ it("keeps the ID reserved after a delivery fails", async ({ runtime, advance }) => {
828
+ await admit(runtime, "fails");
829
+ const lease = await claimOne(runtime, "fails");
830
+ await runtime.executions.acknowledge({
831
+ lease,
832
+ outcome: { status: "failed", error: new Error("boom") }
833
+ });
834
+ expect2(await runtime.executions.admit({
835
+ executionId: "fails",
836
+ workflowId: "workflow",
837
+ params: undefined,
838
+ context: new Map
839
+ })).toMatchObject({ status: "rejected", code: "EXECUTION_ID_CONFLICT" });
840
+ await admit(runtime, "crashes");
841
+ await claimOne(runtime, "crashes");
842
+ advance(REDELIVER_AFTER2);
843
+ expect2(await runtime.executions.admit({
844
+ executionId: "crashes",
845
+ workflowId: "workflow",
846
+ params: undefined,
847
+ context: new Map
848
+ })).toMatchObject({ status: "rejected", code: "EXECUTION_ID_CONFLICT" });
849
+ });
850
+ it("chooses a new ID when none is supplied", async ({ runtime }) => {
851
+ await admit(runtime, "existing");
852
+ const ids = new Set(["existing"]);
853
+ for (let i = 0;i < 20; i++) {
854
+ const admitted = await runtime.executions.admit({
855
+ workflowId: "workflow",
856
+ params: { i },
857
+ context: new Map
858
+ });
859
+ if (admitted.status !== "accepted")
860
+ throw new Error("admission rejected");
861
+ expect2(typeof admitted.executionId).toBe("string");
862
+ expect2(admitted.executionId).not.toBe("");
863
+ expect2(ids.has(admitted.executionId)).toBe(false);
864
+ ids.add(admitted.executionId);
865
+ expect2(await runtime.executions.get(admitted.executionId)).toMatchObject({
866
+ params: { i }
867
+ });
868
+ }
869
+ });
870
+ it("stores the event once and get returns it exactly as admitted", async ({
871
+ runtime
872
+ }) => {
873
+ const params = {
874
+ text: "é\uD83D\uDE80",
875
+ int: 42,
876
+ float: 1.5,
877
+ negative: -3,
878
+ yes: true,
879
+ no: false,
880
+ nothing: null,
881
+ list: [1, "two", { three: [null] }],
882
+ nested: { a: { b: { c: "d" } } },
883
+ empty: {},
884
+ missing: undefined,
885
+ holes: [1, undefined],
886
+ tagged: { $type: "undefined" },
887
+ report: { name: "launch", message: "Ship it", stack: "web" }
888
+ };
889
+ const context = new Map([
890
+ ["tenant", "acme"],
891
+ ["count", 2],
892
+ ["7", { roles: ["admin"] }],
893
+ ["nothing", undefined]
894
+ ]);
895
+ await runtime.executions.admit({
896
+ executionId: "event",
897
+ workflowId: "charge-order",
898
+ params,
899
+ context
900
+ });
901
+ params.nested.a.b.c = "mutated";
902
+ context.set("tenant", "mutated");
903
+ const record = await runtime.executions.get("event");
904
+ expect2(record).toBeDefined();
905
+ expect2(record.workflowId).toBe("charge-order");
906
+ expect2(record.params).toEqual({
907
+ ...params,
908
+ nested: { a: { b: { c: "d" } } }
909
+ });
910
+ const ctx = record.context;
911
+ expect2(typeof ctx.get).toBe("function");
912
+ expect2(typeof ctx.has).toBe("function");
913
+ expect2(Object.keys(record.params)).toContain("missing");
914
+ expect2(record.params.report).not.toBeInstanceOf(Error);
915
+ expect2(ctx.size).toBe(4);
916
+ expect2(ctx.get("tenant")).toBe("acme");
917
+ expect2(ctx.get("7")).toEqual({ roles: ["admin"] });
918
+ expect2([...ctx.entries()]).toEqual([
919
+ ["tenant", "acme"],
920
+ ["count", 2],
921
+ ["7", { roles: ["admin"] }],
922
+ ["nothing", undefined]
923
+ ]);
924
+ await runtime.executions.admit({
925
+ executionId: "no-params",
926
+ workflowId: "workflow",
927
+ params: undefined,
928
+ context: new Map
929
+ });
930
+ const bare = await runtime.executions.get("no-params");
931
+ expect2(bare.params).toBeUndefined();
932
+ expect2(bare.context.size).toBe(0);
933
+ });
934
+ it("hands out leases that name the execution, not a copy of its event", async ({
935
+ runtime,
936
+ clock
937
+ }) => {
938
+ await runtime.executions.admit({
939
+ executionId: "delivery",
940
+ workflowId: "charge-order",
941
+ params: { orderId: 42 },
942
+ context: new Map([["tenant", "acme"]])
943
+ });
944
+ const [lease] = await claim(runtime);
945
+ expect2(Object.keys(lease).sort()).toEqual(["executionId", "expiresAt", "token"]);
946
+ expect2(lease.executionId).toBe("delivery");
947
+ expect2(typeof lease.token).toBe("number");
948
+ expect2(lease.expiresAt).toBe(clock.now + LEASE2);
949
+ const record = await runtime.executions.get(lease.executionId);
950
+ expect2(record).toMatchObject({
951
+ workflowId: "charge-order",
952
+ params: { orderId: 42 }
953
+ });
954
+ expect2(record.context.get("tenant")).toBe("acme");
955
+ });
956
+ const cyclic = {};
957
+ cyclic.self = cyclic;
958
+ const invalidEvents = [
959
+ ["a Date", { params: { at: new Date(0) }, context: new Map }],
960
+ ["a class instance", { params: { point: new Point(1) }, context: new Map }],
961
+ ["NaN", { params: { n: NaN }, context: new Map }],
962
+ ["Infinity", { params: { n: Infinity }, context: new Map }],
963
+ ["a bigint", { params: { n: 10n }, context: new Map }],
964
+ ["a function", { params: { fn: () => 1 }, context: new Map }],
965
+ ["a Map", { params: { map: new Map([["a", 1]]) }, context: new Map }],
966
+ ["a sparse array", { params: { list: [1, , 3] }, context: new Map }],
967
+ ["a cycle", { params: cyclic, context: new Map }],
968
+ ["a Date in context", { params: {}, context: new Map([["at", new Date(0)]]) }],
969
+ ["an object context key", { params: {}, context: new Map([[{}, 1]]) }],
970
+ ["a number context key", { params: {}, context: new Map([[7, 1]]) }]
971
+ ];
972
+ for (const [label, event] of invalidEvents) {
973
+ it(`rejects ${label} with INVALID_EXECUTION_EVENT and stores nothing`, async ({
974
+ runtime
975
+ }) => {
976
+ const rejected = await runtime.executions.admit({
977
+ executionId: "invalid",
978
+ workflowId: "workflow",
979
+ ...event
980
+ });
981
+ expect2(rejected).toMatchObject({
982
+ status: "rejected",
983
+ code: "INVALID_EXECUTION_EVENT"
984
+ });
985
+ expect2(await runtime.executions.get("invalid")).toBeUndefined();
986
+ expect2(await claim(runtime)).toEqual([]);
987
+ await admit(runtime, "invalid");
988
+ });
989
+ }
990
+ it("creates an admitted record that claimDue returns", async ({ runtime, clock }) => {
991
+ await admit(runtime, "admitted");
992
+ expect2(await runtime.executions.get("admitted")).toMatchObject({
993
+ status: "admitted",
994
+ admittedAt: clock.now,
995
+ updatedAt: clock.now
996
+ });
997
+ expect2((await runtime.executions.get("admitted")).outcome).toBeUndefined();
998
+ await claimOne(runtime, "admitted");
999
+ });
1000
+ });
1001
+ describe2("wakes", () => {
1002
+ it("returns an execution once when two wakes are requested before a delivery", async ({
1003
+ runtime,
1004
+ clock
1005
+ }) => {
1006
+ await admitSuspended(runtime, "twice");
1007
+ expect2(await runtime.executions.requestWake({ executionId: "twice", wakeAt: clock.now })).toEqual({ status: "requested" });
1008
+ expect2(await runtime.executions.requestWake({ executionId: "twice", wakeAt: clock.now })).toEqual({ status: "requested" });
1009
+ expect2(await claimedIds(runtime)).toEqual(["twice"]);
1010
+ await admit(runtime, "admitted-and-woken");
1011
+ await runtime.executions.requestWake({
1012
+ executionId: "admitted-and-woken",
1013
+ wakeAt: clock.now
1014
+ });
1015
+ expect2(await claimedIds(runtime)).toEqual(["admitted-and-woken"]);
1016
+ });
1017
+ it("merges wakes into the earliest requested time", async ({ runtime, clock, advance }) => {
1018
+ await admitSuspended(runtime, "earliest");
1019
+ await runtime.executions.requestWake({ executionId: "earliest", wakeAt: clock.now + 200 });
1020
+ await runtime.executions.requestWake({ executionId: "earliest", wakeAt: clock.now + 100 });
1021
+ await runtime.executions.requestWake({ executionId: "earliest", wakeAt: clock.now + 300 });
1022
+ advance(99);
1023
+ expect2(await claim(runtime)).toEqual([]);
1024
+ advance(1);
1025
+ expect2(await claimedIds(runtime)).toEqual(["earliest"]);
1026
+ });
1027
+ it("keeps a wake requested during a delivery after it acknowledges", async ({
1028
+ runtime,
1029
+ clock,
1030
+ advance
1031
+ }) => {
1032
+ await admit(runtime, "during");
1033
+ const lease = await claimOne(runtime, "during");
1034
+ await runtime.executions.requestWake({ executionId: "during", wakeAt: clock.now + 50 });
1035
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
1036
+ advance(49);
1037
+ expect2(await claim(runtime)).toEqual([]);
1038
+ advance(1);
1039
+ expect2(await claimedIds(runtime)).toEqual(["during"]);
1040
+ });
1041
+ it("makes a suspended delivery due at its wakeAt", async ({ runtime, clock, advance }) => {
1042
+ await admit(runtime, "delay");
1043
+ const lease = await claimOne(runtime, "delay");
1044
+ await runtime.executions.acknowledge({
1045
+ lease,
1046
+ outcome: { status: "suspended", wakeAt: clock.now + 500 }
1047
+ });
1048
+ advance(499);
1049
+ expect2(await claim(runtime)).toEqual([]);
1050
+ advance(1);
1051
+ expect2(await claimedIds(runtime)).toEqual(["delay"]);
1052
+ });
1053
+ it("prefers an earlier wake requested during a delivery over its wakeAt", async ({
1054
+ runtime,
1055
+ clock,
1056
+ advance
1057
+ }) => {
1058
+ await admit(runtime, "race");
1059
+ const lease = await claimOne(runtime, "race");
1060
+ await runtime.executions.requestWake({ executionId: "race", wakeAt: clock.now + 100 });
1061
+ await runtime.executions.acknowledge({
1062
+ lease,
1063
+ outcome: { status: "suspended", wakeAt: clock.now + 500 }
1064
+ });
1065
+ advance(99);
1066
+ expect2(await claim(runtime)).toEqual([]);
1067
+ advance(1);
1068
+ expect2(await claimedIds(runtime)).toEqual(["race"]);
1069
+ });
1070
+ it("does not make a suspended delivery due without wakeAt or a new wake", async ({
1071
+ runtime,
1072
+ advance
1073
+ }) => {
1074
+ await admitSuspended(runtime, "idle");
1075
+ advance(REDELIVER_AFTER2 * 10);
1076
+ expect2(await claim(runtime)).toEqual([]);
1077
+ expect2(await runtime.executions.get("idle")).toMatchObject({ status: "suspended" });
1078
+ });
1079
+ it("redelivers an unacknowledged delivery whose paused step reached wakeAt", async ({
1080
+ runtime,
1081
+ clock,
1082
+ advance
1083
+ }) => {
1084
+ await admit(runtime, "paused");
1085
+ const first = await claimOne(runtime, "paused");
1086
+ await runtime.executions.acknowledge({
1087
+ lease: first,
1088
+ outcome: { status: "suspended", wakeAt: clock.now + 100 }
1089
+ });
1090
+ advance(100);
1091
+ const second = await claimOne(runtime, "paused");
1092
+ advance(REDELIVER_AFTER2 - 1);
1093
+ expect2(await claim(runtime)).toEqual([]);
1094
+ advance(1);
1095
+ const third = await claimOne(runtime, "paused");
1096
+ expect2(third.token).toBeGreaterThan(second.token);
1097
+ expect2(await runtime.executions.acknowledge({
1098
+ lease: third,
1099
+ outcome: { status: "completed", result: "done" }
1100
+ })).toEqual({ status: "completed", result: "done" });
1101
+ });
1102
+ it("does not let a future wake requested during a past-due delivery inherit its due time", async ({
1103
+ runtime,
1104
+ clock,
1105
+ advance
1106
+ }) => {
1107
+ await admitSuspended(runtime, "past-due");
1108
+ await runtime.executions.requestWake({ executionId: "past-due", wakeAt: clock.now - 500 });
1109
+ const lease = await claimOne(runtime, "past-due");
1110
+ await runtime.executions.requestWake({ executionId: "past-due", wakeAt: clock.now + 300 });
1111
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
1112
+ expect2(await claim(runtime)).toEqual([]);
1113
+ advance(299);
1114
+ expect2(await claim(runtime)).toEqual([]);
1115
+ advance(1);
1116
+ expect2(await claimedIds(runtime)).toEqual(["past-due"]);
1117
+ });
1118
+ it("rejects a wake for an unknown execution without creating a record", async ({
1119
+ runtime,
1120
+ clock
1121
+ }) => {
1122
+ const result = await runtime.executions.requestWake({
1123
+ executionId: "ghost",
1124
+ wakeAt: clock.now
1125
+ });
1126
+ expect2(result).toMatchObject({ status: "rejected", code: "EXECUTION_NOT_FOUND" });
1127
+ expect2(result.status === "rejected" && typeof result.message).toBe("string");
1128
+ expect2(await runtime.executions.get("ghost")).toBeUndefined();
1129
+ expect2(await claim(runtime)).toEqual([]);
1130
+ await admit(runtime, "ghost");
1131
+ });
1132
+ it("does not return a leased execution again before its lease expires", async ({
1133
+ runtime,
1134
+ clock,
1135
+ advance
1136
+ }) => {
1137
+ await admit(runtime, "claimed");
1138
+ await claimOne(runtime, "claimed");
1139
+ expect2(await claim(runtime)).toEqual([]);
1140
+ await runtime.executions.requestWake({ executionId: "claimed", wakeAt: clock.now });
1141
+ advance(REDELIVER_AFTER2 - 1);
1142
+ expect2(await claim(runtime)).toEqual([]);
1143
+ advance(1);
1144
+ expect2(await claimedIds(runtime)).toEqual(["claimed"]);
1145
+ });
1146
+ it("keeps the wakes of a never-acknowledged delivery pending and redelivers it", async ({
1147
+ runtime,
1148
+ advance
1149
+ }) => {
1150
+ await admit(runtime, "lost");
1151
+ const first = await claimOne(runtime, "lost");
1152
+ advance(REDELIVER_AFTER2);
1153
+ expect2(await runtime.executions.get("lost")).toMatchObject({ status: "admitted" });
1154
+ const second = await claimOne(runtime, "lost");
1155
+ expect2(second.token).toBeGreaterThan(first.token);
1156
+ await runtime.executions.acknowledge({
1157
+ lease: second,
1158
+ outcome: { status: "suspended" }
1159
+ });
1160
+ advance(REDELIVER_AFTER2 * 10);
1161
+ expect2(await claim(runtime)).toEqual([]);
1162
+ });
1163
+ it("ignores a wake for a terminal execution", async ({ runtime, clock, advance }) => {
1164
+ await admitCompleted(runtime, "done", 1);
1165
+ await admit(runtime, "failed");
1166
+ const lease = await claimOne(runtime, "failed");
1167
+ await runtime.executions.acknowledge({
1168
+ lease,
1169
+ outcome: { status: "failed", error: new Error("boom") }
1170
+ });
1171
+ for (const executionId of ["done", "failed"]) {
1172
+ expect2(await runtime.executions.requestWake({ executionId, wakeAt: clock.now })).toEqual({ status: "ignored" });
1173
+ }
1174
+ advance(REDELIVER_AFTER2 * 10);
1175
+ expect2(await claim(runtime)).toEqual([]);
1176
+ });
1177
+ it("clears pending wakes when an execution reaches a terminal status", async ({
1178
+ runtime,
1179
+ clock,
1180
+ advance
1181
+ }) => {
1182
+ await admit(runtime, "finishing");
1183
+ const lease = await claimOne(runtime, "finishing");
1184
+ await runtime.executions.requestWake({ executionId: "finishing", wakeAt: clock.now });
1185
+ await runtime.executions.acknowledge({
1186
+ lease,
1187
+ outcome: { status: "completed", result: null }
1188
+ });
1189
+ advance(REDELIVER_AFTER2 * 10);
1190
+ expect2(await claim(runtime)).toEqual([]);
1191
+ });
1192
+ it("returns each due execution at most once per claim", async ({ runtime, clock }) => {
1193
+ for (const id of ["a", "b", "c"])
1194
+ await admit(runtime, id);
1195
+ await runtime.executions.requestWake({ executionId: "b", wakeAt: clock.now - 1 });
1196
+ await runtime.executions.requestWake({ executionId: "b", wakeAt: clock.now });
1197
+ expect2(await claimedIds(runtime)).toEqual(["a", "b", "c"]);
1198
+ expect2(await claim(runtime)).toEqual([]);
1199
+ });
1200
+ });
1201
+ describe2("store waiters", () => {
1202
+ it("wakes a waiting execution when a store commit satisfies its waiter", async ({
1203
+ runtime
1204
+ }) => {
1205
+ await admitSuspended(runtime, "consumer");
1206
+ const snapshot = await runtime.stores.getStore({
1207
+ definition: Store,
1208
+ id: "consumer"
1209
+ });
1210
+ await runtime.stores.registerWaiter(waiter("consumer", snapshot.instanceId, snapshot.version));
1211
+ expect2(await claim(runtime)).toEqual([]);
1212
+ await appendMessage(runtime, "consumer", "m1");
1213
+ expect2(await claimedIds(runtime)).toEqual(["consumer"]);
1214
+ });
1215
+ it("deletes a waiter with the write that records its step's result", async ({
1216
+ runtime,
1217
+ clock
1218
+ }) => {
1219
+ await admitSuspended(runtime, "recorded");
1220
+ const snapshot = await runtime.stores.getStore({
1221
+ definition: Store,
1222
+ id: "recorded"
1223
+ });
1224
+ const registered = waiter("recorded", snapshot.instanceId, snapshot.version);
1225
+ await runtime.stores.registerWaiter(registered);
1226
+ await runtime.executions.requestWake({ executionId: "recorded", wakeAt: clock.now });
1227
+ const waiting = await claimOne(runtime, "recorded");
1228
+ await runtime.heap.writeStep({
1229
+ lease: waiting,
1230
+ stepKey: registered.stepKey,
1231
+ stepAttempt: 1,
1232
+ done: false,
1233
+ stepResponseJson: JSON.stringify({ waiting: true })
1234
+ });
1235
+ await runtime.executions.acknowledge({
1236
+ lease: waiting,
1237
+ outcome: { status: "suspended" }
1238
+ });
1239
+ await appendMessage(runtime, "recorded", "m1");
1240
+ const lease = await claimOne(runtime, "recorded");
1241
+ await runtime.heap.writeStep({
1242
+ lease,
1243
+ stepKey: registered.stepKey,
1244
+ stepAttempt: 2,
1245
+ done: true,
1246
+ stepResponseJson: JSON.stringify({ id: "m1" })
1247
+ });
1248
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
1249
+ await appendMessage(runtime, "recorded", "m2");
1250
+ expect2(await claim(runtime)).toEqual([]);
1251
+ });
1252
+ });
1253
+ describe2("clock", () => {
1254
+ it("reads time from the runtime clock, not the wall clock", async ({
1255
+ runtime,
1256
+ clock,
1257
+ advance
1258
+ }) => {
1259
+ clock.now = 5000;
1260
+ await admit(runtime, "clocked");
1261
+ expect2(await runtime.executions.get("clocked")).toMatchObject({
1262
+ admittedAt: 5000,
1263
+ updatedAt: 5000
1264
+ });
1265
+ const lease = await claimOne(runtime, "clocked");
1266
+ advance(7);
1267
+ await runtime.executions.acknowledge({
1268
+ lease,
1269
+ outcome: { status: "suspended", wakeAt: Date.now() }
1270
+ });
1271
+ expect2(await runtime.executions.get("clocked")).toMatchObject({
1272
+ status: "suspended",
1273
+ updatedAt: 5007
1274
+ });
1275
+ expect2(await claim(runtime)).toEqual([]);
1276
+ await runtime.executions.requestWake({ executionId: "clocked", wakeAt: 5100 });
1277
+ advance(92);
1278
+ expect2(await claim(runtime)).toEqual([]);
1279
+ advance(1);
1280
+ await claimOne(runtime, "clocked");
1281
+ advance(REDELIVER_AFTER2 - 1);
1282
+ expect2(await claim(runtime)).toEqual([]);
1283
+ advance(1);
1284
+ await claimOne(runtime, "clocked");
1285
+ });
1286
+ });
1287
+ describe2("outcomes", () => {
1288
+ it("records a suspended acknowledgement without an outcome", async ({
1289
+ runtime,
1290
+ advance,
1291
+ clock
1292
+ }) => {
1293
+ await admit(runtime, "suspends");
1294
+ const lease = await claimOne(runtime, "suspends");
1295
+ advance(5);
1296
+ expect2(await runtime.executions.acknowledge({
1297
+ lease,
1298
+ outcome: { status: "suspended", wakeAt: clock.now + 1000 }
1299
+ })).toEqual({ status: "suspended" });
1300
+ const record = await runtime.executions.get("suspends");
1301
+ expect2(record).toMatchObject({
1302
+ status: "suspended",
1303
+ admittedAt: START2,
1304
+ updatedAt: START2 + 5
1305
+ });
1306
+ expect2(record.outcome).toBeUndefined();
1307
+ });
1308
+ it("records a completed acknowledgement and its result", async ({ runtime }) => {
1309
+ const result = { receipt: "r-1", amount: 12.5, lines: [{ sku: "a" }] };
1310
+ await admit(runtime, "completes");
1311
+ const lease = await claimOne(runtime, "completes");
1312
+ expect2(await runtime.executions.acknowledge({
1313
+ lease,
1314
+ outcome: { status: "completed", result }
1315
+ })).toEqual({ status: "completed", result });
1316
+ expect2(await runtime.executions.get("completes")).toMatchObject({
1317
+ status: "completed",
1318
+ outcome: { status: "completed", result }
1319
+ });
1320
+ await admitCompleted(runtime, "void", undefined);
1321
+ const voidRecord = await runtime.executions.get("void");
1322
+ expect2(voidRecord.status).toBe("completed");
1323
+ expect2(voidRecord.outcome).toEqual({ status: "completed", result: undefined });
1324
+ });
1325
+ it("records a failed acknowledgement with the error name and message", async ({
1326
+ runtime
1327
+ }) => {
1328
+ await admit(runtime, "fails");
1329
+ const lease = await claimOne(runtime, "fails");
1330
+ const acknowledged = await runtime.executions.acknowledge({
1331
+ lease,
1332
+ outcome: { status: "failed", error: new PaymentDeclinedError("card declined") }
1333
+ });
1334
+ expect2(acknowledged.status).toBe("failed");
1335
+ const record = await runtime.executions.get("fails");
1336
+ expect2(record.status).toBe("failed");
1337
+ for (const outcome of [acknowledged, record.outcome]) {
1338
+ if (outcome.status !== "failed")
1339
+ throw new Error("expected a failed outcome");
1340
+ expect2(outcome.error).toBeInstanceOf(Error);
1341
+ expect2(outcome.error.name).toBe("PaymentDeclinedError");
1342
+ expect2(outcome.error.message).toBe("card declined");
1343
+ expect2(Object.getPrototypeOf(outcome.error)).toBe(Error.prototype);
1344
+ expect2(outcome.error.code).toBe("CARD_DECLINED");
1345
+ }
1346
+ const again = await runtime.executions.get("fails");
1347
+ expect2(again.outcome).toMatchObject({ status: "failed" });
1348
+ const error = again.outcome.error;
1349
+ expect2([error.name, error.message]).toEqual(["PaymentDeclinedError", "card declined"]);
1350
+ });
1351
+ it("records a failed outcome's registered error class, code, cause and fields", async ({
1352
+ runtime
1353
+ }) => {
1354
+ await admit(runtime, "fails-registered");
1355
+ const lease = await claimOne(runtime, "fails-registered");
1356
+ const error = new GatewayTimeoutError("stripe", {
1357
+ cause: new PaymentDeclinedError("card declined")
1358
+ });
1359
+ const acknowledged = await runtime.executions.acknowledge({
1360
+ lease,
1361
+ outcome: { status: "failed", error }
1362
+ });
1363
+ const record = await runtime.executions.get("fails-registered");
1364
+ for (const outcome of [acknowledged, record.outcome]) {
1365
+ if (outcome.status !== "failed")
1366
+ throw new Error("expected a failed outcome");
1367
+ expect2(outcome.error).toBeInstanceOf(GatewayTimeoutError);
1368
+ expect2(outcome.error).toMatchObject({
1369
+ name: "ConformanceGatewayTimeoutError",
1370
+ message: "stripe timed out",
1371
+ gateway: "stripe"
1372
+ });
1373
+ expect2(outcome.error.stack).toBe(error.stack);
1374
+ const cause = outcome.error.cause;
1375
+ expect2([cause.name, cause.message, cause.code]).toEqual([
1376
+ "PaymentDeclinedError",
1377
+ "card declined",
1378
+ "CARD_DECLINED"
1379
+ ]);
1380
+ }
1381
+ });
1382
+ it("records a completed result's undefined values, errors and tag-shaped objects", async ({
1383
+ runtime
1384
+ }) => {
1385
+ const result = {
1386
+ ok: false,
1387
+ error: new TypeError("bad input"),
1388
+ missing: undefined,
1389
+ tagged: { $type: "error", name: "E", message: "m" }
1390
+ };
1391
+ await admitCompleted(runtime, "completes-tagged", result);
1392
+ const outcome = (await runtime.executions.get("completes-tagged")).outcome;
1393
+ if (outcome.status !== "completed")
1394
+ throw new Error("expected completed");
1395
+ const recorded = outcome.result;
1396
+ expect2(recorded).toEqual(result);
1397
+ expect2(Object.keys(recorded)).toContain("missing");
1398
+ expect2(recorded.error).toBeInstanceOf(TypeError);
1399
+ expect2(recorded.tagged).not.toBeInstanceOf(Error);
1400
+ });
1401
+ it("records a non-durable completed result as failed with INVALID_STEP_VALUE", async ({
1402
+ runtime
1403
+ }) => {
1404
+ await admit(runtime, "invalid-result");
1405
+ const lease = await claimOne(runtime, "invalid-result");
1406
+ const acknowledged = await runtime.executions.acknowledge({
1407
+ lease,
1408
+ outcome: { status: "completed", result: { at: new Date(0) } }
1409
+ });
1410
+ const record = await runtime.executions.get("invalid-result");
1411
+ expect2(record.status).toBe("failed");
1412
+ for (const outcome of [acknowledged, record.outcome]) {
1413
+ expect2(outcome).toMatchObject({
1414
+ status: "failed",
1415
+ error: { code: "INVALID_STEP_VALUE", stepKey: "$$workflow-result$$" }
1416
+ });
1417
+ expect2(outcome.error.name).toBe("WorkflowValueError");
1418
+ }
1419
+ expect2(await claim(runtime)).toEqual([]);
1420
+ });
1421
+ it("keeps the first terminal outcome: a later acknowledgement under the ended lease is lost", async ({
1422
+ runtime,
1423
+ clock,
1424
+ advance
1425
+ }) => {
1426
+ const lease = await admitCompleted(runtime, "first-wins", "first");
1427
+ const recorded = await runtime.executions.get("first-wins");
1428
+ advance(10);
1429
+ for (const outcome of [
1430
+ { status: "failed", error: new Error("second") },
1431
+ { status: "completed", result: "second" },
1432
+ { status: "suspended", wakeAt: clock.now }
1433
+ ]) {
1434
+ expect2(await runtime.executions.acknowledge({ lease, outcome })).toEqual({
1435
+ status: "lost"
1436
+ });
1437
+ }
1438
+ expect2(await runtime.executions.get("first-wins")).toEqual(recorded);
1439
+ advance(REDELIVER_AFTER2 * 10);
1440
+ expect2(await claim(runtime)).toEqual([]);
1441
+ });
1442
+ it("keeps a terminal outcome when a stale lease acknowledges after a reclaim", async ({
1443
+ runtime,
1444
+ advance
1445
+ }) => {
1446
+ await admit(runtime, "duplicate");
1447
+ const slow = await claimOne(runtime, "duplicate");
1448
+ advance(REDELIVER_AFTER2);
1449
+ const redelivered = await claimOne(runtime, "duplicate");
1450
+ expect2(await runtime.executions.acknowledge({
1451
+ lease: redelivered,
1452
+ outcome: { status: "completed", result: "fast" }
1453
+ })).toEqual({ status: "completed", result: "fast" });
1454
+ const recorded = await runtime.executions.get("duplicate");
1455
+ expect2(await runtime.executions.acknowledge({
1456
+ lease: slow,
1457
+ outcome: { status: "completed", result: "slow" }
1458
+ })).toEqual({ status: "lost" });
1459
+ expect2(await runtime.executions.get("duplicate")).toEqual(recorded);
1460
+ advance(REDELIVER_AFTER2 * 10);
1461
+ expect2(await claim(runtime)).toEqual([]);
1462
+ });
1463
+ it("leaves the status unchanged after an infrastructure error and redelivers", async ({
1464
+ runtime,
1465
+ clock,
1466
+ advance
1467
+ }) => {
1468
+ await admitSuspended(runtime, "infra");
1469
+ const before = await runtime.executions.get("infra");
1470
+ await runtime.executions.requestWake({ executionId: "infra", wakeAt: clock.now });
1471
+ await claimOne(runtime, "infra");
1472
+ advance(REDELIVER_AFTER2);
1473
+ expect2(await runtime.executions.get("infra")).toMatchObject({
1474
+ status: "suspended",
1475
+ updatedAt: before.updatedAt
1476
+ });
1477
+ expect2((await runtime.executions.get("infra")).outcome).toBeUndefined();
1478
+ await claimOne(runtime, "infra");
1479
+ });
1480
+ });
1481
+ describe2("leases", () => {
1482
+ it("grants exactly one lease when two clients claim one due execution concurrently", async ({
1483
+ runtime,
1484
+ harness
1485
+ }) => {
1486
+ await admit(runtime, "contended");
1487
+ const peer = await harness.createPeer?.() ?? runtime;
1488
+ const claims = await Promise.all([claim(runtime), claim(peer), claim(runtime)]);
1489
+ const leases = claims.flat();
1490
+ expect2(leases.map((lease) => lease.executionId)).toEqual(["contended"]);
1491
+ });
1492
+ it("never grants a second live lease to concurrent claimers of many executions", async ({
1493
+ runtime,
1494
+ harness
1495
+ }) => {
1496
+ const ids = Array.from({ length: 20 }, (_, i) => `many-${String(i).padStart(2, "0")}`);
1497
+ for (const id of ids)
1498
+ await admit(runtime, id);
1499
+ const peer = await harness.createPeer?.() ?? runtime;
1500
+ const claims = await Promise.all(Array.from({ length: 6 }, (_, i) => claim(i % 2 ? peer : runtime, 4)));
1501
+ const claimed = claims.flat().map((lease) => lease.executionId).sort();
1502
+ expect2(new Set(claimed).size).toBe(claimed.length);
1503
+ expect2(claimed).toEqual(ids);
1504
+ });
1505
+ it("returns at most `limit` leases, earliest due first", async ({
1506
+ runtime,
1507
+ clock,
1508
+ advance
1509
+ }) => {
1510
+ for (const id of ["late", "early", "middle"])
1511
+ await admitSuspended(runtime, id);
1512
+ await runtime.executions.requestWake({ executionId: "late", wakeAt: clock.now + 30 });
1513
+ await runtime.executions.requestWake({ executionId: "early", wakeAt: clock.now + 10 });
1514
+ await runtime.executions.requestWake({ executionId: "middle", wakeAt: clock.now + 20 });
1515
+ advance(30);
1516
+ expect2(await claim(runtime, 0)).toEqual([]);
1517
+ expect2((await claim(runtime, 2)).map((lease) => lease.executionId)).toEqual([
1518
+ "early",
1519
+ "middle"
1520
+ ]);
1521
+ expect2((await claim(runtime, 2)).map((lease) => lease.executionId)).toEqual(["late"]);
1522
+ });
1523
+ it("marks a claimed execution running", async ({ runtime }) => {
1524
+ await admit(runtime, "running");
1525
+ expect2(await runtime.executions.get("running")).toMatchObject({ status: "admitted" });
1526
+ await claimOne(runtime, "running");
1527
+ expect2(await runtime.executions.get("running")).toMatchObject({
1528
+ status: "running",
1529
+ unacknowledgedDeliveryCount: 0
1530
+ });
1531
+ });
1532
+ it("makes an execution claimable with a higher token once its lease expires", async ({
1533
+ runtime,
1534
+ advance
1535
+ }) => {
1536
+ await admit(runtime, "expires");
1537
+ const first = await claimOne(runtime, "expires");
1538
+ advance(LEASE2 - 1);
1539
+ expect2(await claim(runtime)).toEqual([]);
1540
+ expect2(await runtime.executions.get("expires")).toMatchObject({ status: "running" });
1541
+ advance(REDELIVER_AFTER2 - LEASE2 + 1);
1542
+ const second = await claimOne(runtime, "expires");
1543
+ expect2(second.token).toBeGreaterThan(first.token);
1544
+ expect2(await runtime.executions.get("expires")).toMatchObject({
1545
+ status: "running",
1546
+ unacknowledgedDeliveryCount: 1
1547
+ });
1548
+ });
1549
+ it("returns an expired lease's execution to admitted before any acknowledgement", async ({
1550
+ runtime,
1551
+ advance
1552
+ }) => {
1553
+ await admit(runtime, "never-acked");
1554
+ await claimOne(runtime, "never-acked");
1555
+ advance(LEASE2);
1556
+ expect2(await runtime.executions.get("never-acked")).toMatchObject({
1557
+ status: "admitted",
1558
+ unacknowledgedDeliveryCount: 1
1559
+ });
1560
+ });
1561
+ it("returns an expired lease's execution to suspended after an earlier suspended delivery", async ({
1562
+ runtime,
1563
+ clock,
1564
+ advance
1565
+ }) => {
1566
+ await admitSuspended(runtime, "was-suspended");
1567
+ await runtime.executions.requestWake({ executionId: "was-suspended", wakeAt: clock.now });
1568
+ await claimOne(runtime, "was-suspended");
1569
+ advance(LEASE2);
1570
+ expect2(await runtime.executions.get("was-suspended")).toMatchObject({
1571
+ status: "suspended",
1572
+ unacknowledgedDeliveryCount: 1
1573
+ });
1574
+ });
1575
+ it("keeps a renewed lease past its original expiry with one claim", async ({
1576
+ runtime,
1577
+ clock,
1578
+ advance
1579
+ }) => {
1580
+ await admit(runtime, "long");
1581
+ let lease = await claimOne(runtime, "long");
1582
+ for (let i = 0;i < 5; i++) {
1583
+ advance(LEASE2 - 1);
1584
+ const renewed = await runtime.executions.renew({ lease, duration: LEASE2 });
1585
+ expect2(renewed).toEqual({
1586
+ status: "renewed",
1587
+ lease: { executionId: "long", token: lease.token, expiresAt: clock.now + LEASE2 }
1588
+ });
1589
+ if (renewed.status !== "renewed")
1590
+ throw new Error("renewal lost");
1591
+ lease = renewed.lease;
1592
+ expect2(await claim(runtime)).toEqual([]);
1593
+ expect2(await runtime.executions.get("long")).toMatchObject({ status: "running" });
1594
+ }
1595
+ expect2(await runtime.executions.acknowledge({
1596
+ lease,
1597
+ outcome: { status: "completed", result: "done" }
1598
+ })).toEqual({ status: "completed", result: "done" });
1599
+ expect2(await runtime.executions.get("long")).toMatchObject({
1600
+ status: "completed",
1601
+ unacknowledgedDeliveryCount: 0
1602
+ });
1603
+ });
1604
+ it("returns lost when renewing with a stale token", async ({ runtime, advance }) => {
1605
+ await admit(runtime, "stale-renew");
1606
+ const stale = await claimOne(runtime, "stale-renew");
1607
+ advance(REDELIVER_AFTER2);
1608
+ const current = await claimOne(runtime, "stale-renew");
1609
+ expect2(await runtime.executions.renew({ lease: stale, duration: LEASE2 })).toEqual({
1610
+ status: "lost"
1611
+ });
1612
+ advance(LEASE2);
1613
+ expect2(await runtime.executions.get("stale-renew")).toMatchObject({
1614
+ status: "admitted"
1615
+ });
1616
+ expect2(await runtime.executions.renew({ lease: current, duration: LEASE2 })).toMatchObject({
1617
+ status: "renewed"
1618
+ });
1619
+ });
1620
+ it("keeps an expired lease current until another claim issues a higher token", async ({
1621
+ runtime,
1622
+ clock,
1623
+ advance
1624
+ }) => {
1625
+ await admit(runtime, "expired-unclaimed");
1626
+ const lease = await claimOne(runtime, "expired-unclaimed");
1627
+ advance(REDELIVER_AFTER2 * 3);
1628
+ expect2(await runtime.heap.writeStep({
1629
+ lease,
1630
+ stepKey: "step",
1631
+ stepAttempt: 0,
1632
+ done: true,
1633
+ stepResponseJson: JSON.stringify({ value: 1 })
1634
+ })).toEqual({ stepResponseJson: JSON.stringify({ value: 1 }), meta: { attempt: 0, done: true } });
1635
+ const renewed = await runtime.executions.renew({ lease, duration: LEASE2 });
1636
+ expect2(renewed).toEqual({
1637
+ status: "renewed",
1638
+ lease: { ...lease, expiresAt: clock.now + LEASE2 }
1639
+ });
1640
+ expect2(await runtime.executions.get("expired-unclaimed")).toMatchObject({
1641
+ status: "running",
1642
+ unacknowledgedDeliveryCount: 0
1643
+ });
1644
+ expect2(await claim(runtime)).toEqual([]);
1645
+ expect2(await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } })).toEqual({ status: "suspended" });
1646
+ });
1647
+ it("makes a released execution claimable after the redelivery delay with a higher token", async ({
1648
+ runtime,
1649
+ advance
1650
+ }) => {
1651
+ await admitSuspended(runtime, "released");
1652
+ const before = await runtime.executions.get("released");
1653
+ await runtime.executions.requestWake({ executionId: "released", wakeAt: START2 });
1654
+ const first = await claimOne(runtime, "released");
1655
+ expect2(await runtime.executions.release({ lease: first })).toEqual({
1656
+ status: "released"
1657
+ });
1658
+ const released = await runtime.executions.get("released");
1659
+ expect2(released).toMatchObject({ status: "suspended", unacknowledgedDeliveryCount: 1 });
1660
+ expect2(released.outcome).toBeUndefined();
1661
+ expect2(released.updatedAt).toBe(before.updatedAt);
1662
+ expect2(await runtime.executions.release({ lease: first })).toEqual({ status: "lost" });
1663
+ expect2(await runtime.executions.renew({ lease: first, duration: LEASE2 })).toEqual({
1664
+ status: "lost"
1665
+ });
1666
+ expect2(await runtime.executions.acknowledge({ lease: first, outcome: { status: "suspended" } })).toEqual({ status: "lost" });
1667
+ advance(redeliveryDelay2(1) - 1);
1668
+ expect2(await claim(runtime)).toEqual([]);
1669
+ advance(1);
1670
+ const second = await claimOne(runtime, "released");
1671
+ expect2(second.token).toBeGreaterThan(first.token);
1672
+ });
1673
+ it("ends the lease on a suspended acknowledgement and makes the execution due at a wake requested during it", async ({
1674
+ runtime,
1675
+ clock,
1676
+ advance
1677
+ }) => {
1678
+ await admit(runtime, "woken-during");
1679
+ const lease = await claimOne(runtime, "woken-during");
1680
+ await runtime.executions.requestWake({ executionId: "woken-during", wakeAt: clock.now + 50 });
1681
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
1682
+ expect2(await runtime.executions.get("woken-during")).toMatchObject({
1683
+ status: "suspended"
1684
+ });
1685
+ expect2(await runtime.executions.renew({ lease, duration: LEASE2 })).toEqual({
1686
+ status: "lost"
1687
+ });
1688
+ advance(49);
1689
+ expect2(await claim(runtime)).toEqual([]);
1690
+ advance(1);
1691
+ const next = await claimOne(runtime, "woken-during");
1692
+ expect2(next.token).toBeGreaterThan(lease.token);
1693
+ });
1694
+ it("ends the lease on a suspended acknowledgement and makes the execution due at wakeAt", async ({
1695
+ runtime,
1696
+ clock,
1697
+ advance
1698
+ }) => {
1699
+ await admit(runtime, "wake-at");
1700
+ const lease = await claimOne(runtime, "wake-at");
1701
+ await runtime.executions.acknowledge({
1702
+ lease,
1703
+ outcome: { status: "suspended", wakeAt: clock.now + 70 }
1704
+ });
1705
+ expect2(await runtime.executions.renew({ lease, duration: LEASE2 })).toEqual({
1706
+ status: "lost"
1707
+ });
1708
+ advance(69);
1709
+ expect2(await claim(runtime)).toEqual([]);
1710
+ advance(1);
1711
+ await claimOne(runtime, "wake-at");
1712
+ });
1713
+ it("ends the lease on a suspended acknowledgement and leaves the execution not due without a wake", async ({
1714
+ runtime,
1715
+ advance
1716
+ }) => {
1717
+ await admit(runtime, "not-due");
1718
+ const lease = await claimOne(runtime, "not-due");
1719
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
1720
+ expect2(await runtime.executions.release({ lease })).toEqual({ status: "lost" });
1721
+ advance(REDELIVER_AFTER2 * 10);
1722
+ expect2(await claim(runtime)).toEqual([]);
1723
+ expect2(await runtime.executions.get("not-due")).toMatchObject({
1724
+ status: "suspended",
1725
+ unacknowledgedDeliveryCount: 0
1726
+ });
1727
+ });
1728
+ it("returns lost for a lease on an unknown execution", async ({ runtime }) => {
1729
+ const ghost = { executionId: "ghost", token: 1, expiresAt: START2 + LEASE2 };
1730
+ expect2(await runtime.executions.renew({ lease: ghost, duration: LEASE2 })).toEqual({
1731
+ status: "lost"
1732
+ });
1733
+ expect2(await runtime.executions.release({ lease: ghost })).toEqual({ status: "lost" });
1734
+ expect2(await runtime.executions.acknowledge({ lease: ghost, outcome: { status: "suspended" } })).toEqual({ status: "lost" });
1735
+ expect2(await runtime.heap.writeStep({
1736
+ lease: ghost,
1737
+ stepKey: "step",
1738
+ stepAttempt: 0,
1739
+ done: true,
1740
+ stepResponseJson: "{}"
1741
+ })).toEqual({ status: "lost" });
1742
+ expect2(await runtime.executions.get("ghost")).toBeUndefined();
1743
+ });
1744
+ itAfterRestart("keeps leases and tokens across a restart", async ({
1745
+ runtime,
1746
+ advance,
1747
+ restart
1748
+ }) => {
1749
+ await admit(runtime, "held");
1750
+ const held = await claimOne(runtime, "held");
1751
+ const restarted = await restart();
1752
+ expect2(await restarted.executions.get("held")).toMatchObject({ status: "running" });
1753
+ expect2(await claim(restarted)).toEqual([]);
1754
+ expect2(await restarted.executions.renew({ lease: held, duration: LEASE2 })).toMatchObject({
1755
+ status: "renewed"
1756
+ });
1757
+ advance(REDELIVER_AFTER2);
1758
+ const next = await claimOne(restarted, "held");
1759
+ expect2(next.token).toBeGreaterThan(held.token);
1760
+ expect2(await restarted.executions.release({ lease: held })).toEqual({ status: "lost" });
1761
+ });
1762
+ });
1763
+ describe2("redelivery delay", () => {
1764
+ it("delays each redelivery after a release by a non-zero delay no shorter than the last", async ({
1765
+ runtime,
1766
+ clock,
1767
+ advance
1768
+ }) => {
1769
+ await admit(runtime, "retrying");
1770
+ let lease = await claimOne(runtime, "retrying");
1771
+ let previousDelay = 0;
1772
+ for (let attempt = 1;attempt <= 6; attempt++) {
1773
+ await runtime.executions.release({ lease });
1774
+ expect2(await runtime.executions.get("retrying")).toMatchObject({
1775
+ unacknowledgedDeliveryCount: attempt
1776
+ });
1777
+ const releasedAt = clock.now;
1778
+ let next;
1779
+ while ((next = await claim(runtime)).length === 0) {
1780
+ advance(100);
1781
+ if (clock.now - releasedAt > 60 * 60000)
1782
+ throw new Error("never redelivered");
1783
+ }
1784
+ const delay = clock.now - releasedAt;
1785
+ expect2(delay).toBeGreaterThan(0);
1786
+ expect2(delay).toBeGreaterThanOrEqual(previousDelay);
1787
+ previousDelay = delay;
1788
+ lease = next[0];
1789
+ }
1790
+ });
1791
+ it("counts both a release and a later expiry", async ({ runtime, advance }) => {
1792
+ await admit(runtime, "both");
1793
+ const released = await claimOne(runtime, "both");
1794
+ await runtime.executions.release({ lease: released });
1795
+ advance(redeliveryDelay2(1));
1796
+ await claimOne(runtime, "both");
1797
+ advance(LEASE2);
1798
+ expect2(await runtime.executions.get("both")).toMatchObject({
1799
+ unacknowledgedDeliveryCount: 2
1800
+ });
1801
+ advance(redeliveryDelay2(2) - 1);
1802
+ expect2(await claim(runtime)).toEqual([]);
1803
+ advance(1);
1804
+ await claimOne(runtime, "both");
1805
+ expect2(await runtime.executions.get("both")).toMatchObject({
1806
+ unacknowledgedDeliveryCount: 2
1807
+ });
1808
+ });
1809
+ it("resets the count on acknowledgement, so the next wake is not delayed", async ({
1810
+ runtime,
1811
+ clock,
1812
+ advance
1813
+ }) => {
1814
+ await admit(runtime, "recovers");
1815
+ for (let i = 1;i <= 3; i++) {
1816
+ const lease = await claimOne(runtime, "recovers");
1817
+ await runtime.executions.release({ lease });
1818
+ advance(redeliveryDelay2(i));
1819
+ }
1820
+ expect2(await runtime.executions.get("recovers")).toMatchObject({
1821
+ unacknowledgedDeliveryCount: 3
1822
+ });
1823
+ const lease = await claimOne(runtime, "recovers");
1824
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
1825
+ expect2(await runtime.executions.get("recovers")).toMatchObject({
1826
+ status: "suspended",
1827
+ unacknowledgedDeliveryCount: 0
1828
+ });
1829
+ await runtime.executions.requestWake({ executionId: "recovers", wakeAt: clock.now });
1830
+ await claimOne(runtime, "recovers");
1831
+ });
1832
+ it("holds wakes requested during the delay until it passes", async ({
1833
+ runtime,
1834
+ clock,
1835
+ advance
1836
+ }) => {
1837
+ await admit(runtime, "held-back");
1838
+ const lease = await claimOne(runtime, "held-back");
1839
+ await runtime.executions.release({ lease });
1840
+ await runtime.executions.requestWake({ executionId: "held-back", wakeAt: clock.now });
1841
+ expect2(await claim(runtime)).toEqual([]);
1842
+ advance(redeliveryDelay2(1));
1843
+ await claimOne(runtime, "held-back");
1844
+ });
1845
+ });
1846
+ describe2("fencing", () => {
1847
+ async function reclaim(ctx, executionId) {
1848
+ const stale = await claimOne(ctx.runtime, executionId);
1849
+ ctx.advance(REDELIVER_AFTER2);
1850
+ const current = await claimOne(ctx.runtime, executionId);
1851
+ expect2(current.token).toBeGreaterThan(stale.token);
1852
+ return { stale, current };
1853
+ }
1854
+ function write(runtime, lease, stepKey, value, options = {}) {
1855
+ return runtime.heap.writeStep({
1856
+ lease,
1857
+ stepKey,
1858
+ stepAttempt: options.attempt ?? 0,
1859
+ done: options.done ?? true,
1860
+ stepResponseJson: JSON.stringify(value)
1861
+ });
1862
+ }
1863
+ it("rejects a stale holder's heap write after a reclaim and keeps the current records", async (ctx) => {
1864
+ const { runtime } = ctx;
1865
+ await admit(runtime, "stale-write");
1866
+ const { stale, current } = await reclaim(ctx, "stale-write");
1867
+ expect2(await write(runtime, current, "charge", "current")).toMatchObject({
1868
+ stepResponseJson: JSON.stringify("current")
1869
+ });
1870
+ expect2(await write(runtime, stale, "charge", "stale")).toEqual({ status: "lost" });
1871
+ expect2(await write(runtime, stale, "charge", "stale", { attempt: 1 })).toEqual({
1872
+ status: "lost"
1873
+ });
1874
+ expect2(await write(runtime, stale, "other", "stale")).toEqual({ status: "lost" });
1875
+ expect2(await runtime.heap.readStep({ executionId: "stale-write", stepKey: "charge" })).toEqual({
1876
+ stepResponseJson: JSON.stringify("current"),
1877
+ meta: { attempt: 0, done: true }
1878
+ });
1879
+ expect2(await runtime.heap.readStep({ executionId: "stale-write", stepKey: "other" })).toBeNull();
1880
+ });
1881
+ it("rejects a stale holder's acknowledgement after a reclaim and leaves the record unchanged", async (ctx) => {
1882
+ const { runtime } = ctx;
1883
+ await admitSuspended(runtime, "stale-ack");
1884
+ await runtime.executions.requestWake({ executionId: "stale-ack", wakeAt: ctx.clock.now });
1885
+ const { stale, current } = await reclaim(ctx, "stale-ack");
1886
+ const before = await runtime.executions.get("stale-ack");
1887
+ for (const outcome of [
1888
+ { status: "completed", result: "stale" },
1889
+ { status: "failed", error: new Error("stale") },
1890
+ { status: "suspended", wakeAt: ctx.clock.now }
1891
+ ]) {
1892
+ expect2(await runtime.executions.acknowledge({ lease: stale, outcome })).toEqual({
1893
+ status: "lost"
1894
+ });
1895
+ }
1896
+ expect2(await runtime.executions.get("stale-ack")).toEqual(before);
1897
+ expect2(before).toMatchObject({ status: "running" });
1898
+ expect2(await runtime.executions.acknowledge({
1899
+ lease: current,
1900
+ outcome: { status: "completed", result: "current" }
1901
+ })).toEqual({ status: "completed", result: "current" });
1902
+ });
1903
+ it("rejects every fenced write from a holder that pauses past expiry after another claims", async (ctx) => {
1904
+ const { runtime } = ctx;
1905
+ await admit(runtime, "paused");
1906
+ const paused = await claimOne(runtime, "paused");
1907
+ expect2(await write(runtime, paused, "before-pause", 1)).toMatchObject({
1908
+ meta: { attempt: 0, done: true }
1909
+ });
1910
+ ctx.advance(REDELIVER_AFTER2);
1911
+ const peer = await ctx.harness.createPeer?.() ?? runtime;
1912
+ const current = await claimOne(peer, "paused");
1913
+ expect2(await write(runtime, paused, "after-pause", 2)).toEqual({ status: "lost" });
1914
+ expect2(await runtime.executions.renew({ lease: paused, duration: LEASE2 })).toEqual({
1915
+ status: "lost"
1916
+ });
1917
+ expect2(await runtime.executions.release({ lease: paused })).toEqual({ status: "lost" });
1918
+ expect2(await runtime.executions.acknowledge({
1919
+ lease: paused,
1920
+ outcome: { status: "completed", result: "paused" }
1921
+ })).toEqual({ status: "lost" });
1922
+ expect2(await write(peer, current, "after-pause", 3)).toMatchObject({
1923
+ stepResponseJson: "3"
1924
+ });
1925
+ expect2(await peer.executions.get("paused")).toMatchObject({ status: "running" });
1926
+ });
1927
+ it("accepts the current holder's rewrite of an existing step record without changing it", async ({
1928
+ runtime
1929
+ }) => {
1930
+ await admit(runtime, "rewrite");
1931
+ const lease = await claimOne(runtime, "rewrite");
1932
+ const first = await write(runtime, lease, "charge", { receipt: "r-1" });
1933
+ const again = await write(runtime, lease, "charge", { receipt: "r-1" });
1934
+ expect2(again).toEqual(first);
1935
+ expect2(await runtime.heap.readStep({ executionId: "rewrite", stepKey: "charge" })).toEqual(first);
1936
+ });
1937
+ it("stores scalar step values as the exact JSON written, so rewrites match and falsy values replay", async ({
1938
+ runtime
1939
+ }) => {
1940
+ await admit(runtime, "scalars");
1941
+ const lease = await claimOne(runtime, "scalars");
1942
+ for (const [stepKey, value] of [
1943
+ ["zero", 0],
1944
+ ["one", 1],
1945
+ ["float", 1.5],
1946
+ ["false", false],
1947
+ ["null", null],
1948
+ ["numeric-string", "1"]
1949
+ ]) {
1950
+ const record = {
1951
+ stepResponseJson: JSON.stringify(value),
1952
+ meta: { attempt: 0, done: true }
1953
+ };
1954
+ expect2(await write(runtime, lease, stepKey, value)).toEqual(record);
1955
+ expect2(await write(runtime, lease, stepKey, value)).toEqual(record);
1956
+ expect2(await runtime.heap.readStep({ executionId: "scalars", stepKey })).toEqual(record);
1957
+ }
1958
+ });
1959
+ it("keeps an existing step record when the current holder writes a different value, and returns the recorded one", async ({
1960
+ runtime
1961
+ }) => {
1962
+ await admit(runtime, "conflict");
1963
+ const lease = await claimOne(runtime, "conflict");
1964
+ await write(runtime, lease, "charge", { receipt: "r-1" });
1965
+ expect2(await write(runtime, lease, "charge", { receipt: "r-2" }, { done: false })).toEqual({
1966
+ stepResponseJson: JSON.stringify({ receipt: "r-1" }),
1967
+ meta: { attempt: 0, done: true }
1968
+ });
1969
+ expect2(await runtime.heap.readStep({ executionId: "conflict", stepKey: "charge" })).toEqual({
1970
+ stepResponseJson: JSON.stringify({ receipt: "r-1" }),
1971
+ meta: { attempt: 0, done: true }
1972
+ });
1973
+ await runtime.executions.release({ lease });
1974
+ expect2(await runtime.heap.readStep({ executionId: "conflict", stepKey: "charge" })).toEqual({
1975
+ stepResponseJson: JSON.stringify({ receipt: "r-1" }),
1976
+ meta: { attempt: 0, done: true }
1977
+ });
1978
+ });
1979
+ it("keeps a waiter when a stale holder's step result would delete it", async (ctx) => {
1980
+ const { runtime } = ctx;
1981
+ await admitSuspended(runtime, "stale-waiter");
1982
+ const snapshot = await runtime.stores.getStore({
1983
+ definition: Store,
1984
+ id: "stale-waiter"
1985
+ });
1986
+ const registered = waiter("stale-waiter", snapshot.instanceId, snapshot.version);
1987
+ await runtime.stores.registerWaiter(registered);
1988
+ await runtime.executions.requestWake({ executionId: "stale-waiter", wakeAt: ctx.clock.now });
1989
+ const { stale, current } = await reclaim(ctx, "stale-waiter");
1990
+ expect2(await write(runtime, stale, registered.stepKey, { id: "m1" }, { attempt: 1 })).toEqual({ status: "lost" });
1991
+ await runtime.executions.acknowledge({ lease: current, outcome: { status: "suspended" } });
1992
+ await appendMessage(runtime, "stale-waiter", "m1");
1993
+ expect2(await claimedIds(runtime)).toEqual(["stale-waiter"]);
1994
+ });
1995
+ it("applies a stale holder's idempotent store update once and returns the recorded result to the current holder", async (ctx) => {
1996
+ const { runtime } = ctx;
1997
+ await admit(runtime, "store-receipt");
1998
+ await runtime.stores.getStore({
1999
+ definition: Store,
2000
+ id: "store-receipt"
2001
+ });
2002
+ const { stale, current } = await reclaim(ctx, "store-receipt");
2003
+ const stepId = { executionId: "store-receipt", stepKey: "append" };
2004
+ const staleResult = await runtime.stores.updateStore({
2005
+ definition: Store,
2006
+ id: "store-receipt",
2007
+ stepId,
2008
+ updater(draft) {
2009
+ draft.messages.push({ id: "stale" });
2010
+ }
2011
+ });
2012
+ const currentResult = await runtime.stores.updateStore({
2013
+ definition: Store,
2014
+ id: "store-receipt",
2015
+ stepId,
2016
+ updater(draft) {
2017
+ draft.messages.push({ id: "current" });
2018
+ }
2019
+ });
2020
+ expect2(currentResult).toEqual(staleResult);
2021
+ expect2(await runtime.stores.getStore({ definition: Store, id: "store-receipt" })).toMatchObject({ state: { messages: [{ id: "stale" }] }, version: 1 });
2022
+ expect2(await write(runtime, stale, "append", staleResult)).toEqual({ status: "lost" });
2023
+ expect2(await write(runtime, current, "append", currentResult)).toMatchObject({
2024
+ meta: { done: true }
2025
+ });
2026
+ });
2027
+ it("lets a stale holder's wake cause at most one extra delivery and leaves the outcome unchanged", async (ctx) => {
2028
+ const { runtime } = ctx;
2029
+ await admit(runtime, "stale-wake");
2030
+ const { current } = await reclaim(ctx, "stale-wake");
2031
+ await runtime.executions.acknowledge({ lease: current, outcome: { status: "suspended" } });
2032
+ await runtime.executions.requestWake({ executionId: "stale-wake", wakeAt: ctx.clock.now });
2033
+ const extra = await claimOne(runtime, "stale-wake");
2034
+ await runtime.executions.acknowledge({ lease: extra, outcome: { status: "suspended" } });
2035
+ ctx.advance(REDELIVER_AFTER2 * 10);
2036
+ expect2(await claim(runtime)).toEqual([]);
2037
+ expect2(await runtime.executions.get("stale-wake")).toMatchObject({
2038
+ status: "suspended",
2039
+ unacknowledgedDeliveryCount: 0
2040
+ });
2041
+ await admit(runtime, "stale-wake-done");
2042
+ const done = await reclaim(ctx, "stale-wake-done");
2043
+ await runtime.executions.acknowledge({
2044
+ lease: done.current,
2045
+ outcome: { status: "completed", result: "ok" }
2046
+ });
2047
+ expect2(await runtime.executions.requestWake({
2048
+ executionId: "stale-wake-done",
2049
+ wakeAt: ctx.clock.now
2050
+ })).toEqual({ status: "ignored" });
2051
+ expect2(await claim(runtime)).toEqual([]);
2052
+ });
2053
+ });
2054
+ describe2("start", () => {
2055
+ it("leaves no trace when a new start fails before it commits", async ({
2056
+ runtime,
2057
+ harness
2058
+ }, testContext) => {
2059
+ if (!harness.failStartCommits) {
2060
+ testContext.skip();
2061
+ return;
2062
+ }
2063
+ await admitSuspended(runtime, "caller");
2064
+ const restore = await harness.failStartCommits();
2065
+ try {
2066
+ await expect2(startFrom(runtime, "caller", "ship")).rejects.toThrow();
2067
+ } finally {
2068
+ await restore();
2069
+ }
2070
+ expect2(await claim(runtime)).toEqual([]);
2071
+ const started = await startFrom(runtime, "caller", "ship");
2072
+ expect2(started.status).toBe("accepted");
2073
+ const targetId = acceptedId(started);
2074
+ expect2(await runtime.executions.get(targetId)).toMatchObject({
2075
+ status: "admitted",
2076
+ parent: { executionId: "caller", stepKey: "ship" }
2077
+ });
2078
+ expect2(await claimedIds(runtime)).toEqual([targetId]);
2079
+ });
2080
+ it("returns the committed target when a start is repeated after a lost response", async ({
2081
+ runtime,
2082
+ harness
2083
+ }) => {
2084
+ await admitSuspended(runtime, "caller");
2085
+ const first = acceptedId(await startFrom(runtime, "caller", "ship"));
2086
+ const peer = await harness.createPeer?.() ?? runtime;
2087
+ expect2(await startFrom(peer, "caller", "ship")).toEqual({
2088
+ status: "accepted",
2089
+ executionId: first
2090
+ });
2091
+ expect2(await startFrom(runtime, "caller", "ship")).toEqual({
2092
+ status: "accepted",
2093
+ executionId: first
2094
+ });
2095
+ expect2(await claimedIds(runtime)).toEqual([first]);
2096
+ });
2097
+ it("admits one target for two concurrent calls with one request ID", async ({
2098
+ runtime,
2099
+ harness
2100
+ }) => {
2101
+ await admitSuspended(runtime, "caller");
2102
+ const peer = await harness.createPeer?.() ?? runtime;
2103
+ const [a, b] = await Promise.all([
2104
+ startFrom(runtime, "caller", "ship"),
2105
+ startFrom(peer, "caller", "ship")
2106
+ ]);
2107
+ expect2(a.status).toBe("accepted");
2108
+ expect2(b).toEqual(a);
2109
+ expect2(await claimedIds(runtime)).toEqual([acceptedId(a)]);
2110
+ });
2111
+ it("gives different request IDs different execution IDs", async ({ runtime }) => {
2112
+ await admitSuspended(runtime, "caller-a");
2113
+ await admitSuspended(runtime, "caller-b");
2114
+ const ids = [
2115
+ acceptedId(await startFrom(runtime, "caller-a", "ship")),
2116
+ acceptedId(await startFrom(runtime, "caller-a", "bill")),
2117
+ acceptedId(await startFrom(runtime, "caller-b", "ship"))
2118
+ ];
2119
+ expect2(new Set([...ids, "caller-a", "caller-b"]).size).toBe(5);
2120
+ expect2(await claimedIds(runtime)).toEqual([...ids].sort());
2121
+ });
2122
+ it("admits the target due, with the caller as its parent", async ({ runtime, clock }) => {
2123
+ await admitSuspended(runtime, "caller");
2124
+ const targetId = acceptedId(await runtime.executions.start({
2125
+ requestId: { executionId: "caller", stepKey: "ship" },
2126
+ workflowId: "ship-order",
2127
+ params: { orderId: 42, items: [{ sku: "a", qty: 1 }] }
2128
+ }));
2129
+ const target = await runtime.executions.get(targetId);
2130
+ expect2(target).toMatchObject({
2131
+ executionId: targetId,
2132
+ workflowId: "ship-order",
2133
+ params: { orderId: 42, items: [{ sku: "a", qty: 1 }] },
2134
+ status: "admitted",
2135
+ admittedAt: clock.now,
2136
+ parent: { executionId: "caller", stepKey: "ship" }
2137
+ });
2138
+ expect2(await claimedIds(runtime)).toEqual([targetId]);
2139
+ const undefinedParams = acceptedId(await startFrom(runtime, "caller", "ship-undefined", {
2140
+ params: { orderId: 42, note: undefined }
2141
+ }));
2142
+ const withUndefined = (await runtime.executions.get(undefinedParams)).params;
2143
+ expect2(withUndefined).toEqual({ orderId: 42, note: undefined });
2144
+ expect2(Object.keys(withUndefined)).toContain("note");
2145
+ expect2(await runtime.executions.get("caller")).not.toHaveProperty("parent");
2146
+ const bare = acceptedId(await startFrom(runtime, "caller", "bare", { params: undefined }));
2147
+ expect2((await runtime.executions.get(bare)).params).toBeUndefined();
2148
+ });
2149
+ it("admits the target with the caller's context", async ({ runtime }) => {
2150
+ await runtime.executions.admit({
2151
+ executionId: "caller",
2152
+ workflowId: "charge-order",
2153
+ params: undefined,
2154
+ context: new Map([
2155
+ ["tenant", "acme"],
2156
+ ["7", { roles: ["admin"] }],
2157
+ ["nothing", undefined]
2158
+ ])
2159
+ });
2160
+ const targetId = acceptedId(await startFrom(runtime, "caller", "ship"));
2161
+ const target = await runtime.executions.get(targetId);
2162
+ expect2([...target.context.entries()]).toEqual([
2163
+ ["tenant", "acme"],
2164
+ ["7", { roles: ["admin"] }],
2165
+ ["nothing", undefined]
2166
+ ]);
2167
+ });
2168
+ it("returns the original target when a repeat differs only in context", async ({
2169
+ runtime
2170
+ }) => {
2171
+ await runtime.executions.admit({
2172
+ executionId: "caller",
2173
+ workflowId: "charge-order",
2174
+ params: undefined,
2175
+ context: new Map([["tenant", "acme"]])
2176
+ });
2177
+ const request = {
2178
+ requestId: { executionId: "caller", stepKey: "ship" },
2179
+ workflowId: "ship-order",
2180
+ params: { orderId: 42 }
2181
+ };
2182
+ const first = acceptedId(await runtime.executions.start(request));
2183
+ expect2(await runtime.executions.start({
2184
+ ...request,
2185
+ context: new Map([["tenant", "other"]])
2186
+ })).toEqual({ status: "accepted", executionId: first });
2187
+ expect2([...(await runtime.executions.get(first)).context.entries()]).toEqual([["tenant", "acme"]]);
2188
+ });
2189
+ const cyclic = {};
2190
+ cyclic.self = cyclic;
2191
+ const accessor = Object.defineProperty({}, "value", {
2192
+ get: () => 1,
2193
+ enumerable: true
2194
+ });
2195
+ const hidden = Object.defineProperty({}, "value", { value: 1, enumerable: false });
2196
+ const invalidParams = [
2197
+ ["a Date", { at: new Date(0) }],
2198
+ ["a class instance", { point: new Point(1) }],
2199
+ ["NaN", { n: NaN }],
2200
+ ["Infinity", { n: Infinity }],
2201
+ ["a bigint", { n: 10n }],
2202
+ ["a function", { fn: () => 1 }],
2203
+ ["a symbol", { s: Symbol("s") }],
2204
+ ["a symbol key", { [Symbol("s")]: 1 }],
2205
+ ["an accessor", { nested: accessor }],
2206
+ ["a non-enumerable property", { nested: hidden }],
2207
+ ["a Map", { map: new Map([["a", 1]]) }],
2208
+ ["a sparse array", { list: [1, , 3] }],
2209
+ ["a cycle", cyclic],
2210
+ ["an array as params", [1, 2]],
2211
+ ["null as params", null],
2212
+ ["a string as params", "params"],
2213
+ ["a class instance as params", new Point(1)]
2214
+ ];
2215
+ for (const [label, params] of invalidParams) {
2216
+ it(`rejects ${label} with INVALID_START_PARAMETERS and admits nothing`, async ({
2217
+ runtime
2218
+ }) => {
2219
+ await admitSuspended(runtime, "caller");
2220
+ const rejected = await runtime.executions.start({
2221
+ requestId: { executionId: "caller", stepKey: "ship" },
2222
+ workflowId: "ship-order",
2223
+ params
2224
+ });
2225
+ expect2(rejected).toMatchObject({
2226
+ status: "rejected",
2227
+ code: "INVALID_START_PARAMETERS"
2228
+ });
2229
+ expect2(rejected.status === "rejected" && typeof rejected.message).toBe("string");
2230
+ expect2(await claim(runtime)).toEqual([]);
2231
+ expect2((await startFrom(runtime, "caller", "ship")).status).toBe("accepted");
2232
+ });
2233
+ }
2234
+ it("rejects an empty workflow ID with INVALID_START_PARAMETERS and admits nothing", async ({
2235
+ runtime
2236
+ }) => {
2237
+ await admitSuspended(runtime, "caller");
2238
+ expect2(await startFrom(runtime, "caller", "ship", { workflowId: "" })).toMatchObject({
2239
+ status: "rejected",
2240
+ code: "INVALID_START_PARAMETERS"
2241
+ });
2242
+ expect2(await claim(runtime)).toEqual([]);
2243
+ expect2((await startFrom(runtime, "caller", "ship")).status).toBe("accepted");
2244
+ });
2245
+ it("matches parameters whose keys are in a different order", async ({ runtime }) => {
2246
+ const a = { orderId: 42, address: { line1: "1 High St", city: "Leeds" }, zero: 0 };
2247
+ const b = { zero: -0, address: { city: "Leeds", line1: "1 High St" }, orderId: 42 };
2248
+ const encodedA = durableValueCodec.encode(a);
2249
+ expect2(encodedA.status).toBe("encoded");
2250
+ expect2(durableValueCodec.encode(b)).toEqual(encodedA);
2251
+ await admitSuspended(runtime, "caller");
2252
+ const first = acceptedId(await startFrom(runtime, "caller", "ship", { params: a }));
2253
+ expect2(await startFrom(runtime, "caller", "ship", { params: b })).toEqual({
2254
+ status: "accepted",
2255
+ executionId: first
2256
+ });
2257
+ expect2(await claimedIds(runtime)).toEqual([first]);
2258
+ });
2259
+ it("rejects a repeat that changes the workflow ID or parameters, leaving the original unchanged", async ({
2260
+ runtime,
2261
+ advance
2262
+ }) => {
2263
+ await admitSuspended(runtime, "caller");
2264
+ await admitSuspended(runtime, "caller-without-params");
2265
+ const first = acceptedId(await startFrom(runtime, "caller", "ship", { params: { orderId: 42 } }));
2266
+ const bare = acceptedId(await startFrom(runtime, "caller-without-params", "ship", { params: undefined }));
2267
+ const before = await runtime.executions.get(first);
2268
+ advance(10);
2269
+ const repeats = [
2270
+ ["caller", { workflowId: "ship-express", params: { orderId: 42 } }],
2271
+ ["caller", { params: { orderId: 43 } }],
2272
+ ["caller", { params: { orderId: 42, express: true } }],
2273
+ ["caller", { params: undefined }],
2274
+ ["caller", { params: { orderId: 42, express: undefined } }],
2275
+ ["caller-without-params", { params: {} }]
2276
+ ];
2277
+ for (const [caller, target] of repeats) {
2278
+ const mismatch = await startFrom(runtime, caller, "ship", target);
2279
+ expect2(mismatch).toMatchObject({
2280
+ status: "rejected",
2281
+ code: "START_REQUEST_MISMATCH"
2282
+ });
2283
+ expect2(mismatch.status === "rejected" && typeof mismatch.message).toBe("string");
2284
+ }
2285
+ expect2(await runtime.executions.get(first)).toEqual(before);
2286
+ expect2(await claimedIds(runtime)).toEqual([first, bare].sort());
2287
+ expect2(await startFrom(runtime, "caller", "ship", { params: { orderId: 42 } })).toEqual({ status: "accepted", executionId: first });
2288
+ });
2289
+ it("returns the original target after it is terminal, and still rejects a mismatch", async ({
2290
+ runtime
2291
+ }) => {
2292
+ await admitSuspended(runtime, "caller");
2293
+ const first = acceptedId(await startFrom(runtime, "caller", "ship"));
2294
+ const lease = await claimOne(runtime, first);
2295
+ await runtime.executions.acknowledge({
2296
+ lease,
2297
+ outcome: { status: "completed", result: "shipped" }
2298
+ });
2299
+ expect2((await runtime.executions.get(first)).status).toBe("completed");
2300
+ expect2(await startFrom(runtime, "caller", "ship")).toEqual({
2301
+ status: "accepted",
2302
+ executionId: first
2303
+ });
2304
+ expect2(await startFrom(runtime, "caller", "ship", { workflowId: "ship-express" })).toMatchObject({
2305
+ status: "rejected",
2306
+ code: "START_REQUEST_MISMATCH"
2307
+ });
2308
+ expect2(await claim(runtime)).toEqual([]);
2309
+ });
2310
+ itAfterRestart("keeps start requests and parent links across a restart", async ({
2311
+ runtime,
2312
+ restart
2313
+ }) => {
2314
+ await admitSuspended(runtime, "caller");
2315
+ const first = acceptedId(await startFrom(runtime, "caller", "ship"));
2316
+ const record = await runtime.executions.get(first);
2317
+ const restarted = await restart();
2318
+ expect2(await restarted.executions.get(first)).toEqual(record);
2319
+ expect2(record.parent).toEqual({ executionId: "caller", stepKey: "ship" });
2320
+ expect2(await startFrom(restarted, "caller", "ship")).toEqual({
2321
+ status: "accepted",
2322
+ executionId: first
2323
+ });
2324
+ expect2(await startFrom(restarted, "caller", "ship", { workflowId: "ship-express" })).toMatchObject({
2325
+ status: "rejected",
2326
+ code: "START_REQUEST_MISMATCH"
2327
+ });
2328
+ expect2(await claimedIds(restarted)).toEqual([first]);
2329
+ });
2330
+ });
2331
+ describe2("durability", () => {
2332
+ itAfterRestart("preserves admitted and suspended records and their pending wakes across a restart", async ({ runtime, clock, advance, restart }) => {
2333
+ await admit(runtime, "delayed");
2334
+ const delayed = await claimOne(runtime, "delayed");
2335
+ await runtime.executions.acknowledge({
2336
+ lease: delayed,
2337
+ outcome: { status: "suspended", wakeAt: clock.now + 500 }
2338
+ });
2339
+ await admitSuspended(runtime, "woken");
2340
+ await runtime.executions.requestWake({ executionId: "woken", wakeAt: clock.now + 200 });
2341
+ await admitSuspended(runtime, "idle");
2342
+ await admit(runtime, "in-flight");
2343
+ await claimOne(runtime, "in-flight");
2344
+ await admitCompleted(runtime, "finished", { receipt: "r-1" });
2345
+ await runtime.executions.admit({
2346
+ executionId: "admitted",
2347
+ workflowId: "charge-order",
2348
+ params: { orderId: 42 },
2349
+ context: new Map([["tenant", "acme"]])
2350
+ });
2351
+ const records = await Promise.all(["admitted", "delayed", "woken", "idle", "in-flight", "finished"].map((id) => runtime.executions.get(id)));
2352
+ const restarted = await restart();
2353
+ expect2(await Promise.all(["admitted", "delayed", "woken", "idle", "in-flight", "finished"].map((id) => restarted.executions.get(id)))).toEqual(records);
2354
+ expect2([...(await restarted.executions.get("admitted")).context.entries()]).toEqual([["tenant", "acme"]]);
2355
+ expect2(await drain(restarted)).toEqual(["admitted"]);
2356
+ advance(200);
2357
+ expect2(await drain(restarted)).toEqual(["woken"]);
2358
+ advance(300);
2359
+ expect2(await drain(restarted)).toEqual(["delayed"]);
2360
+ advance(REDELIVER_AFTER2 - 500 - 1);
2361
+ expect2(await drain(restarted)).toEqual([]);
2362
+ advance(1);
2363
+ expect2(await drain(restarted)).toEqual(["in-flight"]);
2364
+ expect2(await restarted.executions.admit({
2365
+ executionId: "finished",
2366
+ workflowId: "workflow",
2367
+ params: undefined,
2368
+ context: new Map
2369
+ })).toMatchObject({ status: "rejected", code: "EXECUTION_ID_CONFLICT" });
2370
+ });
2371
+ itAfterRestart("makes a store commit and the wakes it requires durable together", async ({ runtime, restart }) => {
2372
+ await admitSuspended(runtime, "committed");
2373
+ const snapshot = await runtime.stores.getStore({
2374
+ definition: Store,
2375
+ id: "committed"
2376
+ });
2377
+ await runtime.stores.registerWaiter(waiter("committed", snapshot.instanceId, snapshot.version));
2378
+ await appendMessage(runtime, "committed", "m1");
2379
+ const restarted = await restart();
2380
+ expect2(await restarted.stores.getStore({ definition: Store, id: "committed" })).toMatchObject({ state: { messages: [{ id: "m1" }] }, version: 1 });
2381
+ expect2(await claimedIds(restarted)).toEqual(["committed"]);
2382
+ });
2383
+ itAfterRestart("keeps a registered waiter across a restart", async ({ runtime, restart }) => {
2384
+ await admitSuspended(runtime, "durable-waiter");
2385
+ const snapshot = await runtime.stores.getStore({
2386
+ definition: Store,
2387
+ id: "durable-waiter"
2388
+ });
2389
+ await runtime.stores.registerWaiter(waiter("durable-waiter", snapshot.instanceId, snapshot.version));
2390
+ const restarted = await restart();
2391
+ await appendMessage(restarted, "durable-waiter", "m1");
2392
+ expect2(await claimedIds(restarted)).toEqual(["durable-waiter"]);
2393
+ });
2394
+ });
2395
+ });
2396
+ }
2397
+
2398
+ // src/index.ts
2399
+ var WORKFLOW_ID = "workflow";
2400
+ var STEP_KEY = "wait";
2401
+ function schema2() {
2402
+ return {
2403
+ "~standard": {
2404
+ version: 1,
2405
+ vendor: "yieldstar-conformance",
2406
+ validate(value) {
2407
+ return { value };
2408
+ }
2409
+ }
2410
+ };
2411
+ }
2412
+ function waiter(storeName, storeId, instanceId, sinceVersion, readPaths = [["messages"]], executionId = "execution") {
2413
+ return {
2414
+ workflowId: WORKFLOW_ID,
2415
+ executionId,
2416
+ stepKey: STEP_KEY,
2417
+ storeName,
2418
+ storeId,
2419
+ instanceId,
2420
+ sinceVersion,
2421
+ readPaths
2422
+ };
2423
+ }
2424
+ var seeds = new Map;
2425
+ function seeded(storeName, fallback) {
2426
+ return (id) => structuredClone(seeds.get(JSON.stringify([storeName, id])) ?? fallback);
2427
+ }
2428
+ function seed(definition, id, state) {
2429
+ seeds.set(JSON.stringify([definition.name, id]), state);
2430
+ }
2431
+ async function admitWaiting(runtime, ...executionIds) {
2432
+ for (const executionId of executionIds) {
2433
+ const admitted = await runtime.executions.admit({
2434
+ executionId,
2435
+ workflowId: WORKFLOW_ID,
2436
+ params: undefined,
2437
+ context: new Map
2438
+ });
2439
+ if (admitted.status !== "accepted") {
2440
+ throw new Error(`admission of "${executionId}" was rejected: ${admitted.message}`);
2441
+ }
2442
+ }
2443
+ const woken = await drainWakes(runtime);
2444
+ expect3(woken).toEqual([...new Set(executionIds)].sort());
2445
+ }
2446
+ async function drainWakes(runtime) {
2447
+ const leases = await claimAll(runtime);
2448
+ for (const lease of leases) {
2449
+ await runtime.executions.acknowledge({ lease, outcome: { status: "suspended" } });
2450
+ }
2451
+ return leases.map((lease) => lease.executionId).sort();
2452
+ }
2453
+ function claimAll(runtime) {
2454
+ return runtime.executions.claimDue({ limit: 1000, duration: DEFAULT_LEASE_DURATION3 });
2455
+ }
2456
+ async function expectWoken(runtime, expected) {
2457
+ const woken = [];
2458
+ const want = [...expected].sort();
2459
+ for (let attempt = 0;attempt < 100; attempt++) {
2460
+ woken.push(...await drainWakes(runtime));
2461
+ if (woken.length >= want.length)
2462
+ break;
2463
+ await new Promise((resolve) => setTimeout(resolve, 10));
2464
+ }
2465
+ expect3(woken.sort()).toEqual(want);
2466
+ }
2467
+ async function expectNotWoken(runtime) {
2468
+ await new Promise((resolve) => setTimeout(resolve, 20));
2469
+ expect3(await drainWakes(runtime)).toEqual([]);
2470
+ }
2471
+ async function recordStep(runtime, executionId, stepKey = STEP_KEY) {
2472
+ await runtime.executions.requestWake({ executionId, wakeAt: runtime.clock() });
2473
+ const leases = await claimAll(runtime);
2474
+ const lease = leases.find((lease) => lease.executionId === executionId);
2475
+ if (!lease)
2476
+ throw new Error(`could not lease "${executionId}" to record a step`);
2477
+ const written = await runtime.heap.writeStep({
2478
+ lease,
2479
+ stepKey,
2480
+ stepAttempt: 1,
2481
+ done: true,
2482
+ stepResponseJson: JSON.stringify({ done: true })
2483
+ });
2484
+ expect3(written).toMatchObject({ meta: { done: true } });
2485
+ for (const claimed of leases) {
2486
+ await runtime.executions.acknowledge({
2487
+ lease: claimed,
2488
+ outcome: { status: "suspended" }
2489
+ });
2490
+ }
2491
+ }
2492
+ function registerStoreClientConformance({
2493
+ name,
2494
+ create
2495
+ }) {
2496
+ const Store = defineStore2(`${name}-store-conformance`, schema2(), {
2497
+ initial: seeded(`${name}-store-conformance`, { messages: [] })
2498
+ });
2499
+ const TakeStore = defineStore2(`${name}-take-conformance`, schema2(), {
2500
+ initial: seeded(`${name}-take-conformance`, { messages: [] })
2501
+ });
2502
+ describe3(`${name} store connector conformance`, () => {
2503
+ test3("creates, reads, lists, deletes, and recreates logical stores", async () => {
2504
+ const harness = await create();
2505
+ const OtherStore = defineStore2(`${name}-other-conformance`, schema2(), {
2506
+ initial: () => ({ messages: [] })
2507
+ });
2508
+ try {
2509
+ const first = await harness.runtime.stores.getStore({
2510
+ definition: Store,
2511
+ id: "b"
2512
+ });
2513
+ const second = await harness.runtime.stores.getStore({
2514
+ definition: Store,
2515
+ id: "a"
2516
+ });
2517
+ await harness.runtime.stores.getStore({
2518
+ definition: OtherStore,
2519
+ id: "a"
2520
+ });
2521
+ expect3(first.instanceId).toMatch(/^[0-9a-f-]+$/);
2522
+ expect3(second.instanceId).not.toBe(first.instanceId);
2523
+ expect3(await harness.runtime.stores.listStores(Store)).toEqual(["a", "b"]);
2524
+ expect3(await harness.runtime.stores.getStore({ definition: Store, id: "b" })).toEqual(first);
2525
+ await harness.runtime.stores.deleteStore({ definition: Store, id: "b" });
2526
+ expect3(await harness.runtime.stores.listStores(Store)).toEqual(["a"]);
2527
+ const recreated = await harness.runtime.stores.getStore({
2528
+ definition: Store,
2529
+ id: "b"
2530
+ });
2531
+ expect3(recreated).toMatchObject({ state: { messages: [] }, version: 0 });
2532
+ expect3(recreated.instanceId).not.toBe(first.instanceId);
2533
+ await harness.runtime.stores.deleteStore({ definition: Store, id: "missing" });
2534
+ expect3(await harness.runtime.stores.listStores(Store)).toEqual(["a", "b"]);
2535
+ } finally {
2536
+ await harness.dispose?.();
2537
+ }
2538
+ });
2539
+ test3("rejects non-durable state with INVALID_STEP_VALUE and leaves the store unchanged", async () => {
2540
+ const harness = await create();
2541
+ const { stores } = harness.runtime;
2542
+ const invalid = { code: "INVALID_STEP_VALUE", name: "WorkflowValueError" };
2543
+ try {
2544
+ seed(Store, "never-created", { messages: [], unrelated: new Date(0) });
2545
+ await expect3(stores.getStore({ definition: Store, id: "never-created" })).rejects.toMatchObject(invalid);
2546
+ await expect3(stores.updateStore({
2547
+ definition: Store,
2548
+ id: "never-created",
2549
+ stepId: { executionId: "never-created", stepKey: "update" },
2550
+ updater() {}
2551
+ })).rejects.toMatchObject({ ...invalid, stepKey: "update" });
2552
+ expect3(await stores.listStores(Store)).toEqual([]);
2553
+ seed(Store, "durable", { messages: [{ id: "one" }] });
2554
+ const created = await stores.getStore({
2555
+ definition: Store,
2556
+ id: "durable"
2557
+ });
2558
+ const stepId = { executionId: "durable-execution", stepKey: "update" };
2559
+ await expect3(stores.updateStore({
2560
+ definition: Store,
2561
+ id: "durable",
2562
+ stepId,
2563
+ updater(draft) {
2564
+ draft.messages.push({ id: "two" });
2565
+ draft.at = new Map;
2566
+ }
2567
+ })).rejects.toMatchObject({ ...invalid, stepKey: "update" });
2568
+ await expect3(stores.updateStoreFrom({
2569
+ definition: Store,
2570
+ id: "durable",
2571
+ snapshot: created,
2572
+ updater: () => ({ messages: [], unrelated: 1n })
2573
+ })).rejects.toMatchObject(invalid);
2574
+ expect3(await stores.getStore({ definition: Store, id: "durable" })).toMatchObject({
2575
+ state: { messages: [{ id: "one" }] },
2576
+ version: 0
2577
+ });
2578
+ seed(TakeStore, "durable-take", { messages: [{ id: "one" }] });
2579
+ await stores.getStore({
2580
+ definition: TakeStore,
2581
+ id: "durable-take"
2582
+ });
2583
+ await expect3(stores.takeFromStore({
2584
+ definition: TakeStore,
2585
+ id: "durable-take",
2586
+ selector: (state) => state.messages.length > 0 && new Set(state.messages),
2587
+ claim(draft) {
2588
+ draft.messages = [];
2589
+ }
2590
+ })).rejects.toMatchObject(invalid);
2591
+ expect3(await stores.getStore({ definition: TakeStore, id: "durable-take" })).toMatchObject({ state: { messages: [{ id: "one" }] }, version: 0 });
2592
+ const retried = await stores.updateStore({
2593
+ definition: Store,
2594
+ id: "durable",
2595
+ stepId,
2596
+ updater(draft) {
2597
+ draft.messages.push({ id: "two" });
2598
+ }
2599
+ });
2600
+ expect3(retried).toMatchObject({ version: 1, state: { messages: [{ id: "one" }, { id: "two" }] } });
2601
+ } finally {
2602
+ await harness.dispose?.();
2603
+ }
2604
+ });
2605
+ test3("stores state and receipts with the durable value codec", async () => {
2606
+ const harness = await create();
2607
+ const { stores } = harness.runtime;
2608
+ try {
2609
+ seed(Store, "codec", { messages: [], unrelated: undefined });
2610
+ await stores.getStore({
2611
+ definition: Store,
2612
+ id: "codec"
2613
+ });
2614
+ const stepId = { executionId: "codec-execution", stepKey: "update" };
2615
+ const update = () => stores.updateStore({
2616
+ definition: Store,
2617
+ id: "codec",
2618
+ stepId,
2619
+ updater(draft) {
2620
+ draft.messages.push({ id: "one", $type: "undefined" });
2621
+ draft.error = new RangeError("r");
2622
+ }
2623
+ });
2624
+ const first = await update();
2625
+ const replayed = await update();
2626
+ const read = await stores.getStore({ definition: Store, id: "codec" });
2627
+ for (const state of [first.state, replayed.state, read.state]) {
2628
+ expect3(Object.keys(state).sort()).toEqual(["error", "messages", "unrelated"]);
2629
+ expect3(state.messages).toEqual([{ id: "one", $type: "undefined" }]);
2630
+ expect3(state.error).toBeInstanceOf(RangeError);
2631
+ }
2632
+ expect3(replayed.version).toBe(1);
2633
+ if (harness.restart) {
2634
+ const restarted = await harness.restart();
2635
+ const reopened = await restarted.stores.getStore({ definition: Store, id: "codec" });
2636
+ expect3(Object.keys(reopened.state).sort()).toEqual(["error", "messages", "unrelated"]);
2637
+ expect3(reopened.state.error).toBeInstanceOf(RangeError);
2638
+ }
2639
+ } finally {
2640
+ await harness.dispose?.();
2641
+ }
2642
+ });
2643
+ test3("rejects asynchronous updaters and claims without committing", async () => {
2644
+ const harness = await create();
2645
+ try {
2646
+ seed(TakeStore, "sync-callbacks", { messages: [{ id: "one" }] });
2647
+ await harness.runtime.stores.getStore({
2648
+ definition: TakeStore,
2649
+ id: "sync-callbacks"
2650
+ });
2651
+ await expect3(harness.runtime.stores.updateStore({
2652
+ definition: TakeStore,
2653
+ id: "sync-callbacks",
2654
+ updater: async (draft) => {
2655
+ draft.messages.push({ id: "two" });
2656
+ }
2657
+ })).rejects.toThrow(/synchronous/);
2658
+ await expect3(harness.runtime.stores.takeFromStore({
2659
+ definition: TakeStore,
2660
+ id: "sync-callbacks",
2661
+ selector: (state) => state.messages[0],
2662
+ claim: async (_draft, selected) => {
2663
+ selected.claimedBy = "worker";
2664
+ }
2665
+ })).rejects.toThrow(/synchronous/);
2666
+ expect3(await harness.runtime.stores.getStore({ definition: TakeStore, id: "sync-callbacks" })).toMatchObject({ state: { messages: [{ id: "one" }] }, version: 0 });
2667
+ } finally {
2668
+ await harness.dispose?.();
2669
+ }
2670
+ });
2671
+ test3("retries an updater after an interleaved CAS conflict", async () => {
2672
+ let validations = 0;
2673
+ let racing = false;
2674
+ let release;
2675
+ const bothPrepared = new Promise((resolve) => {
2676
+ release = resolve;
2677
+ });
2678
+ const raceSchema = {
2679
+ "~standard": {
2680
+ version: 1,
2681
+ vendor: "yieldstar-conformance",
2682
+ async validate(value) {
2683
+ if (!racing)
2684
+ return { value };
2685
+ validations++;
2686
+ if (validations === 2)
2687
+ release();
2688
+ if (validations <= 2)
2689
+ await bothPrepared;
2690
+ return { value };
2691
+ }
2692
+ }
2693
+ };
2694
+ const RaceStore = defineStore2(`${name}-cas-race-conformance`, raceSchema, {
2695
+ initial: () => ({ messages: [] })
2696
+ });
2697
+ const harness = await create();
2698
+ let updaterRuns = 0;
2699
+ try {
2700
+ await harness.runtime.stores.getStore({
2701
+ definition: RaceStore,
2702
+ id: "race"
2703
+ });
2704
+ racing = true;
2705
+ validations = 0;
2706
+ const results = await Promise.all(["a", "b"].map((id) => harness.runtime.stores.updateStore({
2707
+ definition: RaceStore,
2708
+ id: "race",
2709
+ updater(draft) {
2710
+ updaterRuns++;
2711
+ draft.messages.push(id);
2712
+ }
2713
+ })));
2714
+ expect3(updaterRuns).toBe(3);
2715
+ expect3(results.map((result) => result.version).sort()).toEqual([1, 2]);
2716
+ const final = await harness.runtime.stores.getStore({ definition: RaceStore, id: "race" });
2717
+ expect3(final).toMatchObject({ state: { messages: ["a", "b"] }, version: 2 });
2718
+ } finally {
2719
+ await harness.dispose?.();
2720
+ }
2721
+ });
2722
+ test3("makes a step receipt and mutation exactly-once under concurrent replay", async () => {
2723
+ const harness = await create();
2724
+ let updaterRuns = 0;
2725
+ try {
2726
+ await harness.runtime.stores.getStore({
2727
+ definition: Store,
2728
+ id: "receipt-race"
2729
+ });
2730
+ const results = await Promise.all(["a", "b"].map((id) => harness.runtime.stores.updateStore({
2731
+ definition: Store,
2732
+ id: "receipt-race",
2733
+ stepId: { executionId: "producer", stepKey: "append" },
2734
+ updater(draft) {
2735
+ updaterRuns++;
2736
+ draft.messages.push({ id });
2737
+ }
2738
+ })));
2739
+ expect3(results[1]).toEqual(results[0]);
2740
+ expect3(updaterRuns).toBeGreaterThanOrEqual(1);
2741
+ expect3(updaterRuns).toBeLessThanOrEqual(2);
2742
+ expect3(await harness.runtime.stores.getStore({ definition: Store, id: "receipt-race" })).toMatchObject({ state: { messages: [{ id: results[0].state.messages[0].id }] }, version: 1 });
2743
+ let replayRuns = 0;
2744
+ expect3(await harness.runtime.stores.updateStore({
2745
+ definition: Store,
2746
+ id: "receipt-race",
2747
+ stepId: { executionId: "producer", stepKey: "append" },
2748
+ updater() {
2749
+ replayRuns++;
2750
+ }
2751
+ })).toEqual(results[0]);
2752
+ expect3(replayRuns).toBe(0);
2753
+ await harness.runtime.stores.deleteStore({ definition: Store, id: "receipt-race" });
2754
+ const recreated = await harness.runtime.stores.getStore({
2755
+ definition: Store,
2756
+ id: "receipt-race"
2757
+ });
2758
+ expect3(await harness.runtime.stores.updateStore({
2759
+ definition: Store,
2760
+ id: "receipt-race",
2761
+ stepId: { executionId: "producer", stepKey: "append" },
2762
+ updater() {
2763
+ replayRuns++;
2764
+ }
2765
+ })).toEqual(results[0]);
2766
+ expect3(replayRuns).toBe(0);
2767
+ expect3(await harness.runtime.stores.getStore({ definition: Store, id: "receipt-race" })).toEqual(recreated);
2768
+ } finally {
2769
+ await harness.dispose?.();
2770
+ }
2771
+ });
2772
+ test3("scopes receipts by execution and step key", async () => {
2773
+ const harness = await create();
2774
+ try {
2775
+ await harness.runtime.stores.getStore({
2776
+ definition: Store,
2777
+ id: "receipt-scope"
2778
+ });
2779
+ for (const [executionId, stepKey] of [["one", "a"], ["one", "b"], ["two", "a"]]) {
2780
+ await harness.runtime.stores.updateStore({
2781
+ definition: Store,
2782
+ id: "receipt-scope",
2783
+ stepId: { executionId, stepKey },
2784
+ updater(draft) {
2785
+ draft.messages.push({ id: `${executionId}:${stepKey}` });
2786
+ }
2787
+ });
2788
+ }
2789
+ expect3(await harness.runtime.stores.getStore({ definition: Store, id: "receipt-scope" })).toMatchObject({
2790
+ state: { messages: [{ id: "one:a" }, { id: "one:b" }, { id: "two:a" }] },
2791
+ version: 3
2792
+ });
2793
+ } finally {
2794
+ await harness.dispose?.();
2795
+ }
2796
+ });
2797
+ test3("applies calls without step ids independently and returns detached state", async () => {
2798
+ const harness = await create();
2799
+ try {
2800
+ const initial = await harness.runtime.stores.getStore({
2801
+ definition: Store,
2802
+ id: "external-update"
2803
+ });
2804
+ initial.state.messages.push({ id: "caller-only" });
2805
+ for (const id of ["one", "two"]) {
2806
+ const result = await harness.runtime.stores.updateStore({
2807
+ definition: Store,
2808
+ id: "external-update",
2809
+ updater(draft) {
2810
+ draft.messages.push({ id });
2811
+ }
2812
+ });
2813
+ result.state.messages.push({ id: "result-only" });
2814
+ }
2815
+ expect3(await harness.runtime.stores.getStore({ definition: Store, id: "external-update" })).toMatchObject({ state: { messages: [{ id: "one" }, { id: "two" }] }, version: 2 });
2816
+ } finally {
2817
+ await harness.dispose?.();
2818
+ }
2819
+ });
2820
+ test3("committed state contains no tracking proxies", async () => {
2821
+ const harness = await create();
2822
+ try {
2823
+ seed(Store, "plain-state", { messages: [{ id: "one" }] });
2824
+ await harness.runtime.stores.getStore({
2825
+ definition: Store,
2826
+ id: "plain-state"
2827
+ });
2828
+ await harness.runtime.stores.updateStore({
2829
+ definition: Store,
2830
+ id: "plain-state",
2831
+ updater(draft) {
2832
+ draft.lastMessage = draft.messages[0];
2833
+ draft.wrapped = { inner: draft.messages };
2834
+ }
2835
+ });
2836
+ const snapshot = await harness.runtime.stores.getStore({ definition: Store, id: "plain-state" });
2837
+ expect3(() => structuredClone(snapshot.state)).not.toThrow();
2838
+ expect3(snapshot.state.lastMessage).toEqual({ id: "one" });
2839
+ expect3(snapshot.state.wrapped).toEqual({
2840
+ inner: [{ id: "one" }]
2841
+ });
2842
+ } finally {
2843
+ await harness.dispose?.();
2844
+ }
2845
+ });
2846
+ test3("mutating updates do not deep-diff the full state", async () => {
2847
+ const PerformanceStore = defineStore2(`${name}-bounded-update-conformance`, schema2(), { initial: () => ({ changed: 0, large: {} }) });
2848
+ const harness = await create();
2849
+ const size = 1000;
2850
+ const large = Object.fromEntries(Array.from({ length: size }, (_, index) => [`key-${index}`, index]));
2851
+ const instrument = () => {
2852
+ let reads = 0;
2853
+ const value = new Proxy(large, {
2854
+ get(target, property, receiver) {
2855
+ if (typeof property === "string" && property.startsWith("key-"))
2856
+ reads++;
2857
+ return Reflect.get(target, property, receiver);
2858
+ }
2859
+ });
2860
+ return { value, reads: () => reads };
2861
+ };
2862
+ try {
2863
+ await harness.runtime.stores.getStore({
2864
+ definition: PerformanceStore,
2865
+ id: "mutating"
2866
+ });
2867
+ const mutating = instrument();
2868
+ await harness.runtime.stores.updateStore({
2869
+ definition: PerformanceStore,
2870
+ id: "mutating",
2871
+ updater(draft) {
2872
+ draft.changed = 1;
2873
+ draft.large = mutating.value;
2874
+ }
2875
+ });
2876
+ await harness.runtime.stores.getStore({
2877
+ definition: PerformanceStore,
2878
+ id: "replacement"
2879
+ });
2880
+ const replacement = instrument();
2881
+ await harness.runtime.stores.updateStore({
2882
+ definition: PerformanceStore,
2883
+ id: "replacement",
2884
+ updater: () => ({ changed: 1, large: replacement.value })
2885
+ });
2886
+ expect3(replacement.reads() - mutating.reads()).toBeGreaterThanOrEqual(size);
2887
+ } finally {
2888
+ await harness.dispose?.();
2889
+ }
2890
+ });
2891
+ test3("conditional updates conflict and replay ledger-first", async () => {
2892
+ const harness = await create();
2893
+ let updaterRuns = 0;
2894
+ try {
2895
+ const snapshot = await harness.runtime.stores.getStore({
2896
+ definition: Store,
2897
+ id: "conditional"
2898
+ });
2899
+ const stepId = { executionId: "execution", stepKey: "conditional" };
2900
+ const committed = await harness.runtime.stores.updateStoreFrom({
2901
+ definition: Store,
2902
+ id: "conditional",
2903
+ snapshot,
2904
+ stepId,
2905
+ updater(draft) {
2906
+ updaterRuns++;
2907
+ draft.messages.push({ id: "first" });
2908
+ }
2909
+ });
2910
+ await harness.runtime.stores.updateStore({
2911
+ definition: Store,
2912
+ id: "conditional",
2913
+ updater(draft) {
2914
+ draft.messages.push({ id: "second" });
2915
+ }
2916
+ });
2917
+ expect3(await harness.runtime.stores.updateStoreFrom({
2918
+ definition: Store,
2919
+ id: "conditional",
2920
+ snapshot: { ...snapshot, instanceId: "" },
2921
+ stepId,
2922
+ updater() {
2923
+ updaterRuns++;
2924
+ }
2925
+ })).toEqual(committed);
2926
+ expect3(updaterRuns).toBe(1);
2927
+ expect3(await harness.runtime.stores.updateStoreFrom({
2928
+ definition: Store,
2929
+ id: "conditional",
2930
+ snapshot,
2931
+ updater() {
2932
+ updaterRuns++;
2933
+ }
2934
+ })).toMatchObject({ updated: false, expectedVersion: 0, actualVersion: 2 });
2935
+ expect3(updaterRuns).toBe(1);
2936
+ await expect3(harness.runtime.stores.updateStoreFrom({
2937
+ definition: Store,
2938
+ id: "conditional",
2939
+ snapshot: { ...snapshot, instanceId: "" },
2940
+ updater() {}
2941
+ })).rejects.toThrow(/missing instanceId/);
2942
+ } finally {
2943
+ await harness.dispose?.();
2944
+ }
2945
+ });
2946
+ test3("claims distinct values and records only matched takes", async () => {
2947
+ const harness = await create();
2948
+ let selectorRuns = 0;
2949
+ let claimRuns = 0;
2950
+ try {
2951
+ seed(TakeStore, "take", { messages: [{ id: "one" }, { id: "two" }] });
2952
+ await harness.runtime.stores.getStore({
2953
+ definition: TakeStore,
2954
+ id: "take"
2955
+ });
2956
+ const take = (worker, stepKey) => harness.runtime.stores.takeFromStore({
2957
+ definition: TakeStore,
2958
+ id: "take",
2959
+ stepId: { executionId: "execution", stepKey },
2960
+ selector: (state) => {
2961
+ selectorRuns++;
2962
+ return state.messages.find((message) => !message.claimedBy);
2963
+ },
2964
+ claim: (_draft, selected) => {
2965
+ claimRuns++;
2966
+ selected.claimedBy = worker;
2967
+ }
2968
+ });
2969
+ const [first, second] = await Promise.all([take("a", "a"), take("b", "b")]);
2970
+ if (!first.matched || !second.matched)
2971
+ throw new Error("both takes must match");
2972
+ expect3(first.selected.id).not.toBe(second.selected.id);
2973
+ expect3([first.version, second.version].sort()).toEqual([1, 2]);
2974
+ const selectorRunsAfterCommit = selectorRuns;
2975
+ const claimRunsAfterCommit = claimRuns;
2976
+ expect3(await take("ignored", "a")).toEqual(first);
2977
+ expect3(selectorRuns).toBe(selectorRunsAfterCommit);
2978
+ expect3(claimRuns).toBe(claimRunsAfterCommit);
2979
+ expect3(await harness.runtime.stores.getStore({ definition: TakeStore, id: "take" })).toMatchObject({ version: 2 });
2980
+ const miss = await harness.runtime.stores.takeFromStore({
2981
+ definition: TakeStore,
2982
+ id: "take",
2983
+ stepId: { executionId: "execution", stepKey: "later" },
2984
+ selector: (state) => state.messages.find((message) => !message.claimedBy),
2985
+ claim() {}
2986
+ });
2987
+ if (miss.matched)
2988
+ throw new Error("take must not match");
2989
+ expect3(miss.version).toBe(2);
2990
+ expect3(miss.readPaths).toContainEqual(["messages"]);
2991
+ await harness.runtime.stores.updateStore({
2992
+ definition: TakeStore,
2993
+ id: "take",
2994
+ updater(draft) {
2995
+ draft.messages.push({ id: "three" });
2996
+ }
2997
+ });
2998
+ expect3(await harness.runtime.stores.takeFromStore({
2999
+ definition: TakeStore,
3000
+ id: "take",
3001
+ stepId: { executionId: "execution", stepKey: "later" },
3002
+ selector: (state) => state.messages.find((message) => !message.claimedBy),
3003
+ claim: (_draft, selected) => {
3004
+ selected.claimedBy = "c";
3005
+ }
3006
+ })).toMatchObject({ matched: true });
3007
+ } finally {
3008
+ await harness.dispose?.();
3009
+ }
3010
+ });
3011
+ test3("protects recreated stores during conditional deletion and replays the receipt", async () => {
3012
+ const harness = await create();
3013
+ try {
3014
+ const original = await harness.runtime.stores.getStore({
3015
+ definition: Store,
3016
+ id: "delete"
3017
+ });
3018
+ await harness.runtime.stores.updateStore({
3019
+ definition: Store,
3020
+ id: "delete",
3021
+ updater(draft) {
3022
+ draft.messages.push({ id: "changed" });
3023
+ }
3024
+ });
3025
+ expect3(await harness.runtime.stores.deleteStoreFrom({ definition: Store, id: "delete", snapshot: original })).toMatchObject({ deleted: false, reason: "conflict" });
3026
+ const current = await harness.runtime.stores.getStore({ definition: Store, id: "delete" });
3027
+ const stepId = { executionId: "execution", stepKey: "delete" };
3028
+ expect3(await harness.runtime.stores.deleteStoreFrom({ definition: Store, id: "delete", snapshot: current, stepId })).toEqual({ deleted: true });
3029
+ const recreated = await harness.runtime.stores.getStore({
3030
+ definition: Store,
3031
+ id: "delete"
3032
+ });
3033
+ expect3(recreated.instanceId).not.toBe(original.instanceId);
3034
+ expect3(await harness.runtime.stores.deleteStoreFrom({ definition: Store, id: "delete", snapshot: current, stepId })).toEqual({ deleted: true });
3035
+ expect3(await harness.runtime.stores.getStore({ definition: Store, id: "delete" })).toEqual(recreated);
3036
+ } finally {
3037
+ await harness.dispose?.();
3038
+ }
3039
+ });
3040
+ describe3("missing stores (RFC 006)", () => {
3041
+ function countedStore(suffix, validate) {
3042
+ const evaluations = [];
3043
+ const countedSchema = {
3044
+ "~standard": {
3045
+ version: 1,
3046
+ vendor: "yieldstar-conformance",
3047
+ async validate(value) {
3048
+ await validate?.();
3049
+ return { value };
3050
+ }
3051
+ }
3052
+ };
3053
+ const definition = defineStore2(`${name}-${suffix}-missing-conformance`, countedSchema, {
3054
+ initial(id) {
3055
+ evaluations.push(id);
3056
+ return { createdBy: `initial-${evaluations.length}`, messages: [] };
3057
+ }
3058
+ });
3059
+ return { definition, evaluations };
3060
+ }
3061
+ test3("every operation on a missing store creates it from initial in the same write", async () => {
3062
+ const harness = await create();
3063
+ const { stores } = harness.runtime;
3064
+ const { definition, evaluations } = countedStore("operations");
3065
+ const stepId = (stepKey) => ({ executionId: "operations", stepKey });
3066
+ try {
3067
+ expect3(await stores.getStore({ definition, id: "get" })).toMatchObject({
3068
+ state: { createdBy: "initial-1", messages: [] },
3069
+ version: 0
3070
+ });
3071
+ const updated = await stores.updateStore({
3072
+ definition,
3073
+ id: "update",
3074
+ stepId: stepId("update"),
3075
+ updater(draft) {
3076
+ draft.messages.push({ id: "one" });
3077
+ }
3078
+ });
3079
+ expect3(updated).toEqual({
3080
+ state: { createdBy: "initial-2", messages: [{ id: "one" }] },
3081
+ previousVersion: 0,
3082
+ version: 1
3083
+ });
3084
+ const taken = await stores.takeFromStore({
3085
+ definition,
3086
+ id: "take",
3087
+ stepId: stepId("take"),
3088
+ selector: (state) => state.createdBy,
3089
+ claim(draft) {
3090
+ draft.messages.push({ id: "claimed" });
3091
+ }
3092
+ });
3093
+ expect3(taken).toMatchObject({ matched: true, selected: "initial-3", version: 1 });
3094
+ const unmatched = await stores.takeFromStore({
3095
+ definition,
3096
+ id: "unmatched-take",
3097
+ selector: (state) => state.messages[0],
3098
+ claim() {}
3099
+ });
3100
+ expect3(unmatched).toMatchObject({ matched: false, version: 0 });
3101
+ const stale = { state: undefined, instanceId: "stale", version: 0 };
3102
+ expect3(await stores.updateStoreFrom({
3103
+ definition,
3104
+ id: "update-from",
3105
+ snapshot: stale,
3106
+ updater() {}
3107
+ })).toMatchObject({ updated: false, expectedInstanceId: "stale", actualVersion: 0 });
3108
+ expect3(await stores.deleteStoreFrom({ definition, id: "delete-from", snapshot: stale })).toMatchObject({ deleted: false, reason: "conflict", actualVersion: 0 });
3109
+ expect3(evaluations).toEqual([
3110
+ "get",
3111
+ "update",
3112
+ "take",
3113
+ "unmatched-take",
3114
+ "update-from",
3115
+ "delete-from"
3116
+ ]);
3117
+ expect3(await stores.listStores(definition)).toEqual([
3118
+ "delete-from",
3119
+ "get",
3120
+ "take",
3121
+ "unmatched-take",
3122
+ "update",
3123
+ "update-from"
3124
+ ]);
3125
+ expect3(await stores.getStore({ definition, id: "update" })).toMatchObject({
3126
+ state: updated.state,
3127
+ version: 1
3128
+ });
3129
+ expect3(await stores.getStore({ definition, id: "take" })).toMatchObject({
3130
+ state: { createdBy: "initial-3", messages: [{ id: "claimed" }] },
3131
+ instanceId: taken.instanceId,
3132
+ version: 1
3133
+ });
3134
+ expect3((await stores.getStore({ definition, id: "unmatched-take" })).instanceId).toBe(unmatched.instanceId);
3135
+ expect3(evaluations).toHaveLength(6);
3136
+ } finally {
3137
+ await harness.dispose?.();
3138
+ }
3139
+ });
3140
+ test3("evaluates initial only when the store is missing", async () => {
3141
+ const harness = await create();
3142
+ const { stores } = harness.runtime;
3143
+ const { definition, evaluations } = countedStore("existing");
3144
+ try {
3145
+ const created = await stores.getStore({ definition, id: "existing" });
3146
+ await stores.updateStore({
3147
+ definition,
3148
+ id: "existing",
3149
+ updater(draft) {
3150
+ draft.messages.push({ id: "one" });
3151
+ }
3152
+ });
3153
+ const current = await stores.getStore({ definition, id: "existing" });
3154
+ await stores.takeFromStore({
3155
+ definition,
3156
+ id: "existing",
3157
+ selector: (state) => state.messages[0],
3158
+ claim() {}
3159
+ });
3160
+ await stores.updateStoreFrom({ definition, id: "existing", snapshot: created, updater() {} });
3161
+ await stores.deleteStoreFrom({ definition, id: "existing", snapshot: created });
3162
+ expect3(evaluations).toEqual(["existing"]);
3163
+ await stores.deleteStoreFrom({ definition, id: "existing", snapshot: current });
3164
+ await stores.deleteStore({ definition, id: "existing" });
3165
+ await stores.deleteStore({ definition, id: "never-read" });
3166
+ expect3(await stores.listStores(definition)).toEqual([]);
3167
+ expect3(evaluations).toEqual(["existing"]);
3168
+ } finally {
3169
+ await harness.dispose?.();
3170
+ }
3171
+ });
3172
+ test3("a falsy initial state is a stored state, not a missing store", async () => {
3173
+ const harness = await create();
3174
+ const { stores } = harness.runtime;
3175
+ try {
3176
+ for (const [index, falsy] of [0, "", false, null].entries()) {
3177
+ let evaluations = 0;
3178
+ const Falsy = defineStore2(`${name}-falsy-${index}-missing-conformance`, schema2(), {
3179
+ initial() {
3180
+ evaluations += 1;
3181
+ return falsy;
3182
+ }
3183
+ });
3184
+ expect3(await stores.getStore({ definition: Falsy, id: "falsy" })).toMatchObject({
3185
+ state: falsy,
3186
+ version: 0
3187
+ });
3188
+ expect3(await stores.getStore({ definition: Falsy, id: "falsy" })).toMatchObject({
3189
+ state: falsy,
3190
+ version: 0
3191
+ });
3192
+ expect3(evaluations).toBe(1);
3193
+ }
3194
+ } finally {
3195
+ await harness.dispose?.();
3196
+ }
3197
+ });
3198
+ test3("creates nothing when initial or the operation fails", async () => {
3199
+ const harness = await create();
3200
+ const { stores } = harness.runtime;
3201
+ const Throwing = defineStore2(`${name}-throwing-missing-conformance`, schema2(), {
3202
+ initial() {
3203
+ throw new RangeError("no initial state");
3204
+ }
3205
+ });
3206
+ const { definition } = countedStore("failing");
3207
+ try {
3208
+ await expect3(stores.getStore({ definition: Throwing, id: "throws" })).rejects.toThrow(RangeError);
3209
+ await expect3(stores.updateStore({ definition: Throwing, id: "throws", updater() {} })).rejects.toThrow(RangeError);
3210
+ expect3(await stores.listStores(Throwing)).toEqual([]);
3211
+ await expect3(stores.updateStore({
3212
+ definition,
3213
+ id: "updater-throws",
3214
+ updater() {
3215
+ throw new RangeError("no update");
3216
+ }
3217
+ })).rejects.toThrow(RangeError);
3218
+ await expect3(stores.takeFromStore({
3219
+ definition,
3220
+ id: "claim-throws",
3221
+ selector: (state) => state.createdBy,
3222
+ claim() {
3223
+ throw new RangeError("no claim");
3224
+ }
3225
+ })).rejects.toThrow(RangeError);
3226
+ expect3(await stores.listStores(definition)).toEqual([]);
3227
+ } finally {
3228
+ await harness.dispose?.();
3229
+ }
3230
+ });
3231
+ test3("records exactly one initial state when executions update a missing store at once", async () => {
3232
+ let racing = false;
3233
+ let arrived = 0;
3234
+ let release;
3235
+ const bothRead = new Promise((resolve) => {
3236
+ release = resolve;
3237
+ });
3238
+ const { definition, evaluations } = countedStore("concurrent", async () => {
3239
+ if (!racing || ++arrived > 2)
3240
+ return;
3241
+ if (arrived === 2)
3242
+ release();
3243
+ await bothRead;
3244
+ });
3245
+ const harness = await create();
3246
+ const peer = await harness.createPeer?.() ?? harness.runtime;
3247
+ try {
3248
+ for (const id of ["get", "update"]) {
3249
+ racing = true;
3250
+ arrived = 0;
3251
+ if (id === "get") {
3252
+ const [first, second] = await Promise.all([
3253
+ harness.runtime.stores.getStore({ definition, id }),
3254
+ peer.stores.getStore({ definition, id })
3255
+ ]);
3256
+ expect3(second).toEqual(first);
3257
+ continue;
3258
+ }
3259
+ const results = await Promise.all([harness.runtime, peer].map((runtime, index) => runtime.stores.updateStore({
3260
+ definition,
3261
+ id,
3262
+ stepId: { executionId: `execution-${index}`, stepKey: "append" },
3263
+ updater(draft) {
3264
+ draft.messages.push({ id: `execution-${index}` });
3265
+ }
3266
+ })));
3267
+ expect3(results.map((result) => result.version).sort()).toEqual([1, 2]);
3268
+ }
3269
+ racing = false;
3270
+ expect3(evaluations).toEqual(["get", "get", "update", "update"]);
3271
+ const got = await harness.runtime.stores.getStore({ definition, id: "get" });
3272
+ expect3(got).toMatchObject({ version: 0, state: { messages: [] } });
3273
+ expect3(["initial-1", "initial-2"]).toContain(got.state.createdBy);
3274
+ const updated = await peer.stores.getStore({ definition, id: "update" });
3275
+ expect3(updated.version).toBe(2);
3276
+ expect3(["initial-3", "initial-4"]).toContain(updated.state.createdBy);
3277
+ expect3(updated.state.messages.map((message) => message.id).sort()).toEqual([
3278
+ "execution-0",
3279
+ "execution-1"
3280
+ ]);
3281
+ } finally {
3282
+ await harness.dispose?.();
3283
+ }
3284
+ });
3285
+ test3("replays a committed operation from its receipt without creating the store again", async () => {
3286
+ const harness = await create();
3287
+ const { definition, evaluations } = countedStore("receipt");
3288
+ const stepId = { executionId: "crashed", stepKey: "append" };
3289
+ let updaterRuns = 0;
3290
+ const append = (runtime) => runtime.stores.updateStore({
3291
+ definition,
3292
+ id: "receipt",
3293
+ stepId,
3294
+ updater(draft) {
3295
+ updaterRuns++;
3296
+ draft.messages.push({ id: "once" });
3297
+ }
3298
+ });
3299
+ try {
3300
+ const committed = await append(harness.runtime);
3301
+ const created = await harness.runtime.stores.getStore({ definition, id: "receipt" });
3302
+ const runtime = await harness.restart?.() ?? harness.runtime;
3303
+ expect3(await append(runtime)).toEqual(committed);
3304
+ expect3(await runtime.stores.getStore({ definition, id: "receipt" })).toEqual(created);
3305
+ expect3(created).toMatchObject({ state: { messages: [{ id: "once" }] }, version: 1 });
3306
+ await runtime.stores.deleteStore({ definition, id: "receipt" });
3307
+ expect3(await append(runtime)).toEqual(committed);
3308
+ expect3(await runtime.stores.listStores(definition)).toEqual([]);
3309
+ expect3(updaterRuns).toBe(1);
3310
+ expect3(evaluations).toEqual(["receipt"]);
3311
+ } finally {
3312
+ await harness.dispose?.();
3313
+ }
3314
+ });
3315
+ test3("creates a store deleted between operations again from initial", async () => {
3316
+ const harness = await create();
3317
+ const { definition, evaluations } = countedStore("recreate");
3318
+ const append = (runtime, id) => runtime.stores.updateStore({
3319
+ definition,
3320
+ id: "recreate",
3321
+ stepId: { executionId: "recreate", stepKey: id },
3322
+ updater(draft) {
3323
+ draft.messages.push({ id });
3324
+ }
3325
+ });
3326
+ try {
3327
+ await append(harness.runtime, "first");
3328
+ const original = await harness.runtime.stores.getStore({ definition, id: "recreate" });
3329
+ await harness.runtime.stores.deleteStoreFrom({
3330
+ definition,
3331
+ id: "recreate",
3332
+ snapshot: original,
3333
+ stepId: { executionId: "recreate", stepKey: "delete" }
3334
+ });
3335
+ const runtime = await harness.restart?.() ?? harness.runtime;
3336
+ expect3(await append(runtime, "second")).toEqual({
3337
+ state: { createdBy: "initial-2", messages: [{ id: "second" }] },
3338
+ previousVersion: 0,
3339
+ version: 1
3340
+ });
3341
+ const recreated = await runtime.stores.getStore({ definition, id: "recreate" });
3342
+ expect3(recreated.instanceId).not.toBe(original.instanceId);
3343
+ expect3(evaluations).toEqual(["recreate", "recreate"]);
3344
+ } finally {
3345
+ await harness.dispose?.();
3346
+ }
3347
+ });
3348
+ test3("an unmatched take on a missing store lets its waiter wait", async () => {
3349
+ const harness = await create();
3350
+ const { runtime } = harness;
3351
+ const { definition } = countedStore("waiting-take");
3352
+ try {
3353
+ await admitWaiting(runtime, "taker");
3354
+ const miss = await runtime.stores.takeFromStore({
3355
+ definition,
3356
+ id: "waiting-take",
3357
+ selector: (state) => state.messages[0],
3358
+ claim() {}
3359
+ });
3360
+ if (miss.matched)
3361
+ throw new Error("take must not match");
3362
+ await runtime.stores.registerWaiter(waiter(definition.name, "waiting-take", miss.instanceId, miss.version, miss.readPaths, "taker"));
3363
+ await expectNotWoken(runtime);
3364
+ await runtime.stores.updateStore({
3365
+ definition,
3366
+ id: "waiting-take",
3367
+ updater(draft) {
3368
+ draft.messages.push({ id: "ready" });
3369
+ }
3370
+ });
3371
+ await expectWoken(runtime, ["taker"]);
3372
+ } finally {
3373
+ await harness.dispose?.();
3374
+ }
3375
+ });
3376
+ });
3377
+ test3("wakes an intersecting waiter's execution and keeps the waiter until its step is recorded", async () => {
3378
+ const harness = await create();
3379
+ const { runtime } = harness;
3380
+ try {
3381
+ await admitWaiting(runtime, "execution");
3382
+ const initial = await runtime.stores.getStore({
3383
+ definition: Store,
3384
+ id: "selective-wake"
3385
+ });
3386
+ await runtime.stores.registerWaiter(waiter(Store.name, "selective-wake", initial.instanceId, initial.version));
3387
+ await expectNotWoken(runtime);
3388
+ await runtime.stores.updateStore({
3389
+ definition: Store,
3390
+ id: "selective-wake",
3391
+ updater(draft) {
3392
+ draft.unrelated = 1;
3393
+ }
3394
+ });
3395
+ await expectNotWoken(runtime);
3396
+ await runtime.stores.updateStore({
3397
+ definition: Store,
3398
+ id: "selective-wake",
3399
+ updater(draft) {
3400
+ draft.messages.push({ id: "ready" });
3401
+ }
3402
+ });
3403
+ await expectWoken(runtime, ["execution"]);
3404
+ await runtime.stores.updateStore({
3405
+ definition: Store,
3406
+ id: "selective-wake",
3407
+ updater(draft) {
3408
+ draft.messages.push({ id: "again" });
3409
+ }
3410
+ });
3411
+ await expectWoken(runtime, ["execution"]);
3412
+ await recordStep(runtime, "execution", "some-other-step");
3413
+ await runtime.stores.updateStore({
3414
+ definition: Store,
3415
+ id: "selective-wake",
3416
+ updater(draft) {
3417
+ draft.messages.push({ id: "third" });
3418
+ }
3419
+ });
3420
+ await expectWoken(runtime, ["execution"]);
3421
+ await recordStep(runtime, "execution");
3422
+ await runtime.stores.updateStore({
3423
+ definition: Store,
3424
+ id: "selective-wake",
3425
+ updater(draft) {
3426
+ draft.messages.push({ id: "after-step" });
3427
+ }
3428
+ });
3429
+ await expectNotWoken(runtime);
3430
+ } finally {
3431
+ await harness.dispose?.();
3432
+ }
3433
+ });
3434
+ test3("collapses the wakes of many commits and waiters into one due execution", async () => {
3435
+ const harness = await create();
3436
+ const { runtime } = harness;
3437
+ try {
3438
+ await admitWaiting(runtime, "collapsed", "bystander");
3439
+ for (const id of ["collapse-a", "collapse-b"]) {
3440
+ const snapshot = await runtime.stores.getStore({
3441
+ definition: Store,
3442
+ id
3443
+ });
3444
+ await runtime.stores.registerWaiter({
3445
+ ...waiter(Store.name, id, snapshot.instanceId, snapshot.version),
3446
+ executionId: "collapsed",
3447
+ stepKey: `wait-${id}`
3448
+ });
3449
+ }
3450
+ const shared = await runtime.stores.getStore({
3451
+ definition: Store,
3452
+ id: "collapse-shared"
3453
+ });
3454
+ for (const executionId of ["collapsed", "bystander"]) {
3455
+ await runtime.stores.registerWaiter(waiter(Store.name, "collapse-shared", shared.instanceId, shared.version, [["messages"]], executionId));
3456
+ }
3457
+ for (const id of ["collapse-a", "collapse-b", "collapse-a", "collapse-shared"]) {
3458
+ await runtime.stores.updateStore({
3459
+ definition: Store,
3460
+ id,
3461
+ updater(draft) {
3462
+ draft.messages.push({ id: "ready" });
3463
+ }
3464
+ });
3465
+ }
3466
+ await expectWoken(runtime, ["bystander", "collapsed"]);
3467
+ await expectNotWoken(runtime);
3468
+ } finally {
3469
+ await harness.dispose?.();
3470
+ }
3471
+ });
3472
+ test3("ignores wakes for executions without a record or with a terminal outcome", async () => {
3473
+ const harness = await create();
3474
+ const { runtime } = harness;
3475
+ try {
3476
+ await runtime.executions.admit({
3477
+ executionId: "finished",
3478
+ workflowId: WORKFLOW_ID,
3479
+ params: undefined,
3480
+ context: new Map
3481
+ });
3482
+ const [lease] = await claimAll(runtime);
3483
+ await runtime.executions.acknowledge({
3484
+ lease,
3485
+ outcome: { status: "completed", result: "done" }
3486
+ });
3487
+ const initial = await runtime.stores.getStore({
3488
+ definition: Store,
3489
+ id: "orphan-wake"
3490
+ });
3491
+ for (const executionId of ["unknown", "finished"]) {
3492
+ await runtime.stores.registerWaiter(waiter(Store.name, "orphan-wake", initial.instanceId, initial.version, [["messages"]], executionId));
3493
+ }
3494
+ const committed = await runtime.stores.updateStore({
3495
+ definition: Store,
3496
+ id: "orphan-wake",
3497
+ updater(draft) {
3498
+ draft.messages.push({ id: "ready" });
3499
+ }
3500
+ });
3501
+ expect3(committed.version).toBe(1);
3502
+ await expectNotWoken(runtime);
3503
+ expect3(await runtime.executions.get("unknown")).toBeUndefined();
3504
+ expect3(await runtime.executions.get("finished")).toMatchObject({
3505
+ status: "completed",
3506
+ outcome: { status: "completed", result: "done" }
3507
+ });
3508
+ } finally {
3509
+ await harness.dispose?.();
3510
+ }
3511
+ });
3512
+ test3("deleteStore purges the store's waiters", async () => {
3513
+ const harness = await create();
3514
+ const { runtime } = harness;
3515
+ try {
3516
+ await admitWaiting(runtime, "execution");
3517
+ const original = await runtime.stores.getStore({
3518
+ definition: Store,
3519
+ id: "deleted-waiters"
3520
+ });
3521
+ await runtime.stores.registerWaiter(waiter(Store.name, "deleted-waiters", original.instanceId, original.version));
3522
+ await runtime.stores.deleteStore({ definition: Store, id: "deleted-waiters" });
3523
+ const recreated = await runtime.stores.getStore({
3524
+ definition: Store,
3525
+ id: "deleted-waiters"
3526
+ });
3527
+ expect3(recreated.instanceId).not.toBe(original.instanceId);
3528
+ await runtime.stores.updateStore({
3529
+ definition: Store,
3530
+ id: "deleted-waiters",
3531
+ updater(draft) {
3532
+ draft.messages.push({ id: "new-instance" });
3533
+ }
3534
+ });
3535
+ await expectNotWoken(runtime);
3536
+ const current = await runtime.stores.getStore({ definition: Store, id: "deleted-waiters" });
3537
+ await runtime.stores.registerWaiter(waiter(Store.name, "deleted-waiters", current.instanceId, current.version));
3538
+ await runtime.stores.updateStore({
3539
+ definition: Store,
3540
+ id: "deleted-waiters",
3541
+ updater(draft) {
3542
+ draft.messages.push({ id: "again" });
3543
+ }
3544
+ });
3545
+ await expectWoken(runtime, ["execution"]);
3546
+ } finally {
3547
+ await harness.dispose?.();
3548
+ }
3549
+ });
3550
+ test3("conditional deletion purges the store's waiters", async () => {
3551
+ const harness = await create();
3552
+ const { runtime } = harness;
3553
+ try {
3554
+ await admitWaiting(runtime, "execution");
3555
+ const initial = await runtime.stores.getStore({
3556
+ definition: Store,
3557
+ id: "conditional-delete-wake"
3558
+ });
3559
+ await runtime.stores.registerWaiter(waiter(Store.name, "conditional-delete-wake", initial.instanceId, initial.version));
3560
+ await runtime.stores.updateStore({
3561
+ definition: Store,
3562
+ id: "conditional-delete-wake",
3563
+ updater(draft) {
3564
+ draft.messages.push({ id: "old" });
3565
+ }
3566
+ });
3567
+ await expectWoken(runtime, ["execution"]);
3568
+ const current = await runtime.stores.getStore({
3569
+ definition: Store,
3570
+ id: "conditional-delete-wake"
3571
+ });
3572
+ expect3(await runtime.stores.deleteStoreFrom({
3573
+ definition: Store,
3574
+ id: "conditional-delete-wake",
3575
+ snapshot: current
3576
+ })).toEqual({ deleted: true });
3577
+ await runtime.stores.getStore({
3578
+ definition: Store,
3579
+ id: "conditional-delete-wake"
3580
+ });
3581
+ await runtime.stores.updateStore({
3582
+ definition: Store,
3583
+ id: "conditional-delete-wake",
3584
+ updater(draft) {
3585
+ draft.messages.push({ id: "new" });
3586
+ }
3587
+ });
3588
+ await expectNotWoken(runtime);
3589
+ } finally {
3590
+ await harness.dispose?.();
3591
+ }
3592
+ });
3593
+ test3("wakes for replacement, take-claim, and shifted-array paths", async () => {
3594
+ const QueueStore = defineStore2(`${name}-wake-paths-conformance`, schema2(), {
3595
+ initial: seeded(`${name}-wake-paths-conformance`, { messages: [] })
3596
+ });
3597
+ const harness = await create();
3598
+ const { runtime } = harness;
3599
+ try {
3600
+ await admitWaiting(runtime, "replacement", "claim", "splice");
3601
+ const replacement = await runtime.stores.getStore({
3602
+ definition: QueueStore,
3603
+ id: "replacement"
3604
+ });
3605
+ await runtime.stores.registerWaiter(waiter(QueueStore.name, "replacement", replacement.instanceId, replacement.version, [["messages"]], "replacement"));
3606
+ await runtime.stores.updateStore({
3607
+ definition: QueueStore,
3608
+ id: "replacement",
3609
+ updater: () => ({ messages: [{ id: "ready" }] })
3610
+ });
3611
+ await expectWoken(runtime, ["replacement"]);
3612
+ seed(QueueStore, "claim", { messages: [{ id: "one" }] });
3613
+ const claim = await runtime.stores.getStore({
3614
+ definition: QueueStore,
3615
+ id: "claim"
3616
+ });
3617
+ await runtime.stores.registerWaiter(waiter(QueueStore.name, "claim", claim.instanceId, claim.version, [["messages", 0, "claimedBy"]], "claim"));
3618
+ const claimed = await runtime.stores.takeFromStore({
3619
+ definition: QueueStore,
3620
+ id: "claim",
3621
+ selector: (state) => state.messages[0],
3622
+ claim: (_draft, selected) => {
3623
+ selected.claimedBy = "worker";
3624
+ }
3625
+ });
3626
+ if (!claimed.matched)
3627
+ throw new Error("take must match");
3628
+ expect3(claimed.selected).toEqual({ id: "one", claimedBy: "worker" });
3629
+ await expectWoken(runtime, ["claim"]);
3630
+ seed(QueueStore, "splice", { messages: [{ id: "one" }, { id: "two" }] });
3631
+ const splice = await runtime.stores.getStore({
3632
+ definition: QueueStore,
3633
+ id: "splice"
3634
+ });
3635
+ await runtime.stores.registerWaiter(waiter(QueueStore.name, "splice", splice.instanceId, splice.version, [["messages", 0, "id"]], "splice"));
3636
+ await runtime.stores.takeFromStore({
3637
+ definition: QueueStore,
3638
+ id: "splice",
3639
+ selector: (state) => state.messages[0],
3640
+ claim: (draft) => {
3641
+ draft.messages.splice(0, 1);
3642
+ }
3643
+ });
3644
+ await expectWoken(runtime, ["splice"]);
3645
+ expect3(await runtime.stores.getStore({ definition: QueueStore, id: "splice" })).toMatchObject({ state: { messages: [{ id: "two" }] }, version: 1 });
3646
+ } finally {
3647
+ await harness.dispose?.();
3648
+ }
3649
+ });
3650
+ test3("immediately wakes a stale registration", async () => {
3651
+ const harness = await create();
3652
+ const { runtime } = harness;
3653
+ try {
3654
+ await admitWaiting(runtime, "behind", "replaced", "missing");
3655
+ const initial = await runtime.stores.getStore({
3656
+ definition: Store,
3657
+ id: "stale-registration"
3658
+ });
3659
+ await runtime.stores.updateStore({
3660
+ definition: Store,
3661
+ id: "stale-registration",
3662
+ updater(draft) {
3663
+ draft.messages.push({ id: "first" });
3664
+ }
3665
+ });
3666
+ await runtime.stores.registerWaiter(waiter(Store.name, "stale-registration", initial.instanceId, initial.version, [["messages"]], "behind"));
3667
+ await expectWoken(runtime, ["behind"]);
3668
+ const original = await runtime.stores.getStore({
3669
+ definition: Store,
3670
+ id: "stale-instance"
3671
+ });
3672
+ await runtime.stores.deleteStore({ definition: Store, id: "stale-instance" });
3673
+ await runtime.stores.getStore({
3674
+ definition: Store,
3675
+ id: "stale-instance"
3676
+ });
3677
+ await runtime.stores.registerWaiter(waiter(Store.name, "stale-instance", original.instanceId, original.version, [["messages"]], "replaced"));
3678
+ await expectWoken(runtime, ["replaced"]);
3679
+ const gone = await runtime.stores.getStore({
3680
+ definition: Store,
3681
+ id: "stale-missing"
3682
+ });
3683
+ await runtime.stores.deleteStore({ definition: Store, id: "stale-missing" });
3684
+ await runtime.stores.registerWaiter(waiter(Store.name, "stale-missing", gone.instanceId, gone.version, [["messages"]], "missing"));
3685
+ await expectWoken(runtime, ["missing"]);
3686
+ } finally {
3687
+ await harness.dispose?.();
3688
+ }
3689
+ });
3690
+ test3("does not lose a waiter registration racing a commit", async () => {
3691
+ const harness = await create();
3692
+ const { runtime } = harness;
3693
+ const executionIds = Array.from({ length: 10 }, (_, index) => `registration-race-${index}`);
3694
+ try {
3695
+ await admitWaiting(runtime, ...executionIds);
3696
+ for (const [index, id] of executionIds.entries()) {
3697
+ const initial = await runtime.stores.getStore({
3698
+ definition: Store,
3699
+ id
3700
+ });
3701
+ const register = () => runtime.stores.registerWaiter(waiter(Store.name, id, initial.instanceId, initial.version, [["messages"]], id));
3702
+ const update = () => runtime.stores.updateStore({
3703
+ definition: Store,
3704
+ id,
3705
+ updater(draft) {
3706
+ draft.messages.push({ id: "ready" });
3707
+ }
3708
+ });
3709
+ await Promise.all(index % 2 === 0 ? [register(), update()] : [update(), register()]);
3710
+ }
3711
+ await expectWoken(runtime, executionIds);
3712
+ } finally {
3713
+ await harness.dispose?.();
3714
+ }
3715
+ });
3716
+ test3("qualifies declared shared-client capabilities", async () => {
3717
+ const harness = await create();
3718
+ try {
3719
+ if (!harness.createPeer)
3720
+ return;
3721
+ const primary = harness.runtime;
3722
+ const peer = await harness.createPeer();
3723
+ await primary.stores.getStore({
3724
+ definition: Store,
3725
+ id: "shared-client"
3726
+ });
3727
+ const results = await Promise.all([
3728
+ primary.stores.updateStore({
3729
+ definition: Store,
3730
+ id: "shared-client",
3731
+ updater(draft) {
3732
+ draft.messages.push({ id: "a" });
3733
+ }
3734
+ }),
3735
+ peer.stores.updateStore({
3736
+ definition: Store,
3737
+ id: "shared-client",
3738
+ updater(draft) {
3739
+ draft.messages.push({ id: "b" });
3740
+ }
3741
+ })
3742
+ ]);
3743
+ expect3(results.map((result) => result.version).sort()).toEqual([1, 2]);
3744
+ await admitWaiting(primary, "shared-wake");
3745
+ const wakeInitial = await peer.stores.getStore({
3746
+ definition: Store,
3747
+ id: "shared-wake"
3748
+ });
3749
+ await peer.stores.registerWaiter(waiter(Store.name, "shared-wake", wakeInitial.instanceId, wakeInitial.version, [["messages"]], "shared-wake"));
3750
+ await primary.stores.updateStore({
3751
+ definition: Store,
3752
+ id: "shared-wake",
3753
+ updater(draft) {
3754
+ draft.messages.push({ id: "ready" });
3755
+ }
3756
+ });
3757
+ await expectWoken(peer, ["shared-wake"]);
3758
+ await recordStep(primary, "shared-wake");
3759
+ await peer.stores.updateStore({
3760
+ definition: Store,
3761
+ id: "shared-wake",
3762
+ updater(draft) {
3763
+ draft.messages.push({ id: "after-step" });
3764
+ }
3765
+ });
3766
+ await expectNotWoken(primary);
3767
+ const registrar = await harness.createPeer();
3768
+ const committer = await harness.createPeer();
3769
+ const executionIds = Array.from({ length: 10 }, (_, index) => `shared-registration-race-${index}`);
3770
+ await admitWaiting(primary, ...executionIds);
3771
+ for (const [index, id] of executionIds.entries()) {
3772
+ const snapshot = await committer.stores.getStore({
3773
+ definition: Store,
3774
+ id
3775
+ });
3776
+ const register = () => registrar.stores.registerWaiter(waiter(Store.name, id, snapshot.instanceId, snapshot.version, [["messages"]], id));
3777
+ const update = () => committer.stores.updateStore({
3778
+ definition: Store,
3779
+ id,
3780
+ updater(draft) {
3781
+ draft.messages.push({ id: "ready" });
3782
+ }
3783
+ });
3784
+ await Promise.all(index % 2 === 0 ? [register(), update()] : [update(), register()]);
3785
+ }
3786
+ await expectWoken(primary, executionIds);
3787
+ } finally {
3788
+ await harness.dispose?.();
3789
+ }
3790
+ });
3791
+ test3("qualifies declared durable-wake capabilities", async () => {
3792
+ const harness = await create();
3793
+ try {
3794
+ if (!harness.restart)
3795
+ return;
3796
+ const { runtime } = harness;
3797
+ await admitWaiting(runtime, "durable-wake", "durable-waiter");
3798
+ for (const executionId of ["durable-wake", "durable-waiter"]) {
3799
+ const initial = await runtime.stores.getStore({
3800
+ definition: Store,
3801
+ id: executionId
3802
+ });
3803
+ await runtime.stores.registerWaiter(waiter(Store.name, executionId, initial.instanceId, initial.version, [["messages"]], executionId));
3804
+ }
3805
+ await runtime.stores.updateStore({
3806
+ definition: Store,
3807
+ id: "durable-wake",
3808
+ updater(draft) {
3809
+ draft.messages.push({ id: "ready" });
3810
+ }
3811
+ });
3812
+ const restarted = await harness.restart();
3813
+ expect3(await restarted.stores.getStore({ definition: Store, id: "durable-wake" })).toMatchObject({ state: { messages: [{ id: "ready" }] }, version: 1 });
3814
+ await expectWoken(restarted, ["durable-wake"]);
3815
+ for (const id of ["durable-wake", "durable-waiter"]) {
3816
+ await restarted.stores.updateStore({
3817
+ definition: Store,
3818
+ id,
3819
+ updater(draft) {
3820
+ draft.messages.push({ id: "after-restart" });
3821
+ }
3822
+ });
3823
+ }
3824
+ await expectWoken(restarted, ["durable-wake", "durable-waiter"]);
3825
+ } finally {
3826
+ await harness.dispose?.();
3827
+ }
3828
+ });
3829
+ });
3830
+ }
3831
+ export {
3832
+ registerEffectClientConformance,
3833
+ registerExecutionClientConformance,
3834
+ registerStoreClientConformance
3835
+ };