@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 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
- singletonTag(registered.singleton, input)
1694
+ this.singletons.tag(registered.singleton, input)
1577
1695
  ] : opts?.tags);
1578
- const maxQueueDepth = registered.singleton?.maxQueueDepth;
1579
- if (registered.singleton && maxQueueDepth != null) {
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.wakeNextSingletons(run).catch(() => void 0);
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 && step.events.length > 0 ? step.events : void 0,
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 && step.events.length > 0 ? step.events : void 0,
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 && step.events.length > 0 ? step.events : void 0,
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.wakeNextSingletons(run).catch(() => void 0);
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.admitSingleton(run, registered.singleton)) {
2396
- const wakeAt2 = singletonRetryWakeAt(this.clock());
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
- await this.store.updateRun(run.id, {
2442
- status: "completed",
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
- const error = decision.error ?? {
2466
- message: "workflow failed"
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: "failed",
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: "failed",
2486
- error
2580
+ status: "cancelled"
2487
2581
  };
2488
2582
  }
2489
2583
  const wakeAt = await this.applyCommands(run, decision.commands);
2490
- await this.store.updateRun(run.id, {
2491
- status: "suspended",
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 && cmd.events.length > 0 ? cmd.events : void 0,
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.admitSingleton(run, registered.singleton)) {
2680
- const wakeAt = singletonRetryWakeAt(this.clock());
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
- await this.store.updateRun(run.id, {
2705
- status: "completed",
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
- await this.store.updateRun(run.id, {
2777
- status: "suspended",
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
- await this.store.updateRun(run.id, {
2801
- status: "failed",
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
  }