@mokronos/wfkit 0.1.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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/authoring.d.ts +7 -0
  4. package/dist/authoring.d.ts.map +1 -0
  5. package/dist/authoring.js +8 -0
  6. package/dist/authoring.js.map +1 -0
  7. package/dist/cli/integrations.d.ts +3 -0
  8. package/dist/cli/integrations.d.ts.map +1 -0
  9. package/dist/cli/integrations.js +40 -0
  10. package/dist/cli/integrations.js.map +1 -0
  11. package/dist/cli/main.d.ts +3 -0
  12. package/dist/cli/main.d.ts.map +1 -0
  13. package/dist/cli/main.js +591 -0
  14. package/dist/cli/main.js.map +1 -0
  15. package/dist/core.d.ts +198 -0
  16. package/dist/core.d.ts.map +1 -0
  17. package/dist/core.js +1061 -0
  18. package/dist/core.js.map +1 -0
  19. package/dist/errors.d.ts +3 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +13 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/events.d.ts +154 -0
  24. package/dist/events.d.ts.map +1 -0
  25. package/dist/events.js +24 -0
  26. package/dist/events.js.map +1 -0
  27. package/dist/index.d.ts +15 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +11 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/replay.d.ts +3 -0
  32. package/dist/replay.d.ts.map +1 -0
  33. package/dist/replay.js +28 -0
  34. package/dist/replay.js.map +1 -0
  35. package/dist/runtime.d.ts +70 -0
  36. package/dist/runtime.d.ts.map +1 -0
  37. package/dist/runtime.js +183 -0
  38. package/dist/runtime.js.map +1 -0
  39. package/dist/schema.d.ts +18 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/schema.js +19 -0
  42. package/dist/schema.js.map +1 -0
  43. package/dist/schemas.d.ts +1198 -0
  44. package/dist/schemas.d.ts.map +1 -0
  45. package/dist/schemas.js +383 -0
  46. package/dist/schemas.js.map +1 -0
  47. package/dist/sdk/artifact.d.ts +41 -0
  48. package/dist/sdk/artifact.d.ts.map +1 -0
  49. package/dist/sdk/artifact.js +94 -0
  50. package/dist/sdk/artifact.js.map +1 -0
  51. package/dist/sdk/graph.d.ts +15 -0
  52. package/dist/sdk/graph.d.ts.map +1 -0
  53. package/dist/sdk/graph.js +388 -0
  54. package/dist/sdk/graph.js.map +1 -0
  55. package/dist/sdk/index.d.ts +14 -0
  56. package/dist/sdk/index.d.ts.map +1 -0
  57. package/dist/sdk/index.js +8 -0
  58. package/dist/sdk/index.js.map +1 -0
  59. package/dist/sdk/integrations.d.ts +18 -0
  60. package/dist/sdk/integrations.d.ts.map +1 -0
  61. package/dist/sdk/integrations.js +20 -0
  62. package/dist/sdk/integrations.js.map +1 -0
  63. package/dist/sdk/json.d.ts +4 -0
  64. package/dist/sdk/json.d.ts.map +1 -0
  65. package/dist/sdk/json.js +31 -0
  66. package/dist/sdk/json.js.map +1 -0
  67. package/dist/sdk/loader.d.ts +10 -0
  68. package/dist/sdk/loader.d.ts.map +1 -0
  69. package/dist/sdk/loader.js +75 -0
  70. package/dist/sdk/loader.js.map +1 -0
  71. package/dist/sdk/sdk.d.ts +102 -0
  72. package/dist/sdk/sdk.d.ts.map +1 -0
  73. package/dist/sdk/sdk.js +669 -0
  74. package/dist/sdk/sdk.js.map +1 -0
  75. package/dist/sdk/sqlite.d.ts +8 -0
  76. package/dist/sdk/sqlite.d.ts.map +1 -0
  77. package/dist/sdk/sqlite.js +288 -0
  78. package/dist/sdk/sqlite.js.map +1 -0
  79. package/dist/signal.d.ts +17 -0
  80. package/dist/signal.d.ts.map +1 -0
  81. package/dist/signal.js +78 -0
  82. package/dist/signal.js.map +1 -0
  83. package/dist/testing/index.d.ts +35 -0
  84. package/dist/testing/index.d.ts.map +1 -0
  85. package/dist/testing/index.js +244 -0
  86. package/dist/testing/index.js.map +1 -0
  87. package/package.json +67 -0
  88. package/src/authoring.ts +32 -0
  89. package/src/cli/integrations.ts +75 -0
  90. package/src/cli/main.ts +793 -0
  91. package/src/core.ts +1515 -0
  92. package/src/errors.ts +19 -0
  93. package/src/events.ts +37 -0
  94. package/src/index.ts +109 -0
  95. package/src/replay.ts +29 -0
  96. package/src/runtime.ts +314 -0
  97. package/src/schema.ts +34 -0
  98. package/src/schemas.ts +473 -0
  99. package/src/sdk/artifact.ts +176 -0
  100. package/src/sdk/graph.ts +486 -0
  101. package/src/sdk/index.ts +64 -0
  102. package/src/sdk/integrations.ts +49 -0
  103. package/src/sdk/json.ts +40 -0
  104. package/src/sdk/loader.ts +99 -0
  105. package/src/sdk/sdk.ts +911 -0
  106. package/src/sdk/sqlite.ts +405 -0
  107. package/src/signal.ts +116 -0
  108. package/src/testing/index.ts +341 -0
