@dudousxd/nestjs-durable-core 0.41.0 → 0.43.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.d.cts CHANGED
@@ -837,6 +837,58 @@ interface Transport {
837
837
  * still surfaces. Pairs with {@link groupHealth}. */
838
838
  listWorkerGroups?(): Promise<string[]>;
839
839
  }
840
+ /** How a worker decides its concurrency, carried in {@link WorkerStatus} so a dashboard can tell a
841
+ * fixed knob from a self-regulating one (and, for the latter, where the controller currently sits). */
842
+ interface WorkerConcurrencyStatus {
843
+ /** `fixed` = the configured `concurrency` number, never moves. `adaptive` = the controller tunes it. */
844
+ mode: 'fixed' | 'adaptive';
845
+ /** The concurrency ceiling in effect right now (fixed: the configured value; adaptive: the live limit). */
846
+ limit: number;
847
+ /** Adaptive only — the floor the controller won't go below. */
848
+ min?: number;
849
+ /** Adaptive only — the ceiling the controller won't exceed. */
850
+ max?: number;
851
+ }
852
+ /** The adaptive controller's most recent limit change — the "why did it move" a dashboard shows so an
853
+ * auto-tuner isn't a black box. Absent until the controller first adjusts (or for a fixed worker). */
854
+ interface WorkerAdjust {
855
+ /** Epoch ms of the adjustment. */
856
+ at: number;
857
+ /** Limit before this change. */
858
+ from: number;
859
+ /** Limit after this change. */
860
+ to: number;
861
+ /** Why it moved: `ram_ceiling` (hard brake), `backpressure` (errors/stall), `grow` (had headroom),
862
+ * `shrink` (latency gradient showed queuing), `cpu_ceiling`. */
863
+ reason: 'ram_ceiling' | 'cpu_ceiling' | 'backpressure' | 'grow' | 'shrink';
864
+ }
865
+ /** A live snapshot of a worker's execution state, carried in the worker-liveness heartbeat (so it
866
+ * rides the existing TTL'd key — no extra round-trip) and surfaced per worker in the dashboard. Every
867
+ * field beyond {@link concurrency}/{@link inFlight} is best-effort: a field a runtime can't measure is
868
+ * simply omitted. The shape is shared by the TS and Python SDKs so a mixed-language group reports
869
+ * uniformly. */
870
+ interface WorkerStatus {
871
+ /** Which SDK is running this worker — `node` (TS) or `python`. */
872
+ runtime?: 'node' | 'python';
873
+ /** The worker's concurrency knob and its live ceiling. */
874
+ concurrency: WorkerConcurrencyStatus;
875
+ /** Tasks executing right now (0..limit). With `limit`, this is the saturation a dashboard charts. */
876
+ inFlight: number;
877
+ /** Resident set size in bytes, if the runtime can read it. */
878
+ rssBytes?: number;
879
+ /** The process memory ceiling in bytes (cgroup `memory.max`, else host total), if known. */
880
+ rssLimitBytes?: number;
881
+ /** RSS as a percent of {@link rssLimitBytes} (0..100) — what the adaptive RAM brake watches. */
882
+ rssPct?: number;
883
+ /** Process CPU utilisation percent over the last control tick (0..100×cores), if measured. */
884
+ cpuPct?: number;
885
+ /** Completed tasks per minute over the recent window — the controller's throughput signal. */
886
+ throughputPerMin?: number;
887
+ /** p95 of recent task durations in ms — the latency a dashboard charts against the limit. */
888
+ p95Ms?: number;
889
+ /** The adaptive controller's last limit change (absent for a fixed worker or before the first move). */
890
+ lastAdjust?: WorkerAdjust;
891
+ }
840
892
  /** One worker's liveness record — a TTL'd heartbeat a worker refreshes while it's consuming. Its
841
893
  * ABSENCE (the key expired) is the signal: a worker that died or stalled stops refreshing. */
