@dudousxd/nestjs-durable-core 0.44.1 → 0.46.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
@@ -26,6 +26,8 @@ __export(index_exports, {
26
26
  ContinueAsNew: () => ContinueAsNew,
27
27
  DURABLE_OPTIONS: () => DURABLE_OPTIONS,
28
28
  DURABLE_OPTIONS_CANONICAL: () => DURABLE_OPTIONS_CANONICAL,
29
+ DURABLE_STEP_CONFIG: () => DURABLE_STEP_CONFIG,
30
+ DURABLE_STEP_NAME: () => DURABLE_STEP_NAME,
29
31
  Entities: () => Entities,
30
32
  EventAccumulators: () => EventAccumulators,
31
33
  FatalError: () => FatalError,
@@ -63,12 +65,15 @@ __export(index_exports, {
63
65
  normalizeAttributeRows: () => normalizeAttributeRows,
64
66
  parseDuration: () => parseDuration,
65
67
  prevCronFireMs: () => prevCronFireMs,
66
- remoteStep: () => remoteStep,
67
68
  runSchedules: () => runSchedules,
68
69
  runStepHandler: () => runStepHandler,
70
+ sanitizeQueueToken: () => sanitizeQueueToken,
69
71
  scheduledRunId: () => scheduledRunId,
70
72
  sqlComparator: () => sqlComparator,
73
+ stepConfigOf: () => stepConfigOf,
71
74
  stepId: () => stepId,
75
+ stepNameOf: () => stepNameOf,
76
+ tenantGroup: () => tenantGroup,
72
77
  workflowName: () => workflowName
73
78
  });
74
79
  module.exports = __toCommonJS(index_exports);
@@ -646,7 +651,7 @@ var Entities = class {
646
651
  let state = config.initialState();
647
652
  for (let i = 0; ; i += 1) {
648
653
  const msg = await ctx.waitForSignal(token);
649
- const out = await ctx.step(`op:${i}`, async () => {
654
+ const out = await ctx.localStep(`op:${i}`, async () => {
650
655
  const handler = config.handlers[msg.op];
651
656
  if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${msg.op}"`);
652
657
  const result = await handler(state, msg.arg);
@@ -658,7 +663,7 @@ var Entities = class {
658
663
  state = out.state;
659
664
  await ctx.setEvent("state", state);
660
665
  if (msg.reply) {
661
- await ctx.step(`reply:${i}`, async () => {
666
+ await ctx.localStep(`reply:${i}`, async () => {
662
667
  await this.host.signal(msg.reply, out.result);
663
668
  });
664
669
  }
@@ -772,16 +777,28 @@ function eventMatches(payload, match) {
772
777
  }
773
778
  __name(eventMatches, "eventMatches");
774
779
 
780
+ // src/tenant-group.ts
781
+ function tenantGroup(baseGroup, tenant) {
782
+ return tenant !== void 0 && tenant !== "" && tenant !== "default" ? `${baseGroup}@${tenant}` : baseGroup;
783
+ }
784
+ __name(tenantGroup, "tenantGroup");
785
+ function sanitizeQueueToken(name) {
786
+ return name.replace(/:/g, "-");
787
+ }
788
+ __name(sanitizeQueueToken, "sanitizeQueueToken");
789
+
775
790
  // src/remote-workflow-executor.ts
776
791
  var RemoteWorkflowExecutor = class {
777
792
  static {
778
793
  __name(this, "RemoteWorkflowExecutor");
779
794
  }
780
795
  transport;
781
- group;
782
- constructor(transport, group, _opts = {}) {
796
+ name;
797
+ partition;
798
+ constructor(transport, name, partition, _opts = {}) {
783
799
  this.transport = transport;
784
- this.group = group;
800
+ this.name = name;
801
+ this.partition = partition;
785
802
  }
786
803
  /**
787
804
  * Enqueue one workflow turn under the engine-supplied `taskId`. Does NOT await the decision — the
@@ -801,7 +818,7 @@ var RemoteWorkflowExecutor = class {
801
818
  ...pendingSignals ? {
802
819
  pendingSignals
803
820
  } : {},
804
- group: this.group,
821
+ group: tenantGroup(sanitizeQueueToken(this.name), this.partition),
805
822
  priority: run.priority,
806
823
  attempt: 1
807
824
  };
@@ -973,6 +990,16 @@ var TransportPool = class {
973
990
  liveWorkers: reports.flatMap((r) => r.liveWorkers)
974
991
  };
975
992
  }
993
+ /**
994
+ * Register a control-plane `start-run` consumer on every transport that carries the channel. Only
995
+ * control-plane-side transports implement {@link Transport.onStartRun}; a transport without it is
996
+ * skipped, so a plain worker/in-process pool is a no-op. Mirrors {@link bind}'s per-transport wiring.
997
+ */
998
+ onStartRun(handler) {
999
+ for (const { transport } of this.transports) {
1000
+ transport.onStartRun?.(handler);
1001
+ }
1002
+ }
976
1003
  /** Distinct worker groups with a live heartbeat, merged across every transport that can report it. */
977
1004
  async listWorkerGroups() {
978
1005
  const groups = /* @__PURE__ */ new Set();
@@ -985,6 +1012,17 @@ var TransportPool = class {
985
1012
  ...groups
986
1013
  ];
987
1014
  }
1015
+ /**
1016
+ * Propagate the engine's `namespace` to every transport that partitions by it — so the same
1017
+ * namespace that scopes the store also scopes each transport's queues/keys. A transport that
1018
+ * doesn't partition (no `useNamespace`) is skipped. Idempotent; a transport given an explicit
1019
+ * namespace at construction ignores this (see {@link Transport.useNamespace}).
1020
+ */
1021
+ useNamespace(namespace) {
1022
+ for (const { transport } of this.transports) {
1023
+ transport.useNamespace?.(namespace);
1024
+ }
1025
+ }
988
1026
  /** Pinned `preferId` first, then the rest (failover order). */
989
1027
  ordered(preferId) {
990
1028
  if (!preferId) return this.transports;
@@ -1007,6 +1045,26 @@ function unwrapCompletion(payload, label) {
1007
1045
  }
1008
1046
  __name(unwrapCompletion, "unwrapCompletion");
1009
1047
 
1048
+ // src/step-name-symbol.ts
1049
+ var DURABLE_STEP_NAME = Symbol.for("durable.step.name");
1050
+ var DURABLE_STEP_CONFIG = Symbol.for("durable.step.config");
1051
+ function isStepConfig(value) {
1052
+ return typeof value === "object" && value !== null;
1053
+ }
1054
+ __name(isStepConfig, "isStepConfig");
1055
+ function stepNameOf(ref) {
1056
+ if (typeof ref !== "function") return void 0;
1057
+ const stamped = ref[DURABLE_STEP_NAME];
1058
+ return typeof stamped === "string" ? stamped : void 0;
1059
+ }
1060
+ __name(stepNameOf, "stepNameOf");
1061
+ function stepConfigOf(ref) {
1062
+ if (typeof ref !== "function") return void 0;
1063
+ const stamped = ref[DURABLE_STEP_CONFIG];
1064
+ return isStepConfig(stamped) ? stamped : void 0;
1065
+ }
1066
+ __name(stepConfigOf, "stepConfigOf");
1067
+
1010
1068
  // src/workflow-ref.ts
1011
1069
  var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
1012
1070
  function workflowName(ref) {
@@ -1026,6 +1084,23 @@ function normalizeChildOptions(options) {
1026
1084
  } : options ?? {};
1027
1085
  }
1028
1086
  __name(normalizeChildOptions, "normalizeChildOptions");
1087
+ function resolveStepPolicy(config, opts) {
1088
+ const policy = {};
1089
+ const retries = opts?.retries ?? config?.retries;
1090
+ if (retries !== void 0) policy.retries = retries;
1091
+ const backoff = opts?.backoff ?? config?.backoff;
1092
+ if (backoff !== void 0) policy.backoff = backoff;
1093
+ const backoffMs = opts?.backoffMs ?? config?.backoffMs;
1094
+ if (backoffMs !== void 0) policy.backoffMs = backoffMs;
1095
+ const backoffMaxMs = opts?.backoffMaxMs ?? config?.backoffMaxMs;
1096
+ if (backoffMaxMs !== void 0) policy.backoffMaxMs = backoffMaxMs;
1097
+ const jitter = opts?.jitter ?? config?.jitter;
1098
+ if (jitter !== void 0) policy.jitter = jitter;
1099
+ const timeoutMs = opts?.timeoutMs ?? config?.timeoutMs;
1100
+ if (timeoutMs !== void 0) policy.timeoutMs = timeoutMs;
1101
+ return policy;
1102
+ }
1103
+ __name(resolveStepPolicy, "resolveStepPolicy");
1029
1104
  var Position = class Position2 {
1030
1105
  static {
1031
1106
  __name(this, "Position");
@@ -1051,7 +1126,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1051
1126
  await store.saveCheckpoint(cp);
1052
1127
  replay?.set(cp.seq, cp);
1053
1128
  }, "writeCheckpoint");
1054
- const step = /* @__PURE__ */ __name(async (name, fn, options) => {
1129
+ const localStep = /* @__PURE__ */ __name(async (name, fn, options) => {
1055
1130
  const current = pos.next();
1056
1131
  const existing = await readCheckpoint(current);
1057
1132
  if (existing && existing.name !== name) {
@@ -1125,7 +1200,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1125
1200
  if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
1126
1201
  }
1127
1202
  }
1128
- }, "step");
1203
+ }, "localStep");
1129
1204
  const transaction = /* @__PURE__ */ __name(async (name, fn) => {
1130
1205
  if (!store.transaction) {
1131
1206
  throw new Error("ctx.transaction needs a store that supports transactions (the SQL adapters do). Use ctx.step for non-transactional work.");
@@ -1272,7 +1347,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1272
1347
  throw new WorkflowSuspended(deadline);
1273
1348
  }, "waitForEvent");
1274
1349
  const task = /* @__PURE__ */ __name(async (name, dispatch, options) => {
1275
- await step(`task:dispatch:${name}`, dispatch, options);
1350
+ await localStep(`task:dispatch:${name}`, dispatch, options);
1276
1351
  return unwrapCompletion(await waitForSignal(`task:${runId}:${name}`), `task "${name}"`);
1277
1352
  }, "task");
1278
1353
  const child = /* @__PURE__ */ __name(async (workflow2, input, options) => {
@@ -1485,9 +1560,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1485
1560
  wait
1486
1561
  };
1487
1562
  }, "webhook");
1488
- const now = /* @__PURE__ */ __name(() => step("now", async () => host.clock()), "now");
1489
- const random = /* @__PURE__ */ __name(() => step("random", async () => Math.random()), "random");
1490
- const uuid = /* @__PURE__ */ __name(() => step("uuid", async () => globalThis.crypto.randomUUID()), "uuid");
1563
+ const sideEffect = /* @__PURE__ */ __name((fn) => localStep("sideEffect", async () => fn()), "sideEffect");
1564
+ const now = /* @__PURE__ */ __name(() => localStep("now", async () => host.clock()), "now");
1491
1565
  const upsertSearchAttributes = /* @__PURE__ */ __name(async (attrs) => {
1492
1566
  const current = pos.next();
1493
1567
  const existing = await readCheckpoint(current);
@@ -1506,9 +1580,26 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1506
1580
  output: attrs
1507
1581
  }));
1508
1582
  }, "upsertSearchAttributes");
1583
+ function step(handlerOrName, input, opts) {
1584
+ const name = typeof handlerOrName === "string" ? handlerOrName : stepNameOf(handlerOrName);
1585
+ if (name === void 0) {
1586
+ throw new Error("ctx.step: handler is not a @Step reference (no stamped step name) \u2014 pass a @Step-decorated method or a step name string");
1587
+ }
1588
+ const config = typeof handlerOrName === "string" ? void 0 : stepConfigOf(handlerOrName);
1589
+ const def = {
1590
+ name,
1591
+ ...resolveStepPolicy(config, opts)
1592
+ };
1593
+ return host.callRemote(runId, pos.next(), def, input, opts?.queue, opts?.transport, {
1594
+ priority: opts?.priority,
1595
+ fairnessKey: opts?.fairnessKey
1596
+ });
1597
+ }
1598
+ __name(step, "step");
1509
1599
  return {
1510
1600
  runId,
1511
1601
  step,
1602
+ localStep,
1512
1603
  upsertSearchAttributes,
1513
1604
  transaction,
1514
1605
  callEntity,
@@ -1528,12 +1619,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1528
1619
  onUpdate,
1529
1620
  patched,
1530
1621
  now,
1531
- random,
1532
- uuid,
1533
- call: /* @__PURE__ */ __name((remote, input, opts) => host.callRemote(runId, pos.next(), remote, input, opts?.queue, opts?.transport, {
1534
- priority: opts?.priority,
1535
- fairnessKey: opts?.fairnessKey
1536
- }), "call")
1622
+ sideEffect
1537
1623
  };
1538
1624
  }
1539
1625
  __name(createWorkflowCtx, "createWorkflowCtx");
@@ -1651,7 +1737,10 @@ var WorkflowEngine = class {
1651
1737
  this.admission = deps.admission ?? new InMemoryAdmissionBackend(this.clock);
1652
1738
  this.admission.onFreed?.((queue) => this.wakeQueueWaiters(queue));
1653
1739
  this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
1654
- this.namespace = deps.namespace ?? "default";
1740
+ this.namespace = deps.namespace;
1741
+ if (this.namespace !== void 0) {
1742
+ this.pool.useNamespace(this.namespace);
1743
+ }
1655
1744
  this.leaseMs = deps.leaseMs ?? 3e4;
1656
1745
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
1657
1746
  this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
@@ -1716,6 +1805,13 @@ var WorkflowEngine = class {
1716
1805
  await this.completeRemoteDecision(decision);
1717
1806
  }
1718
1807
  );
1808
+ this.pool.onStartRun(async (message) => {
1809
+ await this.start(message.workflow, message.input, message.runId ?? globalThis.crypto.randomUUID(), {
1810
+ namespace: message.tenant,
1811
+ tags: message.tags,
1812
+ searchAttributes: message.searchAttributes
1813
+ });
1814
+ });
1719
1815
  this.controlPlane?.onControl((msg) => {
1720
1816
  if (msg.from === this.instanceId) return;
1721
1817
  if (msg.kind === "event") {
@@ -1880,7 +1976,7 @@ var WorkflowEngine = class {
1880
1976
  });
1881
1977
  }
1882
1978
  /**
1883
- * Register a flow-control queue referenced by `ctx.call(step, input, { queue })`. Caps concurrent
1979
+ * Register a flow-control queue referenced by `ctx.step(step, input, { queue })`. Caps concurrent
1884
1980
  * in-flight steps and/or the admission rate; blocked calls re-suspend and retry, so the limit is
1885
1981
  * durable. Per engine instance (see {@link QueueConfig}). Registering the same name replaces it.
1886
1982
  */
@@ -1983,6 +2079,23 @@ var WorkflowEngine = class {
1983
2079
  const ancestor = await this.findRemoteAncestor(runId);
1984
2080
  if (ancestor) registered = this.synthesizeRemoteChild(name, ancestor.version, ancestor);
1985
2081
  }
2082
+ if (!registered) {
2083
+ const conventionRun = {
2084
+ id: runId,
2085
+ workflow: name,
2086
+ workflowVersion: "1",
2087
+ status: "pending",
2088
+ // Stamp the namespace the REAL run will carry (:893) so convention routing resolves the same
2089
+ // tenant-suffixed group the run will dispatch to — an operator starting a tenant's run via the
2090
+ // `onStartRun` wire (opts.namespace = tenant) must find the live `<workflow>@<tenant>` group,
2091
+ // not the bare group. Without this the start-run entry path can't reach a suffixed-only worker.
2092
+ namespace: opts?.namespace ?? this.namespace,
2093
+ input,
2094
+ createdAt: /* @__PURE__ */ new Date(),
2095
+ updatedAt: /* @__PURE__ */ new Date()
2096
+ };
2097
+ registered = await this.resolveRemoteByConvention(conventionRun);
2098
+ }
1986
2099
  if (!registered) throw new Error(`workflow ${name} is not registered`);
1987
2100
  await registered.validateInput?.(input);
1988
2101
  const prior = await this.store.getRun(runId);
@@ -2007,7 +2120,7 @@ var WorkflowEngine = class {
2007
2120
  workflow: name,
2008
2121
  workflowVersion: registered.version,
2009
2122
  status: "pending",
2010
- namespace: this.namespace,
2123
+ namespace: opts?.namespace ?? this.namespace,
2011
2124
  input,
2012
2125
  tags,
2013
2126
  searchAttributes: opts?.searchAttributes,
@@ -2044,11 +2157,11 @@ var WorkflowEngine = class {
2044
2157
  error: run.error
2045
2158
  };
2046
2159
  }
2047
- if (run.namespace !== void 0 && run.namespace !== this.namespace) {
2160
+ if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
2048
2161
  await this.store.releaseRunLock(runId);
2049
2162
  throw new NamespaceMismatch();
2050
2163
  }
2051
- const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run);
2164
+ const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
2052
2165
  if (!registered) {
2053
2166
  throw new Error(`workflow ${run.workflow}@${run.workflowVersion} is not registered \u2014 keep the prior version deployed so in-flight runs can drain (skew protection)`);
2054
2167
  }
@@ -2094,6 +2207,34 @@ var WorkflowEngine = class {
2094
2207
  };
2095
2208
  }
2096
2209
  /**
2210
+ * Attempt to route an unregistered workflow to the live worker group of the same name: if the
2211
+ * transport reports a live heartbeat for group `run.workflow`, synthesize a throwaway
2212
+ * {@link RegisteredWorkflow} that dispatches to that group over the primary transport. Returns
2213
+ * `undefined` when the group is not live. Always attempted — there is no opt-in flag; an explicit
2214
+ * `register`/`registerRemote` still takes precedence (this only runs when nothing is registered).
2215
+ *
2216
+ * Recomputed per call (never memoized into `this.workflows`) so it stays correct if group
2217
+ * liveness changes between calls, exactly as {@link synthesizeRemoteChild} does.
2218
+ */
2219
+ async resolveRemoteByConvention(run) {
2220
+ const liveGroups = await this.pool.listWorkerGroups();
2221
+ const group = tenantGroup(sanitizeQueueToken(run.workflow), run.namespace);
2222
+ if (!liveGroups.includes(group)) return void 0;
2223
+ const executor = new RemoteWorkflowExecutor(this.pool.primary, run.workflow, run.namespace);
2224
+ return {
2225
+ name: run.workflow,
2226
+ version: run.workflowVersion,
2227
+ fn: /* @__PURE__ */ __name(() => {
2228
+ throw new Error(`remote workflow ${run.workflow} has no local body`);
2229
+ }, "fn"),
2230
+ hasBody: false,
2231
+ remote: {
2232
+ group,
2233
+ executor
2234
+ }
2235
+ };
2236
+ }
2237
+ /**
2097
2238
  * Walk the parent chain of `childRunId` to the nearest ancestor registered as REMOTE, returning its
2098
2239
  * registration (or `undefined` if there is none). The parent of an awaited child is found via its
2099
2240
  * live `child:<childId>` signal waiter — present for as long as the parent is suspended awaiting the
@@ -2190,6 +2331,7 @@ var WorkflowEngine = class {
2190
2331
  type: "run.failed",
2191
2332
  runId: run.id,
2192
2333
  workflow: run.workflow,
2334
+ namespace: run.namespace,
2193
2335
  error
2194
2336
  });
2195
2337
  }
@@ -2247,6 +2389,7 @@ var WorkflowEngine = class {
2247
2389
  type: "run.failed",
2248
2390
  runId: run.id,
2249
2391
  workflow: run.workflow,
2392
+ namespace: run.namespace,
2250
2393
  error
2251
2394
  });
2252
2395
  this.notifyDead({
@@ -2321,7 +2464,8 @@ var WorkflowEngine = class {
2321
2464
  if (!run) return null;
2322
2465
  const id = newRunId ?? `${runId}~retry~${globalThis.crypto.randomUUID().slice(0, 8)}`;
2323
2466
  await this.start(run.workflow, input, id, {
2324
- tags: run.tags
2467
+ tags: run.tags,
2468
+ namespace: run.namespace
2325
2469
  });
2326
2470
  return {
2327
2471
  runId: id
@@ -2555,6 +2699,7 @@ var WorkflowEngine = class {
2555
2699
  type: "run.failed",
2556
2700
  runId,
2557
2701
  workflow: run.workflow,
2702
+ namespace: run.namespace,
2558
2703
  error
2559
2704
  });
2560
2705
  await this.cancelChildren(runId, opts);
@@ -2831,7 +2976,7 @@ var WorkflowEngine = class {
2831
2976
  });
2832
2977
  }
2833
2978
  async execute(run, fn) {
2834
- const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run);
2979
+ const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
2835
2980
  const lockNow = this.clock();
2836
2981
  if (run.lockedBy !== this.instanceId || (run.lockedUntil ?? 0) <= lockNow) {
2837
2982
  if (!await this.store.tryLockRun(run.id, this.instanceId, lockNow + this.leaseMs, lockNow)) {
@@ -2881,6 +3026,7 @@ var WorkflowEngine = class {
2881
3026
  type: "run.completed",
2882
3027
  runId: run.id,
2883
3028
  workflow: run.workflow,
3029
+ namespace: run.namespace,
2884
3030
  output: outcome.output
2885
3031
  });
2886
3032
  void this.notifyParent(run.id, {
@@ -2903,6 +3049,7 @@ var WorkflowEngine = class {
2903
3049
  type: "run.failed",
2904
3050
  runId: run.id,
2905
3051
  workflow: run.workflow,
3052
+ namespace: run.namespace,
2906
3053
  error: outcome.error
2907
3054
  });
2908
3055
  void this.notifyParent(run.id, {
@@ -2923,7 +3070,8 @@ var WorkflowEngine = class {
2923
3070
  this.emit({
2924
3071
  type: "run.suspended",
2925
3072
  runId: run.id,
2926
- workflow: run.workflow
3073
+ workflow: run.workflow,
3074
+ namespace: run.namespace
2927
3075
  });
2928
3076
  return {
2929
3077
  runId: run.id,
@@ -2944,6 +3092,7 @@ var WorkflowEngine = class {
2944
3092
  type: "run.failed",
2945
3093
  runId: run.id,
2946
3094
  workflow: run.workflow,
3095
+ namespace: run.namespace,
2947
3096
  error
2948
3097
  });
2949
3098
  this.notifyCancelled(run.id);
@@ -2971,7 +3120,8 @@ var WorkflowEngine = class {
2971
3120
  this.emit({
2972
3121
  type: "run.started",
2973
3122
  runId: run.id,
2974
- workflow: run.workflow
3123
+ workflow: run.workflow,
3124
+ namespace: run.namespace
2975
3125
  });
2976
3126
  }
2977
3127
  if (registered.singleton && !await this.singletons.admit(run, registered.singleton)) {
@@ -2984,7 +3134,8 @@ var WorkflowEngine = class {
2984
3134
  this.emit({
2985
3135
  type: "run.suspended",
2986
3136
  runId: run.id,
2987
- workflow: run.workflow
3137
+ workflow: run.workflow,
3138
+ namespace: run.namespace
2988
3139
  });
2989
3140
  await this.store.releaseRunLock(run.id);
2990
3141
  return {
@@ -3006,7 +3157,8 @@ var WorkflowEngine = class {
3006
3157
  this.emit({
3007
3158
  type: "run.suspended",
3008
3159
  runId: run.id,
3009
- workflow: run.workflow
3160
+ workflow: run.workflow,
3161
+ namespace: run.namespace
3010
3162
  });
3011
3163
  await this.store.releaseRunLock(run.id);
3012
3164
  await remote.executor.dispatch(run, history, taskId);
@@ -3045,6 +3197,7 @@ var WorkflowEngine = class {
3045
3197
  type: "run.failed",
3046
3198
  runId: run.id,
3047
3199
  workflow: run.workflow,
3200
+ namespace: run.namespace,
3048
3201
  error
3049
3202
  });
3050
3203
  return {
@@ -3195,13 +3348,16 @@ var WorkflowEngine = class {
3195
3348
  status: "pending",
3196
3349
  input: cmd.input,
3197
3350
  attempts: 1,
3198
- workerGroup: cmd.group,
3351
+ // `cmd.group` is the routing declaration the worker's replay emitted (cross-SDK, or a
3352
+ // group-served TS body) — treated as an isolation partition on top of the step's own
3353
+ // name, mirroring the StepDef formula below (identical expression everywhere).
3354
+ workerGroup: tenantGroup(sanitizeQueueToken(cmd.name), cmd.group),
3199
3355
  enqueuedAt: at,
3200
3356
  startedAt: at,
3201
3357
  finishedAt: at,
3202
3358
  // A `ctx.gather_calls([...])` fan-out stamps every dispatched `call` in the fan with the
3203
- // same group, so the dashboard renders the remote steps as one parallel fan (parity with
3204
- // the gathered `recordStep`/`startChild` tags). Undefined for a lone sequential `ctx.call`.
3359
+ // same group, so the dashboard renders the dispatched steps as one parallel fan (parity
3360
+ // with the gathered `recordStep`/`startChild` tags). Undefined for a lone sequential `ctx.step`.
3205
3361
  parallelGroup: cmd.parallelGroup
3206
3362
  }));
3207
3363
  await this.pool.dispatch({
@@ -3209,7 +3365,7 @@ var WorkflowEngine = class {
3209
3365
  seq: cmd.seq,
3210
3366
  name: cmd.name,
3211
3367
  stepId: id,
3212
- group: cmd.group,
3368
+ group: tenantGroup(sanitizeQueueToken(cmd.name), cmd.group),
3213
3369
  input: cmd.input,
3214
3370
  traceparent: this.traceparent?.(),
3215
3371
  context: this.context?.(),
@@ -3300,6 +3456,7 @@ var WorkflowEngine = class {
3300
3456
  type: "run.failed",
3301
3457
  runId: run.id,
3302
3458
  workflow: run.workflow,
3459
+ namespace: run.namespace,
3303
3460
  error
3304
3461
  });
3305
3462
  return {
@@ -3320,7 +3477,8 @@ var WorkflowEngine = class {
3320
3477
  this.emit({
3321
3478
  type: "run.started",
3322
3479
  runId: run.id,
3323
- workflow: run.workflow
3480
+ workflow: run.workflow,
3481
+ namespace: run.namespace
3324
3482
  });
3325
3483
  }
3326
3484
  if (run.status !== "cancelling" && registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
@@ -3333,7 +3491,8 @@ var WorkflowEngine = class {
3333
3491
  this.emit({
3334
3492
  type: "run.suspended",
3335
3493
  runId: run.id,
3336
- workflow: run.workflow
3494
+ workflow: run.workflow,
3495
+ namespace: run.namespace
3337
3496
  });
3338
3497
  await this.store.releaseRunLock(run.id);
3339
3498
  return {
@@ -3365,7 +3524,8 @@ var WorkflowEngine = class {
3365
3524
  this.emit({
3366
3525
  type: "run.completed",
3367
3526
  runId: run.id,
3368
- workflow: run.workflow
3527
+ workflow: run.workflow,
3528
+ namespace: run.namespace
3369
3529
  });
3370
3530
  void this.notifyParent(run.id, {
3371
3531
  ok: true,
@@ -3584,7 +3744,6 @@ var WorkflowEngine = class {
3584
3744
  this.queueWaiters.get(queue)?.delete(runId);
3585
3745
  this.stepQueue.set(id, queue);
3586
3746
  }
3587
- const validInput = step.input.parse(input);
3588
3747
  const enqueuedAt = /* @__PURE__ */ new Date();
3589
3748
  await this.store.saveCheckpoint({
3590
3749
  runId,
@@ -3593,9 +3752,9 @@ var WorkflowEngine = class {
3593
3752
  kind: "remote",
3594
3753
  stepId: id,
3595
3754
  status: "pending",
3596
- input: validInput,
3755
+ input,
3597
3756
  attempts: attempt,
3598
- workerGroup: step.group,
3757
+ workerGroup: tenantGroup(sanitizeQueueToken(step.name), step.partition),
3599
3758
  enqueuedAt,
3600
3759
  startedAt: enqueuedAt,
3601
3760
  finishedAt: enqueuedAt
@@ -3605,8 +3764,8 @@ var WorkflowEngine = class {
3605
3764
  seq,
3606
3765
  name: step.name,
3607
3766
  stepId: id,
3608
- group: step.group,
3609
- input: validInput,
3767
+ group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
3768
+ input,
3610
3769
  traceparent: this.traceparent?.(),
3611
3770
  context: this.context?.(),
3612
3771
  priority: admission?.priority,
@@ -3671,7 +3830,9 @@ var WorkflowEngine = class {
3671
3830
  const run = await this.store.getRun(decision.runId);
3672
3831
  if (!run || run.awaitingDecisionTaskId !== decision.taskId) return;
3673
3832
  if (run.status === "cancelled" || run.status === "completed" || run.status === "dead") return;
3674
- if (run.namespace !== void 0 && run.namespace !== this.namespace) return;
3833
+ if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
3834
+ return;
3835
+ }
3675
3836
  const nowMs = this.clock();
3676
3837
  if (!await this.store.tryLockRun(decision.runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
3677
3838
  return;
@@ -3737,7 +3898,7 @@ var WorkflowEngine = class {
3737
3898
  /** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
3738
3899
  async callRemoteInMemory(runId, seq, step, input, transport) {
3739
3900
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
3740
- const validInput = step.input.parse(input);
3901
+ const validInput = step.input ? step.input.parse(input) : input;
3741
3902
  const id = stepId(runId, seq);
3742
3903
  const enqueuedAt = /* @__PURE__ */ new Date();
3743
3904
  this.emit({
@@ -3760,7 +3921,7 @@ var WorkflowEngine = class {
3760
3921
  seq,
3761
3922
  name: step.name,
3762
3923
  stepId: id,
3763
- group: step.group,
3924
+ group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
3764
3925
  input: validInput,
3765
3926
  traceparent: this.traceparent?.(),
3766
3927
  context: this.context?.(),
@@ -3768,7 +3929,7 @@ var WorkflowEngine = class {
3768
3929
  }, transport);
3769
3930
  try {
3770
3931
  const resolution = step.timeoutMs ? await this.awaitWithHeartbeat(id, resultPromise, step.timeoutMs) : await resultPromise;
3771
- const output = step.output.parse(resolution.output);
3932
+ const output = step.output ? step.output.parse(resolution.output) : resolution.output;
3772
3933
  const startedAt = resolution.startedAt ? new Date(resolution.startedAt) : enqueuedAt;
3773
3934
  await this.completeStep({
3774
3935
  runId,
@@ -3779,7 +3940,7 @@ var WorkflowEngine = class {
3779
3940
  output,
3780
3941
  events: resolution.events,
3781
3942
  attempts: attempt,
3782
- workerGroup: step.group,
3943
+ workerGroup: tenantGroup(sanitizeQueueToken(step.name), step.partition),
3783
3944
  enqueuedAt,
3784
3945
  startedAt
3785
3946
  });
@@ -3855,16 +4016,6 @@ var TERMINAL_RUN_STATUSES = [
3855
4016
  "dead"
3856
4017
  ];
3857
4018
 
3858
- // src/remote-step-factory.ts
3859
- function remoteStep(config) {
3860
- return {
3861
- ...config,
3862
- group: config.group ?? config.name.split(".")[0] ?? config.name,
3863
- __remote: true
3864
- };
3865
- }
3866
- __name(remoteStep, "remoteStep");
3867
-
3868
4019
  // src/codec-state-store.ts
3869
4020
  var CodecStateStore = class {
3870
4021
  static {
@@ -4589,6 +4740,8 @@ var InMemoryTransport = class {
4589
4740
  ContinueAsNew,
4590
4741
  DURABLE_OPTIONS,
4591
4742
  DURABLE_OPTIONS_CANONICAL,
4743
+ DURABLE_STEP_CONFIG,
4744
+ DURABLE_STEP_NAME,
4592
4745
  Entities,
4593
4746
  EventAccumulators,
4594
4747
  FatalError,
@@ -4626,12 +4779,15 @@ var InMemoryTransport = class {
4626
4779
  normalizeAttributeRows,
4627
4780
  parseDuration,
4628
4781
  prevCronFireMs,
4629
- remoteStep,
4630
4782
  runSchedules,
4631
4783
  runStepHandler,
4784
+ sanitizeQueueToken,
4632
4785
  scheduledRunId,
4633
4786
  sqlComparator,
4787
+ stepConfigOf,
4634
4788
  stepId,
4789
+ stepNameOf,
4790
+ tenantGroup,
4635
4791
  workflowName
4636
4792
  });
4637
4793
  //# sourceMappingURL=index.cjs.map