semola 0.5.4 → 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 (36) hide show
  1. package/README.md +18 -45
  2. package/dist/chunk-CKQMccvm.cjs +28 -0
  3. package/dist/lib/api/index.cjs +29 -15
  4. package/dist/lib/api/index.mjs +30 -16
  5. package/dist/lib/cache/index.cjs +47 -22
  6. package/dist/lib/cache/index.d.cts +3 -24
  7. package/dist/lib/cache/index.d.mts +3 -24
  8. package/dist/lib/cache/index.mjs +48 -23
  9. package/dist/lib/cron/index.cjs +117 -117
  10. package/dist/lib/cron/index.mjs +118 -118
  11. package/dist/lib/errors/index.d.cts +12 -1
  12. package/dist/lib/errors/index.d.mts +12 -1
  13. package/dist/lib/logging/index.cjs +1 -0
  14. package/dist/lib/orm/index.cjs +1642 -0
  15. package/dist/lib/orm/index.d.cts +402 -0
  16. package/dist/lib/orm/index.d.mts +402 -0
  17. package/dist/lib/orm/index.mjs +1630 -0
  18. package/dist/lib/prompts/index.cjs +89 -89
  19. package/dist/lib/prompts/index.d.cts +12 -33
  20. package/dist/lib/prompts/index.d.mts +12 -33
  21. package/dist/lib/prompts/index.mjs +89 -90
  22. package/dist/lib/pubsub/index.cjs +43 -19
  23. package/dist/lib/pubsub/index.d.cts +3 -18
  24. package/dist/lib/pubsub/index.d.mts +3 -18
  25. package/dist/lib/pubsub/index.mjs +44 -20
  26. package/dist/lib/queue/index.cjs +40 -10
  27. package/dist/lib/queue/index.d.cts +11 -4
  28. package/dist/lib/queue/index.d.mts +11 -4
  29. package/dist/lib/queue/index.mjs +39 -11
  30. package/dist/lib/workflow/index.cjs +285 -282
  31. package/dist/lib/workflow/index.d.cts +76 -11
  32. package/dist/lib/workflow/index.d.mts +76 -11
  33. package/dist/lib/workflow/index.mjs +278 -284
  34. package/package.json +11 -1
  35. package/dist/index-BhGNDjPq.d.mts +0 -13
  36. package/dist/index-DxSbeGP-.d.cts +0 -13
@@ -1,10 +1,70 @@
1
- import { err, mightThrow, mightThrowSync, ok } from "../errors/index.mjs";
1
+ import { mightThrow, mightThrowSync } from "../errors/index.mjs";
2
+ import assert from "node:assert";
3
+ //#region src/lib/workflow/errors.ts
4
+ var WorkflowError = class extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "WorkflowError";
8
+ }
9
+ };
10
+ var NotFoundError = class extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "NotFoundError";
14
+ }
15
+ };
16
+ var StateError = class extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "StateError";
20
+ }
21
+ };
22
+ var SerializationError = class extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "SerializationError";
26
+ }
27
+ };
28
+ var ExecutionError = class extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = "ExecutionError";
32
+ }
33
+ };
34
+ var LockError = class extends Error {
35
+ constructor(message) {
36
+ super(message);
37
+ this.name = "LockError";
38
+ }
39
+ };
40
+ var CancelledError = class extends Error {
41
+ constructor(message) {
42
+ super(message);
43
+ this.name = "CancelledError";
44
+ }
45
+ };
46
+ //#endregion
2
47
  //#region src/lib/workflow/index.ts
3
48
  const DEFAULT_LOCK_TTL = 300 * 1e3;
49
+ const DEFAULT_RETRIES = 3;
50
+ const DEFAULT_RETRY_BASE_DELAY = 1e3;
51
+ const DEFAULT_RETRY_MULTIPLIER = 2;
52
+ const DEFAULT_RETRY_MAX_DELAY = 3e4;
4
53
  const now = () => Date.now();
