@dudousxd/nestjs-durable-core 0.46.0 → 0.47.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/index.cjs CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  DURABLE_OPTIONS_CANONICAL: () => DURABLE_OPTIONS_CANONICAL,
29
29
  DURABLE_STEP_CONFIG: () => DURABLE_STEP_CONFIG,
30
30
  DURABLE_STEP_NAME: () => DURABLE_STEP_NAME,
31
+ DurableWorkflow: () => DurableWorkflow,
31
32
  Entities: () => Entities,
32
33
  EventAccumulators: () => EventAccumulators,
33
34
  FatalError: () => FatalError,
@@ -54,9 +55,11 @@ __export(index_exports, {
54
55
  applyAttributeQuery: () => applyAttributeQuery,
55
56
  attributeColumnFor: () => attributeColumnFor,
56
57
  attributeOperand: () => attributeOperand,
58
+ bindWorkflowClass: () => bindWorkflowClass,
57
59
  breakpointToken: () => breakpointToken,
58
60
  collectMetrics: () => collectMetrics,
59
61
  createStepLogger: () => createStepLogger,
62
+ currentWorkflowCtx: () => currentWorkflowCtx,
60
63
  eventMatchOf: () => eventMatchOf,
61
64
  eventMatches: () => eventMatches,
62
65
  eventPrefix: () => eventPrefix,
@@ -65,6 +68,7 @@ __export(index_exports, {
65
68
  normalizeAttributeRows: () => normalizeAttributeRows,
66
69
  parseDuration: () => parseDuration,
67
70
  prevCronFireMs: () => prevCronFireMs,
71
+ runInWorkflowCtx: () => runInWorkflowCtx,
68
72
  runSchedules: () => runSchedules,
69
73
  runStepHandler: () => runStepHandler,
70
74
  sanitizeQueueToken: () => sanitizeQueueToken,
@@ -233,6 +237,218 @@ var InMemoryAdmissionBackend = class {
233
237
  }
234
238
  };
235
239
 
240
+ // src/ambient-ctx.ts
241
+ var import_node_async_hooks = require("async_hooks");
242
+ var STORAGE_KEY = Symbol.for("nestjs-durable:ambient-workflow-ctx");
243
+ var globalRef = globalThis;
244
+ if (!globalRef[STORAGE_KEY]) {
245
+ globalRef[STORAGE_KEY] = new import_node_async_hooks.AsyncLocalStorage();
246
+ }
247
+ var storage = globalRef[STORAGE_KEY];
248
+ function runInWorkflowCtx(ctx, fn) {
249
+ return storage.run(ctx, fn);
250
+ }
251
+ __name(runInWorkflowCtx, "runInWorkflowCtx");
252
+ function currentWorkflowCtx() {
253
+ return storage.getStore();
254
+ }
255
+ __name(currentWorkflowCtx, "currentWorkflowCtx");
256
+
257
+ // src/errors.ts
258
+ var FatalError = class extends Error {
259
+ static {
260
+ __name(this, "FatalError");
261
+ }
262
+ code;
263
+ constructor(message, code) {
264
+ super(message);
265
+ this.name = "FatalError";
266
+ this.code = code;
267
+ }
268
+ };
269
+ var SingletonQueueFullError = class extends Error {
270
+ static {
271
+ __name(this, "SingletonQueueFullError");
272
+ }
273
+ workflow;
274
+ key;
275
+ maxQueueDepth;
276
+ constructor(workflow, key, maxQueueDepth) {
277
+ super(`singleton queue for ${workflow} key "${key}" is full (maxQueueDepth=${maxQueueDepth}); retry later`);
278
+ this.name = "SingletonQueueFullError";
279
+ this.workflow = workflow;
280
+ this.key = key;
281
+ this.maxQueueDepth = maxQueueDepth;
282
+ }
283
+ };
284
+ var SignalTimeoutError = class extends Error {
285
+ static {
286
+ __name(this, "SignalTimeoutError");
287
+ }
288
+ token;
289
+ timeoutMs;
290
+ constructor(token, timeoutMs) {
291
+ super(`timed out after ${timeoutMs}ms waiting for signal "${token}"`);
292
+ this.name = "SignalTimeoutError";
293
+ this.token = token;
294
+ this.timeoutMs = timeoutMs;
295
+ }
296
+ };
297
+ var RemoteStepTimeout = class extends Error {
298
+ static {
299
+ __name(this, "RemoteStepTimeout");
300
+ }
301
+ stepId;
302
+ timeoutMs;
303
+ constructor(stepId2, timeoutMs) {
304
+ super(`remote step ${stepId2} produced no result/heartbeat within ${timeoutMs}ms`);
305
+ this.name = "RemoteStepTimeout";
306
+ this.stepId = stepId2;
307
+ this.timeoutMs = timeoutMs;
308
+ }
309
+ };
310
+ var RemoteWorkflowTimeout = class extends Error {
311
+ static {
312
+ __name(this, "RemoteWorkflowTimeout");
313
+ }
314
+ taskId;
315
+ timeoutMs;
316
+ constructor(taskId, timeoutMs) {
317
+ super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
318
+ this.name = "RemoteWorkflowTimeout";
319
+ this.taskId = taskId;
320
+ this.timeoutMs = timeoutMs;
321
+ }
322
+ };
323
+ var NonDeterminismError = class extends Error {
324
+ static {
325
+ __name(this, "NonDeterminismError");
326
+ }
327
+ runId;
328
+ seq;
329
+ constructor(runId, seq, expected, recorded) {
330
+ super(`non-determinism at ${runId}#${seq}: code expects "${expected}" but history recorded "${recorded}". The workflow changed under an in-flight run \u2014 register a new @Workflow version.`);
331
+ this.name = "NonDeterminismError";
332
+ this.runId = runId;
333
+ this.seq = seq;
334
+ }
335
+ };
336
+ var GatherError = class extends Error {
337
+ static {
338
+ __name(this, "GatherError");
339
+ }
340
+ failures;
341
+ constructor(failures, total) {
342
+ const ids = failures.map((f) => f.id).join(", ");
343
+ const denom = total ?? failures.length;
344
+ super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
345
+ this.name = "GatherError";
346
+ this.failures = failures;
347
+ }
348
+ };
349
+ var WorkflowSuspended = class extends Error {
350
+ static {
351
+ __name(this, "WorkflowSuspended");
352
+ }
353
+ /** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
354
+ wakeAt;
355
+ constructor(wakeAt) {
356
+ super("workflow suspended");
357
+ this.name = "WorkflowSuspended";
358
+ this.wakeAt = wakeAt;
359
+ }
360
+ };
361
+ var ContinueAsNew = class extends Error {
362
+ static {
363
+ __name(this, "ContinueAsNew");
364
+ }
365
+ input;
366
+ constructor(input) {
367
+ super("workflow continued as new"), this.input = input;
368
+ this.name = "ContinueAsNew";
369
+ }
370
+ };
371
+
372
+ // src/workflow-ref.ts
373
+ var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
374
+ function workflowName(ref) {
375
+ if (typeof ref === "string") return ref;
376
+ const name = ref[WORKFLOW_NAME_KEY];
377
+ if (!name) {
378
+ throw new Error(`workflow class ${ref.name} has no registered name \u2014 is it decorated with @Workflow({ name })?`);
379
+ }
380
+ return name;
381
+ }
382
+ __name(workflowName, "workflowName");
383
+
384
+ // src/durable-workflow.ts
385
+ var BINDINGS_KEY = Symbol.for("nestjs-durable:workflow-class-bindings");
386
+ var globalRef2 = globalThis;
387
+ if (!globalRef2[BINDINGS_KEY]) {
388
+ globalRef2[BINDINGS_KEY] = /* @__PURE__ */ new WeakMap();
389
+ }
390
+ var bindings = globalRef2[BINDINGS_KEY];
391
+ function bindWorkflowClass(ctor, client) {
392
+ bindings.set(ctor, client);
393
+ }
394
+ __name(bindWorkflowClass, "bindWorkflowClass");
395
+ function clientOf(ctor) {
396
+ const client = bindings.get(ctor);
397
+ if (!client) {
398
+ throw new Error(`${workflowName(ctor)} is not bound to a durable engine \u2014 the class-first statics only work after the module that registers this workflow has booted (or, in a store-less tenant worker, use the injected WorkflowService/engine facade instead).`);
399
+ }
400
+ return client;
401
+ }
402
+ __name(clientOf, "clientOf");
403
+ var DurableWorkflow = class {
404
+ static {
405
+ __name(this, "DurableWorkflow");
406
+ }
407
+ /**
408
+ * Fire-and-forget start — `engine.start` outside a workflow, `ctx.startChild` (parent-linked)
409
+ * inside one. `opts.id` pins the run id (outside) / child id (inside) for idempotent starts.
410
+ */
411
+ static async start(input, opts) {
412
+ const cls = this;
413
+ const ctx = currentWorkflowCtx();
414
+ if (ctx) {
415
+ const runId2 = await ctx.startChild(cls, input, opts?.id);
416
+ return {
417
+ runId: runId2,
418
+ status: "pending"
419
+ };
420
+ }
421
+ const runId = opts?.id ?? globalThis.crypto.randomUUID();
422
+ return clientOf(cls).start(workflowName(cls), input, runId, opts);
423
+ }
424
+ /**
425
+ * Run-and-await the typed output — `ctx.child` inside a workflow (suspends the parent, zero
426
+ * compute); outside, starts the run and waits until it settles terminally. Throws `FatalError`
427
+ * when the run does not complete. `opts.timeoutMs` bounds the OUTSIDE wait only (inside, a
428
+ * suspended parent costs nothing — bound the child itself with `executionTimeout` instead).
429
+ */
430
+ static async execute(input, opts) {
431
+ const cls = this;
432
+ const ctx = currentWorkflowCtx();
433
+ if (ctx) return ctx.child(cls, input, opts?.id);
434
+ const client = clientOf(cls);
435
+ if (!client.waitForRun) {
436
+ throw new FatalError(`${workflowName(cls)}.execute() outside a workflow needs a store-backed engine to await the result \u2014 this class is bound to a store-less start facade (tenant worker). Use ${workflowName(cls)}.start() and observe the run instead.`, "execute_unsupported");
437
+ }
438
+ const runId = opts?.id ?? globalThis.crypto.randomUUID();
439
+ await client.start(workflowName(cls), input, runId, opts);
440
+ const settled = await client.waitForRun(runId, {
441
+ ...opts?.timeoutMs != null ? {
442
+ timeoutMs: opts.timeoutMs
443
+ } : {},
444
+ until: "terminal"
445
+ });
446
+ if (settled.status === "completed") return settled.output;
447
+ const detail = settled.error ? `: ${settled.error.message}` : "";
448
+ throw new FatalError(`${workflowName(cls)} run ${runId} settled ${settled.status}${detail}`, settled.error?.code ?? "execute_failed");
449
+ }
450
+ };
451
+
236
452
  // src/duration.ts
237
453
  var UNIT_MS = {
238
454
  ms: 1,
@@ -489,121 +705,6 @@ function instantCheckpoint(p) {
489
705
  }
490
706
  __name(instantCheckpoint, "instantCheckpoint");
491
707
 
492
- // src/errors.ts
493
- var FatalError = class extends Error {
494
- static {
495
- __name(this, "FatalError");
496
- }
497
- code;
498
- constructor(message, code) {
499
- super(message);
500
- this.name = "FatalError";
501
- this.code = code;
502
- }
503
- };
504
- var SingletonQueueFullError = class extends Error {
505
- static {
506
- __name(this, "SingletonQueueFullError");
507
- }
508
- workflow;
509
- key;
510
- maxQueueDepth;
511
- constructor(workflow, key, maxQueueDepth) {
512
- super(`singleton queue for ${workflow} key "${key}" is full (maxQueueDepth=${maxQueueDepth}); retry later`);
513
- this.name = "SingletonQueueFullError";
514
- this.workflow = workflow;
515
- this.key = key;
516
- this.maxQueueDepth = maxQueueDepth;
517
- }
518
- };
519
- var SignalTimeoutError = class extends Error {
520
- static {
521
- __name(this, "SignalTimeoutError");
522
- }
523
- token;
524
- timeoutMs;
525
- constructor(token, timeoutMs) {
526
- super(`timed out after ${timeoutMs}ms waiting for signal "${token}"`);
527
- this.name = "SignalTimeoutError";
528
- this.token = token;
529
- this.timeoutMs = timeoutMs;
530
- }
531
- };
532
- var RemoteStepTimeout = class extends Error {
533
- static {
534
- __name(this, "RemoteStepTimeout");
535
- }
536
- stepId;
537
- timeoutMs;
538
- constructor(stepId2, timeoutMs) {
539
- super(`remote step ${stepId2} produced no result/heartbeat within ${timeoutMs}ms`);
540
- this.name = "RemoteStepTimeout";
541
- this.stepId = stepId2;
542
- this.timeoutMs = timeoutMs;
543
- }
544
- };
545
- var RemoteWorkflowTimeout = class extends Error {
546
- static {
547
- __name(this, "RemoteWorkflowTimeout");
548
- }
549
- taskId;
550
- timeoutMs;
551
- constructor(taskId, timeoutMs) {
552
- super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
553
- this.name = "RemoteWorkflowTimeout";
554
- this.taskId = taskId;
555
- this.timeoutMs = timeoutMs;
556
- }
557
- };
558
- var NonDeterminismError = class extends Error {
559
- static {
560
- __name(this, "NonDeterminismError");
561
- }
562
- runId;
563
- seq;
564
- constructor(runId, seq, expected, recorded) {
565
- super(`non-determinism at ${runId}#${seq}: code expects "${expected}" but history recorded "${recorded}". The workflow changed under an in-flight run \u2014 register a new @Workflow version.`);
566
- this.name = "NonDeterminismError";
567
- this.runId = runId;
568
- this.seq = seq;
569
- }
570
- };
571
- var GatherError = class extends Error {
572
- static {
573
- __name(this, "GatherError");
574
- }
575
- failures;
576
- constructor(failures, total) {
577
- const ids = failures.map((f) => f.id).join(", ");
578
- const denom = total ?? failures.length;
579
- super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
580
- this.name = "GatherError";
581
- this.failures = failures;
582
- }
583
- };
584
- var WorkflowSuspended = class extends Error {
585
- static {
586
- __name(this, "WorkflowSuspended");
587
- }
588
- /** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
589
- wakeAt;
590
- constructor(wakeAt) {
591
- super("workflow suspended");
592
- this.name = "WorkflowSuspended";
593
- this.wakeAt = wakeAt;
594
- }
595
- };
596
- var ContinueAsNew = class extends Error {
597
- static {
598
- __name(this, "ContinueAsNew");
599
- }
600
- input;
601
- constructor(input) {
602
- super("workflow continued as new"), this.input = input;
603
- this.name = "ContinueAsNew";
604
- }
605
- };
606
-
607
708
  // src/entities.ts
608
709
  var ENTITY_WORKFLOW = "__entity";
609
710
  var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
@@ -1065,18 +1166,6 @@ function stepConfigOf(ref) {
1065
1166
  }
1066
1167
  __name(stepConfigOf, "stepConfigOf");
1067
1168
 
1068
- // src/workflow-ref.ts
1069
- var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
1070
- function workflowName(ref) {
1071
- if (typeof ref === "string") return ref;
1072
- const name = ref[WORKFLOW_NAME_KEY];
1073
- if (!name) {
1074
- throw new Error(`workflow class ${ref.name} has no registered name \u2014 is it decorated with @Workflow({ name })?`);
1075
- }
1076
- return name;
1077
- }
1078
- __name(workflowName, "workflowName");
1079
-
1080
1169
  // src/workflow-ctx.ts
1081
1170
  function normalizeChildOptions(options) {
1082
1171
  return typeof options === "string" ? {
@@ -1262,6 +1351,20 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1262
1351
  value: buffered.payload
1263
1352
  };
1264
1353
  }, "consumeBuffered");
1354
+ const stampDeadline = /* @__PURE__ */ __name(async (deadlineSeq, label, timeoutMs) => {
1355
+ const recorded = await readCheckpoint(deadlineSeq);
1356
+ const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
1357
+ if (!recorded) {
1358
+ await writeCheckpoint(instantCheckpoint({
1359
+ runId,
1360
+ seq: deadlineSeq,
1361
+ name: `timeout:${label}`,
1362
+ kind: "sleep",
1363
+ wakeAt: deadline
1364
+ }));
1365
+ }
1366
+ return deadline;
1367
+ }, "stampDeadline");
1265
1368
  const waitForSignal = /* @__PURE__ */ __name(async (token, opts) => {
1266
1369
  if (opts?.timeoutMs == null) {
1267
1370
  const current = pos.next();
@@ -1279,17 +1382,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1279
1382
  const timeoutMs = opts.timeoutMs;
1280
1383
  const deadlineSeq = pos.next();
1281
1384
  const waitSeq = pos.next();
1282
- const recorded = await readCheckpoint(deadlineSeq);
1283
- const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
1284
- if (!recorded) {
1285
- await writeCheckpoint(instantCheckpoint({
1286
- runId,
1287
- seq: deadlineSeq,
1288
- name: `timeout:${token}`,
1289
- kind: "sleep",
1290
- wakeAt: deadline
1291
- }));
1292
- }
1385
+ const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
1293
1386
  const waited = await readCheckpoint(waitSeq);
1294
1387
  if (waited && waited.status === "completed") return waited.output;
1295
1388
  const buffered = await consumeBuffered(token, waitSeq);
@@ -1322,17 +1415,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1322
1415
  const deadlineSeq = pos.next();
1323
1416
  const waitSeq = pos.next();
1324
1417
  const token = eventToken(name, opts.match, runId, waitSeq);
1325
- const recorded = await readCheckpoint(deadlineSeq);
1326
- const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
1327
- if (!recorded) {
1328
- await writeCheckpoint(instantCheckpoint({
1329
- runId,
1330
- seq: deadlineSeq,
1331
- name: `timeout:event:${name}`,
1332
- kind: "sleep",
1333
- wakeAt: deadline
1334
- }));
1335
- }
1418
+ const deadline = await stampDeadline(deadlineSeq, `event:${name}`, timeoutMs);
1336
1419
  const waited = await readCheckpoint(waitSeq);
1337
1420
  if (waited && waited.status === "completed") return waited.output;
1338
1421
  if (host.clock() >= deadline) {
@@ -1388,6 +1471,9 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1388
1471
  const cp = existing[i];
1389
1472
  const c = cp?.status === "completed" ? cp.output : null;
1390
1473
  if (c && c.ok === false) {
1474
+ for (let j = 0; j < inputs.length; j += 1) {
1475
+ if (j !== i && existing[j]?.status !== "completed") host.cancelChild?.(id(j));
1476
+ }
1391
1477
  throw new GatherError([
1392
1478
  {
1393
1479
  index: i,
@@ -1544,15 +1630,30 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1544
1630
  const webhook = /* @__PURE__ */ __name(() => {
1545
1631
  const current = pos.next();
1546
1632
  const token = `wh:${runId}:${current}`;
1547
- const wait = /* @__PURE__ */ __name(async () => {
1633
+ const wait = /* @__PURE__ */ __name(async (opts) => {
1634
+ const timeoutMs = opts?.timeoutMs;
1635
+ const deadlineSeq = timeoutMs == null ? -1 : pos.next();
1548
1636
  const existing = await readCheckpoint(current);
1549
1637
  if (existing && existing.status === "completed") return existing.output;
1638
+ if (timeoutMs == null) {
1639
+ await store.putSignalWaiter({
1640
+ token,
1641
+ runId,
1642
+ seq: current
1643
+ });
1644
+ throw new WorkflowSuspended();
1645
+ }
1646
+ const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
1647
+ if (host.clock() >= deadline) {
1648
+ await store.takeSignalWaiter(token).catch(() => void 0);
1649
+ throw new SignalTimeoutError(token, timeoutMs);
1650
+ }
1550
1651
  await store.putSignalWaiter({
1551
1652
  token,
1552
1653
  runId,
1553
1654
  seq: current
1554
1655
  });
1555
- throw new WorkflowSuspended();
1656
+ throw new WorkflowSuspended(deadline);
1556
1657
  }, "wait");
1557
1658
  return {
1558
1659
  token,
@@ -1580,6 +1681,18 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1580
1681
  output: attrs
1581
1682
  }));
1582
1683
  }, "upsertSearchAttributes");
1684
+ function resolveCompensateDef(compensate) {
1685
+ const name = typeof compensate === "string" ? compensate : stepNameOf(compensate);
1686
+ if (name === void 0) {
1687
+ throw new Error("ctx.step: compensate handler is not a @Step reference (no stamped step name) \u2014 pass a @Step-decorated method or a step name string");
1688
+ }
1689
+ const config = typeof compensate === "string" ? void 0 : stepConfigOf(compensate);
1690
+ return {
1691
+ name,
1692
+ ...resolveStepPolicy(config, void 0)
1693
+ };
1694
+ }
1695
+ __name(resolveCompensateDef, "resolveCompensateDef");
1583
1696
  function step(handlerOrName, input, opts) {
1584
1697
  const name = typeof handlerOrName === "string" ? handlerOrName : stepNameOf(handlerOrName);
1585
1698
  if (name === void 0) {
@@ -1593,6 +1706,20 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1593
1706
  return host.callRemote(runId, pos.next(), def, input, opts?.queue, opts?.transport, {
1594
1707
  priority: opts?.priority,
1595
1708
  fairnessKey: opts?.fairnessKey
1709
+ }).then((output) => {
1710
+ if (opts?.compensate) {
1711
+ compensations.push({
1712
+ name,
1713
+ dispatch: {
1714
+ def: resolveCompensateDef(opts.compensate),
1715
+ args: {
1716
+ input,
1717
+ output
1718
+ }
1719
+ }
1720
+ });
1721
+ }
1722
+ return output;
1596
1723
  });
1597
1724
  }
1598
1725
  __name(step, "step");
@@ -2085,7 +2212,7 @@ var WorkflowEngine = class {
2085
2212
  workflow: name,
2086
2213
  workflowVersion: "1",
2087
2214
  status: "pending",
2088
- // Stamp the namespace the REAL run will carry (:893) so convention routing resolves the same
2215
+ // Stamp the namespace the REAL run will carry (below) so convention routing resolves the same
2089
2216
  // tenant-suffixed group the run will dispatch to — an operator starting a tenant's run via the
2090
2217
  // `onStartRun` wire (opts.namespace = tenant) must find the live `<workflow>@<tenant>` group,
2091
2218
  // not the bare group. Without this the start-run entry path can't reach a suffixed-only worker.
@@ -2486,7 +2613,13 @@ var WorkflowEngine = class {
2486
2613
  * `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
2487
2614
  */
2488
2615
  waitForRun(runId, opts) {
2489
- const isSettled = /* @__PURE__ */ __name((run) => run.status !== "pending" && run.status !== "running" && run.status !== "cancelling" && !(run.status === "suspended" && run.awaitingDecisionTaskId !== void 0), "isSettled");
2616
+ const TERMINAL = /* @__PURE__ */ new Set([
2617
+ "completed",
2618
+ "failed",
2619
+ "cancelled",
2620
+ "dead"
2621
+ ]);
2622
+ const isSettled = /* @__PURE__ */ __name((run) => opts?.until === "terminal" ? TERMINAL.has(run.status) : run.status !== "pending" && run.status !== "running" && run.status !== "cancelling" && !(run.status === "suspended" && run.awaitingDecisionTaskId !== void 0), "isSettled");
2490
2623
  const toResult = /* @__PURE__ */ __name((run) => ({
2491
2624
  runId,
2492
2625
  status: run.status,
@@ -2500,6 +2633,7 @@ var WorkflowEngine = class {
2500
2633
  const timer = opts?.timeoutMs != null ? setTimeout(() => {
2501
2634
  if (done) return;
2502
2635
  done = true;
2636
+ if (poller) clearInterval(poller);
2503
2637
  off();
2504
2638
  reject(new Error(`waitForRun(${runId}) timed out after ${opts.timeoutMs}ms`));
2505
2639
  }, opts.timeoutMs) : void 0;
@@ -2507,6 +2641,7 @@ var WorkflowEngine = class {
2507
2641
  if (done) return;
2508
2642
  done = true;
2509
2643
  if (timer) clearTimeout(timer);
2644
+ if (poller) clearInterval(poller);
2510
2645
  off();
2511
2646
  resolve(toResult(run));
2512
2647
  }, "finish");
@@ -2515,6 +2650,11 @@ var WorkflowEngine = class {
2515
2650
  if (run && isSettled(run)) finish(run);
2516
2651
  });
2517
2652
  }, "check");
2653
+ const poller = opts?.until === "terminal" ? (() => {
2654
+ const interval = setInterval(check, 250);
2655
+ interval.unref?.();
2656
+ return interval;
2657
+ })() : void 0;
2518
2658
  off = this.subscribe((ev) => {
2519
2659
  if (ev.runId === runId && (ev.type === "run.completed" || ev.type === "run.failed" || ev.type === "run.suspended")) {
2520
2660
  check();
@@ -3424,7 +3564,9 @@ var WorkflowEngine = class {
3424
3564
  parallelGroup: cmd.parallelGroup
3425
3565
  });
3426
3566
  if (!await this.store.getRun(childId)) {
3427
- queueMicrotask(() => void this.start(cmd.workflow, cmd.input, childId).catch(() => void 0));
3567
+ queueMicrotask(() => void this.start(cmd.workflow, cmd.input, childId, {
3568
+ namespace: run.namespace
3569
+ }).catch(() => void 0));
3428
3570
  }
3429
3571
  } else {
3430
3572
  throw new Error(`remote workflow command '${cmd.kind}' is not supported yet`);
@@ -3436,14 +3578,24 @@ var WorkflowEngine = class {
3436
3578
  * Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
3437
3579
  * exit path of a run with a compensating cancel in flight — whether the resumed replay returned,
3438
3580
  * suspended, hit continue-as-new, or threw — so a cancel is never lost just because the body didn't
3439
- * stop at a suspension point. Clears the in-memory cancel flag.
3581
+ * stop at a suspension point.
3582
+ *
3583
+ * A dispatched compensation still in flight (or backing off a retry) needs the run to keep waiting:
3584
+ * unlike the failure path (which is fine going `suspended` — a resumable state no different from an
3585
+ * ordinary in-flight `ctx.step`), THIS run must stay `cancelling` — `engine.cancel`'s idempotent
3586
+ * re-check, `waitForRun`'s non-terminal treatment, and crash recovery (`recoverIncomplete` only
3587
+ * re-drives `running`/`cancelling` runs — a `cancelling` one with a free lease, whatever it's
3588
+ * internally waiting on) all key off that status. So on a pending unwind this returns WITHOUT
3589
+ * touching `status` (already `cancelling`, untouched since `engine.cancel` set it) — the in-memory
3590
+ * cancel flag stays too, re-derived from the status on any later re-drive regardless.
3440
3591
  */
3441
- async compensateAndCancel(run, compensations) {
3592
+ async compensateAndCancel(run, compensations, replay) {
3593
+ const pending = await this.unwindCompensations(run, compensations, replay);
3594
+ if (pending) return {
3595
+ runId: run.id,
3596
+ status: "cancelling"
3597
+ };
3442
3598
  this.cancelRequested.delete(run.id);
3443
- for (let i = compensations.length - 1; i >= 0; i -= 1) {
3444
- const comp = compensations[i];
3445
- if (comp) await this.runCompensation(run, comp);
3446
- }
3447
3599
  const error = {
3448
3600
  message: "cancelled"
3449
3601
  };
@@ -3504,17 +3656,21 @@ var WorkflowEngine = class {
3504
3656
  const snapshot = await this.store.listCheckpoints(run.id);
3505
3657
  const replay = /* @__PURE__ */ new Map();
3506
3658
  for (const cp of snapshot) replay.set(cp.seq, cp);
3507
- const ctx = createWorkflowCtx(this.ctxHostFor(replay), run.id, compensations, run.workflow);
3659
+ const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow);
3508
3660
  try {
3509
- const output = await fn(ctx, run.input);
3510
- if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
3661
+ const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
3662
+ if (this.cancelRequested.has(run.id)) {
3663
+ return this.compensateAndCancel(run, compensations, replay);
3664
+ }
3511
3665
  return this.settleRun(run, {
3512
3666
  kind: "completed",
3513
3667
  output
3514
3668
  });
3515
3669
  } catch (err) {
3516
3670
  if (err instanceof ContinueAsNew) {
3517
- if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
3671
+ if (this.cancelRequested.has(run.id)) {
3672
+ return this.compensateAndCancel(run, compensations, replay);
3673
+ }
3518
3674
  await this.store.updateRun(run.id, {
3519
3675
  status: "completed",
3520
3676
  output: void 0,
@@ -3539,22 +3695,26 @@ var WorkflowEngine = class {
3539
3695
  };
3540
3696
  }
3541
3697
  if (err instanceof WorkflowSuspended) {
3542
- if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
3698
+ if (this.cancelRequested.has(run.id)) {
3699
+ return this.compensateAndCancel(run, compensations, replay);
3700
+ }
3543
3701
  return this.settleRun(run, {
3544
3702
  kind: "suspended",
3545
3703
  wakeAt: err.wakeAt
3546
3704
  });
3547
3705
  }
3548
- if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
3706
+ if (this.cancelRequested.has(run.id)) {
3707
+ return this.compensateAndCancel(run, compensations, replay);
3708
+ }
3549
3709
  const error = {
3550
3710
  message: err instanceof Error ? err.message : String(err),
3551
3711
  stack: err instanceof Error ? err.stack : void 0
3552
3712
  };
3553
- for (let i = compensations.length - 1; i >= 0; i -= 1) {
3554
- const comp = compensations[i];
3555
- if (!comp) continue;
3556
- await this.runCompensation(run, comp);
3557
- }
3713
+ const pending = await this.unwindCompensations(run, compensations, replay);
3714
+ if (pending) return this.settleRun(run, {
3715
+ kind: "suspended",
3716
+ wakeAt: pending.wakeAt
3717
+ });
3558
3718
  return this.settleRun(run, {
3559
3719
  kind: "failed",
3560
3720
  error
@@ -3564,14 +3724,62 @@ var WorkflowEngine = class {
3564
3724
  }
3565
3725
  }
3566
3726
  /**
3567
- * Run one saga compensation, retried up to `compensationRetries`, emitting a `compensate:<step>`
3568
- * step event for its outcome so a stranded undo is visible. Never throws — a permanently-failing
3569
- * compensation is skipped so it can't mask the original failure.
3727
+ * Undo every registered saga compensation in strict reverse order — the shared unwind both the
3728
+ * run-failure path (`runExecution`'s catch) and the compensating-cancel path
3729
+ * (`compensateAndCancel`) drive. Every entry, LOCAL or DISPATCHED, gets a NEGATIVE checkpoint seq
3730
+ * `-(k + 1)` for its position `k` (0, 1, 2… walking the stack in reverse) — reserved space that can
3731
+ * never collide with a body position (those start at 0), so a saga's undo trail composes onto the
3732
+ * run's ordinary checkpoint history with no store-schema change, and shows up in run detail exactly
3733
+ * like any other step. Deterministic because `compensations` is rebuilt identically on every replay
3734
+ * (see `workflow-ctx.ts`), so the same comp always lands on the same negative seq across a crash +
3735
+ * re-drive — the crash-safety this whole scheme rests on.
3736
+ *
3737
+ * A LOCAL entry runs in-process (`runCompensation`), which checkpoints its own outcome — completed,
3738
+ * or failed once `compensationRetries` are exhausted — so a crash mid-unwind SKIPS an
3739
+ * already-resolved one on the next pass instead of re-running it (and re-spending its retry budget)
3740
+ * from attempt 1. A DISPATCHED entry is an ordinary remote-step dispatch under the hood
3741
+ * (`dispatchCompensation`): when it needs the run to WAIT — freshly dispatched, still in flight, or
3742
+ * backing off a durable retry — this function returns that wait signal immediately (skipping every
3743
+ * comp after it in this pass); the CALLER decides how to represent "still unwinding" (see
3744
+ * `compensateAndCancel`'s doc comment for why that differs between the two callers). Either way, the
3745
+ * run resumes later exactly like any other dispatched step: `completeRemoteResult` completes the
3746
+ * negative-seq checkpoint and re-drives, the body replays and deterministically re-throws the same
3747
+ * exit it took before (the original failure, or the suspension `compensateAndCancel` resumed from),
3748
+ * this function runs again, the now-resolved comp is skipped, and the unwind continues from there.
3749
+ *
3750
+ * Returns `undefined` once every compensation has resolved — the caller then finalizes the run
3751
+ * (`failed` with the ORIGINAL body error, never masked by a comp outcome; or `cancelled`).
3752
+ */
3753
+ async unwindCompensations(run, compensations, replay) {
3754
+ for (let i = compensations.length - 1; i >= 0; i -= 1) {
3755
+ const comp = compensations[i];
3756
+ if (!comp) continue;
3757
+ const seq = -(compensations.length - i);
3758
+ if ("fn" in comp) {
3759
+ const existing = replay?.get(seq) ?? await this.store.getCheckpoint(run.id, seq);
3760
+ if (existing) continue;
3761
+ await this.runCompensation(run, comp, seq, replay);
3762
+ continue;
3763
+ }
3764
+ const pending = await this.dispatchCompensation(run, comp, seq, replay);
3765
+ if (pending) return pending;
3766
+ }
3767
+ return void 0;
3768
+ }
3769
+ /**
3770
+ * Run one LOCAL saga compensation, retried up to `compensationRetries`, emitting a
3771
+ * `compensate:<step>` step event for its outcome so a stranded undo is visible. Never throws — a
3772
+ * permanently-failing compensation is skipped so it can't mask the original failure. Checkpoints
3773
+ * its outcome at `seq` (a negative unwind position reserved by {@link unwindCompensations}) so a
3774
+ * crash mid-unwind skips an already-resolved undo on a later pass instead of re-running it (and
3775
+ * re-spending its retry budget) from attempt 1 — the checkpoint is durability/visibility only; the
3776
+ * runtime retry-in-process-back-to-back semantics of `comp.fn()` are unchanged.
3570
3777
  */
3571
- async runCompensation(run, comp) {
3778
+ async runCompensation(run, comp, seq, replay) {
3572
3779
  const name = `compensate:${comp.name}`;
3780
+ const enqueuedAt = /* @__PURE__ */ new Date();
3573
3781
  for (let attempt = 1; attempt <= this.compensationRetries; attempt += 1) {
3574
- const startedAt = Date.now();
3782
+ const startedAt = /* @__PURE__ */ new Date();
3575
3783
  try {
3576
3784
  await comp.fn();
3577
3785
  this.emit({
@@ -3580,30 +3788,147 @@ var WorkflowEngine = class {
3580
3788
  workflow: run.workflow,
3581
3789
  name,
3582
3790
  kind: "local",
3583
- durationMs: Date.now() - startedAt
3791
+ durationMs: Date.now() - startedAt.getTime()
3584
3792
  });
3793
+ const cp = stepCheckpoint({
3794
+ runId: run.id,
3795
+ seq,
3796
+ name,
3797
+ kind: "local",
3798
+ status: "completed",
3799
+ attempts: attempt,
3800
+ enqueuedAt,
3801
+ startedAt,
3802
+ finishedAt: /* @__PURE__ */ new Date()
3803
+ });
3804
+ await this.store.saveCheckpoint(cp);
3805
+ replay?.set(seq, cp);
3585
3806
  return;
3586
3807
  } catch (err) {
3587
3808
  if (attempt >= this.compensationRetries) {
3809
+ const error = {
3810
+ message: err instanceof Error ? err.message : String(err)
3811
+ };
3588
3812
  this.emit({
3589
3813
  type: "step.failed",
3590
3814
  runId: run.id,
3591
3815
  workflow: run.workflow,
3592
3816
  name,
3593
3817
  kind: "local",
3594
- error: {
3595
- message: err instanceof Error ? err.message : String(err)
3596
- },
3597
- durationMs: Date.now() - startedAt
3818
+ error,
3819
+ durationMs: Date.now() - startedAt.getTime()
3598
3820
  });
3821
+ const cp = stepCheckpoint({
3822
+ runId: run.id,
3823
+ seq,
3824
+ name,
3825
+ kind: "local",
3826
+ status: "failed",
3827
+ attempts: attempt,
3828
+ error,
3829
+ enqueuedAt,
3830
+ startedAt,
3831
+ finishedAt: /* @__PURE__ */ new Date()
3832
+ });
3833
+ await this.store.saveCheckpoint(cp);
3834
+ replay?.set(seq, cp);
3599
3835
  }
3600
3836
  }
3601
3837
  }
3602
3838
  }
3839
+ /**
3840
+ * Resolve (or advance) ONE dispatched saga compensation at its reserved negative `seq` — the undo
3841
+ * handler is an ordinary remote `@Step`, so this mirrors `callRemote`'s pending/failed/absent
3842
+ * branches (engine.ts's `callRemote`), minus the queue/admission and in-memory-`timeoutMs` paths
3843
+ * (V1: dispatch direct by name, always durable). Returns the wait signal when the run must pause for
3844
+ * this compensation (freshly dispatched, still in flight, or backing off before a durable retry);
3845
+ * `undefined` once it's resolved — completed (crash-safe skip), or retries exhausted/non-retryable
3846
+ * (already visible: `completeRemoteResult` emitted `step.failed` for `compensate:<name>` when that
3847
+ * checkpoint landed — never re-emitted here, and never allowed to block the rest of the unwind or
3848
+ * mask the run's original failure).
3849
+ */
3850
+ async dispatchCompensation(run, comp, seq, replay) {
3851
+ const { def, args } = comp.dispatch;
3852
+ const name = `compensate:${comp.name}`;
3853
+ const existing = replay?.get(seq) ?? await this.store.getCheckpoint(run.id, seq);
3854
+ if (existing?.status === "completed") return void 0;
3855
+ if (existing?.status === "pending") return {};
3856
+ if (existing?.status === "failed") {
3857
+ const maxAttempts = Math.max(1, def.retries ?? 1);
3858
+ const retryable = existing.error?.retryable !== false;
3859
+ if (!retryable || existing.attempts >= maxAttempts) return void 0;
3860
+ if (existing.wakeAt == null) {
3861
+ const wakeAt = this.clock() + backoffDelay(existing.attempts, def);
3862
+ const withWakeAt = {
3863
+ ...existing,
3864
+ wakeAt
3865
+ };
3866
+ await this.store.saveCheckpoint(withWakeAt);
3867
+ replay?.set(seq, withWakeAt);
3868
+ return {
3869
+ wakeAt
3870
+ };
3871
+ }
3872
+ if (this.clock() < existing.wakeAt) return {
3873
+ wakeAt: existing.wakeAt
3874
+ };
3875
+ await this.dispatchCompensationAttempt(run, seq, name, def, args, existing.attempts + 1, replay);
3876
+ return {};
3877
+ }
3878
+ await this.dispatchCompensationAttempt(run, seq, name, def, args, 1, replay);
3879
+ return {};
3880
+ }
3881
+ /**
3882
+ * Dispatch (or re-dispatch, on a durable retry) one compensation's undo handler — mirrors
3883
+ * `callRemote`'s dispatch block (persist the `pending` checkpoint BEFORE handing off to the
3884
+ * transport, so a fast result always finds it to complete). Two DIFFERENT names are in play: the
3885
+ * checkpoint's `name` is `compensate:<originalStepName>` (what makes this undo show up on the run's
3886
+ * timeline, and what `completeRemoteResult` emits `step.completed`/`step.failed` under); the WIRE
3887
+ * task's `name` is the undo handler's OWN routing name (`def.name`) — a worker dispatches to the
3888
+ * `@Step` it registered, not to this saga-visibility label.
3889
+ */
3890
+ async dispatchCompensationAttempt(run, seq, name, def, args, attempt, replay) {
3891
+ if (this.pool.size === 0) throw new Error("remote steps require a Transport");
3892
+ const group = tenantGroup(sanitizeQueueToken(def.name), def.partition);
3893
+ const enqueuedAt = /* @__PURE__ */ new Date();
3894
+ const cp = stepCheckpoint({
3895
+ runId: run.id,
3896
+ seq,
3897
+ name,
3898
+ kind: "remote",
3899
+ status: "pending",
3900
+ input: args,
3901
+ attempts: attempt,
3902
+ workerGroup: group,
3903
+ enqueuedAt,
3904
+ startedAt: enqueuedAt,
3905
+ finishedAt: enqueuedAt
3906
+ });
3907
+ await this.store.saveCheckpoint(cp);
3908
+ replay?.set(seq, cp);
3909
+ await this.pool.dispatch({
3910
+ runId: run.id,
3911
+ seq,
3912
+ name: def.name,
3913
+ stepId: cp.stepId,
3914
+ group,
3915
+ input: args,
3916
+ traceparent: this.traceparent?.(),
3917
+ context: this.context?.(),
3918
+ attempt
3919
+ });
3920
+ this.emit({
3921
+ type: "step.started",
3922
+ runId: run.id,
3923
+ seq,
3924
+ name,
3925
+ kind: "remote"
3926
+ });
3927
+ }
3603
3928
  /** The seam handed to {@link createWorkflowCtx}: the authoring API reaches durability + lifecycle
3604
3929
  * (checkpointing, dispatch, child start) through this, so the ctx primitives live in their own
3605
3930
  * module and the engine stays the orchestrator. */
3606
- ctxHostFor(replay) {
3931
+ ctxHostFor(replay, parentNamespace) {
3607
3932
  return {
3608
3933
  store: this.store,
3609
3934
  replay,
@@ -3613,12 +3938,24 @@ var WorkflowEngine = class {
3613
3938
  completeStep: /* @__PURE__ */ __name((s) => this.completeStep(s), "completeStep"),
3614
3939
  failStep: /* @__PURE__ */ __name((s) => this.failStep(s), "failStep"),
3615
3940
  callRemote: /* @__PURE__ */ __name((runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission), "callRemote"),
3616
- // Defer so a fast child can't reentrantly resume a still-running parent.
3941
+ // Defer so a fast child can't reentrantly resume a still-running parent. Stamp the child with
3942
+ // the PARENT run's namespace (not this engine's) so a child belongs to the tenant/partition of
3943
+ // the run it was spawned from — even when an operator (namespace: undefined, drives every
3944
+ // namespace) is the one executing the parent. Otherwise an operator recovery-resuming a
3945
+ // `davi-local` pipeline would stamp its `processing` child `default` and it would leak off the
3946
+ // tenant's worker pool onto the shared/dev workers.
3617
3947
  startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
3618
3948
  queueMicrotask(() => void this.start(workflow, input, id, {
3619
- priority
3949
+ priority,
3950
+ namespace: parentNamespace
3620
3951
  }).catch(() => void 0));
3621
3952
  }, "startChild"),
3953
+ // Deferred for the same reentrancy reason as `startChild` above. `cancel()` is already
3954
+ // idempotent on a terminal/cancelled run (returns its existing status without side effects), so
3955
+ // no extra guard is needed here for the failFast replay case (re-issuing the same cancel calls).
3956
+ cancelChild: /* @__PURE__ */ __name((childId) => {
3957
+ queueMicrotask(() => void this.cancel(childId).catch(() => void 0));
3958
+ }, "cancelChild"),
3622
3959
  // Shallow-merge into the run's searchAttributes (the ctx primitive makes this exactly-once).
3623
3960
  upsertSearchAttributes: /* @__PURE__ */ __name(async (runId, attrs) => {
3624
3961
  const run = await this.store.getRun(runId);
@@ -4742,6 +5079,7 @@ var InMemoryTransport = class {
4742
5079
  DURABLE_OPTIONS_CANONICAL,
4743
5080
  DURABLE_STEP_CONFIG,
4744
5081
  DURABLE_STEP_NAME,
5082
+ DurableWorkflow,
4745
5083
  Entities,
4746
5084
  EventAccumulators,
4747
5085
  FatalError,
@@ -4768,9 +5106,11 @@ var InMemoryTransport = class {
4768
5106
  applyAttributeQuery,
4769
5107
  attributeColumnFor,
4770
5108
  attributeOperand,
5109
+ bindWorkflowClass,
4771
5110
  breakpointToken,
4772
5111
  collectMetrics,
4773
5112
  createStepLogger,
5113
+ currentWorkflowCtx,
4774
5114
  eventMatchOf,
4775
5115
  eventMatches,
4776
5116
  eventPrefix,
@@ -4779,6 +5119,7 @@ var InMemoryTransport = class {
4779
5119
  normalizeAttributeRows,
4780
5120
  parseDuration,
4781
5121
  prevCronFireMs,
5122
+ runInWorkflowCtx,
4782
5123
  runSchedules,
4783
5124
  runStepHandler,
4784
5125
  sanitizeQueueToken,