@dudousxd/nestjs-durable-core 0.44.0 → 0.45.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 +17 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -21
- package/dist/index.d.ts +27 -21
- package/dist/index.js +17 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -802,17 +802,17 @@ interface WorkflowExecutor {
|
|
|
802
802
|
*/
|
|
803
803
|
advance?(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<WorkflowDecision>;
|
|
804
804
|
/**
|
|
805
|
-
* DISPATCH-AND-SUSPEND (broker-backed, multi-instance safe):
|
|
806
|
-
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
805
|
+
* DISPATCH-AND-SUSPEND (broker-backed, multi-instance safe): enqueue the turn to a worker under the
|
|
806
|
+
* ENGINE-SUPPLIED `taskId`, WITHOUT awaiting the decision. The engine generates `taskId`, records it
|
|
807
|
+
* on the run as the awaited marker, AND releases the run's lease — all BEFORE calling this — so the
|
|
808
|
+
* worker's {@link WorkflowDecision} (delivered over {@link Transport.onDecision} and applied DURABLY
|
|
809
|
+
* by {@link WorkflowEngine.completeRemoteDecision}, which looks the run up by `decision.runId`) can
|
|
810
|
+
* never arrive ahead of its marker or contend with a still-held lease and be dropped. This method
|
|
811
|
+
* therefore ONLY enqueues — it owns no taskId generation and returns nothing. No in-memory await, so
|
|
812
|
+
* the decision is never lost to a point-to-point broker handing it to a non-dispatching instance.
|
|
813
|
+
* Provide this OR {@link advance} — exactly one.
|
|
814
|
+
*/
|
|
815
|
+
dispatch?(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
|
|
816
816
|
}
|
|
817
817
|
/**
|
|
818
818
|
* A transport in an ordered pool, identified by `id`. The engine dispatches on the first by default
|
|
@@ -840,6 +840,13 @@ interface Transport {
|
|
|
840
840
|
onResult(handler: (result: StepResult) => Promise<void>): void;
|
|
841
841
|
/** worker → engine: liveness signal for an in-flight long step. */
|
|
842
842
|
onHeartbeat(handler: (beat: Heartbeat) => Promise<void>): void;
|
|
843
|
+
/**
|
|
844
|
+
* Partition this transport's queues/keys by `namespace` (matching the engine's store namespace), so
|
|
845
|
+
* multiple logical deployments can share one broker. Idempotent. An explicit namespace passed to the
|
|
846
|
+
* transport's constructor TAKES PRECEDENCE over this. No-op when `namespace === "default"`. Optional —
|
|
847
|
+
* transports that don't partition simply omit it.
|
|
848
|
+
*/
|
|
849
|
+
useNamespace?(namespace: string): void;
|
|
843
850
|
/** Release the transport's resources (broker workers, queues, connections) for a clean shutdown.
|
|
844
851
|
* Optional — an in-process transport has nothing to close. Called on `onApplicationShutdown`
|
|
845
852
|
* after the engine drains, so a deploy hands off instead of leaving the broker to time out. */
|
|
@@ -2244,15 +2251,16 @@ declare function remoteStep<TInput, TOutput>(config: RemoteStepConfig<TInput, TO
|
|
|
2244
2251
|
|
|
2245
2252
|
/**
|
|
2246
2253
|
* A {@link WorkflowExecutor} backed by a {@link Transport}: it advances a remote workflow by
|
|
2247
|
-
* DISPATCHING a {@link WorkflowTask} over the broker
|
|
2254
|
+
* DISPATCHING a {@link WorkflowTask} over the broker under the ENGINE-SUPPLIED `taskId`, WITHOUT
|
|
2248
2255
|
* awaiting the decision. Pass one to `engine.registerRemote(name, version, { group, executor })` so a
|
|
2249
2256
|
* workflow authored in another SDK (e.g. the Python `durable-worker`) is driven over Redis/BullMQ.
|
|
2250
2257
|
*
|
|
2251
2258
|
* Multi-instance safe by construction: the executor holds NO in-memory state correlating a dispatched
|
|
2252
|
-
* turn to its decision. The engine
|
|
2253
|
-
* worker's {@link import('./interfaces').WorkflowDecision}
|
|
2254
|
-
* `onDecision` and applied DURABLY (by run id) on whatever engine
|
|
2255
|
-
*
|
|
2259
|
+
* turn to its decision. The engine records the `taskId` on the run as the awaited marker AND releases
|
|
2260
|
+
* the run's lease BEFORE calling `dispatch`, so the worker's {@link import('./interfaces').WorkflowDecision},
|
|
2261
|
+
* delivered over the transport's `onDecision` and applied DURABLY (by run id) on whatever engine
|
|
2262
|
+
* instance consumes it — even one that did not dispatch the turn — can never arrive ahead of its marker
|
|
2263
|
+
* or contend with a still-held lease and be dropped. A point-to-point broker can therefore hand the
|
|
2256
2264
|
* decision to any instance without it being dropped (the bug this design fixes).
|
|
2257
2265
|
*
|
|
2258
2266
|
* Recovery-safe: if the engine crashes (or a decision is genuinely lost because the worker died), the
|
|
@@ -2268,12 +2276,10 @@ declare class RemoteWorkflowExecutor implements WorkflowExecutor {
|
|
|
2268
2276
|
timeoutMs?: number;
|
|
2269
2277
|
});
|
|
2270
2278
|
/**
|
|
2271
|
-
*
|
|
2272
|
-
*
|
|
2279
|
+
* Enqueue one workflow turn under the engine-supplied `taskId`. Does NOT await the decision — the
|
|
2280
|
+
* engine has already suspended the run on `taskId` and applies the decision durably via `onDecision`.
|
|
2273
2281
|
*/
|
|
2274
|
-
dispatch(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<
|
|
2275
|
-
taskId: string;
|
|
2276
|
-
}>;
|
|
2282
|
+
dispatch(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
|
|
2277
2283
|
}
|
|
2278
2284
|
|
|
2279
2285
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -802,17 +802,17 @@ interface WorkflowExecutor {
|
|
|
802
802
|
*/
|
|
803
803
|
advance?(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<WorkflowDecision>;
|
|
804
804
|
/**
|
|
805
|
-
* DISPATCH-AND-SUSPEND (broker-backed, multi-instance safe):
|
|
806
|
-
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
805
|
+
* DISPATCH-AND-SUSPEND (broker-backed, multi-instance safe): enqueue the turn to a worker under the
|
|
806
|
+
* ENGINE-SUPPLIED `taskId`, WITHOUT awaiting the decision. The engine generates `taskId`, records it
|
|
807
|
+
* on the run as the awaited marker, AND releases the run's lease — all BEFORE calling this — so the
|
|
808
|
+
* worker's {@link WorkflowDecision} (delivered over {@link Transport.onDecision} and applied DURABLY
|
|
809
|
+
* by {@link WorkflowEngine.completeRemoteDecision}, which looks the run up by `decision.runId`) can
|
|
810
|
+
* never arrive ahead of its marker or contend with a still-held lease and be dropped. This method
|
|
811
|
+
* therefore ONLY enqueues — it owns no taskId generation and returns nothing. No in-memory await, so
|
|
812
|
+
* the decision is never lost to a point-to-point broker handing it to a non-dispatching instance.
|
|
813
|
+
* Provide this OR {@link advance} — exactly one.
|
|
814
|
+
*/
|
|
815
|
+
dispatch?(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
|
|
816
816
|
}
|
|
817
817
|
/**
|
|
818
818
|
* A transport in an ordered pool, identified by `id`. The engine dispatches on the first by default
|
|
@@ -840,6 +840,13 @@ interface Transport {
|
|
|
840
840
|
onResult(handler: (result: StepResult) => Promise<void>): void;
|
|
841
841
|
/** worker → engine: liveness signal for an in-flight long step. */
|
|
842
842
|
onHeartbeat(handler: (beat: Heartbeat) => Promise<void>): void;
|
|
843
|
+
/**
|
|
844
|
+
* Partition this transport's queues/keys by `namespace` (matching the engine's store namespace), so
|
|
845
|
+
* multiple logical deployments can share one broker. Idempotent. An explicit namespace passed to the
|
|
846
|
+
* transport's constructor TAKES PRECEDENCE over this. No-op when `namespace === "default"`. Optional —
|
|
847
|
+
* transports that don't partition simply omit it.
|
|
848
|
+
*/
|
|
849
|
+
useNamespace?(namespace: string): void;
|
|
843
850
|
/** Release the transport's resources (broker workers, queues, connections) for a clean shutdown.
|
|
844
851
|
* Optional — an in-process transport has nothing to close. Called on `onApplicationShutdown`
|
|
845
852
|
* after the engine drains, so a deploy hands off instead of leaving the broker to time out. */
|
|
@@ -2244,15 +2251,16 @@ declare function remoteStep<TInput, TOutput>(config: RemoteStepConfig<TInput, TO
|
|
|
2244
2251
|
|
|
2245
2252
|
/**
|
|
2246
2253
|
* A {@link WorkflowExecutor} backed by a {@link Transport}: it advances a remote workflow by
|
|
2247
|
-
* DISPATCHING a {@link WorkflowTask} over the broker
|
|
2254
|
+
* DISPATCHING a {@link WorkflowTask} over the broker under the ENGINE-SUPPLIED `taskId`, WITHOUT
|
|
2248
2255
|
* awaiting the decision. Pass one to `engine.registerRemote(name, version, { group, executor })` so a
|
|
2249
2256
|
* workflow authored in another SDK (e.g. the Python `durable-worker`) is driven over Redis/BullMQ.
|
|
2250
2257
|
*
|
|
2251
2258
|
* Multi-instance safe by construction: the executor holds NO in-memory state correlating a dispatched
|
|
2252
|
-
* turn to its decision. The engine
|
|
2253
|
-
* worker's {@link import('./interfaces').WorkflowDecision}
|
|
2254
|
-
* `onDecision` and applied DURABLY (by run id) on whatever engine
|
|
2255
|
-
*
|
|
2259
|
+
* turn to its decision. The engine records the `taskId` on the run as the awaited marker AND releases
|
|
2260
|
+
* the run's lease BEFORE calling `dispatch`, so the worker's {@link import('./interfaces').WorkflowDecision},
|
|
2261
|
+
* delivered over the transport's `onDecision` and applied DURABLY (by run id) on whatever engine
|
|
2262
|
+
* instance consumes it — even one that did not dispatch the turn — can never arrive ahead of its marker
|
|
2263
|
+
* or contend with a still-held lease and be dropped. A point-to-point broker can therefore hand the
|
|
2256
2264
|
* decision to any instance without it being dropped (the bug this design fixes).
|
|
2257
2265
|
*
|
|
2258
2266
|
* Recovery-safe: if the engine crashes (or a decision is genuinely lost because the worker died), the
|
|
@@ -2268,12 +2276,10 @@ declare class RemoteWorkflowExecutor implements WorkflowExecutor {
|
|
|
2268
2276
|
timeoutMs?: number;
|
|
2269
2277
|
});
|
|
2270
2278
|
/**
|
|
2271
|
-
*
|
|
2272
|
-
*
|
|
2279
|
+
* Enqueue one workflow turn under the engine-supplied `taskId`. Does NOT await the decision — the
|
|
2280
|
+
* engine has already suspended the run on `taskId` and applies the decision durably via `onDecision`.
|
|
2273
2281
|
*/
|
|
2274
|
-
dispatch(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<
|
|
2275
|
-
taskId: string;
|
|
2276
|
-
}>;
|
|
2282
|
+
dispatch(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
|
|
2277
2283
|
}
|
|
2278
2284
|
|
|
2279
2285
|
/**
|
package/dist/index.js
CHANGED
|
@@ -701,7 +701,6 @@ function eventMatches(payload, match) {
|
|
|
701
701
|
__name(eventMatches, "eventMatches");
|
|
702
702
|
|
|
703
703
|
// src/remote-workflow-executor.ts
|
|
704
|
-
var taskCounter = 0;
|
|
705
704
|
var RemoteWorkflowExecutor = class {
|
|
706
705
|
static {
|
|
707
706
|
__name(this, "RemoteWorkflowExecutor");
|
|
@@ -713,15 +712,13 @@ var RemoteWorkflowExecutor = class {
|
|
|
713
712
|
this.group = group;
|
|
714
713
|
}
|
|
715
714
|
/**
|
|
716
|
-
*
|
|
717
|
-
*
|
|
715
|
+
* Enqueue one workflow turn under the engine-supplied `taskId`. Does NOT await the decision — the
|
|
716
|
+
* engine has already suspended the run on `taskId` and applies the decision durably via `onDecision`.
|
|
718
717
|
*/
|
|
719
|
-
async dispatch(run, history, pendingSignals) {
|
|
718
|
+
async dispatch(run, history, taskId, pendingSignals) {
|
|
720
719
|
if (!this.transport.dispatchWorkflowTask) {
|
|
721
720
|
throw new Error("transport does not support workflow tasks (dispatchWorkflowTask)");
|
|
722
721
|
}
|
|
723
|
-
taskCounter += 1;
|
|
724
|
-
const taskId = `${run.id}:wf:${taskCounter}`;
|
|
725
722
|
const task = {
|
|
726
723
|
taskId,
|
|
727
724
|
runId: run.id,
|
|
@@ -737,9 +734,6 @@ var RemoteWorkflowExecutor = class {
|
|
|
737
734
|
attempt: 1
|
|
738
735
|
};
|
|
739
736
|
await this.transport.dispatchWorkflowTask(task);
|
|
740
|
-
return {
|
|
741
|
-
taskId
|
|
742
|
-
};
|
|
743
737
|
}
|
|
744
738
|
};
|
|
745
739
|
|
|
@@ -919,6 +913,17 @@ var TransportPool = class {
|
|
|
919
913
|
...groups
|
|
920
914
|
];
|
|
921
915
|
}
|
|
916
|
+
/**
|
|
917
|
+
* Propagate the engine's `namespace` to every transport that partitions by it — so the same
|
|
918
|
+
* namespace that scopes the store also scopes each transport's queues/keys. A transport that
|
|
919
|
+
* doesn't partition (no `useNamespace`) is skipped. Idempotent; a transport given an explicit
|
|
920
|
+
* namespace at construction ignores this (see {@link Transport.useNamespace}).
|
|
921
|
+
*/
|
|
922
|
+
useNamespace(namespace) {
|
|
923
|
+
for (const { transport } of this.transports) {
|
|
924
|
+
transport.useNamespace?.(namespace);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
922
927
|
/** Pinned `preferId` first, then the rest (failover order). */
|
|
923
928
|
ordered(preferId) {
|
|
924
929
|
if (!preferId) return this.transports;
|
|
@@ -1586,6 +1591,7 @@ var WorkflowEngine = class {
|
|
|
1586
1591
|
this.admission.onFreed?.((queue) => this.wakeQueueWaiters(queue));
|
|
1587
1592
|
this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
|
|
1588
1593
|
this.namespace = deps.namespace ?? "default";
|
|
1594
|
+
this.pool.useNamespace(this.namespace);
|
|
1589
1595
|
this.leaseMs = deps.leaseMs ?? 3e4;
|
|
1590
1596
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
1591
1597
|
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
@@ -2928,19 +2934,9 @@ var WorkflowEngine = class {
|
|
|
2928
2934
|
}
|
|
2929
2935
|
const history = await this.remoteHistory(run.id);
|
|
2930
2936
|
if (remote.executor.dispatch) {
|
|
2931
|
-
const
|
|
2937
|
+
const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
|
|
2932
2938
|
const silenceMs = this.remoteAdvanceSilenceMs;
|
|
2933
2939
|
const wakeAt = silenceMs != null ? this.clock() + silenceMs : void 0;
|
|
2934
|
-
const fresh = await this.store.getRun(run.id);
|
|
2935
|
-
if (fresh && (fresh.status === "cancelled" || fresh.status === "completed" || fresh.status === "dead")) {
|
|
2936
|
-
await this.store.releaseRunLock(run.id);
|
|
2937
|
-
return {
|
|
2938
|
-
runId: run.id,
|
|
2939
|
-
status: fresh.status,
|
|
2940
|
-
output: fresh.output,
|
|
2941
|
-
error: fresh.error
|
|
2942
|
-
};
|
|
2943
|
-
}
|
|
2944
2940
|
await this.store.updateRun(run.id, {
|
|
2945
2941
|
status: "suspended",
|
|
2946
2942
|
awaitingDecisionTaskId: taskId,
|
|
@@ -2953,6 +2949,7 @@ var WorkflowEngine = class {
|
|
|
2953
2949
|
workflow: run.workflow
|
|
2954
2950
|
});
|
|
2955
2951
|
await this.store.releaseRunLock(run.id);
|
|
2952
|
+
await remote.executor.dispatch(run, history, taskId);
|
|
2956
2953
|
return {
|
|
2957
2954
|
runId: run.id,
|
|
2958
2955
|
status: "suspended"
|