5
- const toErrorMessage = (error) => {
6
- if (error instanceof Error) return error.message;
7
- return String(error);
54
+ const delay = async (ms, signal, isCancelled) => {
55
+ if (signal.aborted) throw new CancelledError("Workflow execution was aborted during retry backoff");
56
+ const deadline = now() + ms;
57
+ const pollInterval = 50;
58
+ while (now() < deadline) {
59
+ if (signal.aborted) throw new CancelledError("Workflow execution was aborted during retry backoff");
60
+ if (isCancelled) {
61
+ if (await isCancelled()) throw new CancelledError("Workflow execution was cancelled during retry backoff");
62
+ }
63
+ const remaining = deadline - now();
64
+ await new Promise((resolve) => {
65
+ setTimeout(resolve, Math.min(pollInterval, remaining));
66
+ });
67
+ }
8
68
  };
9
69
  const envelopeSerialize = (value) => {
10
70
  return JSON.stringify({ value });
@@ -26,69 +86,67 @@ const knownStatuses = [
26
86
  var WorkflowDefinition = class {
27
87
  options;
28
88
  lockTTL;
89
+ retries;
90
+ retryBaseDelay;
91
+ retryMultiplier;
92
+ retryMaxDelay;
29
93
  constructor(options) {
30
94
  this.options = options;
31
95
  this.lockTTL = options.lockTTL ?? DEFAULT_LOCK_TTL;
96
+ this.retries = options.retries ?? DEFAULT_RETRIES;
97
+ this.retryBaseDelay = options.retryBackoff?.baseDelay ?? DEFAULT_RETRY_BASE_DELAY;
98
+ this.retryMultiplier = options.retryBackoff?.multiplier ?? DEFAULT_RETRY_MULTIPLIER;
99
+ this.retryMaxDelay = options.retryBackoff?.maxDelay ?? DEFAULT_RETRY_MAX_DELAY;
100
+ assert.ok(Number.isFinite(this.retries) && this.retries >= 0, "Invalid retries: must be a non-negative finite number");
101
+ assert.ok(Number.isFinite(this.retryBaseDelay) && this.retryBaseDelay > 0, "Invalid retryBackoff.baseDelay: must be a positive finite number");
102
+ assert.ok(Number.isFinite(this.retryMultiplier) && this.retryMultiplier > 0, "Invalid retryBackoff.multiplier: must be a positive finite number");
103
+ assert.ok(Number.isFinite(this.retryMaxDelay) && this.retryMaxDelay > 0, "Invalid retryBackoff.maxDelay: must be a positive finite number");
104
+ }
105
+ runHook(hook) {
106
+ return mightThrow(Promise.resolve().then(() => hook()));
107
+ }
108
+ computeBackoffDelay(attempt) {
109
+ return Math.min(this.retryBaseDelay * this.retryMultiplier ** (attempt - 1), this.retryMaxDelay);
32
110
  }
33
111
  async start(input, options) {
34
112
  const executionId = options?.executionId ?? crypto.randomUUID();
35
- const [createError] = await this.createExecution(executionId, input);
36
- if (createError) return err(createError.type, createError.message);
113
+ await this.createExecution(executionId, input);
37
114
  return this.execute(executionId, input);
38
115
  }
39
116
  async run(input, options) {
40
- const [startError, startData] = await this.start(input, options);
41
- if (startError) return err(startError.type, startError.message);
42
- if (!startData) return err("WorkflowError", "Unexpected empty start result");
43
- if (startData.status === "cancelled") return err("WorkflowCancelledError", `Workflow execution ${startData.executionId} was cancelled`);
44
- if (startData.status !== "completed") return err("WorkflowExecutionError", `Workflow execution ${startData.executionId} did not complete`);
45
- const [getError, execution] = await this.get(startData.executionId);
46
- if (getError) return err(getError.type, getError.message);
47
- if (!execution) return err("WorkflowError", "Unexpected empty execution");
48
- return ok(execution.result);
117
+ const startData = await this.start(input, options);
118
+ if (startData.status === "cancelled") throw new CancelledError(`Workflow execution ${startData.executionId} was cancelled`);
119
+ return (await this.get(startData.executionId)).result;
49
120
  }
50
121
  async resume(executionId) {
51
- const [getError, execution] = await this.get(executionId);
52
- if (getError) return err(getError.type, getError.message);
53
- if (!execution) return err("WorkflowNotFoundError", `Workflow execution ${executionId} not found`);
54
- if (execution.status === "completed") return ok({
122
+ const execution = await this.get(executionId);
123
+ if (execution.status === "completed") return {
55
124
  executionId,
56
125
  status: execution.status
57
- });
58
- if (execution.status === "cancelled") return ok({
126
+ };
127
+ if (execution.status === "cancelled") return {
59
128
  executionId,
60
129
  status: execution.status
61
- });
130
+ };
62
131
  return this.execute(executionId, execution.input);
63
132
  }
64
133
  async get(executionId) {
65
- const [statusError, status] = await this.getMeta(executionId, "status");
66
- if (statusError) return err(statusError.type, statusError.message);
67
- if (!status) return err("WorkflowNotFoundError", `Workflow execution ${executionId} not found`);
134
+ const status = await this.getMeta(executionId, "status");
135
+ if (!status) throw new NotFoundError(`Workflow execution ${executionId} not found`);
68
136
  const normalizedStatus = this.normalizeStatus(status);
69
- if (!normalizedStatus) return err("WorkflowStateError", `Workflow execution ${executionId} has invalid status ${status}`);
70
- const [inputError, input] = await this.readInput(executionId);
71
- if (inputError) return err(inputError.type, inputError.message);
72
- if (input === null) return err("WorkflowStateError", `Workflow execution ${executionId} has invalid input`);
73
- const [resultError, result] = await this.readResult(executionId);
74
- if (resultError) return err(resultError.type, resultError.message);
75
- const [stepsError, steps] = await this.readStepSnapshots(executionId);
76
- if (stepsError) return err(stepsError.type, stepsError.message);
77
- const [createdAtError, createdAt] = await this.readNumberMeta(executionId, "createdAt");
78
- if (createdAtError) return err(createdAtError.type, createdAtError.message);
79
- if (createdAt === null) return err("WorkflowStateError", `Workflow execution ${executionId} is missing createdAt`);
80
- const [updatedAtError, updatedAt] = await this.readNumberMeta(executionId, "updatedAt");
81
- if (updatedAtError) return err(updatedAtError.type, updatedAtError.message);
82
- if (updatedAt === null) return err("WorkflowStateError", `Workflow execution ${executionId} is missing updatedAt`);
83
- const [metaError, errorMessage] = await this.getMeta(executionId, "error");
84
- if (metaError) return err(metaError.type, metaError.message);
85
- const [completedAtError, completedAt] = await this.readNumberMeta(executionId, "completedAt");
86
- if (completedAtError) return err(completedAtError.type, completedAtError.message);
87
- const [failedAtError, failedAt] = await this.readNumberMeta(executionId, "failedAt");
88
- if (failedAtError) return err(failedAtError.type, failedAtError.message);
89
- const [cancelledAtError, cancelledAt] = await this.readNumberMeta(executionId, "cancelledAt");
90
- if (cancelledAtError) return err(cancelledAtError.type, cancelledAtError.message);
91
- return ok({
137
+ if (!normalizedStatus) throw new StateError(`Workflow execution ${executionId} has invalid status ${status}`);
138
+ const input = await this.readInput(executionId);
139
+ const result = await this.readResult(executionId);
140
+ const steps = await this.readStepSnapshots(executionId);
141
+ const createdAt = await this.readNumberMeta(executionId, "createdAt");
142
+ const updatedAt = await this.readNumberMeta(executionId, "updatedAt");
143
+ const errorMessage = await this.getMeta(executionId, "error");
144
+ const completedAt = await this.readNumberMeta(executionId, "completedAt");
145
+ const failedAt = await this.readNumberMeta(executionId, "failedAt");
146
+ const cancelledAt = await this.readNumberMeta(executionId, "cancelledAt");
147
+ if (createdAt === null) throw new StateError(`Workflow execution ${executionId} is missing createdAt`);
148
+ if (updatedAt === null) throw new StateError(`Workflow execution ${executionId} is missing updatedAt`);
149
+ return {
92
150
  id: executionId,
93
151
  name: this.options.name,
94
152
  status: normalizedStatus,
@@ -101,31 +159,24 @@ var WorkflowDefinition = class {
101
159
  failedAt,
102
160
  cancelledAt,
103
161
  steps
104
- });
162
+ };
105
163
  }
106
164
  async cancel(executionId) {
107
- const [getError, execution] = await this.get(executionId);
108
- if (getError) return err(getError.type, getError.message);
109
- if (!execution) return err("WorkflowNotFoundError", `Workflow execution ${executionId} not found`);
110
- if (execution.status === "completed") return err("WorkflowStateError", `Workflow execution ${executionId} is already completed`);
165
+ const execution = await this.get(executionId);
166
+ if (execution.status === "completed") throw new StateError(`Workflow execution ${executionId} is already completed`);
111
167
  const timestamp = now();
112
- const [statusError] = await this.setMeta(executionId, "status", "cancelled");
113
- if (statusError) return err(statusError.type, statusError.message);
114
- const [updatedAtError] = await this.setMeta(executionId, "updatedAt", String(timestamp));
115
- if (updatedAtError) return err(updatedAtError.type, updatedAtError.message);
116
- const [cancelledAtError] = await this.setMeta(executionId, "cancelledAt", String(timestamp));
117
- if (cancelledAtError) return err(cancelledAtError.type, cancelledAtError.message);
118
- const [clearErrorError] = await this.setMeta(executionId, "error", "");
119
- if (clearErrorError) return err(clearErrorError.type, clearErrorError.message);
120
- const [clearFailedAtError] = await this.setMeta(executionId, "failedAt", "");
121
- if (clearFailedAtError) return err(clearFailedAtError.type, clearFailedAtError.message);
122
- return ok({
168
+ await this.setMeta(executionId, "status", "cancelled");
169
+ await this.setMeta(executionId, "updatedAt", String(timestamp));
170
+ await this.setMeta(executionId, "cancelledAt", String(timestamp));
171
+ await this.setMeta(executionId, "error", "");
172
+ await this.setMeta(executionId, "failedAt", "");
173
+ return {
123
174
  executionId,
124
175
  createdAt: execution.createdAt,
125
176
  cancelledAt: timestamp,
126
177
  updatedAt: timestamp,
127
178
  status: "cancelled"
128
- });
179
+ };
129
180
  }
130
181
  executionKey(executionId) {
131
182
  return `workflow:${this.options.name}:execution:${executionId}`;
@@ -140,12 +191,9 @@ var WorkflowDefinition = class {
140
191
  return `${this.executionKey(executionId)}:lock`;
141
192
  }
142
193
  async createExecution(executionId, input) {
143
- const [serializeError, serializedInput] = this.serializeInput(input);
144
- if (serializeError) return err("WorkflowSerializationError", `Unable to serialize workflow input for ${executionId}`);
194
+ const serializedInput = this.serializeInput(input);
145
195
  const timestamp = now();
146
- const [statusReadError, existingStatus] = await this.getMeta(executionId, "status");
147
- if (statusReadError) return err(statusReadError.type, statusReadError.message);
148
- if (existingStatus) return err("WorkflowStateError", `Workflow execution ${executionId} already exists`);
196
+ if (await this.getMeta(executionId, "status")) throw new StateError(`Workflow execution ${executionId} already exists`);
149
197
  const metadata = {
150
198
  status: "pending",
151
199
  input: serializedInput,
@@ -159,61 +207,44 @@ var WorkflowDefinition = class {
159
207
  steps: "[]"
160
208
  };
161
209
  const [writeError] = await mightThrow(this.options.redis.hset(this.metaKey(executionId), metadata));
162
- if (writeError) return err("WorkflowError", `Unable to persist metadata for execution ${executionId}`);
163
- return ok(null);
210
+ if (writeError) throw new WorkflowError(`Unable to persist metadata for execution ${executionId}`);
164
211
  }
165
212
  async execute(executionId, input) {
166
213
  const token = crypto.randomUUID();
167
- const [lockError] = await this.acquireLock(executionId, token);
168
- if (lockError) return err(lockError.type, lockError.message);
169
- const [statusCheckError, currentStatus] = await this.getMeta(executionId, "status");
170
- if (statusCheckError) {
214
+ await this.acquireLock(executionId, token);
215
+ if (await this.getMeta(executionId, "status") === "cancelled") {
171
216
  await this.releaseLock(executionId, token);
172
- return err(statusCheckError.type, statusCheckError.message);
173
- }
174
- if (currentStatus === "cancelled") {
175
- await this.releaseLock(executionId, token);
176
- return err("WorkflowStateError", `Workflow execution ${executionId} was cancelled`);
217
+ throw new StateError(`Workflow execution ${executionId} was cancelled`);
177
218
  }
178
219
  const timestamp = now();
179
- const [runningStatusError] = await this.setMeta(executionId, "status", "running");
180
- if (runningStatusError) {
181
- await this.releaseLock(executionId, token);
182
- return err(runningStatusError.type, runningStatusError.message);
183
- }
184
- const [runningUpdatedAtError] = await this.setMeta(executionId, "updatedAt", String(timestamp));
185
- if (runningUpdatedAtError) {
186
- await this.releaseLock(executionId, token);
187
- return err(runningUpdatedAtError.type, runningUpdatedAtError.message);
188
- }
220
+ await this.setMeta(executionId, "status", "running");
221
+ await this.setMeta(executionId, "updatedAt", String(timestamp));
189
222
  const controller = new AbortController();
190
223
  const renewInterval = Math.floor(this.lockTTL / 3);
191
224
  let lockLost = false;
192
225
  const renewTimer = setInterval(async () => {
193
- const [renewError] = await this.extendLock(executionId, token);
226
+ const [renewError] = await mightThrow(this.extendLock(executionId, token));
194
227
  if (renewError) {
195
228
  lockLost = true;
196
229
  controller.abort();
197
230
  clearInterval(renewTimer);
198
231
  }
199
232
  }, renewInterval);
233
+ if (this.options.hooks?.onStart) await this.runHook(() => this.options.hooks?.onStart?.({
234
+ executionId,
235
+ input
236
+ }));
200
237
  const step = async (name, handler) => {
201
- const [cancelledError, cancelled] = await this.isCancelled(executionId);
202
- if (cancelledError) return Promise.reject(new Error(cancelledError.message));
203
- if (cancelled) {
238
+ await this.throwIfCancelled(executionId, () => {
204
239
  controller.abort();
205
- return Promise.reject(/* @__PURE__ */ new Error("Workflow cancelled"));
206
- }
207
- const [readError, cachedStep] = await this.readStepOutput(executionId, name);
208
- if (readError) return Promise.reject(new Error(readError.message));
240
+ });
241
+ const cachedStep = await this.readStepOutput(executionId, name);
209
242
  if (cachedStep.found) return cachedStep.value;
210
- const [stepError, output] = await mightThrow(Promise.resolve(handler(input, controller.signal)));
211
- if (stepError) return Promise.reject(stepError);
212
- const [writeError] = await this.writeStepOutput(executionId, name, output);
213
- if (writeError) return Promise.reject(new Error(writeError.message));
214
- return output;
243
+ return this.runStepWithRetries(executionId, input, name, handler, controller.signal, () => {
244
+ controller.abort();
245
+ });
215
246
  };
216
- const [handlerError, result] = await mightThrow(Promise.resolve(this.options.handler({
247
+ const handlerOutcome = await mightThrow(Promise.resolve(this.options.handler({
217
248
  input,
218
249
  executionId,
219
250
  signal: controller.signal,
@@ -222,188 +253,166 @@ var WorkflowDefinition = class {
222
253
  clearInterval(renewTimer);
223
254
  if (lockLost) {
224
255
  await this.releaseLock(executionId, token);
225
- return err("WorkflowLockError", `Lock expired during execution ${executionId}`);
226
- }
227
- const [cancelledError, cancelled] = await this.isCancelled(executionId);
228
- if (cancelledError) {
229
- await this.releaseLock(executionId, token);
230
- return err(cancelledError.type, cancelledError.message);
256
+ throw new LockError(`Lock expired during execution ${executionId}`);
231
257
  }
232
- if (cancelled) {
258
+ if (await this.isCancelled(executionId)) {
233
259
  const cancelledAt = now();
234
- const [cancelledStatusError] = await this.setMeta(executionId, "status", "cancelled");
235
- if (cancelledStatusError) {
236
- await this.releaseLock(executionId, token);
237
- return err(cancelledStatusError.type, cancelledStatusError.message);
238
- }
239
- const [cancelledUpdatedAtError] = await this.setMeta(executionId, "updatedAt", String(cancelledAt));
240
- if (cancelledUpdatedAtError) {
241
- await this.releaseLock(executionId, token);
242
- return err(cancelledUpdatedAtError.type, cancelledUpdatedAtError.message);
243
- }
244
- const [cancelledAtError] = await this.setMeta(executionId, "cancelledAt", String(cancelledAt));
245
- if (cancelledAtError) {
246
- await this.releaseLock(executionId, token);
247
- return err(cancelledAtError.type, cancelledAtError.message);
248
- }
260
+ await this.setMeta(executionId, "status", "cancelled");
261
+ await this.setMeta(executionId, "updatedAt", String(cancelledAt));
262
+ await this.setMeta(executionId, "cancelledAt", String(cancelledAt));
263
+ if (this.options.hooks?.onCancel) await this.runHook(() => this.options.hooks?.onCancel?.({
264
+ executionId,
265
+ input
266
+ }));
249
267
  await this.releaseLock(executionId, token);
250
- return ok({
268
+ return {
251
269
  executionId,
252
270
  status: "cancelled"
253
- });
271
+ };
254
272
  }
273
+ const [handlerError, result] = handlerOutcome;
255
274
  if (handlerError) {
256
275
  const failedAt = now();
257
- const [failedStatusError] = await this.setMeta(executionId, "status", "failed");
258
- if (failedStatusError) {
259
- await this.releaseLock(executionId, token);
260
- return err(failedStatusError.type, failedStatusError.message);
261
- }
262
- const [failedMessageError] = await this.setMeta(executionId, "error", toErrorMessage(handlerError));
263
- if (failedMessageError) {
264
- await this.releaseLock(executionId, token);
265
- return err(failedMessageError.type, failedMessageError.message);
266
- }
267
- const [failedUpdatedAtError] = await this.setMeta(executionId, "updatedAt", String(failedAt));
268
- if (failedUpdatedAtError) {
269
- await this.releaseLock(executionId, token);
270
- return err(failedUpdatedAtError.type, failedUpdatedAtError.message);
271
- }
272
- const [failedAtError] = await this.setMeta(executionId, "failedAt", String(failedAt));
273
- if (failedAtError) {
274
- await this.releaseLock(executionId, token);
275
- return err(failedAtError.type, failedAtError.message);
276
- }
276
+ await this.setMeta(executionId, "status", "failed");
277
+ await this.setMeta(executionId, "error", handlerError.message);
278
+ await this.setMeta(executionId, "updatedAt", String(failedAt));
279
+ await this.setMeta(executionId, "failedAt", String(failedAt));
277
280
  await this.releaseLock(executionId, token);
278
- return err("WorkflowExecutionError", `Workflow execution ${executionId} failed: ${toErrorMessage(handlerError)}`);
281
+ throw new ExecutionError(`Workflow execution ${executionId} failed: ${handlerError.message}`);
279
282
  }
280
- const [serializeResultError, serializedResult] = this.serializeResult(result);
283
+ const [serializeResultError, serializedResult] = mightThrowSync(() => this.serializeResult(result));
281
284
  if (serializeResultError) {
282
285
  const failedAt = now();
283
- const [failedStatusError] = await this.setMeta(executionId, "status", "failed");
284
- if (failedStatusError) {
285
- await this.releaseLock(executionId, token);
286
- return err(failedStatusError.type, failedStatusError.message);
287
- }
288
- const [failedMessageError] = await this.setMeta(executionId, "error", serializeResultError.message);
289
- if (failedMessageError) {
290
- await this.releaseLock(executionId, token);
291
- return err(failedMessageError.type, failedMessageError.message);
292
- }
293
- const [failedUpdatedAtError] = await this.setMeta(executionId, "updatedAt", String(failedAt));
294
- if (failedUpdatedAtError) {
295
- await this.releaseLock(executionId, token);
296
- return err(failedUpdatedAtError.type, failedUpdatedAtError.message);
297
- }
298
- const [failedAtError] = await this.setMeta(executionId, "failedAt", String(failedAt));
299
- if (failedAtError) {
300
- await this.releaseLock(executionId, token);
301
- return err(failedAtError.type, failedAtError.message);
302
- }
286
+ await this.setMeta(executionId, "status", "failed");
287
+ await this.setMeta(executionId, "error", serializeResultError.message);
288
+ await this.setMeta(executionId, "updatedAt", String(failedAt));
289
+ await this.setMeta(executionId, "failedAt", String(failedAt));
303
290
  await this.releaseLock(executionId, token);
304
- return err("WorkflowSerializationError", `Unable to serialize workflow result for ${executionId}`);
291
+ throw new SerializationError(`Unable to serialize workflow result for ${executionId}`);
305
292
  }
306
293
  const completedAt = now();
307
- const [completedResultError] = await this.setMeta(executionId, "result", serializedResult);
308
- if (completedResultError) {
309
- await this.releaseLock(executionId, token);
310
- return err(completedResultError.type, completedResultError.message);
311
- }
312
- const [completedStatusError] = await this.setMeta(executionId, "status", "completed");
313
- if (completedStatusError) {
314
- await this.releaseLock(executionId, token);
315
- return err(completedStatusError.type, completedStatusError.message);
316
- }
317
- const [completedClearErrorError] = await this.setMeta(executionId, "error", "");
318
- if (completedClearErrorError) {
319
- await this.releaseLock(executionId, token);
320
- return err(completedClearErrorError.type, completedClearErrorError.message);
321
- }
322
- const [completedClearFailedAtError] = await this.setMeta(executionId, "failedAt", "");
323
- if (completedClearFailedAtError) {
324
- await this.releaseLock(executionId, token);
325
- return err(completedClearFailedAtError.type, completedClearFailedAtError.message);
326
- }
327
- const [completedUpdatedAtError] = await this.setMeta(executionId, "updatedAt", String(completedAt));
328
- if (completedUpdatedAtError) {
329
- await this.releaseLock(executionId, token);
330
- return err(completedUpdatedAtError.type, completedUpdatedAtError.message);
331
- }
332
- const [completedAtError] = await this.setMeta(executionId, "completedAt", String(completedAt));
333
- if (completedAtError) {
334
- await this.releaseLock(executionId, token);
335
- return err(completedAtError.type, completedAtError.message);
336
- }
294
+ await this.setMeta(executionId, "result", serializedResult);
295
+ await this.setMeta(executionId, "status", "completed");
296
+ await this.setMeta(executionId, "error", "");
297
+ await this.setMeta(executionId, "failedAt", "");
298
+ await this.setMeta(executionId, "updatedAt", String(completedAt));
299
+ await this.setMeta(executionId, "completedAt", String(completedAt));
300
+ if (this.options.hooks?.onComplete) await this.runHook(() => this.options.hooks?.onComplete?.({
301
+ executionId,
302
+ input,
303
+ result
304
+ }));
337
305
  await this.releaseLock(executionId, token);
338
- return ok({
306
+ return {
339
307
  executionId,
340
308
  status: "completed"
341
- });
309
+ };
310
+ }
311
+ async throwIfCancelled(executionId, abort) {
312
+ if (await this.isCancelled(executionId)) {
313
+ abort();
314
+ throw new CancelledError(`Workflow execution ${executionId} was cancelled`);
315
+ }
316
+ }
317
+ async runStepWithRetries(executionId, input, stepName, handler, signal, abort) {
318
+ let attempt = 1;
319
+ const errorHistory = [];
320
+ while (true) {
321
+ await this.throwIfCancelled(executionId, abort);
322
+ const [stepError, output] = await mightThrow(Promise.resolve(handler(input, signal)));
323
+ if (!stepError) {
324
+ await this.writeStepOutput(executionId, stepName, output);
325
+ return output;
326
+ }
327
+ const errorMsg = stepError.message;
328
+ errorHistory.push({
329
+ attempt,
330
+ error: errorMsg,
331
+ timestamp: now()
332
+ });
333
+ if (attempt <= this.retries) {
334
+ const nextRetryDelayMs = this.computeBackoffDelay(attempt);
335
+ if (this.options.hooks?.onRetry) await this.runHook(() => this.options.hooks?.onRetry?.({
336
+ executionId,
337
+ input,
338
+ stepName,
339
+ error: errorMsg,
340
+ attempt,
341
+ nextRetryDelayMs,
342
+ retriesRemaining: this.retries - attempt
343
+ }));
344
+ const [delayError] = await mightThrow(delay(nextRetryDelayMs, signal, () => this.isCancelled(executionId)));
345
+ if (delayError) throw delayError;
346
+ attempt++;
347
+ continue;
348
+ }
349
+ if (this.options.hooks?.onError) await this.runHook(() => this.options.hooks?.onError?.({
350
+ executionId,
351
+ input,
352
+ stepName,
353
+ error: errorMsg,
354
+ totalAttempts: attempt,
355
+ errorHistory
356
+ }));
357
+ throw stepError;
358
+ }
342
359
  }
343
360
  async acquireLock(executionId, token) {
344
361
  const [lockError, lockResult] = await mightThrow(this.options.redis.set(this.lockKey(executionId), token, "PX", String(this.lockTTL), "NX"));
345
- if (lockError) return err("WorkflowLockError", `Unable to acquire lock for execution ${executionId}`);
346
- if (lockResult !== "OK") return err("WorkflowLockError", `Workflow execution ${executionId} is already running`);
347
- return ok(null);
362
+ if (lockError) throw new LockError(`Unable to acquire lock for execution ${executionId}`);
363
+ if (lockResult !== "OK") throw new LockError(`Workflow execution ${executionId} is already running`);
348
364
  }
349
365
  async releaseLock(executionId, token) {
350
- const [evalError] = await mightThrow(this.options.redis.send("EVAL", [
366
+ await mightThrow(this.options.redis.send("EVAL", [
351
367
  "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end",
352
368
  "1",
353
369
  this.lockKey(executionId),
354
370
  token
355
371
  ]));
356
- if (evalError) return err("WorkflowLockError", `Unable to release lock for execution ${executionId}`);
357
- return ok(null);
358
372
  }
359
373
  async extendLock(executionId, token) {
360
- const [evalError, result] = await mightThrow(this.options.redis.send("EVAL", [
374
+ const [evalError, extendResult] = await mightThrow(this.options.redis.send("EVAL", [
361
375
  "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) else return 0 end",
362
376
  "1",
363
377
  this.lockKey(executionId),
364
378
  token,
365
379
  String(this.lockTTL)
366
380
  ]));
367
- if (evalError) return err("WorkflowLockError", `Unable to extend lock for execution ${executionId}`);
368
- if (result === 0) return err("WorkflowLockError", `Lock ownership lost for execution ${executionId}`);
369
- return ok(null);
381
+ if (evalError) throw new LockError(`Unable to extend lock for execution ${executionId}`);
382
+ if (extendResult === 0) throw new LockError(`Lock ownership lost for execution ${executionId}`);
370
383
  }
371
384
  async isCancelled(executionId) {
372
- const [statusError, status] = await this.getMeta(executionId, "status");
373
- if (statusError) return err(statusError.type, statusError.message);
374
- return ok(status === "cancelled");
385
+ return await this.getMeta(executionId, "status") === "cancelled";
375
386
  }
376
387
  async setMeta(executionId, field, value) {
377
388
  const [writeError] = await mightThrow(this.options.redis.hset(this.metaKey(executionId), field, value));
378
- if (writeError) return err("WorkflowError", `Unable to persist ${field} for execution ${executionId}`);
379
- return ok(null);
389
+ if (writeError) throw new WorkflowError(`Unable to persist ${field} for execution ${executionId}`);
380
390
  }
381
391
  async getMeta(executionId, field) {
382
392
  const [readError, value] = await mightThrow(this.options.redis.hget(this.metaKey(executionId), field));
383
- if (readError) return err("WorkflowError", `Unable to read ${field} for execution ${executionId}`);
384
- if (value === null || value === void 0) return ok(null);
385
- if (typeof value !== "string") return err("WorkflowStateError", `Invalid ${field} value for execution ${executionId}`);
386
- if (value.length === 0) return ok(null);
387
- return ok(value);
393
+ if (readError) throw new WorkflowError(`Unable to read ${field} for execution ${executionId}`);
394
+ if (value === null || value === void 0) return null;
395
+ if (typeof value !== "string") throw new StateError(`Invalid ${field} value for execution ${executionId}`);
396
+ if (value.length === 0) return null;
397
+ return value;
388
398
  }
389
399
  async readNumberMeta(executionId, field) {
390
- const [readError, value] = await this.getMeta(executionId, field);
391
- if (readError) return err(readError.type, readError.message);
392
- if (!value) return ok(null);
400
+ const value = await this.getMeta(executionId, field);
401
+ if (!value) return null;
393
402
  const parsed = Number(value);
394
- if (!Number.isFinite(parsed)) return err("WorkflowStateError", `Invalid ${field} value for execution ${executionId}`);
395
- return ok(parsed);
403
+ if (!Number.isFinite(parsed)) throw new StateError(`Invalid ${field} value for execution ${executionId}`);
404
+ return parsed;
396
405
  }
397
406
  runSerializer(value, serializer, label) {
398
407
  const [serializeError, serialized] = mightThrowSync(() => serializer(value));
399
- if (serializeError) return err("WorkflowSerializationError", `Unable to serialize ${label}: ${toErrorMessage(serializeError)}`);
400
- if (typeof serialized !== "string") return err("WorkflowSerializationError", `${label} serializer must return a string`);
401
- return ok(serialized);
408
+ if (serializeError) throw new SerializationError(`Unable to serialize ${label}: ${serializeError.message}`);
409
+ if (typeof serialized !== "string") throw new SerializationError(`${label} serializer must return a string`);
410
+ return serialized;
402
411
  }
403
412
  runDeserializer(raw, deserializer, label) {
404
- const [deserializeError, value] = mightThrowSync(() => deserializer(raw));
405
- if (deserializeError) return err("WorkflowSerializationError", `Unable to deserialize ${label}: ${toErrorMessage(deserializeError)}`);
406
- return ok(value);
413
+ const result = mightThrowSync(() => deserializer(raw));
414
+ if (result[0]) throw new SerializationError(`Unable to deserialize ${label}: ${result[0].message}`);
415
+ return result[1];
407
416
  }
408
417
  serializeInput(input) {
409
418
  return this.runSerializer(input, this.options.serializeInput ?? envelopeSerialize, "workflow input");
@@ -413,7 +422,7 @@ var WorkflowDefinition = class {
413
422
  return this.runDeserializer(raw, deserializer, "workflow input");
414
423
  }
415
424
  serializeResult(result) {
416
- if (result === null) return ok(envelopeSerialize(null));
425
+ if (result === null) return envelopeSerialize(null);
417
426
  return this.runSerializer(result, this.options.serializeResult ?? envelopeSerialize, "workflow result");
418
427
  }
419
428
  deserializeResult(raw) {
@@ -428,91 +437,76 @@ var WorkflowDefinition = class {
428
437
  return this.runDeserializer(raw, deserializer, "step output");
429
438
  }
430
439
  async readInput(executionId) {
431
- const [readError, raw] = await this.getMeta(executionId, "input");
432
- if (readError) return err(readError.type, readError.message);
433
- if (!raw) return err("WorkflowStateError", `Workflow execution ${executionId} input not found`);
440
+ const raw = await this.getMeta(executionId, "input");
441
+ if (!raw) throw new StateError(`Workflow execution ${executionId} input not found`);
434
442
  return this.deserializeInput(raw);
435
443
  }
436
444
  async readResult(executionId) {
437
- const [readError, raw] = await this.getMeta(executionId, "result");
438
- if (readError) return err(readError.type, readError.message);
439
- if (!raw) return ok(null);
440
- const [deserializeError, result] = this.deserializeResult(raw);
441
- if (deserializeError) return err(deserializeError.type, deserializeError.message);
442
- return ok(result);
445
+ const raw = await this.getMeta(executionId, "result");
446
+ if (!raw) return null;
447
+ return this.deserializeResult(raw);
443
448
  }
444
449
  async writeStepOutput(executionId, stepName, output) {
445
- const [serializeError, serializedOutput] = this.serializeStepOutput(output);
446
- if (serializeError) return err("WorkflowSerializationError", `Unable to serialize step ${stepName} output`);
447
450
  const payload = {
448
- output: serializedOutput,
451
+ output: this.serializeStepOutput(output),
449
452
  completedAt: now()
450
453
  };
451
454
  const [payloadError, payloadRaw] = mightThrowSync(() => JSON.stringify(payload));
452
- if (payloadError || typeof payloadRaw !== "string") return err("WorkflowSerializationError", `Unable to persist step ${stepName} output`);
455
+ if (payloadError || typeof payloadRaw !== "string") throw new SerializationError(`Unable to persist step ${stepName} output`);
453
456
  const [writeError] = await mightThrow(this.options.redis.hset(this.stepsKey(executionId), stepName, payloadRaw));
454
- if (writeError) return err("WorkflowError", `Unable to persist step ${stepName} for execution ${executionId}`);
455
- const [stepNamesError, stepNames] = await this.readStepNames(executionId);
456
- if (stepNamesError) return err(stepNamesError.type, stepNamesError.message);
457
+ if (writeError) throw new WorkflowError(`Unable to persist step ${stepName} for execution ${executionId}`);
458
+ const stepNames = await this.readStepNames(executionId);
457
459
  if (!stepNames.includes(stepName)) {
458
460
  const nextStepNames = [...stepNames, stepName];
459
461
  const [serializeStepsError, serializedSteps] = mightThrowSync(() => JSON.stringify(nextStepNames));
460
- if (serializeStepsError || typeof serializedSteps !== "string") return err("WorkflowSerializationError", `Unable to persist step history for execution ${executionId}`);
461
- const [updateStepsError] = await this.setMeta(executionId, "steps", serializedSteps);
462
- if (updateStepsError) return err(updateStepsError.type, updateStepsError.message);
462
+ if (serializeStepsError || typeof serializedSteps !== "string") throw new SerializationError(`Unable to persist step history for execution ${executionId}`);
463
+ await this.setMeta(executionId, "steps", serializedSteps);
463
464
  }
464
- const [updatedError] = await this.setMeta(executionId, "updatedAt", String(now()));
465
- if (updatedError) return err(updatedError.type, updatedError.message);
466
- return ok(null);
465
+ await this.setMeta(executionId, "updatedAt", String(now()));
467
466
  }
468
467
  async readStepOutput(executionId, stepName) {
469
468
  const [readError, payloadRaw] = await mightThrow(this.options.redis.hget(this.stepsKey(executionId), stepName));
470
- if (readError) return err("WorkflowError", `Unable to read step ${stepName} for execution ${executionId}`);
471
- if (!payloadRaw) return ok({
469
+ if (readError) throw new WorkflowError(`Unable to read step ${stepName} for execution ${executionId}`);
470
+ if (!payloadRaw) return {
472
471
  found: false,
473
472
  value: null
474
- });
475
- if (typeof payloadRaw !== "string") return err("WorkflowStateError", `Invalid step payload for ${stepName} in execution ${executionId}`);
473
+ };
474
+ if (typeof payloadRaw !== "string") throw new StateError(`Invalid step payload for ${stepName} in execution ${executionId}`);
476
475
  const [parseError, parsed] = mightThrowSync(() => JSON.parse(payloadRaw));
477
- if (parseError || parsed === null || typeof parsed !== "object") return err("WorkflowStateError", `Invalid step payload for ${stepName} in execution ${executionId}`);
478
- if (typeof parsed.output !== "string") return err("WorkflowStateError", `Invalid step output for ${stepName} in execution ${executionId}`);
476
+ if (parseError || parsed === null || typeof parsed !== "object") throw new StateError(`Invalid step payload for ${stepName} in execution ${executionId}`);
477
+ if (typeof parsed.output !== "string") throw new StateError(`Invalid step output for ${stepName} in execution ${executionId}`);
479
478
  const outputRaw = parsed.output;
480
- const [deserializeError, value] = this.deserializeStepOutput(outputRaw);
481
- if (deserializeError) return err(deserializeError.type, deserializeError.message);
482
- return ok({
479
+ return {
483
480
  found: true,
484
- value
485
- });
481
+ value: this.deserializeStepOutput(outputRaw)
482
+ };
486
483
  }
487
484
  async readStepNames(executionId) {
488
- const [readError, stepsRaw] = await this.getMeta(executionId, "steps");
489
- if (readError) return [readError, []];
490
- if (!stepsRaw) return ok([]);
485
+ const stepsRaw = await this.getMeta(executionId, "steps");
486
+ if (!stepsRaw) return [];
491
487
  const [parseError, values] = mightThrowSync(() => JSON.parse(stepsRaw));
492
- if (parseError || !Array.isArray(values)) return err("WorkflowStateError", `Invalid step index for execution ${executionId}`);
488
+ if (parseError || !Array.isArray(values)) throw new StateError(`Invalid step index for execution ${executionId}`);
493
489
  const stepNames = [];
494
490
  for (const value of values) if (typeof value === "string") stepNames.push(value);
495
- return ok(stepNames);
491
+ return stepNames;
496
492
  }
497
493
  async readStepSnapshots(executionId) {
498
- const [stepNamesError, stepNames] = await this.readStepNames(executionId);
499
- if (stepNamesError) return [stepNamesError, []];
494
+ const stepNames = await this.readStepNames(executionId);
500
495
  const steps = [];
501
496
  for (const stepName of stepNames) {
502
497
  const [readError, payloadRaw] = await mightThrow(this.options.redis.hget(this.stepsKey(executionId), stepName));
503
- if (readError) return err("WorkflowError", `Unable to read step ${stepName} for execution ${executionId}`);
498
+ if (readError) throw new WorkflowError(`Unable to read step ${stepName} for execution ${executionId}`);
504
499
  if (!payloadRaw) continue;
505
- if (typeof payloadRaw !== "string") return err("WorkflowStateError", `Invalid step payload for ${stepName} in execution ${executionId}`);
500
+ if (typeof payloadRaw !== "string") throw new StateError(`Invalid step payload for ${stepName} in execution ${executionId}`);
506
501
  const [parseError, parsed] = mightThrowSync(() => JSON.parse(payloadRaw));
507
- if (parseError || parsed === null || typeof parsed !== "object") return err("WorkflowStateError", `Invalid step payload for ${stepName} in execution ${executionId}`);
508
- if (typeof parsed.completedAt !== "number") return err("WorkflowStateError", `Invalid step payload for ${stepName} in execution ${executionId}`);
509
- const completedAt = parsed.completedAt;
502
+ if (parseError || parsed === null || typeof parsed !== "object") throw new StateError(`Invalid step payload for ${stepName} in execution ${executionId}`);
503
+ if (typeof parsed.completedAt !== "number") throw new StateError(`Invalid step payload for ${stepName} in execution ${executionId}`);
510
504
  steps.push({
511
505
  name: stepName,
512
- completedAt
506
+ completedAt: parsed.completedAt
513
507
  });
514
508
  }
515
- return ok(steps);
509
+ return steps;
516
510
  }
517
511
  normalizeStatus(value) {
518
512
  for (const status of knownStatuses) if (status === value) return status;
@@ -530,4 +524,4 @@ const defineWorkflow = (options) => {
530
524
  };
531
525
  };
532
526
  //#endregion
533
- export { defineWorkflow };
527
+ export { CancelledError, ExecutionError, LockError, NotFoundError, SerializationError, StateError, WorkflowError, defineWorkflow };