@dudousxd/nestjs-durable-core 0.49.3 → 0.50.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 +155 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +240 -8
- package/dist/index.d.ts +240 -8
- package/dist/index.js +154 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -71,6 +71,7 @@ __export(index_exports, {
|
|
|
71
71
|
normalizeAttributeRows: () => normalizeAttributeRows,
|
|
72
72
|
parseDuration: () => parseDuration,
|
|
73
73
|
prevCronFireMs: () => prevCronFireMs,
|
|
74
|
+
readSearchAttributes: () => readSearchAttributes,
|
|
74
75
|
resolveRunWaiting: () => resolveRunWaiting,
|
|
75
76
|
runInWorkflowCtx: () => runInWorkflowCtx,
|
|
76
77
|
runSchedules: () => runSchedules,
|
|
@@ -1185,6 +1186,11 @@ function normalizeChildOptions(options) {
|
|
|
1185
1186
|
} : options ?? {};
|
|
1186
1187
|
}
|
|
1187
1188
|
__name(normalizeChildOptions, "normalizeChildOptions");
|
|
1189
|
+
function formatIssuePath(path) {
|
|
1190
|
+
if (!path || path.length === 0) return "(root)";
|
|
1191
|
+
return path.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
|
|
1192
|
+
}
|
|
1193
|
+
__name(formatIssuePath, "formatIssuePath");
|
|
1188
1194
|
function resolveStepPolicy(config, opts) {
|
|
1189
1195
|
const policy = {};
|
|
1190
1196
|
const retries = opts?.retries ?? config?.retries;
|
|
@@ -1215,7 +1221,7 @@ var Position = class Position2 {
|
|
|
1215
1221
|
this.seq -= 1;
|
|
1216
1222
|
}
|
|
1217
1223
|
};
|
|
1218
|
-
function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
1224
|
+
function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttributesSchema) {
|
|
1219
1225
|
const { store, replay } = host;
|
|
1220
1226
|
const pos = new Position();
|
|
1221
1227
|
const readCheckpoint = /* @__PURE__ */ __name((seq) => {
|
|
@@ -1684,6 +1690,24 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1684
1690
|
}
|
|
1685
1691
|
return;
|
|
1686
1692
|
}
|
|
1693
|
+
if (searchAttributesSchema) {
|
|
1694
|
+
const run = await store.getRun(runId);
|
|
1695
|
+
const merged = {
|
|
1696
|
+
...run?.searchAttributes ?? {},
|
|
1697
|
+
...attrs
|
|
1698
|
+
};
|
|
1699
|
+
const result = searchAttributesSchema["~standard"].validate(merged);
|
|
1700
|
+
if (result instanceof Promise) {
|
|
1701
|
+
throw new Error(`workflow "${workflow}": ctx.upsertSearchAttributes schema validation must be synchronous (the searchAttributes schema's \`~standard.validate\` returned a Promise) \u2014 replay-determinism requires a sync validator; drop any async refinement from the schema passed to @Workflow({ searchAttributes }).`);
|
|
1702
|
+
}
|
|
1703
|
+
if (result.issues) {
|
|
1704
|
+
const offendingKeys = [
|
|
1705
|
+
...new Set(result.issues.map((issue) => formatIssuePath(issue.path)))
|
|
1706
|
+
];
|
|
1707
|
+
const details = result.issues.map((issue) => `${formatIssuePath(issue.path)}: ${issue.message}`).join("; ");
|
|
1708
|
+
throw new Error(`workflow "${workflow}": ctx.upsertSearchAttributes rejected by its searchAttributes schema \u2014 offending key(s) ${offendingKeys.join(", ")} \u2014 ${details}`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1687
1711
|
await host.upsertSearchAttributes(runId, attrs);
|
|
1688
1712
|
await writeCheckpoint(instantCheckpoint({
|
|
1689
1713
|
runId,
|
|
@@ -1814,6 +1838,8 @@ var WorkflowEngine = class {
|
|
|
1814
1838
|
maxRecoveryAttempts;
|
|
1815
1839
|
remoteAdvanceSilenceMs;
|
|
1816
1840
|
reconcileMs;
|
|
1841
|
+
remoteRedispatchMs;
|
|
1842
|
+
remoteRedispatchMax;
|
|
1817
1843
|
webhookUrl;
|
|
1818
1844
|
traceparent;
|
|
1819
1845
|
context;
|
|
@@ -1885,6 +1911,8 @@ var WorkflowEngine = class {
|
|
|
1885
1911
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
1886
1912
|
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
1887
1913
|
this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
|
|
1914
|
+
this.remoteRedispatchMs = deps.remoteRedispatchMs || void 0;
|
|
1915
|
+
this.remoteRedispatchMax = Math.max(1, deps.remoteRedispatchMax ?? 10);
|
|
1888
1916
|
this.webhookUrl = deps.webhookUrl;
|
|
1889
1917
|
this.traceparent = deps.traceparent;
|
|
1890
1918
|
this.context = deps.context;
|
|
@@ -2035,6 +2063,7 @@ var WorkflowEngine = class {
|
|
|
2035
2063
|
singleton: opts?.singleton,
|
|
2036
2064
|
executionTimeoutMs: opts?.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
|
|
2037
2065
|
validateInput: opts?.validateInput,
|
|
2066
|
+
searchAttributesSchema: opts?.searchAttributesSchema,
|
|
2038
2067
|
onEvent: opts?.onEvent,
|
|
2039
2068
|
eventBatch: opts?.eventBatch,
|
|
2040
2069
|
// A real, retained in-process body — so `workflowBody` can hand it to an in-app worker, and so
|
|
@@ -2596,6 +2625,49 @@ var WorkflowEngine = class {
|
|
|
2596
2625
|
};
|
|
2597
2626
|
}
|
|
2598
2627
|
/**
|
|
2628
|
+
* Explicitly re-dispatch every remote step of `runId` stuck `pending` — the OPERATOR escape hatch for
|
|
2629
|
+
* a run whose dispatched step job was LOST (worker crashed with no result, or a transport that dropped
|
|
2630
|
+
* the job). No automatic recovery re-drives these: a reconcile re-drive re-suspends a still-pending
|
|
2631
|
+
* step by design (unless `remoteRedispatchMs` is set), `recoverIncomplete` only reclaims LEASED runs,
|
|
2632
|
+
* and `requeue` just replays back to the pending guard. This re-enqueues the same `stepId` so the
|
|
2633
|
+
* (idempotent) step re-runs and its result resumes the run. Safe to call on a healthy run — it re-runs
|
|
2634
|
+
* only in-flight `pending` remote steps, which are idempotent by the durable contract. Returns the
|
|
2635
|
+
* run's current status (with the count re-dispatched), or null if the run is unknown.
|
|
2636
|
+
*/
|
|
2637
|
+
async redispatchPending(runId) {
|
|
2638
|
+
const run = await this.store.getRun(runId);
|
|
2639
|
+
if (!run) return null;
|
|
2640
|
+
const pending = (await this.store.listCheckpoints(runId)).filter((cp) => cp.kind === "remote" && cp.status === "pending");
|
|
2641
|
+
let redispatched = 0;
|
|
2642
|
+
for (const cp of pending) {
|
|
2643
|
+
if (!cp.stepId) continue;
|
|
2644
|
+
const reAttempt = cp.attempts + 1;
|
|
2645
|
+
const reEnqueuedAt = /* @__PURE__ */ new Date();
|
|
2646
|
+
await this.store.saveCheckpoint({
|
|
2647
|
+
...cp,
|
|
2648
|
+
attempts: reAttempt,
|
|
2649
|
+
enqueuedAt: reEnqueuedAt,
|
|
2650
|
+
startedAt: reEnqueuedAt,
|
|
2651
|
+
finishedAt: reEnqueuedAt
|
|
2652
|
+
});
|
|
2653
|
+
await this.dispatchRemoteTask({
|
|
2654
|
+
runId,
|
|
2655
|
+
seq: cp.seq,
|
|
2656
|
+
name: cp.name,
|
|
2657
|
+
stepId: cp.stepId,
|
|
2658
|
+
group: cp.workerGroup ?? sanitizeQueueToken(cp.name),
|
|
2659
|
+
input: cp.input,
|
|
2660
|
+
attempt: reAttempt
|
|
2661
|
+
});
|
|
2662
|
+
redispatched += 1;
|
|
2663
|
+
}
|
|
2664
|
+
return {
|
|
2665
|
+
runId,
|
|
2666
|
+
status: run.status,
|
|
2667
|
+
redispatched
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2599
2671
|
* **Fix-and-replay**: re-run a run (typically a `dead`/`failed` one) with a corrected `input`, as a
|
|
2600
2672
|
* fresh run with clean history. It's a NEW run — `newRunId` defaults to `<runId>~retry~<uuid>` — so
|
|
2601
2673
|
* the original stays inspectable. Returns the new run's id, or null if `runId` is unknown.
|
|
@@ -3691,7 +3763,7 @@ var WorkflowEngine = class {
|
|
|
3691
3763
|
const snapshot = await this.store.listCheckpoints(run.id);
|
|
3692
3764
|
const replay = /* @__PURE__ */ new Map();
|
|
3693
3765
|
for (const cp of snapshot) replay.set(cp.seq, cp);
|
|
3694
|
-
const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow);
|
|
3766
|
+
const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow, registered?.searchAttributesSchema);
|
|
3695
3767
|
try {
|
|
3696
3768
|
const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
|
|
3697
3769
|
if (this.cancelRequested.has(run.id)) {
|
|
@@ -4083,7 +4155,47 @@ var WorkflowEngine = class {
|
|
|
4083
4155
|
if (existing?.status === "completed") return existing.output;
|
|
4084
4156
|
if (this.pool.size === 0) throw new Error("remote steps require a Transport");
|
|
4085
4157
|
if (step.timeoutMs) return this.callRemoteInMemory(runId, seq, step, input, transport);
|
|
4086
|
-
if (existing?.status === "pending")
|
|
4158
|
+
if (existing?.status === "pending") {
|
|
4159
|
+
if (this.remoteRedispatchMs == null) throw new WorkflowSuspended();
|
|
4160
|
+
if (existing.wakeAt == null) {
|
|
4161
|
+
const wakeAt = this.clock() + this.remoteRedispatchMs;
|
|
4162
|
+
await this.store.saveCheckpoint({
|
|
4163
|
+
...existing,
|
|
4164
|
+
wakeAt
|
|
4165
|
+
});
|
|
4166
|
+
throw new WorkflowSuspended(wakeAt);
|
|
4167
|
+
}
|
|
4168
|
+
if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
|
|
4169
|
+
if (existing.attempts >= this.remoteRedispatchMax) {
|
|
4170
|
+
throw new RemoteStepError({
|
|
4171
|
+
message: `remote step "${step.name}" lost \u2014 no result after ${existing.attempts} re-dispatch(es)`,
|
|
4172
|
+
code: "remote_step_lost"
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
const reAttempt = existing.attempts + 1;
|
|
4176
|
+
const nextDeadline = this.clock() + this.remoteRedispatchMs;
|
|
4177
|
+
const reEnqueuedAt = /* @__PURE__ */ new Date();
|
|
4178
|
+
await this.store.saveCheckpoint({
|
|
4179
|
+
...existing,
|
|
4180
|
+
attempts: reAttempt,
|
|
4181
|
+
wakeAt: nextDeadline,
|
|
4182
|
+
enqueuedAt: reEnqueuedAt,
|
|
4183
|
+
startedAt: reEnqueuedAt,
|
|
4184
|
+
finishedAt: reEnqueuedAt
|
|
4185
|
+
});
|
|
4186
|
+
await this.dispatchRemoteTask({
|
|
4187
|
+
runId,
|
|
4188
|
+
seq,
|
|
4189
|
+
name: step.name,
|
|
4190
|
+
stepId: existing.stepId ?? stepId(runId, seq),
|
|
4191
|
+
group: existing.workerGroup ?? tenantGroup(sanitizeQueueToken(step.name), step.partition),
|
|
4192
|
+
input: existing.input,
|
|
4193
|
+
priority: admission?.priority,
|
|
4194
|
+
attempt: reAttempt,
|
|
4195
|
+
transport
|
|
4196
|
+
});
|
|
4197
|
+
throw new WorkflowSuspended(nextDeadline);
|
|
4198
|
+
}
|
|
4087
4199
|
let attempt = 1;
|
|
4088
4200
|
if (existing?.status === "failed") {
|
|
4089
4201
|
const maxAttempts = Math.max(1, step.retries ?? 1);
|
|
@@ -4131,18 +4243,17 @@ var WorkflowEngine = class {
|
|
|
4131
4243
|
startedAt: enqueuedAt,
|
|
4132
4244
|
finishedAt: enqueuedAt
|
|
4133
4245
|
});
|
|
4134
|
-
await this.
|
|
4246
|
+
await this.dispatchRemoteTask({
|
|
4135
4247
|
runId,
|
|
4136
4248
|
seq,
|
|
4137
4249
|
name: step.name,
|
|
4138
4250
|
stepId: id,
|
|
4139
4251
|
group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
|
|
4140
4252
|
input,
|
|
4141
|
-
traceparent: this.traceparent?.(),
|
|
4142
|
-
context: this.context?.(),
|
|
4143
4253
|
priority: admission?.priority,
|
|
4144
|
-
attempt
|
|
4145
|
-
|
|
4254
|
+
attempt,
|
|
4255
|
+
transport
|
|
4256
|
+
});
|
|
4146
4257
|
this.emit({
|
|
4147
4258
|
type: "step.started",
|
|
4148
4259
|
runId,
|
|
@@ -4152,6 +4263,23 @@ var WorkflowEngine = class {
|
|
|
4152
4263
|
});
|
|
4153
4264
|
throw new WorkflowSuspended();
|
|
4154
4265
|
}
|
|
4266
|
+
/** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
|
|
4267
|
+
* dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
|
|
4268
|
+
* {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
|
|
4269
|
+
dispatchRemoteTask(task) {
|
|
4270
|
+
return this.pool.dispatch({
|
|
4271
|
+
runId: task.runId,
|
|
4272
|
+
seq: task.seq,
|
|
4273
|
+
name: task.name,
|
|
4274
|
+
stepId: task.stepId,
|
|
4275
|
+
group: task.group,
|
|
4276
|
+
input: task.input,
|
|
4277
|
+
traceparent: this.traceparent?.(),
|
|
4278
|
+
context: this.context?.(),
|
|
4279
|
+
priority: task.priority,
|
|
4280
|
+
attempt: task.attempt
|
|
4281
|
+
}, task.transport);
|
|
4282
|
+
}
|
|
4155
4283
|
/**
|
|
4156
4284
|
* Complete a durable remote step from its worker result and resume the run — runs on whichever
|
|
4157
4285
|
* instance receives the result (the dispatching one may be gone), so the run is crash/scale-safe.
|
|
@@ -4391,12 +4519,17 @@ var TERMINAL_RUN_STATUSES = [
|
|
|
4391
4519
|
// src/run-waiting.ts
|
|
4392
4520
|
var WEBHOOK_PREFIX = "wh:";
|
|
4393
4521
|
var CHILD_PREFIX = "child:";
|
|
4522
|
+
var BREAKPOINT_PREFIX = "bp:";
|
|
4394
4523
|
var EVENT_PREFIX = "event:";
|
|
4395
4524
|
function classifyWaiterToken(token) {
|
|
4396
4525
|
if (token.startsWith(WEBHOOK_PREFIX)) return {
|
|
4397
4526
|
on: "webhook",
|
|
4398
4527
|
name: token
|
|
4399
4528
|
};
|
|
4529
|
+
if (token.startsWith(BREAKPOINT_PREFIX)) return {
|
|
4530
|
+
on: "breakpoint",
|
|
4531
|
+
name: "breakpoint"
|
|
4532
|
+
};
|
|
4400
4533
|
if (token.startsWith(CHILD_PREFIX)) return {
|
|
4401
4534
|
on: "child",
|
|
4402
4535
|
name: token.slice(CHILD_PREFIX.length)
|
|
@@ -4817,6 +4950,19 @@ function attributeOperand(filter) {
|
|
|
4817
4950
|
}
|
|
4818
4951
|
__name(attributeOperand, "attributeOperand");
|
|
4819
4952
|
|
|
4953
|
+
// src/search-attributes-schema.ts
|
|
4954
|
+
function readSearchAttributes(schema, run) {
|
|
4955
|
+
const result = schema["~standard"].validate(run.searchAttributes ?? {});
|
|
4956
|
+
if (result instanceof Promise) {
|
|
4957
|
+
throw new Error("readSearchAttributes: schema validation must be synchronous (the searchAttributes schema's `~standard.validate` returned a Promise) \u2014 async Standard Schema validators are not supported.");
|
|
4958
|
+
}
|
|
4959
|
+
if (result.issues) {
|
|
4960
|
+
return {};
|
|
4961
|
+
}
|
|
4962
|
+
return result.value;
|
|
4963
|
+
}
|
|
4964
|
+
__name(readSearchAttributes, "readSearchAttributes");
|
|
4965
|
+
|
|
4820
4966
|
// src/tokens.ts
|
|
4821
4967
|
var STATE_STORE = Symbol.for("nestjs-durable:STATE_STORE");
|
|
4822
4968
|
var TRANSPORT = Symbol.for("nestjs-durable:TRANSPORT");
|
|
@@ -5193,6 +5339,7 @@ var InMemoryTransport = class {
|
|
|
5193
5339
|
normalizeAttributeRows,
|
|
5194
5340
|
parseDuration,
|
|
5195
5341
|
prevCronFireMs,
|
|
5342
|
+
readSearchAttributes,
|
|
5196
5343
|
resolveRunWaiting,
|
|
5197
5344
|
runInWorkflowCtx,
|
|
5198
5345
|
runSchedules,
|