@dudousxd/nestjs-durable-core 0.38.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 +116 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +116 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1503,6 +1503,16 @@ declare class WorkflowEngine {
|
|
|
1503
1503
|
* Register a workflow version. Register multiple versions of the same name to keep in-flight
|
|
1504
1504
|
* runs working across a breaking change: old runs resume on the version they started on, new
|
|
1505
1505
|
* runs start on the newest registered version.
|
|
1506
|
+
*
|
|
1507
|
+
* GROUP-SERVED (uniform dispatch, opt-in): pass `{ group, executor }` to have the engine DISPATCH
|
|
1508
|
+
* this workflow's turns to a worker `group` (via `executor.advance`) instead of running `fn`
|
|
1509
|
+
* inline — exactly as {@link registerRemote} does for a cross-SDK (e.g. Python) body, EXCEPT the
|
|
1510
|
+
* body is a TS `fn` the engine retains so an IN-APP worker on `group` can fetch it by name
|
|
1511
|
+
* ({@link workflowBody}) and run it. This is "one app, both roles": the same process owns the
|
|
1512
|
+
* engine AND consumes its own group. Recovery, timers, singleton, cancel and dead-lettering stay
|
|
1513
|
+
* engine concerns, identical to a remote run. OMIT `group`/`executor` (the default) to keep the
|
|
1514
|
+
* inline fast path — `fn` runs in-process and the run pays zero dispatch round-trips. The first
|
|
1515
|
+
* step toward routing every run through a group with no local/remote flag (Phase 3).
|
|
1506
1516
|
*/
|
|
1507
1517
|
register(name: string, version: string, fn: WorkflowFn, opts?: {
|
|
1508
1518
|
tags?: string[] | undefined;
|
|
@@ -1511,7 +1521,22 @@ declare class WorkflowEngine {
|
|
|
1511
1521
|
validateInput?: ((input: unknown) => void | Promise<void>) | undefined;
|
|
1512
1522
|
onEvent?: string[] | undefined;
|
|
1513
1523
|
eventBatch?: EventBatchConfig | undefined;
|
|
1524
|
+
/** Worker group to DISPATCH this workflow's turns to (uniform dispatch). Requires `executor`.
|
|
1525
|
+
* Omit for the default inline fast path. See the method doc. */
|
|
1526
|
+
group?: string | undefined;
|
|
1527
|
+
/** Advances a turn by dispatching a {@link WorkflowTask} to `group`. Requires `group`. For an
|
|
1528
|
+
* in-app worker, pair a transport-backed {@link RemoteWorkflowExecutor} (group consumed by a
|
|
1529
|
+
* worker that calls {@link workflowBody}) so the retained `fn` runs through the transport hop. */
|
|
1530
|
+
executor?: WorkflowExecutor | undefined;
|
|
1514
1531
|
}): void;
|
|
1532
|
+
/**
|
|
1533
|
+
* The retained in-process body for `name@version`, for an IN-APP worker that consumes this engine's
|
|
1534
|
+
* own group to SERVE a group-served `register(..., { group, executor })` workflow (uniform dispatch):
|
|
1535
|
+
* the worker fetches the body by name when it picks up the dispatched {@link WorkflowTask}, replays
|
|
1536
|
+
* it, and returns the {@link WorkflowDecision}. `undefined` for an unregistered name or a body-less
|
|
1537
|
+
* remote (a `registerRemote` / synthesized child carries only a throwing stub, never handed out).
|
|
1538
|
+
*/
|
|
1539
|
+
workflowBody(name: string, version: string): WorkflowFn | undefined;
|
|
1515
1540
|
/**
|
|
1516
1541
|
* Register a workflow whose body runs in another SDK (e.g. Python). The engine owns the run exactly
|
|
1517
1542
|
* as for a TS workflow — it persists checkpoints, recovers, runs timers — but advances it by handing
|
|
@@ -1575,6 +1600,49 @@ declare class WorkflowEngine {
|
|
|
1575
1600
|
/** Read a run's current persisted state (or null if unknown). A thin pass-through to the store. */
|
|
1576
1601
|
getRun(runId: string): Promise<WorkflowRun | null>;
|
|
1577
1602
|
resume(runId: string): Promise<RunResult>;
|
|
1603
|
+
/**
|
|
1604
|
+
* The registration for an UNREGISTERED run, INHERITED from its nearest REMOTE ancestor — or
|
|
1605
|
+
* `undefined` if it has none (the skew-protection error case in {@link resume}). Callers check the
|
|
1606
|
+
* flat registry FIRST (so an explicit `registerRemote(name, …)` ALWAYS wins, and a registered run
|
|
1607
|
+
* never reaches here); this resolves only the new case: a child spawned by a remote workflow (e.g. a
|
|
1608
|
+
* Python `gather_children` / `start_child` fan-out) of a name the host never `registerRemote`-ed. It
|
|
1609
|
+
* is driven as a remote run on the ancestor's group + the SAME executor instance, so the host needs
|
|
1610
|
+
* no redundant registration per child name.
|
|
1611
|
+
*
|
|
1612
|
+
* RECOMPUTED per resume rather than memoized into `this.workflows`: a synthesized child is not a real
|
|
1613
|
+
* registration and must not leak into `latest`, `knownGroups`, or `sweepTimeouts` (which iterate the
|
|
1614
|
+
* registry); recomputing also stays correct if the ancestor's remote registration is re-pointed at a
|
|
1615
|
+
* new executor across a deploy. Only unregistered runs pay it, so registered workflows pay nothing.
|
|
1616
|
+
*/
|
|
1617
|
+
private findInheritedRegistration;
|
|
1618
|
+
/**
|
|
1619
|
+
* A throwaway {@link RegisteredWorkflow} that routes `name@version` as a remote run on the ancestor's
|
|
1620
|
+
* group via the ancestor's SAME executor instance. The executor is group-scoped (it dispatches a
|
|
1621
|
+
* {@link WorkflowTask} carrying `run.workflow`, and the worker picks the body by name), so reusing it
|
|
1622
|
+
* is correct AND required — a second executor on the same group would race the worker for results.
|
|
1623
|
+
* Only `remote` is inherited; the child gets none of the ancestor's singleton/timeout/validator, so
|
|
1624
|
+
* its identity stays its own.
|
|
1625
|
+
*/
|
|
1626
|
+
private synthesizeRemoteChild;
|
|
1627
|
+
/**
|
|
1628
|
+
* Walk the parent chain of `childRunId` to the nearest ancestor registered as REMOTE, returning its
|
|
1629
|
+
* registration (or `undefined` if there is none). The parent of an awaited child is found via its
|
|
1630
|
+
* live `child:<childId>` signal waiter — present for as long as the parent is suspended awaiting the
|
|
1631
|
+
* child, i.e. throughout the child's executable lifetime (start, every resume, crash recovery, cancel
|
|
1632
|
+
* cascade), which is exactly when we resolve. A registered-but-LOCAL parent stops the walk (an
|
|
1633
|
+
* unregistered child of a TS workflow is a genuine misconfiguration → skew error); only an
|
|
1634
|
+
* unregistered parent keeps walking, so a child of an inherited child still reaches the remote root.
|
|
1635
|
+
* `visited` guards against a pathological cyclic id graph.
|
|
1636
|
+
*/
|
|
1637
|
+
private findRemoteAncestor;
|
|
1638
|
+
/**
|
|
1639
|
+
* The run that spawned `childRunId`, via its live `child:<childRunId>` signal waiter — every awaited
|
|
1640
|
+
* child (`ctx.child`, `ctx.all`, a remote `start_child`) suspends its parent on exactly this waiter,
|
|
1641
|
+
* and the waiter is written BEFORE the child is started. Only reached for an UNREGISTERED child, so
|
|
1642
|
+
* registered runs never pay the scan; returns `undefined` once the parent has settled the child (the
|
|
1643
|
+
* waiter is consumed), but by then the child is terminal and no longer resolved.
|
|
1644
|
+
*/
|
|
1645
|
+
private findParentRunId;
|
|
1578
1646
|
/** Track an in-flight execution so {@link drain} can wait for it. */
|
|
1579
1647
|
private track;
|
|
1580
1648
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1503,6 +1503,16 @@ declare class WorkflowEngine {
|
|
|
1503
1503
|
* Register a workflow version. Register multiple versions of the same name to keep in-flight
|
|
1504
1504
|
* runs working across a breaking change: old runs resume on the version they started on, new
|
|
1505
1505
|
* runs start on the newest registered version.
|
|
1506
|
+
*
|
|
1507
|
+
* GROUP-SERVED (uniform dispatch, opt-in): pass `{ group, executor }` to have the engine DISPATCH
|
|
1508
|
+
* this workflow's turns to a worker `group` (via `executor.advance`) instead of running `fn`
|
|
1509
|
+
* inline — exactly as {@link registerRemote} does for a cross-SDK (e.g. Python) body, EXCEPT the
|
|
1510
|
+
* body is a TS `fn` the engine retains so an IN-APP worker on `group` can fetch it by name
|
|
1511
|
+
* ({@link workflowBody}) and run it. This is "one app, both roles": the same process owns the
|
|
1512
|
+
* engine AND consumes its own group. Recovery, timers, singleton, cancel and dead-lettering stay
|
|
1513
|
+
* engine concerns, identical to a remote run. OMIT `group`/`executor` (the default) to keep the
|
|
1514
|
+
* inline fast path — `fn` runs in-process and the run pays zero dispatch round-trips. The first
|
|
1515
|
+
* step toward routing every run through a group with no local/remote flag (Phase 3).
|
|
1506
1516
|
*/
|
|
1507
1517
|
register(name: string, version: string, fn: WorkflowFn, opts?: {
|
|
1508
1518
|
tags?: string[] | undefined;
|
|
@@ -1511,7 +1521,22 @@ declare class WorkflowEngine {
|
|
|
1511
1521
|
validateInput?: ((input: unknown) => void | Promise<void>) | undefined;
|
|
1512
1522
|
onEvent?: string[] | undefined;
|
|
1513
1523
|
eventBatch?: EventBatchConfig | undefined;
|
|
1524
|
+
/** Worker group to DISPATCH this workflow's turns to (uniform dispatch). Requires `executor`.
|
|
1525
|
+
* Omit for the default inline fast path. See the method doc. */
|
|
1526
|
+
group?: string | undefined;
|
|
1527
|
+
/** Advances a turn by dispatching a {@link WorkflowTask} to `group`. Requires `group`. For an
|
|
1528
|
+
* in-app worker, pair a transport-backed {@link RemoteWorkflowExecutor} (group consumed by a
|
|
1529
|
+
* worker that calls {@link workflowBody}) so the retained `fn` runs through the transport hop. */
|
|
1530
|
+
executor?: WorkflowExecutor | undefined;
|
|
1514
1531
|
}): void;
|
|
1532
|
+
/**
|
|
1533
|
+
* The retained in-process body for `name@version`, for an IN-APP worker that consumes this engine's
|
|
1534
|
+
* own group to SERVE a group-served `register(..., { group, executor })` workflow (uniform dispatch):
|
|
1535
|
+
* the worker fetches the body by name when it picks up the dispatched {@link WorkflowTask}, replays
|
|
1536
|
+
* it, and returns the {@link WorkflowDecision}. `undefined` for an unregistered name or a body-less
|
|
1537
|
+
* remote (a `registerRemote` / synthesized child carries only a throwing stub, never handed out).
|
|
1538
|
+
*/
|
|
1539
|
+
workflowBody(name: string, version: string): WorkflowFn | undefined;
|
|
1515
1540
|
/**
|
|
1516
1541
|
* Register a workflow whose body runs in another SDK (e.g. Python). The engine owns the run exactly
|
|
1517
1542
|
* as for a TS workflow — it persists checkpoints, recovers, runs timers — but advances it by handing
|
|
@@ -1575,6 +1600,49 @@ declare class WorkflowEngine {
|
|
|
1575
1600
|
/** Read a run's current persisted state (or null if unknown). A thin pass-through to the store. */
|
|
1576
1601
|
getRun(runId: string): Promise<WorkflowRun | null>;
|
|
1577
1602
|
resume(runId: string): Promise<RunResult>;
|
|
1603
|
+
/**
|
|
1604
|
+
* The registration for an UNREGISTERED run, INHERITED from its nearest REMOTE ancestor — or
|
|
1605
|
+
* `undefined` if it has none (the skew-protection error case in {@link resume}). Callers check the
|
|
1606
|
+
* flat registry FIRST (so an explicit `registerRemote(name, …)` ALWAYS wins, and a registered run
|
|
1607
|
+
* never reaches here); this resolves only the new case: a child spawned by a remote workflow (e.g. a
|
|
1608
|
+
* Python `gather_children` / `start_child` fan-out) of a name the host never `registerRemote`-ed. It
|
|
1609
|
+
* is driven as a remote run on the ancestor's group + the SAME executor instance, so the host needs
|
|
1610
|
+
* no redundant registration per child name.
|
|
1611
|
+
*
|
|
1612
|
+
* RECOMPUTED per resume rather than memoized into `this.workflows`: a synthesized child is not a real
|
|
1613
|
+
* registration and must not leak into `latest`, `knownGroups`, or `sweepTimeouts` (which iterate the
|
|
1614
|
+
* registry); recomputing also stays correct if the ancestor's remote registration is re-pointed at a
|
|
1615
|
+
* new executor across a deploy. Only unregistered runs pay it, so registered workflows pay nothing.
|
|
1616
|
+
*/
|
|
1617
|
+
private findInheritedRegistration;
|
|
1618
|
+
/**
|
|
1619
|
+
* A throwaway {@link RegisteredWorkflow} that routes `name@version` as a remote run on the ancestor's
|
|
1620
|
+
* group via the ancestor's SAME executor instance. The executor is group-scoped (it dispatches a
|
|
1621
|
+
* {@link WorkflowTask} carrying `run.workflow`, and the worker picks the body by name), so reusing it
|
|
1622
|
+
* is correct AND required — a second executor on the same group would race the worker for results.
|
|
1623
|
+
* Only `remote` is inherited; the child gets none of the ancestor's singleton/timeout/validator, so
|
|
1624
|
+
* its identity stays its own.
|
|
1625
|
+
*/
|
|
1626
|
+
private synthesizeRemoteChild;
|
|
1627
|
+
/**
|
|
1628
|
+
* Walk the parent chain of `childRunId` to the nearest ancestor registered as REMOTE, returning its
|
|
1629
|
+
* registration (or `undefined` if there is none). The parent of an awaited child is found via its
|
|
1630
|
+
* live `child:<childId>` signal waiter — present for as long as the parent is suspended awaiting the
|
|
1631
|
+
* child, i.e. throughout the child's executable lifetime (start, every resume, crash recovery, cancel
|
|
1632
|
+
* cascade), which is exactly when we resolve. A registered-but-LOCAL parent stops the walk (an
|
|
1633
|
+
* unregistered child of a TS workflow is a genuine misconfiguration → skew error); only an
|
|
1634
|
+
* unregistered parent keeps walking, so a child of an inherited child still reaches the remote root.
|
|
1635
|
+
* `visited` guards against a pathological cyclic id graph.
|
|
1636
|
+
*/
|
|
1637
|
+
private findRemoteAncestor;
|
|
1638
|
+
/**
|
|
1639
|
+
* The run that spawned `childRunId`, via its live `child:<childRunId>` signal waiter — every awaited
|
|
1640
|
+
* child (`ctx.child`, `ctx.all`, a remote `start_child`) suspends its parent on exactly this waiter,
|
|
1641
|
+
* and the waiter is written BEFORE the child is started. Only reached for an UNREGISTERED child, so
|
|
1642
|
+
* registered runs never pay the scan; returns `undefined` once the parent has settled the child (the
|
|
1643
|
+
* waiter is consumed), but by then the child is terminal and no longer resolved.
|
|
1644
|
+
*/
|
|
1645
|
+
private findParentRunId;
|
|
1578
1646
|
/** Track an in-flight execution so {@link drain} can wait for it. */
|
|
1579
1647
|
private track;
|
|
1580
1648
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1626,8 +1626,21 @@ var WorkflowEngine = class {
|
|
|
1626
1626
|
* Register a workflow version. Register multiple versions of the same name to keep in-flight
|
|
1627
1627
|
* runs working across a breaking change: old runs resume on the version they started on, new
|
|
1628
1628
|
* runs start on the newest registered version.
|
|
1629
|
+
*
|
|
1630
|
+
* GROUP-SERVED (uniform dispatch, opt-in): pass `{ group, executor }` to have the engine DISPATCH
|
|
1631
|
+
* this workflow's turns to a worker `group` (via `executor.advance`) instead of running `fn`
|
|
1632
|
+
* inline — exactly as {@link registerRemote} does for a cross-SDK (e.g. Python) body, EXCEPT the
|
|
1633
|
+
* body is a TS `fn` the engine retains so an IN-APP worker on `group` can fetch it by name
|
|
1634
|
+
* ({@link workflowBody}) and run it. This is "one app, both roles": the same process owns the
|
|
1635
|
+
* engine AND consumes its own group. Recovery, timers, singleton, cancel and dead-lettering stay
|
|
1636
|
+
* engine concerns, identical to a remote run. OMIT `group`/`executor` (the default) to keep the
|
|
1637
|
+
* inline fast path — `fn` runs in-process and the run pays zero dispatch round-trips. The first
|
|
1638
|
+
* step toward routing every run through a group with no local/remote flag (Phase 3).
|
|
1629
1639
|
*/
|
|
1630
1640
|
register(name, version, fn, opts) {
|
|
1641
|
+
if (opts?.group == null !== (opts?.executor == null)) {
|
|
1642
|
+
throw new Error(`workflow ${name}@${version}: register({ group, executor }) needs BOTH group and executor (or neither)`);
|
|
1643
|
+
}
|
|
1631
1644
|
const registered = {
|
|
1632
1645
|
name,
|
|
1633
1646
|
version,
|
|
@@ -1637,8 +1650,17 @@ var WorkflowEngine = class {
|
|
|
1637
1650
|
executionTimeoutMs: opts?.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
|
|
1638
1651
|
validateInput: opts?.validateInput,
|
|
1639
1652
|
onEvent: opts?.onEvent,
|
|
1640
|
-
eventBatch: opts?.eventBatch
|
|
1653
|
+
eventBatch: opts?.eventBatch,
|
|
1654
|
+
// A real, retained in-process body — so `workflowBody` can hand it to an in-app worker, and so
|
|
1655
|
+
// it is NOT confused with the throwing stub `registerRemote` installs for a body-less remote.
|
|
1656
|
+
hasBody: true
|
|
1641
1657
|
};
|
|
1658
|
+
if (opts?.group != null && opts?.executor != null) {
|
|
1659
|
+
registered.remote = {
|
|
1660
|
+
group: opts.group,
|
|
1661
|
+
executor: opts.executor
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1642
1664
|
this.workflows.set(versionKey(name, version), registered);
|
|
1643
1665
|
const current = this.latest.get(name);
|
|
1644
1666
|
if (!current || isNewerVersion(version, current.version)) this.latest.set(name, registered);
|
|
@@ -1649,6 +1671,17 @@ var WorkflowEngine = class {
|
|
|
1649
1671
|
}
|
|
1650
1672
|
}
|
|
1651
1673
|
/**
|
|
1674
|
+
* The retained in-process body for `name@version`, for an IN-APP worker that consumes this engine's
|
|
1675
|
+
* own group to SERVE a group-served `register(..., { group, executor })` workflow (uniform dispatch):
|
|
1676
|
+
* the worker fetches the body by name when it picks up the dispatched {@link WorkflowTask}, replays
|
|
1677
|
+
* it, and returns the {@link WorkflowDecision}. `undefined` for an unregistered name or a body-less
|
|
1678
|
+
* remote (a `registerRemote` / synthesized child carries only a throwing stub, never handed out).
|
|
1679
|
+
*/
|
|
1680
|
+
workflowBody(name, version) {
|
|
1681
|
+
const reg = this.workflows.get(versionKey(name, version));
|
|
1682
|
+
return reg?.hasBody ? reg.fn : void 0;
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1652
1685
|
* Register a workflow whose body runs in another SDK (e.g. Python). The engine owns the run exactly
|
|
1653
1686
|
* as for a TS workflow — it persists checkpoints, recovers, runs timers — but advances it by handing
|
|
1654
1687
|
* the run's history to `executor` (which dispatches a {@link WorkflowTask} to the worker) and applying
|
|
@@ -1774,7 +1807,11 @@ var WorkflowEngine = class {
|
|
|
1774
1807
|
}
|
|
1775
1808
|
async start(workflow, input, runId, opts) {
|
|
1776
1809
|
const name = workflowName(workflow);
|
|
1777
|
-
|
|
1810
|
+
let registered = this.latest.get(name);
|
|
1811
|
+
if (!registered) {
|
|
1812
|
+
const ancestor = await this.findRemoteAncestor(runId);
|
|
1813
|
+
if (ancestor) registered = this.synthesizeRemoteChild(name, ancestor.version, ancestor);
|
|
1814
|
+
}
|
|
1778
1815
|
if (!registered) throw new Error(`workflow ${name} is not registered`);
|
|
1779
1816
|
await registered.validateInput?.(input);
|
|
1780
1817
|
const prior = await this.store.getRun(runId);
|
|
@@ -1835,12 +1872,87 @@ var WorkflowEngine = class {
|
|
|
1835
1872
|
error: run.error
|
|
1836
1873
|
};
|
|
1837
1874
|
}
|
|
1838
|
-
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion));
|
|
1875
|
+
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run);
|
|
1839
1876
|
if (!registered) {
|
|
1840
1877
|
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)`);
|
|
1841
1878
|
}
|
|
1842
1879
|
return this.track(this.execute(run, registered.fn));
|
|
1843
1880
|
}
|
|
1881
|
+
/**
|
|
1882
|
+
* The registration for an UNREGISTERED run, INHERITED from its nearest REMOTE ancestor — or
|
|
1883
|
+
* `undefined` if it has none (the skew-protection error case in {@link resume}). Callers check the
|
|
1884
|
+
* flat registry FIRST (so an explicit `registerRemote(name, …)` ALWAYS wins, and a registered run
|
|
1885
|
+
* never reaches here); this resolves only the new case: a child spawned by a remote workflow (e.g. a
|
|
1886
|
+
* Python `gather_children` / `start_child` fan-out) of a name the host never `registerRemote`-ed. It
|
|
1887
|
+
* is driven as a remote run on the ancestor's group + the SAME executor instance, so the host needs
|
|
1888
|
+
* no redundant registration per child name.
|
|
1889
|
+
*
|
|
1890
|
+
* RECOMPUTED per resume rather than memoized into `this.workflows`: a synthesized child is not a real
|
|
1891
|
+
* registration and must not leak into `latest`, `knownGroups`, or `sweepTimeouts` (which iterate the
|
|
1892
|
+
* registry); recomputing also stays correct if the ancestor's remote registration is re-pointed at a
|
|
1893
|
+
* new executor across a deploy. Only unregistered runs pay it, so registered workflows pay nothing.
|
|
1894
|
+
*/
|
|
1895
|
+
async findInheritedRegistration(run) {
|
|
1896
|
+
const ancestor = await this.findRemoteAncestor(run.id);
|
|
1897
|
+
if (!ancestor) return void 0;
|
|
1898
|
+
return this.synthesizeRemoteChild(run.workflow, run.workflowVersion, ancestor);
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* A throwaway {@link RegisteredWorkflow} that routes `name@version` as a remote run on the ancestor's
|
|
1902
|
+
* group via the ancestor's SAME executor instance. The executor is group-scoped (it dispatches a
|
|
1903
|
+
* {@link WorkflowTask} carrying `run.workflow`, and the worker picks the body by name), so reusing it
|
|
1904
|
+
* is correct AND required — a second executor on the same group would race the worker for results.
|
|
1905
|
+
* Only `remote` is inherited; the child gets none of the ancestor's singleton/timeout/validator, so
|
|
1906
|
+
* its identity stays its own.
|
|
1907
|
+
*/
|
|
1908
|
+
synthesizeRemoteChild(name, version, ancestor) {
|
|
1909
|
+
const remote = ancestor.remote;
|
|
1910
|
+
if (!remote) throw new Error(`workflow ${ancestor.name} is not a remote workflow`);
|
|
1911
|
+
return {
|
|
1912
|
+
name,
|
|
1913
|
+
version,
|
|
1914
|
+
fn: /* @__PURE__ */ __name(() => {
|
|
1915
|
+
throw new Error(`workflow ${name} is remote \u2014 it has no in-process body`);
|
|
1916
|
+
}, "fn"),
|
|
1917
|
+
remote
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Walk the parent chain of `childRunId` to the nearest ancestor registered as REMOTE, returning its
|
|
1922
|
+
* registration (or `undefined` if there is none). The parent of an awaited child is found via its
|
|
1923
|
+
* live `child:<childId>` signal waiter — present for as long as the parent is suspended awaiting the
|
|
1924
|
+
* child, i.e. throughout the child's executable lifetime (start, every resume, crash recovery, cancel
|
|
1925
|
+
* cascade), which is exactly when we resolve. A registered-but-LOCAL parent stops the walk (an
|
|
1926
|
+
* unregistered child of a TS workflow is a genuine misconfiguration → skew error); only an
|
|
1927
|
+
* unregistered parent keeps walking, so a child of an inherited child still reaches the remote root.
|
|
1928
|
+
* `visited` guards against a pathological cyclic id graph.
|
|
1929
|
+
*/
|
|
1930
|
+
async findRemoteAncestor(childRunId, visited = /* @__PURE__ */ new Set()) {
|
|
1931
|
+
if (visited.has(childRunId)) return void 0;
|
|
1932
|
+
visited.add(childRunId);
|
|
1933
|
+
const parentRunId = await this.findParentRunId(childRunId);
|
|
1934
|
+
if (!parentRunId) return void 0;
|
|
1935
|
+
const parent = await this.store.getRun(parentRunId);
|
|
1936
|
+
if (!parent) return void 0;
|
|
1937
|
+
const parentReg = this.workflows.get(versionKey(parent.workflow, parent.workflowVersion));
|
|
1938
|
+
if (parentReg?.remote) return parentReg;
|
|
1939
|
+
if (parentReg) return void 0;
|
|
1940
|
+
return this.findRemoteAncestor(parent.id, visited);
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* The run that spawned `childRunId`, via its live `child:<childRunId>` signal waiter — every awaited
|
|
1944
|
+
* child (`ctx.child`, `ctx.all`, a remote `start_child`) suspends its parent on exactly this waiter,
|
|
1945
|
+
* and the waiter is written BEFORE the child is started. Only reached for an UNREGISTERED child, so
|
|
1946
|
+
* registered runs never pay the scan; returns `undefined` once the parent has settled the child (the
|
|
1947
|
+
* waiter is consumed), but by then the child is terminal and no longer resolved.
|
|
1948
|
+
*/
|
|
1949
|
+
async findParentRunId(childRunId) {
|
|
1950
|
+
const token = `child:${childRunId}`;
|
|
1951
|
+
for (const waiter of await this.store.listSignalWaiters("child:")) {
|
|
1952
|
+
if (waiter.token === token) return waiter.runId;
|
|
1953
|
+
}
|
|
1954
|
+
return void 0;
|
|
1955
|
+
}
|
|
1844
1956
|
/** Track an in-flight execution so {@link drain} can wait for it. */
|
|
1845
1957
|
track(p) {
|
|
1846
1958
|
this.inflight.add(p);
|
|
@@ -2530,7 +2642,7 @@ var WorkflowEngine = class {
|
|
|
2530
2642
|
});
|
|
2531
2643
|
}
|
|
2532
2644
|
async execute(run, fn) {
|
|
2533
|
-
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion));
|
|
2645
|
+
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run);
|
|
2534
2646
|
const lockNow = this.clock();
|
|
2535
2647
|
if (run.lockedBy !== this.instanceId || (run.lockedUntil ?? 0) <= lockNow) {
|
|
2536
2648
|
if (!await this.store.tryLockRun(run.id, this.instanceId, lockNow + this.leaseMs, lockNow)) {
|