@sonamu-kit/tasks 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/.swcrc +17 -0
  2. package/README.md +7 -0
  3. package/dist/backend.d.ts +107 -0
  4. package/dist/backend.d.ts.map +1 -0
  5. package/dist/backend.js +3 -0
  6. package/dist/backend.js.map +1 -0
  7. package/dist/chaos.test.d.ts +2 -0
  8. package/dist/chaos.test.d.ts.map +1 -0
  9. package/dist/chaos.test.js +92 -0
  10. package/dist/chaos.test.js.map +1 -0
  11. package/dist/client.d.ts +178 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +223 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/client.test.d.ts +2 -0
  16. package/dist/client.test.d.ts.map +1 -0
  17. package/dist/client.test.js +339 -0
  18. package/dist/client.test.js.map +1 -0
  19. package/dist/config.d.ts +22 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +23 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/config.test.d.ts +2 -0
  24. package/dist/config.test.d.ts.map +1 -0
  25. package/dist/config.test.js +24 -0
  26. package/dist/config.test.js.map +1 -0
  27. package/dist/core/duration.d.ts +22 -0
  28. package/dist/core/duration.d.ts.map +1 -0
  29. package/dist/core/duration.js +64 -0
  30. package/dist/core/duration.js.map +1 -0
  31. package/dist/core/duration.test.d.ts +2 -0
  32. package/dist/core/duration.test.d.ts.map +1 -0
  33. package/dist/core/duration.test.js +265 -0
  34. package/dist/core/duration.test.js.map +1 -0
  35. package/dist/core/error.d.ts +15 -0
  36. package/dist/core/error.d.ts.map +1 -0
  37. package/dist/core/error.js +25 -0
  38. package/dist/core/error.js.map +1 -0
  39. package/dist/core/error.test.d.ts +2 -0
  40. package/dist/core/error.test.d.ts.map +1 -0
  41. package/dist/core/error.test.js +63 -0
  42. package/dist/core/error.test.js.map +1 -0
  43. package/dist/core/json.d.ts +5 -0
  44. package/dist/core/json.d.ts.map +1 -0
  45. package/dist/core/json.js +3 -0
  46. package/dist/core/json.js.map +1 -0
  47. package/dist/core/result.d.ts +22 -0
  48. package/dist/core/result.d.ts.map +1 -0
  49. package/dist/core/result.js +22 -0
  50. package/dist/core/result.js.map +1 -0
  51. package/dist/core/result.test.d.ts +2 -0
  52. package/dist/core/result.test.d.ts.map +1 -0
  53. package/dist/core/result.test.js +19 -0
  54. package/dist/core/result.test.js.map +1 -0
  55. package/dist/core/retry.d.ts +21 -0
  56. package/dist/core/retry.d.ts.map +1 -0
  57. package/dist/core/retry.js +25 -0
  58. package/dist/core/retry.js.map +1 -0
  59. package/dist/core/retry.test.d.ts +2 -0
  60. package/dist/core/retry.test.d.ts.map +1 -0
  61. package/dist/core/retry.test.js +37 -0
  62. package/dist/core/retry.test.js.map +1 -0
  63. package/dist/core/schema.d.ts +57 -0
  64. package/dist/core/schema.d.ts.map +1 -0
  65. package/dist/core/schema.js +4 -0
  66. package/dist/core/schema.js.map +1 -0
  67. package/dist/core/step.d.ts +96 -0
  68. package/dist/core/step.d.ts.map +1 -0
  69. package/dist/core/step.js +78 -0
  70. package/dist/core/step.js.map +1 -0
  71. package/dist/core/step.test.d.ts +2 -0
  72. package/dist/core/step.test.d.ts.map +1 -0
  73. package/dist/core/step.test.js +356 -0
  74. package/dist/core/step.test.js.map +1 -0
  75. package/dist/core/workflow.d.ts +78 -0
  76. package/dist/core/workflow.d.ts.map +1 -0
  77. package/dist/core/workflow.js +46 -0
  78. package/dist/core/workflow.js.map +1 -0
  79. package/dist/core/workflow.test.d.ts +2 -0
  80. package/dist/core/workflow.test.d.ts.map +1 -0
  81. package/dist/core/workflow.test.js +172 -0
  82. package/dist/core/workflow.test.js.map +1 -0
  83. package/dist/database/backend.d.ts +60 -0
  84. package/dist/database/backend.d.ts.map +1 -0
  85. package/dist/database/backend.js +387 -0
  86. package/dist/database/backend.js.map +1 -0
  87. package/dist/database/backend.test.d.ts +2 -0
  88. package/dist/database/backend.test.d.ts.map +1 -0
  89. package/dist/database/backend.test.js +17 -0
  90. package/dist/database/backend.test.js.map +1 -0
  91. package/dist/database/backend.testsuite.d.ts +20 -0
  92. package/dist/database/backend.testsuite.d.ts.map +1 -0
  93. package/dist/database/backend.testsuite.js +1174 -0
  94. package/dist/database/backend.testsuite.js.map +1 -0
  95. package/dist/database/base.d.ts +12 -0
  96. package/dist/database/base.d.ts.map +1 -0
  97. package/dist/database/base.js +19 -0
  98. package/dist/database/base.js.map +1 -0
  99. package/dist/database/migrations/20251212000000_0_init.js +9 -0
  100. package/dist/database/migrations/20251212000000_0_init.js.map +1 -0
  101. package/dist/database/migrations/20251212000000_1_tables.js +88 -0
  102. package/dist/database/migrations/20251212000000_1_tables.js.map +1 -0
  103. package/dist/database/migrations/20251212000000_2_fk.js +48 -0
  104. package/dist/database/migrations/20251212000000_2_fk.js.map +1 -0
  105. package/dist/database/migrations/20251212000000_3_indexes.js +107 -0
  106. package/dist/database/migrations/20251212000000_3_indexes.js.map +1 -0
  107. package/dist/database/pubsub.d.ts +17 -0
  108. package/dist/database/pubsub.d.ts.map +1 -0
  109. package/dist/database/pubsub.js +70 -0
  110. package/dist/database/pubsub.js.map +1 -0
  111. package/dist/database/pubsub.test.d.ts +2 -0
  112. package/dist/database/pubsub.test.d.ts.map +1 -0
  113. package/dist/database/pubsub.test.js +86 -0
  114. package/dist/database/pubsub.test.js.map +1 -0
  115. package/dist/errors.d.ts +8 -0
  116. package/dist/errors.d.ts.map +1 -0
  117. package/dist/errors.js +21 -0
  118. package/dist/errors.js.map +1 -0
  119. package/dist/execution.d.ts +82 -0
  120. package/dist/execution.d.ts.map +1 -0
  121. package/dist/execution.js +182 -0
  122. package/dist/execution.js.map +1 -0
  123. package/dist/execution.test.d.ts +2 -0
  124. package/dist/execution.test.d.ts.map +1 -0
  125. package/dist/execution.test.js +556 -0
  126. package/dist/execution.test.js.map +1 -0
  127. package/dist/index.d.ts +8 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +6 -0
  130. package/dist/index.js.map +1 -0
  131. package/dist/internal.d.ts +12 -0
  132. package/dist/internal.d.ts.map +1 -0
  133. package/dist/internal.js +5 -0
  134. package/dist/internal.js.map +1 -0
  135. package/dist/practices/01-remote-workflow.d.ts +2 -0
  136. package/dist/practices/01-remote-workflow.d.ts.map +1 -0
  137. package/dist/practices/01-remote-workflow.js +69 -0
  138. package/dist/practices/01-remote-workflow.js.map +1 -0
  139. package/dist/practices/01-remote.d.ts +2 -0
  140. package/dist/practices/01-remote.d.ts.map +1 -0
  141. package/dist/practices/01-remote.js +87 -0
  142. package/dist/practices/01-remote.js.map +1 -0
  143. package/dist/practices/02-local.d.ts +2 -0
  144. package/dist/practices/02-local.d.ts.map +1 -0
  145. package/dist/practices/02-local.js +84 -0
  146. package/dist/practices/02-local.js.map +1 -0
  147. package/dist/practices/03-local-retry.d.ts +2 -0
  148. package/dist/practices/03-local-retry.d.ts.map +1 -0
  149. package/dist/practices/03-local-retry.js +85 -0
  150. package/dist/practices/03-local-retry.js.map +1 -0
  151. package/dist/practices/04-scheduler-dispose.d.ts +2 -0
  152. package/dist/practices/04-scheduler-dispose.d.ts.map +1 -0
  153. package/dist/practices/04-scheduler-dispose.js +65 -0
  154. package/dist/practices/04-scheduler-dispose.js.map +1 -0
  155. package/dist/practices/05-router.d.ts +2 -0
  156. package/dist/practices/05-router.d.ts.map +1 -0
  157. package/dist/practices/05-router.js +80 -0
  158. package/dist/practices/05-router.js.map +1 -0
  159. package/dist/registry.d.ts +33 -0
  160. package/dist/registry.d.ts.map +1 -0
  161. package/dist/registry.js +54 -0
  162. package/dist/registry.js.map +1 -0
  163. package/dist/registry.test.d.ts +2 -0
  164. package/dist/registry.test.d.ts.map +1 -0
  165. package/dist/registry.test.js +95 -0
  166. package/dist/registry.test.js.map +1 -0
  167. package/dist/scheduler.d.ts +22 -0
  168. package/dist/scheduler.d.ts.map +1 -0
  169. package/dist/scheduler.js +117 -0
  170. package/dist/scheduler.js.map +1 -0
  171. package/dist/tasks/index.d.ts +4 -0
  172. package/dist/tasks/index.d.ts.map +1 -0
  173. package/dist/tasks/index.js +5 -0
  174. package/dist/tasks/index.js.map +1 -0
  175. package/dist/tasks/local-task.d.ts +6 -0
  176. package/dist/tasks/local-task.d.ts.map +1 -0
  177. package/dist/tasks/local-task.js +95 -0
  178. package/dist/tasks/local-task.js.map +1 -0
  179. package/dist/tasks/remote-task.d.ts +11 -0
  180. package/dist/tasks/remote-task.d.ts.map +1 -0
  181. package/dist/tasks/remote-task.js +213 -0
  182. package/dist/tasks/remote-task.js.map +1 -0
  183. package/dist/tasks/shared.d.ts +8 -0
  184. package/dist/tasks/shared.d.ts.map +1 -0
  185. package/dist/tasks/shared.js +41 -0
  186. package/dist/tasks/shared.js.map +1 -0
  187. package/dist/testing/connection.d.ts +7 -0
  188. package/dist/testing/connection.d.ts.map +1 -0
  189. package/dist/testing/connection.js +38 -0
  190. package/dist/testing/connection.js.map +1 -0
  191. package/dist/types/config.d.ts +44 -0
  192. package/dist/types/config.d.ts.map +1 -0
  193. package/dist/types/config.js +3 -0
  194. package/dist/types/config.js.map +1 -0
  195. package/dist/types/context.d.ts +18 -0
  196. package/dist/types/context.d.ts.map +1 -0
  197. package/dist/types/context.js +4 -0
  198. package/dist/types/context.js.map +1 -0
  199. package/dist/types/events.d.ts +43 -0
  200. package/dist/types/events.d.ts.map +1 -0
  201. package/dist/types/events.js +3 -0
  202. package/dist/types/events.js.map +1 -0
  203. package/dist/types/index.d.ts +6 -0
  204. package/dist/types/index.d.ts.map +1 -0
  205. package/dist/types/index.js +3 -0
  206. package/dist/types/index.js.map +1 -0
  207. package/dist/types/task-items.d.ts +12 -0
  208. package/dist/types/task-items.d.ts.map +1 -0
  209. package/dist/types/task-items.js +3 -0
  210. package/dist/types/task-items.js.map +1 -0
  211. package/dist/types/utils.d.ts +4 -0
  212. package/dist/types/utils.d.ts.map +1 -0
  213. package/dist/types/utils.js +8 -0
  214. package/dist/types/utils.js.map +1 -0
  215. package/dist/worker.d.ts +61 -0
  216. package/dist/worker.d.ts.map +1 -0
  217. package/dist/worker.js +206 -0
  218. package/dist/worker.js.map +1 -0
  219. package/dist/worker.test.d.ts +2 -0
  220. package/dist/worker.test.d.ts.map +1 -0
  221. package/dist/worker.test.js +1163 -0
  222. package/dist/worker.test.js.map +1 -0
  223. package/dist/workflow.d.ts +44 -0
  224. package/dist/workflow.d.ts.map +1 -0
  225. package/dist/workflow.js +21 -0
  226. package/dist/workflow.js.map +1 -0
  227. package/dist/workflow.test.d.ts +2 -0
  228. package/dist/workflow.test.d.ts.map +1 -0
  229. package/dist/workflow.test.js +73 -0
  230. package/dist/workflow.test.js.map +1 -0
  231. package/nodemon.json +6 -0
  232. package/package.json +63 -0
  233. package/scripts/migrate.ts +11 -0
  234. package/src/backend.ts +133 -0
  235. package/src/chaos.test.ts +108 -0
  236. package/src/client.test.ts +297 -0
  237. package/src/client.ts +331 -0
  238. package/src/config.test.ts +23 -0
  239. package/src/config.ts +35 -0
  240. package/src/core/duration.test.ts +326 -0
  241. package/src/core/duration.ts +86 -0
  242. package/src/core/error.test.ts +77 -0
  243. package/src/core/error.ts +30 -0
  244. package/src/core/json.ts +2 -0
  245. package/src/core/result.test.ts +13 -0
  246. package/src/core/result.ts +29 -0
  247. package/src/core/retry.test.ts +41 -0
  248. package/src/core/retry.ts +29 -0
  249. package/src/core/schema.ts +74 -0
  250. package/src/core/step.test.ts +362 -0
  251. package/src/core/step.ts +152 -0
  252. package/src/core/workflow.test.ts +184 -0
  253. package/src/core/workflow.ts +127 -0
  254. package/src/database/backend.test.ts +16 -0
  255. package/src/database/backend.testsuite.ts +1376 -0
  256. package/src/database/backend.ts +655 -0
  257. package/src/database/base.ts +23 -0
  258. package/src/database/migrations/20251212000000_0_init.ts +10 -0
  259. package/src/database/migrations/20251212000000_1_tables.ts +54 -0
  260. package/src/database/migrations/20251212000000_2_fk.ts +46 -0
  261. package/src/database/migrations/20251212000000_3_indexes.ts +82 -0
  262. package/src/database/pubsub.test.ts +92 -0
  263. package/src/database/pubsub.ts +92 -0
  264. package/src/execution.test.ts +508 -0
  265. package/src/execution.ts +291 -0
  266. package/src/index.ts +7 -0
  267. package/src/internal.ts +11 -0
  268. package/src/practices/01-remote-workflow.ts +61 -0
  269. package/src/registry.test.ts +122 -0
  270. package/src/registry.ts +65 -0
  271. package/src/testing/connection.ts +44 -0
  272. package/src/worker.test.ts +1138 -0
  273. package/src/worker.ts +281 -0
  274. package/src/workflow.test.ts +68 -0
  275. package/src/workflow.ts +84 -0
  276. package/table_ddl.sql +60 -0
  277. package/templates/openworkflow.config.ts +22 -0
  278. package/tsconfig.json +40 -0
  279. package/tsconfig.test.json +4 -0
  280. package/vite.config.ts +13 -0