842
894
  interface WorkerHeartbeat {
@@ -846,6 +898,9 @@ interface WorkerHeartbeat {
846
898
  instanceId: string;
847
899
  /** Epoch ms of the worker's most recent heartbeat. */
848
900
  lastBeatAt: number;
901
+ /** The worker's live execution snapshot, when its heartbeat carries one (newer SDKs stamp a JSON
902
+ * value; an older SDK's bare-timestamp heartbeat leaves this undefined). */
903
+ status?: WorkerStatus;
849
904
  }
850
905
  /** Per-group worker-health snapshot: how much work is queued vs. how many workers are alive to do it.
851
906
  * The actionable alert state is `depth > 0 && liveWorkers.length === 0` — work piling up with no
@@ -1581,6 +1636,29 @@ declare class WorkflowEngine {
1581
1636
  executionTimeout?: string | number;
1582
1637
  validateInput?: (input: unknown) => void | Promise<void>;
1583
1638
  }): void;
1639
+ /**
1640
+ * The convenience form of {@link registerRemote}: register a workflow whose body runs on a worker
1641
+ * `group` (e.g. the Python `durable-worker`, or a separate TS worker), without hand-building a
1642
+ * {@link RemoteWorkflowExecutor}. It wires the standard broker executor — a `RemoteWorkflowExecutor`
1643
+ * over the engine's own transport, dispatching to `opts.group` — for you, so you write
1644
+ * `engine.remote('processing', { group: 'processing' })` instead of
1645
+ * `engine.registerRemote('processing', '1', { group: 'processing', executor: new RemoteWorkflowExecutor(transport, 'processing') })`.
1646
+ *
1647
+ * Defaults the version to `'1'`. All the other {@link registerRemote} options
1648
+ * (`tags` / `singleton` / `executionTimeout` / `validateInput`) pass straight through. Reach for the
1649
+ * low-level {@link registerRemote} when you need a custom {@link WorkflowExecutor} (a different
1650
+ * transport, a fixed `timeoutMs`, a polyglot stub) — `remote()` is the standard wiring on the rails.
1651
+ */
1652
+ remote(name: string, opts: {
1653
+ /** Worker group this workflow's turns are dispatched to (also the executor's target group). */
1654
+ group: string;
1655
+ /** Workflow version. Defaults to `'1'`. */
1656
+ version?: string;
1657
+ tags?: string[];
1658
+ singleton?: SingletonConfig;
1659
+ executionTimeout?: string | number;
1660
+ validateInput?: (input: unknown) => void | Promise<void>;
1661
+ }): void;
1584
1662
  /**
1585
1663
  * Register a flow-control queue referenced by `ctx.call(step, input, { queue })`. Caps concurrent
1586
1664
  * in-flight steps and/or the admission rate; blocked calls re-suspend and retry, so the limit is
@@ -2496,4 +2574,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
2496
2574
  onControl(handler: (msg: ControlMessage) => void): void;
2497
2575
  }
2498
2576
 
2499
- export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, type DurableWebhook, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, type RemoteStepConfig, type RemoteStepDef, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDispatcher, type RunQuery, type RunResult, type RunSchedulesOptions, type RunStatus, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, type StartOptions, type StateStore, type StepCheckpoint, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepResult, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type Transport, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerHeartbeat, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, breakpointToken, collectMetrics, createStepLogger, eventMatchOf, eventMatches, eventPrefix, eventToken, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, remoteStep, runSchedules, runStepHandler, scheduledRunId, sqlComparator, stepId, workflowName };
2577
+ export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, type DurableWebhook, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, type RemoteStepConfig, type RemoteStepDef, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDispatcher, type RunQuery, type RunResult, type RunSchedulesOptions, type RunStatus, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, type StartOptions, type StateStore, type StepCheckpoint, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepResult, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type Transport, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, breakpointToken, collectMetrics, createStepLogger, eventMatchOf, eventMatches, eventPrefix, eventToken, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, remoteStep, runSchedules, runStepHandler, scheduledRunId, sqlComparator, stepId, workflowName };
package/dist/index.d.ts CHANGED
@@ -837,6 +837,58 @@ interface Transport {
837
837
  * still surfaces. Pairs with {@link groupHealth}. */
838
838
  listWorkerGroups?(): Promise<string[]>;
839
839
  }
