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.
- package/README.md +18 -45
- package/dist/chunk-CKQMccvm.cjs +28 -0
- package/dist/lib/api/index.cjs +29 -15
- package/dist/lib/api/index.mjs +30 -16
- package/dist/lib/cache/index.cjs +47 -22
- package/dist/lib/cache/index.d.cts +3 -24
- package/dist/lib/cache/index.d.mts +3 -24
- package/dist/lib/cache/index.mjs +48 -23
- package/dist/lib/cron/index.cjs +117 -117
- package/dist/lib/cron/index.mjs +118 -118
- package/dist/lib/errors/index.d.cts +12 -1
- package/dist/lib/errors/index.d.mts +12 -1
- package/dist/lib/logging/index.cjs +1 -0
- package/dist/lib/orm/index.cjs +1642 -0
- package/dist/lib/orm/index.d.cts +402 -0
- package/dist/lib/orm/index.d.mts +402 -0
- package/dist/lib/orm/index.mjs +1630 -0
- package/dist/lib/prompts/index.cjs +89 -89
- package/dist/lib/prompts/index.d.cts +12 -33
- package/dist/lib/prompts/index.d.mts +12 -33
- package/dist/lib/prompts/index.mjs +89 -90
- package/dist/lib/pubsub/index.cjs +43 -19
- package/dist/lib/pubsub/index.d.cts +3 -18
- package/dist/lib/pubsub/index.d.mts +3 -18
- package/dist/lib/pubsub/index.mjs +44 -20
- package/dist/lib/queue/index.cjs +40 -10
- package/dist/lib/queue/index.d.cts +11 -4
- package/dist/lib/queue/index.d.mts +11 -4
- package/dist/lib/queue/index.mjs +39 -11
- package/dist/lib/workflow/index.cjs +285 -282
- package/dist/lib/workflow/index.d.cts +76 -11
- package/dist/lib/workflow/index.d.mts +76 -11
- package/dist/lib/workflow/index.mjs +278 -284
- package/package.json +11 -1
- package/dist/index-BhGNDjPq.d.mts +0 -13
- package/dist/index-DxSbeGP-.d.cts +0 -13
|
@@ -1,10 +1,70 @@
|
|
|
1
|
-
import {
|
|
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
|
|
6
|
-
if (
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
41
|
-
if (
|
|
42
|
-
|
|
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
|
|
52
|
-
if (
|
|
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
|
|
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
|
|
66
|
-
if (
|
|
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)
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
if (createdAt === null)
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
108
|
-
if (
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
168
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
202
|
-
if (cancelledError) return Promise.reject(new Error(cancelledError.message));
|
|
203
|
-
if (cancelled) {
|
|
238
|
+
await this.throwIfCancelled(executionId, () => {
|
|
204
239
|
controller.abort();
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
258
|
+
if (await this.isCancelled(executionId)) {
|
|
233
259
|
const cancelledAt = now();
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
291
|
+
throw new SerializationError(`Unable to serialize workflow result for ${executionId}`);
|
|
305
292
|
}
|
|
306
293
|
const completedAt = now();
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
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)
|
|
346
|
-
if (lockResult !== "OK")
|
|
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
|
-
|
|
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,
|
|
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)
|
|
368
|
-
if (
|
|
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
|
-
|
|
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)
|
|
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)
|
|
384
|
-
if (value === null || value === void 0) return
|
|
385
|
-
if (typeof value !== "string")
|
|
386
|
-
if (value.length === 0) return
|
|
387
|
-
return
|
|
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
|
|
391
|
-
if (
|
|
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))
|
|
395
|
-
return
|
|
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)
|
|
400
|
-
if (typeof serialized !== "string")
|
|
401
|
-
return
|
|
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
|
|
405
|
-
if (
|
|
406
|
-
return
|
|
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
|
|
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
|
|
432
|
-
if (
|
|
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
|
|
438
|
-
if (
|
|
439
|
-
|
|
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:
|
|
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")
|
|
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)
|
|
455
|
-
const
|
|
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")
|
|
461
|
-
|
|
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
|
-
|
|
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)
|
|
471
|
-
if (!payloadRaw) return
|
|
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")
|
|
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")
|
|
478
|
-
if (typeof parsed.output !== "string")
|
|
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
|
-
|
|
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
|
|
489
|
-
if (
|
|
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))
|
|
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
|
|
491
|
+
return stepNames;
|
|
496
492
|
}
|
|
497
493
|
async readStepSnapshots(executionId) {
|
|
498
|
-
const
|
|
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)
|
|
498
|
+
if (readError) throw new WorkflowError(`Unable to read step ${stepName} for execution ${executionId}`);
|
|
504
499
|
if (!payloadRaw) continue;
|
|
505
|
-
if (typeof payloadRaw !== "string")
|
|
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")
|
|
508
|
-
if (typeof parsed.completedAt !== "number")
|
|
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
|
|
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 };
|