openworkflow 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/README.md +43 -345
  2. package/dist/backend-test/backend.testsuite.d.ts +20 -0
  3. package/dist/backend-test/backend.testsuite.d.ts.map +1 -0
  4. package/dist/{core → backend-test}/backend.testsuite.js +191 -59
  5. package/dist/backend-test/index.d.ts +2 -0
  6. package/dist/backend-test/index.d.ts.map +1 -0
  7. package/dist/backend-test/index.js +1 -0
  8. package/dist/{core/backend.d.ts → backend.d.ts} +7 -5
  9. package/dist/backend.d.ts.map +1 -0
  10. package/dist/{core/backend.js → backend.js} +0 -1
  11. package/dist/backend.testsuite.d.ts +20 -0
  12. package/dist/backend.testsuite.d.ts.map +1 -0
  13. package/dist/{core/backend-test-suite.js → backend.testsuite.js} +301 -171
  14. package/dist/bin/openworkflow.d.ts +3 -0
  15. package/dist/bin/openworkflow.d.ts.map +1 -0
  16. package/dist/bin/openworkflow.js +43 -0
  17. package/dist/chaos.test.d.ts +2 -0
  18. package/dist/chaos.test.d.ts.map +1 -0
  19. package/dist/chaos.test.js +88 -0
  20. package/dist/client.d.ts +141 -0
  21. package/dist/client.d.ts.map +1 -0
  22. package/dist/{sdk/sdk.js → client.js} +43 -71
  23. package/dist/client.test.d.ts +2 -0
  24. package/dist/client.test.d.ts.map +1 -0
  25. package/dist/{sdk/sdk.test.js → client.test.js} +130 -14
  26. package/dist/core/duration.d.ts +4 -2
  27. package/dist/core/duration.d.ts.map +1 -1
  28. package/dist/core/duration.js +3 -2
  29. package/dist/core/duration.test.js +0 -1
  30. package/dist/core/error.d.ts +14 -0
  31. package/dist/core/error.d.ts.map +1 -0
  32. package/dist/core/error.js +17 -0
  33. package/dist/core/error.test.d.ts +2 -0
  34. package/dist/core/error.test.d.ts.map +1 -0
  35. package/dist/core/error.test.js +60 -0
  36. package/dist/core/json.js +0 -1
  37. package/dist/core/result.d.ts +14 -4
  38. package/dist/core/result.d.ts.map +1 -1
  39. package/dist/core/result.js +10 -1
  40. package/dist/core/result.test.js +2 -2
  41. package/dist/core/retry.d.ts +0 -9
  42. package/dist/core/retry.d.ts.map +1 -1
  43. package/dist/core/retry.js +0 -15
  44. package/dist/core/schema.js +0 -1
  45. package/dist/core/step.d.ts +1 -32
  46. package/dist/core/step.d.ts.map +1 -1
  47. package/dist/core/step.js +0 -36
  48. package/dist/core/step.test.js +1 -75
  49. package/dist/core/workflow.d.ts +2 -47
  50. package/dist/core/workflow.d.ts.map +1 -1
  51. package/dist/core/workflow.js +0 -45
  52. package/dist/core/workflow.test.js +1 -104
  53. package/dist/driver.d.ts +116 -0
  54. package/dist/driver.d.ts.map +1 -0
  55. package/dist/driver.js +1 -0
  56. package/dist/{execution/execution.d.ts → execution.d.ts} +4 -26
  57. package/dist/execution.d.ts.map +1 -0
  58. package/dist/{execution/execution.js → execution.js} +4 -5
  59. package/dist/execution.test.d.ts.map +1 -0
  60. package/dist/{execution/execution.test.js → execution.test.js} +4 -5
  61. package/dist/factory.d.ts +74 -0
  62. package/dist/factory.d.ts.map +1 -0
  63. package/dist/factory.js +72 -0
  64. package/dist/index.d.ts +6 -9
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +4 -5
  67. package/dist/internal.d.ts +7 -0
  68. package/dist/internal.d.ts.map +1 -0
  69. package/dist/internal.js +2 -0
  70. package/dist/node-sqlite/backend.d.ts +52 -0
  71. package/dist/node-sqlite/backend.d.ts.map +1 -0
  72. package/dist/node-sqlite/backend.js +673 -0
  73. package/dist/node-sqlite/index.d.ts +11 -0
  74. package/dist/node-sqlite/index.d.ts.map +1 -0
  75. package/dist/node-sqlite/index.js +7 -0
  76. package/dist/node-sqlite/sqlite.d.ts +60 -0
  77. package/dist/node-sqlite/sqlite.d.ts.map +1 -0
  78. package/dist/{backend-sqlite → node-sqlite}/sqlite.js +20 -3
  79. package/dist/postgres/backend.d.ts +44 -0
  80. package/dist/postgres/backend.d.ts.map +1 -0
  81. package/dist/postgres/backend.js +534 -0
  82. package/dist/postgres/backend.test.d.ts +2 -0
  83. package/dist/postgres/backend.test.d.ts.map +1 -0
  84. package/dist/postgres/backend.test.js +19 -0
  85. package/dist/postgres/driver.d.ts +81 -0
  86. package/dist/postgres/driver.d.ts.map +1 -0
  87. package/dist/postgres/driver.js +63 -0
  88. package/dist/postgres/index.d.ts +11 -0
  89. package/dist/postgres/index.d.ts.map +1 -0
  90. package/dist/postgres/index.js +7 -0
  91. package/dist/postgres/internal.d.ts +2 -0
  92. package/dist/postgres/internal.d.ts.map +1 -0
  93. package/dist/postgres/internal.js +1 -0
  94. package/dist/postgres/postgres.d.ts +42 -0
  95. package/dist/postgres/postgres.d.ts.map +1 -0
  96. package/dist/postgres/postgres.js +233 -0
  97. package/dist/postgres/postgres.test.d.ts +2 -0
  98. package/dist/postgres/postgres.test.d.ts.map +1 -0
  99. package/dist/postgres/postgres.test.js +45 -0
  100. package/dist/postgres/scripts/db-migrate.d.ts +2 -0
  101. package/dist/postgres/scripts/db-migrate.d.ts.map +1 -0
  102. package/dist/postgres/scripts/db-migrate.js +4 -0
  103. package/dist/postgres/scripts/db-reset.d.ts +2 -0
  104. package/dist/postgres/scripts/db-reset.d.ts.map +1 -0
  105. package/dist/postgres/scripts/db-reset.js +5 -0
  106. package/dist/postgres/scripts/squawk.d.ts +2 -0
  107. package/dist/postgres/scripts/squawk.d.ts.map +1 -0
  108. package/dist/postgres/scripts/squawk.js +16 -0
  109. package/dist/postgres/vitest.global-setup.d.ts +3 -0
  110. package/dist/postgres/vitest.global-setup.d.ts.map +1 -0
  111. package/dist/postgres/vitest.global-setup.js +7 -0
  112. package/dist/postgres.d.ts +2 -0
  113. package/dist/postgres.d.ts.map +1 -0
  114. package/dist/postgres.js +1 -0
  115. package/dist/registry.d.ts +27 -0
  116. package/dist/registry.d.ts.map +1 -0
  117. package/dist/registry.js +48 -0
  118. package/dist/registry.test.d.ts +2 -0
  119. package/dist/registry.test.d.ts.map +1 -0
  120. package/dist/registry.test.js +109 -0
  121. package/dist/{backend-sqlite → sqlite}/backend.d.ts +8 -4
  122. package/dist/sqlite/backend.d.ts.map +1 -0
  123. package/dist/{backend-sqlite → sqlite}/backend.js +35 -9
  124. package/dist/sqlite/backend.test.d.ts +2 -0
  125. package/dist/sqlite/backend.test.d.ts.map +1 -0
  126. package/dist/sqlite/backend.test.js +50 -0
  127. package/dist/sqlite/driver.d.ts +79 -0
  128. package/dist/sqlite/driver.d.ts.map +1 -0
  129. package/dist/sqlite/driver.js +62 -0
  130. package/dist/sqlite/index.d.ts +13 -0
  131. package/dist/sqlite/index.d.ts.map +1 -0
  132. package/dist/sqlite/index.js +11 -0
  133. package/dist/sqlite/internal.d.ts +2 -0
  134. package/dist/sqlite/internal.d.ts.map +1 -0
  135. package/dist/sqlite/internal.js +1 -0
  136. package/dist/{backend-sqlite → sqlite}/sqlite.d.ts +18 -2
  137. package/dist/sqlite/sqlite.d.ts.map +1 -0
  138. package/dist/sqlite/sqlite.js +246 -0
  139. package/dist/sqlite/sqlite.test.d.ts +2 -0
  140. package/dist/sqlite/sqlite.test.d.ts.map +1 -0
  141. package/dist/sqlite/sqlite.test.js +171 -0
  142. package/dist/sqlite.d.ts +2 -0
  143. package/dist/sqlite.d.ts.map +1 -0
  144. package/dist/sqlite.js +1 -0
  145. package/dist/tsconfig.tsbuildinfo +1 -1
  146. package/dist/{worker/worker.d.ts → worker.d.ts} +11 -4
  147. package/dist/worker.d.ts.map +1 -0
  148. package/dist/{worker/worker.js → worker.js} +20 -11
  149. package/dist/{worker/worker.test.d.ts.map → worker.test.d.ts.map} +1 -1
  150. package/dist/{worker/worker.test.js → worker.test.js} +136 -22
  151. package/dist/workflow.d.ts +60 -0
  152. package/dist/workflow.d.ts.map +1 -0
  153. package/dist/workflow.js +48 -0
  154. package/dist/workflow.test.d.ts +2 -0
  155. package/dist/workflow.test.d.ts.map +1 -0
  156. package/dist/workflow.test.js +84 -0
  157. package/package.json +28 -4
  158. package/dist/backend-sqlite/backend.d.ts.map +0 -1
  159. package/dist/backend-sqlite/backend.js.map +0 -1
  160. package/dist/backend-sqlite/index.d.ts +0 -2
  161. package/dist/backend-sqlite/index.d.ts.map +0 -1
  162. package/dist/backend-sqlite/index.js +0 -2
  163. package/dist/backend-sqlite/index.js.map +0 -1
  164. package/dist/backend-sqlite/sqlite.d.ts.map +0 -1
  165. package/dist/backend-sqlite/sqlite.js.map +0 -1
  166. package/dist/config/config.d.ts +0 -102
  167. package/dist/config/config.d.ts.map +0 -1
  168. package/dist/config/config.js +0 -29
  169. package/dist/config/config.js.map +0 -1
  170. package/dist/config/index.d.ts +0 -3
  171. package/dist/config/index.d.ts.map +0 -1
  172. package/dist/config/index.js +0 -2
  173. package/dist/config/index.js.map +0 -1
  174. package/dist/config.d.ts +0 -28
  175. package/dist/config.d.ts.map +0 -1
  176. package/dist/config.js +0 -41
  177. package/dist/config.js.map +0 -1
  178. package/dist/core/backend-test-suite.d.ts +0 -22
  179. package/dist/core/backend-test-suite.d.ts.map +0 -1
  180. package/dist/core/backend-test-suite.js.map +0 -1
  181. package/dist/core/backend.d.ts.map +0 -1
  182. package/dist/core/backend.js.map +0 -1
  183. package/dist/core/backend.testsuite.d.ts +0 -21
  184. package/dist/core/backend.testsuite.d.ts.map +0 -1
  185. package/dist/core/backend.testsuite.js.map +0 -1
  186. package/dist/core/duration.js.map +0 -1
  187. package/dist/core/duration.test.js.map +0 -1
  188. package/dist/core/json.js.map +0 -1
  189. package/dist/core/result.js.map +0 -1
  190. package/dist/core/result.test.js.map +0 -1
  191. package/dist/core/retry.js.map +0 -1
  192. package/dist/core/retry.test.d.ts +0 -2
  193. package/dist/core/retry.test.d.ts.map +0 -1
  194. package/dist/core/retry.test.js +0 -36
  195. package/dist/core/retry.test.js.map +0 -1
  196. package/dist/core/schema.js.map +0 -1
  197. package/dist/core/step.js.map +0 -1
  198. package/dist/core/step.test.js.map +0 -1
  199. package/dist/core/workflow.js.map +0 -1
  200. package/dist/core/workflow.test.js.map +0 -1
  201. package/dist/execution/execution.d.ts.map +0 -1
  202. package/dist/execution/execution.js.map +0 -1
  203. package/dist/execution/execution.test.d.ts.map +0 -1
  204. package/dist/execution/execution.test.js.map +0 -1
  205. package/dist/global.d.ts +0 -62
  206. package/dist/global.d.ts.map +0 -1
  207. package/dist/global.js +0 -78
  208. package/dist/global.js.map +0 -1
  209. package/dist/index.js.map +0 -1
  210. package/dist/sdk/sdk.d.ts +0 -182
  211. package/dist/sdk/sdk.d.ts.map +0 -1
  212. package/dist/sdk/sdk.js.map +0 -1
  213. package/dist/sdk/sdk.test.d.ts +0 -2
  214. package/dist/sdk/sdk.test.d.ts.map +0 -1
  215. package/dist/sdk/sdk.test.js.map +0 -1
  216. package/dist/worker/worker.d.ts.map +0 -1
  217. package/dist/worker/worker.js.map +0 -1
  218. package/dist/worker/worker.test.js.map +0 -1
  219. /package/dist/{execution/execution.test.d.ts → execution.test.d.ts} +0 -0
  220. /package/dist/{worker/worker.test.d.ts → worker.test.d.ts} +0 -0