840
+ /** How a worker decides its concurrency, carried in {@link WorkerStatus} so a dashboard can tell a
841
+ * fixed knob from a self-regulating one (and, for the latter, where the controller currently sits). */
842
+ interface WorkerConcurrencyStatus {
843
+ /** `fixed` = the configured `concurrency` number, never moves. `adaptive` = the controller tunes it. */
844
+ mode: 'fixed' | 'adaptive';
845
+ /** The concurrency ceiling in effect right now (fixed: the configured value; adaptive: the live limit). */
846
+ limit: number;
847
+ /** Adaptive only — the floor the controller won't go below. */
848
+ min?: number;
849
+ /** Adaptive only — the ceiling the controller won't exceed. */
850
+ max?: number;
851
+ }
852
+ /** The adaptive controller's most recent limit change — the "why did it move" a dashboard shows so an
853
+ * auto-tuner isn't a black box. Absent until the controller first adjusts (or for a fixed worker). */
854
+ interface WorkerAdjust {
855
+ /** Epoch ms of the adjustment. */
856
+ at: number;
857
+ /** Limit before this change. */
858
+ from: number;
859
+ /** Limit after this change. */
860
+ to: number;
861
+ /** Why it moved: `ram_ceiling` (hard brake), `backpressure` (errors/stall), `grow` (had headroom),
862
+ * `shrink` (latency gradient showed queuing), `cpu_ceiling`. */
863
+ reason: 'ram_ceiling' | 'cpu_ceiling' | 'backpressure' | 'grow' | 'shrink';
864
+ }
865
+ /** A live snapshot of a worker's execution state, carried in the worker-liveness heartbeat (so it
866
+ * rides the existing TTL'd key — no extra round-trip) and surfaced per worker in the dashboard. Every
867
+ * field beyond {@link concurrency}/{@link inFlight} is best-effort: a field a runtime can't measure is
868
+ * simply omitted. The shape is shared by the TS and Python SDKs so a mixed-language group reports
869
+ * uniformly. */
870
+ interface WorkerStatus {
871
+ /** Which SDK is running this worker — `node` (TS) or `python`. */
872
+ runtime?: 'node' | 'python';
873
+ /** The worker's concurrency knob and its live ceiling. */
874
+ concurrency: WorkerConcurrencyStatus;
875
+ /** Tasks executing right now (0..limit). With `limit`, this is the saturation a dashboard charts. */
876
+ inFlight: number;
877
+ /** Resident set size in bytes, if the runtime can read it. */
878
+ rssBytes?: number;
879
+ /** The process memory ceiling in bytes (cgroup `memory.max`, else host total), if known. */
880
+ rssLimitBytes?: number;
881
+ /** RSS as a percent of {@link rssLimitBytes} (0..100) — what the adaptive RAM brake watches. */
882
+ rssPct?: number;
883
+ /** Process CPU utilisation percent over the last control tick (0..100×cores), if measured. */
884
+ cpuPct?: number;
885
+ /** Completed tasks per minute over the recent window — the controller's throughput signal. */
886
+ throughputPerMin?: number;
887
+ /** p95 of recent task durations in ms — the latency a dashboard charts against the limit. */
888
+ p95Ms?: number;
889
+ /** The adaptive controller's last limit change (absent for a fixed worker or before the first move). */
890
+ lastAdjust?: WorkerAdjust;
891
+ }
840
892
  /** One worker's liveness record — a TTL'd heartbeat a worker refreshes while it's consuming. Its
841
893
  * ABSENCE (the key expired) is the signal: a worker that died or stalled stops refreshing. */
