@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.js CHANGED
@@ -156,6 +156,218 @@ var InMemoryAdmissionBackend = class {
156
156
  }
157
157
  };
158
158
 
159
+ // src/ambient-ctx.ts
160
+ import { AsyncLocalStorage } from "node:async_hooks";
161
+ var STORAGE_KEY = Symbol.for("nestjs-durable:ambient-workflow-ctx");
162
+ var globalRef = globalThis;
163
+ if (!globalRef[STORAGE_KEY]) {
164
+ globalRef[STORAGE_KEY] = new AsyncLocalStorage();
165
+ }
166
+ var storage = globalRef[STORAGE_KEY];
167
+ function runInWorkflowCtx(ctx, fn) {
168
+ return storage.run(ctx, fn);
169
+ }
170
+ __name(runInWorkflowCtx, "runInWorkflowCtx");
171
+ function currentWorkflowCtx() {
172
+ return storage.getStore();
173
+ }
174
+ __name(currentWorkflowCtx, "currentWorkflowCtx");
175
+
176
+ // src/errors.ts
177
+ var FatalError = class extends Error {
178
+ static {
179
+ __name(this, "FatalError");
180
+ }
181
+ code;
182
+ constructor(message, code) {
183
+ super(message);
184
+ this.name = "FatalError";
185
+ this.code = code;
186
+ }
187
+ };
188
+ var SingletonQueueFullError = class extends Error {
189
+ static {
190
+ __name(this, "SingletonQueueFullError");
191
+ }
192
+ workflow;
193
+ key;
194
+ maxQueueDepth;
195
+ constructor(workflow, key, maxQueueDepth) {
196
+ super(`singleton queue for ${workflow} key "${key}" is full (maxQueueDepth=${maxQueueDepth}); retry later`);
197
+ this.name = "SingletonQueueFullError";
198
+ this.workflow = workflow;
199
+ this.key = key;
200
+ this.maxQueueDepth = maxQueueDepth;
201
+ }
202
+ };
203
+ var SignalTimeoutError = class extends Error {
204
+ static {
205
+ __name(this, "SignalTimeoutError");
206
+ }
207
+ token;
208
+ timeoutMs;
209
+ constructor(token, timeoutMs) {
210
+ super(`timed out after ${timeoutMs}ms waiting for signal "${token}"`);
211
+ this.name = "SignalTimeoutError";
212
+ this.token = token;
213
+ this.timeoutMs = timeoutMs;
214
+ }
215
+ };
216
+ var RemoteStepTimeout = class extends Error {
217
+ static {
218
+ __name(this, "RemoteStepTimeout");
219
+ }
220
+ stepId;
221
+ timeoutMs;
222
+ constructor(stepId2, timeoutMs) {
223
+ super(`remote step ${stepId2} produced no result/heartbeat within ${timeoutMs}ms`);
224
+ this.name = "RemoteStepTimeout";
225
+ this.stepId = stepId2;
226
+ this.timeoutMs = timeoutMs;
227
+ }
228
+ };
229
+ var RemoteWorkflowTimeout = class extends Error {
230
+ static {
231
+ __name(this, "RemoteWorkflowTimeout");
232
+ }
233
+ taskId;
234
+ timeoutMs;
235
+ constructor(taskId, timeoutMs) {
236
+ super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
237
+ this.name = "RemoteWorkflowTimeout";
238
+ this.taskId = taskId;
239
+ this.timeoutMs = timeoutMs;
240
+ }
241
+ };
242
+ var NonDeterminismError = class extends Error {
243
+ static {
244
+ __name(this, "NonDeterminismError");
245
+ }
246
+ runId;
247
+ seq;
248
+ constructor(runId, seq, expected, recorded) {
249
+ 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.`);
250
+ this.name = "NonDeterminismError";
251
+ this.runId = runId;
252
+ this.seq = seq;
253
+ }
254
+ };
255
+ var GatherError = class extends Error {
256
+ static {
257
+ __name(this, "GatherError");
258
+ }
259
+ failures;
260
+ constructor(failures, total) {
261
+ const ids = failures.map((f) => f.id).join(", ");
262
+ const denom = total ?? failures.length;
263
+ super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
264
+ this.name = "GatherError";
265
+ this.failures = failures;
266
+ }
267
+ };
268
+ var WorkflowSuspended = class extends Error {
269
+ static {
270
+ __name(this, "WorkflowSuspended");
271
+ }
272
+ /** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
273
+ wakeAt;
274
+ constructor(wakeAt) {
275
+ super("workflow suspended");
276
+ this.name = "WorkflowSuspended";
277
+ this.wakeAt = wakeAt;
278
+ }
279
+ };
280
+ var ContinueAsNew = class extends Error {
281
+ static {
282
+ __name(this, "ContinueAsNew");
283
+ }
284
+ input;
285
+ constructor(input) {
286
+ super("workflow continued as new"), this.input = input;
287
+ this.name = "ContinueAsNew";
288
+ }
289
+ };
290
+
291
+ // src/workflow-ref.ts
292
+ var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
293
+ function workflowName(ref) {
294
+ if (typeof ref === "string") return ref;
295
+ const name = ref[WORKFLOW_NAME_KEY];
296
+ if (!name) {
297
+ throw new Error(`workflow class ${ref.name} has no registered name \u2014 is it decorated with @Workflow({ name })?`);
298
+ }
299
+ return name;
300
+ }
301
+ __name(workflowName, "workflowName");
302
+
303
+ // src/durable-workflow.ts
304
+ var BINDINGS_KEY = Symbol.for("nestjs-durable:workflow-class-bindings");
305
+ var globalRef2 = globalThis;
306
+ if (!globalRef2[BINDINGS_KEY]) {
307
+ globalRef2[BINDINGS_KEY] = /* @__PURE__ */ new WeakMap();
308
+ }
309
+ var bindings = globalRef2[BINDINGS_KEY];
310
+ function bindWorkflowClass(ctor, client) {
311
+ bindings.set(ctor, client);
312
+ }
313
+ __name(bindWorkflowClass, "bindWorkflowClass");
314
+ function clientOf(ctor) {
315
+ const client = bindings.get(ctor);
316
+ if (!client) {
317
+ 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).`);
318
+ }
319
+ return client;
320
+ }
321
+ __name(clientOf, "clientOf");
322
+ var DurableWorkflow = class {
323
+ static {
324
+ __name(this, "DurableWorkflow");
325
+ }
326
+ /**
327
+ * Fire-and-forget start — `engine.start` outside a workflow, `ctx.startChild` (parent-linked)
328
+ * inside one. `opts.id` pins the run id (outside) / child id (inside) for idempotent starts.
329
+ */
330
+ static async start(input, opts) {
331
+ const cls = this;
332
+ const ctx = currentWorkflowCtx();
333
+ if (ctx) {
334
+ const runId2 = await ctx.startChild(cls, input, opts?.id);
335
+ return {
336
+ runId: runId2,
337
+ status: "pending"
338
+ };
339
+ }
340
+ const runId = opts?.id ?? globalThis.crypto.randomUUID();
341
+ return clientOf(cls).start(workflowName(cls), input, runId, opts);
342
+ }
343
+ /**
344
+ * Run-and-await the typed output — `ctx.child` inside a workflow (suspends the parent, zero
345
+ * compute); outside, starts the run and waits until it settles terminally. Throws `FatalError`
346
+ * when the run does not complete. `opts.timeoutMs` bounds the OUTSIDE wait only (inside, a
347
+ * suspended parent costs nothing — bound the child itself with `executionTimeout` instead).
348
+ */
349
+ static async execute(input, opts) {
350
+ const cls = this;
351
+ const ctx = currentWorkflowCtx();
352
+ if (ctx) return ctx.child(cls, input, opts?.id);
353
+ const client = clientOf(cls);
354
+ if (!client.waitForRun) {
355
+ 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");
356
+ }
357
+ const runId = opts?.id ?? globalThis.crypto.randomUUID();
358
+ await client.start(workflowName(cls), input, runId, opts);
359
+ const settled = await client.waitForRun(runId, {
360
+ ...opts?.timeoutMs != null ? {
361
+ timeoutMs: opts.timeoutMs
362
+ } : {},
363
+ until: "terminal"
364
+ });
365
+ if (settled.status === "completed") return settled.output;
366
+ const detail = settled.error ? `: ${settled.error.message}` : "";
367
+ throw new FatalError(`${workflowName(cls)} run ${runId} settled ${settled.status}${detail}`, settled.error?.code ?? "execute_failed");
368
+ }
369
+ };
370
+
159
371
  // src/duration.ts