@@ -0,0 +1,673 @@
1
+ import { DEFAULT_NAMESPACE_ID, } from "../backend.js";
2
+ import { DEFAULT_RETRY_POLICY } from "../core/retry.js";
3
+ import { newDatabase, migrate, generateUUID, now, addMilliseconds, toJSON, fromJSON, toISO, fromISO, } from "./sqlite.js";
4
+ const DEFAULT_PAGINATION_PAGE_SIZE = 100;
5
+ /**
6
+ * Manages a connection to a SQLite database for workflow operations.
7
+ */
8
+ export class BackendSqlite {
9
+ db;
10
+ namespaceId;
11
+ constructor(db, namespaceId) {
12
+ this.db = db;
13
+ this.namespaceId = namespaceId;
14
+ }
15
+ /**
16
+ * Create and initialize a new BackendSqlite instance. This will
17
+ * automatically run migrations on startup unless `runMigrations` is set to
18
+ * false.
19
+ * @param path - Database path
20
+ * @param options - Backend options
21
+ * @returns A connected backend instance
22
+ */
23
+ static connect(path, options) {
24
+ const { namespaceId, runMigrations } = {
25
+ namespaceId: DEFAULT_NAMESPACE_ID,
26
+ runMigrations: true,
27
+ ...options,
28
+ };
29
+ const db = newDatabase(path);
30
+ if (runMigrations) {
31
+ migrate(db);
32
+ }
33
+ return new BackendSqlite(db, namespaceId);
34
+ }
35
+ /**
36
+ * Create and initialize a new BackendSqlite instance from an existing
37
+ * SQLite database connection. This will automatically run migrations on
38
+ * startup unless `runMigrations` is set to false.
39
+ * @param db - SQLite database connection
40
+ * @param options - Backend options
41
+ * @returns A connected backend instance
42
+ */
43
+ static fromClient(db, options) {
44
+ const { namespaceId, runMigrations } = {
45
+ namespaceId: DEFAULT_NAMESPACE_ID,
46
+ runMigrations: true,
47
+ ...options,
48
+ };
49
+ if (runMigrations) {
50
+ migrate(db);
51
+ }
52
+ return new BackendSqlite(db, namespaceId);
53
+ }
54
+ // eslint-disable-next-line @typescript-eslint/require-await
55
+ async stop() {
56
+ this.db.close();
57
+ }
58
+ async createWorkflowRun(params) {
59
+ const id = generateUUID();
60
+ const currentTime = now();
61
+ const availableAt = params.availableAt
62
+ ? toISO(params.availableAt)
63
+ : currentTime;
64
+ const stmt = this.db.prepare(`
65
+ INSERT INTO "workflow_runs" (
66
+ "namespace_id",
67
+ "id",
68
+ "workflow_name",
69
+ "version",
70
+ "status",
71
+ "idempotency_key",
72
+ "config",
73
+ "context",
74
+ "input",
75
+ "attempts",
76
+ "available_at",
77
+ "deadline_at",
78
+ "created_at",
79
+ "updated_at"
80
+ )
81
+ VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?, 0, ?, ?, ?, ?)
82
+ `);
83
+ stmt.run(this.namespaceId, id, params.workflowName, params.version, params.idempotencyKey, toJSON(params.config), toJSON(params.context), toJSON(params.input), availableAt, toISO(params.deadlineAt), currentTime, currentTime);
84
+ const workflowRun = await this.getWorkflowRun({ workflowRunId: id });
85
+ if (!workflowRun)
86
+ throw new Error("Failed to create workflow run");
87
+ return workflowRun;
88
+ }
89
+ getWorkflowRun(params) {
90
+ const stmt = this.db.prepare(`
91
+ SELECT *
92
+ FROM "workflow_runs"
93
+ WHERE "namespace_id" = ? AND "id" = ?
94
+ LIMIT 1
95
+ `);
96
+ const row = stmt.get(this.namespaceId, params.workflowRunId);
97
+ return Promise.resolve(row ? rowToWorkflowRun(row) : null);
98
+ }
99
+ async claimWorkflowRun(params) {
100
+ const currentTime = now();
101
+ const newAvailableAt = addMilliseconds(currentTime, params.leaseDurationMs);
102
+ // SQLite doesn't have SKIP LOCKED, so we need to handle claims differently
103
+ this.db.exec("BEGIN IMMEDIATE");
104
+ try {
105
+ // 1. mark any deadline-expired workflow runs as failed
106
+ const expireStmt = this.db.prepare(`
107
+ UPDATE "workflow_runs"
108
+ SET
109
+ "status" = 'failed',
110
+ "error" = ?,
111
+ "worker_id" = NULL,
112
+ "available_at" = NULL,
113
+ "finished_at" = ?,
114
+ "updated_at" = ?
115
+ WHERE "namespace_id" = ?
116
+ AND "status" IN ('pending', 'running', 'sleeping')
117
+ AND "deadline_at" IS NOT NULL
118
+ AND "deadline_at" <= ?
119
+ `);
120
+ expireStmt.run(toJSON({ message: "Workflow run deadline exceeded" }), currentTime, currentTime, this.namespaceId, currentTime);
121
+ // 2. find an available workflow run to claim
122
+ const findStmt = this.db.prepare(`
123
+ SELECT "id"
124
+ FROM "workflow_runs"
125
+ WHERE "namespace_id" = ?
126
+ AND "status" IN ('pending', 'running', 'sleeping')
127
+ AND "available_at" <= ?
128
+ AND ("deadline_at" IS NULL OR "deadline_at" > ?)
129
+ ORDER BY
130
+ CASE WHEN "status" = 'pending' THEN 0 ELSE 1 END,
131
+ "available_at",
132
+ "created_at"
133
+ LIMIT 1
134
+ `);
135
+ const candidate = findStmt.get(this.namespaceId, currentTime, currentTime);
136
+ if (!candidate) {
137
+ this.db.exec("COMMIT");
138
+ return null;
139
+ }
140
+ // 3. claim the workflow run
141
+ const claimStmt = this.db.prepare(`
142
+ UPDATE "workflow_runs"
143
+ SET
144
+ "status" = 'running',
145
+ "attempts" = "attempts" + 1,
146
+ "worker_id" = ?,
147
+ "available_at" = ?,
148
+ "started_at" = COALESCE("started_at", ?),
149
+ "updated_at" = ?
150
+ WHERE "id" = ?
151
+ AND "namespace_id" = ?
152
+ `);
153
+ claimStmt.run(params.workerId, newAvailableAt, currentTime, currentTime, candidate.id, this.namespaceId);
154
+ this.db.exec("COMMIT");
155
+ return await this.getWorkflowRun({ workflowRunId: candidate.id });
156
+ }
157
+ catch (error) {
158
+ this.db.exec("ROLLBACK");
159
+ throw error;
160
+ }
161
+ }
162
+ async extendWorkflowRunLease(params) {
163
+ const currentTime = now();
164
+ const newAvailableAt = addMilliseconds(currentTime, params.leaseDurationMs);
165
+ const stmt = this.db.prepare(`
166
+ UPDATE "workflow_runs"
167
+ SET
168
+ "available_at" = ?,
169
+ "updated_at" = ?
170
+ WHERE "namespace_id" = ?
171
+ AND "id" = ?
172
+ AND "status" = 'running'
173
+ AND "worker_id" = ?
174
+ `);
175
+ const result = stmt.run(newAvailableAt, currentTime, this.namespaceId, params.workflowRunId, params.workerId);
176
+ if (result.changes === 0) {
177
+ throw new Error("Failed to extend lease for workflow run");
178
+ }
179
+ const updated = await this.getWorkflowRun({
180
+ workflowRunId: params.workflowRunId,
181
+ });
182
+ if (!updated)
183
+ throw new Error("Failed to extend lease for workflow run");
184
+ return updated;
185
+ }
186
+ async sleepWorkflowRun(params) {
187
+ const currentTime = now();
188
+ const stmt = this.db.prepare(`
189
+ UPDATE "workflow_runs"
190
+ SET
191
+ "status" = 'sleeping',
192
+ "available_at" = ?,
193
+ "worker_id" = NULL,
194
+ "updated_at" = ?
195
+ WHERE "namespace_id" = ?
196
+ AND "id" = ?
197
+ AND "status" NOT IN ('completed', 'failed', 'canceled')
198
+ AND "worker_id" = ?
199
+ `);
200
+ const result = stmt.run(toISO(params.availableAt), currentTime, this.namespaceId, params.workflowRunId, params.workerId);
201
+ if (result.changes === 0) {
202
+ throw new Error("Failed to sleep workflow run");
203
+ }
204
+ const updated = await this.getWorkflowRun({
205
+ workflowRunId: params.workflowRunId,
206
+ });
207
+ if (!updated)
208
+ throw new Error("Failed to sleep workflow run");
209
+ return updated;
210
+ }
211
+ async completeWorkflowRun(params) {
212
+ const currentTime = now();
213
+ const stmt = this.db.prepare(`
214
+ UPDATE "workflow_runs"
215
+ SET
216
+ "status" = 'completed',
217
+ "output" = ?,
218
+ "error" = NULL,
219
+ "worker_id" = ?,
220
+ "available_at" = NULL,
221
+ "finished_at" = ?,
222
+ "updated_at" = ?
223
+ WHERE "namespace_id" = ?
224
+ AND "id" = ?
225
+ AND "status" = 'running'
226
+ AND "worker_id" = ?
227
+ `);
228
+ const result = stmt.run(toJSON(params.output), params.workerId, currentTime, currentTime, this.namespaceId, params.workflowRunId, params.workerId);
229
+ if (result.changes === 0) {
230
+ throw new Error("Failed to mark workflow run completed");
231
+ }
232
+ const updated = await this.getWorkflowRun({
233
+ workflowRunId: params.workflowRunId,
234
+ });
235
+ if (!updated)
236
+ throw new Error("Failed to mark workflow run completed");
237
+ return updated;
238
+ }
239
+ async failWorkflowRun(params) {
240
+ const { workflowRunId, error } = params;
241
+ const { initialIntervalMs, backoffCoefficient, maximumIntervalMs } = DEFAULT_RETRY_POLICY;
242
+ const currentTime = now();
243
+ // Get the current workflow run to access attempts
244
+ const workflowRun = await this.getWorkflowRun({ workflowRunId });
245
+ if (!workflowRun)
246
+ throw new Error("Workflow run not found");
247
+ // Calculate retry delay
248
+ const backoffMs = initialIntervalMs *
249
+ Math.pow(backoffCoefficient, workflowRun.attempts - 1);
250
+ const retryDelayMs = Math.min(backoffMs, maximumIntervalMs);
251
+ // Determine if we should reschedule or permanently fail
252
+ const nextRetryTime = new Date(Date.now() + retryDelayMs);
253
+ const shouldRetry = !workflowRun.deadlineAt || nextRetryTime < workflowRun.deadlineAt;
254
+ const status = shouldRetry ? "pending" : "failed";
255
+ const availableAt = shouldRetry ? nextRetryTime.toISOString() : null;
256
+ const finishedAt = shouldRetry ? null : currentTime;
257
+ const stmt = this.db.prepare(`
258
+ UPDATE "workflow_runs"
259
+ SET
260
+ "status" = ?,
261
+ "available_at" = ?,
262
+ "finished_at" = ?,
263
+ "error" = ?,
264
+ "worker_id" = NULL,
265
+ "started_at" = NULL,
266
+ "updated_at" = ?
267
+ WHERE "namespace_id" = ?
268
+ AND "id" = ?
269
+ AND "status" = 'running'
270
+ AND "worker_id" = ?
271
+ `);
272
+ const result = stmt.run(status, availableAt, finishedAt, toJSON(error), currentTime, this.namespaceId, workflowRunId, params.workerId);
273
+ if (result.changes === 0) {
274
+ throw new Error("Failed to mark workflow run failed");
275
+ }
276
+ const updated = await this.getWorkflowRun({ workflowRunId });
277
+ if (!updated)
278
+ throw new Error("Failed to mark workflow run failed");
279
+ return updated;
280
+ }
281
+ async cancelWorkflowRun(params) {
282
+ const currentTime = now();
283
+ const stmt = this.db.prepare(`
284
+ UPDATE "workflow_runs"
285
+ SET
286
+ "status" = 'canceled',
287
+ "worker_id" = NULL,
288
+ "available_at" = NULL,
289
+ "finished_at" = ?,
290
+ "updated_at" = ?
291
+ WHERE "namespace_id" = ?
292
+ AND "id" = ?
293
+ AND "status" IN ('pending', 'running', 'sleeping')
294
+ `);
295
+ const result = stmt.run(currentTime, currentTime, this.namespaceId, params.workflowRunId);
296
+ if (result.changes === 0) {
297
+ // workflow may already be in a terminal state
298
+ const existing = await this.getWorkflowRun({
299
+ workflowRunId: params.workflowRunId,
300
+ });
301
+ if (!existing) {
302
+ throw new Error(`Workflow run ${params.workflowRunId} does not exist`);
303
+ }
304
+ // if already canceled, just return it
305
+ if (existing.status === "canceled") {
306
+ return existing;
307
+ }
308
+ // throw error for completed/failed workflows
309
+ if (["completed", "failed"].includes(existing.status)) {
310
+ throw new Error(`Cannot cancel workflow run ${params.workflowRunId} with status ${existing.status}`);
311
+ }
312
+ throw new Error("Failed to cancel workflow run");
313
+ }
314
+ const updated = await this.getWorkflowRun({
315
+ workflowRunId: params.workflowRunId,
316
+ });
317
+ if (!updated)
318
+ throw new Error("Failed to cancel workflow run");
319
+ return updated;
320
+ }
321
+ listWorkflowRuns(params) {
322
+ const limit = params.limit ?? DEFAULT_PAGINATION_PAGE_SIZE;
323
+ const { after, before } = params;
324
+ let cursor = null;
325
+ if (after) {
326
+ cursor = decodeCursor(after);
327
+ }
328
+ else if (before) {
329
+ cursor = decodeCursor(before);
330
+ }
331
+ const order = before
332
+ ? `ORDER BY "created_at" ASC, "id" ASC`
333
+ : `ORDER BY "created_at" DESC, "id" DESC`;
334
+ let query;
335
+ let queryParams;
336
+ if (cursor) {
337
+ const op = after ? "<" : ">";
338
+ query = `
339
+ SELECT *
340
+ FROM "workflow_runs"
341
+ WHERE "namespace_id" = ?
342
+ AND ("created_at", "id") ${op} (?, ?)
343
+ ${order}
344
+ LIMIT ?
345
+ `;
346
+ queryParams = [
347
+ this.namespaceId,
348
+ cursor.createdAt.toISOString(),
349
+ cursor.id,
350
+ limit + 1,
351
+ ];
352
+ }
353
+ else {
354
+ query = `
355
+ SELECT *
356
+ FROM "workflow_runs"
357
+ WHERE "namespace_id" = ?
358
+ ${order}
359
+ LIMIT ?
360
+ `;
361
+ queryParams = [this.namespaceId, limit + 1];
362
+ }
363
+ const stmt = this.db.prepare(query);
364
+ const rawRows = stmt.all(...queryParams);
365
+ if (!Array.isArray(rawRows)) {
366
+ return Promise.resolve({
367
+ data: [],
368
+ pagination: { next: null, prev: null },
369
+ });
370
+ }
371
+ const rows = rawRows.map((row) => rowToWorkflowRun(row));
372
+ return Promise.resolve(this.processPaginationResults(rows, limit, !!after, !!before));
373
+ }
374
+ listStepAttempts(params) {
375
+ const limit = params.limit ?? DEFAULT_PAGINATION_PAGE_SIZE;
376
+ const { after, before } = params;
377
+ let cursor = null;
378
+ if (after) {
379
+ cursor = decodeCursor(after);
380
+ }
381
+ else if (before) {
382
+ cursor = decodeCursor(before);
383
+ }
384
+ const order = before
385
+ ? `ORDER BY "created_at" DESC, "id" DESC`
386
+ : `ORDER BY "created_at" ASC, "id" ASC`;
387
+ let query;
388
+ let queryParams;
389
+ if (cursor) {
390
+ const op = after ? ">" : "<";
391
+ query = `
392
+ SELECT *
393
+ FROM "step_attempts"
394
+ WHERE "namespace_id" = ?
395
+ AND "workflow_run_id" = ?
396
+ AND ("created_at", "id") ${op} (?, ?)
397
+ ${order}
398
+ LIMIT ?
399
+ `;
400
+ queryParams = [
401
+ this.namespaceId,
402
+ params.workflowRunId,
403
+ cursor.createdAt.toISOString(),
404
+ cursor.id,
405
+ limit + 1,
406
+ ];
407
+ }
408
+ else {
409
+ query = `
410
+ SELECT *
411
+ FROM "step_attempts"
412
+ WHERE "namespace_id" = ?
413
+ AND "workflow_run_id" = ?
414
+ ${order}
415
+ LIMIT ?
416
+ `;
417
+ queryParams = [this.namespaceId, params.workflowRunId, limit + 1];
418
+ }
419
+ const stmt = this.db.prepare(query);
420
+ const rawRows = stmt.all(...queryParams);
421
+ if (!Array.isArray(rawRows)) {
422
+ return Promise.resolve({
423
+ data: [],
424
+ pagination: { next: null, prev: null },
425
+ });
426
+ }
427
+ const rows = rawRows.map((row) => rowToStepAttempt(row));
428
+ return Promise.resolve(this.processPaginationResults(rows, limit, !!after, !!before));
429
+ }
430
+ processPaginationResults(rows, limit, hasAfter, hasBefore) {
431
+ const data = rows;
432
+ let hasNext = false;
433
+ let hasPrev = false;
434
+ if (hasBefore) {
435
+ data.reverse();
436
+ if (data.length > limit) {
437
+ hasPrev = true;
438
+ data.shift();
439
+ }
440
+ hasNext = true;
441
+ }
442
+ else {
443
+ if (data.length > limit) {
444
+ hasNext = true;
445
+ data.pop();
446
+ }
447
+ if (hasAfter) {
448
+ hasPrev = true;
449
+ }
450
+ }
451
+ const lastItem = data.at(-1);
452
+ const nextCursor = hasNext && lastItem ? encodeCursor(lastItem) : null;
453
+ const firstItem = data[0];
454
+ const prevCursor = hasPrev && firstItem ? encodeCursor(firstItem) : null;
455
+ return {
456
+ data,
457
+ pagination: {
458
+ next: nextCursor,
459
+ prev: prevCursor,
460
+ },
461
+ };
462
+ }
463
+ async createStepAttempt(params) {
464
+ const id = generateUUID();
465
+ const currentTime = now();
466
+ const stmt = this.db.prepare(`
467
+ INSERT INTO "step_attempts" (
468
+ "namespace_id",
469
+ "id",
470
+ "workflow_run_id",
471
+ "step_name",
472
+ "kind",
473
+ "status",
474
+ "config",
475
+ "context",
476
+ "started_at",
477
+ "created_at",
478
+ "updated_at"
479
+ )
480
+ VALUES (?, ?, ?, ?, ?, 'running', ?, ?, ?, ?, ?)
481
+ `);
482
+ stmt.run(this.namespaceId, id, params.workflowRunId, params.stepName, params.kind, toJSON(params.config), toJSON(params.context), currentTime, currentTime, currentTime);
483
+ const stepAttempt = await this.getStepAttempt({ stepAttemptId: id });
484
+ if (!stepAttempt)
485
+ throw new Error("Failed to create step attempt");
486
+ return stepAttempt;
487
+ }
488
+ getStepAttempt(params) {
489
+ const stmt = this.db.prepare(`
490
+ SELECT *
491
+ FROM "step_attempts"
492
+ WHERE "namespace_id" = ? AND "id" = ?
493
+ LIMIT 1
494
+ `);
495
+ const row = stmt.get(this.namespaceId, params.stepAttemptId);
496
+ return Promise.resolve(row ? rowToStepAttempt(row) : null);
497
+ }
498
+ async completeStepAttempt(params) {
499
+ const currentTime = now();
500
+ // Check that the workflow is running and owned by the worker
501
+ const workflowStmt = this.db.prepare(`
502
+ SELECT "id"
503
+ FROM "workflow_runs"
504
+ WHERE "namespace_id" = ?
505
+ AND "id" = ?
506
+ AND "status" = 'running'
507
+ AND "worker_id" = ?
508
+ `);
509
+ const workflowRow = workflowStmt.get(this.namespaceId, params.workflowRunId, params.workerId);
510
+ if (!workflowRow) {
511
+ throw new Error("Failed to mark step attempt completed");
512
+ }
513
+ const stmt = this.db.prepare(`
514
+ UPDATE "step_attempts"
515
+ SET
516
+ "status" = 'completed',
517
+ "output" = ?,
518
+ "error" = NULL,
519
+ "finished_at" = ?,
520
+ "updated_at" = ?
521
+ WHERE "namespace_id" = ?
522
+ AND "workflow_run_id" = ?
523
+ AND "id" = ?
524
+ AND "status" = 'running'
525
+ `);
526
+ const result = stmt.run(toJSON(params.output), currentTime, currentTime, this.namespaceId, params.workflowRunId, params.stepAttemptId);
527
+ if (result.changes === 0) {
528
+ throw new Error("Failed to mark step attempt completed");
529
+ }
530
+ const updated = await this.getStepAttempt({
531
+ stepAttemptId: params.stepAttemptId,
532
+ });
533
+ if (!updated)
534
+ throw new Error("Failed to mark step attempt completed");
535
+ return updated;
536
+ }
537
+ async failStepAttempt(params) {
538
+ const currentTime = now();
539
+ // Check that the workflow is running and owned by the worker
540
+ const workflowStmt = this.db.prepare(`
541
+ SELECT "id"
542
+ FROM "workflow_runs"
543
+ WHERE "namespace_id" = ?
544
+ AND "id" = ?
545
+ AND "status" = 'running'
546
+ AND "worker_id" = ?
547
+ `);
548
+ const workflowRow = workflowStmt.get(this.namespaceId, params.workflowRunId, params.workerId);
549
+ if (!workflowRow) {
550
+ throw new Error("Failed to mark step attempt failed");
551
+ }
552
+ const stmt = this.db.prepare(`
553
+ UPDATE "step_attempts"
554
+ SET
555
+ "status" = 'failed',
556
+ "output" = NULL,
557
+ "error" = ?,
558
+ "finished_at" = ?,
559
+ "updated_at" = ?
560
+ WHERE "namespace_id" = ?
561
+ AND "workflow_run_id" = ?
562
+ AND "id" = ?
563
+ AND "status" = 'running'
564
+ `);
565
+ const result = stmt.run(toJSON(params.error), currentTime, currentTime, this.namespaceId, params.workflowRunId, params.stepAttemptId);
566
+ if (result.changes === 0) {
567
+ throw new Error("Failed to mark step attempt failed");
568
+ }
569
+ const updated = await this.getStepAttempt({
570
+ stepAttemptId: params.stepAttemptId,
571
+ });
572
+ if (!updated)
573
+ throw new Error("Failed to mark step attempt failed");
574
+ return updated;
575
+ }
576
+ }
577
+ // Conversion functions
578
+ /**
579
+ * Convert a database row to a WorkflowRun.
580
+ * @param row - Workflow run row
581
+ * @returns Workflow run
582
+ * @throws {Error} If required fields are missing
583
+ */
584
+ function rowToWorkflowRun(row) {
585
+ const createdAt = fromISO(row.created_at);
586
+ const updatedAt = fromISO(row.updated_at);
587
+ const config = fromJSON(row.config);
588
+ if (!createdAt)
589
+ throw new Error("createdAt is required");
590
+ if (!updatedAt)
591
+ throw new Error("updatedAt is required");
592
+ if (config === null)
593
+ throw new Error("config is required");
594
+ return {
595
+ namespaceId: row.namespace_id,
596
+ id: row.id,
597
+ workflowName: row.workflow_name,
598
+ version: row.version,
599
+ status: row.status,
600
+ idempotencyKey: row.idempotency_key,
601
+ config: config,
602
+ context: fromJSON(row.context),
603
+ input: fromJSON(row.input),
604
+ output: fromJSON(row.output),
605
+ error: fromJSON(row.error),
606
+ attempts: row.attempts,
607
+ parentStepAttemptNamespaceId: row.parent_step_attempt_namespace_id,
608
+ parentStepAttemptId: row.parent_step_attempt_id,
609
+ workerId: row.worker_id,
610
+ availableAt: fromISO(row.available_at),
611
+ deadlineAt: fromISO(row.deadline_at),
612
+ startedAt: fromISO(row.started_at),
613
+ finishedAt: fromISO(row.finished_at),
614
+ createdAt,
615
+ updatedAt,
616
+ };
617
+ }
618
+ /**
619
+ * Convert a database row to a StepAttempt.
620
+ * @param row - Step attempt row
621
+ * @returns Step attempt
622
+ * @throws {Error} If required fields are missing
623
+ */
624
+ function rowToStepAttempt(row) {
625
+ const createdAt = fromISO(row.created_at);
626
+ const updatedAt = fromISO(row.updated_at);
627
+ const config = fromJSON(row.config);
628
+ if (!createdAt)
629
+ throw new Error("createdAt is required");
630
+ if (!updatedAt)
631
+ throw new Error("updatedAt is required");
632
+ if (config === null)
633
+ throw new Error("config is required");
634
+ return {
635
+ namespaceId: row.namespace_id,
636
+ id: row.id,
637
+ workflowRunId: row.workflow_run_id,
638
+ stepName: row.step_name,
639
+ kind: row.kind,
640
+ status: row.status,
641
+ config: config,
642
+ context: fromJSON(row.context),
643
+ output: fromJSON(row.output),
644
+ error: fromJSON(row.error),
645
+ childWorkflowRunNamespaceId: row.child_workflow_run_namespace_id,
646
+ childWorkflowRunId: row.child_workflow_run_id,
647
+ startedAt: fromISO(row.started_at),
648
+ finishedAt: fromISO(row.finished_at),
649
+ createdAt,
650
+ updatedAt,
651
+ };
652
+ }
653
+ /**
654
+ * Encode a pagination cursor to a string.
655
+ * @param item - Cursor data
656
+ * @returns Encoded cursor
657
+ */
658
+ function encodeCursor(item) {
659
+ return Buffer.from(JSON.stringify({ createdAt: item.createdAt, id: item.id })).toString("base64");
660
+ }
661
+ /**
662
+ * Decode a pagination cursor from a string.
663
+ * @param cursor - Encoded cursor
664
+ * @returns Cursor data
665
+ */
666
+ function decodeCursor(cursor) {
667
+ const decoded = Buffer.from(cursor, "base64").toString("utf8");
668
+ const parsed = JSON.parse(decoded);
669
+ return {
670
+ createdAt: new Date(parsed.createdAt),
671
+ id: parsed.id,
672
+ };
673
+ }
@@ -0,0 +1,11 @@
1
+ import { BackendSqlite } from "./backend.js";
2
+ import type { DatabaseSync } from "node:sqlite";
3
+ /**
4
+ * Create a SQLite-backed OpenWorkflow backend using node:sqlite.
5
+ * @param client - SQLite client or database path
6
+ * @param options - Backend options
7
+ * @returns A connected backend instance
8
+ */
9
+ export declare function openworkflow(client: DatabaseSync, options?: Parameters<typeof BackendSqlite.fromClient>[1]): BackendSqlite;
10
+ export declare function openworkflow(path: string, options?: Parameters<typeof BackendSqlite.connect>[1]): BackendSqlite;
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../node-sqlite/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GACvD,aAAa,CAAC;AACjB,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GACpD,aAAa,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { BackendSqlite } from "./backend.js";
2
+ export function openworkflow(clientOrPath, options) {
3
+ if (typeof clientOrPath === "string") {
4
+ return BackendSqlite.connect(clientOrPath, options);
5
+ }
6
+ return BackendSqlite.fromClient(clientOrPath, options);
7
+ }