842
894
  interface WorkerHeartbeat {
@@ -846,6 +898,9 @@ interface WorkerHeartbeat {
846
898
  instanceId: string;
847
899
  /** Epoch ms of the worker's most recent heartbeat. */
848
900
  lastBeatAt: number;
901
+ /** The worker's live execution snapshot, when its heartbeat carries one (newer SDKs stamp a JSON
902
+ * value; an older SDK's bare-timestamp heartbeat leaves this undefined). */
903
+ status?: WorkerStatus;
849
904
  }
850
905
  /** Per-group worker-health snapshot: how much work is queued vs. how many workers are alive to do it.
851
906
  * The actionable alert state is `depth > 0 && liveWorkers.length === 0` — work piling up with no
@@ -1581,6 +1636,29 @@ declare class WorkflowEngine {
1581
1636
  executionTimeout?: string | number;
1582
1637
  validateInput?: (input: unknown) => void | Promise<void>;
1583
1638
  }): void;
1639
+ /**
1640
+ * The convenience form of {@link registerRemote}: register a workflow whose body runs on a worker
1641
+ * `group` (e.g. the Python `durable-worker`, or a separate TS worker), without hand-building a
1642
+ * {@link RemoteWorkflowExecutor}. It wires the standard broker executor — a `RemoteWorkflowExecutor`
1643
+ * over the engine's own transport, dispatching to `opts.group` — for you, so you write
1644
+ * `engine.remote('processing', { group: 'processing' })` instead of
1645
+ * `engine.registerRemote('processing', '1', { group: 'processing', executor: new RemoteWorkflowExecutor(transport, 'processing') })`.
1646
+ *
1647
+ * Defaults the version to `'1'`. All the other {@link registerRemote} options
1648
+ * (`tags` / `singleton` / `executionTimeout` / `validateInput`) pass straight through. Reach for the
1649
+ * low-level {@link registerRemote} when you need a custom {@link WorkflowExecutor} (a different
1650
+ * transport, a fixed `timeoutMs`, a polyglot stub) — `remote()` is the standard wiring on the rails.
1651
+ */
1652
+ remote(name: string, opts: {
1653
+ /** Worker group this workflow's turns are dispatched to (also the executor's target group). */
1654
+ group: string;
1655
+ /** Workflow version. Defaults to `'1'`. */
1656
+ version?: string;
1657
+ tags?: string[];
1658
+ singleton?: SingletonConfig;
1659
+ executionTimeout?: string | number;
1660
+ validateInput?: (input: unknown) => void | Promise<void>;
1661
+ }): void;
1584
1662
  /**
1585
1663
  * Register a flow-control queue referenced by `ctx.call(step, input, { queue })`. Caps concurrent
1586
1664
  * in-flight steps and/or the admission rate; blocked calls re-suspend and retry, so the limit is
@@ -2496,4 +2574,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
2496
2574
  onControl(handler: (msg: ControlMessage) => void): void;
2497
2575
  }
2498
2576
 
2499
- export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, type DurableWebhook, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, type RemoteStepConfig, type RemoteStepDef, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDispatcher, type RunQuery, type RunResult, type RunSchedulesOptions, type RunStatus, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, type StartOptions, type StateStore, type StepCheckpoint, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepResult, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type Transport, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerHeartbeat, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, breakpointToken, collectMetrics, createStepLogger, eventMatchOf, eventMatches, eventPrefix, eventToken, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, remoteStep, runSchedules, runStepHandler, scheduledRunId, sqlComparator, stepId, workflowName };
2577
+ export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, type DurableWebhook, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, type RemoteStepConfig, type RemoteStepDef, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDispatcher, type RunQuery, type RunResult, type RunSchedulesOptions, type RunStatus, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, type StartOptions, type StateStore, type StepCheckpoint, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepResult, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type Transport, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, breakpointToken, collectMetrics, createStepLogger, eventMatchOf, eventMatches, eventPrefix, eventToken, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, remoteStep, runSchedules, runStepHandler, scheduledRunId, sqlComparator, stepId, workflowName };
package/dist/index.js CHANGED
@@ -700,6 +700,70 @@ function eventMatches(payload, match) {
700
700
  }
701
701
  __name(eventMatches, "eventMatches");
702
702
 
703
+ // src/remote-workflow-executor.ts
704
+ var taskCounter = 0;
705
+ var RemoteWorkflowExecutor = class {
706
+ static {
707
+ __name(this, "RemoteWorkflowExecutor");
708
+ }
709
+ transport;
710
+ group;
711
+ opts;
712
+ pending = /* @__PURE__ */ new Map();
713
+ subscribed = false;
714
+ constructor(transport, group, opts = {}) {
715
+ this.transport = transport;
716
+ this.group = group;
717
+ this.opts = opts;
718
+ }
719
+ ensureSubscribed() {
720
+ if (this.subscribed) return;
721
+ if (!this.transport.onDecision) {
722
+ throw new Error("transport does not support workflow decisions (onDecision)");
723
+ }
724
+ this.subscribed = true;
725
+ this.transport.onDecision(async (decision) => {
726
+ const resolve = this.pending.get(decision.taskId);
727
+ if (resolve) {
728
+ this.pending.delete(decision.taskId);
729
+ resolve(decision);
730
+ }
731
+ });
732
+ }
733
+ async advance(run, history) {
734
+ if (!this.transport.dispatchWorkflowTask) {
735
+ throw new Error("transport does not support workflow tasks (dispatchWorkflowTask)");
736
+ }
737
+ this.ensureSubscribed();
738
+ taskCounter += 1;
739
+ const taskId = `${run.id}:wf:${taskCounter}`;
740
+ const task = {
741
+ taskId,
742
+ runId: run.id,
743
+ workflow: run.workflow,
744
+ workflowVersion: run.workflowVersion,
745
+ input: run.input,
746
+ history,
747
+ group: this.group,
748
+ priority: run.priority,
749
+ attempt: 1
750
+ };
751
+ const decision = new Promise((resolve, reject) => {
752
+ this.pending.set(taskId, resolve);
753
+ const { timeoutMs } = this.opts;
754
+ if (timeoutMs) {
755
+ const timer = setTimeout(() => {
756
+ this.pending.delete(taskId);
757
+ reject(new RemoteWorkflowTimeout(taskId, timeoutMs));
758
+ }, timeoutMs);
759
+ timer.unref?.();
760
+ }
761
+ });
762
+ await this.transport.dispatchWorkflowTask(task);
763
+ return decision;
764
+ }
765
+ };
766
+
703
767
  // src/singleton-gate.ts