160
372
  var UNIT_MS = {
161
373
  ms: 1,
@@ -412,121 +624,6 @@ function instantCheckpoint(p) {
412
624
  }
413
625
  __name(instantCheckpoint, "instantCheckpoint");
414
626
 
415
- // src/errors.ts
416
- var FatalError = class extends Error {
417
- static {
418
- __name(this, "FatalError");
419
- }
420
- code;
421
- constructor(message, code) {
422
- super(message);
423
- this.name = "FatalError";
424
- this.code = code;
425
- }
426
- };
427
- var SingletonQueueFullError = class extends Error {
428
- static {
429
- __name(this, "SingletonQueueFullError");
430
- }
431
- workflow;
432
- key;
433
- maxQueueDepth;
434
- constructor(workflow, key, maxQueueDepth) {
435
- super(`singleton queue for ${workflow} key "${key}" is full (maxQueueDepth=${maxQueueDepth}); retry later`);
436
- this.name = "SingletonQueueFullError";
437
- this.workflow = workflow;
438
- this.key = key;
439
- this.maxQueueDepth = maxQueueDepth;
440
- }
441
- };
442
- var SignalTimeoutError = class extends Error {
443
- static {
444
- __name(this, "SignalTimeoutError");
445
- }
446
- token;
447
- timeoutMs;
448
- constructor(token, timeoutMs) {
449
- super(`timed out after ${timeoutMs}ms waiting for signal "${token}"`);
450
- this.name = "SignalTimeoutError";
451
- this.token = token;
452
- this.timeoutMs = timeoutMs;
453
- }
454
- };
455
- var RemoteStepTimeout = class extends Error {
456
- static {
457
- __name(this, "RemoteStepTimeout");
458
- }
459
- stepId;
460
- timeoutMs;
461
- constructor(stepId2, timeoutMs) {
462
- super(`remote step ${stepId2} produced no result/heartbeat within ${timeoutMs}ms`);
463
- this.name = "RemoteStepTimeout";
464
- this.stepId = stepId2;
465
- this.timeoutMs = timeoutMs;
466
- }
467
- };
468
- var RemoteWorkflowTimeout = class extends Error {
469
- static {
470
- __name(this, "RemoteWorkflowTimeout");
471
- }
472
- taskId;
473
- timeoutMs;
474
- constructor(taskId, timeoutMs) {
475
- super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
476
- this.name = "RemoteWorkflowTimeout";
477
- this.taskId = taskId;
478
- this.timeoutMs = timeoutMs;
479
- }
480
- };
481
- var NonDeterminismError = class extends Error {
482
- static {
483
- __name(this, "NonDeterminismError");
484
- }
485
- runId;
486
- seq;
487
- constructor(runId, seq, expected, recorded) {
488
- 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.`);
489
- this.name = "NonDeterminismError";
490
- this.runId = runId;
491
- this.seq = seq;
492
- }
493
- };
494
- var GatherError = class extends Error {
495
- static {
496
- __name(this, "GatherError");
497
- }
498
- failures;
499
- constructor(failures, total) {
500
- const ids = failures.map((f) => f.id).join(", ");
501
- const denom = total ?? failures.length;
502
- super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
503
- this.name = "GatherError";
504
- this.failures = failures;
505
- }
506
- };
507
- var WorkflowSuspended = class extends Error {
508
- static {
509
- __name(this, "WorkflowSuspended");
510
- }
511
- /** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
512
- wakeAt;
513
- constructor(wakeAt) {
514
- super("workflow suspended");
515
- this.name = "WorkflowSuspended";
516
- this.wakeAt = wakeAt;
517
- }
518
- };
519
- var ContinueAsNew = class extends Error {
520
- static {
521
- __name(this, "ContinueAsNew");
522
- }
523
- input;
524
- constructor(input) {
525
- super("workflow continued as new"), this.input = input;
526
- this.name = "ContinueAsNew";
527
- }
528
- };
529
-
530
627
  // src/entities.ts
531
628
  var ENTITY_WORKFLOW = "__entity";
532
629
  var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
@@ -988,18 +1085,6 @@ function stepConfigOf(ref) {
988
1085
  }
989
1086
  __name(stepConfigOf, "stepConfigOf");
990
1087
 
991
- // src/workflow-ref.ts
992
- var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
993
- function workflowName(ref) {
994
- if (typeof ref === "string") return ref;
995
- const name = ref[WORKFLOW_NAME_KEY];
996
- if (!name) {
997
- throw new Error(`workflow class ${ref.name} has no registered name \u2014 is it decorated with @Workflow({ name })?`);
998
- }
999
- return name;
1000
- }
1001
- __name(workflowName, "workflowName");
1002
-
1003
1088
  // src/workflow-ctx.ts
1004
1089
  function normalizeChildOptions(options) {
1005
1090
  return typeof options === "string" ? {
@@ -1185,6 +1270,20 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1185
1270
  value: buffered.payload
1186
1271
  };
1187
1272
  }, "consumeBuffered");
1273
+ const stampDeadline = /* @__PURE__ */ __name(async (deadlineSeq, label, timeoutMs) => {
1274
+ const recorded = await readCheckpoint(deadlineSeq);
1275
+ const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
1276
+ if (!recorded) {
1277
+ await writeCheckpoint(instantCheckpoint({
1278
+ runId,
1279
+ seq: deadlineSeq,
1280
+ name: `timeout:${label}`,
1281
+ kind: "sleep",
1282
+ wakeAt: deadline
1283
+ }));
1284
+ }
1285
+ return deadline;
1286
+ }, "stampDeadline");
1188
1287
  const waitForSignal = /* @__PURE__ */ __name(async (token, opts) => {
1189
1288
  if (opts?.timeoutMs == null) {
1190
1289
  const current = pos.next();
@@ -1202,17 +1301,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1202
1301
  const timeoutMs = opts.timeoutMs;
1203
1302
  const deadlineSeq = pos.next();
1204
1303
  const waitSeq = pos.next();
1205
- const recorded = await readCheckpoint(deadlineSeq);
1206
- const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
1207
- if (!recorded) {
1208
- await writeCheckpoint(instantCheckpoint({
1209
- runId,
1210
- seq: deadlineSeq,
1211
- name: `timeout:${token}`,
1212
- kind: "sleep",
1213
- wakeAt: deadline
1214
- }));
1215
- }
1304
+ const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
1216
1305
  const waited = await readCheckpoint(waitSeq);
1217
1306
  if (waited && waited.status === "completed") return waited.output;
1218
1307
  const buffered = await consumeBuffered(token, waitSeq);
@@ -1245,17 +1334,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1245
1334
  const deadlineSeq = pos.next();
1246
1335
  const waitSeq = pos.next();
1247
1336
  const token = eventToken(name, opts.match, runId, waitSeq);
1248
- const recorded = await readCheckpoint(deadlineSeq);
1249
- const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
1250
- if (!recorded) {
1251
- await writeCheckpoint(instantCheckpoint({
1252
- runId,
1253
- seq: deadlineSeq,
1254
- name: `timeout:event:${name}`,
1255
- kind: "sleep",
1256
- wakeAt: deadline
1257
- }));
1258
- }
1337
+ const deadline = await stampDeadline(deadlineSeq, `event:${name}`, timeoutMs);
1259
1338
  const waited = await readCheckpoint(waitSeq);
1260
1339
  if (waited && waited.status === "completed") return waited.output;
1261
1340
  if (host.clock() >= deadline) {
@@ -1311,6 +1390,9 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1311
1390
  const cp = existing[i];
1312
1391
  const c = cp?.status === "completed" ? cp.output : null;
1313
1392
  if (c && c.ok === false) {
1393
+ for (let j = 0; j < inputs.length; j += 1) {
1394
+ if (j !== i && existing[j]?.status !== "completed") host.cancelChild?.(id(j));
1395
+ }
1314
1396
  throw new GatherError([
1315
1397
  {
1316
1398
  index: i,
@@ -1467,15 +1549,30 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1467
1549
  const webhook = /* @__PURE__ */ __name(() => {
1468
1550
  const current = pos.next();
1469
1551
  const token = `wh:${runId}:${current}`;
1470
- const wait = /* @__PURE__ */ __name(async () => {
1552
+ const wait = /* @__PURE__ */ __name(async (opts) => {
1553
+ const timeoutMs = opts?.timeoutMs;
1554
+ const deadlineSeq = timeoutMs == null ? -1 : pos.next();
1471
1555
  const existing = await readCheckpoint(current);
1472
1556
  if (existing && existing.status === "completed") return existing.output;
1557
+ if (timeoutMs == null) {
1558
+ await store.putSignalWaiter({
1559
+ token,
1560
+ runId,
1561
+ seq: current
1562
+ });
1563
+ throw new WorkflowSuspended();
1564
+ }
1565
+ const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
1566
+ if (host.clock() >= deadline) {
1567
+ await store.takeSignalWaiter(token).catch(() => void 0);
1568
+ throw new SignalTimeoutError(token, timeoutMs);
1569
+ }
1473
1570
  await store.putSignalWaiter({
1474
1571
  token,
1475
1572
  runId,
1476
1573
  seq: current
1477
1574
  });
1478
- throw new WorkflowSuspended();
1575
+ throw new WorkflowSuspended(deadline);
1479
1576
  }, "wait");
1480
1577
  return {
1481
1578
  token,
@@ -1503,6 +1600,18 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1503
1600
  output: attrs
1504
1601
  }));
1505
1602
  }, "upsertSearchAttributes");
1603
+ function resolveCompensateDef(compensate) {
1604
+ const name = typeof compensate === "string" ? compensate : stepNameOf(compensate);
1605
+ if (name === void 0) {
1606
+ 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");
1607
+ }
1608
+ const config = typeof compensate === "string" ? void 0 : stepConfigOf(compensate);
1609
+ return {
1610
+ name,
1611
+ ...resolveStepPolicy(config, void 0)
1612
+ };
1613
+ }
1614
+ __name(resolveCompensateDef, "resolveCompensateDef");
1506
1615
  function step(handlerOrName, input, opts) {
1507
1616
  const name = typeof handlerOrName === "string" ? handlerOrName : stepNameOf(handlerOrName);
1508
1617
  if (name === void 0) {
@@ -1516,6 +1625,20 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1516
1625
  return host.callRemote(runId, pos.next(), def, input, opts?.queue, opts?.transport, {
1517
1626
  priority: opts?.priority,
1518
1627
  fairnessKey: opts?.fairnessKey
1628
+ }).then((output) => {
1629
+ if (opts?.compensate) {
1630
+ compensations.push({
1631
+ name,
1632
+ dispatch: {
1633
+ def: resolveCompensateDef(opts.compensate),
1634
+ args: {
1635
+ input,
1636
+ output
1637
+ }
1638
+ }
1639
+ });
1640
+ }
1641
+ return output;
1519
1642
  });
1520
1643
  }
1521
1644
  __name(step, "step");
@@ -2008,7 +2131,7 @@ var WorkflowEngine = class {
2008
2131
  workflow: name,
2009
2132
  workflowVersion: "1",
2010
2133
  status: "pending",
2011
- // Stamp the namespace the REAL run will carry (:893) so convention routing resolves the same
2134
+ // Stamp the namespace the REAL run will carry (below) so convention routing resolves the same
2012
2135
  // tenant-suffixed group the run will dispatch to — an operator starting a tenant's run via the
2013
2136
  // `onStartRun` wire (opts.namespace = tenant) must find the live `<workflow>@<tenant>` group,
2014
2137
  // not the bare group. Without this the start-run entry path can't reach a suffixed-only worker.
@@ -2409,7 +2532,13 @@ var WorkflowEngine = class {
2409
2532
  * `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
2410
2533
  */
2411
2534
  waitForRun(runId, opts) {
2412
- const isSettled = /* @__PURE__ */ __name((run) => run.status !== "pending" && run.status !== "running" && run.status !== "cancelling" && !(run.status === "suspended" && run.awaitingDecisionTaskId !== void 0), "isSettled");
2535
+ const TERMINAL = /* @__PURE__ */ new Set([
2536
+ "completed",
2537
+ "failed",
2538
+ "cancelled",
2539
+ "dead"
2540
+ ]);
2541
+ 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");
2413
2542
  const toResult = /* @__PURE__ */ __name((run) => ({
2414
2543
  runId,
2415
2544
  status: run.status,
@@ -2423,6 +2552,7 @@ var WorkflowEngine = class {
2423
2552
  const timer = opts?.timeoutMs != null ? setTimeout(() => {
2424
2553
  if (done) return;
2425
2554
  done = true;
2555
+ if (poller) clearInterval(poller);
2426
2556
  off();
2427
2557
  reject(new Error(`waitForRun(${runId}) timed out after ${opts.timeoutMs}ms`));
2428
2558
  }, opts.timeoutMs) : void 0;
@@ -2430,6 +2560,7 @@ var WorkflowEngine = class {
2430
2560
  if (done) return;
2431
2561
  done = true;
2432
2562
  if (timer) clearTimeout(timer);
2563
+ if (poller) clearInterval(poller);
2433
2564
  off();
2434
2565
  resolve(toResult(run));
2435
2566
  }, "finish");
@@ -2438,6 +2569,11 @@ var WorkflowEngine = class {
2438
2569
  if (run && isSettled(run)) finish(run);
2439
2570
  });
2440
2571
  }, "check");
2572
+ const poller = opts?.until === "terminal" ? (() => {
2573
+ const interval = setInterval(check, 250);
2574
+ interval.unref?.();
2575
+ return interval;
2576
+ })() : void 0;
2441
2577
  off = this.subscribe((ev) => {
2442
2578
  if (ev.runId === runId && (ev.type === "run.completed" || ev.type === "run.failed" || ev.type === "run.suspended")) {
2443
2579
  check();
@@ -3347,7 +3483,9 @@ var WorkflowEngine = class {
3347
3483
  parallelGroup: cmd.parallelGroup
3348
3484
  });
3349
3485
  if (!await this.store.getRun(childId)) {
3350
- queueMicrotask(() => void this.start(cmd.workflow, cmd.input, childId).catch(() => void 0));
3486
+ queueMicrotask(() => void this.start(cmd.workflow, cmd.input, childId, {
3487
+ namespace: run.namespace
3488
+ }).catch(() => void 0));
3351
3489
  }
3352
3490
  } else {
3353
3491
  throw new Error(`remote workflow command '${cmd.kind}' is not supported yet`);
@@ -3359,14 +3497,24 @@ var WorkflowEngine = class {
3359
3497
  * Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
3360
3498
  * exit path of a run with a compensating cancel in flight — whether the resumed replay returned,
3361
3499
  * suspended, hit continue-as-new, or threw — so a cancel is never lost just because the body didn't
3362
- * stop at a suspension point. Clears the in-memory cancel flag.
3500
+ * stop at a suspension point.
3501
+ *
3502
+ * A dispatched compensation still in flight (or backing off a retry) needs the run to keep waiting:
3503
+ * unlike the failure path (which is fine going `suspended` — a resumable state no different from an
3504
+ * ordinary in-flight `ctx.step`), THIS run must stay `cancelling` — `engine.cancel`'s idempotent
3505
+ * re-check, `waitForRun`'s non-terminal treatment, and crash recovery (`recoverIncomplete` only
3506
+ * re-drives `running`/`cancelling` runs — a `cancelling` one with a free lease, whatever it's
3507
+ * internally waiting on) all key off that status. So on a pending unwind this returns WITHOUT
3508
+ * touching `status` (already `cancelling`, untouched since `engine.cancel` set it) — the in-memory
3509
+ * cancel flag stays too, re-derived from the status on any later re-drive regardless.
3363
3510
  */
3364
- async compensateAndCancel(run, compensations) {
3511
+ async compensateAndCancel(run, compensations, replay) {
3512
+ const pending = await this.unwindCompensations(run, compensations, replay);
3513
+ if (pending) return {
3514
+ runId: run.id,
3515
+ status: "cancelling"
3516
+ };
3365
3517
  this.cancelRequested.delete(run.id);
3366
- for (let i = compensations.length - 1; i >= 0; i -= 1) {
3367
- const comp = compensations[i];
3368
- if (comp) await this.runCompensation(run, comp);
3369
- }
3370
3518
  const error = {
3371
3519
  message: "cancelled"
3372
3520
  };
@@ -3427,17 +3575,21 @@ var WorkflowEngine = class {
3427
3575
  const snapshot = await this.store.listCheckpoints(run.id);
3428
3576
  const replay = /* @__PURE__ */ new Map();
3429
3577
  for (const cp of snapshot) replay.set(cp.seq, cp);
3430
- const ctx = createWorkflowCtx(this.ctxHostFor(replay), run.id, compensations, run.workflow);
3578
+ const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow);
3431
3579
  try {
3432
- const output = await fn(ctx, run.input);
3433
- if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
3580
+ const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
3581
+ if (this.cancelRequested.has(run.id)) {
3582
+ return this.compensateAndCancel(run, compensations, replay);
3583
+ }
3434
3584
  return this.settleRun(run, {
3435
3585
  kind: "completed",
3436
3586
  output
3437
3587
  });
3438
3588
  } catch (err) {
3439
3589
  if (err instanceof ContinueAsNew) {
3440
- if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
3590
+ if (this.cancelRequested.has(run.id)) {
3591
+ return this.compensateAndCancel(run, compensations, replay);
3592
+ }
3441
3593
  await this.store.updateRun(run.id, {
3442
3594
  status: "completed",
3443
3595
  output: void 0,
@@ -3462,22 +3614,26 @@ var WorkflowEngine = class {
3462
3614
  };
3463
3615
  }
3464
3616
  if (err instanceof WorkflowSuspended) {
3465
- if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
3617
+ if (this.cancelRequested.has(run.id)) {
3618
+ return this.compensateAndCancel(run, compensations, replay);
3619
+ }
3466
3620
  return this.settleRun(run, {
3467
3621
  kind: "suspended",
3468
3622
  wakeAt: err.wakeAt
3469
3623
  });
3470
3624
  }
3471
- if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
3625
+ if (this.cancelRequested.has(run.id)) {
3626
+ return this.compensateAndCancel(run, compensations, replay);
3627
+ }
3472
3628
  const error = {
3473
3629
  message: err instanceof Error ? err.message : String(err),
3474
3630
  stack: err instanceof Error ? err.stack : void 0
3475
3631
  };
3476
- for (let i = compensations.length - 1; i >= 0; i -= 1) {
3477
- const comp = compensations[i];
3478
- if (!comp) continue;
3479
- await this.runCompensation(run, comp);
3480
- }
3632
+ const pending = await this.unwindCompensations(run, compensations, replay);
3633
+ if (pending) return this.settleRun(run, {
3634
+ kind: "suspended",
3635
+ wakeAt: pending.wakeAt
3636
+ });
3481
3637
  return this.settleRun(run, {
3482
3638
  kind: "failed",
3483
3639
  error
@@ -3487,14 +3643,62 @@ var WorkflowEngine = class {
3487
3643
  }
3488
3644
  }
3489
3645
  /**
3490
- * Run one saga compensation, retried up to `compensationRetries`, emitting a `compensate:<step>`
3491
- * step event for its outcome so a stranded undo is visible. Never throws — a permanently-failing
3492
- * compensation is skipped so it can't mask the original failure.
3646
+ * Undo every registered saga compensation in strict reverse order — the shared unwind both the
3647
+ * run-failure path (`runExecution`'s catch) and the compensating-cancel path
3648
+ * (`compensateAndCancel`) drive. Every entry, LOCAL or DISPATCHED, gets a NEGATIVE checkpoint seq
3649
+ * `-(k + 1)` for its position `k` (0, 1, 2… walking the stack in reverse) — reserved space that can
3650
+ * never collide with a body position (those start at 0), so a saga's undo trail composes onto the
3651
+ * run's ordinary checkpoint history with no store-schema change, and shows up in run detail exactly
3652
+ * like any other step. Deterministic because `compensations` is rebuilt identically on every replay
3653
+ * (see `workflow-ctx.ts`), so the same comp always lands on the same negative seq across a crash +
3654
+ * re-drive — the crash-safety this whole scheme rests on.
3655
+ *
3656
+ * A LOCAL entry runs in-process (`runCompensation`), which checkpoints its own outcome — completed,
3657
+ * or failed once `compensationRetries` are exhausted — so a crash mid-unwind SKIPS an
3658
+ * already-resolved one on the next pass instead of re-running it (and re-spending its retry budget)
3659
+ * from attempt 1. A DISPATCHED entry is an ordinary remote-step dispatch under the hood
3660
+ * (`dispatchCompensation`): when it needs the run to WAIT — freshly dispatched, still in flight, or
3661
+ * backing off a durable retry — this function returns that wait signal immediately (skipping every
3662
+ * comp after it in this pass); the CALLER decides how to represent "still unwinding" (see
3663
+ * `compensateAndCancel`'s doc comment for why that differs between the two callers). Either way, the
3664
+ * run resumes later exactly like any other dispatched step: `completeRemoteResult` completes the
3665
+ * negative-seq checkpoint and re-drives, the body replays and deterministically re-throws the same
3666
+ * exit it took before (the original failure, or the suspension `compensateAndCancel` resumed from),
3667
+ * this function runs again, the now-resolved comp is skipped, and the unwind continues from there.
3668
+ *
3669
+ * Returns `undefined` once every compensation has resolved — the caller then finalizes the run
3670
+ * (`failed` with the ORIGINAL body error, never masked by a comp outcome; or `cancelled`).
3671
+ */
3672
+ async unwindCompensations(run, compensations, replay) {
3673
+ for (let i = compensations.length - 1; i >= 0; i -= 1) {
3674
+ const comp = compensations[i];
3675
+ if (!comp) continue;
3676
+ const seq = -(compensations.length - i);
3677
+ if ("fn" in comp) {
3678
+ const existing = replay?.get(seq) ?? await this.store.getCheckpoint(run.id, seq);
3679
+ if (existing) continue;
3680
+ await this.runCompensation(run, comp, seq, replay);
3681
+ continue;
3682
+ }
3683
+ const pending = await this.dispatchCompensation(run, comp, seq, replay);
3684
+ if (pending) return pending;
3685
+ }
3686
+ return void 0;
3687
+ }
3688
+ /**
3689
+ * Run one LOCAL saga compensation, retried up to `compensationRetries`, emitting a
3690
+ * `compensate:<step>` step event for its outcome so a stranded undo is visible. Never throws — a
3691
+ * permanently-failing compensation is skipped so it can't mask the original failure. Checkpoints
3692
+ * its outcome at `seq` (a negative unwind position reserved by {@link unwindCompensations}) so a
3693
+ * crash mid-unwind skips an already-resolved undo on a later pass instead of re-running it (and
3694
+ * re-spending its retry budget) from attempt 1 — the checkpoint is durability/visibility only; the
3695
+ * runtime retry-in-process-back-to-back semantics of `comp.fn()` are unchanged.
3493
3696
  */
3494
- async runCompensation(run, comp) {
3697
+ async runCompensation(run, comp, seq, replay) {
3495
3698
  const name = `compensate:${comp.name}`;
3699
+ const enqueuedAt = /* @__PURE__ */ new Date();
3496
3700
  for (let attempt = 1; attempt <= this.compensationRetries; attempt += 1) {
3497
- const startedAt = Date.now();
3701
+ const startedAt = /* @__PURE__ */ new Date();
3498
3702
  try {
3499
3703
  await comp.fn();
3500
3704
  this.emit({
@@ -3503,30 +3707,147 @@ var WorkflowEngine = class {
3503
3707
  workflow: run.workflow,
3504
3708
  name,
3505
3709
  kind: "local",
3506
- durationMs: Date.now() - startedAt
3710
+ durationMs: Date.now() - startedAt.getTime()
3507
3711
  });
3712
+ const cp = stepCheckpoint({
3713
+ runId: run.id,
3714
+ seq,
3715
+ name,
3716
+ kind: "local",
3717
+ status: "completed",
3718
+ attempts: attempt,
3719
+ enqueuedAt,
3720
+ startedAt,
3721
+ finishedAt: /* @__PURE__ */ new Date()
3722
+ });
3723
+ await this.store.saveCheckpoint(cp);
3724
+ replay?.set(seq, cp);
3508
3725
  return;
3509
3726
  } catch (err) {
3510
3727
  if (attempt >= this.compensationRetries) {
3728
+ const error = {
3729
+ message: err instanceof Error ? err.message : String(err)
3730
+ };
3511
3731
  this.emit({
3512
3732
  type: "step.failed",
3513
3733
  runId: run.id,
3514
3734
  workflow: run.workflow,
3515
3735
  name,
3516
3736
  kind: "local",
3517
- error: {
3518
- message: err instanceof Error ? err.message : String(err)
3519
- },
3520
- durationMs: Date.now() - startedAt
3737
+ error,
3738
+ durationMs: Date.now() - startedAt.getTime()
3521
3739
  });
3740
+ const cp = stepCheckpoint({
3741
+ runId: run.id,
3742
+ seq,
3743
+ name,
3744
+ kind: "local",
3745
+ status: "failed",
3746
+ attempts: attempt,
3747
+ error,
3748
+ enqueuedAt,
3749
+ startedAt,
3750
+ finishedAt: /* @__PURE__ */ new Date()
3751
+ });
3752
+ await this.store.saveCheckpoint(cp);
3753
+ replay?.set(seq, cp);
3522
3754
  }
3523
3755
  }
3524
3756
  }
3525
3757
  }
3758
+ /**
3759
+ * Resolve (or advance) ONE dispatched saga compensation at its reserved negative `seq` — the undo
3760
+ * handler is an ordinary remote `@Step`, so this mirrors `callRemote`'s pending/failed/absent
3761
+ * branches (engine.ts's `callRemote`), minus the queue/admission and in-memory-`timeoutMs` paths
3762
+ * (V1: dispatch direct by name, always durable). Returns the wait signal when the run must pause for
3763
+ * this compensation (freshly dispatched, still in flight, or backing off before a durable retry);
3764
+ * `undefined` once it's resolved — completed (crash-safe skip), or retries exhausted/non-retryable
3765
+ * (already visible: `completeRemoteResult` emitted `step.failed` for `compensate:<name>` when that
3766
+ * checkpoint landed — never re-emitted here, and never allowed to block the rest of the unwind or
3767
+ * mask the run's original failure).
3768
+ */
3769
+ async dispatchCompensation(run, comp, seq, replay) {
3770
+ const { def, args } = comp.dispatch;
3771
+ const name = `compensate:${comp.name}`;
3772
+ const existing = replay?.get(seq) ?? await this.store.getCheckpoint(run.id, seq);
3773
+ if (existing?.status === "completed") return void 0;
3774
+ if (existing?.status === "pending") return {};
3775
+ if (existing?.status === "failed") {
3776
+ const maxAttempts = Math.max(1, def.retries ?? 1);
3777
+ const retryable = existing.error?.retryable !== false;
3778
+ if (!retryable || existing.attempts >= maxAttempts) return void 0;
3779
+ if (existing.wakeAt == null) {
3780
+ const wakeAt = this.clock() + backoffDelay(existing.attempts, def);
3781
+ const withWakeAt = {
3782
+ ...existing,
3783
+ wakeAt
3784
+ };
3785
+ await this.store.saveCheckpoint(withWakeAt);
3786
+ replay?.set(seq, withWakeAt);
3787
+ return {
3788
+ wakeAt
3789
+ };
3790
+ }
3791
+ if (this.clock() < existing.wakeAt) return {
3792
+ wakeAt: existing.wakeAt
3793
+ };
3794
+ await this.dispatchCompensationAttempt(run, seq, name, def, args, existing.attempts + 1, replay);
3795
+ return {};
3796
+ }
3797
+ await this.dispatchCompensationAttempt(run, seq, name, def, args, 1, replay);
3798
+ return {};
3799
+ }
3800
+ /**
3801
+ * Dispatch (or re-dispatch, on a durable retry) one compensation's undo handler — mirrors
3802
+ * `callRemote`'s dispatch block (persist the `pending` checkpoint BEFORE handing off to the
3803
+ * transport, so a fast result always finds it to complete). Two DIFFERENT names are in play: the
3804
+ * checkpoint's `name` is `compensate:<originalStepName>` (what makes this undo show up on the run's
3805
+ * timeline, and what `completeRemoteResult` emits `step.completed`/`step.failed` under); the WIRE
3806
+ * task's `name` is the undo handler's OWN routing name (`def.name`) — a worker dispatches to the
3807
+ * `@Step` it registered, not to this saga-visibility label.
3808
+ */
3809
+ async dispatchCompensationAttempt(run, seq, name, def, args, attempt, replay) {
3810
+ if (this.pool.size === 0) throw new Error("remote steps require a Transport");
3811
+ const group = tenantGroup(sanitizeQueueToken(def.name), def.partition);
3812
+ const enqueuedAt = /* @__PURE__ */ new Date();
3813
+ const cp = stepCheckpoint({
3814
+ runId: run.id,
3815
+ seq,
3816
+ name,
3817
+ kind: "remote",
3818
+ status: "pending",
3819
+ input: args,
3820
+ attempts: attempt,
3821
+ workerGroup: group,
3822
+ enqueuedAt,
3823
+ startedAt: enqueuedAt,
3824
+ finishedAt: enqueuedAt
3825
+ });
3826
+ await this.store.saveCheckpoint(cp);
3827
+ replay?.set(seq, cp);
3828
+ await this.pool.dispatch({
3829
+ runId: run.id,
3830
+ seq,
3831
+ name: def.name,
3832
+ stepId: cp.stepId,
3833
+ group,
3834
+ input: args,
3835
+ traceparent: this.traceparent?.(),
3836
+ context: this.context?.(),
3837
+ attempt
3838
+ });
3839
+ this.emit({
3840
+ type: "step.started",
3841
+ runId: run.id,
3842
+ seq,
3843
+ name,
3844
+ kind: "remote"
3845
+ });
3846
+ }
3526
3847
  /** The seam handed to {@link createWorkflowCtx}: the authoring API reaches durability + lifecycle
3527
3848
  * (checkpointing, dispatch, child start) through this, so the ctx primitives live in their own
3528
3849
  * module and the engine stays the orchestrator. */
3529
- ctxHostFor(replay) {
3850
+ ctxHostFor(replay, parentNamespace) {
3530
3851
  return {
3531
3852
  store: this.store,
3532
3853
  replay,
@@ -3536,12 +3857,24 @@ var WorkflowEngine = class {
3536
3857
  completeStep: /* @__PURE__ */ __name((s) => this.completeStep(s), "completeStep"),
3537
3858
  failStep: /* @__PURE__ */ __name((s) => this.failStep(s), "failStep"),
3538
3859
  callRemote: /* @__PURE__ */ __name((runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission), "callRemote"),
3539
- // Defer so a fast child can't reentrantly resume a still-running parent.
3860
+ // Defer so a fast child can't reentrantly resume a still-running parent. Stamp the child with
3861
+ // the PARENT run's namespace (not this engine's) so a child belongs to the tenant/partition of
3862
+ // the run it was spawned from — even when an operator (namespace: undefined, drives every
3863
+ // namespace) is the one executing the parent. Otherwise an operator recovery-resuming a
3864
+ // `davi-local` pipeline would stamp its `processing` child `default` and it would leak off the
3865
+ // tenant's worker pool onto the shared/dev workers.
3540
3866
  startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
3541
3867
  queueMicrotask(() => void this.start(workflow, input, id, {
3542
- priority
3868
+ priority,
3869
+ namespace: parentNamespace
3543
3870
  }).catch(() => void 0));
3544
3871
  }, "startChild"),
3872
+ // Deferred for the same reentrancy reason as `startChild` above. `cancel()` is already
3873
+ // idempotent on a terminal/cancelled run (returns its existing status without side effects), so
3874
+ // no extra guard is needed here for the failFast replay case (re-issuing the same cancel calls).
3875
+ cancelChild: /* @__PURE__ */ __name((childId) => {
3876
+ queueMicrotask(() => void this.cancel(childId).catch(() => void 0));
3877
+ }, "cancelChild"),
3545
3878
  // Shallow-merge into the run's searchAttributes (the ctx primitive makes this exactly-once).
3546
3879
  upsertSearchAttributes: /* @__PURE__ */ __name(async (runId, attrs) => {
3547
3880
  const run = await this.store.getRun(runId);
@@ -4664,6 +4997,7 @@ export {
4664
4997
  DURABLE_OPTIONS_CANONICAL,
4665
4998
  DURABLE_STEP_CONFIG,
4666
4999
  DURABLE_STEP_NAME,
5000
+ DurableWorkflow,
4667
5001
  Entities,
4668
5002
  EventAccumulators,
4669
5003
  FatalError,
@@ -4690,9 +5024,11 @@ export {
4690
5024
  applyAttributeQuery,
4691
5025
  attributeColumnFor,
4692
5026
  attributeOperand,
5027
+ bindWorkflowClass,
4693
5028
  breakpointToken,
4694
5029
  collectMetrics,
4695
5030
  createStepLogger,
5031
+ currentWorkflowCtx,
4696
5032
  eventMatchOf,
4697
5033
  eventMatches,
4698
5034
  eventPrefix,
@@ -4701,6 +5037,7 @@ export {
4701
5037
  normalizeAttributeRows,
4702
5038
  parseDuration,
4703
5039
  prevCronFireMs,
5040
+ runInWorkflowCtx,
4704
5041
  runSchedules,
4705
5042
  runStepHandler,
4706
5043
  sanitizeQueueToken,