awaitly 2.0.0 → 3.0.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/dist/di-Biw_plBn.d.cts +15 -0
- package/dist/di-DxyH2N3i.d.ts +15 -0
- package/dist/durable.cjs +4670 -0
- package/dist/durable.cjs.map +1 -0
- package/dist/durable.d.cts +9 -0
- package/dist/durable.d.ts +9 -0
- package/dist/durable.js +4610 -0
- package/dist/durable.js.map +1 -0
- package/dist/{di-BbFFfO8y.d.ts → duration-BjVn3QpB.d.cts} +1 -15
- package/dist/{di-BDlT7InM.d.cts → duration-BjVn3QpB.d.ts} +1 -15
- package/dist/engine.cjs +4501 -0
- package/dist/engine.cjs.map +1 -0
- package/dist/engine.d.cts +113 -0
- package/dist/engine.d.ts +113 -0
- package/dist/engine.js +4474 -0
- package/dist/engine.js.map +1 -0
- package/dist/{errors-DtXvrCiO.d.cts → errors-sDSeaZBO.d.cts} +1 -1
- package/dist/{errors-DtXvrCiO.d.ts → errors-sDSeaZBO.d.ts} +1 -1
- package/dist/guards-CwQZro1F.d.ts +72 -0
- package/dist/guards-DhhOJZda.d.cts +72 -0
- package/dist/hitl-BnnnLzh2.d.cts +468 -0
- package/dist/hitl-pYfkQMv_.d.ts +468 -0
- package/dist/hitl.cjs +646 -0
- package/dist/hitl.cjs.map +1 -0
- package/dist/hitl.d.cts +440 -0
- package/dist/hitl.d.ts +440 -0
- package/dist/hitl.js +605 -0
- package/dist/hitl.js.map +1 -0
- package/dist/index-Bew12SZ8.d.ts +417 -0
- package/dist/index-Bwt-iz50.d.cts +417 -0
- package/dist/{types-B8NfNRGX.d.ts → index-Cv4K_3sZ.d.ts} +2 -1162
- package/dist/{types-BZ2f4MRR.d.cts → index-ICmfQi08.d.cts} +2 -1162
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1527
- package/dist/index.d.ts +11 -1527
- package/dist/persistence.cjs +552 -0
- package/dist/persistence.cjs.map +1 -0
- package/dist/persistence.d.cts +254 -0
- package/dist/persistence.d.ts +254 -0
- package/dist/persistence.js +499 -0
- package/dist/persistence.js.map +1 -0
- package/dist/reliability.cjs +1651 -0
- package/dist/reliability.cjs.map +1 -0
- package/dist/reliability.d.cts +1442 -0
- package/dist/reliability.d.ts +1442 -0
- package/dist/reliability.js +1589 -0
- package/dist/reliability.js.map +1 -0
- package/dist/result.d.cts +1 -1
- package/dist/result.d.ts +1 -1
- package/dist/run.cjs +2283 -0
- package/dist/run.cjs.map +1 -0
- package/dist/run.d.cts +90 -0
- package/dist/run.d.ts +90 -0
- package/dist/run.js +2247 -0
- package/dist/run.js.map +1 -0
- package/dist/saga.cjs +3699 -0
- package/dist/saga.cjs.map +1 -0
- package/dist/saga.d.cts +162 -0
- package/dist/saga.d.ts +162 -0
- package/dist/saga.js +3670 -0
- package/dist/saga.js.map +1 -0
- package/dist/store-contract-BI98VYmX.d.ts +66 -0
- package/dist/store-contract-C2HyR_o6.d.cts +66 -0
- package/dist/streaming.cjs +895 -0
- package/dist/streaming.cjs.map +1 -0
- package/dist/streaming.d.cts +594 -0
- package/dist/streaming.d.ts +594 -0
- package/dist/streaming.js +832 -0
- package/dist/streaming.js.map +1 -0
- package/dist/testing.d.cts +4 -2
- package/dist/testing.d.ts +4 -2
- package/dist/types-BDTxgKKc.d.ts +845 -0
- package/dist/types-C9Y71dua.d.cts +845 -0
- package/dist/types-d8q8iQlk.d.cts +323 -0
- package/dist/types-sObbY4mX.d.ts +323 -0
- package/dist/webhook.cjs +461 -0
- package/dist/webhook.cjs.map +1 -0
- package/dist/webhook.d.cts +497 -0
- package/dist/webhook.d.ts +497 -0
- package/dist/webhook.js +422 -0
- package/dist/webhook.js.map +1 -0
- package/dist/workflow.cjs +2 -2614
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +12 -3023
- package/dist/workflow.d.ts +12 -3023
- package/dist/workflow.js +2 -2535
- package/dist/workflow.js.map +1 -1
- package/package.json +46 -1
package/dist/workflow.js
CHANGED
|
@@ -385,7 +385,6 @@ function err(error, options) {
|
|
|
385
385
|
const cause = options?.cause;
|
|
386
386
|
return { ok: false, error, ...cause !== void 0 ? { cause } : {} };
|
|
387
387
|
}
|
|
388
|
-
var isOk = (r) => r.ok;
|
|
389
388
|
var isUnexpectedError = (e) => e instanceof UnexpectedError || typeof e === "object" && e !== null && "_tag" in e && e._tag === "UnexpectedError";
|
|
390
389
|
function extractErrorTag(error) {
|
|
391
390
|
if (error == null) return "unknown";
|
|
@@ -2504,69 +2503,6 @@ function deserializeCauseNew(serialized) {
|
|
|
2504
2503
|
}
|
|
2505
2504
|
return serialized.value !== void 0 ? serialized.value : serialized.stringRepresentation;
|
|
2506
2505
|
}
|
|
2507
|
-
function createMemoryCache(options = {}) {
|
|
2508
|
-
const { maxSize, ttl } = options;
|
|
2509
|
-
const cache = /* @__PURE__ */ new Map();
|
|
2510
|
-
const isExpired = (entry) => {
|
|
2511
|
-
const effectiveTtl = entry.entryTtl ?? ttl;
|
|
2512
|
-
if (!effectiveTtl) return false;
|
|
2513
|
-
return Date.now() - entry.timestamp > effectiveTtl;
|
|
2514
|
-
};
|
|
2515
|
-
const evictExpired = () => {
|
|
2516
|
-
for (const [key, entry] of cache) {
|
|
2517
|
-
if (isExpired(entry)) {
|
|
2518
|
-
cache.delete(key);
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
};
|
|
2522
|
-
const evictOldest = () => {
|
|
2523
|
-
if (!maxSize || cache.size < maxSize) return;
|
|
2524
|
-
let oldestKey;
|
|
2525
|
-
let oldestTime = Infinity;
|
|
2526
|
-
for (const [key, entry] of cache) {
|
|
2527
|
-
if (entry.timestamp < oldestTime) {
|
|
2528
|
-
oldestTime = entry.timestamp;
|
|
2529
|
-
oldestKey = key;
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
if (oldestKey) {
|
|
2533
|
-
cache.delete(oldestKey);
|
|
2534
|
-
}
|
|
2535
|
-
};
|
|
2536
|
-
return {
|
|
2537
|
-
get(key) {
|
|
2538
|
-
evictExpired();
|
|
2539
|
-
const entry = cache.get(key);
|
|
2540
|
-
if (!entry) return void 0;
|
|
2541
|
-
if (isExpired(entry)) {
|
|
2542
|
-
cache.delete(key);
|
|
2543
|
-
return void 0;
|
|
2544
|
-
}
|
|
2545
|
-
return entry.result;
|
|
2546
|
-
},
|
|
2547
|
-
set(key, result, options2) {
|
|
2548
|
-
evictExpired();
|
|
2549
|
-
evictOldest();
|
|
2550
|
-
cache.set(key, { result, timestamp: Date.now(), entryTtl: options2?.ttl });
|
|
2551
|
-
},
|
|
2552
|
-
has(key) {
|
|
2553
|
-
evictExpired();
|
|
2554
|
-
const entry = cache.get(key);
|
|
2555
|
-
if (!entry) return false;
|
|
2556
|
-
if (isExpired(entry)) {
|
|
2557
|
-
cache.delete(key);
|
|
2558
|
-
return false;
|
|
2559
|
-
}
|
|
2560
|
-
return true;
|
|
2561
|
-
},
|
|
2562
|
-
delete(key) {
|
|
2563
|
-
return cache.delete(key);
|
|
2564
|
-
},
|
|
2565
|
-
clear() {
|
|
2566
|
-
cache.clear();
|
|
2567
|
-
}
|
|
2568
|
-
};
|
|
2569
|
-
}
|
|
2570
2506
|
|
|
2571
2507
|
// src/workflow/cache-encoding.ts
|
|
2572
2508
|
function isCachedErrorCause(cause) {
|
|
@@ -2815,24 +2751,7 @@ function createHook() {
|
|
|
2815
2751
|
var STREAM_WRITE_ERROR = "STREAM_WRITE_ERROR";
|
|
2816
2752
|
var STREAM_READ_ERROR = "STREAM_READ_ERROR";
|
|
2817
2753
|
var STREAM_CLOSE_ERROR = "STREAM_CLOSE_ERROR";
|
|
2818
|
-
var STREAM_STORE_ERROR = "STREAM_STORE_ERROR";
|
|
2819
2754
|
var STREAM_ENDED = "STREAM_ENDED";
|
|
2820
|
-
var STREAM_BACKPRESSURE_ERROR = "STREAM_BACKPRESSURE_ERROR";
|
|
2821
|
-
function isStreamEnded(error) {
|
|
2822
|
-
return typeof error === "object" && error !== null && error.type === STREAM_ENDED;
|
|
2823
|
-
}
|
|
2824
|
-
function isStreamWriteError(error) {
|
|
2825
|
-
return typeof error === "object" && error !== null && error.type === STREAM_WRITE_ERROR;
|
|
2826
|
-
}
|
|
2827
|
-
function isStreamReadError(error) {
|
|
2828
|
-
return typeof error === "object" && error !== null && error.type === STREAM_READ_ERROR;
|
|
2829
|
-
}
|
|
2830
|
-
function isStreamStoreError(error) {
|
|
2831
|
-
return typeof error === "object" && error !== null && error.type === STREAM_STORE_ERROR;
|
|
2832
|
-
}
|
|
2833
|
-
function isStreamBackpressureError(error) {
|
|
2834
|
-
return typeof error === "object" && error !== null && error.type === STREAM_BACKPRESSURE_ERROR;
|
|
2835
|
-
}
|
|
2836
2755
|
function streamWriteError(reason, message, cause) {
|
|
2837
2756
|
return {
|
|
2838
2757
|
type: STREAM_WRITE_ERROR,
|
|
@@ -2857,27 +2776,12 @@ function streamCloseError(reason, message, cause) {
|
|
|
2857
2776
|
...cause !== void 0 ? { cause } : {}
|
|
2858
2777
|
};
|
|
2859
2778
|
}
|
|
2860
|
-
function streamStoreError(reason, message, cause) {
|
|
2861
|
-
return {
|
|
2862
|
-
type: STREAM_STORE_ERROR,
|
|
2863
|
-
reason,
|
|
2864
|
-
message,
|
|
2865
|
-
...cause !== void 0 ? { cause } : {}
|
|
2866
|
-
};
|
|
2867
|
-
}
|
|
2868
2779
|
function streamEnded(finalPosition) {
|
|
2869
2780
|
return {
|
|
2870
2781
|
type: STREAM_ENDED,
|
|
2871
2782
|
finalPosition
|
|
2872
2783
|
};
|
|
2873
2784
|
}
|
|
2874
|
-
function streamBackpressureError(bufferedCount, highWaterMark) {
|
|
2875
|
-
return {
|
|
2876
|
-
type: STREAM_BACKPRESSURE_ERROR,
|
|
2877
|
-
bufferedCount,
|
|
2878
|
-
highWaterMark
|
|
2879
|
-
};
|
|
2880
|
-
}
|
|
2881
2785
|
|
|
2882
2786
|
// src/streaming/backpressure.ts
|
|
2883
2787
|
function createBackpressureController(options = {}) {
|
|
@@ -2949,9 +2853,6 @@ function createBackpressureController(options = {}) {
|
|
|
2949
2853
|
}
|
|
2950
2854
|
};
|
|
2951
2855
|
}
|
|
2952
|
-
function shouldApplyBackpressure(controller) {
|
|
2953
|
-
return controller.state === "paused";
|
|
2954
|
-
}
|
|
2955
2856
|
|
|
2956
2857
|
// src/workflow/validation.ts
|
|
2957
2858
|
function isInputValidationError(e) {
|
|
@@ -3831,7 +3732,7 @@ Use serializeResumeState() and deserializeResumeState() from 'awaitly/workflow'
|
|
|
3831
3732
|
const results = [];
|
|
3832
3733
|
let processedCount = 0;
|
|
3833
3734
|
let lastPosition = -1;
|
|
3834
|
-
const
|
|
3735
|
+
const isStreamReader = (s) => {
|
|
3835
3736
|
return typeof s === "object" && s !== null && "read" in s && typeof s.read === "function";
|
|
3836
3737
|
};
|
|
3837
3738
|
const processItem = async (item, itemIndex, itemPosition, namespace) => {
|
|
@@ -3845,7 +3746,7 @@ Use serializeResumeState() and deserializeResumeState() from 'awaitly/workflow'
|
|
|
3845
3746
|
);
|
|
3846
3747
|
return { index: itemIndex, position: itemPosition, result: stepResult };
|
|
3847
3748
|
};
|
|
3848
|
-
if (
|
|
3749
|
+
if (isStreamReader(source)) {
|
|
3849
3750
|
if (concurrency <= 1) {
|
|
3850
3751
|
let itemPosition = source.position;
|
|
3851
3752
|
let readResult = await source.read();
|
|
@@ -4146,2361 +4047,6 @@ Use serializeResumeState() and deserializeResumeState() from 'awaitly/workflow'
|
|
|
4146
4047
|
return workflow;
|
|
4147
4048
|
}
|
|
4148
4049
|
|
|
4149
|
-
// src/durable/index.ts
|
|
4150
|
-
var defaultStore;
|
|
4151
|
-
function createMemorySnapshotStore() {
|
|
4152
|
-
const store = /* @__PURE__ */ new Map();
|
|
4153
|
-
return {
|
|
4154
|
-
async save(id, snapshot) {
|
|
4155
|
-
store.set(id, { snapshot, updatedAt: /* @__PURE__ */ new Date() });
|
|
4156
|
-
},
|
|
4157
|
-
async load(id) {
|
|
4158
|
-
const entry = store.get(id);
|
|
4159
|
-
return entry?.snapshot ?? null;
|
|
4160
|
-
},
|
|
4161
|
-
async delete(id) {
|
|
4162
|
-
store.delete(id);
|
|
4163
|
-
},
|
|
4164
|
-
async list(options) {
|
|
4165
|
-
const prefix = options?.prefix ?? "";
|
|
4166
|
-
const limit = options?.limit ?? 100;
|
|
4167
|
-
const results = [];
|
|
4168
|
-
for (const [id, entry] of store.entries()) {
|
|
4169
|
-
if (prefix && !id.startsWith(prefix)) continue;
|
|
4170
|
-
results.push({ id, updatedAt: entry.updatedAt.toISOString() });
|
|
4171
|
-
if (results.length >= limit) break;
|
|
4172
|
-
}
|
|
4173
|
-
results.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
4174
|
-
return results;
|
|
4175
|
-
},
|
|
4176
|
-
async close() {
|
|
4177
|
-
}
|
|
4178
|
-
};
|
|
4179
|
-
}
|
|
4180
|
-
function getDefaultStore() {
|
|
4181
|
-
if (defaultStore === void 0) {
|
|
4182
|
-
defaultStore = createMemorySnapshotStore();
|
|
4183
|
-
}
|
|
4184
|
-
return defaultStore;
|
|
4185
|
-
}
|
|
4186
|
-
function isVersionMismatch(error) {
|
|
4187
|
-
return typeof error === "object" && error !== null && error.type === "VERSION_MISMATCH";
|
|
4188
|
-
}
|
|
4189
|
-
function isConcurrentExecution(error) {
|
|
4190
|
-
return typeof error === "object" && error !== null && error.type === "CONCURRENT_EXECUTION";
|
|
4191
|
-
}
|
|
4192
|
-
function isPersistenceError(error) {
|
|
4193
|
-
return typeof error === "object" && error !== null && error.type === "PERSISTENCE_ERROR";
|
|
4194
|
-
}
|
|
4195
|
-
function isLeaseExpired(error) {
|
|
4196
|
-
return typeof error === "object" && error !== null && error.type === "LEASE_EXPIRED";
|
|
4197
|
-
}
|
|
4198
|
-
function isIdempotencyConflict(error) {
|
|
4199
|
-
return typeof error === "object" && error !== null && error.type === "IDEMPOTENCY_CONFLICT";
|
|
4200
|
-
}
|
|
4201
|
-
function hasWorkflowLock(store) {
|
|
4202
|
-
return typeof store.tryAcquire === "function" && typeof store.release === "function";
|
|
4203
|
-
}
|
|
4204
|
-
var activeWorkflows = /* @__PURE__ */ new Set();
|
|
4205
|
-
var pendingIdempotencyRuns = /* @__PURE__ */ new Map();
|
|
4206
|
-
var durable = {
|
|
4207
|
-
/**
|
|
4208
|
-
* Execute a workflow with automatic state persistence.
|
|
4209
|
-
*
|
|
4210
|
-
* Features:
|
|
4211
|
-
* - **Automatic checkpointing**: State is saved after each keyed step
|
|
4212
|
-
* - **Crash recovery**: Resume from the last completed step on restart
|
|
4213
|
-
* - **Version checking**: Reject resume if workflow logic version changed
|
|
4214
|
-
* - **Concurrency control**: Prevent duplicate executions of the same workflow ID
|
|
4215
|
-
* - **Cancellation support**: Integrates with AbortSignal for graceful shutdown
|
|
4216
|
-
*
|
|
4217
|
-
* ## How It Works
|
|
4218
|
-
*
|
|
4219
|
-
* 1. On start: Load existing state from store (if any)
|
|
4220
|
-
* 2. Check version compatibility (reject if mismatch)
|
|
4221
|
-
* 3. Pre-populate cache from loaded state (skip completed steps)
|
|
4222
|
-
* 4. Execute workflow, persisting state after each keyed step
|
|
4223
|
-
* 5. On completion: Delete stored state (clean up)
|
|
4224
|
-
* 6. On error/cancellation: State remains for future resume
|
|
4225
|
-
*
|
|
4226
|
-
* ## Important Notes
|
|
4227
|
-
*
|
|
4228
|
-
* - **Only keyed steps are durable**: Use `{ key: 'step-name' }` option
|
|
4229
|
-
* - **Steps should be idempotent**: They may be retried on resume
|
|
4230
|
-
* - **Serialization**: State is JSON-serialized; complex objects may lose fidelity
|
|
4231
|
-
*
|
|
4232
|
-
* @param deps - Workflow dependencies (Result-returning functions)
|
|
4233
|
-
* @param fn - Workflow function receiving ({ step, deps, ctx })
|
|
4234
|
-
* @param options - Durable execution options
|
|
4235
|
-
* @returns AsyncResult with workflow result or error
|
|
4236
|
-
*
|
|
4237
|
-
* @example
|
|
4238
|
-
* ```typescript
|
|
4239
|
-
* import { durable } from 'awaitly/workflow';
|
|
4240
|
-
*
|
|
4241
|
-
* // Zero-config: uses in-memory store (per process)
|
|
4242
|
-
* const result = await durable.run(
|
|
4243
|
-
* { fetchUser, createOrder, sendEmail },
|
|
4244
|
-
* async ({ step, deps: { fetchUser, createOrder, sendEmail } }) => {
|
|
4245
|
-
* const user = await step(() => fetchUser('123'), { key: 'fetch-user' });
|
|
4246
|
-
* const order = await step(() => createOrder(user), { key: 'create-order' });
|
|
4247
|
-
* await step(() => sendEmail(order), { key: 'send-email' });
|
|
4248
|
-
* return order;
|
|
4249
|
-
* },
|
|
4250
|
-
* { id: 'checkout-123' }
|
|
4251
|
-
* );
|
|
4252
|
-
*
|
|
4253
|
-
* // Persistence across restarts: pass a store adapter
|
|
4254
|
-
* import { postgres } from 'awaitly-postgres'; // or awaitly-mongo, awaitly-libsql
|
|
4255
|
-
* const store = postgres(process.env.DATABASE_URL);
|
|
4256
|
-
* await durable.run(deps, fn, { id: 'checkout-123', store });
|
|
4257
|
-
*
|
|
4258
|
-
* if (result.ok) {
|
|
4259
|
-
* console.log('Order completed:', result.value);
|
|
4260
|
-
* } else if (isWorkflowCancelled(result.error)) {
|
|
4261
|
-
* console.log('Workflow cancelled at:', result.error.lastStepKey);
|
|
4262
|
-
* }
|
|
4263
|
-
* ```
|
|
4264
|
-
*/
|
|
4265
|
-
async run(deps, fn, options) {
|
|
4266
|
-
const {
|
|
4267
|
-
id,
|
|
4268
|
-
store: storeOption,
|
|
4269
|
-
version = 1,
|
|
4270
|
-
allowConcurrent = false,
|
|
4271
|
-
lockTtlMs = 6e4,
|
|
4272
|
-
heartbeatIntervalMs,
|
|
4273
|
-
abortOnLeaseLoss,
|
|
4274
|
-
metadata,
|
|
4275
|
-
signal,
|
|
4276
|
-
createContext,
|
|
4277
|
-
onEvent,
|
|
4278
|
-
onError,
|
|
4279
|
-
onVersionMismatch,
|
|
4280
|
-
idempotencyKey,
|
|
4281
|
-
input
|
|
4282
|
-
} = options;
|
|
4283
|
-
const effectiveStore = storeOption ?? getDefaultStore();
|
|
4284
|
-
let resolveIdempotencyRun;
|
|
4285
|
-
if (idempotencyKey) {
|
|
4286
|
-
const idemId = `idem:${idempotencyKey}`;
|
|
4287
|
-
const pending = pendingIdempotencyRuns.get(idemId);
|
|
4288
|
-
if (pending) {
|
|
4289
|
-
return await pending;
|
|
4290
|
-
}
|
|
4291
|
-
pendingIdempotencyRuns.set(idemId, new Promise((r) => {
|
|
4292
|
-
resolveIdempotencyRun = r;
|
|
4293
|
-
}));
|
|
4294
|
-
try {
|
|
4295
|
-
const idemSnapshot = await effectiveStore.load(idemId);
|
|
4296
|
-
if (idemSnapshot) {
|
|
4297
|
-
if (input !== void 0 && idemSnapshot.metadata?.input !== void 0) {
|
|
4298
|
-
const storedInput = JSON.stringify(idemSnapshot.metadata.input);
|
|
4299
|
-
const currentInput = JSON.stringify(input);
|
|
4300
|
-
if (storedInput !== currentInput) {
|
|
4301
|
-
const result = err({
|
|
4302
|
-
type: "IDEMPOTENCY_CONFLICT",
|
|
4303
|
-
idempotencyKey,
|
|
4304
|
-
workflowId: id,
|
|
4305
|
-
message: `Idempotency key '${idempotencyKey}' already used with different input for workflow '${id}'.`
|
|
4306
|
-
});
|
|
4307
|
-
resolveIdempotencyRun(result);
|
|
4308
|
-
pendingIdempotencyRuns.delete(idemId);
|
|
4309
|
-
resolveIdempotencyRun = void 0;
|
|
4310
|
-
return result;
|
|
4311
|
-
}
|
|
4312
|
-
}
|
|
4313
|
-
if (idemSnapshot.execution.status === "completed" && idemSnapshot.metadata?.finalResult !== void 0) {
|
|
4314
|
-
const result = idemSnapshot.metadata.finalResult;
|
|
4315
|
-
resolveIdempotencyRun(result);
|
|
4316
|
-
pendingIdempotencyRuns.delete(idemId);
|
|
4317
|
-
resolveIdempotencyRun = void 0;
|
|
4318
|
-
return result;
|
|
4319
|
-
}
|
|
4320
|
-
if (idemSnapshot.execution.status === "running") {
|
|
4321
|
-
const result = err({
|
|
4322
|
-
type: "CONCURRENT_EXECUTION",
|
|
4323
|
-
workflowId: id,
|
|
4324
|
-
message: `Workflow '${id}' with idempotency key '${idempotencyKey}' is already running.`,
|
|
4325
|
-
reason: "cross-process"
|
|
4326
|
-
});
|
|
4327
|
-
resolveIdempotencyRun(result);
|
|
4328
|
-
pendingIdempotencyRuns.delete(idemId);
|
|
4329
|
-
resolveIdempotencyRun = void 0;
|
|
4330
|
-
return result;
|
|
4331
|
-
}
|
|
4332
|
-
}
|
|
4333
|
-
} catch {
|
|
4334
|
-
}
|
|
4335
|
-
try {
|
|
4336
|
-
await effectiveStore.save(idemId, {
|
|
4337
|
-
formatVersion: 1,
|
|
4338
|
-
steps: {},
|
|
4339
|
-
execution: {
|
|
4340
|
-
status: "running",
|
|
4341
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
4342
|
-
},
|
|
4343
|
-
metadata: {
|
|
4344
|
-
workflowId: id,
|
|
4345
|
-
idempotencyKey,
|
|
4346
|
-
input
|
|
4347
|
-
}
|
|
4348
|
-
});
|
|
4349
|
-
} catch {
|
|
4350
|
-
}
|
|
4351
|
-
}
|
|
4352
|
-
if (!allowConcurrent && activeWorkflows.has(id)) {
|
|
4353
|
-
const error = {
|
|
4354
|
-
type: "CONCURRENT_EXECUTION",
|
|
4355
|
-
workflowId: id,
|
|
4356
|
-
message: `Workflow '${id}' is already running. Set allowConcurrent: true to allow parallel executions.`,
|
|
4357
|
-
reason: "in-process"
|
|
4358
|
-
};
|
|
4359
|
-
return err(error);
|
|
4360
|
-
}
|
|
4361
|
-
let leaseOwnerToken = null;
|
|
4362
|
-
if (!allowConcurrent && hasWorkflowLock(effectiveStore)) {
|
|
4363
|
-
let lease;
|
|
4364
|
-
try {
|
|
4365
|
-
lease = await effectiveStore.tryAcquire(id, { ttlMs: lockTtlMs });
|
|
4366
|
-
} catch (lockError) {
|
|
4367
|
-
const error = {
|
|
4368
|
-
type: "PERSISTENCE_ERROR",
|
|
4369
|
-
operation: "load",
|
|
4370
|
-
workflowId: id,
|
|
4371
|
-
cause: lockError,
|
|
4372
|
-
message: `Failed to acquire lock for workflow '${id}': ${lockError instanceof Error ? lockError.message : String(lockError)}`
|
|
4373
|
-
};
|
|
4374
|
-
return err(error);
|
|
4375
|
-
}
|
|
4376
|
-
if (lease === null) {
|
|
4377
|
-
const error = {
|
|
4378
|
-
type: "CONCURRENT_EXECUTION",
|
|
4379
|
-
workflowId: id,
|
|
4380
|
-
message: `Workflow '${id}' is already running (lease held by another process). Set allowConcurrent: true to allow parallel executions.`,
|
|
4381
|
-
reason: "cross-process"
|
|
4382
|
-
};
|
|
4383
|
-
return err(error);
|
|
4384
|
-
}
|
|
4385
|
-
leaseOwnerToken = lease.ownerToken;
|
|
4386
|
-
}
|
|
4387
|
-
let heartbeatTimer;
|
|
4388
|
-
let leaseAbortController;
|
|
4389
|
-
const lockStore = effectiveStore;
|
|
4390
|
-
if (leaseOwnerToken && typeof lockStore.renew === "function") {
|
|
4391
|
-
const heartbeatMs = heartbeatIntervalMs ?? Math.floor(lockTtlMs / 3);
|
|
4392
|
-
leaseAbortController = new AbortController();
|
|
4393
|
-
heartbeatTimer = setInterval(async () => {
|
|
4394
|
-
try {
|
|
4395
|
-
const renewed = await lockStore.renew(id, leaseOwnerToken, { ttlMs: lockTtlMs });
|
|
4396
|
-
if (!renewed) {
|
|
4397
|
-
leaseAbortController.abort(new Error("Lease expired"));
|
|
4398
|
-
}
|
|
4399
|
-
} catch {
|
|
4400
|
-
leaseAbortController.abort(new Error("Lease renewal failed"));
|
|
4401
|
-
}
|
|
4402
|
-
}, heartbeatMs);
|
|
4403
|
-
}
|
|
4404
|
-
activeWorkflows.add(id);
|
|
4405
|
-
let durableResult;
|
|
4406
|
-
try {
|
|
4407
|
-
let existingSnapshot = null;
|
|
4408
|
-
try {
|
|
4409
|
-
existingSnapshot = await effectiveStore.load(id);
|
|
4410
|
-
} catch (loadError) {
|
|
4411
|
-
const error = {
|
|
4412
|
-
type: "PERSISTENCE_ERROR",
|
|
4413
|
-
operation: "load",
|
|
4414
|
-
workflowId: id,
|
|
4415
|
-
cause: loadError,
|
|
4416
|
-
message: `Failed to load state for workflow '${id}': ${loadError instanceof Error ? loadError.message : String(loadError)}`
|
|
4417
|
-
};
|
|
4418
|
-
durableResult = err(error);
|
|
4419
|
-
return err(error);
|
|
4420
|
-
}
|
|
4421
|
-
if (existingSnapshot) {
|
|
4422
|
-
try {
|
|
4423
|
-
assertValidSnapshot(existingSnapshot);
|
|
4424
|
-
} catch (validationError2) {
|
|
4425
|
-
if (validationError2 instanceof SnapshotFormatError) {
|
|
4426
|
-
const error = {
|
|
4427
|
-
type: "PERSISTENCE_ERROR",
|
|
4428
|
-
operation: "load",
|
|
4429
|
-
workflowId: id,
|
|
4430
|
-
cause: validationError2,
|
|
4431
|
-
message: `Invalid snapshot format for workflow '${id}': ${validationError2.message}`
|
|
4432
|
-
};
|
|
4433
|
-
durableResult = err(error);
|
|
4434
|
-
return err(error);
|
|
4435
|
-
}
|
|
4436
|
-
throw validationError2;
|
|
4437
|
-
}
|
|
4438
|
-
}
|
|
4439
|
-
if (existingSnapshot) {
|
|
4440
|
-
const storedVersion = typeof existingSnapshot.metadata?.version === "number" ? existingSnapshot.metadata.version : 1;
|
|
4441
|
-
if (storedVersion !== version) {
|
|
4442
|
-
const error = {
|
|
4443
|
-
type: "VERSION_MISMATCH",
|
|
4444
|
-
workflowId: id,
|
|
4445
|
-
storedVersion,
|
|
4446
|
-
requestedVersion: version,
|
|
4447
|
-
currentVersion: version,
|
|
4448
|
-
message: `Workflow '${id}' has stored state at version ${storedVersion} but this run requested version ${version}. Migrate the stored state to the new version, or clear state for this id (e.g. durable.deleteState(store, '${id}')) and re-run.`
|
|
4449
|
-
};
|
|
4450
|
-
if (!onVersionMismatch) {
|
|
4451
|
-
durableResult = err(error);
|
|
4452
|
-
return err(error);
|
|
4453
|
-
}
|
|
4454
|
-
const resolution = await Promise.resolve(
|
|
4455
|
-
onVersionMismatch({ id, storedVersion, requestedVersion: version })
|
|
4456
|
-
);
|
|
4457
|
-
if (resolution === "throw") {
|
|
4458
|
-
durableResult = err(error);
|
|
4459
|
-
return err(error);
|
|
4460
|
-
}
|
|
4461
|
-
if (resolution === "clear") {
|
|
4462
|
-
try {
|
|
4463
|
-
await effectiveStore.delete(id);
|
|
4464
|
-
} catch {
|
|
4465
|
-
}
|
|
4466
|
-
existingSnapshot = null;
|
|
4467
|
-
} else {
|
|
4468
|
-
existingSnapshot = resolution.migratedSnapshot;
|
|
4469
|
-
}
|
|
4470
|
-
}
|
|
4471
|
-
}
|
|
4472
|
-
const emitDurableEvent = (event, ctx) => {
|
|
4473
|
-
if (onEvent) {
|
|
4474
|
-
onEvent(event, ctx);
|
|
4475
|
-
}
|
|
4476
|
-
};
|
|
4477
|
-
const resumeCollector = createResumeStateCollector();
|
|
4478
|
-
const workflowOptions = {
|
|
4479
|
-
// Restore from existing snapshot
|
|
4480
|
-
snapshot: existingSnapshot,
|
|
4481
|
-
// Persist after each keyed step
|
|
4482
|
-
onAfterStep: async (stepKey, result2, wfId, ctx) => {
|
|
4483
|
-
try {
|
|
4484
|
-
const collectedState = resumeCollector.getResumeState();
|
|
4485
|
-
const steps = {};
|
|
4486
|
-
for (const [key, entry] of collectedState.steps) {
|
|
4487
|
-
if (entry.result.ok) {
|
|
4488
|
-
steps[key] = { ok: true, value: entry.result.value };
|
|
4489
|
-
} else {
|
|
4490
|
-
const cause = entry.result.cause;
|
|
4491
|
-
const serializedCause = cause instanceof Error ? serializeError(cause) : serializeThrown(cause);
|
|
4492
|
-
const origin = entry.meta?.origin === "throw" ? "throw" : "result";
|
|
4493
|
-
steps[key] = {
|
|
4494
|
-
ok: false,
|
|
4495
|
-
error: entry.result.error,
|
|
4496
|
-
cause: serializedCause,
|
|
4497
|
-
meta: { origin }
|
|
4498
|
-
};
|
|
4499
|
-
}
|
|
4500
|
-
}
|
|
4501
|
-
const currentSnapshot = {
|
|
4502
|
-
formatVersion: 1,
|
|
4503
|
-
workflowName: id,
|
|
4504
|
-
steps,
|
|
4505
|
-
execution: {
|
|
4506
|
-
status: "running",
|
|
4507
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4508
|
-
currentStepId: stepKey
|
|
4509
|
-
},
|
|
4510
|
-
metadata: {
|
|
4511
|
-
...existingSnapshot?.metadata ?? {},
|
|
4512
|
-
...metadata,
|
|
4513
|
-
version,
|
|
4514
|
-
lastStepKey: stepKey
|
|
4515
|
-
}
|
|
4516
|
-
};
|
|
4517
|
-
let snapshotToSave = existingSnapshot ? mergeSnapshots(existingSnapshot, currentSnapshot) : currentSnapshot;
|
|
4518
|
-
if (snapshotToSave.warnings && snapshotToSave.warnings.length > 0) {
|
|
4519
|
-
const currentStepKeys = new Set(Object.keys(currentSnapshot.steps));
|
|
4520
|
-
const filtered = snapshotToSave.warnings.filter(
|
|
4521
|
-
(w) => !currentStepKeys.has(w.stepId)
|
|
4522
|
-
);
|
|
4523
|
-
snapshotToSave = {
|
|
4524
|
-
...snapshotToSave,
|
|
4525
|
-
warnings: filtered.length > 0 ? filtered : void 0
|
|
4526
|
-
};
|
|
4527
|
-
}
|
|
4528
|
-
await effectiveStore.save(id, snapshotToSave);
|
|
4529
|
-
emitDurableEvent(
|
|
4530
|
-
{
|
|
4531
|
-
type: "persist_success",
|
|
4532
|
-
workflowId: wfId,
|
|
4533
|
-
stepKey,
|
|
4534
|
-
ts: Date.now(),
|
|
4535
|
-
context: ctx
|
|
4536
|
-
},
|
|
4537
|
-
ctx
|
|
4538
|
-
);
|
|
4539
|
-
} catch (persistError) {
|
|
4540
|
-
emitDurableEvent(
|
|
4541
|
-
{
|
|
4542
|
-
type: "persist_error",
|
|
4543
|
-
workflowId: wfId,
|
|
4544
|
-
stepKey,
|
|
4545
|
-
error: persistError,
|
|
4546
|
-
ts: Date.now(),
|
|
4547
|
-
context: ctx
|
|
4548
|
-
},
|
|
4549
|
-
ctx
|
|
4550
|
-
);
|
|
4551
|
-
}
|
|
4552
|
-
},
|
|
4553
|
-
// Forward events and collect step results for snapshot building
|
|
4554
|
-
onEvent: (event, ctx) => {
|
|
4555
|
-
resumeCollector.handleEvent(event);
|
|
4556
|
-
emitDurableEvent(event, ctx);
|
|
4557
|
-
},
|
|
4558
|
-
onError,
|
|
4559
|
-
signal: leaseAbortController && signal ? AbortSignal.any([signal, leaseAbortController.signal]) : leaseAbortController?.signal ?? signal,
|
|
4560
|
-
createContext
|
|
4561
|
-
};
|
|
4562
|
-
let workflowInstance;
|
|
4563
|
-
try {
|
|
4564
|
-
workflowInstance = createWorkflow(id, deps, workflowOptions);
|
|
4565
|
-
} catch (createError) {
|
|
4566
|
-
if (createError instanceof SnapshotFormatError) {
|
|
4567
|
-
const error = {
|
|
4568
|
-
type: "PERSISTENCE_ERROR",
|
|
4569
|
-
operation: "load",
|
|
4570
|
-
workflowId: id,
|
|
4571
|
-
cause: createError,
|
|
4572
|
-
message: `Invalid snapshot format for workflow '${id}': ${createError.message}`
|
|
4573
|
-
};
|
|
4574
|
-
durableResult = err(error);
|
|
4575
|
-
return err(error);
|
|
4576
|
-
}
|
|
4577
|
-
throw createError;
|
|
4578
|
-
}
|
|
4579
|
-
let result;
|
|
4580
|
-
try {
|
|
4581
|
-
result = await workflowInstance.run(fn);
|
|
4582
|
-
} catch (runError) {
|
|
4583
|
-
if (runError instanceof SnapshotFormatError || runError instanceof SnapshotDecodeError) {
|
|
4584
|
-
const error = {
|
|
4585
|
-
type: "PERSISTENCE_ERROR",
|
|
4586
|
-
operation: "load",
|
|
4587
|
-
workflowId: id,
|
|
4588
|
-
cause: runError,
|
|
4589
|
-
message: `Invalid snapshot format for workflow '${id}': ${runError.message}`
|
|
4590
|
-
};
|
|
4591
|
-
durableResult = err(error);
|
|
4592
|
-
return err(error);
|
|
4593
|
-
}
|
|
4594
|
-
throw runError;
|
|
4595
|
-
}
|
|
4596
|
-
if (abortOnLeaseLoss !== false && leaseAbortController?.signal.aborted) {
|
|
4597
|
-
const leaseErr = err({
|
|
4598
|
-
type: "LEASE_EXPIRED",
|
|
4599
|
-
workflowId: id,
|
|
4600
|
-
message: `Lease expired for workflow '${id}' during execution. The workflow may have been reclaimed by another process.`
|
|
4601
|
-
});
|
|
4602
|
-
durableResult = leaseErr;
|
|
4603
|
-
return leaseErr;
|
|
4604
|
-
}
|
|
4605
|
-
if (result.ok) {
|
|
4606
|
-
try {
|
|
4607
|
-
await effectiveStore.delete(id);
|
|
4608
|
-
} catch (deleteError) {
|
|
4609
|
-
const error = {
|
|
4610
|
-
type: "PERSISTENCE_ERROR",
|
|
4611
|
-
operation: "delete",
|
|
4612
|
-
workflowId: id,
|
|
4613
|
-
cause: deleteError,
|
|
4614
|
-
message: `Failed to delete state for workflow '${id}': ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`
|
|
4615
|
-
};
|
|
4616
|
-
durableResult = err(error);
|
|
4617
|
-
return err(error);
|
|
4618
|
-
}
|
|
4619
|
-
if (idempotencyKey) {
|
|
4620
|
-
const idemId = `idem:${idempotencyKey}`;
|
|
4621
|
-
try {
|
|
4622
|
-
await effectiveStore.save(idemId, {
|
|
4623
|
-
formatVersion: 1,
|
|
4624
|
-
steps: {},
|
|
4625
|
-
execution: {
|
|
4626
|
-
status: "completed",
|
|
4627
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4628
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4629
|
-
},
|
|
4630
|
-
metadata: {
|
|
4631
|
-
workflowId: id,
|
|
4632
|
-
idempotencyKey,
|
|
4633
|
-
input,
|
|
4634
|
-
finalResult: result
|
|
4635
|
-
}
|
|
4636
|
-
});
|
|
4637
|
-
} catch {
|
|
4638
|
-
}
|
|
4639
|
-
}
|
|
4640
|
-
}
|
|
4641
|
-
durableResult = result;
|
|
4642
|
-
return result;
|
|
4643
|
-
} finally {
|
|
4644
|
-
activeWorkflows.delete(id);
|
|
4645
|
-
if (resolveIdempotencyRun) {
|
|
4646
|
-
resolveIdempotencyRun(durableResult);
|
|
4647
|
-
pendingIdempotencyRuns.delete(`idem:${idempotencyKey}`);
|
|
4648
|
-
}
|
|
4649
|
-
if (heartbeatTimer) {
|
|
4650
|
-
clearInterval(heartbeatTimer);
|
|
4651
|
-
}
|
|
4652
|
-
if (leaseOwnerToken !== null && hasWorkflowLock(effectiveStore)) {
|
|
4653
|
-
try {
|
|
4654
|
-
await effectiveStore.release(id, leaseOwnerToken);
|
|
4655
|
-
} catch {
|
|
4656
|
-
}
|
|
4657
|
-
}
|
|
4658
|
-
}
|
|
4659
|
-
},
|
|
4660
|
-
/**
|
|
4661
|
-
* Clear all persisted workflow state from the store.
|
|
4662
|
-
* Use for admin/testing. If the store implements `clear()`, that is used;
|
|
4663
|
-
* otherwise clears by listing and deleting in pages.
|
|
4664
|
-
*
|
|
4665
|
-
* @param store - Snapshot store
|
|
4666
|
-
*/
|
|
4667
|
-
async clearState(store) {
|
|
4668
|
-
const storeWithClear = store;
|
|
4669
|
-
if (typeof storeWithClear.clear === "function") {
|
|
4670
|
-
await storeWithClear.clear();
|
|
4671
|
-
return;
|
|
4672
|
-
}
|
|
4673
|
-
const limit = 100;
|
|
4674
|
-
for (; ; ) {
|
|
4675
|
-
const entries = await store.list({ limit });
|
|
4676
|
-
if (entries.length === 0) break;
|
|
4677
|
-
const ids = entries.map((e) => e.id);
|
|
4678
|
-
await this.deleteStates(store, ids, { continueOnError: true });
|
|
4679
|
-
if (entries.length < limit) break;
|
|
4680
|
-
}
|
|
4681
|
-
},
|
|
4682
|
-
/**
|
|
4683
|
-
* Check if a workflow ID has persisted state (can be resumed).
|
|
4684
|
-
*
|
|
4685
|
-
* @param store - Snapshot store
|
|
4686
|
-
* @param id - Workflow execution ID
|
|
4687
|
-
* @returns `true` if state exists, `false` otherwise (including on store errors)
|
|
4688
|
-
*/
|
|
4689
|
-
async hasState(store, id) {
|
|
4690
|
-
try {
|
|
4691
|
-
const snapshot = await store.load(id);
|
|
4692
|
-
return snapshot !== null;
|
|
4693
|
-
} catch {
|
|
4694
|
-
return false;
|
|
4695
|
-
}
|
|
4696
|
-
},
|
|
4697
|
-
/**
|
|
4698
|
-
* Delete persisted state for a workflow (cancel resume capability).
|
|
4699
|
-
* Deleting is effectively an ack/reset: the workflow can no longer resume from that state.
|
|
4700
|
-
* If you delete while a run is in flight, the run continues; on success it may delete again (no-op) or save (recreating state).
|
|
4701
|
-
* For multi-worker safety, prefer deleting only when the workflow is not running or when you hold the lock.
|
|
4702
|
-
*
|
|
4703
|
-
* @param store - Snapshot store
|
|
4704
|
-
* @param id - Workflow execution ID
|
|
4705
|
-
* @returns `true` on success, `false` on store errors
|
|
4706
|
-
*/
|
|
4707
|
-
async deleteState(store, id) {
|
|
4708
|
-
try {
|
|
4709
|
-
await store.delete(id);
|
|
4710
|
-
return true;
|
|
4711
|
-
} catch {
|
|
4712
|
-
return false;
|
|
4713
|
-
}
|
|
4714
|
-
},
|
|
4715
|
-
/**
|
|
4716
|
-
* Bulk delete persisted state for multiple workflow IDs (best-effort).
|
|
4717
|
-
* Use for admin/cleanup. Deletes in a loop with optional concurrency.
|
|
4718
|
-
*
|
|
4719
|
-
* @param store - Snapshot store
|
|
4720
|
-
* @param ids - Workflow execution IDs to delete
|
|
4721
|
-
* @param options - Optional concurrency and error handling
|
|
4722
|
-
* @returns Count of deleted entries and any errors when continueOnError is true
|
|
4723
|
-
*/
|
|
4724
|
-
async deleteStates(store, ids, options = {}) {
|
|
4725
|
-
const { concurrency = 10, continueOnError = true } = options;
|
|
4726
|
-
if (ids.length === 0) {
|
|
4727
|
-
return { deleted: 0 };
|
|
4728
|
-
}
|
|
4729
|
-
const errors = [];
|
|
4730
|
-
let deleted = 0;
|
|
4731
|
-
const run2 = async (id) => {
|
|
4732
|
-
try {
|
|
4733
|
-
await store.delete(id);
|
|
4734
|
-
deleted++;
|
|
4735
|
-
} catch (error) {
|
|
4736
|
-
if (continueOnError) errors.push({ id, error });
|
|
4737
|
-
else throw error;
|
|
4738
|
-
}
|
|
4739
|
-
};
|
|
4740
|
-
const limit = Math.max(1, concurrency);
|
|
4741
|
-
for (let i = 0; i < ids.length; i += limit) {
|
|
4742
|
-
const batch = ids.slice(i, i + limit);
|
|
4743
|
-
await Promise.all(batch.map((id) => run2(id)));
|
|
4744
|
-
}
|
|
4745
|
-
return errors.length > 0 ? { deleted, errors } : { deleted };
|
|
4746
|
-
},
|
|
4747
|
-
/**
|
|
4748
|
-
* List workflow IDs with persisted state.
|
|
4749
|
-
*
|
|
4750
|
-
* @param store - Snapshot store
|
|
4751
|
-
* @param options - Optional prefix and limit
|
|
4752
|
-
* @returns Array of { id, updatedAt } entries
|
|
4753
|
-
*/
|
|
4754
|
-
async listPending(store, options) {
|
|
4755
|
-
try {
|
|
4756
|
-
return await store.list(options);
|
|
4757
|
-
} catch {
|
|
4758
|
-
return [];
|
|
4759
|
-
}
|
|
4760
|
-
}
|
|
4761
|
-
};
|
|
4762
|
-
|
|
4763
|
-
// src/versioning.ts
|
|
4764
|
-
function isMigrationError(error) {
|
|
4765
|
-
return typeof error === "object" && error !== null && error.type === "MIGRATION_ERROR";
|
|
4766
|
-
}
|
|
4767
|
-
function isVersionIncompatibleError(error) {
|
|
4768
|
-
return typeof error === "object" && error !== null && error.type === "VERSION_INCOMPATIBLE";
|
|
4769
|
-
}
|
|
4770
|
-
async function migrateState(state, targetVersion, migrations) {
|
|
4771
|
-
let currentState = state.state;
|
|
4772
|
-
let currentVersion = state.version;
|
|
4773
|
-
if (currentVersion > targetVersion) {
|
|
4774
|
-
return err({
|
|
4775
|
-
type: "VERSION_INCOMPATIBLE",
|
|
4776
|
-
stateVersion: currentVersion,
|
|
4777
|
-
currentVersion: targetVersion,
|
|
4778
|
-
reason: "State version is higher than current workflow version. Cannot downgrade."
|
|
4779
|
-
});
|
|
4780
|
-
}
|
|
4781
|
-
if (currentVersion === targetVersion) {
|
|
4782
|
-
return ok2({ version: currentVersion, state: currentState });
|
|
4783
|
-
}
|
|
4784
|
-
while (currentVersion < targetVersion) {
|
|
4785
|
-
const migration = migrations[currentVersion];
|
|
4786
|
-
if (!migration) {
|
|
4787
|
-
return err({
|
|
4788
|
-
type: "VERSION_INCOMPATIBLE",
|
|
4789
|
-
stateVersion: state.version,
|
|
4790
|
-
currentVersion: targetVersion,
|
|
4791
|
-
reason: `No migration found for version ${currentVersion} to ${currentVersion + 1}`
|
|
4792
|
-
});
|
|
4793
|
-
}
|
|
4794
|
-
try {
|
|
4795
|
-
currentState = await migration(currentState);
|
|
4796
|
-
currentVersion++;
|
|
4797
|
-
} catch (cause) {
|
|
4798
|
-
return err({
|
|
4799
|
-
type: "MIGRATION_ERROR",
|
|
4800
|
-
fromVersion: currentVersion,
|
|
4801
|
-
toVersion: currentVersion + 1,
|
|
4802
|
-
cause
|
|
4803
|
-
});
|
|
4804
|
-
}
|
|
4805
|
-
}
|
|
4806
|
-
return ok2({ version: currentVersion, state: currentState });
|
|
4807
|
-
}
|
|
4808
|
-
function createVersionedStateLoader(config) {
|
|
4809
|
-
const { version, migrations = {}, strictVersioning = true } = config;
|
|
4810
|
-
return async (versionedState) => {
|
|
4811
|
-
if (!versionedState) {
|
|
4812
|
-
return ok2(void 0);
|
|
4813
|
-
}
|
|
4814
|
-
if (strictVersioning && versionedState.version > version) {
|
|
4815
|
-
return err({
|
|
4816
|
-
type: "VERSION_INCOMPATIBLE",
|
|
4817
|
-
stateVersion: versionedState.version,
|
|
4818
|
-
currentVersion: version,
|
|
4819
|
-
reason: "Saved state is from a newer workflow version"
|
|
4820
|
-
});
|
|
4821
|
-
}
|
|
4822
|
-
if (versionedState.version === version) {
|
|
4823
|
-
return ok2(versionedState.state);
|
|
4824
|
-
}
|
|
4825
|
-
const result = await migrateState(versionedState, version, migrations);
|
|
4826
|
-
if (!result.ok) {
|
|
4827
|
-
return result;
|
|
4828
|
-
}
|
|
4829
|
-
return ok2(result.value.state);
|
|
4830
|
-
};
|
|
4831
|
-
}
|
|
4832
|
-
function createVersionedState(state, version) {
|
|
4833
|
-
return { version, state };
|
|
4834
|
-
}
|
|
4835
|
-
function parseVersionedState(json) {
|
|
4836
|
-
if (!json) return null;
|
|
4837
|
-
try {
|
|
4838
|
-
const parsed = typeof json === "string" ? JSON.parse(json) : json;
|
|
4839
|
-
if (typeof parsed !== "object" || parsed === null || !("version" in parsed) || typeof parsed.version !== "number" || !("state" in parsed) || !parsed.state || !Array.isArray(parsed.state.steps)) {
|
|
4840
|
-
return null;
|
|
4841
|
-
}
|
|
4842
|
-
const typedParsed = parsed;
|
|
4843
|
-
const steps = new Map(typedParsed.state.steps);
|
|
4844
|
-
return {
|
|
4845
|
-
version: typedParsed.version,
|
|
4846
|
-
state: { steps }
|
|
4847
|
-
};
|
|
4848
|
-
} catch {
|
|
4849
|
-
return null;
|
|
4850
|
-
}
|
|
4851
|
-
}
|
|
4852
|
-
function stringifyVersionedState(state) {
|
|
4853
|
-
return JSON.stringify({
|
|
4854
|
-
version: state.version,
|
|
4855
|
-
state: {
|
|
4856
|
-
steps: Array.from(state.state.steps.entries())
|
|
4857
|
-
}
|
|
4858
|
-
});
|
|
4859
|
-
}
|
|
4860
|
-
function createKeyRenameMigration(renames) {
|
|
4861
|
-
return (state) => {
|
|
4862
|
-
const newSteps = /* @__PURE__ */ new Map();
|
|
4863
|
-
for (const [key, entry] of state.steps) {
|
|
4864
|
-
const newKey = renames[key] ?? key;
|
|
4865
|
-
newSteps.set(newKey, entry);
|
|
4866
|
-
}
|
|
4867
|
-
return { steps: newSteps };
|
|
4868
|
-
};
|
|
4869
|
-
}
|
|
4870
|
-
function createKeyRemoveMigration(keysToRemove) {
|
|
4871
|
-
const keysSet = new Set(keysToRemove);
|
|
4872
|
-
return (state) => {
|
|
4873
|
-
const newSteps = /* @__PURE__ */ new Map();
|
|
4874
|
-
for (const [key, entry] of state.steps) {
|
|
4875
|
-
if (!keysSet.has(key)) {
|
|
4876
|
-
newSteps.set(key, entry);
|
|
4877
|
-
}
|
|
4878
|
-
}
|
|
4879
|
-
return { steps: newSteps };
|
|
4880
|
-
};
|
|
4881
|
-
}
|
|
4882
|
-
function createValueTransformMigration(transforms) {
|
|
4883
|
-
return (state) => {
|
|
4884
|
-
const newSteps = /* @__PURE__ */ new Map();
|
|
4885
|
-
for (const [key, entry] of state.steps) {
|
|
4886
|
-
const transform = transforms[key];
|
|
4887
|
-
newSteps.set(key, transform ? transform(entry) : entry);
|
|
4888
|
-
}
|
|
4889
|
-
return { steps: newSteps };
|
|
4890
|
-
};
|
|
4891
|
-
}
|
|
4892
|
-
function composeMigrations(migrations) {
|
|
4893
|
-
return async (state) => {
|
|
4894
|
-
let currentState = state;
|
|
4895
|
-
for (const migration of migrations) {
|
|
4896
|
-
currentState = await migration(currentState);
|
|
4897
|
-
}
|
|
4898
|
-
return currentState;
|
|
4899
|
-
};
|
|
4900
|
-
}
|
|
4901
|
-
|
|
4902
|
-
// src/hitl/index.ts
|
|
4903
|
-
function createMemoryApprovalStore() {
|
|
4904
|
-
const approvals = /* @__PURE__ */ new Map();
|
|
4905
|
-
return {
|
|
4906
|
-
async getApproval(key) {
|
|
4907
|
-
const approval = approvals.get(key);
|
|
4908
|
-
if (!approval) {
|
|
4909
|
-
return { status: "pending" };
|
|
4910
|
-
}
|
|
4911
|
-
if (approval.expiresAt && Date.now() > approval.expiresAt) {
|
|
4912
|
-
return { status: "expired", expiredAt: approval.expiresAt };
|
|
4913
|
-
}
|
|
4914
|
-
return approval;
|
|
4915
|
-
},
|
|
4916
|
-
async createApproval(key, options) {
|
|
4917
|
-
approvals.set(key, {
|
|
4918
|
-
status: "pending",
|
|
4919
|
-
metadata: options?.metadata,
|
|
4920
|
-
expiresAt: options?.expiresAt
|
|
4921
|
-
});
|
|
4922
|
-
},
|
|
4923
|
-
async grantApproval(key, value, options) {
|
|
4924
|
-
approvals.set(key, {
|
|
4925
|
-
status: "approved",
|
|
4926
|
-
value,
|
|
4927
|
-
approvedBy: options?.approvedBy,
|
|
4928
|
-
approvedAt: Date.now()
|
|
4929
|
-
});
|
|
4930
|
-
},
|
|
4931
|
-
async rejectApproval(key, reason, options) {
|
|
4932
|
-
approvals.set(key, {
|
|
4933
|
-
status: "rejected",
|
|
4934
|
-
reason,
|
|
4935
|
-
rejectedBy: options?.rejectedBy,
|
|
4936
|
-
rejectedAt: Date.now()
|
|
4937
|
-
});
|
|
4938
|
-
},
|
|
4939
|
-
async editApproval(key, originalValue, editedValue, options) {
|
|
4940
|
-
approvals.set(key, {
|
|
4941
|
-
status: "edited",
|
|
4942
|
-
originalValue,
|
|
4943
|
-
editedValue,
|
|
4944
|
-
editedBy: options?.editedBy,
|
|
4945
|
-
editedAt: Date.now()
|
|
4946
|
-
});
|
|
4947
|
-
},
|
|
4948
|
-
async cancelApproval(key) {
|
|
4949
|
-
approvals.delete(key);
|
|
4950
|
-
},
|
|
4951
|
-
async listPending(options) {
|
|
4952
|
-
const pending = [];
|
|
4953
|
-
for (const [key, approval] of approvals) {
|
|
4954
|
-
if (approval.status === "pending") {
|
|
4955
|
-
if (!options?.prefix || key.startsWith(options.prefix)) {
|
|
4956
|
-
pending.push(key);
|
|
4957
|
-
}
|
|
4958
|
-
}
|
|
4959
|
-
}
|
|
4960
|
-
return pending;
|
|
4961
|
-
}
|
|
4962
|
-
};
|
|
4963
|
-
}
|
|
4964
|
-
function createMemoryWorkflowStateStore() {
|
|
4965
|
-
const states = /* @__PURE__ */ new Map();
|
|
4966
|
-
return {
|
|
4967
|
-
async save(state) {
|
|
4968
|
-
states.set(state.runId, { ...state, updatedAt: Date.now() });
|
|
4969
|
-
},
|
|
4970
|
-
async load(runId) {
|
|
4971
|
-
return states.get(runId);
|
|
4972
|
-
},
|
|
4973
|
-
async delete(runId) {
|
|
4974
|
-
states.delete(runId);
|
|
4975
|
-
},
|
|
4976
|
-
async list(options) {
|
|
4977
|
-
const results = [];
|
|
4978
|
-
for (const [runId, state] of states) {
|
|
4979
|
-
if (options?.workflowName && state.workflowName !== options.workflowName) {
|
|
4980
|
-
continue;
|
|
4981
|
-
}
|
|
4982
|
-
if (options?.hasPendingApprovals !== void 0) {
|
|
4983
|
-
const hasPending = state.pendingApprovals.length > 0;
|
|
4984
|
-
if (options.hasPendingApprovals !== hasPending) {
|
|
4985
|
-
continue;
|
|
4986
|
-
}
|
|
4987
|
-
}
|
|
4988
|
-
results.push(runId);
|
|
4989
|
-
}
|
|
4990
|
-
return results;
|
|
4991
|
-
},
|
|
4992
|
-
async findByPendingApproval(approvalKey) {
|
|
4993
|
-
const results = [];
|
|
4994
|
-
for (const [runId, state] of states) {
|
|
4995
|
-
if (state.pendingApprovals.includes(approvalKey)) {
|
|
4996
|
-
results.push(runId);
|
|
4997
|
-
}
|
|
4998
|
-
}
|
|
4999
|
-
return results;
|
|
5000
|
-
}
|
|
5001
|
-
};
|
|
5002
|
-
}
|
|
5003
|
-
function createHITLOrchestrator(options) {
|
|
5004
|
-
const {
|
|
5005
|
-
approvalStore,
|
|
5006
|
-
workflowStateStore,
|
|
5007
|
-
defaultExpirationMs = 7 * 24 * 60 * 60 * 1e3,
|
|
5008
|
-
// 7 days
|
|
5009
|
-
logger = () => {
|
|
5010
|
-
},
|
|
5011
|
-
notificationChannel
|
|
5012
|
-
} = options;
|
|
5013
|
-
async function execute(workflowName, workflowFactory, workflowFn, input, opts) {
|
|
5014
|
-
const runId = opts?.runId ?? crypto.randomUUID();
|
|
5015
|
-
const collector = createApprovalStateCollector();
|
|
5016
|
-
const workflow = workflowFactory({
|
|
5017
|
-
onEvent: collector.handleEvent
|
|
5018
|
-
});
|
|
5019
|
-
const args = input;
|
|
5020
|
-
const result = await workflow.run(
|
|
5021
|
-
async (context) => workflowFn({ ...context, args })
|
|
5022
|
-
);
|
|
5023
|
-
const pendingApprovals = collector.getPendingApprovals().map((p) => p.stepKey);
|
|
5024
|
-
if (pendingApprovals.length > 0) {
|
|
5025
|
-
const state = {
|
|
5026
|
-
runId,
|
|
5027
|
-
workflowName,
|
|
5028
|
-
resumeState: collector.getResumeState(),
|
|
5029
|
-
pendingApprovals,
|
|
5030
|
-
input,
|
|
5031
|
-
metadata: opts?.metadata,
|
|
5032
|
-
startedAt: Date.now(),
|
|
5033
|
-
updatedAt: Date.now()
|
|
5034
|
-
};
|
|
5035
|
-
await workflowStateStore.save(state);
|
|
5036
|
-
let reason;
|
|
5037
|
-
let pendingMetadata;
|
|
5038
|
-
if (!result.ok && isPendingApproval(result.error)) {
|
|
5039
|
-
reason = result.error.reason;
|
|
5040
|
-
pendingMetadata = result.error.metadata;
|
|
5041
|
-
}
|
|
5042
|
-
const expiresAt = Date.now() + defaultExpirationMs;
|
|
5043
|
-
for (const key of pendingApprovals) {
|
|
5044
|
-
await approvalStore.createApproval(key, {
|
|
5045
|
-
metadata: { ...pendingMetadata, runId, workflowName },
|
|
5046
|
-
expiresAt
|
|
5047
|
-
});
|
|
5048
|
-
}
|
|
5049
|
-
if (notificationChannel) {
|
|
5050
|
-
for (const key of pendingApprovals) {
|
|
5051
|
-
try {
|
|
5052
|
-
const pendingInfo = collector.getPendingApprovals().find((p) => p.stepKey === key);
|
|
5053
|
-
await notificationChannel.onApprovalNeeded({
|
|
5054
|
-
approvalKey: key,
|
|
5055
|
-
runId,
|
|
5056
|
-
workflowName,
|
|
5057
|
-
reason,
|
|
5058
|
-
metadata: { ...opts?.metadata, ...pendingMetadata },
|
|
5059
|
-
expiresAt,
|
|
5060
|
-
summary: reason ?? `Approval needed for ${key}`,
|
|
5061
|
-
pendingArgs: pendingInfo?.error.metadata
|
|
5062
|
-
});
|
|
5063
|
-
} catch (notifyError) {
|
|
5064
|
-
logger(`Failed to notify for approval ${key}: ${notifyError}`);
|
|
5065
|
-
}
|
|
5066
|
-
}
|
|
5067
|
-
}
|
|
5068
|
-
logger(`Workflow ${runId} paused, waiting for: ${pendingApprovals.join(", ")}`);
|
|
5069
|
-
return {
|
|
5070
|
-
status: "paused",
|
|
5071
|
-
runId,
|
|
5072
|
-
pendingApprovals,
|
|
5073
|
-
reason
|
|
5074
|
-
};
|
|
5075
|
-
}
|
|
5076
|
-
logger(`Workflow ${runId} completed`);
|
|
5077
|
-
return { status: "completed", result };
|
|
5078
|
-
}
|
|
5079
|
-
async function resume(runId, workflowFactory, workflowFn) {
|
|
5080
|
-
const savedState = await workflowStateStore.load(runId);
|
|
5081
|
-
if (!savedState) {
|
|
5082
|
-
throw new Error(`Workflow run not found: ${runId}`);
|
|
5083
|
-
}
|
|
5084
|
-
for (const key of savedState.pendingApprovals) {
|
|
5085
|
-
const status = await approvalStore.getApproval(key);
|
|
5086
|
-
if (status.status === "pending") {
|
|
5087
|
-
return {
|
|
5088
|
-
status: "paused",
|
|
5089
|
-
runId,
|
|
5090
|
-
pendingApprovals: savedState.pendingApprovals
|
|
5091
|
-
};
|
|
5092
|
-
}
|
|
5093
|
-
if (status.status === "rejected") {
|
|
5094
|
-
}
|
|
5095
|
-
if (status.status === "expired") {
|
|
5096
|
-
throw new Error(`Approval ${key} has expired`);
|
|
5097
|
-
}
|
|
5098
|
-
}
|
|
5099
|
-
let resumeState = savedState.resumeState;
|
|
5100
|
-
for (const key of savedState.pendingApprovals) {
|
|
5101
|
-
const status = await approvalStore.getApproval(key);
|
|
5102
|
-
if (status.status === "approved") {
|
|
5103
|
-
resumeState = injectApproval(resumeState, {
|
|
5104
|
-
stepKey: key,
|
|
5105
|
-
value: status.value
|
|
5106
|
-
});
|
|
5107
|
-
} else if (status.status === "edited") {
|
|
5108
|
-
resumeState = injectApproval(resumeState, {
|
|
5109
|
-
stepKey: key,
|
|
5110
|
-
value: status.editedValue
|
|
5111
|
-
});
|
|
5112
|
-
}
|
|
5113
|
-
}
|
|
5114
|
-
const collector = createApprovalStateCollector();
|
|
5115
|
-
const workflow = workflowFactory({
|
|
5116
|
-
resumeState,
|
|
5117
|
-
onEvent: collector.handleEvent
|
|
5118
|
-
});
|
|
5119
|
-
const args = savedState.input;
|
|
5120
|
-
const result = await workflow.run(
|
|
5121
|
-
async (context) => workflowFn({ ...context, args })
|
|
5122
|
-
);
|
|
5123
|
-
const newPendingApprovals = collector.getPendingApprovals().map((p) => p.stepKey);
|
|
5124
|
-
if (newPendingApprovals.length > 0) {
|
|
5125
|
-
const updatedState = {
|
|
5126
|
-
...savedState,
|
|
5127
|
-
resumeState: collector.getResumeState(),
|
|
5128
|
-
pendingApprovals: newPendingApprovals,
|
|
5129
|
-
updatedAt: Date.now()
|
|
5130
|
-
};
|
|
5131
|
-
await workflowStateStore.save(updatedState);
|
|
5132
|
-
for (const key of newPendingApprovals) {
|
|
5133
|
-
await approvalStore.createApproval(key, {
|
|
5134
|
-
metadata: { runId, workflowName: savedState.workflowName },
|
|
5135
|
-
expiresAt: Date.now() + defaultExpirationMs
|
|
5136
|
-
});
|
|
5137
|
-
}
|
|
5138
|
-
logger(`Workflow ${runId} paused again, waiting for: ${newPendingApprovals.join(", ")}`);
|
|
5139
|
-
let reason;
|
|
5140
|
-
if (!result.ok && isPendingApproval(result.error)) {
|
|
5141
|
-
reason = result.error.reason;
|
|
5142
|
-
}
|
|
5143
|
-
return {
|
|
5144
|
-
status: "paused",
|
|
5145
|
-
runId,
|
|
5146
|
-
pendingApprovals: newPendingApprovals,
|
|
5147
|
-
reason
|
|
5148
|
-
};
|
|
5149
|
-
}
|
|
5150
|
-
await workflowStateStore.delete(runId);
|
|
5151
|
-
logger(`Workflow ${runId} resumed and completed`);
|
|
5152
|
-
return { status: "resumed", runId, result };
|
|
5153
|
-
}
|
|
5154
|
-
async function grantApprovalFn(approvalKey, value, opts) {
|
|
5155
|
-
await approvalStore.grantApproval(approvalKey, value, {
|
|
5156
|
-
approvedBy: opts?.approvedBy
|
|
5157
|
-
});
|
|
5158
|
-
const grantedAt = Date.now();
|
|
5159
|
-
const resumedWorkflows = [];
|
|
5160
|
-
if (opts?.autoResume !== false) {
|
|
5161
|
-
const waitingWorkflows = await workflowStateStore.findByPendingApproval(approvalKey);
|
|
5162
|
-
for (const runId of waitingWorkflows) {
|
|
5163
|
-
resumedWorkflows.push(runId);
|
|
5164
|
-
}
|
|
5165
|
-
}
|
|
5166
|
-
if (notificationChannel?.onApprovalResolved) {
|
|
5167
|
-
await notificationChannel.onApprovalResolved({
|
|
5168
|
-
approvalKey,
|
|
5169
|
-
action: "approved",
|
|
5170
|
-
actorId: opts?.approvedBy,
|
|
5171
|
-
resolvedAt: grantedAt,
|
|
5172
|
-
value
|
|
5173
|
-
});
|
|
5174
|
-
}
|
|
5175
|
-
logger(`Approval ${approvalKey} granted by ${opts?.approvedBy ?? "unknown"}`);
|
|
5176
|
-
return { grantedAt, resumedWorkflows };
|
|
5177
|
-
}
|
|
5178
|
-
async function rejectApprovalFn(approvalKey, reason, opts) {
|
|
5179
|
-
await approvalStore.rejectApproval(approvalKey, reason, {
|
|
5180
|
-
rejectedBy: opts?.rejectedBy
|
|
5181
|
-
});
|
|
5182
|
-
if (notificationChannel?.onApprovalResolved) {
|
|
5183
|
-
await notificationChannel.onApprovalResolved({
|
|
5184
|
-
approvalKey,
|
|
5185
|
-
action: "rejected",
|
|
5186
|
-
actorId: opts?.rejectedBy,
|
|
5187
|
-
resolvedAt: Date.now(),
|
|
5188
|
-
reason
|
|
5189
|
-
});
|
|
5190
|
-
}
|
|
5191
|
-
logger(`Approval ${approvalKey} rejected: ${reason}`);
|
|
5192
|
-
}
|
|
5193
|
-
async function editApprovalFn(approvalKey, originalValue, editedValue, opts) {
|
|
5194
|
-
await approvalStore.editApproval(approvalKey, originalValue, editedValue, {
|
|
5195
|
-
editedBy: opts?.editedBy
|
|
5196
|
-
});
|
|
5197
|
-
const editedAt = Date.now();
|
|
5198
|
-
if (notificationChannel?.onApprovalResolved) {
|
|
5199
|
-
await notificationChannel.onApprovalResolved({
|
|
5200
|
-
approvalKey,
|
|
5201
|
-
action: "edited",
|
|
5202
|
-
actorId: opts?.editedBy,
|
|
5203
|
-
resolvedAt: editedAt,
|
|
5204
|
-
value: editedValue,
|
|
5205
|
-
originalValue
|
|
5206
|
-
});
|
|
5207
|
-
}
|
|
5208
|
-
logger(`Approval ${approvalKey} edited by ${opts?.editedBy ?? "unknown"}`);
|
|
5209
|
-
return { editedAt };
|
|
5210
|
-
}
|
|
5211
|
-
async function pollApproval(approvalKey, opts) {
|
|
5212
|
-
const {
|
|
5213
|
-
intervalMs = 1e3,
|
|
5214
|
-
maxPolls,
|
|
5215
|
-
timeoutMs,
|
|
5216
|
-
onPollStart,
|
|
5217
|
-
onPollComplete
|
|
5218
|
-
} = opts ?? {};
|
|
5219
|
-
const startTime = Date.now();
|
|
5220
|
-
let pollCount = 0;
|
|
5221
|
-
while (true) {
|
|
5222
|
-
onPollStart?.();
|
|
5223
|
-
const status = await approvalStore.getApproval(approvalKey);
|
|
5224
|
-
onPollComplete?.(status);
|
|
5225
|
-
if (status.status !== "pending") {
|
|
5226
|
-
return status;
|
|
5227
|
-
}
|
|
5228
|
-
pollCount++;
|
|
5229
|
-
if (maxPolls !== void 0 && pollCount >= maxPolls) {
|
|
5230
|
-
return { status: "pending" };
|
|
5231
|
-
}
|
|
5232
|
-
if (timeoutMs !== void 0 && Date.now() - startTime >= timeoutMs) {
|
|
5233
|
-
return { status: "pending" };
|
|
5234
|
-
}
|
|
5235
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
5236
|
-
}
|
|
5237
|
-
}
|
|
5238
|
-
async function getWorkflowStatus(runId) {
|
|
5239
|
-
return workflowStateStore.load(runId);
|
|
5240
|
-
}
|
|
5241
|
-
async function listPendingWorkflows(workflowName) {
|
|
5242
|
-
return workflowStateStore.list({
|
|
5243
|
-
workflowName,
|
|
5244
|
-
hasPendingApprovals: true
|
|
5245
|
-
});
|
|
5246
|
-
}
|
|
5247
|
-
async function cleanup(maxAgeMs) {
|
|
5248
|
-
const allWorkflows = await workflowStateStore.list();
|
|
5249
|
-
let cleaned = 0;
|
|
5250
|
-
const cutoff = Date.now() - maxAgeMs;
|
|
5251
|
-
for (const runId of allWorkflows) {
|
|
5252
|
-
const state = await workflowStateStore.load(runId);
|
|
5253
|
-
if (state && state.updatedAt < cutoff && state.pendingApprovals.length === 0) {
|
|
5254
|
-
await workflowStateStore.delete(runId);
|
|
5255
|
-
cleaned++;
|
|
5256
|
-
}
|
|
5257
|
-
}
|
|
5258
|
-
logger(`Cleaned up ${cleaned} old workflow states`);
|
|
5259
|
-
return cleaned;
|
|
5260
|
-
}
|
|
5261
|
-
return {
|
|
5262
|
-
execute,
|
|
5263
|
-
resume,
|
|
5264
|
-
grantApproval: grantApprovalFn,
|
|
5265
|
-
rejectApproval: rejectApprovalFn,
|
|
5266
|
-
editApproval: editApprovalFn,
|
|
5267
|
-
pollApproval,
|
|
5268
|
-
getWorkflowStatus,
|
|
5269
|
-
listPendingWorkflows,
|
|
5270
|
-
cleanup
|
|
5271
|
-
};
|
|
5272
|
-
}
|
|
5273
|
-
function createApprovalWebhookHandler(store) {
|
|
5274
|
-
return async (request) => {
|
|
5275
|
-
const { key, action, value, originalValue, editedValue, reason, actorId } = request;
|
|
5276
|
-
try {
|
|
5277
|
-
switch (action) {
|
|
5278
|
-
case "approve":
|
|
5279
|
-
await store.grantApproval(key, value, { approvedBy: actorId });
|
|
5280
|
-
return {
|
|
5281
|
-
success: true,
|
|
5282
|
-
message: `Approval ${key} granted`,
|
|
5283
|
-
data: { key, action, timestamp: Date.now() }
|
|
5284
|
-
};
|
|
5285
|
-
case "reject":
|
|
5286
|
-
if (!reason) {
|
|
5287
|
-
return { success: false, message: "Reason is required for rejection" };
|
|
5288
|
-
}
|
|
5289
|
-
await store.rejectApproval(key, reason, { rejectedBy: actorId });
|
|
5290
|
-
return {
|
|
5291
|
-
success: true,
|
|
5292
|
-
message: `Approval ${key} rejected`,
|
|
5293
|
-
data: { key, action, timestamp: Date.now() }
|
|
5294
|
-
};
|
|
5295
|
-
case "edit":
|
|
5296
|
-
if (originalValue === void 0 || editedValue === void 0) {
|
|
5297
|
-
return { success: false, message: "Both originalValue and editedValue are required for edit" };
|
|
5298
|
-
}
|
|
5299
|
-
await store.editApproval(key, originalValue, editedValue, { editedBy: actorId });
|
|
5300
|
-
return {
|
|
5301
|
-
success: true,
|
|
5302
|
-
message: `Approval ${key} edited`,
|
|
5303
|
-
data: { key, action, timestamp: Date.now() }
|
|
5304
|
-
};
|
|
5305
|
-
case "cancel":
|
|
5306
|
-
await store.cancelApproval(key);
|
|
5307
|
-
return {
|
|
5308
|
-
success: true,
|
|
5309
|
-
message: `Approval ${key} cancelled`,
|
|
5310
|
-
data: { key, action, timestamp: Date.now() }
|
|
5311
|
-
};
|
|
5312
|
-
default:
|
|
5313
|
-
return { success: false, message: `Unknown action: ${action}` };
|
|
5314
|
-
}
|
|
5315
|
-
} catch (error) {
|
|
5316
|
-
return {
|
|
5317
|
-
success: false,
|
|
5318
|
-
message: error instanceof Error ? error.message : String(error)
|
|
5319
|
-
};
|
|
5320
|
-
}
|
|
5321
|
-
};
|
|
5322
|
-
}
|
|
5323
|
-
function createApprovalChecker(store) {
|
|
5324
|
-
return (key) => async () => {
|
|
5325
|
-
const status = await store.getApproval(key);
|
|
5326
|
-
switch (status.status) {
|
|
5327
|
-
case "pending":
|
|
5328
|
-
return { status: "pending" };
|
|
5329
|
-
case "approved":
|
|
5330
|
-
return { status: "approved", value: status.value };
|
|
5331
|
-
case "edited":
|
|
5332
|
-
return { status: "approved", value: status.editedValue };
|
|
5333
|
-
case "rejected":
|
|
5334
|
-
return { status: "rejected", reason: status.reason };
|
|
5335
|
-
case "expired":
|
|
5336
|
-
return { status: "rejected", reason: "Approval request expired" };
|
|
5337
|
-
default:
|
|
5338
|
-
return { status: "pending" };
|
|
5339
|
-
}
|
|
5340
|
-
};
|
|
5341
|
-
}
|
|
5342
|
-
|
|
5343
|
-
// src/saga.ts
|
|
5344
|
-
function isSagaCompensationError(error) {
|
|
5345
|
-
return typeof error === "object" && error !== null && error.type === "SAGA_COMPENSATION_ERROR";
|
|
5346
|
-
}
|
|
5347
|
-
function createSagaWorkflow(workflowName, deps, options) {
|
|
5348
|
-
return createWorkflow(workflowName, deps, options);
|
|
5349
|
-
}
|
|
5350
|
-
async function runSaga(fn, options) {
|
|
5351
|
-
const sagaId = crypto.randomUUID();
|
|
5352
|
-
const startTime = performance.now();
|
|
5353
|
-
const compensations = [];
|
|
5354
|
-
const emit = (e) => options?.onEvent?.(e);
|
|
5355
|
-
emit({ type: "saga_start", sagaId, ts: Date.now() });
|
|
5356
|
-
const stepFn = async (name, operation, stepOptions) => {
|
|
5357
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
5358
|
-
throw new TypeError(
|
|
5359
|
-
"step(name, operation, options?): first argument must be a string."
|
|
5360
|
-
);
|
|
5361
|
-
}
|
|
5362
|
-
const result = await operation();
|
|
5363
|
-
if (result.ok) {
|
|
5364
|
-
if (stepOptions?.compensate) {
|
|
5365
|
-
compensations.push({
|
|
5366
|
-
name,
|
|
5367
|
-
value: result.value,
|
|
5368
|
-
compensate: stepOptions.compensate
|
|
5369
|
-
});
|
|
5370
|
-
}
|
|
5371
|
-
return result.value;
|
|
5372
|
-
}
|
|
5373
|
-
throw createEarlyExit(result.error, {
|
|
5374
|
-
origin: "result",
|
|
5375
|
-
resultCause: result.cause
|
|
5376
|
-
});
|
|
5377
|
-
};
|
|
5378
|
-
const stepTry = async (name, operation, opts) => {
|
|
5379
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
5380
|
-
throw new TypeError(
|
|
5381
|
-
"step.try(name, operation, options): first argument must be a string."
|
|
5382
|
-
);
|
|
5383
|
-
}
|
|
5384
|
-
const mapToError = "error" in opts ? () => opts.error : opts.onError;
|
|
5385
|
-
try {
|
|
5386
|
-
const value = await operation();
|
|
5387
|
-
if (opts.compensate) {
|
|
5388
|
-
compensations.push({
|
|
5389
|
-
name,
|
|
5390
|
-
value,
|
|
5391
|
-
compensate: opts.compensate
|
|
5392
|
-
});
|
|
5393
|
-
}
|
|
5394
|
-
return value;
|
|
5395
|
-
} catch (thrown) {
|
|
5396
|
-
const mapped = mapToError(thrown);
|
|
5397
|
-
throw createEarlyExit(mapped, { origin: "throw", thrown });
|
|
5398
|
-
}
|
|
5399
|
-
};
|
|
5400
|
-
const step = Object.assign(stepFn, { try: stepTry });
|
|
5401
|
-
try {
|
|
5402
|
-
const value = await fn({ step });
|
|
5403
|
-
emit({
|
|
5404
|
-
type: "saga_success",
|
|
5405
|
-
sagaId,
|
|
5406
|
-
ts: Date.now(),
|
|
5407
|
-
durationMs: performance.now() - startTime
|
|
5408
|
-
});
|
|
5409
|
-
return ok2(value);
|
|
5410
|
-
} catch (thrown) {
|
|
5411
|
-
const durationMs = performance.now() - startTime;
|
|
5412
|
-
const originalError = isEarlyExit(thrown) ? thrown.error : thrown;
|
|
5413
|
-
emit({ type: "saga_error", sagaId, ts: Date.now(), durationMs, error: originalError });
|
|
5414
|
-
emit({
|
|
5415
|
-
type: "saga_compensation_start",
|
|
5416
|
-
sagaId,
|
|
5417
|
-
ts: Date.now(),
|
|
5418
|
-
stepCount: compensations.length
|
|
5419
|
-
});
|
|
5420
|
-
const compensationStart = performance.now();
|
|
5421
|
-
const compensationErrors = [];
|
|
5422
|
-
for (let i = compensations.length - 1; i >= 0; i--) {
|
|
5423
|
-
const comp = compensations[i];
|
|
5424
|
-
try {
|
|
5425
|
-
await comp.compensate(comp.value);
|
|
5426
|
-
emit({
|
|
5427
|
-
type: "saga_compensation_step",
|
|
5428
|
-
sagaId,
|
|
5429
|
-
stepName: comp.name,
|
|
5430
|
-
ts: Date.now(),
|
|
5431
|
-
success: true
|
|
5432
|
-
});
|
|
5433
|
-
} catch (error) {
|
|
5434
|
-
compensationErrors.push({ stepName: comp.name, error });
|
|
5435
|
-
emit({
|
|
5436
|
-
type: "saga_compensation_step",
|
|
5437
|
-
sagaId,
|
|
5438
|
-
stepName: comp.name,
|
|
5439
|
-
ts: Date.now(),
|
|
5440
|
-
success: false,
|
|
5441
|
-
error
|
|
5442
|
-
});
|
|
5443
|
-
}
|
|
5444
|
-
}
|
|
5445
|
-
emit({
|
|
5446
|
-
type: "saga_compensation_end",
|
|
5447
|
-
sagaId,
|
|
5448
|
-
ts: Date.now(),
|
|
5449
|
-
durationMs: performance.now() - compensationStart,
|
|
5450
|
-
success: compensationErrors.length === 0,
|
|
5451
|
-
failedCount: compensationErrors.length
|
|
5452
|
-
});
|
|
5453
|
-
if (compensationErrors.length > 0) {
|
|
5454
|
-
const sagaError = {
|
|
5455
|
-
type: "SAGA_COMPENSATION_ERROR",
|
|
5456
|
-
originalError,
|
|
5457
|
-
compensationErrors
|
|
5458
|
-
};
|
|
5459
|
-
options?.onError?.(sagaError);
|
|
5460
|
-
if (options?.throwOnCompensationFailure) throw sagaError;
|
|
5461
|
-
return err(sagaError);
|
|
5462
|
-
}
|
|
5463
|
-
options?.onError?.(originalError);
|
|
5464
|
-
if (!isEarlyExit(thrown)) {
|
|
5465
|
-
return err(new UnexpectedError({ cause: thrown }));
|
|
5466
|
-
}
|
|
5467
|
-
return err(originalError);
|
|
5468
|
-
}
|
|
5469
|
-
}
|
|
5470
|
-
|
|
5471
|
-
// src/streaming/stores/memory.ts
|
|
5472
|
-
function createMemoryStreamStore(options = {}) {
|
|
5473
|
-
const maxItemsPerStream = options.maxItemsPerStream ?? Infinity;
|
|
5474
|
-
const streams = /* @__PURE__ */ new Map();
|
|
5475
|
-
function getStreamKey(workflowId, namespace) {
|
|
5476
|
-
return `${workflowId}:${namespace}`;
|
|
5477
|
-
}
|
|
5478
|
-
function getOrCreateStream(workflowId, namespace) {
|
|
5479
|
-
const key = getStreamKey(workflowId, namespace);
|
|
5480
|
-
let data = streams.get(key);
|
|
5481
|
-
if (!data) {
|
|
5482
|
-
data = {
|
|
5483
|
-
metadata: {
|
|
5484
|
-
id: key,
|
|
5485
|
-
namespace,
|
|
5486
|
-
workflowId,
|
|
5487
|
-
length: 0,
|
|
5488
|
-
closed: false,
|
|
5489
|
-
createdAt: Date.now()
|
|
5490
|
-
},
|
|
5491
|
-
items: [],
|
|
5492
|
-
subscribers: /* @__PURE__ */ new Set()
|
|
5493
|
-
};
|
|
5494
|
-
streams.set(key, data);
|
|
5495
|
-
}
|
|
5496
|
-
return data;
|
|
5497
|
-
}
|
|
5498
|
-
return {
|
|
5499
|
-
async append(workflowId, namespace, item) {
|
|
5500
|
-
try {
|
|
5501
|
-
const data = getOrCreateStream(workflowId, namespace);
|
|
5502
|
-
if (data.metadata.closed) {
|
|
5503
|
-
return err(
|
|
5504
|
-
streamStoreError("write_error", "Cannot write to closed stream")
|
|
5505
|
-
);
|
|
5506
|
-
}
|
|
5507
|
-
if (data.items.length >= maxItemsPerStream) {
|
|
5508
|
-
return err(
|
|
5509
|
-
streamStoreError(
|
|
5510
|
-
"write_error",
|
|
5511
|
-
`Stream exceeded max items limit (${maxItemsPerStream})`
|
|
5512
|
-
)
|
|
5513
|
-
);
|
|
5514
|
-
}
|
|
5515
|
-
data.items.push(item);
|
|
5516
|
-
data.metadata.length = data.items.length;
|
|
5517
|
-
data.metadata.lastWriteAt = Date.now();
|
|
5518
|
-
for (const callback of data.subscribers) {
|
|
5519
|
-
try {
|
|
5520
|
-
callback(item);
|
|
5521
|
-
} catch {
|
|
5522
|
-
}
|
|
5523
|
-
}
|
|
5524
|
-
return ok2(void 0);
|
|
5525
|
-
} catch (cause) {
|
|
5526
|
-
return err(
|
|
5527
|
-
streamStoreError("write_error", "Failed to append item", cause)
|
|
5528
|
-
);
|
|
5529
|
-
}
|
|
5530
|
-
},
|
|
5531
|
-
async read(workflowId, namespace, startIndex, limit) {
|
|
5532
|
-
try {
|
|
5533
|
-
const key = getStreamKey(workflowId, namespace);
|
|
5534
|
-
const data = streams.get(key);
|
|
5535
|
-
if (!data) {
|
|
5536
|
-
return ok2([]);
|
|
5537
|
-
}
|
|
5538
|
-
const endIndex = limit !== void 0 ? Math.min(startIndex + limit, data.items.length) : data.items.length;
|
|
5539
|
-
const items = data.items.slice(startIndex, endIndex);
|
|
5540
|
-
return ok2(items);
|
|
5541
|
-
} catch (cause) {
|
|
5542
|
-
return err(streamStoreError("read_error", "Failed to read items", cause));
|
|
5543
|
-
}
|
|
5544
|
-
},
|
|
5545
|
-
async getMetadata(workflowId, namespace) {
|
|
5546
|
-
try {
|
|
5547
|
-
const key = getStreamKey(workflowId, namespace);
|
|
5548
|
-
const data = streams.get(key);
|
|
5549
|
-
return ok2(data?.metadata);
|
|
5550
|
-
} catch (cause) {
|
|
5551
|
-
return err(
|
|
5552
|
-
streamStoreError("metadata_error", "Failed to get metadata", cause)
|
|
5553
|
-
);
|
|
5554
|
-
}
|
|
5555
|
-
},
|
|
5556
|
-
async closeStream(workflowId, namespace) {
|
|
5557
|
-
try {
|
|
5558
|
-
const key = getStreamKey(workflowId, namespace);
|
|
5559
|
-
const data = streams.get(key);
|
|
5560
|
-
if (data) {
|
|
5561
|
-
data.metadata.closed = true;
|
|
5562
|
-
data.metadata.closedAt = Date.now();
|
|
5563
|
-
}
|
|
5564
|
-
return ok2(void 0);
|
|
5565
|
-
} catch (cause) {
|
|
5566
|
-
return err(
|
|
5567
|
-
streamStoreError("close_error", "Failed to close stream", cause)
|
|
5568
|
-
);
|
|
5569
|
-
}
|
|
5570
|
-
},
|
|
5571
|
-
subscribe(workflowId, namespace, callback) {
|
|
5572
|
-
const data = getOrCreateStream(workflowId, namespace);
|
|
5573
|
-
const typedCallback = callback;
|
|
5574
|
-
data.subscribers.add(typedCallback);
|
|
5575
|
-
return () => {
|
|
5576
|
-
data.subscribers.delete(typedCallback);
|
|
5577
|
-
};
|
|
5578
|
-
}
|
|
5579
|
-
};
|
|
5580
|
-
}
|
|
5581
|
-
function createTestableMemoryStreamStore(options = {}) {
|
|
5582
|
-
const streams = /* @__PURE__ */ new Map();
|
|
5583
|
-
const maxItemsPerStream = options.maxItemsPerStream ?? Infinity;
|
|
5584
|
-
function getStreamKey(workflowId, namespace) {
|
|
5585
|
-
return `${workflowId}:${namespace}`;
|
|
5586
|
-
}
|
|
5587
|
-
function getOrCreateStream(workflowId, namespace) {
|
|
5588
|
-
const key = getStreamKey(workflowId, namespace);
|
|
5589
|
-
let data = streams.get(key);
|
|
5590
|
-
if (!data) {
|
|
5591
|
-
data = {
|
|
5592
|
-
metadata: {
|
|
5593
|
-
id: key,
|
|
5594
|
-
namespace,
|
|
5595
|
-
workflowId,
|
|
5596
|
-
length: 0,
|
|
5597
|
-
closed: false,
|
|
5598
|
-
createdAt: Date.now()
|
|
5599
|
-
},
|
|
5600
|
-
items: [],
|
|
5601
|
-
subscribers: /* @__PURE__ */ new Set()
|
|
5602
|
-
};
|
|
5603
|
-
streams.set(key, data);
|
|
5604
|
-
}
|
|
5605
|
-
return data;
|
|
5606
|
-
}
|
|
5607
|
-
const store = createMemoryStreamStore(options);
|
|
5608
|
-
return {
|
|
5609
|
-
...store,
|
|
5610
|
-
// Override to use our local streams map
|
|
5611
|
-
async append(workflowId, namespace, item) {
|
|
5612
|
-
try {
|
|
5613
|
-
const data = getOrCreateStream(workflowId, namespace);
|
|
5614
|
-
if (data.metadata.closed) {
|
|
5615
|
-
return err(
|
|
5616
|
-
streamStoreError("write_error", "Cannot write to closed stream")
|
|
5617
|
-
);
|
|
5618
|
-
}
|
|
5619
|
-
if (data.items.length >= maxItemsPerStream) {
|
|
5620
|
-
return err(
|
|
5621
|
-
streamStoreError(
|
|
5622
|
-
"write_error",
|
|
5623
|
-
`Stream exceeded max items limit (${maxItemsPerStream})`
|
|
5624
|
-
)
|
|
5625
|
-
);
|
|
5626
|
-
}
|
|
5627
|
-
data.items.push(item);
|
|
5628
|
-
data.metadata.length = data.items.length;
|
|
5629
|
-
data.metadata.lastWriteAt = Date.now();
|
|
5630
|
-
for (const callback of data.subscribers) {
|
|
5631
|
-
try {
|
|
5632
|
-
callback(item);
|
|
5633
|
-
} catch {
|
|
5634
|
-
}
|
|
5635
|
-
}
|
|
5636
|
-
return ok2(void 0);
|
|
5637
|
-
} catch (cause) {
|
|
5638
|
-
return err(
|
|
5639
|
-
streamStoreError("write_error", "Failed to append item", cause)
|
|
5640
|
-
);
|
|
5641
|
-
}
|
|
5642
|
-
},
|
|
5643
|
-
async read(workflowId, namespace, startIndex, limit) {
|
|
5644
|
-
try {
|
|
5645
|
-
const key = getStreamKey(workflowId, namespace);
|
|
5646
|
-
const data = streams.get(key);
|
|
5647
|
-
if (!data) {
|
|
5648
|
-
return ok2([]);
|
|
5649
|
-
}
|
|
5650
|
-
const endIndex = limit !== void 0 ? Math.min(startIndex + limit, data.items.length) : data.items.length;
|
|
5651
|
-
const items = data.items.slice(startIndex, endIndex);
|
|
5652
|
-
return ok2(items);
|
|
5653
|
-
} catch (cause) {
|
|
5654
|
-
return err(streamStoreError("read_error", "Failed to read items", cause));
|
|
5655
|
-
}
|
|
5656
|
-
},
|
|
5657
|
-
async getMetadata(workflowId, namespace) {
|
|
5658
|
-
try {
|
|
5659
|
-
const key = getStreamKey(workflowId, namespace);
|
|
5660
|
-
const data = streams.get(key);
|
|
5661
|
-
return ok2(data?.metadata);
|
|
5662
|
-
} catch (cause) {
|
|
5663
|
-
return err(
|
|
5664
|
-
streamStoreError("metadata_error", "Failed to get metadata", cause)
|
|
5665
|
-
);
|
|
5666
|
-
}
|
|
5667
|
-
},
|
|
5668
|
-
async closeStream(workflowId, namespace) {
|
|
5669
|
-
try {
|
|
5670
|
-
const key = getStreamKey(workflowId, namespace);
|
|
5671
|
-
const data = streams.get(key);
|
|
5672
|
-
if (data) {
|
|
5673
|
-
data.metadata.closed = true;
|
|
5674
|
-
data.metadata.closedAt = Date.now();
|
|
5675
|
-
}
|
|
5676
|
-
return ok2(void 0);
|
|
5677
|
-
} catch (cause) {
|
|
5678
|
-
return err(
|
|
5679
|
-
streamStoreError("close_error", "Failed to close stream", cause)
|
|
5680
|
-
);
|
|
5681
|
-
}
|
|
5682
|
-
},
|
|
5683
|
-
subscribe(workflowId, namespace, callback) {
|
|
5684
|
-
const data = getOrCreateStream(workflowId, namespace);
|
|
5685
|
-
const typedCallback = callback;
|
|
5686
|
-
data.subscribers.add(typedCallback);
|
|
5687
|
-
return () => {
|
|
5688
|
-
data.subscribers.delete(typedCallback);
|
|
5689
|
-
};
|
|
5690
|
-
},
|
|
5691
|
-
clear() {
|
|
5692
|
-
streams.clear();
|
|
5693
|
-
},
|
|
5694
|
-
keys() {
|
|
5695
|
-
return Array.from(streams.keys());
|
|
5696
|
-
},
|
|
5697
|
-
has(workflowId, namespace) {
|
|
5698
|
-
return streams.has(getStreamKey(workflowId, namespace));
|
|
5699
|
-
},
|
|
5700
|
-
delete(workflowId, namespace) {
|
|
5701
|
-
return streams.delete(getStreamKey(workflowId, namespace));
|
|
5702
|
-
}
|
|
5703
|
-
};
|
|
5704
|
-
}
|
|
5705
|
-
|
|
5706
|
-
// src/streaming/stores/file.ts
|
|
5707
|
-
function createFileStreamStore(options) {
|
|
5708
|
-
const { directory, fs } = options;
|
|
5709
|
-
const subscribers = /* @__PURE__ */ new Map();
|
|
5710
|
-
function getStreamDir(workflowId, namespace) {
|
|
5711
|
-
return `${directory}/${workflowId}/${namespace}`;
|
|
5712
|
-
}
|
|
5713
|
-
function getMetadataPath(workflowId, namespace) {
|
|
5714
|
-
return `${getStreamDir(workflowId, namespace)}/metadata.json`;
|
|
5715
|
-
}
|
|
5716
|
-
function getItemsPath(workflowId, namespace) {
|
|
5717
|
-
return `${getStreamDir(workflowId, namespace)}/items.jsonl`;
|
|
5718
|
-
}
|
|
5719
|
-
function getSubscriberKey(workflowId, namespace) {
|
|
5720
|
-
return `${workflowId}:${namespace}`;
|
|
5721
|
-
}
|
|
5722
|
-
async function ensureDir(workflowId, namespace) {
|
|
5723
|
-
const dir = getStreamDir(workflowId, namespace);
|
|
5724
|
-
await fs.mkdir(dir, { recursive: true });
|
|
5725
|
-
}
|
|
5726
|
-
async function readMetadata(workflowId, namespace) {
|
|
5727
|
-
const metaPath = getMetadataPath(workflowId, namespace);
|
|
5728
|
-
if (!await fs.exists(metaPath)) {
|
|
5729
|
-
return void 0;
|
|
5730
|
-
}
|
|
5731
|
-
const content = await fs.readFile(metaPath);
|
|
5732
|
-
return JSON.parse(content);
|
|
5733
|
-
}
|
|
5734
|
-
async function writeMetadata(workflowId, namespace, metadata) {
|
|
5735
|
-
await ensureDir(workflowId, namespace);
|
|
5736
|
-
const metaPath = getMetadataPath(workflowId, namespace);
|
|
5737
|
-
await fs.writeFile(metaPath, JSON.stringify(metadata, null, 2));
|
|
5738
|
-
}
|
|
5739
|
-
async function appendItem(workflowId, namespace, item) {
|
|
5740
|
-
await ensureDir(workflowId, namespace);
|
|
5741
|
-
const itemsPath = getItemsPath(workflowId, namespace);
|
|
5742
|
-
const line = JSON.stringify(item) + "\n";
|
|
5743
|
-
let content = "";
|
|
5744
|
-
if (await fs.exists(itemsPath)) {
|
|
5745
|
-
content = await fs.readFile(itemsPath);
|
|
5746
|
-
}
|
|
5747
|
-
await fs.writeFile(itemsPath, content + line);
|
|
5748
|
-
}
|
|
5749
|
-
async function readItems(workflowId, namespace, startIndex, limit) {
|
|
5750
|
-
const itemsPath = getItemsPath(workflowId, namespace);
|
|
5751
|
-
if (!await fs.exists(itemsPath)) {
|
|
5752
|
-
return [];
|
|
5753
|
-
}
|
|
5754
|
-
const content = await fs.readFile(itemsPath);
|
|
5755
|
-
const lines = content.trim().split("\n").filter(Boolean);
|
|
5756
|
-
const items = [];
|
|
5757
|
-
for (const line of lines) {
|
|
5758
|
-
const item = JSON.parse(line);
|
|
5759
|
-
if (item.position >= startIndex) {
|
|
5760
|
-
items.push(item);
|
|
5761
|
-
if (limit !== void 0 && items.length >= limit) {
|
|
5762
|
-
break;
|
|
5763
|
-
}
|
|
5764
|
-
}
|
|
5765
|
-
}
|
|
5766
|
-
return items;
|
|
5767
|
-
}
|
|
5768
|
-
return {
|
|
5769
|
-
async append(workflowId, namespace, item) {
|
|
5770
|
-
try {
|
|
5771
|
-
let metadata = await readMetadata(workflowId, namespace);
|
|
5772
|
-
if (!metadata) {
|
|
5773
|
-
metadata = {
|
|
5774
|
-
id: `${workflowId}:${namespace}`,
|
|
5775
|
-
namespace,
|
|
5776
|
-
workflowId,
|
|
5777
|
-
length: 0,
|
|
5778
|
-
closed: false,
|
|
5779
|
-
createdAt: Date.now()
|
|
5780
|
-
};
|
|
5781
|
-
}
|
|
5782
|
-
if (metadata.closed) {
|
|
5783
|
-
return err(
|
|
5784
|
-
streamStoreError("write_error", "Cannot write to closed stream")
|
|
5785
|
-
);
|
|
5786
|
-
}
|
|
5787
|
-
const serializedItem = {
|
|
5788
|
-
value: item.value,
|
|
5789
|
-
position: item.position,
|
|
5790
|
-
ts: item.ts
|
|
5791
|
-
};
|
|
5792
|
-
await appendItem(workflowId, namespace, serializedItem);
|
|
5793
|
-
metadata.length++;
|
|
5794
|
-
metadata.lastWriteAt = Date.now();
|
|
5795
|
-
await writeMetadata(workflowId, namespace, metadata);
|
|
5796
|
-
const key = getSubscriberKey(workflowId, namespace);
|
|
5797
|
-
const subs = subscribers.get(key);
|
|
5798
|
-
if (subs) {
|
|
5799
|
-
for (const callback of subs) {
|
|
5800
|
-
try {
|
|
5801
|
-
callback(item);
|
|
5802
|
-
} catch {
|
|
5803
|
-
}
|
|
5804
|
-
}
|
|
5805
|
-
}
|
|
5806
|
-
return ok2(void 0);
|
|
5807
|
-
} catch (cause) {
|
|
5808
|
-
return err(
|
|
5809
|
-
streamStoreError("write_error", "Failed to append item", cause)
|
|
5810
|
-
);
|
|
5811
|
-
}
|
|
5812
|
-
},
|
|
5813
|
-
async read(workflowId, namespace, startIndex, limit) {
|
|
5814
|
-
try {
|
|
5815
|
-
const serializedItems = await readItems(
|
|
5816
|
-
workflowId,
|
|
5817
|
-
namespace,
|
|
5818
|
-
startIndex,
|
|
5819
|
-
limit
|
|
5820
|
-
);
|
|
5821
|
-
const items = serializedItems.map((si) => ({
|
|
5822
|
-
value: si.value,
|
|
5823
|
-
position: si.position,
|
|
5824
|
-
ts: si.ts
|
|
5825
|
-
}));
|
|
5826
|
-
return ok2(items);
|
|
5827
|
-
} catch (cause) {
|
|
5828
|
-
return err(streamStoreError("read_error", "Failed to read items", cause));
|
|
5829
|
-
}
|
|
5830
|
-
},
|
|
5831
|
-
async getMetadata(workflowId, namespace) {
|
|
5832
|
-
try {
|
|
5833
|
-
const meta = await readMetadata(workflowId, namespace);
|
|
5834
|
-
if (!meta) {
|
|
5835
|
-
return ok2(void 0);
|
|
5836
|
-
}
|
|
5837
|
-
return ok2({
|
|
5838
|
-
id: meta.id,
|
|
5839
|
-
namespace: meta.namespace,
|
|
5840
|
-
workflowId: meta.workflowId,
|
|
5841
|
-
length: meta.length,
|
|
5842
|
-
closed: meta.closed,
|
|
5843
|
-
createdAt: meta.createdAt,
|
|
5844
|
-
lastWriteAt: meta.lastWriteAt,
|
|
5845
|
-
closedAt: meta.closedAt
|
|
5846
|
-
});
|
|
5847
|
-
} catch (cause) {
|
|
5848
|
-
return err(
|
|
5849
|
-
streamStoreError("metadata_error", "Failed to get metadata", cause)
|
|
5850
|
-
);
|
|
5851
|
-
}
|
|
5852
|
-
},
|
|
5853
|
-
async closeStream(workflowId, namespace) {
|
|
5854
|
-
try {
|
|
5855
|
-
const metadata = await readMetadata(workflowId, namespace);
|
|
5856
|
-
if (metadata) {
|
|
5857
|
-
metadata.closed = true;
|
|
5858
|
-
metadata.closedAt = Date.now();
|
|
5859
|
-
await writeMetadata(workflowId, namespace, metadata);
|
|
5860
|
-
}
|
|
5861
|
-
return ok2(void 0);
|
|
5862
|
-
} catch (cause) {
|
|
5863
|
-
return err(
|
|
5864
|
-
streamStoreError("close_error", "Failed to close stream", cause)
|
|
5865
|
-
);
|
|
5866
|
-
}
|
|
5867
|
-
},
|
|
5868
|
-
subscribe(workflowId, namespace, callback) {
|
|
5869
|
-
const key = getSubscriberKey(workflowId, namespace);
|
|
5870
|
-
let subs = subscribers.get(key);
|
|
5871
|
-
if (!subs) {
|
|
5872
|
-
subs = /* @__PURE__ */ new Set();
|
|
5873
|
-
subscribers.set(key, subs);
|
|
5874
|
-
}
|
|
5875
|
-
const typedCallback = callback;
|
|
5876
|
-
subs.add(typedCallback);
|
|
5877
|
-
return () => {
|
|
5878
|
-
subs?.delete(typedCallback);
|
|
5879
|
-
if (subs?.size === 0) {
|
|
5880
|
-
subscribers.delete(key);
|
|
5881
|
-
}
|
|
5882
|
-
};
|
|
5883
|
-
}
|
|
5884
|
-
};
|
|
5885
|
-
}
|
|
5886
|
-
|
|
5887
|
-
// src/streaming/transformers.ts
|
|
5888
|
-
async function* toAsyncIterable(reader) {
|
|
5889
|
-
while (reader.readable) {
|
|
5890
|
-
const result = await reader.read();
|
|
5891
|
-
if (!result.ok) {
|
|
5892
|
-
if (isStreamEnded(result.error)) {
|
|
5893
|
-
break;
|
|
5894
|
-
}
|
|
5895
|
-
throw result.error;
|
|
5896
|
-
}
|
|
5897
|
-
yield result.value;
|
|
5898
|
-
}
|
|
5899
|
-
}
|
|
5900
|
-
async function* map(source, fn) {
|
|
5901
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
5902
|
-
let index = 0;
|
|
5903
|
-
for await (const item of iterable) {
|
|
5904
|
-
yield await fn(item, index++);
|
|
5905
|
-
}
|
|
5906
|
-
}
|
|
5907
|
-
async function* filter(source, predicate) {
|
|
5908
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
5909
|
-
let index = 0;
|
|
5910
|
-
for await (const item of iterable) {
|
|
5911
|
-
if (await predicate(item, index++)) {
|
|
5912
|
-
yield item;
|
|
5913
|
-
}
|
|
5914
|
-
}
|
|
5915
|
-
}
|
|
5916
|
-
async function* chunk(source, size) {
|
|
5917
|
-
if (size < 1) {
|
|
5918
|
-
throw new Error("Chunk size must be at least 1");
|
|
5919
|
-
}
|
|
5920
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
5921
|
-
let currentChunk = [];
|
|
5922
|
-
for await (const item of iterable) {
|
|
5923
|
-
currentChunk.push(item);
|
|
5924
|
-
if (currentChunk.length >= size) {
|
|
5925
|
-
yield currentChunk;
|
|
5926
|
-
currentChunk = [];
|
|
5927
|
-
}
|
|
5928
|
-
}
|
|
5929
|
-
if (currentChunk.length > 0) {
|
|
5930
|
-
yield currentChunk;
|
|
5931
|
-
}
|
|
5932
|
-
}
|
|
5933
|
-
async function* flatMap(source, fn) {
|
|
5934
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
5935
|
-
let index = 0;
|
|
5936
|
-
for await (const item of iterable) {
|
|
5937
|
-
const mapped = fn(item, index++);
|
|
5938
|
-
if (Symbol.asyncIterator in mapped) {
|
|
5939
|
-
for await (const subItem of mapped) {
|
|
5940
|
-
yield subItem;
|
|
5941
|
-
}
|
|
5942
|
-
} else {
|
|
5943
|
-
for (const subItem of mapped) {
|
|
5944
|
-
yield subItem;
|
|
5945
|
-
}
|
|
5946
|
-
}
|
|
5947
|
-
}
|
|
5948
|
-
}
|
|
5949
|
-
async function* flatMapAsync(source, fn) {
|
|
5950
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
5951
|
-
let index = 0;
|
|
5952
|
-
for await (const item of iterable) {
|
|
5953
|
-
const result = await fn(item, index++);
|
|
5954
|
-
if (!result.ok) {
|
|
5955
|
-
yield err(result.error);
|
|
5956
|
-
return;
|
|
5957
|
-
}
|
|
5958
|
-
const mapped = result.value;
|
|
5959
|
-
if (Symbol.asyncIterator in mapped) {
|
|
5960
|
-
for await (const subItem of mapped) {
|
|
5961
|
-
yield ok2(subItem);
|
|
5962
|
-
}
|
|
5963
|
-
} else {
|
|
5964
|
-
for (const subItem of mapped) {
|
|
5965
|
-
yield ok2(subItem);
|
|
5966
|
-
}
|
|
5967
|
-
}
|
|
5968
|
-
}
|
|
5969
|
-
}
|
|
5970
|
-
async function* mapAsync(source, fn) {
|
|
5971
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
5972
|
-
let index = 0;
|
|
5973
|
-
for await (const item of iterable) {
|
|
5974
|
-
yield await fn(item, index++);
|
|
5975
|
-
}
|
|
5976
|
-
}
|
|
5977
|
-
async function* take(source, count) {
|
|
5978
|
-
if (count <= 0) return;
|
|
5979
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
5980
|
-
let taken = 0;
|
|
5981
|
-
for await (const item of iterable) {
|
|
5982
|
-
yield item;
|
|
5983
|
-
taken++;
|
|
5984
|
-
if (taken >= count) break;
|
|
5985
|
-
}
|
|
5986
|
-
}
|
|
5987
|
-
async function* skip(source, count) {
|
|
5988
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
5989
|
-
let skipped = 0;
|
|
5990
|
-
for await (const item of iterable) {
|
|
5991
|
-
if (skipped < count) {
|
|
5992
|
-
skipped++;
|
|
5993
|
-
continue;
|
|
5994
|
-
}
|
|
5995
|
-
yield item;
|
|
5996
|
-
}
|
|
5997
|
-
}
|
|
5998
|
-
async function* takeWhile(source, predicate) {
|
|
5999
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
6000
|
-
let index = 0;
|
|
6001
|
-
for await (const item of iterable) {
|
|
6002
|
-
if (!await predicate(item, index++)) break;
|
|
6003
|
-
yield item;
|
|
6004
|
-
}
|
|
6005
|
-
}
|
|
6006
|
-
async function* skipWhile(source, predicate) {
|
|
6007
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
6008
|
-
let skipping = true;
|
|
6009
|
-
let index = 0;
|
|
6010
|
-
for await (const item of iterable) {
|
|
6011
|
-
if (skipping && await predicate(item, index++)) {
|
|
6012
|
-
continue;
|
|
6013
|
-
}
|
|
6014
|
-
skipping = false;
|
|
6015
|
-
yield item;
|
|
6016
|
-
}
|
|
6017
|
-
}
|
|
6018
|
-
async function collect(source) {
|
|
6019
|
-
const items = [];
|
|
6020
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
6021
|
-
for await (const item of iterable) {
|
|
6022
|
-
items.push(item);
|
|
6023
|
-
}
|
|
6024
|
-
return items;
|
|
6025
|
-
}
|
|
6026
|
-
async function reduce(source, reducer, initial) {
|
|
6027
|
-
let accumulator = initial;
|
|
6028
|
-
const iterable = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
6029
|
-
let index = 0;
|
|
6030
|
-
for await (const item of iterable) {
|
|
6031
|
-
accumulator = await reducer(accumulator, item, index++);
|
|
6032
|
-
}
|
|
6033
|
-
return accumulator;
|
|
6034
|
-
}
|
|
6035
|
-
function isStreamReader(value) {
|
|
6036
|
-
return typeof value === "object" && value !== null && "read" in value && typeof value.read === "function" && "readable" in value;
|
|
6037
|
-
}
|
|
6038
|
-
function pipe(source, ...transformers) {
|
|
6039
|
-
let result = isStreamReader(source) ? toAsyncIterable(source) : source;
|
|
6040
|
-
for (const transformer of transformers) {
|
|
6041
|
-
result = transformer(result);
|
|
6042
|
-
}
|
|
6043
|
-
return result;
|
|
6044
|
-
}
|
|
6045
|
-
|
|
6046
|
-
// src/streaming/index.ts
|
|
6047
|
-
function getStreamReader(options) {
|
|
6048
|
-
const {
|
|
6049
|
-
store,
|
|
6050
|
-
workflowId,
|
|
6051
|
-
namespace = "default",
|
|
6052
|
-
startIndex = 0,
|
|
6053
|
-
pollInterval = 100,
|
|
6054
|
-
pollTimeout = 3e4
|
|
6055
|
-
} = options;
|
|
6056
|
-
let position = startIndex;
|
|
6057
|
-
let readable = true;
|
|
6058
|
-
let closed = false;
|
|
6059
|
-
let bufferedItems = [];
|
|
6060
|
-
let bufferIndex = 0;
|
|
6061
|
-
const reader = {
|
|
6062
|
-
async read() {
|
|
6063
|
-
if (closed) {
|
|
6064
|
-
return err(streamReadError("closed", "Reader is closed"));
|
|
6065
|
-
}
|
|
6066
|
-
if (bufferIndex < bufferedItems.length) {
|
|
6067
|
-
const item = bufferedItems[bufferIndex++];
|
|
6068
|
-
position = item.position + 1;
|
|
6069
|
-
return ok2(item.value);
|
|
6070
|
-
}
|
|
6071
|
-
const pollStart = Date.now();
|
|
6072
|
-
while (Date.now() - pollStart < pollTimeout) {
|
|
6073
|
-
const result = await store.read(workflowId, namespace, position, 100);
|
|
6074
|
-
if (!result.ok) {
|
|
6075
|
-
return err(streamReadError("store_error", result.error.message, result.error));
|
|
6076
|
-
}
|
|
6077
|
-
const items = result.value;
|
|
6078
|
-
if (items.length > 0) {
|
|
6079
|
-
bufferedItems = items;
|
|
6080
|
-
bufferIndex = 1;
|
|
6081
|
-
const item = items[0];
|
|
6082
|
-
position = item.position + 1;
|
|
6083
|
-
return ok2(item.value);
|
|
6084
|
-
}
|
|
6085
|
-
const metaResult = await store.getMetadata(workflowId, namespace);
|
|
6086
|
-
if (metaResult.ok && metaResult.value?.closed) {
|
|
6087
|
-
readable = false;
|
|
6088
|
-
return err(streamEnded(position));
|
|
6089
|
-
}
|
|
6090
|
-
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
6091
|
-
}
|
|
6092
|
-
readable = false;
|
|
6093
|
-
return err(streamEnded(position));
|
|
6094
|
-
},
|
|
6095
|
-
close() {
|
|
6096
|
-
closed = true;
|
|
6097
|
-
readable = false;
|
|
6098
|
-
bufferedItems = [];
|
|
6099
|
-
},
|
|
6100
|
-
get readable() {
|
|
6101
|
-
return readable;
|
|
6102
|
-
},
|
|
6103
|
-
get position() {
|
|
6104
|
-
return position;
|
|
6105
|
-
},
|
|
6106
|
-
get namespace() {
|
|
6107
|
-
return namespace;
|
|
6108
|
-
}
|
|
6109
|
-
};
|
|
6110
|
-
return reader;
|
|
6111
|
-
}
|
|
6112
|
-
|
|
6113
|
-
// src/webhook/index.ts
|
|
6114
|
-
function isValidationError(e) {
|
|
6115
|
-
return typeof e === "object" && e !== null && e.type === "VALIDATION_ERROR";
|
|
6116
|
-
}
|
|
6117
|
-
function defaultValidationErrorMapper(error) {
|
|
6118
|
-
return {
|
|
6119
|
-
status: 400,
|
|
6120
|
-
body: {
|
|
6121
|
-
error: {
|
|
6122
|
-
type: error.type,
|
|
6123
|
-
message: error.message,
|
|
6124
|
-
details: error.field ? { field: error.field } : error.details
|
|
6125
|
-
}
|
|
6126
|
-
}
|
|
6127
|
-
};
|
|
6128
|
-
}
|
|
6129
|
-
function defaultUnexpectedErrorMapper(error) {
|
|
6130
|
-
return {
|
|
6131
|
-
status: 500,
|
|
6132
|
-
body: {
|
|
6133
|
-
error: {
|
|
6134
|
-
type: "INTERNAL_ERROR",
|
|
6135
|
-
message: error instanceof Error ? error.message : "An unexpected error occurred"
|
|
6136
|
-
}
|
|
6137
|
-
}
|
|
6138
|
-
};
|
|
6139
|
-
}
|
|
6140
|
-
function createWebhookHandler(workflow, workflowFn, config) {
|
|
6141
|
-
const {
|
|
6142
|
-
validateInput: validateInput2,
|
|
6143
|
-
mapResult,
|
|
6144
|
-
mapValidationError = defaultValidationErrorMapper,
|
|
6145
|
-
mapUnexpectedError = defaultUnexpectedErrorMapper,
|
|
6146
|
-
beforeValidation,
|
|
6147
|
-
afterResponse
|
|
6148
|
-
} = config;
|
|
6149
|
-
return async (req) => {
|
|
6150
|
-
try {
|
|
6151
|
-
const processedReq = beforeValidation ? await beforeValidation(req) : req;
|
|
6152
|
-
const validationResult = await validateInput2(processedReq);
|
|
6153
|
-
if (!isOk(validationResult)) {
|
|
6154
|
-
const response2 = mapValidationError(validationResult.error, processedReq);
|
|
6155
|
-
return afterResponse ? await afterResponse(response2, processedReq) : response2;
|
|
6156
|
-
}
|
|
6157
|
-
const args = validationResult.value;
|
|
6158
|
-
const workflowResult = await workflow.run(
|
|
6159
|
-
async ({ step, deps }) => workflowFn({ step, deps, args })
|
|
6160
|
-
);
|
|
6161
|
-
const response = mapResult(workflowResult, processedReq);
|
|
6162
|
-
return afterResponse ? await afterResponse(response, processedReq) : response;
|
|
6163
|
-
} catch (error) {
|
|
6164
|
-
const response = mapUnexpectedError(error, req);
|
|
6165
|
-
return afterResponse ? await afterResponse(response, req) : response;
|
|
6166
|
-
}
|
|
6167
|
-
};
|
|
6168
|
-
}
|
|
6169
|
-
function createSimpleHandler(config) {
|
|
6170
|
-
const {
|
|
6171
|
-
validateInput: validateInput2,
|
|
6172
|
-
handler,
|
|
6173
|
-
mapResult,
|
|
6174
|
-
mapValidationError = defaultValidationErrorMapper,
|
|
6175
|
-
mapUnexpectedError = defaultUnexpectedErrorMapper
|
|
6176
|
-
} = config;
|
|
6177
|
-
return async (req) => {
|
|
6178
|
-
try {
|
|
6179
|
-
const validationResult = await validateInput2(req);
|
|
6180
|
-
if (!isOk(validationResult)) {
|
|
6181
|
-
return mapValidationError(validationResult.error, req);
|
|
6182
|
-
}
|
|
6183
|
-
const result = await handler(validationResult.value, req);
|
|
6184
|
-
return mapResult(result, req);
|
|
6185
|
-
} catch (error) {
|
|
6186
|
-
return mapUnexpectedError(error, req);
|
|
6187
|
-
}
|
|
6188
|
-
};
|
|
6189
|
-
}
|
|
6190
|
-
function createResultMapper(mappings, options = {}) {
|
|
6191
|
-
const { defaultStatus = 500, successStatus = 200 } = options;
|
|
6192
|
-
const errorMap = /* @__PURE__ */ new Map();
|
|
6193
|
-
for (const mapping of mappings) {
|
|
6194
|
-
errorMap.set(mapping.error, mapping);
|
|
6195
|
-
}
|
|
6196
|
-
return (result) => {
|
|
6197
|
-
if (result.ok) {
|
|
6198
|
-
return {
|
|
6199
|
-
status: successStatus,
|
|
6200
|
-
body: result.value
|
|
6201
|
-
};
|
|
6202
|
-
}
|
|
6203
|
-
if (isUnexpectedError(result.error)) {
|
|
6204
|
-
return {
|
|
6205
|
-
status: 500,
|
|
6206
|
-
body: {
|
|
6207
|
-
error: {
|
|
6208
|
-
type: "INTERNAL_ERROR",
|
|
6209
|
-
message: result.error instanceof Error ? result.error.message : "An unexpected error occurred"
|
|
6210
|
-
}
|
|
6211
|
-
}
|
|
6212
|
-
};
|
|
6213
|
-
}
|
|
6214
|
-
const mapping = errorMap.get(result.error);
|
|
6215
|
-
if (mapping) {
|
|
6216
|
-
return {
|
|
6217
|
-
status: mapping.status,
|
|
6218
|
-
body: {
|
|
6219
|
-
error: {
|
|
6220
|
-
type: String(mapping.error),
|
|
6221
|
-
message: mapping.message
|
|
6222
|
-
}
|
|
6223
|
-
}
|
|
6224
|
-
};
|
|
6225
|
-
}
|
|
6226
|
-
return {
|
|
6227
|
-
status: defaultStatus,
|
|
6228
|
-
body: {
|
|
6229
|
-
error: {
|
|
6230
|
-
type: String(result.error)
|
|
6231
|
-
}
|
|
6232
|
-
}
|
|
6233
|
-
};
|
|
6234
|
-
};
|
|
6235
|
-
}
|
|
6236
|
-
function toWebhookRequest(req) {
|
|
6237
|
-
return {
|
|
6238
|
-
method: req.method,
|
|
6239
|
-
path: req.path,
|
|
6240
|
-
headers: req.headers,
|
|
6241
|
-
body: req.body,
|
|
6242
|
-
query: req.query,
|
|
6243
|
-
params: req.params,
|
|
6244
|
-
raw: req
|
|
6245
|
-
};
|
|
6246
|
-
}
|
|
6247
|
-
function sendWebhookResponse(res, response) {
|
|
6248
|
-
if (response.headers) {
|
|
6249
|
-
res.set(response.headers);
|
|
6250
|
-
}
|
|
6251
|
-
res.status(response.status).json(response.body);
|
|
6252
|
-
}
|
|
6253
|
-
function createExpressHandler(handler) {
|
|
6254
|
-
return async (req, res) => {
|
|
6255
|
-
const webhookReq = toWebhookRequest(req);
|
|
6256
|
-
const response = await handler(webhookReq);
|
|
6257
|
-
sendWebhookResponse(res, response);
|
|
6258
|
-
};
|
|
6259
|
-
}
|
|
6260
|
-
function validationError(message, field, details) {
|
|
6261
|
-
return {
|
|
6262
|
-
type: "VALIDATION_ERROR",
|
|
6263
|
-
message,
|
|
6264
|
-
field,
|
|
6265
|
-
details
|
|
6266
|
-
};
|
|
6267
|
-
}
|
|
6268
|
-
function requireFields(fields) {
|
|
6269
|
-
return (body) => {
|
|
6270
|
-
for (const field of fields) {
|
|
6271
|
-
if (body[field] === void 0 || body[field] === null || body[field] === "") {
|
|
6272
|
-
return err(validationError(`Missing required field: ${field}`, field));
|
|
6273
|
-
}
|
|
6274
|
-
}
|
|
6275
|
-
return ok2(void 0);
|
|
6276
|
-
};
|
|
6277
|
-
}
|
|
6278
|
-
function composeValidators(...validators) {
|
|
6279
|
-
return (input) => {
|
|
6280
|
-
for (const validator of validators) {
|
|
6281
|
-
const result = validator(input);
|
|
6282
|
-
if (!result.ok) return result;
|
|
6283
|
-
}
|
|
6284
|
-
return ok2(void 0);
|
|
6285
|
-
};
|
|
6286
|
-
}
|
|
6287
|
-
function createEventHandler(workflow, workflowFn, config) {
|
|
6288
|
-
const { validatePayload, mapResult } = config;
|
|
6289
|
-
return async (event) => {
|
|
6290
|
-
try {
|
|
6291
|
-
const validationResult = validatePayload(event);
|
|
6292
|
-
if (!isOk(validationResult)) {
|
|
6293
|
-
return {
|
|
6294
|
-
success: false,
|
|
6295
|
-
ack: true,
|
|
6296
|
-
// Don't retry validation errors
|
|
6297
|
-
error: {
|
|
6298
|
-
type: validationResult.error.type,
|
|
6299
|
-
message: validationResult.error.message,
|
|
6300
|
-
retryable: false
|
|
6301
|
-
}
|
|
6302
|
-
};
|
|
6303
|
-
}
|
|
6304
|
-
const args = validationResult.value;
|
|
6305
|
-
const workflowResult = await workflow.run(
|
|
6306
|
-
async ({ step, deps }) => workflowFn({ step, deps, args })
|
|
6307
|
-
);
|
|
6308
|
-
return mapResult(workflowResult, event);
|
|
6309
|
-
} catch (error) {
|
|
6310
|
-
return {
|
|
6311
|
-
success: false,
|
|
6312
|
-
ack: false,
|
|
6313
|
-
// Retry unexpected errors
|
|
6314
|
-
error: {
|
|
6315
|
-
type: "UnexpectedError",
|
|
6316
|
-
message: error instanceof Error ? error.message : String(error),
|
|
6317
|
-
retryable: true
|
|
6318
|
-
}
|
|
6319
|
-
};
|
|
6320
|
-
}
|
|
6321
|
-
};
|
|
6322
|
-
}
|
|
6323
|
-
|
|
6324
|
-
// src/engine/index.ts
|
|
6325
|
-
import { randomUUID } from "crypto";
|
|
6326
|
-
function createEngine(options) {
|
|
6327
|
-
const {
|
|
6328
|
-
store,
|
|
6329
|
-
workflows,
|
|
6330
|
-
concurrency = 5,
|
|
6331
|
-
onEvent,
|
|
6332
|
-
onError
|
|
6333
|
-
} = options;
|
|
6334
|
-
const schedules = /* @__PURE__ */ new Map();
|
|
6335
|
-
let pollTimer;
|
|
6336
|
-
let running = false;
|
|
6337
|
-
let tickInFlight = false;
|
|
6338
|
-
function emit(event) {
|
|
6339
|
-
try {
|
|
6340
|
-
onEvent?.(event);
|
|
6341
|
-
} catch {
|
|
6342
|
-
}
|
|
6343
|
-
}
|
|
6344
|
-
async function enqueue(name, opts) {
|
|
6345
|
-
const wf = workflows[name];
|
|
6346
|
-
if (!wf) {
|
|
6347
|
-
throw new Error(`Unknown workflow: '${name}'. Registered: ${Object.keys(workflows).join(", ")}`);
|
|
6348
|
-
}
|
|
6349
|
-
const id = opts?.id ?? `${name}:${randomUUID()}`;
|
|
6350
|
-
const snapshot = {
|
|
6351
|
-
formatVersion: 1,
|
|
6352
|
-
workflowName: name,
|
|
6353
|
-
steps: {},
|
|
6354
|
-
execution: {
|
|
6355
|
-
status: "running",
|
|
6356
|
-
// "running" so durable.run can resume it
|
|
6357
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
6358
|
-
},
|
|
6359
|
-
metadata: {
|
|
6360
|
-
engineState: "queued",
|
|
6361
|
-
workflowName: name,
|
|
6362
|
-
input: opts?.input ?? null,
|
|
6363
|
-
idempotencyKey: opts?.idempotencyKey,
|
|
6364
|
-
enqueuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6365
|
-
...opts?.metadata
|
|
6366
|
-
}
|
|
6367
|
-
};
|
|
6368
|
-
await store.save(id, snapshot);
|
|
6369
|
-
emit({ type: "workflow_enqueued", workflowName: name, id, ts: Date.now() });
|
|
6370
|
-
return id;
|
|
6371
|
-
}
|
|
6372
|
-
async function tick() {
|
|
6373
|
-
if (tickInFlight) return 0;
|
|
6374
|
-
tickInFlight = true;
|
|
6375
|
-
try {
|
|
6376
|
-
const entries = await store.list({ limit: concurrency * 2 });
|
|
6377
|
-
let processed = 0;
|
|
6378
|
-
const queued = [];
|
|
6379
|
-
for (const entry of entries) {
|
|
6380
|
-
if (queued.length >= concurrency) break;
|
|
6381
|
-
const snapshot = await store.load(entry.id);
|
|
6382
|
-
if (snapshot && snapshot.metadata?.engineState === "queued" && typeof snapshot.metadata?.workflowName === "string") {
|
|
6383
|
-
queued.push({ id: entry.id, workflowName: snapshot.metadata.workflowName, snapshot });
|
|
6384
|
-
}
|
|
6385
|
-
}
|
|
6386
|
-
const results = await Promise.allSettled(
|
|
6387
|
-
queued.map(async ({ id, workflowName, snapshot }) => {
|
|
6388
|
-
const wf = workflows[workflowName];
|
|
6389
|
-
if (!wf) return;
|
|
6390
|
-
const processingSnapshot = {
|
|
6391
|
-
...snapshot,
|
|
6392
|
-
metadata: {
|
|
6393
|
-
...snapshot.metadata,
|
|
6394
|
-
engineState: "processing"
|
|
6395
|
-
}
|
|
6396
|
-
};
|
|
6397
|
-
await store.save(id, processingSnapshot);
|
|
6398
|
-
emit({ type: "workflow_started", workflowName, id, ts: Date.now() });
|
|
6399
|
-
const result = await durable.run(wf.deps, wf.fn, {
|
|
6400
|
-
id,
|
|
6401
|
-
store,
|
|
6402
|
-
idempotencyKey: snapshot.metadata?.idempotencyKey,
|
|
6403
|
-
input: snapshot.metadata?.input,
|
|
6404
|
-
...wf.durableDefaults
|
|
6405
|
-
});
|
|
6406
|
-
if (result.ok) {
|
|
6407
|
-
emit({ type: "workflow_completed", workflowName, id, ts: Date.now() });
|
|
6408
|
-
} else {
|
|
6409
|
-
emit({ type: "workflow_failed", workflowName, id, error: result.error, ts: Date.now() });
|
|
6410
|
-
}
|
|
6411
|
-
processed++;
|
|
6412
|
-
})
|
|
6413
|
-
);
|
|
6414
|
-
for (const r of results) {
|
|
6415
|
-
if (r.status === "rejected") {
|
|
6416
|
-
try {
|
|
6417
|
-
onError?.(r.reason);
|
|
6418
|
-
} catch {
|
|
6419
|
-
}
|
|
6420
|
-
}
|
|
6421
|
-
}
|
|
6422
|
-
emit({ type: "engine_tick", ts: Date.now(), processed });
|
|
6423
|
-
return processed;
|
|
6424
|
-
} finally {
|
|
6425
|
-
tickInFlight = false;
|
|
6426
|
-
}
|
|
6427
|
-
}
|
|
6428
|
-
function schedule(name, opts) {
|
|
6429
|
-
const scheduleId = opts?.id ? `schedule:${opts.id}` : `schedule:${randomUUID()}`;
|
|
6430
|
-
if (opts.immediate) {
|
|
6431
|
-
void enqueue(name, opts).catch((e) => {
|
|
6432
|
-
try {
|
|
6433
|
-
onError?.(e);
|
|
6434
|
-
} catch {
|
|
6435
|
-
}
|
|
6436
|
-
});
|
|
6437
|
-
}
|
|
6438
|
-
const interval = setInterval(() => {
|
|
6439
|
-
void enqueue(name, opts).catch((e) => {
|
|
6440
|
-
try {
|
|
6441
|
-
onError?.(e);
|
|
6442
|
-
} catch {
|
|
6443
|
-
}
|
|
6444
|
-
});
|
|
6445
|
-
}, opts.intervalMs);
|
|
6446
|
-
schedules.set(scheduleId, interval);
|
|
6447
|
-
emit({ type: "schedule_created", workflowName: name, scheduleId, intervalMs: opts.intervalMs, ts: Date.now() });
|
|
6448
|
-
return scheduleId;
|
|
6449
|
-
}
|
|
6450
|
-
function unschedule(scheduleId) {
|
|
6451
|
-
const interval = schedules.get(scheduleId);
|
|
6452
|
-
if (!interval) return false;
|
|
6453
|
-
clearInterval(interval);
|
|
6454
|
-
schedules.delete(scheduleId);
|
|
6455
|
-
emit({ type: "schedule_removed", scheduleId, ts: Date.now() });
|
|
6456
|
-
return true;
|
|
6457
|
-
}
|
|
6458
|
-
function start(pollIntervalMs = 1e3) {
|
|
6459
|
-
if (running) return;
|
|
6460
|
-
running = true;
|
|
6461
|
-
emit({ type: "engine_start", ts: Date.now() });
|
|
6462
|
-
void tick().catch((e) => {
|
|
6463
|
-
try {
|
|
6464
|
-
onError?.(e);
|
|
6465
|
-
} catch {
|
|
6466
|
-
}
|
|
6467
|
-
});
|
|
6468
|
-
pollTimer = setInterval(() => {
|
|
6469
|
-
void tick().catch((e) => {
|
|
6470
|
-
try {
|
|
6471
|
-
onError?.(e);
|
|
6472
|
-
} catch {
|
|
6473
|
-
}
|
|
6474
|
-
});
|
|
6475
|
-
}, pollIntervalMs);
|
|
6476
|
-
}
|
|
6477
|
-
async function stop() {
|
|
6478
|
-
if (!running) return;
|
|
6479
|
-
running = false;
|
|
6480
|
-
if (pollTimer) {
|
|
6481
|
-
clearInterval(pollTimer);
|
|
6482
|
-
pollTimer = void 0;
|
|
6483
|
-
}
|
|
6484
|
-
for (const [id, interval] of schedules) {
|
|
6485
|
-
clearInterval(interval);
|
|
6486
|
-
schedules.delete(id);
|
|
6487
|
-
}
|
|
6488
|
-
while (tickInFlight) {
|
|
6489
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
6490
|
-
}
|
|
6491
|
-
emit({ type: "engine_stop", ts: Date.now() });
|
|
6492
|
-
}
|
|
6493
|
-
return {
|
|
6494
|
-
enqueue,
|
|
6495
|
-
schedule,
|
|
6496
|
-
unschedule,
|
|
6497
|
-
start,
|
|
6498
|
-
stop,
|
|
6499
|
-
tick,
|
|
6500
|
-
status: () => ({ running, pendingSchedules: schedules.size })
|
|
6501
|
-
};
|
|
6502
|
-
}
|
|
6503
|
-
|
|
6504
4050
|
// src/resource.ts
|
|
6505
4051
|
function isResourceCleanupError(error) {
|
|
6506
4052
|
return typeof error === "object" && error !== null && error.type === "RESOURCE_CLEANUP_ERROR";
|
|
@@ -6792,66 +4338,26 @@ export {
|
|
|
6792
4338
|
Duration,
|
|
6793
4339
|
HOOK_STEP_KEY_PREFIX2 as HOOK_STEP_KEY_PREFIX,
|
|
6794
4340
|
STEP_TIMEOUT_MARKER,
|
|
6795
|
-
STREAM_BACKPRESSURE_ERROR,
|
|
6796
|
-
STREAM_CLOSE_ERROR,
|
|
6797
|
-
STREAM_ENDED,
|
|
6798
|
-
STREAM_READ_ERROR,
|
|
6799
|
-
STREAM_STORE_ERROR,
|
|
6800
|
-
STREAM_WRITE_ERROR,
|
|
6801
4341
|
SnapshotDecodeError,
|
|
6802
4342
|
SnapshotFormatError,
|
|
6803
4343
|
SnapshotMismatchError,
|
|
6804
4344
|
UnexpectedError,
|
|
6805
4345
|
assertValidSnapshot,
|
|
6806
4346
|
batchPresets,
|
|
6807
|
-
chunk,
|
|
6808
4347
|
clearStep,
|
|
6809
|
-
collect,
|
|
6810
|
-
composeMigrations,
|
|
6811
|
-
composeValidators,
|
|
6812
|
-
createApprovalChecker,
|
|
6813
4348
|
createApprovalStateCollector,
|
|
6814
4349
|
createApprovalStep,
|
|
6815
|
-
createApprovalWebhookHandler,
|
|
6816
|
-
createBackpressureController,
|
|
6817
|
-
createEngine,
|
|
6818
|
-
createEventHandler,
|
|
6819
|
-
createExpressHandler,
|
|
6820
|
-
createFileStreamStore,
|
|
6821
|
-
createHITLOrchestrator,
|
|
6822
4350
|
createHook,
|
|
6823
|
-
createKeyRemoveMigration,
|
|
6824
|
-
createKeyRenameMigration,
|
|
6825
|
-
createMemoryApprovalStore,
|
|
6826
|
-
createMemoryCache,
|
|
6827
|
-
createMemoryStreamStore,
|
|
6828
|
-
createMemoryWorkflowStateStore,
|
|
6829
4351
|
createResource,
|
|
6830
4352
|
createResourceScope,
|
|
6831
|
-
createResultMapper,
|
|
6832
4353
|
createResumeStateCollector,
|
|
6833
|
-
createSagaWorkflow,
|
|
6834
|
-
createSimpleHandler,
|
|
6835
|
-
createTestableMemoryStreamStore,
|
|
6836
|
-
createValueTransformMigration,
|
|
6837
|
-
createVersionedState,
|
|
6838
|
-
createVersionedStateLoader,
|
|
6839
|
-
createWebhookHandler,
|
|
6840
4354
|
createWorkflow,
|
|
6841
4355
|
days,
|
|
6842
|
-
defaultUnexpectedErrorMapper,
|
|
6843
|
-
defaultValidationErrorMapper,
|
|
6844
|
-
deserializeCauseNew,
|
|
6845
4356
|
deserializeResumeState,
|
|
6846
|
-
durable,
|
|
6847
|
-
filter,
|
|
6848
|
-
flatMap,
|
|
6849
|
-
flatMapAsync,
|
|
6850
4357
|
gatedStep,
|
|
6851
4358
|
getPendingApprovals,
|
|
6852
4359
|
getPendingHooks,
|
|
6853
4360
|
getStepTimeoutMeta,
|
|
6854
|
-
getStreamReader,
|
|
6855
4361
|
hasPendingApproval,
|
|
6856
4362
|
hasPendingHook,
|
|
6857
4363
|
hours,
|
|
@@ -6859,75 +4365,36 @@ export {
|
|
|
6859
4365
|
injectHook,
|
|
6860
4366
|
isApprovalRejected,
|
|
6861
4367
|
isBatchProcessingError,
|
|
6862
|
-
isConcurrentExecution,
|
|
6863
4368
|
isDuration,
|
|
6864
|
-
isIdempotencyConflict,
|
|
6865
4369
|
isInputValidationError,
|
|
6866
4370
|
isInvalidBatchConfigError,
|
|
6867
|
-
isLeaseExpired,
|
|
6868
|
-
isMigrationError,
|
|
6869
4371
|
isPendingApproval,
|
|
6870
4372
|
isPendingHook,
|
|
6871
|
-
isPersistenceError,
|
|
6872
4373
|
isResourceCleanupError,
|
|
6873
4374
|
isResumeState,
|
|
6874
|
-
isSagaCompensationError,
|
|
6875
4375
|
isSerializedResumeState,
|
|
6876
4376
|
isStepComplete,
|
|
6877
4377
|
isStepTimeoutError,
|
|
6878
|
-
isStreamBackpressureError,
|
|
6879
|
-
isStreamEnded,
|
|
6880
|
-
isStreamReadError,
|
|
6881
|
-
isStreamStoreError,
|
|
6882
|
-
isStreamWriteError,
|
|
6883
|
-
isValidationError,
|
|
6884
|
-
isVersionIncompatibleError,
|
|
6885
|
-
isVersionMismatch,
|
|
6886
4378
|
isWorkflowCancelled,
|
|
6887
4379
|
isWorkflowSnapshot,
|
|
6888
4380
|
looksLikeWorkflowSnapshot,
|
|
6889
|
-
map,
|
|
6890
|
-
mapAsync,
|
|
6891
4381
|
mergeSnapshots,
|
|
6892
4382
|
millis,
|
|
6893
4383
|
minutes,
|
|
6894
|
-
parseVersionedState,
|
|
6895
4384
|
pendingApproval,
|
|
6896
4385
|
pendingHook,
|
|
6897
|
-
pipe,
|
|
6898
4386
|
processInBatches,
|
|
6899
|
-
reduce,
|
|
6900
|
-
requireFields,
|
|
6901
4387
|
run,
|
|
6902
|
-
runSaga,
|
|
6903
4388
|
seconds,
|
|
6904
|
-
sendWebhookResponse,
|
|
6905
|
-
serializeError,
|
|
6906
4389
|
serializeResumeState,
|
|
6907
|
-
serializeThrown,
|
|
6908
|
-
shouldApplyBackpressure,
|
|
6909
|
-
skip,
|
|
6910
|
-
skipWhile,
|
|
6911
|
-
streamBackpressureError,
|
|
6912
|
-
streamCloseError,
|
|
6913
|
-
streamEnded,
|
|
6914
|
-
streamReadError,
|
|
6915
|
-
streamStoreError,
|
|
6916
|
-
streamWriteError,
|
|
6917
|
-
stringifyVersionedState,
|
|
6918
|
-
take,
|
|
6919
|
-
takeWhile,
|
|
6920
|
-
toAsyncIterable,
|
|
6921
4390
|
toDays,
|
|
6922
4391
|
toHours,
|
|
6923
4392
|
toMillis,
|
|
6924
4393
|
toMinutes,
|
|
6925
4394
|
toResumeState,
|
|
6926
4395
|
toSeconds,
|
|
6927
|
-
toWebhookRequest,
|
|
6928
4396
|
validateInput,
|
|
6929
4397
|
validateSnapshot,
|
|
6930
|
-
validationError,
|
|
6931
4398
|
withDeps,
|
|
6932
4399
|
withScope
|
|
6933
4400
|
};
|