@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
package/src/worker.ts ADDED
@@ -0,0 +1,281 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { Backend } from "./backend";
3
+ import type { WorkflowRun } from "./core/workflow";
4
+ import { executeWorkflow } from "./execution";
5
+ import type { WorkflowRegistry } from "./registry";
6
+ import type { Workflow } from "./workflow";
7
+
8
+ const DEFAULT_LEASE_DURATION_MS = 30 * 1000; // 30s
9
+ const DEFAULT_POLL_INTERVAL_MS = 100; // 100ms
10
+ const DEFAULT_CONCURRENCY = 1;
11
+
12
+ /**
13
+ * Configures how a Worker polls the backend, leases workflow runs, and
14
+ * registers workflows.
15
+ */
16
+ export interface WorkerOptions {
17
+ backend: Backend;
18
+ registry: WorkflowRegistry;
19
+ concurrency?: number | undefined;
20
+ usePubSub?: boolean;
21
+ listenDelay?: number;
22
+ }
23
+
24
+ /**
25
+ * Runs workflows by polling the backend, dispatching runs across a concurrency
26
+ * pool, and heartbeating/extending leases.
27
+ */
28
+ export class Worker {
29
+ private readonly backend: Backend;
30
+ private readonly workerIds: string[];
31
+ private readonly registry: WorkflowRegistry;
32
+ private readonly activeExecutions = new Set<WorkflowExecution>();
33
+ private running = false;
34
+ private loopPromise: Promise<void> | null = null;
35
+
36
+ private usePubSub: boolean;
37
+ private listenDelay: number;
38
+
39
+ constructor(options: WorkerOptions) {
40
+ this.backend = options.backend;
41
+ this.registry = options.registry;
42
+ this.usePubSub = options.usePubSub ?? true;
43
+ this.listenDelay = options.listenDelay ?? 500;
44
+
45
+ const concurrency = Math.max(DEFAULT_CONCURRENCY, options.concurrency ?? DEFAULT_CONCURRENCY);
46
+
47
+ // generate worker IDs for every concurrency slot
48
+ this.workerIds = Array.from({ length: concurrency }, () => randomUUID());
49
+ }
50
+
51
+ /**
52
+ * Start the worker. It will begin polling for and executing workflows.
53
+ * @returns Promise resolved when started
54
+ */
55
+ async start(): Promise<void> {
56
+ if (this.running) return;
57
+ this.running = true;
58
+ this.loopPromise = this.runLoop();
59
+ await Promise.resolve();
60
+ }
61
+
62
+ /**
63
+ * Stop the worker gracefully. Waits for all active workflow runs to complete
64
+ * before returning.
65
+ * @returns Promise resolved when stopped
66
+ */
67
+ async stop(): Promise<void> {
68
+ this.running = false;
69
+
70
+ // wait for the poll loop to stop
71
+ if (this.loopPromise) await this.loopPromise;
72
+
73
+ // wait for all active executions to finish
74
+ while (this.activeExecutions.size > 0) await sleep(100);
75
+ }
76
+
77
+ /**
78
+ * Processes one round of work claims and execution. Exposed for testing.
79
+ * Returns the number of workflow runs claimed.
80
+ * @returns Number of workflow runs claimed
81
+ */
82
+ async tick(): Promise<number> {
83
+ const availableSlots = this.concurrency - this.activeExecutions.size;
84
+ if (availableSlots <= 0) return 0;
85
+
86
+ // claim work for each available slot
87
+ const claims = Array.from({ length: availableSlots }, (_, i) => {
88
+ const availableWorkerId = this.workerIds[i % this.workerIds.length];
89
+ return availableWorkerId
90
+ ? this.claimAndProcessWorkflowRunInBackground(availableWorkerId)
91
+ : Promise.resolve(null);
92
+ });
93
+
94
+ const claimed = await Promise.all(claims);
95
+ return claimed.filter((run) => run !== null).length;
96
+ }
97
+
98
+ /**
99
+ * Get the configured concurrency limit.
100
+ * @returns Concurrency limit
101
+ */
102
+ private get concurrency(): number {
103
+ return this.workerIds.length;
104
+ }
105
+
106
+ /*
107
+ * Main run loop that continuously ticks while the worker is running.
108
+ * Only sleeps when no work was claimed to avoid busy-waiting.
109
+ */
110
+ private async runLoop(): Promise<void> {
111
+ if (this.usePubSub) {
112
+ this.backend.subscribe(async (result) => {
113
+ if (!result.ok) {
114
+ return;
115
+ }
116
+
117
+ await sleep(this.listenDelay);
118
+ await this.tick();
119
+ });
120
+ }
121
+
122
+ while (this.running) {
123
+ try {
124
+ const claimedCount = await this.tick();
125
+ // only sleep if we didn't claim any work
126
+ if (claimedCount === 0) {
127
+ await sleep(DEFAULT_POLL_INTERVAL_MS);
128
+ }
129
+ } catch (error) {
130
+ console.error("Worker tick failed:", error);
131
+ await sleep(DEFAULT_POLL_INTERVAL_MS);
132
+ }
133
+ }
134
+ }
135
+
136
+ /*
137
+ * Cclaim and process a workflow run for the given worker ID. Do not await the
138
+ * processing here to avoid blocking the caller.
139
+ * Returns the claimed workflow run, or null if none was available.
140
+ */
141
+ private async claimAndProcessWorkflowRunInBackground(
142
+ workerId: string,
143
+ ): Promise<WorkflowRun | null> {
144
+ // claim workflow run
145
+ const workflowRun = await this.backend.claimWorkflowRun({
146
+ workerId,
147
+ leaseDurationMs: DEFAULT_LEASE_DURATION_MS,
148
+ });
149
+ if (!workflowRun) return null;
150
+
151
+ const workflow = this.registry.get(workflowRun.workflowName, workflowRun.version);
152
+ if (!workflow) {
153
+ const versionStr = workflowRun.version ? ` (version: ${workflowRun.version})` : "";
154
+ await this.backend.failWorkflowRun({
155
+ workflowRunId: workflowRun.id,
156
+ workerId,
157
+ error: {
158
+ message: `Workflow "${workflowRun.workflowName}"${versionStr} is not registered`,
159
+ },
160
+ });
161
+ return null;
162
+ }
163
+
164
+ // create execution and start processing *async* w/o blocking
165
+ const execution = new WorkflowExecution({
166
+ backend: this.backend,
167
+ workflowRun,
168
+ workerId,
169
+ });
170
+ this.activeExecutions.add(execution);
171
+
172
+ this.processExecutionInBackground(execution, workflow)
173
+ .catch(() => {
174
+ // errors are already handled in processExecution
175
+ })
176
+ .finally(() => {
177
+ execution.stopHeartbeat();
178
+ this.activeExecutions.delete(execution);
179
+ });
180
+
181
+ return workflowRun;
182
+ }
183
+
184
+ /**
185
+ * Process a workflow execution, handling heartbeats, step execution, and
186
+ * marking success or failure.
187
+ * @param execution - Workflow execution
188
+ * @param workflow - Workflow to execute
189
+ * @returns Promise resolved when processing completes
190
+ */
191
+ private async processExecutionInBackground(
192
+ execution: WorkflowExecution,
193
+ workflow: Workflow<unknown, unknown, unknown>,
194
+ ): Promise<void> {
195
+ // start heartbeating
196
+ execution.startHeartbeat();
197
+
198
+ try {
199
+ await executeWorkflow({
200
+ backend: this.backend,
201
+ workflowRun: execution.workflowRun,
202
+ workflowFn: workflow.fn,
203
+ workflowVersion: execution.workflowRun.version,
204
+ workerId: execution.workerId,
205
+ });
206
+ } catch (error) {
207
+ // specifically for unexpected errors in the execution wrapper itself, not
208
+ // for business logic errors (those are handled inside executeWorkflow)
209
+ console.error(
210
+ `Critical error during workflow execution for run ${execution.workflowRun.id}:`,
211
+ error,
212
+ );
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Configures the options for a WorkflowExecution.
219
+ */
220
+ interface WorkflowExecutionOptions {
221
+ backend: Backend;
222
+ workflowRun: WorkflowRun;
223
+ workerId: string;
224
+ }
225
+
226
+ /**
227
+ * Tracks a claimed workflow run and maintains its heartbeat lease for the
228
+ * worker.
229
+ */
230
+ class WorkflowExecution {
231
+ private backend: Backend;
232
+ workflowRun: WorkflowRun;
233
+ workerId: string;
234
+ private heartbeatTimer: NodeJS.Timeout | null = null;
235
+
236
+ constructor(options: WorkflowExecutionOptions) {
237
+ this.backend = options.backend;
238
+ this.workflowRun = options.workflowRun;
239
+ this.workerId = options.workerId;
240
+ }
241
+
242
+ /**
243
+ * Start the heartbeat loop for this execution, heartbeating at half the lease
244
+ * duration.
245
+ */
246
+ startHeartbeat(): void {
247
+ const leaseDurationMs = DEFAULT_LEASE_DURATION_MS;
248
+ const heartbeatIntervalMs = leaseDurationMs / 2;
249
+
250
+ this.heartbeatTimer = setInterval(() => {
251
+ this.backend
252
+ .extendWorkflowRunLease({
253
+ workflowRunId: this.workflowRun.id,
254
+ workerId: this.workerId,
255
+ leaseDurationMs,
256
+ })
257
+ .catch((error: unknown) => {
258
+ console.error("Heartbeat failed:", error);
259
+ });
260
+ }, heartbeatIntervalMs);
261
+ }
262
+
263
+ /**
264
+ * Stop the heartbeat loop.
265
+ */
266
+ stopHeartbeat(): void {
267
+ if (this.heartbeatTimer) {
268
+ clearInterval(this.heartbeatTimer);
269
+ this.heartbeatTimer = null;
270
+ }
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Sleep for a given duration.
276
+ * @param ms - Milliseconds to sleep
277
+ * @returns Promise resolved after sleeping
278
+ */
279
+ function sleep(ms: number): Promise<void> {
280
+ return new Promise((resolve) => setTimeout(resolve, ms));
281
+ }
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { defineWorkflow, defineWorkflowSpec } from "./workflow";
3
+
4
+ describe("defineWorkflowSpec", () => {
5
+ test("returns spec (passthrough)", () => {
6
+ const spec = { name: "test-workflow" };
7
+ const definedSpec = defineWorkflowSpec(spec);
8
+
9
+ expect(definedSpec).toStrictEqual(spec);
10
+ });
11
+ });
12
+
13
+ describe("defineWorkflow", () => {
14
+ test("returns workflow with spec and fn", () => {
15
+ // eslint-disable-next-line unicorn/consistent-function-scoping
16
+ function fn() {
17
+ return { result: "done" };
18
+ }
19
+
20
+ const spec = { name: "test-workflow" };
21
+ const workflow = defineWorkflow(spec, fn);
22
+
23
+ expect(workflow).toStrictEqual({
24
+ spec,
25
+ fn,
26
+ });
27
+ });
28
+ });
29
+
30
+ // --- type checks below -------------------------------------------------------
31
+ // they're unused but useful to ensure that the types work as expected for both
32
+ // defineWorkflowSpec and defineWorkflow
33
+
34
+ const inferredTypesSpec = defineWorkflowSpec({
35
+ name: "inferred-types",
36
+ });
37
+
38
+ defineWorkflow(inferredTypesSpec, async ({ step }) => {
39
+ await step.run({ name: "step-1" }, () => {
40
+ return "success";
41
+ });
42
+
43
+ return { result: "done" };
44
+ });
45
+
46
+ const explicitInputTypeSpec = defineWorkflowSpec<{ name: string }>({
47
+ name: "explicit-input-type",
48
+ });
49
+
50
+ defineWorkflow(explicitInputTypeSpec, async ({ step }) => {
51
+ await step.run({ name: "step-1" }, () => {
52
+ return "success";
53
+ });
54
+
55
+ return { result: "done" };
56
+ });
57
+
58
+ const explicitInputAndOutputTypesSpec = defineWorkflowSpec<{ name: string }, { result: string }>({
59
+ name: "explicit-input-and-output-types",
60
+ });
61
+
62
+ defineWorkflow(explicitInputAndOutputTypesSpec, async ({ step }) => {
63
+ await step.run({ name: "step-1" }, () => {
64
+ return "success";
65
+ });
66
+
67
+ return { result: "done" };
68
+ });
@@ -0,0 +1,84 @@
1
+ import type { StandardSchemaV1 } from "./core/schema";
2
+ import type { WorkflowFunction } from "./execution";
3
+
4
+ export interface WorkflowSpec<Input, Output, RawInput> {
5
+ /** The name of the workflow. */
6
+ readonly name: string;
7
+ /** The version of the workflow. */
8
+ readonly version?: string;
9
+ /** The schema used to validate inputs. */
10
+ readonly schema?: StandardSchemaV1<RawInput, Input>;
11
+ /** Phantom type carrier - won't exist at runtime. */
12
+ readonly __types?: {
13
+ output: Output;
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Define a workflow spec.
19
+ * @param spec - The workflow spec
20
+ * @returns The workflow spec
21
+ */
22
+ export function defineWorkflowSpec<Input, Output = unknown, RawInput = Input>(
23
+ spec: WorkflowSpec<Input, Output, RawInput>,
24
+ ): WorkflowSpec<Input, Output, RawInput> {
25
+ return spec;
26
+ }
27
+
28
+ /**
29
+ * A workflow spec and implementation.
30
+ */
31
+ export interface Workflow<Input, Output, RawInput> {
32
+ /** The workflow spec. */
33
+ readonly spec: WorkflowSpec<Input, Output, RawInput>;
34
+ /** The workflow implementation function. */
35
+ readonly fn: WorkflowFunction<Input, Output>;
36
+ }
37
+
38
+ /**
39
+ * Define a workflow.
40
+ * @param spec - The workflow spec
41
+ * @param fn - The workflow implementation function
42
+ * @returns The workflow
43
+ */
44
+ // Handles:
45
+ // - `defineWorkflow(spec, fn)` (0 generics)
46
+ // - `defineWorkflow<Input, Output>(spec, fn)` (2 generics)
47
+ export function defineWorkflow<Input, Output, RawInput = Input>(
48
+ spec: WorkflowSpec<Input, Output, RawInput>,
49
+ fn: WorkflowFunction<Input, Output>,
50
+ ): Workflow<Input, Output, RawInput>;
51
+
52
+ /**
53
+ * Define a workflow.
54
+ * @param spec - The workflow spec
55
+ * @param fn - The workflow implementation function
56
+ * @returns The workflow
57
+ */
58
+ // Handles:
59
+ // - `defineWorkflow<Input>(spec, fn)` (1 generic)
60
+ export function defineWorkflow<
61
+ Input,
62
+ WorkflowFn extends WorkflowFunction<Input, unknown> = WorkflowFunction<Input, unknown>,
63
+ RawInput = Input,
64
+ >(
65
+ spec: WorkflowSpec<Input, Awaited<ReturnType<WorkflowFn>>, RawInput>,
66
+ fn: WorkflowFn,
67
+ ): Workflow<Input, Awaited<ReturnType<WorkflowFn>>, RawInput>;
68
+
69
+ /**
70
+ * Define a workflow.
71
+ * @internal
72
+ * @param spec - The workflow spec
73
+ * @param fn - The workflow implementation function
74
+ * @returns The workflow
75
+ */
76
+ export function defineWorkflow<Input, Output, RawInput>(
77
+ spec: WorkflowSpec<Input, Output, RawInput>,
78
+ fn: WorkflowFunction<Input, Output>,
79
+ ): Workflow<Input, Output, RawInput> {
80
+ return {
81
+ spec,
82
+ fn,
83
+ };
84
+ }
package/table_ddl.sql ADDED
@@ -0,0 +1,60 @@
1
+ -- TaskNode에서 처리할 Task Item 목록
2
+ CREATE TABLE sonamu_task_items (
3
+ id BINARY(16) PRIMARY KEY,
4
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
5
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
6
+
7
+ -- Task의 Namespace
8
+ namespace VARCHAR(255) NOT NULL,
9
+
10
+ -- pending, pending_for_retry
11
+ status VARCHAR(32) NOT NULL DEFAULT "pending",
12
+
13
+ -- Task의 시도 횟수를 상태로 저장
14
+ attempt INTEGER NOT NULL DEFAULT 1,
15
+
16
+ -- payload는 JSON이든 무엇이든 일단 BLOB으로 저장
17
+ payload MEDIUMBLOB NOT NULL
18
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
19
+
20
+ -- TaskNode에서 처리가 끝난 Task Item 목록
21
+ CREATE TABLE sonamu_archived_task_items (
22
+ id BINARY(16) PRIMARY KEY,
23
+ created_at TIMESTAMP NOT NULL,
24
+ completed_at TIMESTAMP NOT NULL,
25
+ namespace VARCHAR(255) NOT NULL,
26
+ payload MEDIUMBLOB NOT NULL,
27
+
28
+ -- Task가 몇번째 시도에 끝났는지 저장
29
+ attempt INTEGER NOT NULL DEFAULT 1,
30
+
31
+ -- error, completed
32
+ status VARCHAR(32) NOT NULL
33
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
34
+
35
+ CREATE TABLE sonamu_task_events (
36
+ id INT AUTO_INCREMENT PRIMARY KEY,
37
+ timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
38
+
39
+ -- Event의 종류 네임스페이스
40
+ -- start | stop | fetch | process:(start|error|complete)
41
+ event_type VARCHAR(255) NOT NULL,
42
+
43
+ -- TaskNode에서 자동 생성된 노드의 UUIDv7
44
+ node_id BINARY(16) NOT NULL,
45
+ -- 사용자가 지정한 노드 이름
46
+ node_name VARCHAR(64),
47
+
48
+ -- Task의 id
49
+ task_item_id BINARY(16),
50
+ -- Task의 당시 몇번째인지 기록해둠.
51
+ attempt INTEGER,
52
+
53
+ -- event_type이 stop일 때와 process:error:*일 때만 있음
54
+ -- TaskNode Stop: app_shutdown | process_signal | unknown
55
+ -- Process Error: no_route | serialization | timeout | max_retries_exceeded | exception
56
+ reason VARCHAR(32),
57
+
58
+ error_message VARCHAR(1000),
59
+ error_stack TEXT
60
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -0,0 +1,22 @@
1
+ import type { Knex } from "knex";
2
+ import { BackendPostgres, defineConfig } from "../src/index";
3
+
4
+ const config: Knex.Config = {
5
+ client: "pg",
6
+ connection: {
7
+ host: "localhost",
8
+ port: 51000,
9
+ user: "postgres",
10
+ password: "postgres",
11
+ database: "postgres",
12
+ },
13
+ } as const;
14
+
15
+ // Use Postgres (configured with Knex config)
16
+ const backend = await BackendPostgres.connect(config, {
17
+ runMigrations: false,
18
+ });
19
+
20
+ export default defineConfig({
21
+ backend,
22
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "rootDir": "src",
6
+ "outDir": "dist",
7
+ "sourceMap": true,
8
+ "lib": ["esnext"],
9
+ "types": ["node"],
10
+ "declaration": true,
11
+ "declarationMap": true,
12
+ "strict": true,
13
+ "noImplicitAny": true,
14
+ "strictNullChecks": true,
15
+ "strictFunctionTypes": true,
16
+ "strictBindCallApply": true,
17
+ "strictPropertyInitialization": true,
18
+ "noImplicitThis": true,
19
+ "alwaysStrict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "noImplicitReturns": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "skipLibCheck": true,
25
+ "moduleResolution": "bundler",
26
+ "esModuleInterop": true,
27
+ "forceConsistentCasingInFileNames": true,
28
+ "noErrorTruncation": true
29
+ },
30
+ "exclude": [
31
+ "node_modules",
32
+ "dist",
33
+ "src/**/*.ignore.ts",
34
+ "**/__mocks__/**",
35
+ "*.config.ts",
36
+ "templates/openworkflow.config.ts",
37
+ "scripts/**/*.ts",
38
+ "src/database/migrations/**/*.ts"
39
+ ]
40
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["src/**/*.test.ts"]
4
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ plugins: [],
5
+ test: {
6
+ include: ["src/**/*.test.ts"],
7
+ exclude: ["src/**/*.test-hold.ts", "**/node_modules/**", "**/.yarn/**", "**/dist/**"],
8
+ maxConcurrency: 4,
9
+ },
10
+ resolve: {
11
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".json", ".mjs", ".cjs", ".mts", ".cts"],
12
+ },
13
+ });