@dudousxd/nestjs-durable-core 0.37.0 → 0.39.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 +146 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +146 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1434,9 +1434,28 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1434
1434
|
const now = /* @__PURE__ */ __name(() => step("now", async () => host.clock()), "now");
|
|
1435
1435
|
const random = /* @__PURE__ */ __name(() => step("random", async () => Math.random()), "random");
|
|
1436
1436
|
const uuid = /* @__PURE__ */ __name(() => step("uuid", async () => globalThis.crypto.randomUUID()), "uuid");
|
|
1437
|
+
const upsertSearchAttributes = /* @__PURE__ */ __name(async (attrs) => {
|
|
1438
|
+
const current = pos.next();
|
|
1439
|
+
const existing = await readCheckpoint(current);
|
|
1440
|
+
if (existing) {
|
|
1441
|
+
if (existing.name !== "searchAttributes") {
|
|
1442
|
+
throw new NonDeterminismError(runId, current, "searchAttributes", existing.name);
|
|
1443
|
+
}
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
await host.upsertSearchAttributes(runId, attrs);
|
|
1447
|
+
await writeCheckpoint(instantCheckpoint({
|
|
1448
|
+
runId,
|
|
1449
|
+
seq: current,
|
|
1450
|
+
name: "searchAttributes",
|
|
1451
|
+
kind: "local",
|
|
1452
|
+
output: attrs
|
|
1453
|
+
}));
|
|
1454
|
+
}, "upsertSearchAttributes");
|
|
1437
1455
|
return {
|
|
1438
1456
|
runId,
|
|
1439
1457
|
step,
|
|
1458
|
+
upsertSearchAttributes,
|
|
1440
1459
|
transaction,
|
|
1441
1460
|
callEntity,
|
|
1442
1461
|
signalEntity,
|
|
@@ -1679,8 +1698,21 @@ var WorkflowEngine = class {
|
|
|
1679
1698
|
* Register a workflow version. Register multiple versions of the same name to keep in-flight
|
|
1680
1699
|
* runs working across a breaking change: old runs resume on the version they started on, new
|
|
1681
1700
|
* runs start on the newest registered version.
|
|
1701
|
+
*
|
|
1702
|
+
* GROUP-SERVED (uniform dispatch, opt-in): pass `{ group, executor }` to have the engine DISPATCH
|
|
1703
|
+
* this workflow's turns to a worker `group` (via `executor.advance`) instead of running `fn`
|
|
1704
|
+
* inline — exactly as {@link registerRemote} does for a cross-SDK (e.g. Python) body, EXCEPT the
|
|
1705
|
+
* body is a TS `fn` the engine retains so an IN-APP worker on `group` can fetch it by name
|
|
1706
|
+
* ({@link workflowBody}) and run it. This is "one app, both roles": the same process owns the
|
|
1707
|
+
* engine AND consumes its own group. Recovery, timers, singleton, cancel and dead-lettering stay
|
|
1708
|
+
* engine concerns, identical to a remote run. OMIT `group`/`executor` (the default) to keep the
|
|
1709
|
+
* inline fast path — `fn` runs in-process and the run pays zero dispatch round-trips. The first
|
|
1710
|
+
* step toward routing every run through a group with no local/remote flag (Phase 3).
|
|
1682
1711
|
*/
|
|
1683
1712
|
register(name, version, fn, opts) {
|
|
1713
|
+
if (opts?.group == null !== (opts?.executor == null)) {
|
|
1714
|
+
throw new Error(`workflow ${name}@${version}: register({ group, executor }) needs BOTH group and executor (or neither)`);
|
|
1715
|
+
}
|
|
1684
1716
|
const registered = {
|
|
1685
1717
|
name,
|
|
1686
1718
|
version,
|
|
@@ -1690,8 +1722,17 @@ var WorkflowEngine = class {
|
|
|
1690
1722
|
executionTimeoutMs: opts?.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
|
|
1691
1723
|
validateInput: opts?.validateInput,
|
|
1692
1724
|
onEvent: opts?.onEvent,
|
|
1693
|
-
eventBatch: opts?.eventBatch
|
|
1725
|
+
eventBatch: opts?.eventBatch,
|
|
1726
|
+
// A real, retained in-process body — so `workflowBody` can hand it to an in-app worker, and so
|
|
1727
|
+
// it is NOT confused with the throwing stub `registerRemote` installs for a body-less remote.
|
|
1728
|
+
hasBody: true
|
|
1694
1729
|
};
|
|
1730
|
+
if (opts?.group != null && opts?.executor != null) {
|
|
1731
|
+
registered.remote = {
|
|
1732
|
+
group: opts.group,
|
|
1733
|
+
executor: opts.executor
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1695
1736
|
this.workflows.set(versionKey(name, version), registered);
|
|
1696
1737
|
const current = this.latest.get(name);
|
|
1697
1738
|
if (!current || isNewerVersion(version, current.version)) this.latest.set(name, registered);
|
|
@@ -1702,6 +1743,17 @@ var WorkflowEngine = class {
|
|
|
1702
1743
|
}
|
|
1703
1744
|
}
|
|
1704
1745
|
/**
|
|
1746
|
+
* The retained in-process body for `name@version`, for an IN-APP worker that consumes this engine's
|
|
1747
|
+
* own group to SERVE a group-served `register(..., { group, executor })` workflow (uniform dispatch):
|
|
1748
|
+
* the worker fetches the body by name when it picks up the dispatched {@link WorkflowTask}, replays
|
|
1749
|
+
* it, and returns the {@link WorkflowDecision}. `undefined` for an unregistered name or a body-less
|
|
1750
|
+
* remote (a `registerRemote` / synthesized child carries only a throwing stub, never handed out).
|
|
1751
|
+
*/
|
|
1752
|
+
workflowBody(name, version) {
|
|
1753
|
+
const reg = this.workflows.get(versionKey(name, version));
|
|
1754
|
+
return reg?.hasBody ? reg.fn : void 0;
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1705
1757
|
* Register a workflow whose body runs in another SDK (e.g. Python). The engine owns the run exactly
|
|
1706
1758
|
* as for a TS workflow — it persists checkpoints, recovers, runs timers — but advances it by handing
|
|
1707
1759
|
* the run's history to `executor` (which dispatches a {@link WorkflowTask} to the worker) and applying
|
|
@@ -1827,7 +1879,11 @@ var WorkflowEngine = class {
|
|
|
1827
1879
|
}
|
|
1828
1880
|
async start(workflow, input, runId, opts) {
|
|
1829
1881
|
const name = workflowName(workflow);
|
|
1830
|
-
|
|
1882
|
+
let registered = this.latest.get(name);
|
|
1883
|
+
if (!registered) {
|
|
1884
|
+
const ancestor = await this.findRemoteAncestor(runId);
|
|
1885
|
+
if (ancestor) registered = this.synthesizeRemoteChild(name, ancestor.version, ancestor);
|
|
1886
|
+
}
|
|
1831
1887
|
if (!registered) throw new Error(`workflow ${name} is not registered`);
|
|
1832
1888
|
await registered.validateInput?.(input);
|
|
1833
1889
|
const prior = await this.store.getRun(runId);
|
|
@@ -1888,12 +1944,87 @@ var WorkflowEngine = class {
|
|
|
1888
1944
|
error: run.error
|
|
1889
1945
|
};
|
|
1890
1946
|
}
|
|
1891
|
-
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion));
|
|
1947
|
+
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run);
|
|
1892
1948
|
if (!registered) {
|
|
1893
1949
|
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)`);
|
|
1894
1950
|
}
|
|
1895
1951
|
return this.track(this.execute(run, registered.fn));
|
|
1896
1952
|
}
|
|
1953
|
+
/**
|
|
1954
|
+
* The registration for an UNREGISTERED run, INHERITED from its nearest REMOTE ancestor — or
|
|
1955
|
+
* `undefined` if it has none (the skew-protection error case in {@link resume}). Callers check the
|
|
1956
|
+
* flat registry FIRST (so an explicit `registerRemote(name, …)` ALWAYS wins, and a registered run
|
|
1957
|
+
* never reaches here); this resolves only the new case: a child spawned by a remote workflow (e.g. a
|
|
1958
|
+
* Python `gather_children` / `start_child` fan-out) of a name the host never `registerRemote`-ed. It
|
|
1959
|
+
* is driven as a remote run on the ancestor's group + the SAME executor instance, so the host needs
|
|
1960
|
+
* no redundant registration per child name.
|
|
1961
|
+
*
|
|
1962
|
+
* RECOMPUTED per resume rather than memoized into `this.workflows`: a synthesized child is not a real
|
|
1963
|
+
* registration and must not leak into `latest`, `knownGroups`, or `sweepTimeouts` (which iterate the
|
|
1964
|
+
* registry); recomputing also stays correct if the ancestor's remote registration is re-pointed at a
|
|
1965
|
+
* new executor across a deploy. Only unregistered runs pay it, so registered workflows pay nothing.
|
|
1966
|
+
*/
|
|
1967
|
+
async findInheritedRegistration(run) {
|
|
1968
|
+
const ancestor = await this.findRemoteAncestor(run.id);
|
|
1969
|
+
if (!ancestor) return void 0;
|
|
1970
|
+
return this.synthesizeRemoteChild(run.workflow, run.workflowVersion, ancestor);
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* A throwaway {@link RegisteredWorkflow} that routes `name@version` as a remote run on the ancestor's
|
|
1974
|
+
* group via the ancestor's SAME executor instance. The executor is group-scoped (it dispatches a
|
|
1975
|
+
* {@link WorkflowTask} carrying `run.workflow`, and the worker picks the body by name), so reusing it
|
|
1976
|
+
* is correct AND required — a second executor on the same group would race the worker for results.
|
|
1977
|
+
* Only `remote` is inherited; the child gets none of the ancestor's singleton/timeout/validator, so
|
|
1978
|
+
* its identity stays its own.
|
|
1979
|
+
*/
|
|
1980
|
+
synthesizeRemoteChild(name, version, ancestor) {
|
|
1981
|
+
const remote = ancestor.remote;
|
|
1982
|
+
if (!remote) throw new Error(`workflow ${ancestor.name} is not a remote workflow`);
|
|
1983
|
+
return {
|
|
1984
|
+
name,
|
|
1985
|
+
version,
|
|
1986
|
+
fn: /* @__PURE__ */ __name(() => {
|
|
1987
|
+
throw new Error(`workflow ${name} is remote \u2014 it has no in-process body`);
|
|
1988
|
+
}, "fn"),
|
|
1989
|
+
remote
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* Walk the parent chain of `childRunId` to the nearest ancestor registered as REMOTE, returning its
|
|
1994
|
+
* registration (or `undefined` if there is none). The parent of an awaited child is found via its
|
|
1995
|
+
* live `child:<childId>` signal waiter — present for as long as the parent is suspended awaiting the
|
|
1996
|
+
* child, i.e. throughout the child's executable lifetime (start, every resume, crash recovery, cancel
|
|
1997
|
+
* cascade), which is exactly when we resolve. A registered-but-LOCAL parent stops the walk (an
|
|
1998
|
+
* unregistered child of a TS workflow is a genuine misconfiguration → skew error); only an
|
|
1999
|
+
* unregistered parent keeps walking, so a child of an inherited child still reaches the remote root.
|
|
2000
|
+
* `visited` guards against a pathological cyclic id graph.
|
|
2001
|
+
*/
|
|
2002
|
+
async findRemoteAncestor(childRunId, visited = /* @__PURE__ */ new Set()) {
|
|
2003
|
+
if (visited.has(childRunId)) return void 0;
|
|
2004
|
+
visited.add(childRunId);
|
|
2005
|
+
const parentRunId = await this.findParentRunId(childRunId);
|
|
2006
|
+
if (!parentRunId) return void 0;
|
|
2007
|
+
const parent = await this.store.getRun(parentRunId);
|
|
2008
|
+
if (!parent) return void 0;
|
|
2009
|
+
const parentReg = this.workflows.get(versionKey(parent.workflow, parent.workflowVersion));
|
|
2010
|
+
if (parentReg?.remote) return parentReg;
|
|
2011
|
+
if (parentReg) return void 0;
|
|
2012
|
+
return this.findRemoteAncestor(parent.id, visited);
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* The run that spawned `childRunId`, via its live `child:<childRunId>` signal waiter — every awaited
|
|
2016
|
+
* child (`ctx.child`, `ctx.all`, a remote `start_child`) suspends its parent on exactly this waiter,
|
|
2017
|
+
* and the waiter is written BEFORE the child is started. Only reached for an UNREGISTERED child, so
|
|
2018
|
+
* registered runs never pay the scan; returns `undefined` once the parent has settled the child (the
|
|
2019
|
+
* waiter is consumed), but by then the child is terminal and no longer resolved.
|
|
2020
|
+
*/
|
|
2021
|
+
async findParentRunId(childRunId) {
|
|
2022
|
+
const token = `child:${childRunId}`;
|
|
2023
|
+
for (const waiter of await this.store.listSignalWaiters("child:")) {
|
|
2024
|
+
if (waiter.token === token) return waiter.runId;
|
|
2025
|
+
}
|
|
2026
|
+
return void 0;
|
|
2027
|
+
}
|
|
1897
2028
|
/** Track an in-flight execution so {@link drain} can wait for it. */
|
|
1898
2029
|
track(p) {
|
|
1899
2030
|
this.inflight.add(p);
|
|
@@ -2583,7 +2714,7 @@ var WorkflowEngine = class {
|
|
|
2583
2714
|
});
|
|
2584
2715
|
}
|
|
2585
2716
|
async execute(run, fn) {
|
|
2586
|
-
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion));
|
|
2717
|
+
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run);
|
|
2587
2718
|
const lockNow = this.clock();
|
|
2588
2719
|
if (run.lockedBy !== this.instanceId || (run.lockedUntil ?? 0) <= lockNow) {
|
|
2589
2720
|
if (!await this.store.tryLockRun(run.id, this.instanceId, lockNow + this.leaseMs, lockNow)) {
|
|
@@ -3165,6 +3296,17 @@ var WorkflowEngine = class {
|
|
|
3165
3296
|
priority
|
|
3166
3297
|
}).catch(() => void 0));
|
|
3167
3298
|
}, "startChild"),
|
|
3299
|
+
// Shallow-merge into the run's searchAttributes (the ctx primitive makes this exactly-once).
|
|
3300
|
+
upsertSearchAttributes: /* @__PURE__ */ __name(async (runId, attrs) => {
|
|
3301
|
+
const run = await this.store.getRun(runId);
|
|
3302
|
+
await this.store.updateRun(runId, {
|
|
3303
|
+
searchAttributes: {
|
|
3304
|
+
...run?.searchAttributes ?? {},
|
|
3305
|
+
...attrs
|
|
3306
|
+
},
|
|
3307
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3308
|
+
});
|
|
3309
|
+
}, "upsertSearchAttributes"),
|
|
3168
3310
|
signalEntity: /* @__PURE__ */ __name((name, key, op, arg, reply) => {
|
|
3169
3311
|
queueMicrotask(() => void this.entities.dispatch(name, key, op, arg, reply).catch(() => void 0));
|
|
3170
3312
|
}, "signalEntity"),
|