openworkflow 0.6.0 → 0.6.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 (97) hide show
  1. package/README.md +2 -0
  2. package/dist/bin/openworkflow.js +0 -0
  3. package/dist/internal.d.ts +0 -1
  4. package/dist/internal.d.ts.map +1 -1
  5. package/dist/internal.js +0 -1
  6. package/package.json +6 -2
  7. package/dist/backend-test/backend.testsuite.d.ts +0 -20
  8. package/dist/backend-test/backend.testsuite.d.ts.map +0 -1
  9. package/dist/backend-test/backend.testsuite.js +0 -1090
  10. package/dist/backend-test/index.d.ts +0 -2
  11. package/dist/backend-test/index.d.ts.map +0 -1
  12. package/dist/backend-test/index.js +0 -1
  13. package/dist/backend.testsuite.d.ts +0 -20
  14. package/dist/backend.testsuite.d.ts.map +0 -1
  15. package/dist/backend.testsuite.js +0 -1090
  16. package/dist/chaos.test.d.ts +0 -2
  17. package/dist/chaos.test.d.ts.map +0 -1
  18. package/dist/chaos.test.js +0 -88
  19. package/dist/client.test.d.ts +0 -2
  20. package/dist/client.test.d.ts.map +0 -1
  21. package/dist/client.test.js +0 -311
  22. package/dist/core/duration.test.d.ts +0 -2
  23. package/dist/core/duration.test.d.ts.map +0 -1
  24. package/dist/core/duration.test.js +0 -263
  25. package/dist/core/error.test.d.ts +0 -2
  26. package/dist/core/error.test.d.ts.map +0 -1
  27. package/dist/core/error.test.js +0 -60
  28. package/dist/core/result.test.d.ts +0 -2
  29. package/dist/core/result.test.d.ts.map +0 -1
  30. package/dist/core/result.test.js +0 -11
  31. package/dist/core/step.test.d.ts +0 -2
  32. package/dist/core/step.test.d.ts.map +0 -1
  33. package/dist/core/step.test.js +0 -266
  34. package/dist/core/workflow.test.d.ts +0 -2
  35. package/dist/core/workflow.test.d.ts.map +0 -1
  36. package/dist/core/workflow.test.js +0 -113
  37. package/dist/driver.d.ts +0 -116
  38. package/dist/driver.d.ts.map +0 -1
  39. package/dist/driver.js +0 -1
  40. package/dist/execution.test.d.ts +0 -2
  41. package/dist/execution.test.d.ts.map +0 -1
  42. package/dist/execution.test.js +0 -381
  43. package/dist/factory.d.ts +0 -74
  44. package/dist/factory.d.ts.map +0 -1
  45. package/dist/factory.js +0 -72
  46. package/dist/node-sqlite/backend.d.ts +0 -52
  47. package/dist/node-sqlite/backend.d.ts.map +0 -1
  48. package/dist/node-sqlite/backend.js +0 -673
  49. package/dist/node-sqlite/index.d.ts +0 -11
  50. package/dist/node-sqlite/index.d.ts.map +0 -1
  51. package/dist/node-sqlite/index.js +0 -7
  52. package/dist/node-sqlite/sqlite.d.ts +0 -60
  53. package/dist/node-sqlite/sqlite.d.ts.map +0 -1
  54. package/dist/node-sqlite/sqlite.js +0 -246
  55. package/dist/postgres/backend.test.d.ts +0 -2
  56. package/dist/postgres/backend.test.d.ts.map +0 -1
  57. package/dist/postgres/backend.test.js +0 -19
  58. package/dist/postgres/driver.d.ts +0 -81
  59. package/dist/postgres/driver.d.ts.map +0 -1
  60. package/dist/postgres/driver.js +0 -63
  61. package/dist/postgres/index.d.ts +0 -11
  62. package/dist/postgres/index.d.ts.map +0 -1
  63. package/dist/postgres/index.js +0 -7
  64. package/dist/postgres/internal.d.ts +0 -2
  65. package/dist/postgres/internal.d.ts.map +0 -1
  66. package/dist/postgres/internal.js +0 -1
  67. package/dist/postgres/postgres.test.d.ts +0 -2
  68. package/dist/postgres/postgres.test.d.ts.map +0 -1
  69. package/dist/postgres/postgres.test.js +0 -45
  70. package/dist/postgres/vitest.global-setup.d.ts +0 -3
  71. package/dist/postgres/vitest.global-setup.d.ts.map +0 -1
  72. package/dist/postgres/vitest.global-setup.js +0 -7
  73. package/dist/registry.test.d.ts +0 -2
  74. package/dist/registry.test.d.ts.map +0 -1
  75. package/dist/registry.test.js +0 -109
  76. package/dist/sqlite/backend.test.d.ts +0 -2
  77. package/dist/sqlite/backend.test.d.ts.map +0 -1
  78. package/dist/sqlite/backend.test.js +0 -50
  79. package/dist/sqlite/driver.d.ts +0 -79
  80. package/dist/sqlite/driver.d.ts.map +0 -1
  81. package/dist/sqlite/driver.js +0 -62
  82. package/dist/sqlite/index.d.ts +0 -13
  83. package/dist/sqlite/index.d.ts.map +0 -1
  84. package/dist/sqlite/index.js +0 -11
  85. package/dist/sqlite/internal.d.ts +0 -2
  86. package/dist/sqlite/internal.d.ts.map +0 -1
  87. package/dist/sqlite/internal.js +0 -1
  88. package/dist/sqlite/sqlite.test.d.ts +0 -2
  89. package/dist/sqlite/sqlite.test.d.ts.map +0 -1
  90. package/dist/sqlite/sqlite.test.js +0 -171
  91. package/dist/tsconfig.tsbuildinfo +0 -1
  92. package/dist/worker.test.d.ts +0 -2
  93. package/dist/worker.test.d.ts.map +0 -1
  94. package/dist/worker.test.js +0 -900
  95. package/dist/workflow.test.d.ts +0 -2
  96. package/dist/workflow.test.d.ts.map +0 -1
  97. package/dist/workflow.test.js +0 -84
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=chaos.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"chaos.test.d.ts","sourceRoot":"","sources":["../chaos.test.ts"],"names":[],"mappings":""}
@@ -1,88 +0,0 @@
1
- import { OpenWorkflow } from "./client.js";
2
- import { BackendPostgres } from "./postgres.js";
3
- import { DEFAULT_POSTGRES_URL } from "./postgres/postgres.js";
4
- import { randomInt, randomUUID } from "node:crypto";
5
- import { describe, expect, test } from "vitest";
6
- const TOTAL_STEPS = 50;
7
- const WORKER_COUNT = 3;
8
- const WORKER_CONCURRENCY = 2;
9
- const STEP_DURATION_MS = 25;
10
- const CHAOS_DURATION_MS = 5000;
11
- const CHAOS_INTERVAL_MS = 200;
12
- const TEST_TIMEOUT_MS = 30_000;
13
- describe("chaos test", () => {
14
- test("workflow completes despite random worker deaths", async () => {
15
- const backend = await createBackend();
16
- const client = new OpenWorkflow({ backend });
17
- const workflow = client.defineWorkflow({ name: "chaos-workflow" }, async ({ step }) => {
18
- const results = [];
19
- for (let i = 0; i < TOTAL_STEPS; i++) {
20
- const stepName = `step-${i.toString()}`;
21
- const result = await step.run({ name: stepName }, async () => {
22
- await sleep(STEP_DURATION_MS); // fake work
23
- return i;
24
- });
25
- results.push(result);
26
- }
27
- return results;
28
- });
29
- const workers = await Promise.all(Array.from({ length: WORKER_COUNT }, () => createAndStartWorker(client)));
30
- const handle = await workflow.run();
31
- let workflowCompleted = false;
32
- let chaosTask = null;
33
- try {
34
- chaosTask = runChaosTest({
35
- client,
36
- workers,
37
- durationMs: CHAOS_DURATION_MS,
38
- intervalMs: CHAOS_INTERVAL_MS,
39
- shouldStop: () => workflowCompleted,
40
- });
41
- const result = await handle.result();
42
- workflowCompleted = true;
43
- const restarts = await chaosTask;
44
- expect(result).toHaveLength(TOTAL_STEPS);
45
- expect(result[TOTAL_STEPS - 1]).toBe(TOTAL_STEPS - 1);
46
- expect(restarts).toBeGreaterThan(0);
47
- }
48
- finally {
49
- workflowCompleted = true;
50
- if (chaosTask)
51
- await chaosTask;
52
- await Promise.all(workers.map((worker) => worker.stop()));
53
- await backend.stop();
54
- }
55
- }, TEST_TIMEOUT_MS);
56
- });
57
- async function runChaosTest(params) {
58
- const { client, workers, durationMs, intervalMs, shouldStop } = params;
59
- const chaosEndsAt = Date.now() + durationMs;
60
- let restartCount = 0;
61
- while (Date.now() < chaosEndsAt && !shouldStop()) {
62
- await sleep(intervalMs);
63
- if (workers.length === 0) {
64
- workers.push(await createAndStartWorker(client));
65
- continue;
66
- }
67
- const index = randomInt(workers.length);
68
- const victim = workers.splice(index, 1)[0];
69
- await victim?.stop();
70
- const replacement = await createAndStartWorker(client);
71
- workers.push(replacement);
72
- restartCount++;
73
- }
74
- return restartCount;
75
- }
76
- async function createBackend() {
77
- return await BackendPostgres.connect(DEFAULT_POSTGRES_URL, {
78
- namespaceId: randomUUID(),
79
- });
80
- }
81
- async function createAndStartWorker(client) {
82
- const worker = client.newWorker({ concurrency: WORKER_CONCURRENCY });
83
- await worker.start();
84
- return worker;
85
- }
86
- function sleep(ms) {
87
- return new Promise((resolve) => setTimeout(resolve, ms));
88
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=client.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../client.test.ts"],"names":[],"mappings":""}
@@ -1,311 +0,0 @@
1
- import { OpenWorkflow } from "./client.js";
2
- import { BackendPostgres } from "./postgres.js";
3
- import { DEFAULT_POSTGRES_URL } from "./postgres/postgres.js";
4
- import { defineWorkflowSpec } from "./workflow.js";
5
- import { type as arkType } from "arktype";
6
- import { randomUUID } from "node:crypto";
7
- import * as v from "valibot";
8
- import { describe, expect, test } from "vitest";
9
- import { number as yupNumber, object as yupObject, string as yupString, } from "yup";
10
- import { z } from "zod";
11
- describe("OpenWorkflow", () => {
12
- test("enqueues workflow runs via backend", async () => {
13
- const backend = await createBackend();
14
- const client = new OpenWorkflow({ backend });
15
- const workflow = client.defineWorkflow({ name: "enqueue-test" }, noopFn);
16
- await workflow.run({ docUrl: "https://example.com" });
17
- const workerId = "enqueue-worker";
18
- const claimed = await backend.claimWorkflowRun({
19
- workerId,
20
- leaseDurationMs: 1000,
21
- });
22
- expect(claimed?.workflowName).toBe("enqueue-test");
23
- expect(claimed?.workerId).toBe(workerId);
24
- expect(claimed?.input).toEqual({ docUrl: "https://example.com" });
25
- });
26
- describe("schema validation", () => {
27
- describe("Zod schema", () => {
28
- const schema = z.object({
29
- userId: z.uuid(),
30
- count: z.number().int().positive(),
31
- });
32
- test("accepts valid input", async () => {
33
- const backend = await createBackend();
34
- const client = new OpenWorkflow({ backend });
35
- const workflow = client.defineWorkflow({ name: "schema-zod-valid", schema }, noopFn);
36
- const handle = await workflow.run({
37
- userId: randomUUID(),
38
- count: 3,
39
- });
40
- await handle.cancel();
41
- });
42
- test("rejects invalid input", async () => {
43
- const backend = await createBackend();
44
- const client = new OpenWorkflow({ backend });
45
- const workflow = client.defineWorkflow({ name: "schema-zod-invalid", schema }, noopFn);
46
- await expect(workflow.run({ userId: "not-a-uuid", count: 0 })).rejects.toThrow();
47
- });
48
- });
49
- describe("ArkType schema", () => {
50
- const schema = arkType({
51
- name: "string",
52
- platform: "'android' | 'ios'",
53
- });
54
- test("accepts valid input", async () => {
55
- const backend = await createBackend();
56
- const client = new OpenWorkflow({ backend });
57
- const workflow = client.defineWorkflow({ name: "schema-arktype-valid", schema }, noopFn);
58
- const handle = await workflow.run({
59
- name: "Riley",
60
- platform: "android",
61
- });
62
- await handle.cancel();
63
- });
64
- test("rejects invalid input", async () => {
65
- const backend = await createBackend();
66
- const client = new OpenWorkflow({ backend });
67
- const workflow = client.defineWorkflow({ name: "schema-arktype-invalid", schema }, noopFn);
68
- await expect(workflow.run({ name: "Riley", platform: "web" })).rejects.toThrow();
69
- });
70
- });
71
- describe("Valibot schema", () => {
72
- const schema = v.object({
73
- key1: v.string(),
74
- key2: v.number(),
75
- });
76
- test("accepts valid input", async () => {
77
- const backend = await createBackend();
78
- const client = new OpenWorkflow({ backend });
79
- const workflow = client.defineWorkflow({ name: "schema-valibot-valid", schema }, noopFn);
80
- const handle = await workflow.run({
81
- key1: "value",
82
- key2: 42,
83
- });
84
- await handle.cancel();
85
- });
86
- test("rejects invalid input", async () => {
87
- const backend = await createBackend();
88
- const client = new OpenWorkflow({ backend });
89
- const workflow = client.defineWorkflow({ name: "schema-valibot-invalid", schema }, noopFn);
90
- await expect(workflow.run({ key1: "value", key2: "oops" })).rejects.toThrow();
91
- });
92
- });
93
- describe("Yup schema", () => {
94
- const schema = yupObject({
95
- name: yupString().required(),
96
- age: yupNumber().required().integer().positive(),
97
- });
98
- test("accepts valid input", async () => {
99
- const backend = await createBackend();
100
- const client = new OpenWorkflow({ backend });
101
- const workflow = client.defineWorkflow({ name: "schema-yup-valid", schema }, noopFn);
102
- const handle = await workflow.run({
103
- name: "Mona",
104
- age: 32,
105
- });
106
- await handle.cancel();
107
- });
108
- test("rejects invalid input", async () => {
109
- const backend = await createBackend();
110
- const client = new OpenWorkflow({ backend });
111
- const workflow = client.defineWorkflow({ name: "schema-yup-invalid", schema }, noopFn);
112
- await expect(workflow.run({ name: "Mona", age: -10 })).rejects.toThrow();
113
- });
114
- });
115
- });
116
- test("result resolves when workflow succeeds", async () => {
117
- const backend = await createBackend();
118
- const client = new OpenWorkflow({ backend });
119
- const workflow = client.defineWorkflow({ name: "result-success" }, noopFn);
120
- const handle = await workflow.run({ value: 1 });
121
- const workerId = "test-worker";
122
- const claimed = await backend.claimWorkflowRun({
123
- workerId,
124
- leaseDurationMs: 1000,
125
- });
126
- expect(claimed).not.toBeNull();
127
- if (!claimed)
128
- throw new Error("workflow run was not claimed");
129
- await backend.completeWorkflowRun({
130
- workflowRunId: claimed.id,
131
- workerId,
132
- output: { ok: true },
133
- });
134
- // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
135
- const result = await handle.result();
136
- expect(result).toEqual({ ok: true });
137
- });
138
- test("result rejects when workflow fails", async () => {
139
- const backend = await createBackend();
140
- const client = new OpenWorkflow({ backend });
141
- const workflow = client.defineWorkflow({ name: "result-failure" }, noopFn);
142
- await workflow.run({ value: 1 });
143
- const workerId = "test-worker";
144
- const claimed = await backend.claimWorkflowRun({
145
- workerId,
146
- leaseDurationMs: 1000,
147
- });
148
- expect(claimed).not.toBeNull();
149
- if (!claimed)
150
- throw new Error("workflow run was not claimed");
151
- // mark as failed (should reschedule))
152
- await backend.failWorkflowRun({
153
- workflowRunId: claimed.id,
154
- workerId,
155
- error: { message: "boom" },
156
- });
157
- const rescheduled = await backend.getWorkflowRun({
158
- workflowRunId: claimed.id,
159
- });
160
- expect(rescheduled?.status).toBe("pending");
161
- expect(rescheduled?.error).toEqual({ message: "boom" });
162
- });
163
- test("creates workflow run with deadline", async () => {
164
- const backend = await createBackend();
165
- const client = new OpenWorkflow({ backend });
166
- const workflow = client.defineWorkflow({ name: "deadline-test" }, noopFn);
167
- const deadline = new Date(Date.now() + 60_000); // in 1 minute
168
- const handle = await workflow.run({ value: 1 }, { deadlineAt: deadline });
169
- expect(handle.workflowRun.deadlineAt).not.toBeNull();
170
- expect(handle.workflowRun.deadlineAt?.getTime()).toBe(deadline.getTime());
171
- });
172
- test("creates workflow run with version", async () => {
173
- const backend = await createBackend();
174
- const client = new OpenWorkflow({ backend });
175
- const workflow = client.defineWorkflow({ name: "versioned-test", version: "v2.0" }, noopFn);
176
- const handle = await workflow.run({ value: 1 });
177
- expect(handle.workflowRun.version).toBe("v2.0");
178
- });
179
- test("creates workflow run without version", async () => {
180
- const backend = await createBackend();
181
- const client = new OpenWorkflow({ backend });
182
- const workflow = client.defineWorkflow({ name: "unversioned-test" }, noopFn);
183
- const handle = await workflow.run({ value: 1 });
184
- expect(handle.workflowRun.version).toBeNull();
185
- });
186
- test("cancels workflow run via handle", async () => {
187
- const backend = await createBackend();
188
- const client = new OpenWorkflow({ backend });
189
- const workflow = client.defineWorkflow({ name: "cancel-test" }, noopFn);
190
- const handle = await workflow.run({ value: 1 });
191
- await handle.cancel();
192
- const workflowRun = await backend.getWorkflowRun({
193
- workflowRunId: handle.workflowRun.id,
194
- });
195
- expect(workflowRun?.status).toBe("canceled");
196
- expect(workflowRun?.finishedAt).not.toBeNull();
197
- });
198
- describe("defineWorkflowSpec / implementWorkflow API", () => {
199
- test("defineWorkflowSpec returns a spec that can be used to schedule runs", async () => {
200
- const backend = await createBackend();
201
- const client = new OpenWorkflow({ backend });
202
- const spec = defineWorkflowSpec({ name: "declare-test" });
203
- const handle = await client.runWorkflow(spec, { message: "hello" });
204
- expect(handle.workflowRun.workflowName).toBe("declare-test");
205
- await handle.cancel();
206
- });
207
- test("implementWorkflow registers the workflow for worker execution", async () => {
208
- const backend = await createBackend();
209
- const client = new OpenWorkflow({ backend });
210
- const spec = defineWorkflowSpec({ name: "implement-test" });
211
- client.implementWorkflow(spec, ({ input }) => {
212
- return { received: input };
213
- });
214
- const handle = await client.runWorkflow(spec, { data: 42 });
215
- const worker = client.newWorker();
216
- await worker.tick();
217
- await sleep(100); // wait for background execution
218
- const result = await handle.result();
219
- expect(result).toEqual({ received: { data: 42 } });
220
- });
221
- test("implementWorkflow throws when workflow is already registered", async () => {
222
- const backend = await createBackend();
223
- const client = new OpenWorkflow({ backend });
224
- const spec = defineWorkflowSpec({ name: "duplicate-test" });
225
- client.implementWorkflow(spec, noopFn);
226
- expect(() => {
227
- client.implementWorkflow(spec, noopFn);
228
- }).toThrow('Workflow "duplicate-test" is already registered');
229
- });
230
- test("implementWorkflow allows registering different versions of the same workflow", async () => {
231
- const backend = await createBackend();
232
- const client = new OpenWorkflow({ backend });
233
- const specV1 = defineWorkflowSpec({
234
- name: "multi-version",
235
- version: "v1",
236
- });
237
- const specV2 = defineWorkflowSpec({
238
- name: "multi-version",
239
- version: "v2",
240
- });
241
- // no throwing...
242
- client.implementWorkflow(specV1, noopFn);
243
- client.implementWorkflow(specV2, noopFn);
244
- });
245
- test("implementWorkflow throws for same name+version combination", async () => {
246
- const backend = await createBackend();
247
- const client = new OpenWorkflow({ backend });
248
- const spec1 = defineWorkflowSpec({
249
- name: "version-duplicate",
250
- version: "v1",
251
- });
252
- const spec2 = defineWorkflowSpec({
253
- name: "version-duplicate",
254
- version: "v1",
255
- });
256
- client.implementWorkflow(spec1, noopFn);
257
- expect(() => {
258
- client.implementWorkflow(spec2, noopFn);
259
- }).toThrow('Workflow "version-duplicate" (version: v1) is already registered');
260
- });
261
- test("defineWorkflowSpec with schema validates input on runWorkflow", async () => {
262
- const backend = await createBackend();
263
- const client = new OpenWorkflow({ backend });
264
- const schema = z.object({
265
- email: z.email(),
266
- });
267
- const spec = defineWorkflowSpec({
268
- name: "declare-schema-test",
269
- schema,
270
- });
271
- const handle = await client.runWorkflow(spec, {
272
- email: "test@example.com",
273
- });
274
- await handle.cancel();
275
- await expect(client.runWorkflow(spec, { email: "not-an-email" })).rejects.toThrow();
276
- });
277
- test("defineWorkflowSpec with version sets version on workflow run", async () => {
278
- const backend = await createBackend();
279
- const client = new OpenWorkflow({ backend });
280
- const spec = defineWorkflowSpec({
281
- name: "declare-version-test",
282
- version: "v1.2.3",
283
- });
284
- const handle = await client.runWorkflow(spec);
285
- expect(handle.workflowRun.version).toBe("v1.2.3");
286
- await handle.cancel();
287
- });
288
- test("defineWorkflow defines a workflow", async () => {
289
- const backend = await createBackend();
290
- const client = new OpenWorkflow({ backend });
291
- const workflow = client.defineWorkflow({ name: "define-wrap-test" }, ({ input }) => ({ doubled: input.n * 2 }));
292
- const handle = await workflow.run({ n: 21 });
293
- const worker = client.newWorker();
294
- await worker.tick();
295
- await sleep(100); // wait for background execution
296
- const result = await handle.result();
297
- expect(result).toEqual({ doubled: 42 });
298
- });
299
- });
300
- });
301
- async function createBackend() {
302
- return await BackendPostgres.connect(DEFAULT_POSTGRES_URL, {
303
- namespaceId: randomUUID(), // unique namespace per test
304
- });
305
- }
306
- function sleep(ms) {
307
- return new Promise((resolve) => setTimeout(resolve, ms));
308
- }
309
- async function noopFn() {
310
- // no-op
311
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=duration.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"duration.test.d.ts","sourceRoot":"","sources":["../../core/duration.test.ts"],"names":[],"mappings":""}