@dudousxd/nestjs-durable-core 0.28.2 → 0.29.1
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/index.cjs +245 -207
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -17
- package/dist/index.d.ts +13 -17
- package/dist/index.js +245 -207
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -275,6 +275,31 @@ async function runStepHandler(task, handler) {
|
|
|
275
275
|
__name(runStepHandler, "runStepHandler");
|
|
276
276
|
|
|
277
277
|
// src/checkpoints.ts
|
|
278
|
+
function nonEmptyEvents(events) {
|
|
279
|
+
return events && events.length > 0 ? events : void 0;
|
|
280
|
+
}
|
|
281
|
+
__name(nonEmptyEvents, "nonEmptyEvents");
|
|
282
|
+
function stepCheckpoint(p) {
|
|
283
|
+
return {
|
|
284
|
+
runId: p.runId,
|
|
285
|
+
seq: p.seq,
|
|
286
|
+
name: p.name,
|
|
287
|
+
kind: p.kind,
|
|
288
|
+
stepId: stepId(p.runId, p.seq),
|
|
289
|
+
status: p.status,
|
|
290
|
+
input: p.input,
|
|
291
|
+
output: p.output,
|
|
292
|
+
error: p.error,
|
|
293
|
+
events: nonEmptyEvents(p.events),
|
|
294
|
+
attempts: p.attempts,
|
|
295
|
+
workerGroup: p.workerGroup,
|
|
296
|
+
wakeAt: p.wakeAt,
|
|
297
|
+
enqueuedAt: p.enqueuedAt,
|
|
298
|
+
startedAt: p.startedAt,
|
|
299
|
+
finishedAt: p.finishedAt
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
__name(stepCheckpoint, "stepCheckpoint");
|
|
278
303
|
function instantCheckpoint(p) {
|
|
279
304
|
const at = /* @__PURE__ */ new Date();
|
|
280
305
|
return {
|
|
@@ -683,6 +708,93 @@ var QueueController = class {
|
|
|
683
708
|
}
|
|
684
709
|
};
|
|
685
710
|
|
|
711
|
+
// src/singleton-gate.ts
|
|
712
|
+
var SINGLETON_RETRY_MS = 1e3;
|
|
713
|
+
var SINGLETON_RETRY_JITTER_MS = 250;
|
|
714
|
+
var SingletonGate = class {
|
|
715
|
+
static {
|
|
716
|
+
__name(this, "SingletonGate");
|
|
717
|
+
}
|
|
718
|
+
deps;
|
|
719
|
+
constructor(deps) {
|
|
720
|
+
this.deps = deps;
|
|
721
|
+
}
|
|
722
|
+
/** The tag a singleton run carries, so the gate can find others sharing its key. */
|
|
723
|
+
tag(cfg, input) {
|
|
724
|
+
return `singleton:${cfg.key(input)}`;
|
|
725
|
+
}
|
|
726
|
+
/** Next wake time for a gated run: the base retry delay jittered to avoid a wakeup stampede. */
|
|
727
|
+
retryWakeAt() {
|
|
728
|
+
return this.deps.clock() + SINGLETON_RETRY_MS + Math.floor((Math.random() * 2 - 1) * SINGLETON_RETRY_JITTER_MS);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Reject a start that would grow the same-key backlog past `limit + maxQueueDepth` (counting
|
|
732
|
+
* `pending`/`running`/`suspended` runs sharing the key in one scan). No-op when no `maxQueueDepth`
|
|
733
|
+
* is configured.
|
|
734
|
+
*/
|
|
735
|
+
async assertCapacity(workflow, cfg, input) {
|
|
736
|
+
if (cfg.maxQueueDepth == null) return;
|
|
737
|
+
const cap = (cfg.limit ?? 1) + cfg.maxQueueDepth;
|
|
738
|
+
const queued = await this.deps.store.listRuns({
|
|
739
|
+
tag: this.tag(cfg, input),
|
|
740
|
+
workflow,
|
|
741
|
+
statuses: [
|
|
742
|
+
"pending",
|
|
743
|
+
"running",
|
|
744
|
+
"suspended"
|
|
745
|
+
]
|
|
746
|
+
});
|
|
747
|
+
if (queued.length >= cap) {
|
|
748
|
+
throw new SingletonQueueFullError(workflow, cfg.key(input), cfg.maxQueueDepth);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Whether `run` may run now under its key: it's among the `limit` oldest in-flight (running or
|
|
753
|
+
* suspended) runs sharing the key, by `(createdAt, id)` order. A consistent store gives every
|
|
754
|
+
* instance the same ordering, so admission is race-free + FIFO.
|
|
755
|
+
*/
|
|
756
|
+
async admit(run, cfg) {
|
|
757
|
+
const inflight = (await this.deps.store.listRuns({
|
|
758
|
+
tag: this.tag(cfg, run.input),
|
|
759
|
+
workflow: run.workflow,
|
|
760
|
+
statuses: [
|
|
761
|
+
"running",
|
|
762
|
+
"suspended"
|
|
763
|
+
]
|
|
764
|
+
})).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
|
|
765
|
+
const idx = inflight.findIndex((r) => r.id === run.id);
|
|
766
|
+
return idx >= 0 && idx < (cfg.limit ?? 1);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Notify-on-release: a singleton run settled (freeing a slot), so dispatch the oldest gated
|
|
770
|
+
* (`suspended` + same tag) waiters now instead of waiting for their ~1s retry timer. Each re-checks
|
|
771
|
+
* admission in the executor and runs only if it actually wins a slot, so FIFO/race-free guarantees
|
|
772
|
+
* hold; the durable timer remains the cross-instance/crash fallback.
|
|
773
|
+
*/
|
|
774
|
+
async wakeNext(settled) {
|
|
775
|
+
const cfg = this.deps.configFor(settled);
|
|
776
|
+
if (!cfg) return;
|
|
777
|
+
const tag = settled.tags?.find((t) => t.startsWith("singleton:"));
|
|
778
|
+
if (!tag) return;
|
|
779
|
+
const gated = (await this.deps.store.listRuns({
|
|
780
|
+
tag,
|
|
781
|
+
workflow: settled.workflow,
|
|
782
|
+
statuses: [
|
|
783
|
+
"suspended"
|
|
784
|
+
]
|
|
785
|
+
})).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
|
|
786
|
+
for (const next of gated.slice(0, cfg.limit ?? 1)) {
|
|
787
|
+
if (next.wakeAt != null) {
|
|
788
|
+
await this.deps.store.updateRun(next.id, {
|
|
789
|
+
wakeAt: void 0,
|
|
790
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
791
|
+
}).catch(() => void 0);
|
|
792
|
+
}
|
|
793
|
+
this.deps.dispatch(next.id);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
|
|
686
798
|
// src/transport-pool.ts
|
|
687
799
|
var TransportPool = class {
|
|
688
800
|
static {
|
|
@@ -1213,10 +1325,6 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1213
1325
|
__name(createWorkflowCtx, "createWorkflowCtx");
|
|
1214
1326
|
|
|
1215
1327
|
// src/engine.ts
|
|
1216
|
-
var SINGLETON_RETRY_MS = 1e3;
|
|
1217
|
-
var SINGLETON_RETRY_JITTER_MS = 250;
|
|
1218
|
-
var singletonRetryWakeAt = /* @__PURE__ */ __name((nowMs) => nowMs + SINGLETON_RETRY_MS + Math.floor((Math.random() * 2 - 1) * SINGLETON_RETRY_JITTER_MS), "singletonRetryWakeAt");
|
|
1219
|
-
var singletonTag = /* @__PURE__ */ __name((cfg, input) => `singleton:${cfg.key(input)}`, "singletonTag");
|
|
1220
1328
|
function mergeTags(staticTags, runTags) {
|
|
1221
1329
|
if (!staticTags?.length && !runTags?.length) return void 0;
|
|
1222
1330
|
return [
|
|
@@ -1266,6 +1374,8 @@ var WorkflowEngine = class {
|
|
|
1266
1374
|
trackStepStart;
|
|
1267
1375
|
/** Where a freshly-started run executes — in-process by default (see {@link RunDispatcher}). */
|
|
1268
1376
|
runDispatcher;
|
|
1377
|
+
/** Per-key serialization for singleton workflows (admission, back-pressure, notify-on-release). */
|
|
1378
|
+
singletons;
|
|
1269
1379
|
/** Every registered workflow, keyed by `name@version` — so old versions stay runnable. */
|
|
1270
1380
|
workflows = /* @__PURE__ */ new Map();
|
|
1271
1381
|
/** The newest registered version per workflow name — used to `start` new runs. */
|
|
@@ -1323,6 +1433,14 @@ var WorkflowEngine = class {
|
|
|
1323
1433
|
dispatch: /* @__PURE__ */ __name((runId) => queueMicrotask(() => void this.runOne(runId).catch(() => {
|
|
1324
1434
|
})), "dispatch")
|
|
1325
1435
|
};
|
|
1436
|
+
this.singletons = new SingletonGate({
|
|
1437
|
+
store: this.store,
|
|
1438
|
+
clock: this.clock,
|
|
1439
|
+
dispatch: /* @__PURE__ */ __name((runId) => {
|
|
1440
|
+
this.runDispatcher.dispatch(runId);
|
|
1441
|
+
}, "dispatch"),
|
|
1442
|
+
configFor: /* @__PURE__ */ __name((run) => (this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? this.latest.get(run.workflow))?.singleton, "configFor")
|
|
1443
|
+
});
|
|
1326
1444
|
this.pool.bind(
|
|
1327
1445
|
async (result) => {
|
|
1328
1446
|
const waiter = this.pending.get(result.stepId);
|
|
@@ -1573,25 +1691,10 @@ var WorkflowEngine = class {
|
|
|
1573
1691
|
const now = /* @__PURE__ */ new Date();
|
|
1574
1692
|
const tags = mergeTags(registered.tags, registered.singleton ? [
|
|
1575
1693
|
...opts?.tags ?? [],
|
|
1576
|
-
|
|
1694
|
+
this.singletons.tag(registered.singleton, input)
|
|
1577
1695
|
] : opts?.tags);
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
const cfg = registered.singleton;
|
|
1581
|
-
const key = cfg.key(input);
|
|
1582
|
-
const cap = (cfg.limit ?? 1) + maxQueueDepth;
|
|
1583
|
-
const queued = await this.store.listRuns({
|
|
1584
|
-
tag: singletonTag(cfg, input),
|
|
1585
|
-
workflow: name,
|
|
1586
|
-
statuses: [
|
|
1587
|
-
"pending",
|
|
1588
|
-
"running",
|
|
1589
|
-
"suspended"
|
|
1590
|
-
]
|
|
1591
|
-
});
|
|
1592
|
-
if (queued.length >= cap) {
|
|
1593
|
-
throw new SingletonQueueFullError(name, key, maxQueueDepth);
|
|
1594
|
-
}
|
|
1696
|
+
if (registered.singleton) {
|
|
1697
|
+
await this.singletons.assertCapacity(name, registered.singleton, input);
|
|
1595
1698
|
}
|
|
1596
1699
|
const run = {
|
|
1597
1700
|
id: runId,
|
|
@@ -2050,7 +2153,7 @@ var WorkflowEngine = class {
|
|
|
2050
2153
|
from: this.instanceId
|
|
2051
2154
|
}).catch(() => void 0);
|
|
2052
2155
|
}
|
|
2053
|
-
void this.
|
|
2156
|
+
void this.singletons.wakeNext(run).catch(() => void 0);
|
|
2054
2157
|
return {
|
|
2055
2158
|
runId,
|
|
2056
2159
|
status: "cancelled",
|
|
@@ -2157,21 +2260,19 @@ var WorkflowEngine = class {
|
|
|
2157
2260
|
*/
|
|
2158
2261
|
async persistStepEvent(event) {
|
|
2159
2262
|
const startedAt = new Date(event.startedAt);
|
|
2160
|
-
const events = event.events && event.events.length > 0 ? event.events : void 0;
|
|
2161
2263
|
if (event.phase === "running") {
|
|
2162
|
-
await this.store.saveCheckpoint({
|
|
2264
|
+
await this.store.saveCheckpoint(stepCheckpoint({
|
|
2163
2265
|
runId: event.runId,
|
|
2164
2266
|
seq: event.seq,
|
|
2165
2267
|
name: event.name,
|
|
2166
2268
|
kind: "local",
|
|
2167
|
-
stepId: stepId(event.runId, event.seq),
|
|
2168
2269
|
status: "running",
|
|
2169
|
-
events,
|
|
2270
|
+
events: event.events,
|
|
2170
2271
|
attempts: 1,
|
|
2171
2272
|
enqueuedAt: startedAt,
|
|
2172
2273
|
startedAt,
|
|
2173
2274
|
finishedAt: startedAt
|
|
2174
|
-
});
|
|
2275
|
+
}));
|
|
2175
2276
|
this.emit({
|
|
2176
2277
|
type: "step.started",
|
|
2177
2278
|
runId: event.runId,
|
|
@@ -2182,21 +2283,20 @@ var WorkflowEngine = class {
|
|
|
2182
2283
|
return;
|
|
2183
2284
|
}
|
|
2184
2285
|
const failed = event.phase === "failed";
|
|
2185
|
-
await this.store.saveCheckpoint({
|
|
2286
|
+
await this.store.saveCheckpoint(stepCheckpoint({
|
|
2186
2287
|
runId: event.runId,
|
|
2187
2288
|
seq: event.seq,
|
|
2188
2289
|
name: event.name,
|
|
2189
2290
|
kind: "local",
|
|
2190
|
-
stepId: stepId(event.runId, event.seq),
|
|
2191
2291
|
status: failed ? "failed" : "completed",
|
|
2192
2292
|
output: failed ? void 0 : event.output,
|
|
2193
2293
|
error: failed ? event.error : void 0,
|
|
2194
|
-
events,
|
|
2294
|
+
events: event.events,
|
|
2195
2295
|
attempts: 1,
|
|
2196
2296
|
enqueuedAt: startedAt,
|
|
2197
2297
|
startedAt,
|
|
2198
2298
|
finishedAt: event.finishedAt != null ? new Date(event.finishedAt) : /* @__PURE__ */ new Date()
|
|
2199
|
-
});
|
|
2299
|
+
}));
|
|
2200
2300
|
this.emit({
|
|
2201
2301
|
type: failed ? "step.failed" : "step.completed",
|
|
2202
2302
|
runId: event.runId,
|
|
@@ -2216,21 +2316,20 @@ var WorkflowEngine = class {
|
|
|
2216
2316
|
*/
|
|
2217
2317
|
async startStep(step) {
|
|
2218
2318
|
if (this.trackStepStart) {
|
|
2219
|
-
await this.store.saveCheckpoint({
|
|
2319
|
+
await this.store.saveCheckpoint(stepCheckpoint({
|
|
2220
2320
|
runId: step.runId,
|
|
2221
2321
|
seq: step.seq,
|
|
2222
2322
|
name: step.name,
|
|
2223
2323
|
kind: step.kind,
|
|
2224
|
-
stepId: stepId(step.runId, step.seq),
|
|
2225
2324
|
status: "running",
|
|
2226
2325
|
input: step.input,
|
|
2227
|
-
events: step.events
|
|
2326
|
+
events: step.events,
|
|
2228
2327
|
attempts: step.attempts,
|
|
2229
2328
|
workerGroup: step.workerGroup,
|
|
2230
2329
|
enqueuedAt: step.enqueuedAt,
|
|
2231
2330
|
startedAt: step.startedAt,
|
|
2232
2331
|
finishedAt: step.startedAt
|
|
2233
|
-
});
|
|
2332
|
+
}));
|
|
2234
2333
|
}
|
|
2235
2334
|
this.emit({
|
|
2236
2335
|
type: "step.started",
|
|
@@ -2242,22 +2341,21 @@ var WorkflowEngine = class {
|
|
|
2242
2341
|
}
|
|
2243
2342
|
/** Checkpoint a finished step and announce it — the two things that must always happen together. */
|
|
2244
2343
|
async completeStep(step) {
|
|
2245
|
-
await this.store.saveCheckpoint({
|
|
2344
|
+
await this.store.saveCheckpoint(stepCheckpoint({
|
|
2246
2345
|
runId: step.runId,
|
|
2247
2346
|
seq: step.seq,
|
|
2248
2347
|
name: step.name,
|
|
2249
2348
|
kind: step.kind,
|
|
2250
|
-
stepId: stepId(step.runId, step.seq),
|
|
2251
2349
|
status: "completed",
|
|
2252
2350
|
input: step.input,
|
|
2253
2351
|
output: step.output,
|
|
2254
|
-
events: step.events
|
|
2352
|
+
events: step.events,
|
|
2255
2353
|
attempts: step.attempts,
|
|
2256
2354
|
workerGroup: step.workerGroup,
|
|
2257
2355
|
enqueuedAt: step.enqueuedAt,
|
|
2258
2356
|
startedAt: step.startedAt,
|
|
2259
2357
|
finishedAt: /* @__PURE__ */ new Date()
|
|
2260
|
-
});
|
|
2358
|
+
}));
|
|
2261
2359
|
this.emit({
|
|
2262
2360
|
type: "step.completed",
|
|
2263
2361
|
runId: step.runId,
|
|
@@ -2271,22 +2369,21 @@ var WorkflowEngine = class {
|
|
|
2271
2369
|
}
|
|
2272
2370
|
/** Checkpoint a step that failed terminally, so the failure point is visible (not just the run). */
|
|
2273
2371
|
async failStep(step) {
|
|
2274
|
-
await this.store.saveCheckpoint({
|
|
2372
|
+
await this.store.saveCheckpoint(stepCheckpoint({
|
|
2275
2373
|
runId: step.runId,
|
|
2276
2374
|
seq: step.seq,
|
|
2277
2375
|
name: step.name,
|
|
2278
2376
|
kind: step.kind,
|
|
2279
|
-
stepId: stepId(step.runId, step.seq),
|
|
2280
2377
|
status: "failed",
|
|
2281
2378
|
input: step.input,
|
|
2282
2379
|
error: step.error,
|
|
2283
|
-
events: step.events
|
|
2380
|
+
events: step.events,
|
|
2284
2381
|
attempts: step.attempts,
|
|
2285
2382
|
workerGroup: step.workerGroup,
|
|
2286
2383
|
enqueuedAt: step.enqueuedAt,
|
|
2287
2384
|
startedAt: step.startedAt,
|
|
2288
2385
|
finishedAt: /* @__PURE__ */ new Date()
|
|
2289
|
-
});
|
|
2386
|
+
}));
|
|
2290
2387
|
this.emit({
|
|
2291
2388
|
type: "step.failed",
|
|
2292
2389
|
runId: step.runId,
|
|
@@ -2298,55 +2395,6 @@ var WorkflowEngine = class {
|
|
|
2298
2395
|
durationMs: Date.now() - step.startedAt.getTime()
|
|
2299
2396
|
});
|
|
2300
2397
|
}
|
|
2301
|
-
/**
|
|
2302
|
-
* Whether `run` may run now under its singleton key: it's among the `limit` oldest in-flight runs
|
|
2303
|
-
* (running or suspended) sharing the key, by `(createdAt, id)` order. A consistent store gives every
|
|
2304
|
-
* instance the same ordering, so admission is race-free + FIFO.
|
|
2305
|
-
*/
|
|
2306
|
-
async admitSingleton(run, cfg) {
|
|
2307
|
-
const tag = singletonTag(cfg, run.input);
|
|
2308
|
-
const inflight = (await this.store.listRuns({
|
|
2309
|
-
tag,
|
|
2310
|
-
workflow: run.workflow,
|
|
2311
|
-
statuses: [
|
|
2312
|
-
"running",
|
|
2313
|
-
"suspended"
|
|
2314
|
-
]
|
|
2315
|
-
})).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
|
|
2316
|
-
const idx = inflight.findIndex((r) => r.id === run.id);
|
|
2317
|
-
return idx >= 0 && idx < (cfg.limit ?? 1);
|
|
2318
|
-
}
|
|
2319
|
-
/**
|
|
2320
|
-
* Notify-on-release: a singleton run just settled (freeing a slot), so wake the gated waiters that
|
|
2321
|
-
* can now be admitted — immediately, instead of letting them sleep until their ~1s retry timer.
|
|
2322
|
-
* Dispatches up to `limit` of the oldest gated (`suspended` + same singleton tag) waiters by
|
|
2323
|
-
* `(createdAt, id)`; each re-checks admission in `runExecution` and runs only if it actually wins a
|
|
2324
|
-
* slot, so this never breaks the FIFO + race-free guarantees (the durable timer remains the
|
|
2325
|
-
* cross-instance / crash fallback — this just removes the poll latency on the happy path).
|
|
2326
|
-
*/
|
|
2327
|
-
async wakeNextSingletons(settled) {
|
|
2328
|
-
const registered = this.workflows.get(versionKey(settled.workflow, settled.workflowVersion)) ?? this.latest.get(settled.workflow);
|
|
2329
|
-
const cfg = registered?.singleton;
|
|
2330
|
-
if (!cfg) return;
|
|
2331
|
-
const tag = settled.tags?.find((t) => t.startsWith("singleton:"));
|
|
2332
|
-
if (!tag) return;
|
|
2333
|
-
const gated = (await this.store.listRuns({
|
|
2334
|
-
tag,
|
|
2335
|
-
workflow: settled.workflow,
|
|
2336
|
-
statuses: [
|
|
2337
|
-
"suspended"
|
|
2338
|
-
]
|
|
2339
|
-
})).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
|
|
2340
|
-
for (const next of gated.slice(0, cfg.limit ?? 1)) {
|
|
2341
|
-
if (next.wakeAt != null) {
|
|
2342
|
-
await this.store.updateRun(next.id, {
|
|
2343
|
-
wakeAt: void 0,
|
|
2344
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2345
|
-
}).catch(() => void 0);
|
|
2346
|
-
}
|
|
2347
|
-
this.runDispatcher.dispatch(next.id);
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
2398
|
async execute(run, fn) {
|
|
2351
2399
|
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion));
|
|
2352
2400
|
const lockNow = this.clock();
|
|
@@ -2365,7 +2413,7 @@ var WorkflowEngine = class {
|
|
|
2365
2413
|
try {
|
|
2366
2414
|
const result = registered?.remote ? await this.runRemoteExecution(run, registered) : await this.runExecution(run, fn, registered);
|
|
2367
2415
|
if (registered?.singleton && (result.status === "completed" || result.status === "failed" || result.status === "cancelled" || result.status === "dead")) {
|
|
2368
|
-
void this.
|
|
2416
|
+
void this.singletons.wakeNext(run).catch(() => void 0);
|
|
2369
2417
|
}
|
|
2370
2418
|
return result;
|
|
2371
2419
|
} finally {
|
|
@@ -2378,6 +2426,75 @@ var WorkflowEngine = class {
|
|
|
2378
2426
|
* {@link runExecution}'s settle/suspend; the lease is held by {@link execute}. The result that lands
|
|
2379
2427
|
* us back here (a remote step finished, a timer fired) goes through `resume` like any TS workflow.
|
|
2380
2428
|
*/
|
|
2429
|
+
/**
|
|
2430
|
+
* Apply a terminal/suspended transition once: persist the new run status, emit the matching
|
|
2431
|
+
* lifecycle event, and (on completion/failure) wake a waiting parent. Both the TS and remote
|
|
2432
|
+
* executors funnel their completed/failed/suspended outcomes through here so a status update can
|
|
2433
|
+
* never drift from its event or `notifyParent` call. Does NOT touch the run lock — each executor
|
|
2434
|
+
* keeps its own lease handling (the TS executor's `finally`, the remote path's caller).
|
|
2435
|
+
*/
|
|
2436
|
+
async settleRun(run, outcome) {
|
|
2437
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
2438
|
+
if (outcome.kind === "completed") {
|
|
2439
|
+
await this.store.updateRun(run.id, {
|
|
2440
|
+
status: "completed",
|
|
2441
|
+
output: outcome.output,
|
|
2442
|
+
error: void 0,
|
|
2443
|
+
updatedAt
|
|
2444
|
+
});
|
|
2445
|
+
this.emit({
|
|
2446
|
+
type: "run.completed",
|
|
2447
|
+
runId: run.id,
|
|
2448
|
+
workflow: run.workflow,
|
|
2449
|
+
output: outcome.output
|
|
2450
|
+
});
|
|
2451
|
+
void this.notifyParent(run.id, {
|
|
2452
|
+
ok: true,
|
|
2453
|
+
value: outcome.output
|
|
2454
|
+
});
|
|
2455
|
+
return {
|
|
2456
|
+
runId: run.id,
|
|
2457
|
+
status: "completed",
|
|
2458
|
+
output: outcome.output
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
if (outcome.kind === "failed") {
|
|
2462
|
+
await this.store.updateRun(run.id, {
|
|
2463
|
+
status: "failed",
|
|
2464
|
+
error: outcome.error,
|
|
2465
|
+
updatedAt
|
|
2466
|
+
});
|
|
2467
|
+
this.emit({
|
|
2468
|
+
type: "run.failed",
|
|
2469
|
+
runId: run.id,
|
|
2470
|
+
workflow: run.workflow,
|
|
2471
|
+
error: outcome.error
|
|
2472
|
+
});
|
|
2473
|
+
void this.notifyParent(run.id, {
|
|
2474
|
+
ok: false,
|
|
2475
|
+
error: outcome.error.message
|
|
2476
|
+
});
|
|
2477
|
+
return {
|
|
2478
|
+
runId: run.id,
|
|
2479
|
+
status: "failed",
|
|
2480
|
+
error: outcome.error
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
await this.store.updateRun(run.id, {
|
|
2484
|
+
status: "suspended",
|
|
2485
|
+
wakeAt: outcome.wakeAt,
|
|
2486
|
+
updatedAt
|
|
2487
|
+
});
|
|
2488
|
+
this.emit({
|
|
2489
|
+
type: "run.suspended",
|
|
2490
|
+
runId: run.id,
|
|
2491
|
+
workflow: run.workflow
|
|
2492
|
+
});
|
|
2493
|
+
return {
|
|
2494
|
+
runId: run.id,
|
|
2495
|
+
status: "suspended"
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2381
2498
|
async runRemoteExecution(run, registered) {
|
|
2382
2499
|
const remote = registered.remote;
|
|
2383
2500
|
if (run.status === "pending") {
|
|
@@ -2392,8 +2509,8 @@ var WorkflowEngine = class {
|
|
|
2392
2509
|
workflow: run.workflow
|
|
2393
2510
|
});
|
|
2394
2511
|
}
|
|
2395
|
-
if (registered.singleton && !await this.
|
|
2396
|
-
const wakeAt2 =
|
|
2512
|
+
if (registered.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
2513
|
+
const wakeAt2 = this.singletons.retryWakeAt();
|
|
2397
2514
|
await this.store.updateRun(run.id, {
|
|
2398
2515
|
status: "suspended",
|
|
2399
2516
|
wakeAt: wakeAt2,
|
|
@@ -2438,69 +2555,36 @@ var WorkflowEngine = class {
|
|
|
2438
2555
|
}
|
|
2439
2556
|
if (decision.status === "completed") {
|
|
2440
2557
|
await this.applyCommands(run, decision.commands);
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
output: decision.output,
|
|
2444
|
-
error: void 0,
|
|
2445
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2446
|
-
});
|
|
2447
|
-
this.emit({
|
|
2448
|
-
type: "run.completed",
|
|
2449
|
-
runId: run.id,
|
|
2450
|
-
workflow: run.workflow,
|
|
2558
|
+
return this.settleRun(run, {
|
|
2559
|
+
kind: "completed",
|
|
2451
2560
|
output: decision.output
|
|
2452
2561
|
});
|
|
2453
|
-
void this.notifyParent(run.id, {
|
|
2454
|
-
ok: true,
|
|
2455
|
-
value: decision.output
|
|
2456
|
-
});
|
|
2457
|
-
return {
|
|
2458
|
-
runId: run.id,
|
|
2459
|
-
status: "completed",
|
|
2460
|
-
output: decision.output
|
|
2461
|
-
};
|
|
2462
2562
|
}
|
|
2463
2563
|
if (decision.status === "failed") {
|
|
2464
2564
|
await this.applyCommands(run, decision.commands);
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2565
|
+
return this.settleRun(run, {
|
|
2566
|
+
kind: "failed",
|
|
2567
|
+
error: decision.error ?? {
|
|
2568
|
+
message: "workflow failed"
|
|
2569
|
+
}
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
if (decision.status === "cancelled") {
|
|
2573
|
+
await this.applyCommands(run, decision.commands);
|
|
2468
2574
|
await this.store.updateRun(run.id, {
|
|
2469
|
-
status: "
|
|
2470
|
-
error,
|
|
2575
|
+
status: "cancelled",
|
|
2471
2576
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2472
2577
|
});
|
|
2473
|
-
this.emit({
|
|
2474
|
-
type: "run.failed",
|
|
2475
|
-
runId: run.id,
|
|
2476
|
-
workflow: run.workflow,
|
|
2477
|
-
error
|
|
2478
|
-
});
|
|
2479
|
-
void this.notifyParent(run.id, {
|
|
2480
|
-
ok: false,
|
|
2481
|
-
error: error.message
|
|
2482
|
-
});
|
|
2483
2578
|
return {
|
|
2484
2579
|
runId: run.id,
|
|
2485
|
-
status: "
|
|
2486
|
-
error
|
|
2580
|
+
status: "cancelled"
|
|
2487
2581
|
};
|
|
2488
2582
|
}
|
|
2489
2583
|
const wakeAt = await this.applyCommands(run, decision.commands);
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
wakeAt
|
|
2493
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2584
|
+
return this.settleRun(run, {
|
|
2585
|
+
kind: "suspended",
|
|
2586
|
+
wakeAt
|
|
2494
2587
|
});
|
|
2495
|
-
this.emit({
|
|
2496
|
-
type: "run.suspended",
|
|
2497
|
-
runId: run.id,
|
|
2498
|
-
workflow: run.workflow
|
|
2499
|
-
});
|
|
2500
|
-
return {
|
|
2501
|
-
runId: run.id,
|
|
2502
|
-
status: "suspended"
|
|
2503
|
-
};
|
|
2504
2588
|
}
|
|
2505
2589
|
/** The run's resolved durable ops as replay inputs: completed/failed steps + elapsed timers. */
|
|
2506
2590
|
async remoteHistory(runId) {
|
|
@@ -2555,21 +2639,20 @@ var WorkflowEngine = class {
|
|
|
2555
2639
|
if (cmd.kind === "recordStep") {
|
|
2556
2640
|
const startedAt = cmd.startedAt != null ? new Date(cmd.startedAt) : at;
|
|
2557
2641
|
const finishedAt = cmd.finishedAt != null ? new Date(cmd.finishedAt) : at;
|
|
2558
|
-
await this.store.saveCheckpoint({
|
|
2642
|
+
await this.store.saveCheckpoint(stepCheckpoint({
|
|
2559
2643
|
runId: run.id,
|
|
2560
2644
|
seq: cmd.seq,
|
|
2561
2645
|
name: cmd.name,
|
|
2562
2646
|
kind: "local",
|
|
2563
|
-
stepId: id,
|
|
2564
2647
|
status: cmd.error ? "failed" : "completed",
|
|
2565
2648
|
output: cmd.output,
|
|
2566
2649
|
error: cmd.error,
|
|
2567
|
-
events: cmd.events
|
|
2650
|
+
events: cmd.events,
|
|
2568
2651
|
attempts: 1,
|
|
2569
2652
|
enqueuedAt: startedAt,
|
|
2570
2653
|
startedAt,
|
|
2571
2654
|
finishedAt
|
|
2572
|
-
});
|
|
2655
|
+
}));
|
|
2573
2656
|
this.emit({
|
|
2574
2657
|
type: cmd.error ? "step.failed" : "step.completed",
|
|
2575
2658
|
runId: run.id,
|
|
@@ -2580,12 +2663,11 @@ var WorkflowEngine = class {
|
|
|
2580
2663
|
error: cmd.error
|
|
2581
2664
|
});
|
|
2582
2665
|
} else if (cmd.kind === "call") {
|
|
2583
|
-
await this.store.saveCheckpoint({
|
|
2666
|
+
await this.store.saveCheckpoint(stepCheckpoint({
|
|
2584
2667
|
runId: run.id,
|
|
2585
2668
|
seq: cmd.seq,
|
|
2586
2669
|
name: cmd.name,
|
|
2587
2670
|
kind: "remote",
|
|
2588
|
-
stepId: id,
|
|
2589
2671
|
status: "pending",
|
|
2590
2672
|
input: cmd.input,
|
|
2591
2673
|
attempts: 1,
|
|
@@ -2593,7 +2675,7 @@ var WorkflowEngine = class {
|
|
|
2593
2675
|
enqueuedAt: at,
|
|
2594
2676
|
startedAt: at,
|
|
2595
2677
|
finishedAt: at
|
|
2596
|
-
});
|
|
2678
|
+
}));
|
|
2597
2679
|
await this.pool.dispatch({
|
|
2598
2680
|
runId: run.id,
|
|
2599
2681
|
seq: cmd.seq,
|
|
@@ -2614,19 +2696,18 @@ var WorkflowEngine = class {
|
|
|
2614
2696
|
});
|
|
2615
2697
|
} else if (cmd.kind === "sleep") {
|
|
2616
2698
|
const deadline = this.clock() + cmd.ms;
|
|
2617
|
-
await this.store.saveCheckpoint({
|
|
2699
|
+
await this.store.saveCheckpoint(stepCheckpoint({
|
|
2618
2700
|
runId: run.id,
|
|
2619
2701
|
seq: cmd.seq,
|
|
2620
2702
|
name: `sleep:${cmd.seq}`,
|
|
2621
2703
|
kind: "sleep",
|
|
2622
|
-
stepId: id,
|
|
2623
2704
|
status: "pending",
|
|
2624
2705
|
attempts: 1,
|
|
2625
2706
|
wakeAt: deadline,
|
|
2626
2707
|
enqueuedAt: at,
|
|
2627
2708
|
startedAt: at,
|
|
2628
2709
|
finishedAt: at
|
|
2629
|
-
});
|
|
2710
|
+
}));
|
|
2630
2711
|
wakeAt = wakeAt == null ? deadline : Math.min(wakeAt, deadline);
|
|
2631
2712
|
} else if (cmd.kind === "waitSignal") {
|
|
2632
2713
|
const buffered = await this.store.takeBufferedSignal(cmd.signal);
|
|
@@ -2676,8 +2757,8 @@ var WorkflowEngine = class {
|
|
|
2676
2757
|
workflow: run.workflow
|
|
2677
2758
|
});
|
|
2678
2759
|
}
|
|
2679
|
-
if (registered?.singleton && !await this.
|
|
2680
|
-
const wakeAt =
|
|
2760
|
+
if (registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
2761
|
+
const wakeAt = this.singletons.retryWakeAt();
|
|
2681
2762
|
await this.store.updateRun(run.id, {
|
|
2682
2763
|
status: "suspended",
|
|
2683
2764
|
wakeAt,
|
|
@@ -2701,27 +2782,10 @@ var WorkflowEngine = class {
|
|
|
2701
2782
|
const ctx = createWorkflowCtx(this.ctxHostFor(replay), run.id, compensations, run.workflow);
|
|
2702
2783
|
try {
|
|
2703
2784
|
const output = await fn(ctx, run.input);
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
output,
|
|
2707
|
-
error: void 0,
|
|
2708
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2709
|
-
});
|
|
2710
|
-
this.emit({
|
|
2711
|
-
type: "run.completed",
|
|
2712
|
-
runId: run.id,
|
|
2713
|
-
workflow: run.workflow,
|
|
2785
|
+
return this.settleRun(run, {
|
|
2786
|
+
kind: "completed",
|
|
2714
2787
|
output
|
|
2715
2788
|
});
|
|
2716
|
-
void this.notifyParent(run.id, {
|
|
2717
|
-
ok: true,
|
|
2718
|
-
value: output
|
|
2719
|
-
});
|
|
2720
|
-
return {
|
|
2721
|
-
runId: run.id,
|
|
2722
|
-
status: "completed",
|
|
2723
|
-
output
|
|
2724
|
-
};
|
|
2725
2789
|
} catch (err) {
|
|
2726
2790
|
if (err instanceof ContinueAsNew) {
|
|
2727
2791
|
await this.store.updateRun(run.id, {
|
|
@@ -2773,20 +2837,10 @@ var WorkflowEngine = class {
|
|
|
2773
2837
|
error: error2
|
|
2774
2838
|
};
|
|
2775
2839
|
}
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
wakeAt: err.wakeAt
|
|
2779
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2840
|
+
return this.settleRun(run, {
|
|
2841
|
+
kind: "suspended",
|
|
2842
|
+
wakeAt: err.wakeAt
|
|
2780
2843
|
});
|
|
2781
|
-
this.emit({
|
|
2782
|
-
type: "run.suspended",
|
|
2783
|
-
runId: run.id,
|
|
2784
|
-
workflow: run.workflow
|
|
2785
|
-
});
|
|
2786
|
-
return {
|
|
2787
|
-
runId: run.id,
|
|
2788
|
-
status: "suspended"
|
|
2789
|
-
};
|
|
2790
2844
|
}
|
|
2791
2845
|
const error = {
|
|
2792
2846
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -2797,26 +2851,10 @@ var WorkflowEngine = class {
|
|
|
2797
2851
|
if (!comp) continue;
|
|
2798
2852
|
await this.runCompensation(run, comp);
|
|
2799
2853
|
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
error,
|
|
2803
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2804
|
-
});
|
|
2805
|
-
this.emit({
|
|
2806
|
-
type: "run.failed",
|
|
2807
|
-
runId: run.id,
|
|
2808
|
-
workflow: run.workflow,
|
|
2854
|
+
return this.settleRun(run, {
|
|
2855
|
+
kind: "failed",
|
|
2809
2856
|
error
|
|
2810
2857
|
});
|
|
2811
|
-
void this.notifyParent(run.id, {
|
|
2812
|
-
ok: false,
|
|
2813
|
-
error: error.message
|
|
2814
|
-
});
|
|
2815
|
-
return {
|
|
2816
|
-
runId: run.id,
|
|
2817
|
-
status: "failed",
|
|
2818
|
-
error
|
|
2819
|
-
};
|
|
2820
2858
|
} finally {
|
|
2821
2859
|
await this.store.releaseRunLock(run.id);
|
|
2822
2860
|
}
|