@@ -0,0 +1,669 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdirSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { Schema } from "effect";
5
+ import { Cancelled, cancellationDeferredName, createInMemoryDeterminismState } from "../core.js";
6
+ import { ExecutionId, WorkflowHistoryEvent as WorkflowHistoryEventSchema } from "../schemas.js";
7
+ import { createWorkflowRuntime, executeWorkflow } from "../runtime.js";
8
+ import { cancelSignalWaits, decodeSignal, deliverSignal, getSignalSchema } from "../signal.js";
9
+ import { createFileWorkflowStore } from "./artifact.js";
10
+ import { parseJsonText, toJsonText } from "./json.js";
11
+ import { loadWorkflowArtifact } from "./loader.js";
12
+ import { replayDedupeKey } from "../replay.js";
13
+ export { Cancelled } from "../core.js";
14
+ export class MissingWorkflowVersionError extends Error {
15
+ _tag = "MissingWorkflowVersionError";
16
+ workflowName;
17
+ version;
18
+ constructor(options) {
19
+ super(`Workflow ${options.workflowName}@v${options.version} is not registered in this runtime; re-register that exact version before resuming this execution`);
20
+ this.name = "MissingWorkflowVersionError";
21
+ this.workflowName = options.workflowName;
22
+ this.version = options.version;
23
+ }
24
+ }
25
+ const executionId = () => crypto.randomUUID();
26
+ const nowIso = () => new Date().toISOString();
27
+ const encodeStoredValue = (value) => JSON.stringify({ value });
28
+ const decodeStoredValue = (json) => JSON.parse(json).value;
29
+ const optionalActor = (actor) => actor === undefined ? {} : { actor };
30
+ const optionalFinishedAt = (finishedAt) => finishedAt === undefined ? {} : { finishedAt };
31
+ const optionalCursor = (cursor) => cursor === undefined ? {} : { cursor };
32
+ const signalKey = (event) => `${event.name}:${event.invocation}`;
33
+ const optionalTimeout = (timeout) => timeout === undefined ? {} : { timeout };
34
+ export const pendingSignalsFromHistory = (history) => {
35
+ const consumed = new Set();
36
+ for (const record of history) {
37
+ const event = record.event;
38
+ if (event.type === "signal.received" || event.type === "signal.timeout") {
39
+ consumed.add(signalKey(event));
40
+ }
41
+ }
42
+ return history.flatMap((record) => {
43
+ const event = record.event;
44
+ if (event.type !== "signal.waiting" || consumed.has(signalKey(event))) {
45
+ return [];
46
+ }
47
+ return [{
48
+ name: event.name,
49
+ invocation: event.invocation,
50
+ activityName: event.activityName,
51
+ ...optionalTimeout(event.timeout),
52
+ ...(event.payloadSchema === undefined ? {} : { payloadSchema: event.payloadSchema })
53
+ }];
54
+ });
55
+ };
56
+ export const createWorkflowClient = (runtime = createWorkflowRuntime({ backend: "memory" })) => runtime.backend === "sqlite" ? createDurableWorkflowClient(runtime) : createMemoryWorkflowClient(runtime);
57
+ const createMemoryWorkflowClient = (runtime) => {
58
+ const executions = new Map();
59
+ const idempotencyKeys = new Map();
60
+ const appendHistory = (record, event) => {
61
+ record.history.push({
62
+ sequence: record.history.length + 1,
63
+ createdAt: nowIso(),
64
+ event
65
+ });
66
+ };
67
+ const requireExecution = (id) => {
68
+ const execution = executions.get(id);
69
+ if (execution === undefined) {
70
+ throw new Error(`Unknown workflow execution: ${id}`);
71
+ }
72
+ return execution;
73
+ };
74
+ const statusFromEvent = (event) => {
75
+ switch (event.type) {
76
+ case "sleep.started":
77
+ case "signal.waiting":
78
+ return "suspended";
79
+ case "sleep.completed":
80
+ case "signal.received":
81
+ case "signal.timeout":
82
+ case "step.started":
83
+ case "step.completed":
84
+ return "running";
85
+ default:
86
+ return undefined;
87
+ }
88
+ };
89
+ return {
90
+ async start(workflow, payload, opts = {}) {
91
+ const workflowKey = `${workflow.name}@v${workflow.version}`;
92
+ if (opts.idempotencyKey !== undefined) {
93
+ const existingId = idempotencyKeys.get(`${workflowKey}:${opts.idempotencyKey}`);
94
+ if (existingId !== undefined) {
95
+ return { executionId: existingId, version: workflow.version };
96
+ }
97
+ }
98
+ const id = executionId();
99
+ let resolveResult;
100
+ const resultPromise = new Promise((resolve) => {
101
+ resolveResult = resolve;
102
+ });
103
+ const execution = {
104
+ executionId: id,
105
+ workflow,
106
+ payload,
107
+ status: "running",
108
+ startedAt: nowIso(),
109
+ history: [],
110
+ resultPromise,
111
+ resolveResult
112
+ };
113
+ executions.set(id, execution);
114
+ if (opts.idempotencyKey !== undefined) {
115
+ idempotencyKeys.set(`${workflowKey}:${opts.idempotencyKey}`, id);
116
+ }
117
+ appendHistory(execution, {
118
+ type: "execution.started",
119
+ executionId: ExecutionId.make(id),
120
+ workflowName: workflow.name,
121
+ version: workflow.version,
122
+ payload,
123
+ ...optionalActor(opts.actor)
124
+ });
125
+ void workflow
126
+ .executeInMemory(payload, {
127
+ executionId: id,
128
+ determinism: createInMemoryDeterminismState(),
129
+ ...(runtime?.secrets === undefined ? {} : { secrets: runtime.secrets }),
130
+ onEvent: async (event) => {
131
+ appendHistory(execution, event);
132
+ const nextStatus = statusFromEvent(event);
133
+ if (nextStatus !== undefined && execution.status !== "failed") {
134
+ execution.status = nextStatus;
135
+ }
136
+ if (event.type === "sleep.started") {
137
+ await new Promise((resolve) => setTimeout(resolve, 10));
138
+ }
139
+ }
140
+ })
141
+ .then((value) => {
142
+ execution.status = "completed";
143
+ execution.finishedAt = nowIso();
144
+ execution.result = { type: "completed", value };
145
+ execution.resolveResult(execution.result);
146
+ }, (error) => {
147
+ execution.status = "failed";
148
+ execution.finishedAt = nowIso();
149
+ execution.result = { type: "failed", error };
150
+ execution.resolveResult(execution.result);
151
+ });
152
+ return { executionId: id, version: workflow.version };
153
+ },
154
+ async signal(id, name, payload, opts = {}) {
155
+ const execution = requireExecution(id);
156
+ await deliverSignal(id, name, payload);
157
+ appendHistory(execution, {
158
+ type: "signal.delivered",
159
+ executionId: ExecutionId.make(id),
160
+ name,
161
+ payload,
162
+ ...optionalActor(opts.actor)
163
+ });
164
+ },
165
+ result(id) {
166
+ return requireExecution(id).resultPromise;
167
+ },
168
+ async status(id) {
169
+ return requireExecution(id).status;
170
+ },
171
+ async list(workflow, opts = {}) {
172
+ const all = Array.from(executions.values())
173
+ .filter((execution) => execution.workflow.name === workflow.name)
174
+ .filter((execution) => opts.version === undefined || execution.workflow.version === opts.version)
175
+ .filter((execution) => opts.status === undefined || execution.status === opts.status)
176
+ .sort((left, right) => left.startedAt.localeCompare(right.startedAt));
177
+ const start = opts.cursor === undefined ? 0 : Number.parseInt(opts.cursor, 10);
178
+ const limit = opts.limit ?? all.length;
179
+ const page = all.slice(start, start + limit);
180
+ const next = start + limit < all.length ? String(start + limit) : undefined;
181
+ return {
182
+ executions: page.map((execution) => ({
183
+ executionId: execution.executionId,
184
+ workflowName: execution.workflow.name,
185
+ version: execution.workflow.version,
186
+ status: execution.status,
187
+ startedAt: execution.startedAt,
188
+ ...optionalFinishedAt(execution.finishedAt)
189
+ })),
190
+ ...optionalCursor(next)
191
+ };
192
+ },
193
+ async history(id) {
194
+ return requireExecution(id).history;
195
+ },
196
+ async pendingSignals(id) {
197
+ return pendingSignalsFromHistory(requireExecution(id).history);
198
+ },
199
+ async cancel(id, opts = {}) {
200
+ const execution = requireExecution(id);
201
+ const compensate = opts.compensate ?? true;
202
+ appendHistory(execution, {
203
+ type: "execution.cancelled",
204
+ executionId: ExecutionId.make(id),
205
+ compensate,
206
+ ...optionalActor(opts.actor)
207
+ });
208
+ if (compensate) {
209
+ execution.status = "compensating";
210
+ }
211
+ cancelSignalWaits(id, new Cancelled({ compensate }));
212
+ const result = await execution.resultPromise;
213
+ if (result.type === "failed") {
214
+ execution.status = "failed";
215
+ }
216
+ }
217
+ };
218
+ };
219
+ const migrateClientDb = (db) => {
220
+ db.exec(`
221
+ CREATE TABLE IF NOT EXISTS wf_client_executions (
222
+ id TEXT PRIMARY KEY,
223
+ workflow_name TEXT NOT NULL,
224
+ workflow_version INTEGER NOT NULL,
225
+ status TEXT NOT NULL,
226
+ payload_json TEXT NOT NULL,
227
+ idempotency_key TEXT,
228
+ actor TEXT,
229
+ result_json TEXT,
230
+ error_json TEXT,
231
+ started_at TEXT NOT NULL,
232
+ finished_at TEXT
233
+ );
234
+
235
+ CREATE UNIQUE INDEX IF NOT EXISTS wf_client_executions_idempotency_idx
236
+ ON wf_client_executions(workflow_name, workflow_version, idempotency_key)
237
+ WHERE idempotency_key IS NOT NULL;
238
+
239
+ CREATE TABLE IF NOT EXISTS wf_client_workflows (
240
+ workflow_name TEXT NOT NULL,
241
+ workflow_version INTEGER NOT NULL,
242
+ source_hash TEXT NOT NULL,
243
+ registered_at TEXT NOT NULL,
244
+ PRIMARY KEY (workflow_name, workflow_version)
245
+ );
246
+
247
+ CREATE TABLE IF NOT EXISTS wf_client_history (
248
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
249
+ execution_id TEXT NOT NULL,
250
+ sequence INTEGER NOT NULL,
251
+ event_json TEXT NOT NULL,
252
+ created_at TEXT NOT NULL,
253
+ dedupe_key TEXT,
254
+ UNIQUE(execution_id, sequence)
255
+ );
256
+ `);
257
+ const columns = db.query("PRAGMA table_info(wf_client_history)").all();
258
+ if (!columns.some((column) => column.name === "dedupe_key")) {
259
+ db.exec("ALTER TABLE wf_client_history ADD COLUMN dedupe_key TEXT");
260
+ }
261
+ db.exec(`
262
+ CREATE INDEX IF NOT EXISTS wf_client_history_dedupe_idx
263
+ ON wf_client_history(execution_id, dedupe_key)
264
+ WHERE dedupe_key IS NOT NULL
265
+ `);
266
+ };
267
+ const createDurableWorkflowClient = (runtime) => {
268
+ const databasePath = runtime.databasePath;
269
+ if (databasePath === undefined) {
270
+ throw new Error("SQLite workflow client requires runtime.databasePath");
271
+ }
272
+ mkdirSync(path.dirname(path.resolve(databasePath)), { recursive: true });
273
+ const db = new Database(databasePath, { create: true, readwrite: true });
274
+ migrateClientDb(db);
275
+ const runPromises = new Map();
276
+ const registerCatalog = (workflow) => {
277
+ const existing = db.query(`
278
+ SELECT workflow_name, workflow_version, source_hash, registered_at
279
+ FROM wf_client_workflows
280
+ WHERE workflow_name = ?
281
+ AND workflow_version = ?
282
+ `).get(workflow.name, workflow.version);
283
+ if (existing !== null) {
284
+ if (existing.source_hash !== workflow.sourceHash) {
285
+ throw new Error(`Workflow ${workflow.name}@v${workflow.version} is already cataloged with different source; register a new version instead`);
286
+ }
287
+ return;
288
+ }
289
+ db.query(`
290
+ INSERT INTO wf_client_workflows (workflow_name, workflow_version, source_hash, registered_at)
291
+ VALUES (?, ?, ?, ?)
292
+ `).run(workflow.name, workflow.version, workflow.sourceHash, nowIso());
293
+ };
294
+ const registerCatalogFromRuntime = (name) => {
295
+ for (const workflow of runtime.listWorkflows(name)) {
296
+ registerCatalog(workflow);
297
+ }
298
+ };
299
+ // Returns false when the event is a replay re-emission that is already
300
+ // recorded (the engine replays workflow code whenever a suspended run
301
+ // resumes, possibly in another process).
302
+ const appendHistory = (executionId, event) => {
303
+ const dedupeKey = replayDedupeKey(event) ?? null;
304
+ if (dedupeKey !== null) {
305
+ const existing = db.query(`
306
+ SELECT id
307
+ FROM wf_client_history
308
+ WHERE execution_id = ?
309
+ AND dedupe_key = ?
310
+ `).get(executionId, dedupeKey);
311
+ if (existing !== null) {
312
+ return false;
313
+ }
314
+ }
315
+ const sequence = db.query(`
316
+ SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence
317
+ FROM wf_client_history
318
+ WHERE execution_id = ?
319
+ `).get(executionId)?.sequence ?? 1;
320
+ db.query(`
321
+ INSERT INTO wf_client_history (execution_id, sequence, event_json, created_at, dedupe_key)
322
+ VALUES (?, ?, ?, ?, ?)
323
+ `).run(executionId, sequence, toJsonText(event), nowIso(), dedupeKey);
324
+ return true;
325
+ };
326
+ const updateStatus = (executionId, status) => {
327
+ db.query(`
328
+ UPDATE wf_client_executions
329
+ SET status = ?
330
+ WHERE id = ?
331
+ `).run(status, executionId);
332
+ };
333
+ const getRow = (executionId) => {
334
+ const row = db.query(`
335
+ SELECT *
336
+ FROM wf_client_executions
337
+ WHERE id = ?
338
+ `).get(executionId);
339
+ if (row === null) {
340
+ throw new Error(`Unknown workflow execution: ${executionId}`);
341
+ }
342
+ return row;
343
+ };
344
+ const workflowFor = (row) => {
345
+ const workflow = runtime.getWorkflow(row.workflow_name, row.workflow_version);
346
+ if (workflow === undefined) {
347
+ throw new MissingWorkflowVersionError({
348
+ workflowName: row.workflow_name,
349
+ version: row.workflow_version
350
+ });
351
+ }
352
+ return workflow;
353
+ };
354
+ const workflowForStart = (workflow, version) => {
355
+ runtime.register([workflow]);
356
+ registerCatalogFromRuntime(workflow.name);
357
+ const selected = version === undefined
358
+ ? runtime.getLatestWorkflow(workflow.name)
359
+ : runtime.getWorkflow(workflow.name, version);
360
+ if (selected === undefined) {
361
+ throw new MissingWorkflowVersionError({
362
+ workflowName: workflow.name,
363
+ version: version ?? workflow.version
364
+ });
365
+ }
366
+ registerCatalog(selected);
367
+ return selected;
368
+ };
369
+ const statusFromEvent = (event) => {
370
+ switch (event.type) {
371
+ case "sleep.started":
372
+ case "signal.waiting":
373
+ return "suspended";
374
+ case "sleep.completed":
375
+ case "signal.received":
376
+ case "signal.timeout":
377
+ case "step.started":
378
+ case "step.completed":
379
+ return "running";
380
+ default:
381
+ return undefined;
382
+ }
383
+ };
384
+ const makeEventSink = (executionId) => async (event) => {
385
+ if (!appendHistory(executionId, event)) {
386
+ // Replay re-emission: the status transition already happened when the
387
+ // event fired for real, so don't let the replay flap it.
388
+ return;
389
+ }
390
+ const status = statusFromEvent(event);
391
+ if (status !== undefined) {
392
+ updateStatus(executionId, status);
393
+ }
394
+ };
395
+ const readHistory = (executionId) => {
396
+ getRow(executionId);
397
+ return db.query(`
398
+ SELECT sequence, event_json, created_at
399
+ FROM wf_client_history
400
+ WHERE execution_id = ?
401
+ ORDER BY sequence
402
+ `).all(executionId).map((row) => ({
403
+ sequence: row.sequence,
404
+ createdAt: row.created_at,
405
+ event: Schema.decodeUnknownSync(WorkflowHistoryEventSchema)(JSON.parse(row.event_json))
406
+ }));
407
+ };
408
+ const runToTerminal = async (row) => {
409
+ if (row.status === "completed") {
410
+ return { type: "completed", value: parseJsonText(row.result_json) };
411
+ }
412
+ if (row.status === "failed") {
413
+ return { type: "failed", error: parseJsonText(row.error_json) };
414
+ }
415
+ const existing = runPromises.get(row.id);
416
+ if (existing !== undefined) {
417
+ return existing;
418
+ }
419
+ const promise = (async () => {
420
+ const workflow = workflowFor(row);
421
+ try {
422
+ const value = await runtime.execute({
423
+ workflow,
424
+ payload: decodeStoredValue(row.payload_json),
425
+ executionId: row.id,
426
+ onEvent: makeEventSink(row.id)
427
+ });
428
+ db.query(`
429
+ UPDATE wf_client_executions
430
+ SET status = 'completed',
431
+ result_json = ?,
432
+ finished_at = ?
433
+ WHERE id = ?
434
+ `).run(toJsonText(value), nowIso(), row.id);
435
+ return { type: "completed", value };
436
+ }
437
+ catch (error) {
438
+ db.query(`
439
+ UPDATE wf_client_executions
440
+ SET status = 'failed',
441
+ error_json = ?,
442
+ finished_at = ?
443
+ WHERE id = ?
444
+ `).run(toJsonText(error), nowIso(), row.id);
445
+ return { type: "failed", error };
446
+ }
447
+ })();
448
+ runPromises.set(row.id, promise);
449
+ try {
450
+ return await promise;
451
+ }
452
+ finally {
453
+ if (runPromises.get(row.id) === promise) {
454
+ runPromises.delete(row.id);
455
+ }
456
+ }
457
+ };
458
+ return {
459
+ async start(workflow, payload, opts = {}) {
460
+ const selectedWorkflow = workflowForStart(workflow, opts.version);
461
+ if (opts.idempotencyKey !== undefined) {
462
+ const existing = db.query(`
463
+ SELECT id
464
+ FROM wf_client_executions
465
+ WHERE workflow_name = ?
466
+ AND workflow_version = ?
467
+ AND idempotency_key = ?
468
+ `).get(selectedWorkflow.name, selectedWorkflow.version, opts.idempotencyKey);
469
+ if (existing !== null) {
470
+ return { executionId: existing.id, version: selectedWorkflow.version };
471
+ }
472
+ }
473
+ const id = executionId();
474
+ db.query(`
475
+ INSERT INTO wf_client_executions (
476
+ id,
477
+ workflow_name,
478
+ workflow_version,
479
+ status,
480
+ payload_json,
481
+ idempotency_key,
482
+ actor,
483
+ started_at
484
+ )
485
+ VALUES (?, ?, ?, 'running', ?, ?, ?, ?)
486
+ `).run(id, selectedWorkflow.name, selectedWorkflow.version, encodeStoredValue(payload), opts.idempotencyKey ?? null, opts.actor ?? null, nowIso());
487
+ appendHistory(id, {
488
+ type: "execution.started",
489
+ executionId: ExecutionId.make(id),
490
+ workflowName: selectedWorkflow.name,
491
+ version: selectedWorkflow.version,
492
+ payload,
493
+ ...optionalActor(opts.actor)
494
+ });
495
+ void runToTerminal(getRow(id));
496
+ return { executionId: id, version: selectedWorkflow.version };
497
+ },
498
+ async signal(executionId, name, payload, opts = {}) {
499
+ const row = getRow(executionId);
500
+ const workflow = workflowFor(row);
501
+ const waiting = pendingSignalsFromHistory(readHistory(executionId))
502
+ .filter((signal) => signal.name === name)
503
+ .at(-1);
504
+ if (waiting === undefined) {
505
+ throw new Error(`Execution ${executionId} is not waiting for signal ${name}`);
506
+ }
507
+ // Validate the payload against the schema of the wait the run is parked
508
+ // at BEFORE completing the durable deferred — a bad value persisted into
509
+ // the deferred would otherwise fail the run at replay. In a fresh
510
+ // process the schema registry is empty until the run replays, so nudge
511
+ // a resume and wait for the workflow to park again.
512
+ let schema = getSignalSchema(executionId, name);
513
+ if (schema === undefined) {
514
+ await runtime.resume({ workflow, executionId });
515
+ for (let attempt = 0; attempt < 50 && schema === undefined; attempt++) {
516
+ await new Promise((resolve) => setTimeout(resolve, 50));
517
+ schema = getSignalSchema(executionId, name);
518
+ }
519
+ }
520
+ if (schema !== undefined) {
521
+ decodeSignal(schema, payload);
522
+ }
523
+ await runtime.deliverSignal({
524
+ workflow,
525
+ executionId,
526
+ deferredName: `signal:${waiting.activityName}`,
527
+ payload,
528
+ onEvent: makeEventSink(executionId)
529
+ });
530
+ appendHistory(executionId, {
531
+ type: "signal.delivered",
532
+ executionId: ExecutionId.make(executionId),
533
+ name,
534
+ payload,
535
+ ...optionalActor(opts.actor)
536
+ });
537
+ updateStatus(executionId, "running");
538
+ },
539
+ result(executionId) {
540
+ return runToTerminal(getRow(executionId));
541
+ },
542
+ async status(executionId) {
543
+ return getRow(executionId).status;
544
+ },
545
+ async list(workflow, opts = {}) {
546
+ const rows = db.query(`
547
+ SELECT *
548
+ FROM wf_client_executions
549
+ WHERE workflow_name = ?
550
+ ORDER BY started_at
551
+ `).all(workflow.name)
552
+ .filter((row) => opts.version === undefined || row.workflow_version === opts.version)
553
+ .filter((row) => opts.status === undefined || row.status === opts.status);
554
+ const start = opts.cursor === undefined ? 0 : Number.parseInt(opts.cursor, 10);
555
+ const limit = opts.limit ?? rows.length;
556
+ const page = rows.slice(start, start + limit);
557
+ const next = start + limit < rows.length ? String(start + limit) : undefined;
558
+ return {
559
+ executions: page.map((row) => ({
560
+ executionId: row.id,
561
+ workflowName: row.workflow_name,
562
+ version: row.workflow_version,
563
+ status: row.status,
564
+ startedAt: row.started_at,
565
+ ...optionalFinishedAt(row.finished_at ?? undefined)
566
+ })),
567
+ ...optionalCursor(next)
568
+ };
569
+ },
570
+ async history(executionId) {
571
+ return readHistory(executionId);
572
+ },
573
+ async pendingSignals(executionId) {
574
+ return pendingSignalsFromHistory(readHistory(executionId));
575
+ },
576
+ async cancel(executionId, opts = {}) {
577
+ const row = getRow(executionId);
578
+ const workflow = workflowFor(row);
579
+ const compensate = opts.compensate ?? true;
580
+ appendHistory(executionId, {
581
+ type: "execution.cancelled",
582
+ executionId: ExecutionId.make(executionId),
583
+ compensate,
584
+ ...optionalActor(opts.actor)
585
+ });
586
+ if (compensate) {
587
+ // Complete the reserved cancellation deferred: the execution wakes at
588
+ // its current suspension point, fails with Cancelled, and unwinds the
589
+ // compensation stack before being recorded as failed.
590
+ updateStatus(executionId, "compensating");
591
+ await runtime.deliverSignal({
592
+ workflow,
593
+ executionId,
594
+ deferredName: cancellationDeferredName,
595
+ payload: { compensate: true, ...(opts.actor === undefined ? {} : { actor: opts.actor }) },
596
+ onEvent: makeEventSink(executionId)
597
+ });
598
+ db.query(`
599
+ UPDATE wf_client_executions
600
+ SET status = 'failed',
601
+ error_json = ?,
602
+ finished_at = ?
603
+ WHERE id = ?
604
+ `).run(toJsonText(new Cancelled({ compensate: true })), nowIso(), executionId);
605
+ }
606
+ else {
607
+ // Hard kill: engine-level interrupt, no unwind.
608
+ updateStatus(executionId, "failed");
609
+ await runtime.interrupt({ workflow, executionId });
610
+ }
611
+ }
612
+ };
613
+ };
614
+ export const createWorkflowSdk = (options = {}) => {
615
+ const store = options.store ?? createFileWorkflowStore();
616
+ const runStore = options.runStore;
617
+ return {
618
+ listWorkflows() {
619
+ return store.list();
620
+ },
621
+ getWorkflow(id) {
622
+ return store.get(id);
623
+ },
624
+ listRuns() {
625
+ return runStore?.listRuns() ?? Promise.resolve([]);
626
+ },
627
+ listRunEvents(runId) {
628
+ return runStore?.listRunEvents(runId) ?? Promise.resolve([]);
629
+ },
630
+ async runWorkflow({ id, input, onEvent }) {
631
+ const artifact = await store.get(id);
632
+ if (artifact === undefined) {
633
+ throw new Error(`Unknown workflow id: ${id}`);
634
+ }
635
+ const runId = `${artifact.id}:${artifact.version}:${executionId()}`;
636
+ await runStore?.startRun({ id: runId, workflow: artifact, input });
637
+ const loaded = await loadWorkflowArtifact(artifact);
638
+ const onRuntimeEvent = async (event) => {
639
+ await runStore?.appendRunEvent({ runId, type: event.type, event });
640
+ await onEvent?.(event);
641
+ };
642
+ try {
643
+ const result = await executeWorkflow(loaded.workflow, input, { onEvent: onRuntimeEvent });
644
+ await runStore?.completeRun({ runId, result });
645
+ return {
646
+ artifact: loaded.artifact,
647
+ exportName: loaded.exportName,
648
+ runId,
649
+ result
650
+ };
651
+ }
652
+ catch (error) {
653
+ await runStore?.failRun({ runId, error: serializeError(error) });
654
+ throw error;
655
+ }
656
+ }
657
+ };
658
+ };
659
+ const serializeError = (error) => {
660
+ if (error instanceof Error) {
661
+ return {
662
+ name: error.name,
663
+ message: error.message,
664
+ stack: error.stack
665
+ };
666
+ }
667
+ return JSON.parse(toJsonText(error));
668
+ };
669
+ //# sourceMappingURL=sdk.js.map