@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,291 @@
1
+ import type { Backend } from "./backend";
2
+ import type { DurationString } from "./core/duration";
3
+ import { serializeError } from "./core/error";
4
+ import type { JsonValue } from "./core/json";
5
+ import type { StepAttempt, StepAttemptCache } from "./core/step";
6
+ import {
7
+ addToStepAttemptCache,
8
+ calculateSleepResumeAt,
9
+ createSleepContext,
10
+ createStepAttemptCacheFromAttempts,
11
+ getCachedStepAttempt,
12
+ normalizeStepOutput,
13
+ } from "./core/step";
14
+ import type { WorkflowRun } from "./core/workflow";
15
+
16
+ /**
17
+ * Config for an individual step defined with `step.run()`.
18
+ */
19
+ export interface StepFunctionConfig {
20
+ /**
21
+ * The name of the step.
22
+ */
23
+ name: string;
24
+ }
25
+
26
+ /**
27
+ * Represents the API for defining steps within a workflow. Used within a
28
+ * workflow handler to define steps by calling `step.run()`.
29
+ */
30
+ export interface StepApi {
31
+ run<Output>(config: Readonly<StepFunctionConfig>, fn: StepFunction<Output>): Promise<Output>;
32
+ sleep(name: string, duration: DurationString): Promise<void>;
33
+ }
34
+
35
+ /**
36
+ * The step definition (defined by the user) that executes user code. Can return
37
+ * undefined (e.g., when using `return;`) which will be converted to null.
38
+ */
39
+ export type StepFunction<Output> = () => Promise<Output | undefined> | Output | undefined;
40
+
41
+ /**
42
+ * Params passed to a workflow function for the user to use when defining steps.
43
+ */
44
+ export interface WorkflowFunctionParams<Input> {
45
+ input: Input;
46
+ step: StepApi;
47
+ version: string | null;
48
+ }
49
+
50
+ /**
51
+ * The workflow definition's function (defined by the user) that the user uses
52
+ * to define the workflow's steps.
53
+ */
54
+ export type WorkflowFunction<Input, Output> = (
55
+ params: Readonly<WorkflowFunctionParams<Input>>,
56
+ ) => Promise<Output> | Output;
57
+
58
+ /**
59
+ * Signal thrown when a workflow needs to sleep. Contains the time when the
60
+ * workflow should resume.
61
+ */
62
+ class SleepSignal extends Error {
63
+ readonly resumeAt: Date;
64
+
65
+ constructor(resumeAt: Readonly<Date>) {
66
+ super("SleepSignal");
67
+ this.name = "SleepSignal";
68
+ this.resumeAt = resumeAt;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Configures the options for a StepExecutor.
74
+ */
75
+ export interface StepExecutorOptions {
76
+ backend: Backend;
77
+ workflowRunId: string;
78
+ workerId: string;
79
+ attempts: StepAttempt[];
80
+ }
81
+
82
+ /**
83
+ * Replays prior step attempts and persists new ones while memoizing
84
+ * deterministic step outputs.
85
+ */
86
+ export class StepExecutor implements StepApi {
87
+ private readonly backend: Backend;
88
+ private readonly workflowRunId: string;
89
+ private readonly workerId: string;
90
+ private cache: StepAttemptCache;
91
+
92
+ constructor(options: Readonly<StepExecutorOptions>) {
93
+ this.backend = options.backend;
94
+ this.workflowRunId = options.workflowRunId;
95
+ this.workerId = options.workerId;
96
+
97
+ this.cache = createStepAttemptCacheFromAttempts(options.attempts);
98
+ }
99
+
100
+ async run<Output>(
101
+ config: Readonly<StepFunctionConfig>,
102
+ fn: StepFunction<Output>,
103
+ ): Promise<Output> {
104
+ const { name } = config;
105
+
106
+ // return cached result if available
107
+ const existingAttempt = getCachedStepAttempt(this.cache, name);
108
+ if (existingAttempt) {
109
+ return existingAttempt.output as Output;
110
+ }
111
+
112
+ // not in cache, create new step attempt
113
+ const attempt = await this.backend.createStepAttempt({
114
+ workflowRunId: this.workflowRunId,
115
+ workerId: this.workerId,
116
+ stepName: name,
117
+ kind: "function",
118
+ config: {},
119
+ context: null,
120
+ });
121
+
122
+ try {
123
+ // execute step function
124
+ const result = await fn();
125
+ const output = normalizeStepOutput(result);
126
+
127
+ // mark success
128
+ const savedAttempt = await this.backend.completeStepAttempt({
129
+ workflowRunId: this.workflowRunId,
130
+ stepAttemptId: attempt.id,
131
+ workerId: this.workerId,
132
+ output,
133
+ });
134
+
135
+ // cache result
136
+ this.cache = addToStepAttemptCache(this.cache, savedAttempt);
137
+
138
+ return savedAttempt.output as Output;
139
+ } catch (error) {
140
+ // mark failure
141
+ await this.backend.failStepAttempt({
142
+ workflowRunId: this.workflowRunId,
143
+ stepAttemptId: attempt.id,
144
+ workerId: this.workerId,
145
+ error: serializeError(error),
146
+ });
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ async sleep(name: string, duration: DurationString): Promise<void> {
152
+ // return cached result if this sleep already completed
153
+ const existingAttempt = getCachedStepAttempt(this.cache, name);
154
+ if (existingAttempt) return;
155
+
156
+ // create new step attempt for the sleep
157
+ const result = calculateSleepResumeAt(duration);
158
+ if (!result.ok) {
159
+ throw result.error;
160
+ }
161
+ const resumeAt = result.value;
162
+ const context = createSleepContext(resumeAt);
163
+
164
+ await this.backend.createStepAttempt({
165
+ workflowRunId: this.workflowRunId,
166
+ workerId: this.workerId,
167
+ stepName: name,
168
+ kind: "sleep",
169
+ config: {},
170
+ context,
171
+ });
172
+
173
+ // throw sleep signal to trigger postponement
174
+ // we do not mark the step as completed here; it will be updated
175
+ // when the workflow resumes
176
+ throw new SleepSignal(resumeAt);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Parameters for the workflow execution use case.
182
+ */
183
+ export interface ExecuteWorkflowParams {
184
+ backend: Backend;
185
+ workflowRun: WorkflowRun;
186
+ workflowFn: WorkflowFunction<unknown, unknown>;
187
+ workflowVersion: string | null;
188
+ workerId: string;
189
+ }
190
+
191
+ /**
192
+ * Execute a workflow run. This is the core application use case that handles:
193
+ * - Loading step history
194
+ * - Handling sleeping steps
195
+ * - Creating the step executor
196
+ * - Executing the workflow function
197
+ * - Completing, failing, or sleeping the workflow run based on the outcome
198
+ * @param params - The execution parameters
199
+ */
200
+ export async function executeWorkflow(params: Readonly<ExecuteWorkflowParams>): Promise<void> {
201
+ const { backend, workflowRun, workflowFn, workflowVersion, workerId } = params;
202
+
203
+ try {
204
+ // load all pages of step history
205
+ const attempts: StepAttempt[] = [];
206
+ let cursor: string | undefined;
207
+ do {
208
+ const response = await backend.listStepAttempts({
209
+ workflowRunId: workflowRun.id,
210
+ ...(cursor ? { after: cursor } : {}),
211
+ limit: 1000,
212
+ });
213
+ attempts.push(...response.data);
214
+ cursor = response.pagination.next ?? undefined;
215
+ } while (cursor);
216
+
217
+ // mark any sleep steps as completed if their sleep duration has elapsed,
218
+ // or rethrow SleepSignal if still sleeping
219
+ for (let i = 0; i < attempts.length; i++) {
220
+ const attempt = attempts[i];
221
+ if (!attempt) continue;
222
+
223
+ if (
224
+ attempt.status === "running" &&
225
+ attempt.kind === "sleep" &&
226
+ attempt.context?.kind === "sleep"
227
+ ) {
228
+ const now = Date.now();
229
+ const resumeAt = new Date(attempt.context.resumeAt);
230
+ const resumeAtMs = resumeAt.getTime();
231
+
232
+ if (now < resumeAtMs) {
233
+ // sleep duration HAS NOT elapsed yet, throw signal to put workflow
234
+ // back to sleep
235
+ throw new SleepSignal(resumeAt);
236
+ }
237
+
238
+ // sleep duration HAS elapsed, mark the step as completed and continue
239
+ const completed = await backend.completeStepAttempt({
240
+ workflowRunId: workflowRun.id,
241
+ stepAttemptId: attempt.id,
242
+ workerId,
243
+ output: null,
244
+ });
245
+
246
+ // update cache w/ completed attempt
247
+ attempts[i] = completed;
248
+ }
249
+ }
250
+
251
+ // create step executor
252
+ const executor = new StepExecutor({
253
+ backend,
254
+ workflowRunId: workflowRun.id,
255
+ workerId,
256
+ attempts,
257
+ });
258
+
259
+ // execute workflow
260
+ const output = await workflowFn({
261
+ input: workflowRun.input as unknown,
262
+ step: executor,
263
+ version: workflowVersion,
264
+ });
265
+
266
+ // mark success
267
+ await backend.completeWorkflowRun({
268
+ workflowRunId: workflowRun.id,
269
+ workerId,
270
+ output: (output ?? null) as JsonValue,
271
+ });
272
+ } catch (error) {
273
+ // handle sleep signal by setting workflow to sleeping status
274
+ if (error instanceof SleepSignal) {
275
+ await backend.sleepWorkflowRun({
276
+ workflowRunId: workflowRun.id,
277
+ workerId,
278
+ availableAt: error.resumeAt,
279
+ });
280
+
281
+ return;
282
+ }
283
+
284
+ // mark failure
285
+ await backend.failWorkflowRun({
286
+ workflowRunId: workflowRun.id,
287
+ workerId,
288
+ error: serializeError(error),
289
+ });
290
+ }
291
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type { OpenWorkflowOptions } from "./client";
2
+ export { declareWorkflow, OpenWorkflow } from "./client";
3
+ export type { OpenWorkflowConfig, WorkerConfig } from "./config";
4
+ export { defineConfig } from "./config";
5
+ export { BackendPostgres } from "./database/backend";
6
+ export type { WorkerOptions } from "./worker";
7
+ export { Worker } from "./worker";
@@ -0,0 +1,11 @@
1
+ export * from "./backend";
2
+ export type * from "./client";
3
+ export { loadConfig } from "./config";
4
+ export type { DurationString } from "./core/duration";
5
+ export type { JsonValue } from "./core/json";
6
+ export { DEFAULT_RETRY_POLICY } from "./core/retry";
7
+ export type { StandardSchemaV1 } from "./core/schema";
8
+ export type { StepAttempt } from "./core/step";
9
+ export type { SchemaInput, SchemaOutput, WorkflowRun } from "./core/workflow";
10
+ export type { StepApi, WorkflowFunction } from "./execution";
11
+ export type { WorkflowSpec } from "./workflow";
@@ -0,0 +1,61 @@
1
+ import assert from "node:assert";
2
+ import { randomUUID } from "node:crypto";
3
+ import { BackendPostgres, OpenWorkflow } from "../";
4
+ import { KNEX_GLOBAL_CONFIG } from "../testing/connection";
5
+
6
+ let _backend: BackendPostgres | null = null;
7
+
8
+ async function getBackend(): Promise<BackendPostgres> {
9
+ if (_backend !== null) {
10
+ return _backend;
11
+ }
12
+
13
+ _backend = await BackendPostgres.connect(KNEX_GLOBAL_CONFIG, {
14
+ runMigrations: true,
15
+ namespaceId: randomUUID(),
16
+ });
17
+
18
+ return _backend;
19
+ }
20
+
21
+ async function practice() {
22
+ const backend = await getBackend();
23
+ const ow = new OpenWorkflow({ backend });
24
+
25
+ const sampleWorkflow = ow.defineWorkflow({ name: "sample-workflow" }, async ({ step }) => {
26
+ const { result: result1 } = await step.run({ name: "test-1" }, async () => {
27
+ return {
28
+ result: ["Result from test-1"],
29
+ };
30
+ });
31
+
32
+ await step.run({ name: "test-2" }, async () => {
33
+ return await new Promise<void>((resolve) => setTimeout(resolve, 1000));
34
+ });
35
+
36
+ return await step.run({ name: "test-3" }, async () => {
37
+ return {
38
+ result: [...result1, "Result from test-3"],
39
+ };
40
+ });
41
+ });
42
+
43
+ // create a worker that will listen to the channel and process the workflow runs
44
+ const worker = ow.newWorker({ concurrency: 3 });
45
+ await worker.start();
46
+
47
+ const handle = await sampleWorkflow.run();
48
+
49
+ await new Promise((resolve) => setTimeout(resolve, 3000));
50
+ await worker.tick();
51
+
52
+ const result = await handle.result();
53
+ assert.deepEqual(result, {
54
+ result: ["Result from test-1", "Result from test-3"],
55
+ });
56
+
57
+ await worker.stop();
58
+ await backend.stop();
59
+ }
60
+
61
+ await practice();
@@ -0,0 +1,122 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { WorkflowRegistry } from "./registry";
3
+ import { defineWorkflow } from "./workflow";
4
+
5
+ describe("WorkflowRegistry", () => {
6
+ describe("register", () => {
7
+ test("registers a workflow without version", () => {
8
+ const registry = new WorkflowRegistry();
9
+ const workflow = createMockWorkflow("my-workflow");
10
+
11
+ registry.register(workflow);
12
+
13
+ expect(registry.get("my-workflow", null)).toBe(workflow);
14
+ });
15
+
16
+ test("registers a workflow with version", () => {
17
+ const registry = new WorkflowRegistry();
18
+ const workflow = createMockWorkflow("my-workflow", "v1");
19
+
20
+ registry.register(workflow);
21
+
22
+ expect(registry.get("my-workflow", "v1")).toBe(workflow);
23
+ });
24
+
25
+ test("registers multiple versions of the same workflow", () => {
26
+ const registry = new WorkflowRegistry();
27
+ const v1 = createMockWorkflow("my-workflow", "v1");
28
+ const v2 = createMockWorkflow("my-workflow", "v2");
29
+
30
+ registry.register(v1);
31
+ registry.register(v2);
32
+
33
+ expect(registry.get("my-workflow", "v1")).toBe(v1);
34
+ expect(registry.get("my-workflow", "v2")).toBe(v2);
35
+ });
36
+
37
+ test("registers different workflows with same version", () => {
38
+ const registry = new WorkflowRegistry();
39
+ const workflow1 = createMockWorkflow("workflow-a", "v1");
40
+ const workflow2 = createMockWorkflow("workflow-b", "v1");
41
+
42
+ registry.register(workflow1);
43
+ registry.register(workflow2);
44
+
45
+ expect(registry.get("workflow-a", "v1")).toBe(workflow1);
46
+ expect(registry.get("workflow-b", "v1")).toBe(workflow2);
47
+ });
48
+
49
+ test("throws when registering duplicate unversioned workflow", () => {
50
+ const registry = new WorkflowRegistry();
51
+ registry.register(createMockWorkflow("my-workflow"));
52
+
53
+ expect(() => {
54
+ registry.register(createMockWorkflow("my-workflow"));
55
+ }).toThrow('Workflow "my-workflow" is already registered');
56
+ });
57
+
58
+ test("throws when registering duplicate versioned workflow", () => {
59
+ const registry = new WorkflowRegistry();
60
+ registry.register(createMockWorkflow("my-workflow", "v1"));
61
+
62
+ expect(() => {
63
+ registry.register(createMockWorkflow("my-workflow", "v1"));
64
+ }).toThrow('Workflow "my-workflow" (version: v1) is already registered');
65
+ });
66
+
67
+ test("allows same name with different versions", () => {
68
+ const registry = new WorkflowRegistry();
69
+ const versioned = createMockWorkflow("my-workflow", "v1");
70
+ const unversioned = createMockWorkflow("my-workflow");
71
+
72
+ registry.register(versioned);
73
+ registry.register(unversioned);
74
+
75
+ expect(registry.get("my-workflow", "v1")).toBe(versioned);
76
+ expect(registry.get("my-workflow", null)).toBe(unversioned);
77
+ });
78
+ });
79
+
80
+ describe("get", () => {
81
+ test("returns undefined for non-existent workflow", () => {
82
+ const registry = new WorkflowRegistry();
83
+
84
+ expect(registry.get("non-existent", null)).toBeUndefined();
85
+ });
86
+
87
+ test("returns undefined for wrong version", () => {
88
+ const registry = new WorkflowRegistry();
89
+ registry.register(createMockWorkflow("my-workflow", "v1"));
90
+
91
+ expect(registry.get("my-workflow", "v2")).toBeUndefined();
92
+ expect(registry.get("my-workflow", null)).toBeUndefined();
93
+ });
94
+
95
+ test("returns undefined for versioned lookup on unversioned workflow", () => {
96
+ const registry = new WorkflowRegistry();
97
+ registry.register(createMockWorkflow("my-workflow"));
98
+
99
+ expect(registry.get("my-workflow", "v1")).toBeUndefined();
100
+ });
101
+
102
+ test("returns the registered workflow", () => {
103
+ const registry = new WorkflowRegistry();
104
+ const workflow = createMockWorkflow("my-workflow");
105
+ registry.register(workflow);
106
+
107
+ expect(registry.get("my-workflow", null)).toBe(workflow);
108
+ });
109
+ });
110
+ });
111
+
112
+ function createMockWorkflow(name: string, version?: string) {
113
+ return defineWorkflow(
114
+ {
115
+ name,
116
+ ...(version && { version }),
117
+ },
118
+ async () => {
119
+ // no-op
120
+ },
121
+ );
122
+ }
@@ -0,0 +1,65 @@
1
+ import type { Workflow } from "./workflow";
2
+
3
+ /**
4
+ * A registry for storing and retrieving workflows by name and version.
5
+ * Provides a centralized way to manage workflow registrations.
6
+ */
7
+ export class WorkflowRegistry {
8
+ private readonly workflows = new Map<string, Workflow<unknown, unknown, unknown>>();
9
+
10
+ /**
11
+ * Register a workflow in the registry.
12
+ * @param workflow - The workflow to register
13
+ * @throws {Error} If a workflow with the same name and version is already registered
14
+ */
15
+ register(workflow: Workflow<unknown, unknown, unknown>): void {
16
+ const name = workflow.spec.name;
17
+ const version = workflow.spec.version ?? null;
18
+ const key = registryKey(name, version);
19
+ if (this.workflows.has(key)) {
20
+ const versionStr = version ? ` (version: ${version})` : "";
21
+ throw new Error(`Workflow "${name}"${versionStr} is already registered`);
22
+ }
23
+ this.workflows.set(key, workflow);
24
+ }
25
+
26
+ /**
27
+ * Get a workflow from the registry by name and version.
28
+ * @param name - The workflow name
29
+ * @param version - The workflow version (null for unversioned)
30
+ * @returns The workflow if found, undefined otherwise
31
+ */
32
+ get(name: string, version: string | null): Workflow<unknown, unknown, unknown> | undefined {
33
+ const key = registryKey(name, version);
34
+ return this.workflows.get(key);
35
+ }
36
+
37
+ /**
38
+ * Get a workflow from the registry by name and version.
39
+ * @param name - The workflow name
40
+ * @param version - The workflow version (null for unversioned)
41
+ * @returns The workflow if found, undefined otherwise
42
+ */
43
+ has(name: string, version: string | null): boolean {
44
+ const key = registryKey(name, version);
45
+ return this.workflows.has(key);
46
+ }
47
+
48
+ /**
49
+ * Remove a workflow from the registry by name and version.
50
+ */
51
+ remove(name: string, version: string | null): void {
52
+ const key = registryKey(name, version);
53
+ this.workflows.delete(key);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Build a registry key from name and version.
59
+ * @param name - Workflow name
60
+ * @param version - Workflow version (or null)
61
+ * @returns Registry key
62
+ */
63
+ function registryKey(name: string, version: string | null): string {
64
+ return version ? `${name}@${version}` : name;
65
+ }
@@ -0,0 +1,44 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import knex, { type Knex } from "knex";
3
+ import { BackendPostgres } from "../database/backend";
4
+ import { migrate as baseMigrate, DEFAULT_SCHEMA } from "../database/base";
5
+
6
+ let backend: BackendPostgres | null = null;
7
+
8
+ export const KNEX_GLOBAL_CONFIG: Knex.Config = {
9
+ client: "pg",
10
+ connection: {
11
+ host: "127.0.0.1",
12
+ port: 5432,
13
+ user: "postgres",
14
+ password: "miomock123",
15
+ database: "postgres",
16
+ },
17
+ pool: {
18
+ max: 50,
19
+ },
20
+ } as const;
21
+
22
+ export async function migrate(): Promise<void> {
23
+ await baseMigrate(knex(KNEX_GLOBAL_CONFIG), DEFAULT_SCHEMA);
24
+ }
25
+
26
+ export async function createBackend(): Promise<BackendPostgres> {
27
+ if (backend) {
28
+ return backend;
29
+ }
30
+
31
+ backend = await BackendPostgres.connect(KNEX_GLOBAL_CONFIG, {
32
+ namespaceId: randomUUID(),
33
+ });
34
+
35
+ return backend;
36
+ }
37
+
38
+ export async function stopBackend(): Promise<void> {
39
+ if (backend) {
40
+ await backend.stop();
41
+ }
42
+
43
+ backend = null;
44
+ }