@adonis-agora/durable 0.7.0 → 0.8.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/CHANGELOG.md +64 -0
- package/README.md +1 -1
- package/dist/commands/main.d.ts +3 -2
- package/dist/commands/main.d.ts.map +1 -1
- package/dist/commands/main.js +3 -2
- package/dist/commands/main.js.map +1 -1
- package/dist/commands/make_workflow.d.ts +4 -3
- package/dist/commands/make_workflow.d.ts.map +1 -1
- package/dist/commands/make_workflow.js +4 -3
- package/dist/commands/make_workflow.js.map +1 -1
- package/dist/configure.d.ts +6 -5
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +9 -6
- package/dist/configure.js.map +1 -1
- package/dist/providers/dashboard_provider.d.ts +34 -3
- package/dist/providers/dashboard_provider.d.ts.map +1 -1
- package/dist/providers/dashboard_provider.js +110 -6
- package/dist/providers/dashboard_provider.js.map +1 -1
- package/dist/src/base-workflow.d.ts +78 -0
- package/dist/src/base-workflow.d.ts.map +1 -0
- package/dist/src/base-workflow.js +106 -0
- package/dist/src/base-workflow.js.map +1 -0
- package/dist/src/codec-state-store.d.ts +13 -0
- package/dist/src/codec-state-store.d.ts.map +1 -1
- package/dist/src/codec-state-store.js +13 -0
- package/dist/src/codec-state-store.js.map +1 -1
- package/dist/src/control-flow-signal.d.ts +50 -0
- package/dist/src/control-flow-signal.d.ts.map +1 -0
- package/dist/src/control-flow-signal.js +54 -0
- package/dist/src/control-flow-signal.js.map +1 -0
- package/dist/src/control-plane-redis/redis-control-plane.d.ts +57 -5
- package/dist/src/control-plane-redis/redis-control-plane.d.ts.map +1 -1
- package/dist/src/control-plane-redis/redis-control-plane.js +100 -2
- package/dist/src/control-plane-redis/redis-control-plane.js.map +1 -1
- package/dist/src/dashboard/auth.d.ts +89 -0
- package/dist/src/dashboard/auth.d.ts.map +1 -0
- package/dist/src/dashboard/auth.js +113 -0
- package/dist/src/dashboard/auth.js.map +1 -0
- package/dist/src/dashboard/define_config.d.ts +17 -0
- package/dist/src/dashboard/define_config.d.ts.map +1 -1
- package/dist/src/dashboard/define_config.js +23 -1
- package/dist/src/dashboard/define_config.js.map +1 -1
- package/dist/src/dashboard/index.d.ts +5 -0
- package/dist/src/dashboard/index.d.ts.map +1 -1
- package/dist/src/dashboard/index.js +4 -0
- package/dist/src/dashboard/index.js.map +1 -1
- package/dist/src/dashboard/login_page.d.ts +23 -0
- package/dist/src/dashboard/login_page.d.ts.map +1 -0
- package/dist/src/dashboard/login_page.js +170 -0
- package/dist/src/dashboard/login_page.js.map +1 -0
- package/dist/src/dashboard/session_cookie.d.ts +42 -0
- package/dist/src/dashboard/session_cookie.d.ts.map +1 -0
- package/dist/src/dashboard/session_cookie.js +97 -0
- package/dist/src/dashboard/session_cookie.js.map +1 -0
- package/dist/src/define_config.d.ts +3 -2
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/engine.d.ts +51 -1
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +213 -27
- package/dist/src/engine.js.map +1 -1
- package/dist/src/errors.d.ts +6 -1
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +7 -2
- package/dist/src/errors.js.map +1 -1
- package/dist/src/event-accumulators.d.ts +1 -1
- package/dist/src/event-accumulators.d.ts.map +1 -1
- package/dist/src/events.d.ts +16 -0
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +26 -1
- package/dist/src/events.js.map +1 -1
- package/dist/src/hooks/workflows.d.ts +3 -2
- package/dist/src/hooks/workflows.d.ts.map +1 -1
- package/dist/src/hooks/workflows.js +3 -2
- package/dist/src/hooks/workflows.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/interfaces.d.ts +44 -1
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/services/main.d.ts +13 -0
- package/dist/src/services/main.d.ts.map +1 -0
- package/dist/src/services/main.js +17 -0
- package/dist/src/services/main.js.map +1 -0
- package/dist/src/step-name-symbol.d.ts +2 -2
- package/dist/src/step-name-symbol.js +2 -2
- package/dist/src/stores/lucid-schema.d.ts +1 -0
- package/dist/src/stores/lucid-schema.d.ts.map +1 -1
- package/dist/src/stores/lucid-schema.js +15 -0
- package/dist/src/stores/lucid-schema.js.map +1 -1
- package/dist/src/stores/lucid.d.ts +13 -0
- package/dist/src/stores/lucid.d.ts.map +1 -1
- package/dist/src/stores/lucid.js +43 -0
- package/dist/src/stores/lucid.js.map +1 -1
- package/dist/src/testing/in-memory-state-store.d.ts +16 -0
- package/dist/src/testing/in-memory-state-store.d.ts.map +1 -1
- package/dist/src/testing/in-memory-state-store.js +29 -0
- package/dist/src/testing/in-memory-state-store.js.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.js +31 -0
- package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
- package/dist/src/workflow-als.d.ts +21 -0
- package/dist/src/workflow-als.d.ts.map +1 -0
- package/dist/src/workflow-als.js +22 -0
- package/dist/src/workflow-als.js.map +1 -0
- package/dist/src/workflow-ctx.d.ts.map +1 -1
- package/dist/src/workflow-ctx.js +83 -4
- package/dist/src/workflow-ctx.js.map +1 -1
- package/dist/src/workflow-discovery.d.ts +21 -15
- package/dist/src/workflow-discovery.d.ts.map +1 -1
- package/dist/src/workflow-discovery.js +17 -13
- package/dist/src/workflow-discovery.js.map +1 -1
- package/dist/src/workflow-ref.d.ts +9 -27
- package/dist/src/workflow-ref.d.ts.map +1 -1
- package/dist/src/workflow-ref.js +12 -34
- package/dist/src/workflow-ref.js.map +1 -1
- package/dist/stubs/database/migrations/create_durable_tables.stub +13 -1
- package/dist/stubs/make/workflow/main.stub +4 -3
- package/package.json +6 -1
package/dist/src/engine.js
CHANGED
|
@@ -11,6 +11,7 @@ import { RemoteWorkflowExecutor } from './remote-workflow-executor.js';
|
|
|
11
11
|
import { SingletonGate } from './singleton-gate.js';
|
|
12
12
|
import { sanitizeQueueToken, tenantGroup } from './tenant-group.js';
|
|
13
13
|
import { TransportPool } from './transport-pool.js';
|
|
14
|
+
import { workflowAls } from './workflow-als.js';
|
|
14
15
|
import { createWorkflowCtx, } from './workflow-ctx.js';
|
|
15
16
|
import { workflowName, } from './workflow-ref.js';
|
|
16
17
|
/** Union of a workflow's static tags and a run's start-time tags, de-duplicated, or undefined if none. */
|
|
@@ -592,7 +593,13 @@ export class WorkflowEngine {
|
|
|
592
593
|
continue;
|
|
593
594
|
const error = { message: 'execution timeout', code: 'execution_timeout' };
|
|
594
595
|
await this.store.updateRun(run.id, { status: 'cancelled', error, updatedAt: new Date() });
|
|
595
|
-
this.emit({
|
|
596
|
+
this.emit({
|
|
597
|
+
type: 'run.failed',
|
|
598
|
+
runId: run.id,
|
|
599
|
+
workflow: run.workflow,
|
|
600
|
+
namespace: run.namespace,
|
|
601
|
+
error,
|
|
602
|
+
});
|
|
596
603
|
}
|
|
597
604
|
}
|
|
598
605
|
}
|
|
@@ -639,7 +646,13 @@ export class WorkflowEngine {
|
|
|
639
646
|
};
|
|
640
647
|
await this.store.updateRun(run.id, { status: 'dead', error, updatedAt: new Date() });
|
|
641
648
|
await this.store.releaseRunLock(run.id);
|
|
642
|
-
this.emit({
|
|
649
|
+
this.emit({
|
|
650
|
+
type: 'run.failed',
|
|
651
|
+
runId: run.id,
|
|
652
|
+
workflow: run.workflow,
|
|
653
|
+
namespace: run.namespace,
|
|
654
|
+
error,
|
|
655
|
+
});
|
|
643
656
|
this.notifyDead({ ...run, status: 'dead', error, recoveryAttempts: attempts });
|
|
644
657
|
return { runId: run.id, status: 'dead', error };
|
|
645
658
|
}
|
|
@@ -721,9 +734,17 @@ export class WorkflowEngine {
|
|
|
721
734
|
* Resolve once `runId` reaches a settled state — terminal (completed/failed/cancelled/dead) or
|
|
722
735
|
* suspended (handed off to a timer/signal/event). The async counterpart to dispatch: pair it with
|
|
723
736
|
* `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
|
|
737
|
+
*
|
|
738
|
+
* Pass `{ terminal: true }` to wait for a strictly TERMINAL state only (completed/failed/cancelled/
|
|
739
|
+
* dead) — a `suspended` run (e.g. one parked on `ctx.sleep`/`ctx.waitForSignal`) does NOT resolve;
|
|
740
|
+
* the wait continues until the run reaches an end state. This is what `BaseWorkflow.start` needs so
|
|
741
|
+
* "I want the result" blocks through a suspension instead of returning early with no output. The
|
|
742
|
+
* default (`terminal` unset/false) resolves on any settled state, suspended included.
|
|
724
743
|
*/
|
|
725
744
|
waitForRun(runId, opts) {
|
|
745
|
+
const isTerminal = (s) => s === 'completed' || s === 'failed' || s === 'cancelled' || s === 'dead';
|
|
726
746
|
const isSettled = (s) => s !== 'pending' && s !== 'running';
|
|
747
|
+
const shouldResolve = opts?.terminal ? isTerminal : isSettled;
|
|
727
748
|
const toResult = (run) => ({
|
|
728
749
|
runId,
|
|
729
750
|
status: run.status,
|
|
@@ -753,7 +774,7 @@ export class WorkflowEngine {
|
|
|
753
774
|
};
|
|
754
775
|
const check = () => {
|
|
755
776
|
void this.store.getRun(runId).then((run) => {
|
|
756
|
-
if (run &&
|
|
777
|
+
if (run && shouldResolve(run.status))
|
|
757
778
|
finish(run);
|
|
758
779
|
});
|
|
759
780
|
};
|
|
@@ -797,6 +818,17 @@ export class WorkflowEngine {
|
|
|
797
818
|
* 2. **Starts** a fresh run of every workflow registered with `onEvent: [name]`, passing the
|
|
798
819
|
* payload as input. Idempotent by `evt:<id>:<workflow>` — pass `opts.id` to dedupe redeliveries
|
|
799
820
|
* of the same logical event (default: a fresh uuid, so each publish triggers once).
|
|
821
|
+
*
|
|
822
|
+
* Reliable (buffered) delivery — mirrors `signalWithStart`'s reliability contract, so document it
|
|
823
|
+
* exactly: a publish that resumes ≥1 live waiter, OR routes into an `eventBatch` accumulator /
|
|
824
|
+
* starts ≥1 subscriber, is NOT buffered (fan-out stays live-only). A publish that touches NEITHER
|
|
825
|
+
* buffers ONE copy (via {@link StateStore.bufferEvent}) unless `opts.buffer === false`, so it isn't
|
|
826
|
+
* silently dropped just because nobody was listening yet. That buffered copy is consumed by the
|
|
827
|
+
* FIRST future `ctx.waitForEvent(name, { match })` whose match accepts its payload — point-to-point
|
|
828
|
+
* on redelivery, never by a later-registered onEvent subscriber. Right after buffering, this
|
|
829
|
+
* re-checks `listSignalWaiters` ONCE (sandwich parity with `signal`'s take → buffer → re-check) so a
|
|
830
|
+
* waiter that registers in the sliver between the initial miss and the buffer write is still paired
|
|
831
|
+
* instead of leaving both rows stranded (`waitForEvent`'s post-registration scan is the mirror half).
|
|
800
832
|
*/
|
|
801
833
|
async publishEvent(name, payload, opts) {
|
|
802
834
|
let touched = 0;
|
|
@@ -830,20 +862,45 @@ export class WorkflowEngine {
|
|
|
830
862
|
}
|
|
831
863
|
}
|
|
832
864
|
}
|
|
865
|
+
// Nobody received it live (no waiter matched, no subscriber exists/started) — buffer ONE copy
|
|
866
|
+
// unless the caller opted out. `touched > 0` covers BOTH the waiter fan-out and the subscriber loop.
|
|
867
|
+
if (touched > 0 || opts?.buffer === false)
|
|
868
|
+
return touched;
|
|
869
|
+
const bufferedId = globalThis.crypto.randomUUID();
|
|
870
|
+
await this.store.bufferEvent({ name, payload, id: bufferedId, publishedAt: this.clock() });
|
|
871
|
+
// Re-check: a waiter may have registered in the window between the miss above and this buffer
|
|
872
|
+
// write (the events-side mirror of engine.signal's interleaving proof). Only the FIRST
|
|
873
|
+
// late-registered matching waiter matters — only one buffered copy exists.
|
|
874
|
+
const lateWaiters = await this.store.listSignalWaiters(eventPrefix(name));
|
|
875
|
+
const lateWaiter = lateWaiters.find((w) => eventMatches(payload, eventMatchOf(w.token)));
|
|
876
|
+
if (!lateWaiter)
|
|
877
|
+
return touched;
|
|
878
|
+
if (!(await this.store.removeBufferedEvent(bufferedId)))
|
|
879
|
+
return touched; // claimed elsewhere already
|
|
880
|
+
// `takeSignalWaiter` is safe here even though it deletes ANY row for the token: an event token
|
|
881
|
+
// embeds this ONE `waitForEvent` call's own `runId#seq` (see eventToken in events.ts), so no other
|
|
882
|
+
// registration could ever share it.
|
|
883
|
+
const waiter = await this.store.takeSignalWaiter(lateWaiter.token);
|
|
884
|
+
if (waiter) {
|
|
885
|
+
const settled = await this.deliverSignal(waiter, payload);
|
|
886
|
+
if (settled)
|
|
887
|
+
touched += 1;
|
|
888
|
+
}
|
|
889
|
+
// If `waiter` is null, that exact registration resolved itself some other way in the interim (most
|
|
890
|
+
// likely its own timeout deadline) — the buffered copy is already spent, so drop it rather than
|
|
891
|
+
// resuming a run a second time or re-buffering for a THIRD, unrelated waiter to pick up later.
|
|
833
892
|
return touched;
|
|
834
893
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
return null;
|
|
842
|
-
}
|
|
894
|
+
/**
|
|
895
|
+
* Deliver a signal to an already-known waiter: write its resolving `signal:<token>` checkpoint and
|
|
896
|
+
* resume the run. Shared by {@link signal}'s direct hit and its post-buffer re-check reclaim — both
|
|
897
|
+
* resolve a waiter the exact same way.
|
|
898
|
+
*/
|
|
899
|
+
async deliverSignal(waiter, payload) {
|
|
843
900
|
await this.store.saveCheckpoint(instantCheckpoint({
|
|
844
901
|
runId: waiter.runId,
|
|
845
902
|
seq: waiter.seq,
|
|
846
|
-
name: `signal:${token}`,
|
|
903
|
+
name: `signal:${waiter.token}`,
|
|
847
904
|
kind: 'signal',
|
|
848
905
|
output: payload,
|
|
849
906
|
// Carry the awaiting command's fan group (set when a `ctx.gather_children`/`ctx.all` fan-out
|
|
@@ -854,6 +911,40 @@ export class WorkflowEngine {
|
|
|
854
911
|
}));
|
|
855
912
|
return this.resume(waiter.runId);
|
|
856
913
|
}
|
|
914
|
+
/**
|
|
915
|
+
* Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
|
|
916
|
+
* the run result — or null if no run is (or ends up) waiting for it.
|
|
917
|
+
*
|
|
918
|
+
* Race with the waiter side (`waitForSignal`'s check → `putSignalWaiter` → re-check): a waiter can
|
|
919
|
+
* register in the sliver between this method's initial `takeSignalWaiter` miss and its
|
|
920
|
+
* `bufferSignal` write, and the naive old flow (buffer and return) would then leave BOTH a buffered
|
|
921
|
+
* payload and a registered waiter sitting in the store with nothing left to pair them — a lost wake,
|
|
922
|
+
* forever suspended. The interleaving proof: the waiter side does check → put → re-check, this side
|
|
923
|
+
* does take → buffer → re-check; whichever side's SECOND look runs last always observes the other
|
|
924
|
+
* side's write, and the destructive `take*` ops arbitrate so a signal is never delivered twice.
|
|
925
|
+
*/
|
|
926
|
+
async signal(token, payload) {
|
|
927
|
+
const waiter = await this.store.takeSignalWaiter(token);
|
|
928
|
+
if (waiter)
|
|
929
|
+
return this.deliverSignal(waiter, payload);
|
|
930
|
+
// No one was waiting yet — buffer it so the next `waitForSignal(token)` consumes it instead of
|
|
931
|
+
// dropping it (reliable signals; the basis of `signalWithStart`).
|
|
932
|
+
await this.store.bufferSignal(token, payload);
|
|
933
|
+
// Re-check: a waiter may have registered in the window between the miss above and this buffer
|
|
934
|
+
// write (see the interleaving proof above).
|
|
935
|
+
const lateWaiter = await this.store.takeSignalWaiter(token);
|
|
936
|
+
if (!lateWaiter)
|
|
937
|
+
return null;
|
|
938
|
+
const reclaimed = await this.store.takeBufferedSignal(token);
|
|
939
|
+
if (!reclaimed) {
|
|
940
|
+
// The waiter side won the race to consume the buffer itself (its own checkpoint+resume already
|
|
941
|
+
// delivered the payload) in the sliver between our takeSignalWaiter and takeBufferedSignal just
|
|
942
|
+
// above. We've already (destructively) taken `lateWaiter` off the store, but there is nothing
|
|
943
|
+
// left to deliver — drop it deliberately rather than resuming the run a second time.
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
return this.deliverSignal(lateWaiter, reclaimed.payload);
|
|
947
|
+
}
|
|
857
948
|
/**
|
|
858
949
|
* Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
|
|
859
950
|
* signal buffering: if the run is new (or busy / not yet waiting), the signal is buffered and
|
|
@@ -892,6 +983,27 @@ export class WorkflowEngine {
|
|
|
892
983
|
notifyParent(runId, completion) {
|
|
893
984
|
void this.signal(`child:${runId}`, completion).catch(() => undefined);
|
|
894
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* Deferred child start shared by the in-process ctx host and the remote `startChild` command.
|
|
988
|
+
* Deferred (microtask) so a fast child can't reentrantly resume a still-suspending parent. A start
|
|
989
|
+
* that THROWS (unregistered/unroutable workflow, input validation, singleton back-pressure, store
|
|
990
|
+
* failure) must NOT be swallowed: the parent is already suspended on `child:<childId>` (the waiter
|
|
991
|
+
* is put BEFORE start on every path), so a silent drop parks it in suspended-forever, invisibly
|
|
992
|
+
* re-attempting on every recovery wake — a misconfigured remote child looks exactly like a healthy
|
|
993
|
+
* long wait. Instead the failure is delivered to that waiter like a failed child (notifyParent):
|
|
994
|
+
* the parent resumes and fails loudly with the cause. For a fire-and-forget `ctx.startChild`
|
|
995
|
+
* (spawn — no waiter) the completion is buffered; a later join by the same id consumes it and
|
|
996
|
+
* correctly observes the failed start.
|
|
997
|
+
*/
|
|
998
|
+
startChildDeferred(workflow, input, childId, opts) {
|
|
999
|
+
queueMicrotask(() => void this.start(workflow, input, childId, opts).catch((err) => {
|
|
1000
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1001
|
+
this.notifyParent(childId, {
|
|
1002
|
+
ok: false,
|
|
1003
|
+
error: `child workflow "${workflow}" failed to start: ${message}`,
|
|
1004
|
+
});
|
|
1005
|
+
}));
|
|
1006
|
+
}
|
|
895
1007
|
/**
|
|
896
1008
|
* Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
|
|
897
1009
|
* `{ compensate: true }` to undo the saga first: the suspended run is resumed so its completed
|
|
@@ -929,7 +1041,13 @@ export class WorkflowEngine {
|
|
|
929
1041
|
}
|
|
930
1042
|
const error = { message: 'cancelled' };
|
|
931
1043
|
await this.store.updateRun(runId, { status: 'cancelled', error, updatedAt: new Date() });
|
|
932
|
-
this.emit({
|
|
1044
|
+
this.emit({
|
|
1045
|
+
type: 'run.failed',
|
|
1046
|
+
runId,
|
|
1047
|
+
workflow: run.workflow,
|
|
1048
|
+
namespace: run.namespace,
|
|
1049
|
+
error,
|
|
1050
|
+
});
|
|
933
1051
|
await this.cancelChildren(runId, opts);
|
|
934
1052
|
// Notify local cancel listeners now (a worker on this pod), and broadcast so the instance/worker
|
|
935
1053
|
// actually running this run learns of it and can abort cooperatively (the store already records
|
|
@@ -1266,8 +1384,22 @@ export class WorkflowEngine {
|
|
|
1266
1384
|
void this.notifyParent(run.id, { ok: false, error: outcome.error.message });
|
|
1267
1385
|
return { runId: run.id, status: 'failed', error: outcome.error };
|
|
1268
1386
|
}
|
|
1387
|
+
// This outcome was computed by a turn that started from a possibly-stale run snapshot. If the run
|
|
1388
|
+
// was cancelled WHILE that turn was still executing (e.g. `ctx.all`'s failFast cancelling a
|
|
1389
|
+
// sibling mid-turn — plain `cancel()` writes `cancelled` directly, without waiting for the target's
|
|
1390
|
+
// in-flight turn to notice), this now-stale "suspended" outcome must not resurrect it: re-check the
|
|
1391
|
+
// CURRENT persisted status right before writing and echo it instead of clobbering a real cancel.
|
|
1392
|
+
const latest = await this.store.getRun(run.id);
|
|
1393
|
+
if (latest?.status === 'cancelled') {
|
|
1394
|
+
return { runId: run.id, status: 'cancelled', error: latest.error };
|
|
1395
|
+
}
|
|
1269
1396
|
await this.store.updateRun(run.id, { status: 'suspended', wakeAt: outcome.wakeAt, updatedAt });
|
|
1270
|
-
this.emit({
|
|
1397
|
+
this.emit({
|
|
1398
|
+
type: 'run.suspended',
|
|
1399
|
+
runId: run.id,
|
|
1400
|
+
workflow: run.workflow,
|
|
1401
|
+
namespace: run.namespace,
|
|
1402
|
+
});
|
|
1271
1403
|
return { runId: run.id, status: 'suspended' };
|
|
1272
1404
|
}
|
|
1273
1405
|
async runRemoteExecution(run, registered) {
|
|
@@ -1275,12 +1407,22 @@ export class WorkflowEngine {
|
|
|
1275
1407
|
if (run.status === 'pending') {
|
|
1276
1408
|
await this.store.updateRun(run.id, { status: 'running', updatedAt: new Date() });
|
|
1277
1409
|
run.status = 'running';
|
|
1278
|
-
this.emit({
|
|
1410
|
+
this.emit({
|
|
1411
|
+
type: 'run.started',
|
|
1412
|
+
runId: run.id,
|
|
1413
|
+
workflow: run.workflow,
|
|
1414
|
+
namespace: run.namespace,
|
|
1415
|
+
});
|
|
1279
1416
|
}
|
|
1280
1417
|
if (registered.singleton && !(await this.singletons.admit(run, registered.singleton))) {
|
|
1281
1418
|
const wakeAt = this.singletons.retryWakeAt();
|
|
1282
1419
|
await this.store.updateRun(run.id, { status: 'suspended', wakeAt, updatedAt: new Date() });
|
|
1283
|
-
this.emit({
|
|
1420
|
+
this.emit({
|
|
1421
|
+
type: 'run.suspended',
|
|
1422
|
+
runId: run.id,
|
|
1423
|
+
workflow: run.workflow,
|
|
1424
|
+
namespace: run.namespace,
|
|
1425
|
+
});
|
|
1284
1426
|
await this.store.releaseRunLock(run.id);
|
|
1285
1427
|
return { runId: run.id, status: 'suspended' };
|
|
1286
1428
|
}
|
|
@@ -1295,7 +1437,13 @@ export class WorkflowEngine {
|
|
|
1295
1437
|
stack: err instanceof Error ? err.stack : undefined,
|
|
1296
1438
|
};
|
|
1297
1439
|
await this.store.updateRun(run.id, { status: 'failed', error, updatedAt: new Date() });
|
|
1298
|
-
this.emit({
|
|
1440
|
+
this.emit({
|
|
1441
|
+
type: 'run.failed',
|
|
1442
|
+
runId: run.id,
|
|
1443
|
+
workflow: run.workflow,
|
|
1444
|
+
namespace: run.namespace,
|
|
1445
|
+
error,
|
|
1446
|
+
});
|
|
1299
1447
|
return { runId: run.id, status: 'failed', error };
|
|
1300
1448
|
}
|
|
1301
1449
|
if (decision.status === 'completed') {
|
|
@@ -1483,19 +1631,33 @@ export class WorkflowEngine {
|
|
|
1483
1631
|
// engine.signal(name, payload) delivers it. If the signal was already delivered (buffered
|
|
1484
1632
|
// before the workflow reached this point — e.g. signalWithStart), resolve it now and re-drive
|
|
1485
1633
|
// on a macrotask, AFTER this turn suspends and frees the run lock (a re-entrant resume bails).
|
|
1486
|
-
const
|
|
1487
|
-
if (buffered) {
|
|
1634
|
+
const deliverBuffered = async (payload) => {
|
|
1488
1635
|
await this.store.saveCheckpoint(instantCheckpoint({
|
|
1489
1636
|
runId: run.id,
|
|
1490
1637
|
seq: cmd.seq,
|
|
1491
1638
|
name: `signal:${cmd.signal}`,
|
|
1492
1639
|
kind: 'signal',
|
|
1493
|
-
output:
|
|
1640
|
+
output: payload,
|
|
1494
1641
|
}));
|
|
1495
1642
|
setTimeout(() => void this.resume(run.id).catch(() => undefined), 0);
|
|
1643
|
+
};
|
|
1644
|
+
const buffered = await this.store.takeBufferedSignal(cmd.signal);
|
|
1645
|
+
if (buffered) {
|
|
1646
|
+
await deliverBuffered(buffered.payload);
|
|
1496
1647
|
}
|
|
1497
1648
|
else {
|
|
1649
|
+
// Same reorder as the in-process ctx.waitForSignal: register on a miss, then re-check once
|
|
1650
|
+
// more before suspending — closing the lost-wake window where a signal races in between the
|
|
1651
|
+
// miss above and the registration below (see the interleaving proof at engine.signal).
|
|
1498
1652
|
await this.store.putSignalWaiter({ token: cmd.signal, runId: run.id, seq: cmd.seq });
|
|
1653
|
+
const lateBuffered = await this.store.takeBufferedSignal(cmd.signal);
|
|
1654
|
+
if (lateBuffered) {
|
|
1655
|
+
// Resolved it ourselves — remove OUR OWN row via the exact match (not
|
|
1656
|
+
// takeSignalWaiter(cmd.signal), which deletes ANY row for this token and could steal a
|
|
1657
|
+
// different run's waiter that has since claimed the same token).
|
|
1658
|
+
await this.store.removeSignalWaiter({ token: cmd.signal, runId: run.id, seq: cmd.seq });
|
|
1659
|
+
await deliverBuffered(lateBuffered.payload);
|
|
1660
|
+
}
|
|
1499
1661
|
}
|
|
1500
1662
|
}
|
|
1501
1663
|
else if (cmd.kind === 'startChild') {
|
|
@@ -1516,7 +1678,7 @@ export class WorkflowEngine {
|
|
|
1516
1678
|
parallelGroup: cmd.parallelGroup,
|
|
1517
1679
|
});
|
|
1518
1680
|
if (!(await this.store.getRun(childId))) {
|
|
1519
|
-
|
|
1681
|
+
this.startChildDeferred(cmd.workflow, cmd.input, childId);
|
|
1520
1682
|
}
|
|
1521
1683
|
}
|
|
1522
1684
|
else {
|
|
@@ -1533,14 +1695,24 @@ export class WorkflowEngine {
|
|
|
1533
1695
|
if (run.status === 'pending') {
|
|
1534
1696
|
await this.store.updateRun(run.id, { status: 'running', updatedAt: new Date() });
|
|
1535
1697
|
run.status = 'running';
|
|
1536
|
-
this.emit({
|
|
1698
|
+
this.emit({
|
|
1699
|
+
type: 'run.started',
|
|
1700
|
+
runId: run.id,
|
|
1701
|
+
workflow: run.workflow,
|
|
1702
|
+
namespace: run.namespace,
|
|
1703
|
+
});
|
|
1537
1704
|
}
|
|
1538
1705
|
// Singleton admission gate: if this run shares its key with `limit` older in-flight runs, wait
|
|
1539
1706
|
// (suspend on a short timer) until a slot frees instead of running now. Re-checked on each resume.
|
|
1540
1707
|
if (registered?.singleton && !(await this.singletons.admit(run, registered.singleton))) {
|
|
1541
1708
|
const wakeAt = this.singletons.retryWakeAt();
|
|
1542
1709
|
await this.store.updateRun(run.id, { status: 'suspended', wakeAt, updatedAt: new Date() });
|
|
1543
|
-
this.emit({
|
|
1710
|
+
this.emit({
|
|
1711
|
+
type: 'run.suspended',
|
|
1712
|
+
runId: run.id,
|
|
1713
|
+
workflow: run.workflow,
|
|
1714
|
+
namespace: run.namespace,
|
|
1715
|
+
});
|
|
1544
1716
|
await this.store.releaseRunLock(run.id);
|
|
1545
1717
|
return { runId: run.id, status: 'suspended' };
|
|
1546
1718
|
}
|
|
@@ -1557,7 +1729,10 @@ export class WorkflowEngine {
|
|
|
1557
1729
|
replay.set(cp.seq, cp);
|
|
1558
1730
|
const ctx = createWorkflowCtx(this.ctxHostFor(replay), run.id, compensations, run.workflow);
|
|
1559
1731
|
try {
|
|
1560
|
-
|
|
1732
|
+
// Establish the ambient ctx for the duration of this body turn so context-aware statics
|
|
1733
|
+
// (`BaseWorkflow.start`/`dispatch`) reachable from `fn` route through this run's `ctx.child`/
|
|
1734
|
+
// `ctx.startChild`. Re-set on every replay turn (each is its own async scope) — correct.
|
|
1735
|
+
const output = await workflowAls.run(ctx, () => fn(ctx, run.input));
|
|
1561
1736
|
return this.settleRun(run, { kind: 'completed', output });
|
|
1562
1737
|
}
|
|
1563
1738
|
catch (err) {
|
|
@@ -1571,7 +1746,12 @@ export class WorkflowEngine {
|
|
|
1571
1746
|
error: undefined,
|
|
1572
1747
|
updatedAt: new Date(),
|
|
1573
1748
|
});
|
|
1574
|
-
this.emit({
|
|
1749
|
+
this.emit({
|
|
1750
|
+
type: 'run.completed',
|
|
1751
|
+
runId: run.id,
|
|
1752
|
+
workflow: run.workflow,
|
|
1753
|
+
namespace: run.namespace,
|
|
1754
|
+
});
|
|
1575
1755
|
void this.notifyParent(run.id, { ok: true, value: undefined });
|
|
1576
1756
|
const nextId = nextContinuationId(run.id);
|
|
1577
1757
|
queueMicrotask(() => void this.start(run.workflow, err.input, nextId).catch(() => undefined));
|
|
@@ -1589,7 +1769,13 @@ export class WorkflowEngine {
|
|
|
1589
1769
|
}
|
|
1590
1770
|
const error = { message: 'cancelled' };
|
|
1591
1771
|
await this.store.updateRun(run.id, { status: 'cancelled', error, updatedAt: new Date() });
|
|
1592
|
-
this.emit({
|
|
1772
|
+
this.emit({
|
|
1773
|
+
type: 'run.failed',
|
|
1774
|
+
runId: run.id,
|
|
1775
|
+
workflow: run.workflow,
|
|
1776
|
+
namespace: run.namespace,
|
|
1777
|
+
error,
|
|
1778
|
+
});
|
|
1593
1779
|
return { runId: run.id, status: 'cancelled', error };
|
|
1594
1780
|
}
|
|
1595
1781
|
return this.settleRun(run, { kind: 'suspended', wakeAt: err.wakeAt });
|
|
@@ -1711,7 +1897,7 @@ export class WorkflowEngine {
|
|
|
1711
1897
|
callRemote: (runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission),
|
|
1712
1898
|
// Defer so a fast child can't reentrantly resume a still-running parent.
|
|
1713
1899
|
startChild: (workflow, input, id, priority) => {
|
|
1714
|
-
|
|
1900
|
+
this.startChildDeferred(workflow, input, id, { priority });
|
|
1715
1901
|
},
|
|
1716
1902
|
// Shallow-merge into the run's searchAttributes (the ctx primitive makes this exactly-once).
|
|
1717
1903
|
upsertSearchAttributes: async (runId, attrs) => {
|