workerflow 0.1.0 → 0.2.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/README.md +32 -10
- package/package.json +1 -1
- package/src/definition.ts +126 -174
- package/src/migrations/0000_initial.ts +84 -285
- package/src/runtime.ts +609 -950
- package/test/runtime.spec.ts +618 -1074
- package/demo/README.md +0 -73
- package/demo/index.html +0 -13
- package/demo/package.json +0 -33
- package/demo/public/vite.svg +0 -1
- package/demo/src/App.css +0 -0
- package/demo/src/App.tsx +0 -9
- package/demo/src/assets/Cloudflare_Logo.svg +0 -51
- package/demo/src/assets/react.svg +0 -1
- package/demo/src/index.css +0 -1
- package/demo/src/main.tsx +0 -10
- package/demo/tsconfig.app.json +0 -28
- package/demo/tsconfig.json +0 -14
- package/demo/tsconfig.node.json +0 -25
- package/demo/tsconfig.worker.json +0 -13
- package/demo/vite.config.ts +0 -9
- package/demo/worker/index.ts +0 -16
- package/demo/worker-configuration.d.ts +0 -12851
- package/demo/wrangler.jsonc +0 -32
package/test/runtime.spec.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { env } from "cloudflare:workers";
|
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
import {
|
|
5
5
|
WorkflowRuntimeContext,
|
|
6
|
+
type RunStep,
|
|
7
|
+
type RunStepAttempt,
|
|
6
8
|
type RunStepId,
|
|
7
9
|
type SleepStepId,
|
|
8
10
|
type WaitStepId,
|
|
@@ -98,12 +100,14 @@ describe("WorkflowRuntime", () => {
|
|
|
98
100
|
await expect(promise).resolves.toBe("failed");
|
|
99
101
|
const steps = instance.getSteps_experimental();
|
|
100
102
|
expect(steps).toHaveLength(1);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
const step = steps[0]!;
|
|
104
|
+
expect(step.type).toBe("run");
|
|
105
|
+
const attempts = (step as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
106
|
+
expect(attempts).toHaveLength(1);
|
|
107
|
+
expect(attempts[0]).toMatchObject({
|
|
103
108
|
state: "failed",
|
|
104
109
|
errorMessage: "NonRetryableStepError: This is a non-retryable step error",
|
|
105
|
-
errorName: "NonRetryableStepError"
|
|
106
|
-
attemptCount: 1
|
|
110
|
+
errorName: "NonRetryableStepError"
|
|
107
111
|
});
|
|
108
112
|
});
|
|
109
113
|
} finally {
|
|
@@ -136,12 +140,14 @@ describe("WorkflowRuntime", () => {
|
|
|
136
140
|
await expect(promise).resolves.toBe("failed");
|
|
137
141
|
const steps = instance.getSteps_experimental();
|
|
138
142
|
expect(steps).toHaveLength(1);
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
const step = steps[0]!;
|
|
144
|
+
expect(step.type).toBe("run");
|
|
145
|
+
const attempts = (step as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
146
|
+
expect(attempts).toHaveLength(2);
|
|
147
|
+
expect(attempts[1]).toMatchObject({
|
|
141
148
|
state: "failed",
|
|
142
149
|
errorMessage: "Error: test",
|
|
143
|
-
errorName: "Error"
|
|
144
|
-
attemptCount: 2
|
|
150
|
+
errorName: "Error"
|
|
145
151
|
});
|
|
146
152
|
});
|
|
147
153
|
} finally {
|
|
@@ -198,11 +204,12 @@ describe("WorkflowRuntime", () => {
|
|
|
198
204
|
await expect(promise).resolves.toBe("completed");
|
|
199
205
|
const steps = instance.getSteps_experimental();
|
|
200
206
|
expect(steps).toHaveLength(1);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
207
|
+
const step = steps[0]!;
|
|
208
|
+
expect(step.type).toBe("run");
|
|
209
|
+
const attempts = (step as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
210
|
+
expect(attempts).toHaveLength(2);
|
|
211
|
+
expect(attempts[0]).toMatchObject({ state: "failed" });
|
|
212
|
+
expect(attempts[1]).toMatchObject({ state: "succeeded" });
|
|
206
213
|
// First next(): failed attempt yields suspended. Retry alarm: second next() replays `execute()` and completes
|
|
207
214
|
// the successful attempt in the same invocation (no extra immediate loop).
|
|
208
215
|
expect(nextSpy).toHaveBeenCalledTimes(2);
|
|
@@ -266,8 +273,16 @@ describe("WorkflowRuntime", () => {
|
|
|
266
273
|
|
|
267
274
|
const steps = instance.getSteps_experimental();
|
|
268
275
|
expect(steps).toHaveLength(2);
|
|
269
|
-
|
|
270
|
-
expect(
|
|
276
|
+
const firstStep = steps[0]!;
|
|
277
|
+
expect(firstStep.id).toBe("step-a");
|
|
278
|
+
expect(firstStep.type).toBe("run");
|
|
279
|
+
const firstAttempts = (firstStep as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
280
|
+
expect(firstAttempts[firstAttempts.length - 1]).toMatchObject({ state: "succeeded" });
|
|
281
|
+
const secondStep = steps[1]!;
|
|
282
|
+
expect(secondStep.id).toBe("step-b");
|
|
283
|
+
expect(secondStep.type).toBe("run");
|
|
284
|
+
const secondAttempts = (secondStep as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
285
|
+
expect(secondAttempts[secondAttempts.length - 1]).toMatchObject({ state: "succeeded" });
|
|
271
286
|
|
|
272
287
|
expect(nextSpy).toHaveBeenCalledTimes(2);
|
|
273
288
|
});
|
|
@@ -300,7 +315,10 @@ describe("WorkflowRuntime", () => {
|
|
|
300
315
|
await expect(promise).resolves.toBe("completed");
|
|
301
316
|
|
|
302
317
|
const steps = instance.getSteps_experimental();
|
|
303
|
-
|
|
318
|
+
const before = steps.find((s) => s.id === "before-sleep");
|
|
319
|
+
expect(before?.type).toBe("run");
|
|
320
|
+
const beforeAttempts = (before as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
321
|
+
expect(beforeAttempts[beforeAttempts.length - 1]).toMatchObject({ state: "succeeded" });
|
|
304
322
|
expect(steps.find((s) => s.id === "sleep-after-run")).toMatchObject({
|
|
305
323
|
type: "sleep",
|
|
306
324
|
state: "elapsed",
|
|
@@ -414,26 +432,16 @@ describe("WorkflowRuntime", () => {
|
|
|
414
432
|
await expect(promise).resolves.toBe("completed");
|
|
415
433
|
|
|
416
434
|
const steps = instance.getSteps_experimental();
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
type: "run",
|
|
424
|
-
parentStepId: "L0",
|
|
425
|
-
state: "succeeded"
|
|
426
|
-
});
|
|
427
|
-
expect(steps.find((s) => s.id === "L2")).toMatchObject({
|
|
428
|
-
type: "run",
|
|
429
|
-
parentStepId: "L1",
|
|
430
|
-
state: "succeeded"
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
const events = await instance.getStepEvents_experimental();
|
|
434
|
-
for (const id of ["L0", "L1"] as const) {
|
|
435
|
-
expect(events.filter((event) => event.stepId === id && event.type === "attempt_failed")).toHaveLength(0);
|
|
435
|
+
for (const id of ["L0", "L1", "L2"] as const) {
|
|
436
|
+
const row = steps.find((s) => s.id === id);
|
|
437
|
+
expect(row?.type).toBe("run");
|
|
438
|
+
const attempts = (row as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
439
|
+
expect(attempts.filter((a) => a.state === "failed")).toHaveLength(0);
|
|
440
|
+
expect(attempts[attempts.length - 1]).toMatchObject({ state: "succeeded" });
|
|
436
441
|
}
|
|
442
|
+
expect(steps.find((s) => s.id === "L0")).toMatchObject({ parentStepId: null });
|
|
443
|
+
expect(steps.find((s) => s.id === "L1")).toMatchObject({ parentStepId: "L0" });
|
|
444
|
+
expect(steps.find((s) => s.id === "L2")).toMatchObject({ parentStepId: "L1" });
|
|
437
445
|
});
|
|
438
446
|
} finally {
|
|
439
447
|
executeSpy.mockRestore();
|
|
@@ -462,11 +470,11 @@ describe("WorkflowRuntime", () => {
|
|
|
462
470
|
await expect(promise).resolves.toBe("completed");
|
|
463
471
|
|
|
464
472
|
const steps = instance.getSteps_experimental();
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
});
|
|
473
|
+
const rootAfter = steps.find((s) => s.id === "root-after");
|
|
474
|
+
expect(rootAfter?.type).toBe("run");
|
|
475
|
+
expect(rootAfter).toMatchObject({ parentStepId: null });
|
|
476
|
+
const raa = (rootAfter as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
477
|
+
expect(raa[raa.length - 1]).toMatchObject({ state: "succeeded" });
|
|
470
478
|
expect(steps.find((s) => s.id === "nest-inner")).toMatchObject({
|
|
471
479
|
parentStepId: "nest-outer"
|
|
472
480
|
});
|
|
@@ -504,30 +512,15 @@ describe("WorkflowRuntime", () => {
|
|
|
504
512
|
await expect(promise).resolves.toBe("completed");
|
|
505
513
|
|
|
506
514
|
const steps = instance.getSteps_experimental();
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
});
|
|
517
|
-
expect(steps.find((s) => s.id === "branch-b")).toMatchObject({
|
|
518
|
-
type: "run",
|
|
519
|
-
parentStepId: null,
|
|
520
|
-
state: "succeeded"
|
|
521
|
-
});
|
|
522
|
-
expect(steps.find((s) => s.id === "branch-b-inner")).toMatchObject({
|
|
523
|
-
type: "run",
|
|
524
|
-
parentStepId: "branch-b",
|
|
525
|
-
state: "succeeded"
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
const events = await instance.getStepEvents_experimental();
|
|
529
|
-
expect(events.filter((event) => event.stepId === "branch-a" && event.type === "attempt_failed")).toHaveLength(0);
|
|
530
|
-
expect(events.filter((event) => event.stepId === "branch-b" && event.type === "attempt_failed")).toHaveLength(0);
|
|
515
|
+
for (const id of ["branch-a", "branch-a-inner", "branch-b", "branch-b-inner"] as const) {
|
|
516
|
+
const row = steps.find((s) => s.id === id);
|
|
517
|
+
expect(row?.type).toBe("run");
|
|
518
|
+
const attempts = (row as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
519
|
+
expect(attempts.filter((a) => a.state === "failed")).toHaveLength(0);
|
|
520
|
+
expect(attempts[attempts.length - 1]).toMatchObject({ state: "succeeded" });
|
|
521
|
+
}
|
|
522
|
+
expect(steps.find((s) => s.id === "branch-a-inner")).toMatchObject({ parentStepId: "branch-a" });
|
|
523
|
+
expect(steps.find((s) => s.id === "branch-b-inner")).toMatchObject({ parentStepId: "branch-b" });
|
|
531
524
|
});
|
|
532
525
|
} finally {
|
|
533
526
|
executeSpy.mockRestore();
|
|
@@ -560,11 +553,11 @@ describe("WorkflowRuntime", () => {
|
|
|
560
553
|
await expect(promise).resolves.toBe("completed");
|
|
561
554
|
|
|
562
555
|
const steps = instance.getSteps_experimental();
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
});
|
|
556
|
+
const nbInner = steps.find((s) => s.id === "nested-branch-inner");
|
|
557
|
+
expect(nbInner?.type).toBe("run");
|
|
558
|
+
expect(nbInner).toMatchObject({ parentStepId: "nested-branch" });
|
|
559
|
+
const nbia = (nbInner as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
560
|
+
expect(nbia[nbia.length - 1]).toMatchObject({ state: "succeeded" });
|
|
568
561
|
expect(steps.find((s) => s.id === "parallel-wait-nested")).toMatchObject({
|
|
569
562
|
type: "wait",
|
|
570
563
|
state: "waiting"
|
|
@@ -629,7 +622,10 @@ describe("WorkflowRuntime", () => {
|
|
|
629
622
|
await instance.create({ definitionVersion: "2026-03-19" });
|
|
630
623
|
|
|
631
624
|
await expect
|
|
632
|
-
.poll(() =>
|
|
625
|
+
.poll(() => {
|
|
626
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "deep-wait");
|
|
627
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
628
|
+
})
|
|
633
629
|
.toBe("waiting");
|
|
634
630
|
|
|
635
631
|
const stepsWaiting = instance.getSteps_experimental();
|
|
@@ -646,12 +642,12 @@ describe("WorkflowRuntime", () => {
|
|
|
646
642
|
type: "wait",
|
|
647
643
|
parentStepId: "outer-wait",
|
|
648
644
|
state: "satisfied",
|
|
649
|
-
payload:
|
|
650
|
-
});
|
|
651
|
-
expect(instance.getSteps_experimental().find((s) => s.id === "outer-wait")).toMatchObject({
|
|
652
|
-
type: "run",
|
|
653
|
-
state: "succeeded"
|
|
645
|
+
payload: { ok: true }
|
|
654
646
|
});
|
|
647
|
+
const outerWait = instance.getSteps_experimental().find((s) => s.id === "outer-wait");
|
|
648
|
+
expect(outerWait?.type).toBe("run");
|
|
649
|
+
const owa = (outerWait as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
650
|
+
expect(owa[owa.length - 1]).toMatchObject({ state: "succeeded" });
|
|
655
651
|
});
|
|
656
652
|
} finally {
|
|
657
653
|
executeSpy.mockRestore();
|
|
@@ -681,16 +677,14 @@ describe("WorkflowRuntime", () => {
|
|
|
681
677
|
await expect(promise).resolves.toBe("failed");
|
|
682
678
|
|
|
683
679
|
const steps = instance.getSteps_experimental();
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
expect(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
errorName: "NonRetryableStepError"
|
|
693
|
-
});
|
|
680
|
+
const fi = steps.find((s) => s.id === "fail-inner");
|
|
681
|
+
expect(fi?.type).toBe("run");
|
|
682
|
+
const fia = (fi as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
683
|
+
expect(fia[fia.length - 1]).toMatchObject({ state: "failed", errorName: "NonRetryableStepError" });
|
|
684
|
+
const fo = steps.find((s) => s.id === "fail-outer");
|
|
685
|
+
expect(fo?.type).toBe("run");
|
|
686
|
+
const foa = (fo as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
687
|
+
expect(foa[foa.length - 1]).toMatchObject({ state: "failed", errorName: "NonRetryableStepError" });
|
|
694
688
|
});
|
|
695
689
|
} finally {
|
|
696
690
|
executeSpy.mockRestore();
|
|
@@ -730,16 +724,15 @@ describe("WorkflowRuntime", () => {
|
|
|
730
724
|
await expect(promise).resolves.toBe("completed");
|
|
731
725
|
|
|
732
726
|
expect(innerAttempts).toBe(2);
|
|
733
|
-
const
|
|
734
|
-
expect(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
expect(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
});
|
|
727
|
+
const outerRow = instance.getSteps_experimental().find((s) => s.id === "suspend-outer");
|
|
728
|
+
expect(outerRow?.type).toBe("run");
|
|
729
|
+
const oa = (outerRow as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
730
|
+
expect(oa.filter((a) => a.state === "failed")).toHaveLength(0);
|
|
731
|
+
expect(oa[oa.length - 1]).toMatchObject({ state: "succeeded" });
|
|
732
|
+
const innerRow = instance.getSteps_experimental().find((s) => s.id === "suspend-inner");
|
|
733
|
+
expect(innerRow?.type).toBe("run");
|
|
734
|
+
const ia = (innerRow as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
735
|
+
expect(ia[ia.length - 1]).toMatchObject({ state: "succeeded" });
|
|
743
736
|
});
|
|
744
737
|
} finally {
|
|
745
738
|
executeSpy.mockRestore();
|
|
@@ -773,17 +766,20 @@ describe("WorkflowRuntime", () => {
|
|
|
773
766
|
await expect(promise).resolves.toBe("failed");
|
|
774
767
|
|
|
775
768
|
const steps = instance.getSteps_experimental();
|
|
776
|
-
|
|
777
|
-
|
|
769
|
+
const exInner = steps.find((s) => s.id === "ex-inner");
|
|
770
|
+
expect(exInner?.type).toBe("run");
|
|
771
|
+
const exIa = (exInner as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
772
|
+
expect(exIa[exIa.length - 1]).toMatchObject({
|
|
778
773
|
state: "failed",
|
|
779
774
|
errorName: "Error",
|
|
780
775
|
errorMessage: "Error: always fail"
|
|
781
776
|
});
|
|
782
|
-
|
|
783
|
-
|
|
777
|
+
const exOuter = steps.find((s) => s.id === "ex-outer");
|
|
778
|
+
expect(exOuter?.type).toBe("run");
|
|
779
|
+
const exOa = (exOuter as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
780
|
+
expect(exOa[exOa.length - 1]).toMatchObject({
|
|
784
781
|
state: "failed",
|
|
785
|
-
errorName: "Error"
|
|
786
|
-
errorMessage: "Error"
|
|
782
|
+
errorName: "Error"
|
|
787
783
|
});
|
|
788
784
|
});
|
|
789
785
|
} finally {
|
|
@@ -813,12 +809,14 @@ describe("WorkflowRuntime", () => {
|
|
|
813
809
|
await expect(promise).resolves.toBe("failed");
|
|
814
810
|
|
|
815
811
|
const steps = instance.getSteps_experimental();
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
|
|
812
|
+
const pi = steps.find((s) => s.id === "post-inner");
|
|
813
|
+
expect(pi?.type).toBe("run");
|
|
814
|
+
const pia = (pi as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
815
|
+
expect(pia[pia.length - 1]).toMatchObject({ state: "succeeded" });
|
|
816
|
+
const po = steps.find((s) => s.id === "post-outer");
|
|
817
|
+
expect(po?.type).toBe("run");
|
|
818
|
+
const poa = (po as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
819
|
+
expect(poa[poa.length - 1]).toMatchObject({
|
|
822
820
|
state: "failed",
|
|
823
821
|
errorMessage: expect.stringContaining("outer-only failure")
|
|
824
822
|
});
|
|
@@ -849,21 +847,25 @@ describe("WorkflowRuntime", () => {
|
|
|
849
847
|
|
|
850
848
|
await instance.create({ definitionVersion: "2026-03-19" });
|
|
851
849
|
await expect
|
|
852
|
-
.poll(() =>
|
|
850
|
+
.poll(() => {
|
|
851
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "root-deep-wait");
|
|
852
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
853
|
+
})
|
|
853
854
|
.toBe("waiting");
|
|
854
855
|
|
|
855
|
-
const
|
|
856
|
-
expect(
|
|
857
|
-
|
|
858
|
-
).toHaveLength(0);
|
|
856
|
+
const rootRunBefore = instance.getSteps_experimental().find((s) => s.id === "root-wait-run");
|
|
857
|
+
expect(rootRunBefore?.type).toBe("run");
|
|
858
|
+
const rba = (rootRunBefore as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
859
|
+
expect(rba.filter((a) => a.state === "failed")).toHaveLength(0);
|
|
859
860
|
|
|
860
861
|
await instance.handleInboundEvent("root-deep-ev", true);
|
|
861
862
|
await expect(promise).resolves.toBe("completed");
|
|
862
863
|
|
|
863
|
-
const
|
|
864
|
-
expect(
|
|
865
|
-
|
|
866
|
-
).toHaveLength(0);
|
|
864
|
+
const rootRunAfter = instance.getSteps_experimental().find((s) => s.id === "root-wait-run");
|
|
865
|
+
expect(rootRunAfter?.type).toBe("run");
|
|
866
|
+
const raa = (rootRunAfter as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
867
|
+
expect(raa.filter((a) => a.state === "failed")).toHaveLength(0);
|
|
868
|
+
expect(raa[raa.length - 1]).toMatchObject({ state: "succeeded" });
|
|
867
869
|
});
|
|
868
870
|
} finally {
|
|
869
871
|
executeSpy.mockRestore();
|
|
@@ -898,10 +900,10 @@ describe("WorkflowRuntime", () => {
|
|
|
898
900
|
|
|
899
901
|
const steps = instance.getSteps_experimental();
|
|
900
902
|
expect(steps).toHaveLength(2);
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
});
|
|
903
|
+
const prun = steps.find((s) => s.id === "parallel-run");
|
|
904
|
+
expect(prun?.type).toBe("run");
|
|
905
|
+
const pra = (prun as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
906
|
+
expect(pra[pra.length - 1]).toMatchObject({ state: "succeeded" });
|
|
905
907
|
expect(steps.find((s) => s.id === "parallel-wait")).toMatchObject({
|
|
906
908
|
type: "wait",
|
|
907
909
|
state: "waiting"
|
|
@@ -937,15 +939,14 @@ describe("WorkflowRuntime", () => {
|
|
|
937
939
|
|
|
938
940
|
const steps = instance.getSteps_experimental();
|
|
939
941
|
expect(steps).toHaveLength(2);
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
expect(
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
});
|
|
942
|
+
const pf = steps.find((s) => s.id === "parallel-fail");
|
|
943
|
+
expect(pf?.type).toBe("run");
|
|
944
|
+
const pfa = (pf as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
945
|
+
expect(pfa[pfa.length - 1]).toMatchObject({ state: "failed", errorName: "NonRetryableStepError" });
|
|
946
|
+
const pok = steps.find((s) => s.id === "parallel-ok");
|
|
947
|
+
expect(pok?.type).toBe("run");
|
|
948
|
+
const poka = (pok as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
949
|
+
expect(poka[poka.length - 1]).toMatchObject({ state: "succeeded" });
|
|
949
950
|
});
|
|
950
951
|
} finally {
|
|
951
952
|
executeSpy.mockRestore();
|
|
@@ -982,7 +983,10 @@ describe("WorkflowRuntime", () => {
|
|
|
982
983
|
expect(terminalStatuses).toHaveLength(0);
|
|
983
984
|
|
|
984
985
|
await expect
|
|
985
|
-
.poll(() =>
|
|
986
|
+
.poll(() => {
|
|
987
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "allsettled-rerun-wait");
|
|
988
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
989
|
+
})
|
|
986
990
|
.toBe("waiting");
|
|
987
991
|
|
|
988
992
|
const steps = instance.getSteps_experimental();
|
|
@@ -992,10 +996,10 @@ describe("WorkflowRuntime", () => {
|
|
|
992
996
|
});
|
|
993
997
|
// Unlike `Promise.all`, `allSettled` waits for every branch before returning, so the run can finish
|
|
994
998
|
// durably before we rethrow the `wait()` rejection.
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
});
|
|
999
|
+
const asRun = steps.find((s) => s.id === "allsettled-rerun-run");
|
|
1000
|
+
expect(asRun?.type).toBe("run");
|
|
1001
|
+
const asra = (asRun as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
1002
|
+
expect(asra[asra.length - 1]).toMatchObject({ state: "succeeded" });
|
|
999
1003
|
});
|
|
1000
1004
|
} finally {
|
|
1001
1005
|
executeSpy.mockRestore();
|
|
@@ -1029,7 +1033,10 @@ describe("WorkflowRuntime", () => {
|
|
|
1029
1033
|
expect(terminalStatuses).toHaveLength(0);
|
|
1030
1034
|
|
|
1031
1035
|
await expect
|
|
1032
|
-
.poll(() =>
|
|
1036
|
+
.poll(() => {
|
|
1037
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "parallel-wait");
|
|
1038
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1039
|
+
})
|
|
1033
1040
|
.toBe("waiting");
|
|
1034
1041
|
|
|
1035
1042
|
const steps = instance.getSteps_experimental();
|
|
@@ -1037,11 +1044,11 @@ describe("WorkflowRuntime", () => {
|
|
|
1037
1044
|
type: "wait",
|
|
1038
1045
|
state: "waiting"
|
|
1039
1046
|
});
|
|
1040
|
-
// `wait()` usually wins the race; the run branch can still be mid-flight so the
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
});
|
|
1047
|
+
// `wait()` usually wins the race; the run branch can still be mid-flight so the latest attempt may still be in flight.
|
|
1048
|
+
const parRun = steps.find((s) => s.id === "parallel-run");
|
|
1049
|
+
expect(parRun?.type).toBe("run");
|
|
1050
|
+
const paa = (parRun as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
1051
|
+
expect(paa[paa.length - 1]).toMatchObject({ state: "started" });
|
|
1045
1052
|
});
|
|
1046
1053
|
} finally {
|
|
1047
1054
|
executeSpy.mockRestore();
|
|
@@ -1113,7 +1120,10 @@ describe("WorkflowRuntime", () => {
|
|
|
1113
1120
|
await instance.create({ definitionVersion: "2026-03-19" });
|
|
1114
1121
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
1115
1122
|
await expect
|
|
1116
|
-
.poll(() =>
|
|
1123
|
+
.poll(() => {
|
|
1124
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-1");
|
|
1125
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1126
|
+
})
|
|
1117
1127
|
.toBe("waiting");
|
|
1118
1128
|
|
|
1119
1129
|
await instance.pause();
|
|
@@ -1255,7 +1265,10 @@ describe("WorkflowRuntime", () => {
|
|
|
1255
1265
|
await instance.create({ definitionVersion: "2026-03-19" });
|
|
1256
1266
|
|
|
1257
1267
|
await expect
|
|
1258
|
-
.poll(() =>
|
|
1268
|
+
.poll(() => {
|
|
1269
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-1");
|
|
1270
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1271
|
+
})
|
|
1259
1272
|
.toBe("waiting");
|
|
1260
1273
|
|
|
1261
1274
|
await instance.pause();
|
|
@@ -1295,7 +1308,10 @@ describe("WorkflowRuntime", () => {
|
|
|
1295
1308
|
|
|
1296
1309
|
await instance.create({ definitionVersion: "2026-03-19" });
|
|
1297
1310
|
await expect
|
|
1298
|
-
.poll(() =>
|
|
1311
|
+
.poll(() => {
|
|
1312
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-1");
|
|
1313
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1314
|
+
})
|
|
1299
1315
|
.toBe("waiting");
|
|
1300
1316
|
await instance.pause();
|
|
1301
1317
|
|
|
@@ -1307,7 +1323,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1307
1323
|
expect(instance.getSteps_experimental().find((s) => s.id === "wait-1")).toMatchObject({
|
|
1308
1324
|
type: "wait",
|
|
1309
1325
|
state: "satisfied",
|
|
1310
|
-
payload:
|
|
1326
|
+
payload: { data: "test" }
|
|
1311
1327
|
});
|
|
1312
1328
|
});
|
|
1313
1329
|
} finally {
|
|
@@ -1330,7 +1346,10 @@ describe("WorkflowRuntime", () => {
|
|
|
1330
1346
|
await instance.create({ definitionVersion: "2026-03-19" });
|
|
1331
1347
|
await expect.poll(() => instance.getStatus()).toBe("running");
|
|
1332
1348
|
await expect
|
|
1333
|
-
.poll(() =>
|
|
1349
|
+
.poll(() => {
|
|
1350
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-1");
|
|
1351
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1352
|
+
})
|
|
1334
1353
|
.toBe("waiting");
|
|
1335
1354
|
|
|
1336
1355
|
await instance.pause();
|
|
@@ -1348,6 +1367,235 @@ describe("WorkflowRuntime", () => {
|
|
|
1348
1367
|
});
|
|
1349
1368
|
});
|
|
1350
1369
|
|
|
1370
|
+
describe("handleInboundEvent()", () => {
|
|
1371
|
+
it("persists satisfied wait payload on inbound_events with claimed_by pointing at the wait step", async () => {
|
|
1372
|
+
const executeSpy = vi
|
|
1373
|
+
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
1374
|
+
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
1375
|
+
await this.wait("wait-inbound-row", "evt-claim", {
|
|
1376
|
+
timeoutAt: Date.now() + 86_400_000
|
|
1377
|
+
});
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
try {
|
|
1381
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1382
|
+
await runInDurableObject(stub, async (instance, state) => {
|
|
1383
|
+
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
1384
|
+
instance.onStatusChange_experimental = async (status) => {
|
|
1385
|
+
if (status === "running") return;
|
|
1386
|
+
resolve(status);
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
await instance.create({ definitionVersion: "2026-03-19" });
|
|
1390
|
+
|
|
1391
|
+
await expect
|
|
1392
|
+
.poll(() => {
|
|
1393
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-inbound-row");
|
|
1394
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1395
|
+
})
|
|
1396
|
+
.toBe("waiting");
|
|
1397
|
+
|
|
1398
|
+
await instance.handleInboundEvent("evt-claim", { trace: "x" });
|
|
1399
|
+
await expect(promise).resolves.toBe("completed");
|
|
1400
|
+
|
|
1401
|
+
const formatted = instance.getSteps_experimental().find((s) => s.id === "wait-inbound-row");
|
|
1402
|
+
expect(formatted).toMatchObject({
|
|
1403
|
+
type: "wait",
|
|
1404
|
+
state: "satisfied",
|
|
1405
|
+
payload: { trace: "x" }
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
const rows = state.storage.sql
|
|
1409
|
+
.exec<{ payload: string; claimed_by: string | null }>(
|
|
1410
|
+
`SELECT payload, claimed_by FROM inbound_events WHERE claimed_by = ?`,
|
|
1411
|
+
"wait-inbound-row"
|
|
1412
|
+
)
|
|
1413
|
+
.toArray();
|
|
1414
|
+
expect(rows).toHaveLength(1);
|
|
1415
|
+
expect(rows[0]!.claimed_by).toBe("wait-inbound-row");
|
|
1416
|
+
expect(JSON.parse(rows[0]!.payload)).toEqual({ trace: "x" });
|
|
1417
|
+
});
|
|
1418
|
+
} finally {
|
|
1419
|
+
executeSpy.mockRestore();
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
it("satisfies a waiting wait step when called without a payload", async () => {
|
|
1424
|
+
const executeSpy = vi
|
|
1425
|
+
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
1426
|
+
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
1427
|
+
const payload = await this.wait<undefined>("wait-no-payload", "evt");
|
|
1428
|
+
await this.run("after-wait-no-payload", async () => payload);
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
try {
|
|
1432
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1433
|
+
await runInDurableObject(stub, async (instance) => {
|
|
1434
|
+
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
1435
|
+
instance.onStatusChange_experimental = async (status) => {
|
|
1436
|
+
if (status === "running") return;
|
|
1437
|
+
resolve(status);
|
|
1438
|
+
};
|
|
1439
|
+
|
|
1440
|
+
await instance.create({ definitionVersion: "2026-03-19" });
|
|
1441
|
+
|
|
1442
|
+
await expect
|
|
1443
|
+
.poll(() => {
|
|
1444
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-no-payload");
|
|
1445
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1446
|
+
})
|
|
1447
|
+
.toBe("waiting");
|
|
1448
|
+
|
|
1449
|
+
await instance.handleInboundEvent("evt");
|
|
1450
|
+
await expect(promise).resolves.toBe("completed");
|
|
1451
|
+
|
|
1452
|
+
expect(instance.getSteps_experimental().find((s) => s.id === "wait-no-payload")).toMatchObject({
|
|
1453
|
+
type: "wait",
|
|
1454
|
+
state: "satisfied",
|
|
1455
|
+
payload: undefined
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
const afterWait = instance.getSteps_experimental().find((s) => s.id === "after-wait-no-payload");
|
|
1459
|
+
expect(afterWait?.type).toBe("run");
|
|
1460
|
+
const attempts = (afterWait as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
1461
|
+
expect(attempts[attempts.length - 1]).toMatchObject({
|
|
1462
|
+
state: "succeeded",
|
|
1463
|
+
resultType: "none"
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
} finally {
|
|
1467
|
+
executeSpy.mockRestore();
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
it("satisfies a waiting wait step when called with an explicit null payload", async () => {
|
|
1472
|
+
const executeSpy = vi
|
|
1473
|
+
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
1474
|
+
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
1475
|
+
const payload = await this.wait<null>("wait-null", "evt");
|
|
1476
|
+
await this.run("after-wait-null", async () => payload);
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
try {
|
|
1480
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1481
|
+
await runInDurableObject(stub, async (instance) => {
|
|
1482
|
+
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
1483
|
+
instance.onStatusChange_experimental = async (status) => {
|
|
1484
|
+
if (status === "running") return;
|
|
1485
|
+
resolve(status);
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
await instance.create({ definitionVersion: "2026-03-19" });
|
|
1489
|
+
|
|
1490
|
+
await expect
|
|
1491
|
+
.poll(() => {
|
|
1492
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-null");
|
|
1493
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1494
|
+
})
|
|
1495
|
+
.toBe("waiting");
|
|
1496
|
+
|
|
1497
|
+
await instance.handleInboundEvent("evt", null);
|
|
1498
|
+
await expect(promise).resolves.toBe("completed");
|
|
1499
|
+
|
|
1500
|
+
expect(instance.getSteps_experimental().find((s) => s.id === "wait-null")).toMatchObject({
|
|
1501
|
+
type: "wait",
|
|
1502
|
+
state: "satisfied",
|
|
1503
|
+
payload: null
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
const afterWait = instance.getSteps_experimental().find((s) => s.id === "after-wait-null");
|
|
1507
|
+
expect(afterWait?.type).toBe("run");
|
|
1508
|
+
const attempts = (afterWait as RunStep & { attempts: RunStepAttempt[] }).attempts;
|
|
1509
|
+
expect(attempts[attempts.length - 1]).toMatchObject({
|
|
1510
|
+
state: "succeeded",
|
|
1511
|
+
resultType: "json",
|
|
1512
|
+
resultJson: "null"
|
|
1513
|
+
});
|
|
1514
|
+
});
|
|
1515
|
+
} finally {
|
|
1516
|
+
executeSpy.mockRestore();
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
it("satisfies a waiting wait step via queued payloadless event claimed on resume", async () => {
|
|
1521
|
+
const executeSpy = vi
|
|
1522
|
+
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
1523
|
+
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
1524
|
+
await this.wait("wait-queued", "evt");
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
try {
|
|
1528
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1529
|
+
await runInDurableObject(stub, async (instance) => {
|
|
1530
|
+
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
1531
|
+
instance.onStatusChange_experimental = async (status) => {
|
|
1532
|
+
if (status === "running" || status === "paused") return;
|
|
1533
|
+
resolve(status);
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
await instance.create({ definitionVersion: "2026-03-19" });
|
|
1537
|
+
|
|
1538
|
+
await expect
|
|
1539
|
+
.poll(() => {
|
|
1540
|
+
const step = instance.getSteps_experimental().find((s) => s.id === "wait-queued");
|
|
1541
|
+
return step?.type === "wait" ? step.state : undefined;
|
|
1542
|
+
})
|
|
1543
|
+
.toBe("waiting");
|
|
1544
|
+
|
|
1545
|
+
await instance.pause();
|
|
1546
|
+
|
|
1547
|
+
// Queue the event without a payload while paused
|
|
1548
|
+
await instance.handleInboundEvent("evt");
|
|
1549
|
+
|
|
1550
|
+
// The wait step should still be waiting (event was only queued, not claimed)
|
|
1551
|
+
expect(instance.getSteps_experimental().find((s) => s.id === "wait-queued")).toMatchObject({
|
|
1552
|
+
type: "wait",
|
|
1553
|
+
state: "waiting"
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
await instance.resume();
|
|
1557
|
+
await expect(promise).resolves.toBe("completed");
|
|
1558
|
+
|
|
1559
|
+
expect(instance.getSteps_experimental().find((s) => s.id === "wait-queued")).toMatchObject({
|
|
1560
|
+
type: "wait",
|
|
1561
|
+
state: "satisfied",
|
|
1562
|
+
payload: undefined
|
|
1563
|
+
});
|
|
1564
|
+
});
|
|
1565
|
+
} finally {
|
|
1566
|
+
executeSpy.mockRestore();
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
it("is a no-op when the workflow is in a terminal state", async () => {
|
|
1571
|
+
const executeSpy = vi
|
|
1572
|
+
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
1573
|
+
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
1574
|
+
await this.run("step-1", async () => "done");
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
try {
|
|
1578
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1579
|
+
await runInDurableObject(stub, async (instance) => {
|
|
1580
|
+
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
1581
|
+
instance.onStatusChange_experimental = async (status) => {
|
|
1582
|
+
if (status === "running") return;
|
|
1583
|
+
resolve(status);
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
await instance.create({ definitionVersion: "2026-03-19" });
|
|
1587
|
+
await expect(promise).resolves.toBe("completed");
|
|
1588
|
+
|
|
1589
|
+
// Should not throw even though there is no matching wait step
|
|
1590
|
+
await instance.handleInboundEvent("any-event", { data: 1 });
|
|
1591
|
+
expect(instance.getStatus()).toBe("completed");
|
|
1592
|
+
});
|
|
1593
|
+
} finally {
|
|
1594
|
+
executeSpy.mockRestore();
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1351
1599
|
describe("getWorkflowEvents_experimental()", () => {
|
|
1352
1600
|
it("records 'created' when workflow is first initialized", async () => {
|
|
1353
1601
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
@@ -1513,20 +1761,22 @@ describe("WorkflowRuntime", () => {
|
|
|
1513
1761
|
});
|
|
1514
1762
|
});
|
|
1515
1763
|
|
|
1764
|
+
|
|
1516
1765
|
describe("WorkflowRuntimeContext", () => {
|
|
1517
1766
|
describe("run steps", () => {
|
|
1518
|
-
describe("
|
|
1767
|
+
describe("getOrCreateRunStep()", () => {
|
|
1519
1768
|
it("creates a new run step", async () => {
|
|
1520
1769
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1521
1770
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1522
1771
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1523
|
-
const step =
|
|
1772
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1524
1773
|
expect(step).toMatchObject({
|
|
1525
1774
|
id: "step-1",
|
|
1526
1775
|
type: "run",
|
|
1527
|
-
|
|
1528
|
-
|
|
1776
|
+
maxAttempts: 3,
|
|
1777
|
+
parentStepId: null
|
|
1529
1778
|
});
|
|
1779
|
+
expect(step.attempts).toEqual([]);
|
|
1530
1780
|
});
|
|
1531
1781
|
});
|
|
1532
1782
|
|
|
@@ -1534,25 +1784,19 @@ describe("WorkflowRuntime", () => {
|
|
|
1534
1784
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1535
1785
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1536
1786
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1537
|
-
const first =
|
|
1538
|
-
|
|
1539
|
-
parentStepId: null
|
|
1540
|
-
});
|
|
1541
|
-
const second = await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
1542
|
-
type: "run",
|
|
1543
|
-
parentStepId: null
|
|
1544
|
-
});
|
|
1787
|
+
const first = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1788
|
+
const second = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1545
1789
|
expect(first).toEqual(second);
|
|
1546
1790
|
});
|
|
1547
1791
|
});
|
|
1548
1792
|
|
|
1549
|
-
it("
|
|
1793
|
+
it("leaves attempts empty until handleRunAttemptStarted", async () => {
|
|
1550
1794
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1551
|
-
await runInDurableObject(stub, async (
|
|
1795
|
+
await runInDurableObject(stub, async (_instance, state) => {
|
|
1552
1796
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1797
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1798
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1799
|
+
expect(step.attempts).toEqual([]);
|
|
1556
1800
|
});
|
|
1557
1801
|
});
|
|
1558
1802
|
|
|
@@ -1560,8 +1804,7 @@ describe("WorkflowRuntime", () => {
|
|
|
1560
1804
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1561
1805
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1562
1806
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1563
|
-
const step =
|
|
1564
|
-
type: "run",
|
|
1807
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), {
|
|
1565
1808
|
maxAttempts: 5,
|
|
1566
1809
|
parentStepId: null
|
|
1567
1810
|
});
|
|
@@ -1574,14 +1817,14 @@ describe("WorkflowRuntime", () => {
|
|
|
1574
1817
|
});
|
|
1575
1818
|
});
|
|
1576
1819
|
|
|
1577
|
-
describe("
|
|
1820
|
+
describe("hasInProgressChildSteps()", () => {
|
|
1578
1821
|
it("returns false when the run step has no direct child rows", async () => {
|
|
1579
1822
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1580
1823
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1581
1824
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1825
|
+
context.getOrCreateRunStep(createRunStepId("leaf"), { parentStepId: null });
|
|
1826
|
+
context.handleRunAttemptStarted(createRunStepId("leaf"));
|
|
1827
|
+
expect(context.hasInProgressChildSteps(createRunStepId("leaf"))).toBe(false);
|
|
1585
1828
|
});
|
|
1586
1829
|
});
|
|
1587
1830
|
|
|
@@ -1589,13 +1832,12 @@ describe("WorkflowRuntime", () => {
|
|
|
1589
1832
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1590
1833
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1591
1834
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
type: "run",
|
|
1835
|
+
context.getOrCreateRunStep(createRunStepId("parent"), { parentStepId: null });
|
|
1836
|
+
context.handleRunAttemptStarted(createRunStepId("parent"));
|
|
1837
|
+
context.getOrCreateRunStep(createRunStepId("child"), {
|
|
1596
1838
|
parentStepId: createRunStepId("parent")
|
|
1597
1839
|
});
|
|
1598
|
-
|
|
1840
|
+
expect(context.hasInProgressChildSteps(createRunStepId("parent"))).toBe(true);
|
|
1599
1841
|
});
|
|
1600
1842
|
});
|
|
1601
1843
|
|
|
@@ -1603,14 +1845,13 @@ describe("WorkflowRuntime", () => {
|
|
|
1603
1845
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1604
1846
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1605
1847
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
type: "run",
|
|
1848
|
+
context.getOrCreateRunStep(createRunStepId("parent"), { parentStepId: null });
|
|
1849
|
+
context.handleRunAttemptStarted(createRunStepId("parent"));
|
|
1850
|
+
context.getOrCreateRunStep(createRunStepId("child"), {
|
|
1610
1851
|
parentStepId: createRunStepId("parent")
|
|
1611
1852
|
});
|
|
1612
|
-
|
|
1613
|
-
|
|
1853
|
+
context.handleRunAttemptStarted(createRunStepId("child"));
|
|
1854
|
+
expect(context.hasInProgressChildSteps(createRunStepId("parent"))).toBe(true);
|
|
1614
1855
|
});
|
|
1615
1856
|
});
|
|
1616
1857
|
|
|
@@ -1618,14 +1859,13 @@ describe("WorkflowRuntime", () => {
|
|
|
1618
1859
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1619
1860
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1620
1861
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
type: "run",
|
|
1862
|
+
context.getOrCreateRunStep(createRunStepId("gp"), { parentStepId: null });
|
|
1863
|
+
context.handleRunAttemptStarted(createRunStepId("gp"));
|
|
1864
|
+
context.getOrCreateRunStep(createRunStepId("mid"), { parentStepId: createRunStepId("gp") });
|
|
1865
|
+
context.getOrCreateRunStep(createRunStepId("leaf"), {
|
|
1626
1866
|
parentStepId: createRunStepId("mid")
|
|
1627
1867
|
});
|
|
1628
|
-
|
|
1868
|
+
expect(context.hasInProgressChildSteps(createRunStepId("gp"))).toBe(true);
|
|
1629
1869
|
});
|
|
1630
1870
|
});
|
|
1631
1871
|
|
|
@@ -1633,14 +1873,13 @@ describe("WorkflowRuntime", () => {
|
|
|
1633
1873
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1634
1874
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1635
1875
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
type: "run",
|
|
1876
|
+
context.getOrCreateRunStep(createRunStepId("mid"), { parentStepId: null });
|
|
1877
|
+
context.handleRunAttemptStarted(createRunStepId("mid"));
|
|
1878
|
+
context.getOrCreateRunStep(createRunStepId("leaf"), {
|
|
1640
1879
|
parentStepId: createRunStepId("mid")
|
|
1641
1880
|
});
|
|
1642
|
-
|
|
1643
|
-
|
|
1881
|
+
expect(context.hasInProgressChildSteps(createRunStepId("mid"))).toBe(true);
|
|
1882
|
+
expect(context.hasInProgressChildSteps(createRunStepId("leaf"))).toBe(false);
|
|
1644
1883
|
});
|
|
1645
1884
|
});
|
|
1646
1885
|
|
|
@@ -1648,20 +1887,15 @@ describe("WorkflowRuntime", () => {
|
|
|
1648
1887
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1649
1888
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1650
1889
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1890
|
+
context.getOrCreateRunStep(createRunStepId("par"), { parentStepId: null });
|
|
1891
|
+
context.handleRunAttemptStarted(createRunStepId("par"));
|
|
1892
|
+
context.getOrCreateRunStep(createRunStepId("bad-child"), {
|
|
1893
|
+
parentStepId: createRunStepId("par"),
|
|
1894
|
+
maxAttempts: 1
|
|
1656
1895
|
});
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
errorMessage: "x",
|
|
1661
|
-
attemptCount: 1,
|
|
1662
|
-
isNonRetryableStepError: true
|
|
1663
|
-
});
|
|
1664
|
-
await expect(context.hasRunningOrWaitingChildSteps(createRunStepId("par"))).resolves.toBe(false);
|
|
1896
|
+
context.handleRunAttemptStarted(createRunStepId("bad-child"));
|
|
1897
|
+
context.handleRunAttemptFailed(createRunStepId("bad-child"), { errorMessage: "x" });
|
|
1898
|
+
expect(context.hasInProgressChildSteps(createRunStepId("par"))).toBe(false);
|
|
1665
1899
|
});
|
|
1666
1900
|
});
|
|
1667
1901
|
|
|
@@ -1669,453 +1903,154 @@ describe("WorkflowRuntime", () => {
|
|
|
1669
1903
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1670
1904
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1671
1905
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
type: "sleep",
|
|
1677
|
-
wakeAt,
|
|
1906
|
+
context.getOrCreateRunStep(createRunStepId("run-parent"), { parentStepId: null });
|
|
1907
|
+
context.handleRunAttemptStarted(createRunStepId("run-parent"));
|
|
1908
|
+
context.getOrCreateSleepStep(createSleepStepId("child-sleep"), {
|
|
1909
|
+
wakeAt: new Date(Date.now() + 60_000),
|
|
1678
1910
|
parentStepId: createRunStepId("run-parent")
|
|
1679
1911
|
});
|
|
1680
|
-
|
|
1912
|
+
expect(context.hasInProgressChildSteps(createRunStepId("run-parent"))).toBe(true);
|
|
1681
1913
|
});
|
|
1682
1914
|
});
|
|
1683
1915
|
});
|
|
1684
1916
|
|
|
1685
|
-
describe("
|
|
1686
|
-
it("
|
|
1917
|
+
describe("handleRunAttemptStarted()", () => {
|
|
1918
|
+
it("inserts a started attempt", async () => {
|
|
1687
1919
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1688
1920
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1689
1921
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
type: "run",
|
|
1697
|
-
parentStepId: null
|
|
1698
|
-
});
|
|
1699
|
-
expect(updatedStep).toMatchObject({
|
|
1700
|
-
state: "running",
|
|
1701
|
-
attemptCount: 1
|
|
1702
|
-
});
|
|
1922
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1923
|
+
const started = context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
1924
|
+
expect(started).toMatchObject({ state: "started", stepId: "step-1" });
|
|
1925
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1926
|
+
expect(step.attempts).toHaveLength(1);
|
|
1927
|
+
expect(step.attempts[0]).toMatchObject({ state: "started" });
|
|
1703
1928
|
});
|
|
1704
1929
|
});
|
|
1705
1930
|
|
|
1706
|
-
it("
|
|
1931
|
+
it("throws when the step does not exist", async () => {
|
|
1707
1932
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1708
|
-
await runInDurableObject(stub, async (
|
|
1933
|
+
await runInDurableObject(stub, async (_instance, state) => {
|
|
1709
1934
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1710
|
-
|
|
1711
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1712
|
-
type: "running",
|
|
1713
|
-
attemptCount: 1
|
|
1714
|
-
});
|
|
1715
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
1716
|
-
{
|
|
1717
|
-
type: "attempt_started",
|
|
1718
|
-
stepId: "step-1",
|
|
1719
|
-
attemptNumber: 1,
|
|
1720
|
-
recordedAt: expect.any(Date)
|
|
1721
|
-
}
|
|
1722
|
-
]);
|
|
1935
|
+
expect(() => context.handleRunAttemptStarted(createRunStepId("nonexistent"))).toThrow(/not found/);
|
|
1723
1936
|
});
|
|
1724
1937
|
});
|
|
1725
1938
|
|
|
1726
|
-
it("throws when
|
|
1939
|
+
it("throws when an attempt is already in progress", async () => {
|
|
1727
1940
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1728
1941
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1729
1942
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
attemptCount: 1
|
|
1734
|
-
})
|
|
1735
|
-
).rejects.toThrow(/not found/);
|
|
1943
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1944
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
1945
|
+
expect(() => context.handleRunAttemptStarted(createRunStepId("step-1"))).toThrow(/already in progress/);
|
|
1736
1946
|
});
|
|
1737
1947
|
});
|
|
1948
|
+
});
|
|
1738
1949
|
|
|
1739
|
-
|
|
1950
|
+
describe("handleRunAttemptSucceeded()", () => {
|
|
1951
|
+
it("marks the in-flight attempt succeeded with a json result", async () => {
|
|
1740
1952
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1741
1953
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1742
1954
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1955
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1956
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
1957
|
+
const done = context.handleRunAttemptSucceeded(createRunStepId("step-1"), JSON.stringify(0));
|
|
1958
|
+
expect(done).toMatchObject({
|
|
1959
|
+
state: "succeeded",
|
|
1960
|
+
resultType: "json",
|
|
1961
|
+
resultJson: JSON.stringify(0)
|
|
1962
|
+
});
|
|
1963
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1964
|
+
expect(step.attempts).toHaveLength(1);
|
|
1965
|
+
expect(step.attempts[0]).toMatchObject({
|
|
1966
|
+
state: "succeeded",
|
|
1967
|
+
resultType: "json",
|
|
1968
|
+
resultJson: JSON.stringify(0)
|
|
1747
1969
|
});
|
|
1748
|
-
await expect(
|
|
1749
|
-
context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1750
|
-
type: "running",
|
|
1751
|
-
attemptCount: 2
|
|
1752
|
-
})
|
|
1753
|
-
).rejects.toThrow(/Expected 'pending' but got running/);
|
|
1754
1970
|
});
|
|
1755
1971
|
});
|
|
1756
1972
|
|
|
1757
|
-
it("
|
|
1973
|
+
it("marks the in-flight attempt succeeded with result_type none", async () => {
|
|
1758
1974
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1759
1975
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1760
1976
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
type: "failed",
|
|
1768
|
-
attemptCount: 1,
|
|
1769
|
-
errorMessage: "backoff"
|
|
1977
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
1978
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
1979
|
+
const done = context.handleRunAttemptSucceeded(createRunStepId("step-1"), null);
|
|
1980
|
+
expect(done).toMatchObject({
|
|
1981
|
+
state: "succeeded",
|
|
1982
|
+
resultType: "none"
|
|
1770
1983
|
});
|
|
1771
|
-
const future = Date.now() + 3600_000;
|
|
1772
|
-
state.storage.sql.exec("UPDATE steps SET next_attempt_at = ? WHERE id = 'step-1'", future);
|
|
1773
|
-
await expect(
|
|
1774
|
-
context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1775
|
-
type: "running",
|
|
1776
|
-
attemptCount: 2
|
|
1777
|
-
})
|
|
1778
|
-
).rejects.toThrow(/next attempt at/);
|
|
1779
1984
|
});
|
|
1780
1985
|
});
|
|
1781
1986
|
|
|
1782
|
-
it("
|
|
1987
|
+
it("throws when the step does not exist", async () => {
|
|
1783
1988
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1784
1989
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1785
1990
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1991
|
+
expect(() => context.handleRunAttemptSucceeded(createRunStepId("nonexistent"), null)).toThrow(/not found/);
|
|
1992
|
+
});
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
it("throws when no attempt is in progress", async () => {
|
|
1996
|
+
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1997
|
+
await runInDurableObject(stub, async (_instance, state) => {
|
|
1998
|
+
const context = new WorkflowRuntimeContext(state.storage);
|
|
1999
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
2000
|
+
expect(() => context.handleRunAttemptSucceeded(createRunStepId("step-1"), null)).toThrow(
|
|
2001
|
+
/No attempt in progress/
|
|
2002
|
+
);
|
|
1793
2003
|
});
|
|
1794
2004
|
});
|
|
1795
2005
|
});
|
|
1796
2006
|
|
|
1797
|
-
describe("
|
|
1798
|
-
it("
|
|
2007
|
+
describe("handleRunAttemptFailed()", () => {
|
|
2008
|
+
it("marks terminal failed when max attempts exhausted", async () => {
|
|
1799
2009
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1800
2010
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1801
2011
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
attemptCount: 1,
|
|
1810
|
-
result: JSON.stringify({ value: 0 })
|
|
1811
|
-
});
|
|
1812
|
-
const updatedStep = await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
1813
|
-
type: "run",
|
|
1814
|
-
parentStepId: null
|
|
1815
|
-
});
|
|
1816
|
-
expect(updatedStep).toMatchObject({
|
|
1817
|
-
state: "succeeded",
|
|
1818
|
-
attemptCount: 1,
|
|
1819
|
-
result: JSON.stringify({ value: 0 }),
|
|
1820
|
-
resolvedAt: expect.any(Date)
|
|
1821
|
-
});
|
|
1822
|
-
});
|
|
1823
|
-
});
|
|
1824
|
-
|
|
1825
|
-
it("writes an 'attempt_succeeded' step event when a run step is succeeded", async () => {
|
|
1826
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1827
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
1828
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
1829
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
1830
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1831
|
-
type: "running",
|
|
1832
|
-
attemptCount: 1
|
|
1833
|
-
});
|
|
1834
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1835
|
-
type: "succeeded",
|
|
1836
|
-
attemptCount: 1,
|
|
1837
|
-
result: JSON.stringify({ value: 0 })
|
|
1838
|
-
});
|
|
1839
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
1840
|
-
{
|
|
1841
|
-
type: "attempt_started",
|
|
1842
|
-
stepId: "step-1",
|
|
1843
|
-
attemptNumber: 1,
|
|
1844
|
-
recordedAt: expect.any(Date)
|
|
1845
|
-
},
|
|
1846
|
-
{
|
|
1847
|
-
type: "attempt_succeeded",
|
|
1848
|
-
stepId: "step-1",
|
|
1849
|
-
attemptNumber: 1,
|
|
1850
|
-
recordedAt: expect.any(Date)
|
|
1851
|
-
}
|
|
1852
|
-
]);
|
|
1853
|
-
});
|
|
1854
|
-
});
|
|
1855
|
-
|
|
1856
|
-
it("throws when the attempt number does not match", async () => {
|
|
1857
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1858
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
1859
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
1860
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
1861
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1862
|
-
type: "running",
|
|
1863
|
-
attemptCount: 1
|
|
1864
|
-
});
|
|
1865
|
-
await expect(
|
|
1866
|
-
context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1867
|
-
type: "succeeded",
|
|
1868
|
-
attemptCount: 999,
|
|
1869
|
-
result: JSON.stringify({ value: 0 })
|
|
1870
|
-
})
|
|
1871
|
-
).rejects.toThrow(/Unexpected attempt count/);
|
|
1872
|
-
});
|
|
1873
|
-
});
|
|
1874
|
-
|
|
1875
|
-
it("throws when the step does not exist", async () => {
|
|
1876
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1877
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
1878
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
1879
|
-
await expect(
|
|
1880
|
-
context.handleRunAttemptEvent(createRunStepId("nonexistent"), {
|
|
1881
|
-
type: "succeeded",
|
|
1882
|
-
attemptCount: 1,
|
|
1883
|
-
result: JSON.stringify({ value: 0 })
|
|
1884
|
-
})
|
|
1885
|
-
).rejects.toThrow(/not found/);
|
|
1886
|
-
});
|
|
1887
|
-
});
|
|
1888
|
-
|
|
1889
|
-
it("rejects when the step is still in 'pending' state", async () => {
|
|
1890
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1891
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
1892
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
1893
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
1894
|
-
await expect(
|
|
1895
|
-
context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1896
|
-
type: "succeeded",
|
|
1897
|
-
attemptCount: 1,
|
|
1898
|
-
result: JSON.stringify({ value: 1 })
|
|
1899
|
-
})
|
|
1900
|
-
).rejects.toThrow(/Expected 'running' but got pending/);
|
|
1901
|
-
});
|
|
1902
|
-
});
|
|
1903
|
-
});
|
|
1904
|
-
|
|
1905
|
-
describe("handleRunAttemptEvent({ type: 'failed' })", () => {
|
|
1906
|
-
it("moves a run step from 'running' to 'failed'", async () => {
|
|
1907
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1908
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
1909
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
1910
|
-
await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
1911
|
-
type: "run",
|
|
1912
|
-
maxAttempts: 1,
|
|
1913
|
-
parentStepId: null
|
|
1914
|
-
});
|
|
1915
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1916
|
-
type: "running",
|
|
1917
|
-
attemptCount: 1
|
|
1918
|
-
});
|
|
1919
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1920
|
-
type: "failed",
|
|
1921
|
-
attemptCount: 1,
|
|
1922
|
-
errorMessage: "error"
|
|
1923
|
-
});
|
|
1924
|
-
const updatedStep = await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
1925
|
-
type: "run",
|
|
1926
|
-
maxAttempts: 1,
|
|
1927
|
-
parentStepId: null
|
|
1928
|
-
});
|
|
1929
|
-
expect(updatedStep).toMatchObject({
|
|
1930
|
-
state: "failed",
|
|
1931
|
-
attemptCount: 1,
|
|
1932
|
-
errorMessage: "error"
|
|
2012
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { maxAttempts: 1, parentStepId: null });
|
|
2013
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
2014
|
+
const failed = context.handleRunAttemptFailed(createRunStepId("step-1"), { errorMessage: "error" });
|
|
2015
|
+
expect(failed).toMatchObject({
|
|
2016
|
+
state: "failed",
|
|
2017
|
+
errorMessage: "error",
|
|
2018
|
+
nextAttemptAt: undefined
|
|
1933
2019
|
});
|
|
2020
|
+
const step = context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
2021
|
+
expect(step.attempts).toHaveLength(1);
|
|
2022
|
+
expect(step.attempts[0]).toMatchObject({ state: "failed", errorMessage: "error" });
|
|
1934
2023
|
});
|
|
1935
2024
|
});
|
|
1936
2025
|
|
|
1937
|
-
it("
|
|
1938
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1939
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
1940
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
1941
|
-
await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
1942
|
-
type: "run",
|
|
1943
|
-
maxAttempts: 1,
|
|
1944
|
-
parentStepId: null
|
|
1945
|
-
});
|
|
1946
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1947
|
-
type: "running",
|
|
1948
|
-
attemptCount: 1
|
|
1949
|
-
});
|
|
1950
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
1951
|
-
type: "failed",
|
|
1952
|
-
attemptCount: 1,
|
|
1953
|
-
errorMessage: "error"
|
|
1954
|
-
});
|
|
1955
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
1956
|
-
{
|
|
1957
|
-
type: "attempt_started",
|
|
1958
|
-
stepId: "step-1",
|
|
1959
|
-
attemptNumber: 1,
|
|
1960
|
-
recordedAt: expect.any(Date)
|
|
1961
|
-
},
|
|
1962
|
-
{
|
|
1963
|
-
type: "attempt_failed",
|
|
1964
|
-
stepId: "step-1",
|
|
1965
|
-
attemptNumber: 1,
|
|
1966
|
-
errorMessage: "error",
|
|
1967
|
-
recordedAt: expect.any(Date)
|
|
1968
|
-
}
|
|
1969
|
-
]);
|
|
1970
|
-
});
|
|
1971
|
-
});
|
|
1972
|
-
|
|
1973
|
-
it("moves a run step back to 'pending' when retries are available", async () => {
|
|
2026
|
+
it("records next_attempt_at when retries remain", async () => {
|
|
1974
2027
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
1975
2028
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
1976
2029
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
const updatedStep = await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
1988
|
-
type: "run",
|
|
1989
|
-
parentStepId: null
|
|
1990
|
-
});
|
|
1991
|
-
expect(updatedStep).toMatchObject({
|
|
1992
|
-
state: "pending",
|
|
1993
|
-
attemptCount: 1
|
|
1994
|
-
});
|
|
1995
|
-
expect(updatedStep).toHaveProperty("nextAttemptAt");
|
|
1996
|
-
expect((updatedStep as { nextAttemptAt: Date }).nextAttemptAt.getTime()).toBeGreaterThan(Date.now());
|
|
2030
|
+
const before = Date.now();
|
|
2031
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
2032
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
2033
|
+
const failed = context.handleRunAttemptFailed(createRunStepId("step-1"), { errorMessage: "transient" });
|
|
2034
|
+
const after = Date.now();
|
|
2035
|
+
expect(failed.state).toBe("failed");
|
|
2036
|
+
if (failed.state !== "failed") throw new Error("expected failed");
|
|
2037
|
+
expect(failed.nextAttemptAt).toBeDefined();
|
|
2038
|
+
expect(failed.nextAttemptAt!.getTime()).toBeGreaterThanOrEqual(before + 250);
|
|
2039
|
+
expect(failed.nextAttemptAt!.getTime()).toBeLessThanOrEqual(after + 500 + 100);
|
|
1997
2040
|
});
|
|
1998
2041
|
});
|
|
1999
2042
|
|
|
2000
|
-
it("
|
|
2043
|
+
it("marks terminal failed when isNonRetryableStepError is true", async () => {
|
|
2001
2044
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2002
2045
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2003
2046
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
});
|
|
2009
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2010
|
-
type: "running",
|
|
2011
|
-
attemptCount: 1
|
|
2012
|
-
});
|
|
2013
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2014
|
-
type: "failed",
|
|
2015
|
-
attemptCount: 1,
|
|
2016
|
-
errorMessage: "transient error",
|
|
2047
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { maxAttempts: 10, parentStepId: null });
|
|
2048
|
+
context.handleRunAttemptStarted(createRunStepId("step-1"));
|
|
2049
|
+
const failed = context.handleRunAttemptFailed(createRunStepId("step-1"), {
|
|
2050
|
+
errorMessage: "x",
|
|
2017
2051
|
isNonRetryableStepError: true
|
|
2018
2052
|
});
|
|
2019
|
-
|
|
2020
|
-
type: "run",
|
|
2021
|
-
parentStepId: null
|
|
2022
|
-
});
|
|
2023
|
-
expect(updatedStep).toMatchObject({
|
|
2024
|
-
state: "failed",
|
|
2025
|
-
attemptCount: 1,
|
|
2026
|
-
errorMessage: "transient error"
|
|
2027
|
-
});
|
|
2028
|
-
});
|
|
2029
|
-
});
|
|
2030
|
-
|
|
2031
|
-
it("schedules an alarm when retries are available", async () => {
|
|
2032
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2033
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2034
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2035
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
2036
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2037
|
-
type: "running",
|
|
2038
|
-
attemptCount: 1
|
|
2039
|
-
});
|
|
2040
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2041
|
-
type: "failed",
|
|
2042
|
-
attemptCount: 1,
|
|
2043
|
-
errorMessage: "transient error"
|
|
2044
|
-
});
|
|
2045
|
-
const alarm = await state.storage.getAlarm();
|
|
2046
|
-
expect(alarm).not.toBeNull();
|
|
2047
|
-
expect(alarm).toBeGreaterThan(Date.now());
|
|
2048
|
-
});
|
|
2049
|
-
});
|
|
2050
|
-
|
|
2051
|
-
it("replaces an existing alarm when retries are available", async () => {
|
|
2052
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2053
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2054
|
-
await state.storage.setAlarm(Date.now() + 999_999);
|
|
2055
|
-
|
|
2056
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2057
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
2058
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2059
|
-
type: "running",
|
|
2060
|
-
attemptCount: 1
|
|
2061
|
-
});
|
|
2062
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2063
|
-
type: "failed",
|
|
2064
|
-
attemptCount: 1,
|
|
2065
|
-
errorMessage: "transient error"
|
|
2066
|
-
});
|
|
2067
|
-
const alarm = await state.storage.getAlarm();
|
|
2068
|
-
expect(alarm).not.toBeNull();
|
|
2069
|
-
expect(alarm).toBeLessThan(Date.now() + 999_999);
|
|
2070
|
-
});
|
|
2071
|
-
});
|
|
2072
|
-
|
|
2073
|
-
it("writes an 'attempt_failed' step event when retrying", async () => {
|
|
2074
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2075
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2076
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2077
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
2078
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2079
|
-
type: "running",
|
|
2080
|
-
attemptCount: 1
|
|
2081
|
-
});
|
|
2082
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2083
|
-
type: "failed",
|
|
2084
|
-
attemptCount: 1,
|
|
2085
|
-
errorMessage: "transient error"
|
|
2086
|
-
});
|
|
2087
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2088
|
-
{
|
|
2089
|
-
type: "attempt_started",
|
|
2090
|
-
stepId: "step-1",
|
|
2091
|
-
attemptNumber: 1
|
|
2092
|
-
},
|
|
2093
|
-
{
|
|
2094
|
-
type: "attempt_failed",
|
|
2095
|
-
stepId: "step-1",
|
|
2096
|
-
attemptNumber: 1,
|
|
2097
|
-
errorMessage: "transient error"
|
|
2098
|
-
}
|
|
2099
|
-
]);
|
|
2100
|
-
});
|
|
2101
|
-
});
|
|
2102
|
-
|
|
2103
|
-
it("throws when the attempt number does not match", async () => {
|
|
2104
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2105
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2106
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2107
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
2108
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2109
|
-
type: "running",
|
|
2110
|
-
attemptCount: 1
|
|
2111
|
-
});
|
|
2112
|
-
await expect(
|
|
2113
|
-
context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2114
|
-
type: "failed",
|
|
2115
|
-
attemptCount: 999,
|
|
2116
|
-
errorMessage: "error"
|
|
2117
|
-
})
|
|
2118
|
-
).rejects.toThrow(/Unexpected attempt count/);
|
|
2053
|
+
expect(failed).toMatchObject({ state: "failed", nextAttemptAt: undefined });
|
|
2119
2054
|
});
|
|
2120
2055
|
});
|
|
2121
2056
|
|
|
@@ -2123,80 +2058,41 @@ describe("WorkflowRuntime", () => {
|
|
|
2123
2058
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2124
2059
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2125
2060
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
attemptCount: 1,
|
|
2130
|
-
errorMessage: "error"
|
|
2131
|
-
})
|
|
2132
|
-
).rejects.toThrow(/not found/);
|
|
2133
|
-
});
|
|
2134
|
-
});
|
|
2135
|
-
|
|
2136
|
-
it("uses backoff delay for next attempt when retrying", async () => {
|
|
2137
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2138
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2139
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2140
|
-
const before = Date.now();
|
|
2141
|
-
await context.getOrCreateStep(createRunStepId("step-1"), { type: "run", parentStepId: null });
|
|
2142
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2143
|
-
type: "running",
|
|
2144
|
-
attemptCount: 1
|
|
2145
|
-
});
|
|
2146
|
-
await context.handleRunAttemptEvent(createRunStepId("step-1"), {
|
|
2147
|
-
type: "failed",
|
|
2148
|
-
attemptCount: 1,
|
|
2149
|
-
errorMessage: "transient error"
|
|
2150
|
-
});
|
|
2151
|
-
const after = Date.now();
|
|
2152
|
-
const updatedStep = await context.getOrCreateStep(createRunStepId("step-1"), {
|
|
2153
|
-
type: "run",
|
|
2154
|
-
parentStepId: null
|
|
2155
|
-
});
|
|
2156
|
-
expect(updatedStep).toMatchObject({
|
|
2157
|
-
state: "pending",
|
|
2158
|
-
attemptCount: 1
|
|
2159
|
-
});
|
|
2160
|
-
const nextAttemptAt = (updatedStep as { nextAttemptAt: Date }).nextAttemptAt.getTime();
|
|
2161
|
-
expect(nextAttemptAt).toBeGreaterThanOrEqual(before + 250);
|
|
2162
|
-
expect(nextAttemptAt).toBeLessThanOrEqual(after + 500 + 100);
|
|
2061
|
+
expect(() => context.handleRunAttemptFailed(createRunStepId("nonexistent"), { errorMessage: "e" })).toThrow(
|
|
2062
|
+
/not found/
|
|
2063
|
+
);
|
|
2163
2064
|
});
|
|
2164
2065
|
});
|
|
2165
2066
|
|
|
2166
|
-
it("
|
|
2067
|
+
it("throws when no attempt is in progress", async () => {
|
|
2167
2068
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2168
2069
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2169
2070
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
attemptCount: 1,
|
|
2175
|
-
errorMessage: "bad"
|
|
2176
|
-
})
|
|
2177
|
-
).rejects.toThrow(/Expected 'running' but got pending/);
|
|
2071
|
+
context.getOrCreateRunStep(createRunStepId("step-1"), { parentStepId: null });
|
|
2072
|
+
expect(() => context.handleRunAttemptFailed(createRunStepId("step-1"), { errorMessage: "bad" })).toThrow(
|
|
2073
|
+
/No attempt in progress/
|
|
2074
|
+
);
|
|
2178
2075
|
});
|
|
2179
2076
|
});
|
|
2180
2077
|
});
|
|
2181
2078
|
});
|
|
2182
2079
|
|
|
2183
2080
|
describe("sleep steps", () => {
|
|
2184
|
-
describe("
|
|
2081
|
+
describe("getOrCreateSleepStep()", () => {
|
|
2185
2082
|
it("creates a sleep step", async () => {
|
|
2186
2083
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2187
2084
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2188
2085
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2189
2086
|
const wakeAt = new Date(Date.now() + 60_000);
|
|
2190
|
-
const step =
|
|
2191
|
-
|
|
2192
|
-
wakeAt: wakeAt,
|
|
2087
|
+
const step = context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2088
|
+
wakeAt,
|
|
2193
2089
|
parentStepId: null
|
|
2194
2090
|
});
|
|
2195
2091
|
expect(step).toMatchObject({
|
|
2196
2092
|
id: "sleep-1",
|
|
2197
2093
|
type: "sleep",
|
|
2198
2094
|
state: "waiting",
|
|
2199
|
-
wakeAt
|
|
2095
|
+
wakeAt
|
|
2200
2096
|
});
|
|
2201
2097
|
});
|
|
2202
2098
|
});
|
|
@@ -2205,13 +2101,9 @@ describe("WorkflowRuntime", () => {
|
|
|
2205
2101
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2206
2102
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2207
2103
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
parentStepId: null
|
|
2212
|
-
});
|
|
2213
|
-
const second = await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2214
|
-
type: "sleep",
|
|
2104
|
+
const w = new Date();
|
|
2105
|
+
const first = context.getOrCreateSleepStep(createSleepStepId("sleep-1"), { wakeAt: w, parentStepId: null });
|
|
2106
|
+
const second = context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2215
2107
|
wakeAt: new Date(),
|
|
2216
2108
|
parentStepId: null
|
|
2217
2109
|
});
|
|
@@ -2219,178 +2111,75 @@ describe("WorkflowRuntime", () => {
|
|
|
2219
2111
|
});
|
|
2220
2112
|
});
|
|
2221
2113
|
|
|
2222
|
-
it("
|
|
2223
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2224
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2225
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2226
|
-
const wakeAt = new Date();
|
|
2227
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2228
|
-
type: "sleep",
|
|
2229
|
-
wakeAt: wakeAt,
|
|
2230
|
-
parentStepId: null
|
|
2231
|
-
});
|
|
2232
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2233
|
-
{
|
|
2234
|
-
type: "sleep_waiting",
|
|
2235
|
-
stepId: "sleep-1",
|
|
2236
|
-
wakeAt: wakeAt,
|
|
2237
|
-
recordedAt: expect.any(Date)
|
|
2238
|
-
}
|
|
2239
|
-
]);
|
|
2240
|
-
});
|
|
2241
|
-
});
|
|
2242
|
-
|
|
2243
|
-
it("schedules an alarm", async () => {
|
|
2114
|
+
it("does not set a durable object alarm by itself", async () => {
|
|
2244
2115
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2245
2116
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2246
2117
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2247
2118
|
const wakeAt = new Date(Date.now() + 60_000);
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
wakeAt: wakeAt,
|
|
2251
|
-
parentStepId: null
|
|
2252
|
-
});
|
|
2253
|
-
expect(await state.storage.getAlarm()).toBe(wakeAt.getTime());
|
|
2119
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), { wakeAt, parentStepId: null });
|
|
2120
|
+
expect(await state.storage.getAlarm()).toBeNull();
|
|
2254
2121
|
});
|
|
2255
2122
|
});
|
|
2256
2123
|
|
|
2257
|
-
it("
|
|
2124
|
+
it("leaves an existing alarm unchanged when creating a sleep step", async () => {
|
|
2258
2125
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2259
2126
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2260
|
-
|
|
2261
|
-
|
|
2127
|
+
const prior = Date.now() + 999_999;
|
|
2128
|
+
await state.storage.setAlarm(prior);
|
|
2262
2129
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
type: "sleep",
|
|
2266
|
-
wakeAt: wakeAt,
|
|
2130
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2131
|
+
wakeAt: new Date(Date.now() + 60_000),
|
|
2267
2132
|
parentStepId: null
|
|
2268
2133
|
});
|
|
2269
|
-
expect(await state.storage.getAlarm()).toBe(
|
|
2134
|
+
expect(await state.storage.getAlarm()).toBe(prior);
|
|
2270
2135
|
});
|
|
2271
2136
|
});
|
|
2272
2137
|
|
|
2273
|
-
it("does not
|
|
2138
|
+
it("does not set an alarm when re-reading an existing sleep step after deleteAlarm", async () => {
|
|
2274
2139
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2275
2140
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2276
2141
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2277
2142
|
const pastWakeAt = new Date(Date.now() - 10_000);
|
|
2278
|
-
|
|
2279
|
-
type: "sleep",
|
|
2143
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2280
2144
|
wakeAt: pastWakeAt,
|
|
2281
2145
|
parentStepId: null
|
|
2282
2146
|
});
|
|
2283
2147
|
await state.storage.deleteAlarm();
|
|
2284
|
-
|
|
2285
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2286
|
-
type: "sleep",
|
|
2148
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2287
2149
|
wakeAt: pastWakeAt,
|
|
2288
2150
|
parentStepId: null
|
|
2289
2151
|
});
|
|
2290
2152
|
expect(await state.storage.getAlarm()).toBeNull();
|
|
2291
2153
|
});
|
|
2292
2154
|
});
|
|
2293
|
-
|
|
2294
|
-
it("does not append a second sleep_waiting step_events row when the sleep step already exists", async () => {
|
|
2295
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2296
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2297
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2298
|
-
const wakeAt = new Date(Date.now() + 30_000);
|
|
2299
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), { type: "sleep", wakeAt, parentStepId: null });
|
|
2300
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), { type: "sleep", wakeAt, parentStepId: null });
|
|
2301
|
-
const events = await instance.getStepEvents_experimental();
|
|
2302
|
-
expect(events).toHaveLength(1);
|
|
2303
|
-
expect(events[0]).toMatchObject({
|
|
2304
|
-
type: "sleep_waiting",
|
|
2305
|
-
stepId: "sleep-1",
|
|
2306
|
-
wakeAt: wakeAt,
|
|
2307
|
-
recordedAt: expect.any(Date)
|
|
2308
|
-
});
|
|
2309
|
-
});
|
|
2310
|
-
});
|
|
2311
|
-
|
|
2312
|
-
it("does not set an alarm when re-reading an already elapsed sleep step", async () => {
|
|
2313
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2314
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2315
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2316
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2317
|
-
type: "sleep",
|
|
2318
|
-
wakeAt: new Date(Date.now() + 60_000),
|
|
2319
|
-
parentStepId: null
|
|
2320
|
-
});
|
|
2321
|
-
await state.storage.deleteAlarm();
|
|
2322
|
-
const now = Date.now();
|
|
2323
|
-
state.storage.sql.exec(
|
|
2324
|
-
"UPDATE steps SET state = 'elapsed', wake_at = NULL, resolved_at = ? WHERE id = 'sleep-1'",
|
|
2325
|
-
now
|
|
2326
|
-
);
|
|
2327
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2328
|
-
type: "sleep",
|
|
2329
|
-
wakeAt: new Date(),
|
|
2330
|
-
parentStepId: null
|
|
2331
|
-
});
|
|
2332
|
-
expect(await state.storage.getAlarm()).toBeNull();
|
|
2333
|
-
});
|
|
2334
|
-
});
|
|
2335
2155
|
});
|
|
2336
2156
|
|
|
2337
|
-
describe("
|
|
2157
|
+
describe("handleSleepStepElapsed()", () => {
|
|
2338
2158
|
it("moves a sleep step from 'waiting' to 'elapsed'", async () => {
|
|
2339
2159
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2340
2160
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2341
2161
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2342
|
-
|
|
2343
|
-
type: "sleep",
|
|
2162
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2344
2163
|
wakeAt: new Date(),
|
|
2345
2164
|
parentStepId: null
|
|
2346
2165
|
});
|
|
2347
|
-
context.
|
|
2348
|
-
const
|
|
2349
|
-
type: "sleep",
|
|
2166
|
+
context.handleSleepStepElapsed(createSleepStepId("sleep-1"));
|
|
2167
|
+
const step = context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2350
2168
|
wakeAt: new Date(),
|
|
2351
2169
|
parentStepId: null
|
|
2352
2170
|
});
|
|
2353
|
-
expect(
|
|
2171
|
+
expect(step).toMatchObject({
|
|
2354
2172
|
state: "elapsed",
|
|
2355
2173
|
resolvedAt: expect.any(Date)
|
|
2356
2174
|
});
|
|
2357
2175
|
});
|
|
2358
2176
|
});
|
|
2359
2177
|
|
|
2360
|
-
it("writes a 'sleep_elapsed' step event when a sleep step is elapsed", async () => {
|
|
2361
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2362
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2363
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2364
|
-
const wakeAt = new Date(Date.now() + 60_000);
|
|
2365
|
-
await context.getOrCreateStep(createSleepStepId("sleep-1"), {
|
|
2366
|
-
type: "sleep",
|
|
2367
|
-
wakeAt: wakeAt,
|
|
2368
|
-
parentStepId: null
|
|
2369
|
-
});
|
|
2370
|
-
context.handleSleepStepEvent(createSleepStepId("sleep-1"), { type: "elapsed" });
|
|
2371
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2372
|
-
{
|
|
2373
|
-
type: "sleep_waiting",
|
|
2374
|
-
stepId: "sleep-1",
|
|
2375
|
-
wakeAt: wakeAt,
|
|
2376
|
-
recordedAt: expect.any(Date)
|
|
2377
|
-
},
|
|
2378
|
-
{
|
|
2379
|
-
type: "sleep_elapsed",
|
|
2380
|
-
stepId: "sleep-1",
|
|
2381
|
-
recordedAt: expect.any(Date)
|
|
2382
|
-
}
|
|
2383
|
-
]);
|
|
2384
|
-
});
|
|
2385
|
-
});
|
|
2386
|
-
|
|
2387
2178
|
it("throws when the step does not exist", async () => {
|
|
2388
2179
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2389
2180
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2390
2181
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2391
|
-
expect(() => context.
|
|
2392
|
-
/not found/
|
|
2393
|
-
);
|
|
2182
|
+
expect(() => context.handleSleepStepElapsed(createSleepStepId("nonexistent"))).toThrow(/not found/);
|
|
2394
2183
|
});
|
|
2395
2184
|
});
|
|
2396
2185
|
|
|
@@ -2398,13 +2187,12 @@ describe("WorkflowRuntime", () => {
|
|
|
2398
2187
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2399
2188
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2400
2189
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
wakeAt: new Date(),
|
|
2190
|
+
context.getOrCreateSleepStep(createSleepStepId("sleep-1"), {
|
|
2191
|
+
wakeAt: new Date(Date.now() + 60_000),
|
|
2404
2192
|
parentStepId: null
|
|
2405
2193
|
});
|
|
2406
|
-
context.
|
|
2407
|
-
expect(() => context.
|
|
2194
|
+
context.handleSleepStepElapsed(createSleepStepId("sleep-1"));
|
|
2195
|
+
expect(() => context.handleSleepStepElapsed(createSleepStepId("sleep-1"))).toThrow(
|
|
2408
2196
|
/Expected 'waiting' but got elapsed/
|
|
2409
2197
|
);
|
|
2410
2198
|
});
|
|
@@ -2413,13 +2201,12 @@ describe("WorkflowRuntime", () => {
|
|
|
2413
2201
|
});
|
|
2414
2202
|
|
|
2415
2203
|
describe("wait steps", () => {
|
|
2416
|
-
describe("
|
|
2204
|
+
describe("getOrCreateWaitStep()", () => {
|
|
2417
2205
|
it("creates a wait step when no timeout is provided", async () => {
|
|
2418
2206
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2419
2207
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2420
2208
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2421
|
-
const step =
|
|
2422
|
-
type: "wait",
|
|
2209
|
+
const step = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2423
2210
|
eventName: "event-1",
|
|
2424
2211
|
parentStepId: null
|
|
2425
2212
|
});
|
|
@@ -2437,371 +2224,131 @@ describe("WorkflowRuntime", () => {
|
|
|
2437
2224
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2438
2225
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2439
2226
|
const timeoutAt = new Date(Date.now() + 60_000);
|
|
2440
|
-
const step =
|
|
2441
|
-
type: "wait",
|
|
2227
|
+
const step = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2442
2228
|
eventName: "event-1",
|
|
2443
2229
|
parentStepId: null,
|
|
2444
|
-
timeoutAt
|
|
2230
|
+
timeoutAt
|
|
2445
2231
|
});
|
|
2446
2232
|
expect(step).toMatchObject({
|
|
2447
2233
|
id: "wait-1",
|
|
2448
2234
|
type: "wait",
|
|
2449
2235
|
state: "waiting",
|
|
2450
2236
|
eventName: "event-1",
|
|
2451
|
-
timeoutAt
|
|
2237
|
+
timeoutAt
|
|
2452
2238
|
});
|
|
2453
2239
|
});
|
|
2454
2240
|
});
|
|
2455
2241
|
|
|
2456
|
-
it("writes a 'wait_waiting' step event", async () => {
|
|
2457
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2458
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2459
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2460
|
-
const timeoutAt = new Date(Date.now() + 60_000);
|
|
2461
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2462
|
-
type: "wait",
|
|
2463
|
-
eventName: "event-1",
|
|
2464
|
-
parentStepId: null,
|
|
2465
|
-
timeoutAt: timeoutAt
|
|
2466
|
-
});
|
|
2467
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2468
|
-
{
|
|
2469
|
-
type: "wait_waiting",
|
|
2470
|
-
stepId: "wait-1",
|
|
2471
|
-
eventName: "event-1",
|
|
2472
|
-
timeoutAt: timeoutAt,
|
|
2473
|
-
recordedAt: expect.any(Date)
|
|
2474
|
-
}
|
|
2475
|
-
]);
|
|
2476
|
-
});
|
|
2477
|
-
});
|
|
2478
|
-
|
|
2479
2242
|
it("creates a wait step once and returns the same durable row on subsequent reads", async () => {
|
|
2480
2243
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2481
2244
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2482
2245
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2483
|
-
const first =
|
|
2484
|
-
type: "wait",
|
|
2246
|
+
const first = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2485
2247
|
eventName: "event-1",
|
|
2486
|
-
parentStepId: null
|
|
2487
|
-
timeoutAt: new Date(Date.now() + 60_000)
|
|
2248
|
+
parentStepId: null
|
|
2488
2249
|
});
|
|
2489
|
-
const second =
|
|
2490
|
-
type: "wait",
|
|
2250
|
+
const second = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2491
2251
|
eventName: "event-1",
|
|
2492
|
-
parentStepId: null
|
|
2493
|
-
timeoutAt: new Date(Date.now() + 60_000)
|
|
2252
|
+
parentStepId: null
|
|
2494
2253
|
});
|
|
2495
2254
|
expect(first).toEqual(second);
|
|
2496
2255
|
});
|
|
2497
2256
|
});
|
|
2498
2257
|
|
|
2499
|
-
it("
|
|
2500
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2501
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2502
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2503
|
-
const timeoutAt = new Date(Date.now() + 60_000);
|
|
2504
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2505
|
-
type: "wait",
|
|
2506
|
-
eventName: "event-1",
|
|
2507
|
-
parentStepId: null,
|
|
2508
|
-
timeoutAt: timeoutAt
|
|
2509
|
-
});
|
|
2510
|
-
expect(await state.storage.getAlarm()).toBe(timeoutAt.getTime());
|
|
2511
|
-
});
|
|
2512
|
-
});
|
|
2513
|
-
|
|
2514
|
-
it("replaces any existing alarm with a new alarm when a timeout is provided", async () => {
|
|
2258
|
+
it("does not set a durable object alarm by itself", async () => {
|
|
2515
2259
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2516
2260
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2517
|
-
await state.storage.setAlarm(Date.now() + 999_999);
|
|
2518
|
-
|
|
2519
2261
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2520
|
-
|
|
2521
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2522
|
-
type: "wait",
|
|
2262
|
+
context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2523
2263
|
eventName: "event-1",
|
|
2524
2264
|
parentStepId: null,
|
|
2525
|
-
timeoutAt:
|
|
2526
|
-
});
|
|
2527
|
-
expect(await state.storage.getAlarm()).toBe(timeoutAt.getTime());
|
|
2528
|
-
});
|
|
2529
|
-
});
|
|
2530
|
-
|
|
2531
|
-
it("doesn't schedule an alarm when no timeout is provided", async () => {
|
|
2532
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2533
|
-
await runInDurableObject(stub, async (_instance, state) => {
|
|
2534
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2535
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2536
|
-
type: "wait",
|
|
2537
|
-
eventName: "event-1",
|
|
2538
|
-
parentStepId: null
|
|
2265
|
+
timeoutAt: new Date(Date.now() + 60_000)
|
|
2539
2266
|
});
|
|
2540
2267
|
expect(await state.storage.getAlarm()).toBeNull();
|
|
2541
2268
|
});
|
|
2542
2269
|
});
|
|
2543
2270
|
|
|
2544
|
-
it("
|
|
2271
|
+
it("leaves an existing alarm unchanged when creating a wait step with timeout", async () => {
|
|
2545
2272
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2546
2273
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2274
|
+
const prior = Date.now() + 999_999;
|
|
2275
|
+
await state.storage.setAlarm(prior);
|
|
2547
2276
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2548
|
-
|
|
2549
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2550
|
-
type: "wait",
|
|
2277
|
+
context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2551
2278
|
eventName: "event-1",
|
|
2552
2279
|
parentStepId: null,
|
|
2553
|
-
timeoutAt:
|
|
2554
|
-
});
|
|
2555
|
-
await state.storage.deleteAlarm();
|
|
2556
|
-
|
|
2557
|
-
const shiftedTimeout = new Date(Date.now() + 120_000);
|
|
2558
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2559
|
-
type: "wait",
|
|
2560
|
-
eventName: "event-1",
|
|
2561
|
-
parentStepId: null,
|
|
2562
|
-
timeoutAt: shiftedTimeout
|
|
2280
|
+
timeoutAt: new Date(Date.now() + 60_000)
|
|
2563
2281
|
});
|
|
2564
|
-
expect(await state.storage.getAlarm()).toBe(
|
|
2282
|
+
expect(await state.storage.getAlarm()).toBe(prior);
|
|
2565
2283
|
});
|
|
2566
2284
|
});
|
|
2567
2285
|
|
|
2568
|
-
it("
|
|
2286
|
+
it("satisfies from a queued inbound event when creating the wait step", async () => {
|
|
2569
2287
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2570
2288
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2571
2289
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
});
|
|
2579
|
-
await state.storage.deleteAlarm();
|
|
2580
|
-
|
|
2581
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2582
|
-
type: "wait",
|
|
2583
|
-
eventName: "event-1",
|
|
2584
|
-
parentStepId: null,
|
|
2585
|
-
timeoutAt: pastTimeout
|
|
2586
|
-
});
|
|
2587
|
-
expect(await state.storage.getAlarm()).toBeNull();
|
|
2588
|
-
});
|
|
2589
|
-
});
|
|
2590
|
-
});
|
|
2591
|
-
|
|
2592
|
-
describe("handleInboundEvent()", () => {
|
|
2593
|
-
it("moves a wait step from 'waiting' to 'satisfied' when event is delivered", async () => {
|
|
2594
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2595
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2596
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2597
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2598
|
-
type: "wait",
|
|
2599
|
-
eventName: "event-1",
|
|
2600
|
-
parentStepId: null
|
|
2601
|
-
});
|
|
2602
|
-
await instance.handleInboundEvent("event-1", "payload");
|
|
2603
|
-
const updatedStep = await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2604
|
-
type: "wait",
|
|
2605
|
-
eventName: "event-1",
|
|
2606
|
-
parentStepId: null
|
|
2607
|
-
});
|
|
2608
|
-
expect(updatedStep).toMatchObject({
|
|
2609
|
-
state: "satisfied",
|
|
2610
|
-
payload: JSON.stringify("payload"),
|
|
2611
|
-
resolvedAt: expect.any(Date)
|
|
2612
|
-
});
|
|
2613
|
-
});
|
|
2614
|
-
});
|
|
2615
|
-
|
|
2616
|
-
it("writes a 'wait_satisfied' step event when a wait step is satisfied", async () => {
|
|
2617
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2618
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2619
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2620
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2621
|
-
type: "wait",
|
|
2622
|
-
eventName: "event-1",
|
|
2623
|
-
parentStepId: null
|
|
2624
|
-
});
|
|
2625
|
-
await instance.handleInboundEvent("event-1", "payload");
|
|
2626
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2627
|
-
{
|
|
2628
|
-
type: "wait_waiting",
|
|
2629
|
-
stepId: "wait-1",
|
|
2630
|
-
eventName: "event-1",
|
|
2631
|
-
recordedAt: expect.any(Date)
|
|
2632
|
-
},
|
|
2633
|
-
{
|
|
2634
|
-
type: "wait_satisfied",
|
|
2635
|
-
stepId: "wait-1",
|
|
2636
|
-
payload: JSON.stringify("payload"),
|
|
2637
|
-
recordedAt: expect.any(Date)
|
|
2638
|
-
}
|
|
2639
|
-
]);
|
|
2640
|
-
});
|
|
2641
|
-
});
|
|
2642
|
-
|
|
2643
|
-
it("queues the event when no matching wait step exists", async () => {
|
|
2644
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2645
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2646
|
-
await instance.handleInboundEvent("event-1", "queued-payload");
|
|
2647
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2648
|
-
const step = await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2649
|
-
type: "wait",
|
|
2290
|
+
state.storage.sql.exec(
|
|
2291
|
+
`INSERT INTO inbound_events (event_name, payload) VALUES (?, ?)`,
|
|
2292
|
+
"event-1",
|
|
2293
|
+
JSON.stringify({ v: 1 })
|
|
2294
|
+
);
|
|
2295
|
+
const step = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2650
2296
|
eventName: "event-1",
|
|
2651
2297
|
parentStepId: null
|
|
2652
2298
|
});
|
|
2653
2299
|
expect(step).toMatchObject({
|
|
2654
2300
|
state: "satisfied",
|
|
2655
|
-
payload:
|
|
2656
|
-
});
|
|
2657
|
-
});
|
|
2658
|
-
});
|
|
2659
|
-
|
|
2660
|
-
it("consumes queued inbound events in FIFO order when several waits are created", async () => {
|
|
2661
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2662
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2663
|
-
await instance.handleInboundEvent("event-1", "first");
|
|
2664
|
-
// Distinct `created_at` so FIFO ordering does not depend on random `inbound_events.id` when timestamps tie.
|
|
2665
|
-
await new Promise((r) => setTimeout(r, 2));
|
|
2666
|
-
await instance.handleInboundEvent("event-1", "second");
|
|
2667
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2668
|
-
const step1 = await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2669
|
-
type: "wait",
|
|
2670
|
-
eventName: "event-1",
|
|
2671
|
-
parentStepId: null
|
|
2672
|
-
});
|
|
2673
|
-
const step2 = await context.getOrCreateStep(createWaitStepId("wait-2"), {
|
|
2674
|
-
type: "wait",
|
|
2675
|
-
eventName: "event-1",
|
|
2676
|
-
parentStepId: null
|
|
2677
|
-
});
|
|
2678
|
-
expect(step1).toMatchObject({
|
|
2679
|
-
state: "satisfied",
|
|
2680
|
-
payload: JSON.stringify("first")
|
|
2681
|
-
});
|
|
2682
|
-
expect(step2).toMatchObject({
|
|
2683
|
-
state: "satisfied",
|
|
2684
|
-
payload: JSON.stringify("second")
|
|
2301
|
+
payload: { v: 1 }
|
|
2685
2302
|
});
|
|
2686
2303
|
});
|
|
2687
2304
|
});
|
|
2688
2305
|
|
|
2689
|
-
it("
|
|
2306
|
+
it("rejects a second inbound_events row with the same claimed_by", async () => {
|
|
2690
2307
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2691
|
-
await runInDurableObject(stub, async (
|
|
2308
|
+
await runInDurableObject(stub, async (_instance, state) => {
|
|
2692
2309
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2693
|
-
|
|
2694
|
-
type: "wait",
|
|
2695
|
-
eventName: "event-1",
|
|
2696
|
-
parentStepId: null
|
|
2697
|
-
});
|
|
2698
|
-
await context.getOrCreateStep(createWaitStepId("wait-2"), {
|
|
2699
|
-
type: "wait",
|
|
2700
|
-
eventName: "event-1",
|
|
2701
|
-
parentStepId: null
|
|
2702
|
-
});
|
|
2703
|
-
await instance.handleInboundEvent("event-1", "payload");
|
|
2704
|
-
const step1 = await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2705
|
-
type: "wait",
|
|
2310
|
+
context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2706
2311
|
eventName: "event-1",
|
|
2707
2312
|
parentStepId: null
|
|
2708
2313
|
});
|
|
2709
|
-
const
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2314
|
+
const t = Date.now();
|
|
2315
|
+
state.storage.sql.exec(
|
|
2316
|
+
`INSERT INTO inbound_events (event_name, payload, claimed_by, claimed_at) VALUES (?, ?, ?, ?)`,
|
|
2317
|
+
"event-1",
|
|
2318
|
+
null,
|
|
2319
|
+
"wait-1",
|
|
2320
|
+
t
|
|
2321
|
+
);
|
|
2322
|
+
expect(() =>
|
|
2323
|
+
state.storage.sql.exec(
|
|
2324
|
+
`INSERT INTO inbound_events (event_name, payload, claimed_by, claimed_at) VALUES (?, ?, ?, ?)`,
|
|
2325
|
+
"event-1",
|
|
2326
|
+
null,
|
|
2327
|
+
"wait-1",
|
|
2328
|
+
t + 1
|
|
2329
|
+
)
|
|
2330
|
+
).toThrow(/UNIQUE/);
|
|
2719
2331
|
});
|
|
2720
2332
|
});
|
|
2721
|
-
|
|
2722
|
-
it("does not change steps when the workflow is terminal", async () => {
|
|
2723
|
-
const executeSpy = vi
|
|
2724
|
-
.spyOn(TestWorkflowDefinition.prototype, "execute")
|
|
2725
|
-
.mockImplementation(async function (this: TestWorkflowDefinition) {
|
|
2726
|
-
await this.run("step-1", async () => 1);
|
|
2727
|
-
});
|
|
2728
|
-
try {
|
|
2729
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2730
|
-
await runInDurableObject(stub, async (instance) => {
|
|
2731
|
-
const { resolve, promise } = Promise.withResolvers<WorkflowStatus>();
|
|
2732
|
-
instance.onStatusChange_experimental = async (status) => {
|
|
2733
|
-
if (status === "running") return;
|
|
2734
|
-
resolve(status);
|
|
2735
|
-
};
|
|
2736
|
-
await instance.create({ definitionVersion: "2026-03-19" });
|
|
2737
|
-
await expect(promise).resolves.toBe("completed");
|
|
2738
|
-
|
|
2739
|
-
const stepsBefore = instance.getSteps_experimental();
|
|
2740
|
-
const eventsBefore = instance.getStepEvents_experimental();
|
|
2741
|
-
|
|
2742
|
-
await instance.handleInboundEvent("event-1", { ignored: true });
|
|
2743
|
-
|
|
2744
|
-
expect(instance.getSteps_experimental()).toEqual(stepsBefore);
|
|
2745
|
-
expect(instance.getStepEvents_experimental()).toEqual(eventsBefore);
|
|
2746
|
-
});
|
|
2747
|
-
} finally {
|
|
2748
|
-
executeSpy.mockRestore();
|
|
2749
|
-
}
|
|
2750
|
-
});
|
|
2751
2333
|
});
|
|
2752
2334
|
|
|
2753
|
-
describe("
|
|
2335
|
+
describe("handleWaitStepTimedOut()", () => {
|
|
2754
2336
|
it("moves a wait step from 'waiting' to 'timed_out'", async () => {
|
|
2755
2337
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2756
2338
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2757
2339
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2758
2340
|
const timeoutAt = new Date(Date.now() - 1000);
|
|
2759
|
-
|
|
2760
|
-
type: "wait",
|
|
2341
|
+
context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2761
2342
|
eventName: "event-1",
|
|
2762
2343
|
parentStepId: null,
|
|
2763
|
-
timeoutAt
|
|
2344
|
+
timeoutAt
|
|
2764
2345
|
});
|
|
2765
|
-
context.
|
|
2766
|
-
const
|
|
2767
|
-
type: "wait",
|
|
2346
|
+
context.handleWaitStepTimedOut(createWaitStepId("wait-1"));
|
|
2347
|
+
const step = context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2768
2348
|
eventName: "event-1",
|
|
2769
|
-
parentStepId: null
|
|
2770
|
-
timeoutAt: timeoutAt
|
|
2771
|
-
});
|
|
2772
|
-
expect(updatedStep).toMatchObject({
|
|
2773
|
-
state: "timed_out",
|
|
2774
|
-
resolvedAt: expect.any(Date)
|
|
2775
|
-
});
|
|
2776
|
-
});
|
|
2777
|
-
});
|
|
2778
|
-
|
|
2779
|
-
it("writes a 'wait_timed_out' step event when a wait step is timed out", async () => {
|
|
2780
|
-
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2781
|
-
await runInDurableObject(stub, async (instance, state) => {
|
|
2782
|
-
const context = new WorkflowRuntimeContext(state.storage);
|
|
2783
|
-
const timeoutAt = new Date(Date.now() - 1000);
|
|
2784
|
-
await context.getOrCreateStep(createWaitStepId("wait-1"), {
|
|
2785
|
-
type: "wait",
|
|
2786
|
-
eventName: "event-1",
|
|
2787
|
-
parentStepId: null,
|
|
2788
|
-
timeoutAt: timeoutAt
|
|
2349
|
+
parentStepId: null
|
|
2789
2350
|
});
|
|
2790
|
-
|
|
2791
|
-
expect(await instance.getStepEvents_experimental()).toMatchObject([
|
|
2792
|
-
{
|
|
2793
|
-
type: "wait_waiting",
|
|
2794
|
-
stepId: "wait-1",
|
|
2795
|
-
eventName: "event-1",
|
|
2796
|
-
timeoutAt: timeoutAt,
|
|
2797
|
-
recordedAt: expect.any(Date)
|
|
2798
|
-
},
|
|
2799
|
-
{
|
|
2800
|
-
type: "wait_timed_out",
|
|
2801
|
-
stepId: "wait-1",
|
|
2802
|
-
recordedAt: expect.any(Date)
|
|
2803
|
-
}
|
|
2804
|
-
]);
|
|
2351
|
+
expect(step).toMatchObject({ state: "timed_out" });
|
|
2805
2352
|
});
|
|
2806
2353
|
});
|
|
2807
2354
|
|
|
@@ -2809,9 +2356,7 @@ describe("WorkflowRuntime", () => {
|
|
|
2809
2356
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2810
2357
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2811
2358
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2812
|
-
expect(() => context.
|
|
2813
|
-
/not found/
|
|
2814
|
-
);
|
|
2359
|
+
expect(() => context.handleWaitStepTimedOut(createWaitStepId("nonexistent"))).toThrow(/not found/);
|
|
2815
2360
|
});
|
|
2816
2361
|
});
|
|
2817
2362
|
|
|
@@ -2819,14 +2364,13 @@ describe("WorkflowRuntime", () => {
|
|
|
2819
2364
|
const stub = env.TEST_WORKFLOW_RUNTIME.getByName(crypto.randomUUID());
|
|
2820
2365
|
await runInDurableObject(stub, async (_instance, state) => {
|
|
2821
2366
|
const context = new WorkflowRuntimeContext(state.storage);
|
|
2822
|
-
|
|
2823
|
-
type: "wait",
|
|
2367
|
+
context.getOrCreateWaitStep(createWaitStepId("wait-1"), {
|
|
2824
2368
|
eventName: "event-1",
|
|
2825
2369
|
parentStepId: null,
|
|
2826
2370
|
timeoutAt: new Date(Date.now() - 1000)
|
|
2827
2371
|
});
|
|
2828
|
-
context.
|
|
2829
|
-
expect(() => context.
|
|
2372
|
+
context.handleWaitStepTimedOut(createWaitStepId("wait-1"));
|
|
2373
|
+
expect(() => context.handleWaitStepTimedOut(createWaitStepId("wait-1"))).toThrow(
|
|
2830
2374
|
/Expected 'waiting' but got timed_out/
|
|
2831
2375
|
);
|
|
2832
2376
|
});
|