@@ -0,0 +1,74 @@
1
+ /** The Standard Schema interface. https://standardschema.dev */
2
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
3
+ /** The Standard Schema properties. */
4
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
5
+ }
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-namespace
8
+ export declare namespace StandardSchemaV1 {
9
+ /** The Standard Schema properties interface. */
10
+ // eslint-disable-next-line functional/no-mixed-types
11
+ export interface Props<Input = unknown, Output = Input> {
12
+ /** The version number of the standard. */
13
+ readonly version: 1;
14
+ /** The vendor name of the schema library. */
15
+ readonly vendor: string;
16
+ /** Validates unknown input values. */
17
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
18
+ /** Inferred types associated with the schema. */
19
+ readonly types?: Types<Input, Output> | undefined;
20
+ }
21
+
22
+ /** The result interface of the validate function. */
23
+ export type Result<Output> = SuccessResult<Output> | FailureResult;
24
+
25
+ /** The result interface if validation succeeds. */
26
+ export interface SuccessResult<Output> {
27
+ /** The typed output value. */
28
+ readonly value: Output;
29
+ /** The non-existent issues. */
30
+ readonly issues?: undefined;
31
+ }
32
+
33
+ /** The result interface if validation fails. */
34
+ export interface FailureResult {
35
+ /** The issues of failed validation. */
36
+ readonly issues: readonly Issue[];
37
+ }
38
+
39
+ /** The issue interface of the failure output. */
40
+ export interface Issue {
41
+ /** The error message of the issue. */
42
+ readonly message: string;
43
+ /** The path of the issue, if any. */
44
+ readonly path?: readonly (PropertyKey | PathSegment)[] | undefined;
45
+ }
46
+
47
+ /** The path segment interface of the issue. */
48
+ export interface PathSegment {
49
+ /** The key representing a path segment. */
50
+ readonly key: PropertyKey;
51
+ }
52
+
53
+ /** The Standard Schema types interface. */
54
+ export interface Types<Input = unknown, Output = Input> {
55
+ /** The input type of the schema. */
56
+ readonly input: Input;
57
+ /** The output type of the schema. */
58
+ readonly output: Output;
59
+ }
60
+
61
+ /** Infers the input type of a Standard Schema. */
62
+ export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
63
+ Schema["~standard"]["types"]
64
+ >["input"];
65
+
66
+ /** Infers the output type of a Standard Schema. */
67
+ export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
68
+ Schema["~standard"]["types"]
69
+ >["output"];
70
+
71
+ // eslint-disable-next-line unicorn/require-module-specifiers
72
+ // biome-ignore lint/complexity/noUselessEmptyExport: needed for granular visibility control of TS namespace
73
+ export {};
74
+ }
@@ -0,0 +1,362 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { ok } from "./result";
3
+ import type { StepAttempt, StepAttemptCache } from "./step";
4
+ import {
5
+ addToStepAttemptCache,
6
+ calculateSleepResumeAt,
7
+ createSleepContext,
8
+ createStepAttemptCacheFromAttempts,
9
+ getCachedStepAttempt,
10
+ hasCompletedStep,
11
+ normalizeStepOutput,
12
+ } from "./step";
13
+
14
+ describe("createStepAttemptCacheFromAttempts", () => {
15
+ test("creates empty cache from empty array", () => {
16
+ const cache = createStepAttemptCacheFromAttempts([]);
17
+
18
+ expect(cache.size).toBe(0);
19
+ });
20
+
21
+ test("includes completed attempts in cache", () => {
22
+ const attempt = createMockStepAttempt({
23
+ stepName: "step-a",
24
+ status: "completed",
25
+ output: "result",
26
+ });
27
+ const cache = createStepAttemptCacheFromAttempts([attempt]);
28
+
29
+ expect(cache.size).toBe(1);
30
+ expect(cache.get("step-a")).toBe(attempt);
31
+ });
32
+
33
+ test("includes succeeded attempts in cache (deprecated status)", () => {
34
+ const attempt = createMockStepAttempt({
35
+ stepName: "step-b",
36
+ status: "succeeded",
37
+ output: "result",
38
+ });
39
+ const cache = createStepAttemptCacheFromAttempts([attempt]);
40
+
41
+ expect(cache.size).toBe(1);
42
+ expect(cache.get("step-b")).toBe(attempt);
43
+ });
44
+
45
+ test("excludes running attempts from cache", () => {
46
+ const attempt = createMockStepAttempt({
47
+ stepName: "step-c",
48
+ status: "running",
49
+ });
50
+ const cache = createStepAttemptCacheFromAttempts([attempt]);
51
+
52
+ expect(cache.size).toBe(0);
53
+ });
54
+
55
+ test("excludes failed attempts from cache", () => {
56
+ const attempt = createMockStepAttempt({
57
+ stepName: "step-d",
58
+ status: "failed",
59
+ error: { message: "failed" },
60
+ });
61
+ const cache = createStepAttemptCacheFromAttempts([attempt]);
62
+
63
+ expect(cache.size).toBe(0);
64
+ });
65
+
66
+ test("filters mixed statuses correctly", () => {
67
+ const attempts = [
68
+ createMockStepAttempt({
69
+ stepName: "completed-step",
70
+ status: "completed",
71
+ }),
72
+ createMockStepAttempt({ stepName: "running-step", status: "running" }),
73
+ createMockStepAttempt({ stepName: "failed-step", status: "failed" }),
74
+ createMockStepAttempt({
75
+ stepName: "succeeded-step",
76
+ status: "succeeded",
77
+ }),
78
+ ];
79
+ const cache = createStepAttemptCacheFromAttempts(attempts);
80
+
81
+ expect(cache.size).toBe(2);
82
+ expect(cache.has("completed-step")).toBe(true);
83
+ expect(cache.has("succeeded-step")).toBe(true);
84
+ expect(cache.has("running-step")).toBe(false);
85
+ expect(cache.has("failed-step")).toBe(false);
86
+ });
87
+
88
+ test("uses step name as cache key", () => {
89
+ const attempt = createMockStepAttempt({
90
+ stepName: "my-unique-step-name",
91
+ status: "completed",
92
+ });
93
+ const cache = createStepAttemptCacheFromAttempts([attempt]);
94
+
95
+ expect(cache.get("my-unique-step-name")).toBe(attempt);
96
+ expect(cache.get("other-name")).toBeUndefined();
97
+ });
98
+ });
99
+
100
+ describe("getCachedStepAttempt", () => {
101
+ test("returns cached attempt when present", () => {
102
+ const attempt = createMockStepAttempt({ stepName: "cached-step" });
103
+ const cache: StepAttemptCache = new Map([["cached-step", attempt]]);
104
+
105
+ const result = getCachedStepAttempt(cache, "cached-step");
106
+
107
+ expect(result).toBe(attempt);
108
+ });
109
+
110
+ test("returns undefined when step not in cache", () => {
111
+ const cache: StepAttemptCache = new Map();
112
+
113
+ const result = getCachedStepAttempt(cache, "missing-step");
114
+
115
+ expect(result).toBeUndefined();
116
+ });
117
+
118
+ test("returns undefined for similar but different step names", () => {
119
+ const attempt = createMockStepAttempt({ stepName: "step-1" });
120
+ const cache: StepAttemptCache = new Map([["step-1", attempt]]);
121
+
122
+ expect(getCachedStepAttempt(cache, "step-2")).toBeUndefined();
123
+ expect(getCachedStepAttempt(cache, "Step-1")).toBeUndefined();
124
+ expect(getCachedStepAttempt(cache, "step-1 ")).toBeUndefined();
125
+ });
126
+ });
127
+
128
+ describe("hasCompletedStep", () => {
129
+ test("returns true when step is in cache", () => {
130
+ const attempt = createMockStepAttempt({ stepName: "step-x" });
131
+ const cache: StepAttemptCache = new Map([["step-x", attempt]]);
132
+
133
+ expect(hasCompletedStep(cache, "step-x")).toBe(true);
134
+ });
135
+
136
+ test("returns false when step is not in cache", () => {
137
+ const cache: StepAttemptCache = new Map();
138
+
139
+ expect(hasCompletedStep(cache, "step-y")).toBe(false);
140
+ });
141
+
142
+ test("returns false for empty cache", () => {
143
+ const cache: StepAttemptCache = new Map();
144
+
145
+ expect(hasCompletedStep(cache, "any-step")).toBe(false);
146
+ });
147
+ });
148
+
149
+ describe("addToStepAttemptCache", () => {
150
+ test("adds attempt to empty cache", () => {
151
+ const cache: StepAttemptCache = new Map();
152
+ const attempt = createMockStepAttempt({ stepName: "new-step" });
153
+
154
+ const newCache = addToStepAttemptCache(cache, attempt);
155
+
156
+ expect(newCache.size).toBe(1);
157
+ expect(newCache.get("new-step")).toBe(attempt);
158
+ });
159
+
160
+ test("adds attempt to existing cache", () => {
161
+ const existing = createMockStepAttempt({ stepName: "existing-step" });
162
+ const cache: StepAttemptCache = new Map([["existing-step", existing]]);
163
+ const newAttempt = createMockStepAttempt({ stepName: "new-step" });
164
+
165
+ const newCache = addToStepAttemptCache(cache, newAttempt);
166
+
167
+ expect(newCache.size).toBe(2);
168
+ expect(newCache.get("existing-step")).toBe(existing);
169
+ expect(newCache.get("new-step")).toBe(newAttempt);
170
+ });
171
+
172
+ test("does not mutate original cache (immutable)", () => {
173
+ const existing = createMockStepAttempt({ stepName: "existing-step" });
174
+ const cache: StepAttemptCache = new Map([["existing-step", existing]]);
175
+ const newAttempt = createMockStepAttempt({ stepName: "new-step" });
176
+
177
+ const newCache = addToStepAttemptCache(cache, newAttempt);
178
+
179
+ expect(cache.size).toBe(1);
180
+ expect(cache.has("new-step")).toBe(false);
181
+ expect(newCache.size).toBe(2);
182
+ });
183
+
184
+ test("overwrites existing entry with same step name", () => {
185
+ const original = createMockStepAttempt({
186
+ stepName: "step",
187
+ output: "original",
188
+ });
189
+ const cache: StepAttemptCache = new Map([["step", original]]);
190
+ const replacement = createMockStepAttempt({
191
+ stepName: "step",
192
+ output: "replacement",
193
+ });
194
+
195
+ const newCache = addToStepAttemptCache(cache, replacement);
196
+
197
+ expect(newCache.size).toBe(1);
198
+ expect(newCache.get("step")?.output).toBe("replacement");
199
+ });
200
+ });
201
+
202
+ describe("normalizeStepOutput", () => {
203
+ test("passes through string values", () => {
204
+ expect(normalizeStepOutput("hello")).toBe("hello");
205
+ });
206
+
207
+ test("passes through number values", () => {
208
+ expect(normalizeStepOutput(42)).toBe(42);
209
+ expect(normalizeStepOutput(3.14)).toBe(3.14);
210
+ expect(normalizeStepOutput(0)).toBe(0);
211
+ expect(normalizeStepOutput(-1)).toBe(-1);
212
+ });
213
+
214
+ test("passes through boolean values", () => {
215
+ expect(normalizeStepOutput(true)).toBe(true);
216
+ expect(normalizeStepOutput(false)).toBe(false);
217
+ });
218
+
219
+ test("passes through null", () => {
220
+ expect(normalizeStepOutput(null)).toBeNull();
221
+ });
222
+
223
+ test("converts undefined to null", () => {
224
+ // eslint-disable-next-line unicorn/no-useless-undefined
225
+ expect(normalizeStepOutput(undefined)).toBeNull();
226
+ });
227
+
228
+ test("passes through object values", () => {
229
+ const obj = { foo: "bar", nested: { baz: 123 } };
230
+ expect(normalizeStepOutput(obj)).toBe(obj);
231
+ });
232
+
233
+ test("passes through array values", () => {
234
+ const arr = [1, 2, 3];
235
+ expect(normalizeStepOutput(arr)).toBe(arr);
236
+ });
237
+
238
+ test("passes through empty object", () => {
239
+ const obj = {};
240
+ expect(normalizeStepOutput(obj)).toBe(obj);
241
+ });
242
+
243
+ test("passes through empty array", () => {
244
+ const arr: unknown[] = [];
245
+ expect(normalizeStepOutput(arr)).toBe(arr);
246
+ });
247
+ });
248
+
249
+ describe("calculateSleepResumeAt", () => {
250
+ test("calculates resume time from duration string", () => {
251
+ const now = 1_000_000;
252
+ const result = calculateSleepResumeAt("5s", now);
253
+
254
+ expect(result).toEqual(ok(new Date(now + 5000)));
255
+ });
256
+
257
+ test("calculates resume time with milliseconds", () => {
258
+ const now = 1_000_000;
259
+ const result = calculateSleepResumeAt("500ms", now);
260
+
261
+ expect(result).toEqual(ok(new Date(now + 500)));
262
+ });
263
+
264
+ test("calculates resume time with minutes", () => {
265
+ const now = 1_000_000;
266
+ const result = calculateSleepResumeAt("2m", now);
267
+
268
+ expect(result).toEqual(ok(new Date(now + 2 * 60 * 1000)));
269
+ });
270
+
271
+ test("calculates resume time with hours", () => {
272
+ const now = 1_000_000;
273
+ const result = calculateSleepResumeAt("1h", now);
274
+
275
+ expect(result).toEqual(ok(new Date(now + 60 * 60 * 1000)));
276
+ });
277
+
278
+ test("uses Date.now() when now is not provided", () => {
279
+ const before = Date.now();
280
+ const result = calculateSleepResumeAt("1s");
281
+ const after = Date.now();
282
+
283
+ expect(result.ok).toBe(true);
284
+ if (result.ok) {
285
+ const resumeTime = result.value.getTime();
286
+ expect(resumeTime).toBeGreaterThanOrEqual(before + 1000);
287
+ expect(resumeTime).toBeLessThanOrEqual(after + 1000);
288
+ }
289
+ });
290
+
291
+ test("returns error for invalid duration", () => {
292
+ // @ts-expect-error testing invalid input
293
+ const result = calculateSleepResumeAt("invalid");
294
+
295
+ expect(result.ok).toBe(false);
296
+ if (!result.ok) {
297
+ expect(result.error).toBeInstanceOf(Error);
298
+ }
299
+ });
300
+
301
+ test("returns error for empty duration", () => {
302
+ // @ts-expect-error testing invalid input
303
+ const result = calculateSleepResumeAt("");
304
+
305
+ expect(result.ok).toBe(false);
306
+ });
307
+ });
308
+
309
+ describe("createSleepContext", () => {
310
+ test("creates sleep context with ISO string timestamp", () => {
311
+ const resumeAt = new Date("2025-06-15T10:30:00.000Z");
312
+ const context = createSleepContext(resumeAt);
313
+
314
+ expect(context).toEqual({
315
+ kind: "sleep",
316
+ resumeAt: "2025-06-15T10:30:00.000Z",
317
+ });
318
+ });
319
+
320
+ test("preserves millisecond precision", () => {
321
+ const resumeAt = new Date("2025-01-01T00:00:00.123Z");
322
+ const context = createSleepContext(resumeAt);
323
+
324
+ expect(context.resumeAt).toBe("2025-01-01T00:00:00.123Z");
325
+ });
326
+
327
+ test("always has kind set to sleep", () => {
328
+ const resumeAt = new Date();
329
+ const context = createSleepContext(resumeAt);
330
+
331
+ expect(context.kind).toBe("sleep");
332
+ });
333
+
334
+ test("creates context from current date", () => {
335
+ const now = new Date();
336
+ const context = createSleepContext(now);
337
+
338
+ expect(context.resumeAt).toBe(now.toISOString());
339
+ });
340
+ });
341
+
342
+ function createMockStepAttempt(overrides: Partial<StepAttempt> = {}): StepAttempt {
343
+ return {
344
+ namespaceId: "default",
345
+ id: "step-1",
346
+ workflowRunId: "workflow-1",
347
+ stepName: "test-step",
348
+ kind: "function",
349
+ status: "completed",
350
+ config: {},
351
+ context: null,
352
+ output: null,
353
+ error: null,
354
+ childWorkflowRunNamespaceId: null,
355
+ childWorkflowRunId: null,
356
+ startedAt: new Date("2025-01-01T00:00:00Z"),
357
+ finishedAt: new Date("2025-01-01T00:00:01Z"),
358
+ createdAt: new Date("2025-01-01T00:00:00Z"),
359
+ updatedAt: new Date("2025-01-01T00:00:01Z"),
360
+ ...overrides,
361
+ };
362
+ }
@@ -0,0 +1,152 @@
1
+ import type { DurationString } from "./duration";
2
+ import { parseDuration } from "./duration";
3
+ import type { JsonValue } from "./json";
4
+ import type { Result } from "./result";
5
+ import { err, ok } from "./result";
6
+
7
+ /**
8
+ * The kind of step in a workflow.
9
+ */
10
+ export type StepKind = "function" | "sleep";
11
+
12
+ /**
13
+ * Status of a step attempt through its lifecycle.
14
+ */
15
+ export type StepAttemptStatus =
16
+ | "running"
17
+ | "succeeded" // deprecated in favor of 'completed'
18
+ | "completed"
19
+ | "failed";
20
+
21
+ /**
22
+ * Context for a step attempt (currently only used for sleep steps).
23
+ */
24
+ export interface StepAttemptContext {
25
+ kind: "sleep";
26
+ resumeAt: string;
27
+ }
28
+
29
+ /**
30
+ * StepAttempt represents a single attempt of a step within a workflow.
31
+ */
32
+ export interface StepAttempt {
33
+ namespaceId: string;
34
+ id: string;
35
+ workflowRunId: string;
36
+ stepName: string;
37
+ kind: StepKind;
38
+ status: StepAttemptStatus;
39
+ config: JsonValue; // user-defined config
40
+ context: StepAttemptContext | null; // runtime execution metadata
41
+ output: JsonValue | null;
42
+ error: JsonValue | null;
43
+ childWorkflowRunNamespaceId: string | null;
44
+ childWorkflowRunId: string | null;
45
+ startedAt: Date | null;
46
+ finishedAt: Date | null;
47
+ createdAt: Date;
48
+ updatedAt: Date;
49
+ }
50
+
51
+ /**
52
+ * Immutable cache for step attempts, keyed by step name.
53
+ */
54
+ export type StepAttemptCache = ReadonlyMap<string, StepAttempt>;
55
+
56
+ /**
57
+ * Create a step attempt cache from an array of attempts. Only includes
58
+ * successful attempts (completed or succeeded status).
59
+ * @param attempts - Array of step attempts to cache
60
+ * @returns An immutable map of step name to successful attempt
61
+ */
62
+ export function createStepAttemptCacheFromAttempts(
63
+ attempts: readonly StepAttempt[],
64
+ ): StepAttemptCache {
65
+ // 'succeeded' status is deprecated in favor of 'completed'
66
+ const successfulAttempts = attempts.filter(
67
+ (attempt) => attempt.status === "succeeded" || attempt.status === "completed",
68
+ );
69
+
70
+ return new Map(successfulAttempts.map((attempt) => [attempt.stepName, attempt]));
71
+ }
72
+
73
+ /**
74
+ * Get a cached step attempt by name.
75
+ * @param cache - The step attempt cache
76
+ * @param stepName - The name of the step to look up
77
+ * @returns The cached attempt or undefined if not found
78
+ */
79
+ export function getCachedStepAttempt(
80
+ cache: StepAttemptCache,
81
+ stepName: string,
82
+ ): StepAttempt | undefined {
83
+ return cache.get(stepName);
84
+ }
85
+
86
+ /**
87
+ * Check if a step attempt is cached (has completed successfully).
88
+ * @param cache - The step attempt cache
89
+ * @param stepName - The name of the step to check
90
+ * @returns True if the step has a cached successful result
91
+ */
92
+ export function hasCompletedStep(cache: StepAttemptCache, stepName: string): boolean {
93
+ return cache.has(stepName);
94
+ }
95
+
96
+ /**
97
+ * Add a step attempt to the cache (returns new cache, original unchanged). This
98
+ * is an immutable operation.
99
+ * @param cache - The existing step attempt cache
100
+ * @param attempt - The attempt to add
101
+ * @returns A new cache with the attempt added
102
+ */
103
+ export function addToStepAttemptCache(
104
+ cache: StepAttemptCache,
105
+ attempt: Readonly<StepAttempt>,
106
+ ): StepAttemptCache {
107
+ return new Map([...cache, [attempt.stepName, attempt]]);
108
+ }
109
+
110
+ /**
111
+ * Convert a step function result to a JSON-compatible value. Undefined values
112
+ * are converted to null for JSON serialization.
113
+ * @param result - The result from a step function
114
+ * @returns A JSON-serializable value
115
+ */
116
+ export function normalizeStepOutput(result: unknown): JsonValue {
117
+ return (result ?? null) as JsonValue;
118
+ }
119
+
120
+ /**
121
+ * Calculate the resume time for a sleep step.
122
+ * @param duration - The duration string to sleep for
123
+ * @param now - The current timestamp (defaults to Date.now())
124
+ * @returns A Result containing the resume Date or an Error
125
+ */
126
+ export function calculateSleepResumeAt(
127
+ duration: DurationString,
128
+ now: number = Date.now(),
129
+ ): Result<Date> {
130
+ const result = parseDuration(duration);
131
+
132
+ if (!result.ok) {
133
+ return err(result.error);
134
+ }
135
+
136
+ return ok(new Date(now + result.value));
137
+ }
138
+
139
+ /**
140
+ * Create the context object for a sleep step attempt.
141
+ * @param resumeAt - The time when the sleep should resume
142
+ * @returns The context object for the sleep step
143
+ */
144
+ export function createSleepContext(resumeAt: Readonly<Date>): {
145
+ kind: "sleep";
146
+ resumeAt: string;
147
+ } {
148
+ return {
149
+ kind: "sleep" as const,
150
+ resumeAt: resumeAt.toISOString(),
151
+ };
152
+ }