@usehelical/workflows 0.0.1-alpha.12 → 0.0.1-alpha.2

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.
@@ -1,650 +0,0 @@
1
- import { serializeError as serializeError$1, deserializeError as deserializeError$1 } from 'serialize-error';
2
- import { AsyncLocalStorage } from 'async_hooks';
3
- import { sql } from 'kysely';
4
-
5
- // core/workflow.ts
6
- var WorkflowStatus = /* @__PURE__ */ ((WorkflowStatus2) => {
7
- WorkflowStatus2["PENDING"] = "PENDING";
8
- WorkflowStatus2["QUEUED"] = "QUEUED";
9
- WorkflowStatus2["SUCCESS"] = "SUCCESS";
10
- WorkflowStatus2["ERROR"] = "ERROR";
11
- WorkflowStatus2["CANCELLED"] = "CANCELLED";
12
- WorkflowStatus2["MAX_RECOVERY_ATTEMPTS_EXCEEDED"] = "MAX_RECOVERY_ATTEMPTS_EXCEEDED";
13
- return WorkflowStatus2;
14
- })(WorkflowStatus || {});
15
- function defineWorkflow(fn, options = {}) {
16
- return () => {
17
- return {
18
- fn,
19
- maxRecoveryAttempts: options.maxRecoveryAttempts
20
- };
21
- };
22
- }
23
-
24
- // core/internal/errors.ts
25
- var RunCancelledError = class extends Error {
26
- constructor() {
27
- super("This workflow run has been cancelled");
28
- this.name = "RUN_CANCELLED" /* RUN_CANCELLED */;
29
- }
30
- };
31
- var RunNotFoundError = class extends Error {
32
- constructor(runId) {
33
- super(`Workflow run "${runId}" not found`);
34
- this.name = "RUN_NOT_FOUND" /* RUN_NOT_FOUND */;
35
- }
36
- };
37
- var RunOutsideOfWorkflowError = class extends Error {
38
- constructor() {
39
- super("This function must be called within a workflow");
40
- this.name = "RUN_OUTSIDE_OF_WORKFLOW" /* RUN_OUTSIDE_OF_WORKFLOW */;
41
- }
42
- };
43
- var FatalError = class extends Error {
44
- constructor(message) {
45
- super(message);
46
- this.name = "FATAL_ERROR" /* FATAL_ERROR */;
47
- }
48
- };
49
- var MaxRetriesExceededError = class extends Error {
50
- attemptErrors;
51
- stepName;
52
- maxAttempts;
53
- constructor(stepName, maxAttempts, errors) {
54
- const formattedErrors = errors.map((error, index) => `Attempt ${index + 1}: ${error.message}`).join(". ");
55
- super(`Step "${stepName}" failed after ${maxAttempts + 1} attempts. ${formattedErrors}`);
56
- this.name = "MAX_RETRIES_EXCEEDED" /* MAX_RETRIES_EXCEEDED */;
57
- this.attemptErrors = errors;
58
- this.stepName = stepName;
59
- this.maxAttempts = maxAttempts;
60
- }
61
- };
62
- var ErrorThatShouldNeverHappen = class extends Error {
63
- constructor(message) {
64
- super(message);
65
- this.name = "ERROR_THAT_SHOULD_NEVER_HAPPEN" /* ERROR_THAT_SHOULD_NEVER_HAPPEN */;
66
- }
67
- };
68
- var SerializationError = class extends Error {
69
- constructor(message) {
70
- super(message);
71
- this.name = "SERIALIZATION_ERROR" /* SERIALIZATION_ERROR */;
72
- }
73
- };
74
- var MaxRecoveryAttemptsExceededError = class extends Error {
75
- constructor(runId, maxAttempts) {
76
- super(`Max recovery attempts exceeded for run "${runId}" after ${maxAttempts + 1} attempts`);
77
- this.name = "MAX_RECOVERY_ATTEMPTS_EXCEEDED" /* MAX_RECOVERY_ATTEMPTS_EXCEEDED */;
78
- }
79
- };
80
- var WorkflowNotFoundError = class extends Error {
81
- constructor(workflowName) {
82
- super(`Workflow "${workflowName}" not found`);
83
- this.name = "WORKFLOW_NOT_FOUND" /* WORKFLOW_NOT_FOUND */;
84
- }
85
- };
86
- var TimeoutError = class extends Error {
87
- constructor(message) {
88
- super(message);
89
- this.name = "TIMEOUT" /* TIMEOUT */;
90
- }
91
- };
92
- var DeadlineError = class extends Error {
93
- constructor(message) {
94
- super(message);
95
- this.name = "DEADLINE" /* DEADLINE */;
96
- }
97
- };
98
- var QueueNotFoundError = class extends Error {
99
- constructor(message) {
100
- super(message);
101
- this.name = "QUEUE_NOT_FOUND" /* QUEUE_NOT_FOUND */;
102
- }
103
- };
104
- function serialize(value) {
105
- try {
106
- return JSON.stringify(value);
107
- } catch (error) {
108
- throw new SerializationError(error.message);
109
- }
110
- }
111
- function deserialize(value) {
112
- try {
113
- return JSON.parse(value);
114
- } catch (error) {
115
- throw new SerializationError(error.message);
116
- }
117
- }
118
- function serializeError(error) {
119
- try {
120
- return JSON.stringify(serializeError$1(error));
121
- } catch (error2) {
122
- throw new SerializationError(error2.message);
123
- }
124
- }
125
- function deserializeError(serialized) {
126
- try {
127
- return deserializeError$1(JSON.parse(serialized));
128
- } catch (error) {
129
- throw new SerializationError(error.message);
130
- }
131
- }
132
-
133
- // core/internal/utils/sleep.ts
134
- function sleep(ms) {
135
- return new Promise((resolve) => setTimeout(resolve, ms));
136
- }
137
-
138
- // core/internal/db/retry.ts
139
- var RETRY_SQLSTATE_PREFIXES = /* @__PURE__ */ new Set([
140
- "08",
141
- // Connection Exception
142
- "40",
143
- // Transaction Rollback (deadlock_detected, serialization_failure)
144
- "53",
145
- // Insufficient Resources
146
- "55",
147
- // Object Not In Prerequisite State (lock_not_available)
148
- "57"
149
- // Operator Intervention (admin_shutdown, cannot_connect_now)
150
- ]);
151
- var RETRY_SQLSTATE_CODES = /* @__PURE__ */ new Set([
152
- "40003",
153
- // statement_completion_unknown
154
- "40001",
155
- // serialization_failure
156
- "40P01",
157
- // deadlock_detected
158
- "55P03"
159
- // lock_not_available
160
- ]);
161
- var RETRY_NODE_ERRNOS = /* @__PURE__ */ new Set([
162
- "ECONNRESET",
163
- "ECONNREFUSED",
164
- "EHOSTUNREACH",
165
- "ENETUNREACH",
166
- "ETIMEDOUT",
167
- "ECONNABORTED",
168
- "EPIPE"
169
- ]);
170
- function isPgDatabaseError(e) {
171
- return !!e && typeof e === "object" && typeof e.code === "string" && e.code.length === 5;
172
- }
173
- function sqlStateLooksRetryable(sqlstate) {
174
- if (!sqlstate) return false;
175
- if (RETRY_SQLSTATE_CODES.has(sqlstate)) return true;
176
- const prefix = sqlstate.toString().slice(0, 2);
177
- return RETRY_SQLSTATE_PREFIXES.has(prefix);
178
- }
179
- function nodeErrnoLooksRetryable(e) {
180
- const code = e.code;
181
- return !!code && RETRY_NODE_ERRNOS.has(code);
182
- }
183
- function messageLooksRetryable(msg) {
184
- const m = msg.toLowerCase();
185
- return msg.includes("ECONNREFUSED") || msg.includes("ECONNRESET") || m.includes("connection timeout") || m.includes("server closed the connection") || m.includes("connection terminated unexpectedly") || m.includes("client has encountered a connection error") || m.includes("timeout exceeded when trying to connect") || m.includes("could not connect to server") || m.includes("connection pool exhausted") || m.includes("too many clients");
186
- }
187
- function* unwrapErrors(e) {
188
- const queue = [e];
189
- const seen = /* @__PURE__ */ new Set();
190
- while (queue.length) {
191
- const cur = queue.shift();
192
- if (cur && typeof cur === "object") {
193
- if (seen.has(cur)) continue;
194
- seen.add(cur);
195
- const ae = cur;
196
- if (Array.isArray(ae.errors)) queue.push(...ae.errors);
197
- const withCause = cur;
198
- if (withCause.cause) queue.push(withCause.cause);
199
- const wrapped = cur;
200
- if (wrapped.error) queue.push(wrapped.error);
201
- }
202
- yield cur;
203
- }
204
- }
205
- function isRetriableDBError(err) {
206
- for (const e of unwrapErrors(err)) {
207
- const anyErr = e;
208
- if (isPgDatabaseError(anyErr) && sqlStateLooksRetryable(anyErr.code)) {
209
- return true;
210
- }
211
- if (nodeErrnoLooksRetryable(anyErr)) {
212
- return true;
213
- }
214
- if (e instanceof Error) {
215
- if (e.stack && messageLooksRetryable(e.stack)) return true;
216
- if (e.message && messageLooksRetryable(e.message)) return true;
217
- }
218
- if (messageLooksRetryable(String(e))) return true;
219
- }
220
- return false;
221
- }
222
- async function withDbRetry(fn, options = {}) {
223
- const { initialBackoffMs = 1e3, maxBackoffMs = 6e4, onRetry } = options;
224
- let attempt = 0;
225
- let backoffMs = initialBackoffMs;
226
- while (true) {
227
- try {
228
- return await fn();
229
- } catch (error) {
230
- if (!isRetriableDBError(error)) {
231
- throw error;
232
- }
233
- attempt++;
234
- const jitter = 0.5 + Math.random();
235
- const delayMs = Math.min(backoffMs * jitter, maxBackoffMs);
236
- if (onRetry) {
237
- onRetry(error, attempt, delayMs);
238
- } else {
239
- console.warn(
240
- `Database connection failed: ${error instanceof Error ? error.message : String(error)}. Retrying in ${(delayMs / 1e3).toFixed(2)}s (attempt ${attempt})`
241
- );
242
- }
243
- await sleep(delayMs);
244
- backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
245
- }
246
- }
247
- }
248
- function getExecutionContext() {
249
- const store = asyncLocalStorage.getStore();
250
- if (!store) {
251
- throw new RunOutsideOfWorkflowError();
252
- }
253
- return store;
254
- }
255
- var asyncLocalStorage = new AsyncLocalStorage();
256
- function runWithExecutionContext(store, callback) {
257
- return asyncLocalStorage.run(store, callback);
258
- }
259
-
260
- // core/internal/repository/get-run.ts
261
- async function getRun(db, runId) {
262
- const result = await db.selectFrom("runs").select(["id", "inputs", "output", "error", "status", "change_id"]).where("id", "=", runId).executeTakeFirst();
263
- if (!result) {
264
- return void 0;
265
- }
266
- return {
267
- id: result.id,
268
- input: result.inputs ?? void 0,
269
- output: result.output ?? void 0,
270
- error: result.error ?? void 0,
271
- status: result.status,
272
- changeId: result.change_id
273
- };
274
- }
275
-
276
- // core/internal/repository/insert-operation.ts
277
- async function insertOperation(tx, runId, operationName, sequenceId, result, error) {
278
- await tx.insertInto("operations").values({
279
- run_id: runId,
280
- name: operationName,
281
- sequence_id: sequenceId,
282
- output: result,
283
- error
284
- }).execute();
285
- }
286
-
287
- // core/internal/operation-manager.ts
288
- var OperationManager = class {
289
- constructor(db, runId, operations = []) {
290
- this.db = db;
291
- this.runId = runId;
292
- this.operations = operations;
293
- }
294
- sequenceId = 0;
295
- lastReservedSequenceId = null;
296
- getOperationResult() {
297
- const operation = this.operations.pop();
298
- if (operation) {
299
- this.sequenceId++;
300
- return operation;
301
- }
302
- return null;
303
- }
304
- reserveSequenceId() {
305
- const reserved = this.sequenceId++;
306
- this.lastReservedSequenceId = reserved;
307
- return reserved;
308
- }
309
- getCurrentSequenceId() {
310
- return this.sequenceId;
311
- }
312
- /**
313
- * Gets the sequence ID that was most recently reserved for the current operation.
314
- * This is the ID that will be (or was) recorded in the database for this operation.
315
- * Returns null if no sequence ID has been reserved yet.
316
- */
317
- getActiveSequenceId() {
318
- return this.lastReservedSequenceId;
319
- }
320
- async recordResult(operationName, sequenceId, result, tx) {
321
- if (tx) {
322
- await insertOperation(tx, this.runId, operationName, sequenceId, result ?? void 0);
323
- } else {
324
- await withDbRetry(async () => {
325
- await insertOperation(this.db, this.runId, operationName, sequenceId, result ?? void 0);
326
- });
327
- }
328
- }
329
- async recordError(operationName, sequenceId, error, tx) {
330
- if (tx) {
331
- await insertOperation(
332
- tx,
333
- this.runId,
334
- operationName,
335
- sequenceId,
336
- void 0,
337
- error ?? void 0
338
- );
339
- } else {
340
- await withDbRetry(async () => {
341
- await insertOperation(
342
- this.db,
343
- this.runId,
344
- operationName,
345
- sequenceId,
346
- void 0,
347
- error ?? void 0
348
- );
349
- });
350
- }
351
- }
352
- };
353
- function returnOrThrowOperationResult(op) {
354
- if (op.error) {
355
- throw deserializeError(op.error);
356
- }
357
- if (op.result === null || op.result === void 0) {
358
- return void 0;
359
- }
360
- return deserialize(op.result);
361
- }
362
- async function executeAndRecordOperation(operationManager, operationName, callback) {
363
- const seqId = operationManager.reserveSequenceId();
364
- try {
365
- const result = await callback();
366
- const serializedResult = serialize(result);
367
- await checkCancellation();
368
- await operationManager.recordResult(operationName, seqId, serializedResult);
369
- return result;
370
- } catch (error) {
371
- if (error instanceof RunCancelledError) {
372
- throw error;
373
- }
374
- const err = error instanceof Error ? error : new Error(String(error));
375
- await operationManager.recordError(operationName, seqId, serializeError(err));
376
- throw error;
377
- }
378
- }
379
- async function checkCancellation() {
380
- const { abortSignal, runId, db } = getExecutionContext();
381
- if (abortSignal.aborted) {
382
- throw new RunCancelledError();
383
- }
384
- const run = await withDbRetry(async () => getRun(db, runId));
385
- if (run?.status === "CANCELLED" /* CANCELLED */) {
386
- throw new RunCancelledError();
387
- }
388
- }
389
-
390
- // client/utils.ts
391
- function createExecutionContext({
392
- ctx,
393
- abortSignal,
394
- runId,
395
- runPath,
396
- operations
397
- }) {
398
- return {
399
- runId,
400
- runPath,
401
- executorId: ctx.executorId,
402
- abortSignal,
403
- operationManager: new OperationManager(ctx.db, runId, operations || []),
404
- messageEventBus: ctx.messageEventBus,
405
- stateEventBus: ctx.stateEventBus,
406
- workflowRegistry: ctx.workflowRegistry,
407
- runEventBus: ctx.runEventBus,
408
- runRegistry: ctx.runRegistry,
409
- queueRegistry: ctx.queueRegistry,
410
- db: ctx.db
411
- };
412
- }
413
- async function recordRunResult(db, runId, result) {
414
- const [{ change_id }] = await db.updateTable("runs").set({
415
- output: result.result,
416
- error: result.error,
417
- status: result.error ? "ERROR" /* ERROR */ : "SUCCESS" /* SUCCESS */,
418
- updated_at: sql`(extract(epoch from now()) * 1000)::bigint`
419
- }).where("id", "=", runId).returning(["change_id"]).execute();
420
- return change_id;
421
- }
422
- async function cancelRun(runId, db) {
423
- return withDbRetry(async () => {
424
- return db.transaction().execute(async (tx) => {
425
- const result = await tx.updateTable("runs").set({
426
- status: "CANCELLED" /* CANCELLED */,
427
- updated_at: sql`(extract(epoch from now()) * 1000)::bigint`
428
- }).where(
429
- (eb) => eb.and([
430
- eb("id", "=", runId),
431
- eb("status", "not in", [
432
- "CANCELLED" /* CANCELLED */,
433
- "SUCCESS" /* SUCCESS */,
434
- "ERROR" /* ERROR */
435
- ])
436
- ])
437
- ).returning(["change_id", "path"]).executeTakeFirst();
438
- if (!result) {
439
- const exists = await tx.selectFrom("runs").select([]).where("id", "=", runId).executeTakeFirst();
440
- if (exists) {
441
- return void 0;
442
- }
443
- throw new RunNotFoundError(runId);
444
- }
445
- await sql`
446
- UPDATE runs
447
- SET
448
- status = ${"CANCELLED" /* CANCELLED */},
449
- updated_at = (extract(epoch from now()) * 1000)::bigint
450
- WHERE path @> ARRAY[${runId}]::text[]
451
- AND id != ${runId}
452
- AND status NOT IN (${"CANCELLED" /* CANCELLED */}, ${"SUCCESS" /* SUCCESS */}, ${"ERROR" /* ERROR */})
453
- `.execute(tx);
454
- return {
455
- path: result.path
456
- };
457
- });
458
- });
459
- }
460
-
461
- // core/internal/execute-workflow.ts
462
- async function executeWorkflow(ctx, params) {
463
- const { db, runRegistry } = ctx;
464
- const { options, runId, runPath, fn, args, operations } = params;
465
- const abortController = new AbortController();
466
- const [deadline] = getDeadlineAndReason({
467
- timeout: options?.timeout,
468
- deadline: options?.deadline
469
- });
470
- const runStore = createExecutionContext({
471
- ctx,
472
- abortSignal: AbortSignal.any(
473
- [abortController.signal].concat(deadline ? [AbortSignal.timeout(deadline - Date.now())] : [])
474
- ),
475
- runId,
476
- runPath,
477
- operations
478
- });
479
- const executionPromise = (async () => {
480
- try {
481
- const result = await runWithExecutionContext(runStore, async () => {
482
- return await runWithTimeout(async () => {
483
- return await fn(...args);
484
- });
485
- });
486
- await recordRunResult(db, runId, { result: result ? serialize(result) : void 0 });
487
- return result;
488
- } catch (error) {
489
- await recordRunResult(db, runId, { error: serializeError(error) });
490
- throw error;
491
- } finally {
492
- runRegistry.unregisterRun(runId);
493
- }
494
- })();
495
- runRegistry.registerRun(runId, {
496
- store: runStore,
497
- promise: executionPromise,
498
- abortController
499
- });
500
- }
501
- function getDeadlineAndReason({
502
- timeout,
503
- deadline
504
- }) {
505
- const now = Date.now();
506
- const timeoutDeadline = timeout ? now + timeout : void 0;
507
- if (timeoutDeadline && deadline) {
508
- return [
509
- Math.min(timeoutDeadline, deadline),
510
- timeoutDeadline < deadline ? "timeout" : "deadline"
511
- ];
512
- } else if (timeoutDeadline) {
513
- return [timeoutDeadline, "timeout"];
514
- } else if (deadline) {
515
- return [deadline, "deadline"];
516
- }
517
- return [void 0, void 0];
518
- }
519
- async function runWithTimeout(fn) {
520
- const { runId, db, abortSignal } = getExecutionContext();
521
- const abortPromise = new Promise((_, reject) => {
522
- abortSignal.throwIfAborted();
523
- abortSignal.addEventListener(
524
- "abort",
525
- () => {
526
- if (abortSignal.reason?.name === "TimeoutError") {
527
- reject(new TimeoutError(`Workflow timed out`));
528
- return;
529
- }
530
- reject(new RunCancelledError());
531
- },
532
- { once: true }
533
- );
534
- });
535
- const callPromise = fn();
536
- try {
537
- return await Promise.race([callPromise, abortPromise]);
538
- } catch (error) {
539
- if (error instanceof TimeoutError || error instanceof DeadlineError) {
540
- await cancelRun(runId, db);
541
- }
542
- await callPromise.catch(() => {
543
- });
544
- throw error;
545
- }
546
- }
547
-
548
- // core/internal/repository/get-run-status.ts
549
- async function getRunStatus(db, runId) {
550
- const run = await db.selectFrom("runs").select("status").where("id", "=", runId).executeTakeFirst();
551
- if (!run) {
552
- throw new RunNotFoundError(runId);
553
- }
554
- return run.status;
555
- }
556
-
557
- // client/run.ts
558
- function createRunHandle(runtimeContext, id) {
559
- const { runRegistry, db, runEventBus } = runtimeContext;
560
- const run = runRegistry.getRun(id);
561
- if (run) {
562
- return {
563
- id,
564
- status: () => getRunStatusFromRegistry(run),
565
- result: () => run.promise
566
- };
567
- }
568
- return {
569
- id,
570
- status: () => getRunStatus(db, id),
571
- result: () => getRunResult(id, runEventBus, db)
572
- };
573
- }
574
- async function getRunStatusFromRegistry(runEntry) {
575
- if (runEntry.store.abortSignal.aborted) {
576
- return "CANCELLED" /* CANCELLED */;
577
- }
578
- const promiseState = runEntry.getPromiseState();
579
- if (promiseState === "pending") {
580
- return "PENDING" /* PENDING */;
581
- }
582
- if (promiseState === "fulfilled") {
583
- return "SUCCESS" /* SUCCESS */;
584
- }
585
- return "ERROR" /* ERROR */;
586
- }
587
- async function getRunResult(id, runEventBus, db) {
588
- const run = await getRun(db, id);
589
- if (!run) {
590
- throw new RunNotFoundError(id);
591
- }
592
- if (run.status === "CANCELLED" /* CANCELLED */) {
593
- throw new RunCancelledError();
594
- }
595
- if (run.status === "ERROR" /* ERROR */) {
596
- throw run.error ? deserializeError(run.error) : void 0;
597
- }
598
- return new Promise((resolve, reject) => {
599
- const unsubscribe = runEventBus.subscribe(id, "*", async (e) => {
600
- if (e.status === "CANCELLED" /* CANCELLED */) {
601
- unsubscribe();
602
- reject(new RunCancelledError());
603
- return;
604
- }
605
- if (e.status === "SUCCESS" /* SUCCESS */ || e.status === "ERROR" /* ERROR */) {
606
- unsubscribe();
607
- try {
608
- const completedRun = await getRun(db, id);
609
- if (!completedRun) {
610
- reject(new RunNotFoundError(id));
611
- return;
612
- }
613
- if (completedRun.status === "ERROR" /* ERROR */) {
614
- reject(
615
- completedRun.error ? deserializeError(completedRun.error) : new Error("error")
616
- );
617
- return;
618
- }
619
- resolve(
620
- completedRun.output ? deserialize(completedRun.output) : void 0
621
- );
622
- } catch (error) {
623
- reject(error);
624
- }
625
- }
626
- });
627
- });
628
- }
629
- async function insertPendingRun(db, options) {
630
- const result = await db.insertInto("runs").values({
631
- id: options.runId,
632
- path: options.path,
633
- inputs: options.inputs,
634
- executor_id: options.executorId,
635
- workflow_name: options.workflowName,
636
- status: "PENDING" /* PENDING */,
637
- started_at_epoch_ms: sql`(extract(epoch from now()) * 1000)::bigint`,
638
- created_at: sql`(extract(epoch from now()) * 1000)::bigint`,
639
- updated_at: sql`(extract(epoch from now()) * 1000)::bigint`
640
- }).returning(["id", "path", "change_id"]).executeTakeFirst();
641
- return {
642
- runId: result.id,
643
- path: result.path,
644
- changeId: result.change_id
645
- };
646
- }
647
-
648
- export { ErrorThatShouldNeverHappen, FatalError, MaxRecoveryAttemptsExceededError, MaxRetriesExceededError, QueueNotFoundError, RunCancelledError, RunNotFoundError, TimeoutError, WorkflowNotFoundError, WorkflowStatus, cancelRun, createRunHandle, defineWorkflow, deserialize, deserializeError, executeAndRecordOperation, executeWorkflow, getExecutionContext, getRun, insertPendingRun, returnOrThrowOperationResult, serialize, serializeError, sleep, withDbRetry };
649
- //# sourceMappingURL=chunk-WKVKC6AI.js.map
650
- //# sourceMappingURL=chunk-WKVKC6AI.js.map