704
768
  var SINGLETON_RETRY_MS = 1e3;
705
769
  var SINGLETON_RETRY_JITTER_MS = 250;
@@ -801,6 +865,19 @@ var TransportPool = class {
801
865
  get size() {
802
866
  return this.transports.length;
803
867
  }
868
+ /**
869
+ * The primary (first) transport — the one `dispatch` prefers when no `preferId` is pinned. A
870
+ * {@link RemoteWorkflowExecutor} built by `engine.remote()` rides it (it needs a single
871
+ * `dispatchWorkflowTask`/`onDecision` Transport, not the whole failover pool). Throws if the pool
872
+ * is empty — there is nothing to dispatch a remote workflow over.
873
+ */
874
+ get primary() {
875
+ const first = this.transports[0];
876
+ if (!first) {
877
+ throw new Error("no transport configured \u2014 engine.remote() needs a transport (or transports)");
878
+ }
879
+ return first.transport;
880
+ }
804
881
  /** Register the engine's result/heartbeat/step-event handlers on EVERY transport — a result can come
805
882
  * back on whichever one delivered the task, so failover stays symmetric. `onStepEvent` is wired only
806
883
  * on transports that carry streamed workflow step lifecycle (BullMQ); others simply skip it. */
@@ -1720,6 +1797,28 @@ var WorkflowEngine = class {
1720
1797
  if (!current || isNewerVersion(version, current.version)) this.latest.set(name, registered);
1721
1798
  }
1722
1799
  /**
1800
+ * The convenience form of {@link registerRemote}: register a workflow whose body runs on a worker
1801
+ * `group` (e.g. the Python `durable-worker`, or a separate TS worker), without hand-building a
1802
+ * {@link RemoteWorkflowExecutor}. It wires the standard broker executor — a `RemoteWorkflowExecutor`
1803
+ * over the engine's own transport, dispatching to `opts.group` — for you, so you write
1804
+ * `engine.remote('processing', { group: 'processing' })` instead of
1805
+ * `engine.registerRemote('processing', '1', { group: 'processing', executor: new RemoteWorkflowExecutor(transport, 'processing') })`.
1806
+ *
1807
+ * Defaults the version to `'1'`. All the other {@link registerRemote} options
1808
+ * (`tags` / `singleton` / `executionTimeout` / `validateInput`) pass straight through. Reach for the
1809
+ * low-level {@link registerRemote} when you need a custom {@link WorkflowExecutor} (a different
1810
+ * transport, a fixed `timeoutMs`, a polyglot stub) — `remote()` is the standard wiring on the rails.
1811
+ */
1812
+ remote(name, opts) {
1813
+ const { group, version, ...rest } = opts;
1814
+ const executor = new RemoteWorkflowExecutor(this.pool.primary, group);
1815
+ this.registerRemote(name, version ?? "1", {
1816
+ group,
1817
+ executor,
1818
+ ...rest
1819
+ });
1820
+ }
1821
+ /**
1723
1822
  * Register a flow-control queue referenced by `ctx.call(step, input, { queue })`. Caps concurrent
1724
1823
  * in-flight steps and/or the admission rate; blocked calls re-suspend and retry, so the limit is
1725
1824
  * durable. Per engine instance (see {@link QueueConfig}). Registering the same name replaces it.
@@ -3615,70 +3714,6 @@ function remoteStep(config) {
3615
3714
  }
3616
3715
  __name(remoteStep, "remoteStep");
3617
3716
 
3618
- // src/remote-workflow-executor.ts
3619
- var taskCounter = 0;
3620
- var RemoteWorkflowExecutor = class {
3621
- static {
3622
- __name(this, "RemoteWorkflowExecutor");
3623
- }
3624
- transport;
3625
- group;
3626
- opts;
3627
- pending = /* @__PURE__ */ new Map();
3628
- subscribed = false;
3629
- constructor(transport, group, opts = {}) {
3630
- this.transport = transport;
3631
- this.group = group;
3632
- this.opts = opts;
3633
- }
3634
- ensureSubscribed() {
3635
- if (this.subscribed) return;
3636
- if (!this.transport.onDecision) {
3637
- throw new Error("transport does not support workflow decisions (onDecision)");
3638
- }
3639
- this.subscribed = true;
3640
- this.transport.onDecision(async (decision) => {
3641
- const resolve = this.pending.get(decision.taskId);
3642
- if (resolve) {
3643
- this.pending.delete(decision.taskId);
3644
- resolve(decision);
3645
- }
3646
- });
3647
- }
3648
- async advance(run, history) {
3649
- if (!this.transport.dispatchWorkflowTask) {
3650
- throw new Error("transport does not support workflow tasks (dispatchWorkflowTask)");
3651
- }
3652
- this.ensureSubscribed();
3653
- taskCounter += 1;
3654
- const taskId = `${run.id}:wf:${taskCounter}`;
3655
- const task = {
3656
- taskId,
3657
- runId: run.id,
3658
- workflow: run.workflow,
3659
- workflowVersion: run.workflowVersion,
3660
- input: run.input,
3661
- history,
3662
- group: this.group,
3663
- priority: run.priority,
3664
- attempt: 1
3665
- };
3666
- const decision = new Promise((resolve, reject) => {
3667
- this.pending.set(taskId, resolve);
3668
- const { timeoutMs } = this.opts;
3669
- if (timeoutMs) {
3670
- const timer = setTimeout(() => {
3671
- this.pending.delete(taskId);
3672
- reject(new RemoteWorkflowTimeout(taskId, timeoutMs));
3673
- }, timeoutMs);
3674
- timer.unref?.();
3675
- }
3676
- });
3677
- await this.transport.dispatchWorkflowTask(task);
3678
- return decision;
3679
- }
3680
- };
3681
-
3682
3717
  // src/codec-state-store.ts
3683
3718
  var CodecStateStore = class {
3684
3719
  static {