@voyant-travel/workflows-orchestrator 0.107.10
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/LICENSE +201 -0
- package/NOTICE +52 -0
- package/README.md +76 -0
- package/dist/abort-registry.d.ts +6 -0
- package/dist/abort-registry.d.ts.map +1 -0
- package/dist/abort-registry.js +37 -0
- package/dist/concurrency.d.ts +31 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +145 -0
- package/dist/drive.d.ts +67 -0
- package/dist/drive.d.ts.map +1 -0
- package/dist/drive.js +373 -0
- package/dist/driver-inmemory.d.ts +30 -0
- package/dist/driver-inmemory.d.ts.map +1 -0
- package/dist/driver-inmemory.js +394 -0
- package/dist/event-router.d.ts +51 -0
- package/dist/event-router.d.ts.map +1 -0
- package/dist/event-router.js +68 -0
- package/dist/http-step-handler.d.ts +25 -0
- package/dist/http-step-handler.d.ts.map +1 -0
- package/dist/http-step-handler.js +78 -0
- package/dist/in-memory-store.d.ts +5 -0
- package/dist/in-memory-store.d.ts.map +1 -0
- package/dist/in-memory-store.js +41 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/journal-helpers.d.ts +3 -0
- package/dist/journal-helpers.d.ts.map +1 -0
- package/dist/journal-helpers.js +9 -0
- package/dist/orchestrator.d.ts +116 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +411 -0
- package/dist/resume-run.d.ts +40 -0
- package/dist/resume-run.d.ts.map +1 -0
- package/dist/resume-run.js +119 -0
- package/dist/schedule.d.ts +51 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +243 -0
- package/dist/testing/driver-compliance.d.ts +58 -0
- package/dist/testing/driver-compliance.d.ts.map +1 -0
- package/dist/testing/driver-compliance.js +667 -0
- package/dist/types.d.ts +182 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/package.json +51 -0
- package/src/__tests__/orchestrator-test-support.ts +18 -0
- package/src/abort-registry.ts +41 -0
- package/src/concurrency.ts +217 -0
- package/src/drive.ts +477 -0
- package/src/driver-inmemory.ts +511 -0
- package/src/event-router.ts +120 -0
- package/src/http-step-handler.ts +112 -0
- package/src/in-memory-store.ts +44 -0
- package/src/index.ts +73 -0
- package/src/journal-helpers.ts +11 -0
- package/src/orchestrator.ts +527 -0
- package/src/resume-run.ts +162 -0
- package/src/schedule.ts +310 -0
- package/src/testing/driver-compliance.ts +800 -0
- package/src/types.ts +201 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { registerRunAbort, signalRunAbort, unregisterRunAbort, } from "./abort-registry.js";
|
|
2
|
+
export { type ConcurrencyRunHooks, createInProcessConcurrencyCoordinator, type InProcessConcurrencyCoordinator, type RuntimeConcurrencyPolicy, resolveConcurrencyKey, WorkflowConcurrencyRejectedError, } from "./concurrency.js";
|
|
3
|
+
export { applyWaitpointInjection, type DriveOptions, driveUntilPaused } from "./drive.js";
|
|
4
|
+
export { createInMemoryDriver, type InMemoryDriverOptions, } from "./driver-inmemory.js";
|
|
5
|
+
export { type RouteEventArgs, type RouterMatch, routeEvent, } from "./event-router.js";
|
|
6
|
+
export { createHttpStepHandler, type HttpStepHandlerDeps, type HttpStepTarget, } from "./http-step-handler.js";
|
|
7
|
+
export { createInMemoryRunStore } from "./in-memory-store.js";
|
|
8
|
+
export { emptyJournal } from "./journal-helpers.js";
|
|
9
|
+
export { type CancelArgs, cancel, type OrchestratorDeps, type ResumeArgs, type ResumeDueAlarmsArgs, resume, resumeDueAlarms, type TriggerArgs, trigger, } from "./orchestrator.js";
|
|
10
|
+
export { type BuildResumeJournalInput, type BuildResumeJournalResult, type BuildSeededResumeJournalInput, buildResumeJournal, buildSeededResumeJournal, } from "./resume-run.js";
|
|
11
|
+
export { type CronSpec, computeNextFire, createScheduler, manifestScheduleSources, nextCronFire, parseCron, type SchedulableDeclaration, type SchedulerDeps, type SchedulerHandle, type ScheduleSource, toMs, } from "./schedule.js";
|
|
12
|
+
export * from "./types.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,kBAAkB,GACnB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,KAAK,mBAAmB,EACxB,qCAAqC,EACrC,KAAK,+BAA+B,EACpC,KAAK,wBAAwB,EAC7B,qBAAqB,EACrB,gCAAgC,GACjC,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,uBAAuB,EAAE,KAAK,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AACzF,OAAO,EACL,oBAAoB,EACpB,KAAK,qBAAqB,GAC3B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,UAAU,GACX,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,cAAc,GACpB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EACL,KAAK,UAAU,EACf,MAAM,EACN,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,mBAAmB,EACxB,MAAM,EACN,eAAe,EACf,KAAK,WAAW,EAChB,OAAO,GACR,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,6BAA6B,EAClC,kBAAkB,EAClB,wBAAwB,GACzB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,KAAK,QAAQ,EACb,eAAe,EACf,eAAe,EACf,uBAAuB,EACvB,YAAY,EACZ,SAAS,EACT,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,IAAI,GACL,MAAM,eAAe,CAAA;AACtB,cAAc,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @voyant-travel/workflows-orchestrator
|
|
2
|
+
//
|
|
3
|
+
// Reference orchestrator for Voyant Workflows. Drives runs through
|
|
4
|
+
// the tenant step handler over the v1 wire protocol. Transport- and
|
|
5
|
+
// storage-agnostic: compose with a RunRecordStore of your choice
|
|
6
|
+
// (in-memory for tests, Postgres-backed for production).
|
|
7
|
+
//
|
|
8
|
+
// See docs/runtime-protocol.md §2 + §5 for the contract this
|
|
9
|
+
// implements and docs/design.md §6 for the broader orchestrator
|
|
10
|
+
// state-machine design.
|
|
11
|
+
export { registerRunAbort, signalRunAbort, unregisterRunAbort, } from "./abort-registry.js";
|
|
12
|
+
export { createInProcessConcurrencyCoordinator, resolveConcurrencyKey, WorkflowConcurrencyRejectedError, } from "./concurrency.js";
|
|
13
|
+
export { applyWaitpointInjection, driveUntilPaused } from "./drive.js";
|
|
14
|
+
export { createInMemoryDriver, } from "./driver-inmemory.js";
|
|
15
|
+
export { routeEvent, } from "./event-router.js";
|
|
16
|
+
export { createHttpStepHandler, } from "./http-step-handler.js";
|
|
17
|
+
export { createInMemoryRunStore } from "./in-memory-store.js";
|
|
18
|
+
export { emptyJournal } from "./journal-helpers.js";
|
|
19
|
+
export { cancel, resume, resumeDueAlarms, trigger, } from "./orchestrator.js";
|
|
20
|
+
export { buildResumeJournal, buildSeededResumeJournal, } from "./resume-run.js";
|
|
21
|
+
export { computeNextFire, createScheduler, manifestScheduleSources, nextCronFire, parseCron, toMs, } from "./schedule.js";
|
|
22
|
+
export * from "./types.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journal-helpers.d.ts","sourceRoot":"","sources":["../src/journal-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAA;AAErE,wBAAgB,YAAY,IAAI,YAAY,CAQ3C"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Duration } from "@voyant-travel/workflows";
|
|
2
|
+
import { type DriveOptions } from "./drive.js";
|
|
3
|
+
import type { JournalSlice, RunRecord, RunRecordStore, RunTrigger, StepHandler, WaitpointInjection } from "./types.js";
|
|
4
|
+
export interface TriggerArgs {
|
|
5
|
+
workflowId: string;
|
|
6
|
+
workflowVersion: string;
|
|
7
|
+
input: unknown;
|
|
8
|
+
tenantMeta: RunRecord["tenantMeta"];
|
|
9
|
+
environment?: RunRecord["environment"];
|
|
10
|
+
triggeredBy?: RunTrigger;
|
|
11
|
+
tags?: string[];
|
|
12
|
+
runNumber?: number;
|
|
13
|
+
/** Optional id to use; defaults to `run_` + crypto random. */
|
|
14
|
+
runId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Caller-supplied idempotency token. When set and the caller did not
|
|
17
|
+
* explicitly pass `runId`, the orchestrator derives a deterministic
|
|
18
|
+
* runId from `(workflowId, idempotencyKey)` so retries of the same
|
|
19
|
+
* trigger return the same run record without re-driving. The key is
|
|
20
|
+
* also persisted onto `RunRecord.idempotencyKey` so persistent stores
|
|
21
|
+
* can populate dedup columns / unique indexes natively.
|
|
22
|
+
*
|
|
23
|
+
* See architecture doc §15.2 for the full ingest-side derivation
|
|
24
|
+
* (eventId → idempotencyKey via `${filterId}:${eventId}`).
|
|
25
|
+
*/
|
|
26
|
+
idempotencyKey?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional trigger-time delay. When set to a future instant, the
|
|
29
|
+
* orchestrator persists the run in `waiting` on a synthetic DATETIME
|
|
30
|
+
* waitpoint and leaves execution to the normal wakeup/time-wheel path.
|
|
31
|
+
*/
|
|
32
|
+
delay?: Duration | Date;
|
|
33
|
+
/** Higher values are claimed first by scheduler/time-wheel stores. */
|
|
34
|
+
priority?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Optional journal seed. Used by external replay/resume callers
|
|
37
|
+
* that need a new run to skip steps already completed by a parent
|
|
38
|
+
* run.
|
|
39
|
+
*/
|
|
40
|
+
initialJournal?: JournalSlice;
|
|
41
|
+
/**
|
|
42
|
+
* Metadata cursor paired with `initialJournal`. Resume callers that
|
|
43
|
+
* seed a journal with an existing `metadataState` must also seed the
|
|
44
|
+
* positional cursor so replayed metadata mutations are not applied
|
|
45
|
+
* twice.
|
|
46
|
+
*/
|
|
47
|
+
initialMetadataAppliedCount?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Compute-time budget in ms, typically from `WorkflowConfig.timeout`.
|
|
50
|
+
* Parked time on waitpoints does not count against this. When the
|
|
51
|
+
* cumulative invocation time exceeds it, the run ends `failed`
|
|
52
|
+
* with `code: "WORKFLOW_TIMEOUT"`. Undefined / 0 = no limit.
|
|
53
|
+
*/
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
/**
|
|
56
|
+
* For child runs spawned by `ctx.invoke` on a parent that may park.
|
|
57
|
+
* When set, the orchestrator records it on the child's record so
|
|
58
|
+
* the child's terminal status cascade-resumes the parent.
|
|
59
|
+
*/
|
|
60
|
+
parent?: {
|
|
61
|
+
runId: string;
|
|
62
|
+
waitpointId: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Internal lifecycle hook used by driver-level coordinators that need
|
|
66
|
+
* the persisted run id before the first invocation starts.
|
|
67
|
+
*/
|
|
68
|
+
onRunRecordCreated?: (record: RunRecord) => void;
|
|
69
|
+
}
|
|
70
|
+
export interface OrchestratorDeps extends DriveOptions {
|
|
71
|
+
store: RunRecordStore;
|
|
72
|
+
handler: StepHandler;
|
|
73
|
+
/** id generator; defaults to `run_<random>`. */
|
|
74
|
+
idGenerator?: () => string;
|
|
75
|
+
}
|
|
76
|
+
export declare function trigger(args: TriggerArgs, deps: OrchestratorDeps): Promise<RunRecord>;
|
|
77
|
+
export interface ResumeArgs {
|
|
78
|
+
runId: string;
|
|
79
|
+
injection: WaitpointInjection;
|
|
80
|
+
}
|
|
81
|
+
export declare function resume(args: ResumeArgs, deps: OrchestratorDeps): Promise<{
|
|
82
|
+
ok: true;
|
|
83
|
+
record: RunRecord;
|
|
84
|
+
} | {
|
|
85
|
+
ok: false;
|
|
86
|
+
status: "not_found" | "not_parked" | "no_match";
|
|
87
|
+
message: string;
|
|
88
|
+
}>;
|
|
89
|
+
export interface ResumeDueAlarmsArgs {
|
|
90
|
+
runId: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve every DATETIME waitpoint whose `wakeAt` has passed, drive
|
|
94
|
+
* the run forward, and persist. Returns the saved record, or null
|
|
95
|
+
* when the run isn't in `waiting` state (already terminal / running
|
|
96
|
+
* elsewhere), or when no DATETIME waitpoints are actually due yet —
|
|
97
|
+
* both are no-ops that the caller can treat as "nothing to do."
|
|
98
|
+
*
|
|
99
|
+
* Callers (local serve loop, CF DO alarm handler) are responsible for
|
|
100
|
+
* scheduling the actual wake-up timer. This function is transport-
|
|
101
|
+
* agnostic: given `now()`, it does the resolve + drive + save.
|
|
102
|
+
*/
|
|
103
|
+
export declare function resumeDueAlarms(args: ResumeDueAlarmsArgs, deps: OrchestratorDeps): Promise<RunRecord | null>;
|
|
104
|
+
export interface CancelArgs {
|
|
105
|
+
runId: string;
|
|
106
|
+
reason?: string;
|
|
107
|
+
}
|
|
108
|
+
export declare function cancel(args: CancelArgs, deps: OrchestratorDeps): Promise<{
|
|
109
|
+
ok: true;
|
|
110
|
+
record: RunRecord;
|
|
111
|
+
} | {
|
|
112
|
+
ok: false;
|
|
113
|
+
status: "not_found" | "already_terminal";
|
|
114
|
+
message: string;
|
|
115
|
+
}>;
|
|
116
|
+
//# sourceMappingURL=orchestrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../src/orchestrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// agent-quality: file-size exception -- owner: workflows-orchestrator; existing module stays co-located until a dedicated split preserves behavior and tests.
|
|
2
|
+
// Public entry points for the reference orchestrator.
|
|
3
|
+
//
|
|
4
|
+
// `trigger()` creates a RunRecord, drives it forward through the
|
|
5
|
+
// tenant handler, and persists the resulting record.
|
|
6
|
+
// `resume()` accepts a waitpoint injection for a parked run, applies
|
|
7
|
+
// it, drives forward, and persists.
|
|
8
|
+
// `cancel()` closes out a run without running compensations (they
|
|
9
|
+
// must come from the tenant handler, not the orchestrator).
|
|
10
|
+
import { registerRunAbort, signalRunAbort, unregisterRunAbort } from "./abort-registry.js";
|
|
11
|
+
import { applyWaitpointInjection, driveUntilPaused } from "./drive.js";
|
|
12
|
+
import { emptyJournal } from "./journal-helpers.js";
|
|
13
|
+
export async function trigger(args, deps) {
|
|
14
|
+
const now = deps.now ?? (() => Date.now());
|
|
15
|
+
const createdAt = now();
|
|
16
|
+
// Idempotency: caller-supplied `runId` wins (explicit). Otherwise, when
|
|
17
|
+
// `idempotencyKey` is supplied, derive a deterministic runId from
|
|
18
|
+
// `(workflowId, idempotencyKey)`. We then route through `store.tryInsert`
|
|
19
|
+
// — atomic check-or-insert — so concurrent triggers with the same
|
|
20
|
+
// derived runId never both proceed to drive (architecture doc §15.2,
|
|
21
|
+
// closes the race the get-then-save pattern leaves open).
|
|
22
|
+
const explicitRunId = args.runId;
|
|
23
|
+
const idempotencyKey = args.idempotencyKey;
|
|
24
|
+
const derivedRunId = idempotencyKey !== undefined ? `idem-${args.workflowId}-${idempotencyKey}` : undefined;
|
|
25
|
+
const id = explicitRunId ?? derivedRunId ?? deps.idGenerator?.() ?? defaultRunId(now);
|
|
26
|
+
const isIdempotent = explicitRunId !== undefined || derivedRunId !== undefined;
|
|
27
|
+
const delayWakeAt = resolveDelayWakeAt(args.delay, createdAt);
|
|
28
|
+
const record = {
|
|
29
|
+
id,
|
|
30
|
+
workflowId: args.workflowId,
|
|
31
|
+
workflowVersion: args.workflowVersion,
|
|
32
|
+
status: delayWakeAt !== undefined ? "waiting" : "running",
|
|
33
|
+
input: args.input,
|
|
34
|
+
journal: args.initialJournal ? cloneJournal(args.initialJournal) : emptyJournal(),
|
|
35
|
+
invocationCount: 0,
|
|
36
|
+
metadataAppliedCount: args.initialMetadataAppliedCount ?? 0,
|
|
37
|
+
computeTimeMs: 0,
|
|
38
|
+
timeoutMs: args.timeoutMs,
|
|
39
|
+
priority: args.priority,
|
|
40
|
+
parent: args.parent,
|
|
41
|
+
pendingWaitpoints: delayWakeAt !== undefined
|
|
42
|
+
? [
|
|
43
|
+
{
|
|
44
|
+
clientWaitpointId: triggerDelayWaitpointId(id),
|
|
45
|
+
kind: "DATETIME",
|
|
46
|
+
meta: {
|
|
47
|
+
wakeAt: delayWakeAt,
|
|
48
|
+
durationMs: Math.max(0, delayWakeAt - createdAt),
|
|
49
|
+
source: "trigger.delay",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
: [],
|
|
54
|
+
streams: {},
|
|
55
|
+
startedAt: delayWakeAt ?? createdAt,
|
|
56
|
+
triggeredBy: args.triggeredBy ?? { kind: "api" },
|
|
57
|
+
tags: args.tags ?? [],
|
|
58
|
+
environment: args.environment ?? "development",
|
|
59
|
+
tenantMeta: args.tenantMeta,
|
|
60
|
+
runMeta: { number: args.runNumber ?? 1, attempt: 1 },
|
|
61
|
+
idempotencyKey,
|
|
62
|
+
};
|
|
63
|
+
// Idempotent path uses tryInsert; non-idempotent (auto-generated) ids
|
|
64
|
+
// can't collide so the existing save() is sufficient.
|
|
65
|
+
if (isIdempotent) {
|
|
66
|
+
const result = await deps.store.tryInsert(record);
|
|
67
|
+
if (!result.created) {
|
|
68
|
+
// Another caller raced in first. Return their record without
|
|
69
|
+
// re-driving — drive() is what actually fires side effects.
|
|
70
|
+
return result.record;
|
|
71
|
+
}
|
|
72
|
+
args.onRunRecordCreated?.(record);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Persist up-front so concurrent `cancel(runId)` calls can find the
|
|
76
|
+
// run before any invocation has completed.
|
|
77
|
+
await deps.store.save(record);
|
|
78
|
+
args.onRunRecordCreated?.(record);
|
|
79
|
+
}
|
|
80
|
+
if (delayWakeAt !== undefined) {
|
|
81
|
+
return record;
|
|
82
|
+
}
|
|
83
|
+
const abortCtrl = registerRunAbort(id);
|
|
84
|
+
try {
|
|
85
|
+
await driveUntilPaused(record, {
|
|
86
|
+
...driveOptionsFor(deps),
|
|
87
|
+
signal: abortCtrl.signal,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
unregisterRunAbort(id);
|
|
92
|
+
}
|
|
93
|
+
// If an external cancel fired during drive, the aborted step may
|
|
94
|
+
// have surfaced as `failed` (the step threw on abort). The user's
|
|
95
|
+
// intent was cancel, so adopt the store's `cancelled` status.
|
|
96
|
+
if (abortCtrl.signal.aborted) {
|
|
97
|
+
const latest = await deps.store.get(id);
|
|
98
|
+
if (latest?.status === "cancelled") {
|
|
99
|
+
record.status = "cancelled";
|
|
100
|
+
record.completedAt = latest.completedAt ?? now();
|
|
101
|
+
record.pendingWaitpoints = [];
|
|
102
|
+
record.error = latest.error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return deps.store.save(record);
|
|
106
|
+
}
|
|
107
|
+
function resolveDelayWakeAt(delay, now) {
|
|
108
|
+
if (delay === undefined)
|
|
109
|
+
return undefined;
|
|
110
|
+
const wakeAt = delay instanceof Date ? delay.getTime() : now + durationToMs(delay);
|
|
111
|
+
return wakeAt > now ? wakeAt : undefined;
|
|
112
|
+
}
|
|
113
|
+
function durationToMs(duration) {
|
|
114
|
+
if (typeof duration === "number")
|
|
115
|
+
return duration;
|
|
116
|
+
const m = /^(\d+)(ms|s|m|h|d|w)$/.exec(duration);
|
|
117
|
+
if (!m)
|
|
118
|
+
throw new Error(`invalid duration: ${String(duration)}`);
|
|
119
|
+
const n = Number(m[1]);
|
|
120
|
+
switch (m[2]) {
|
|
121
|
+
case "ms":
|
|
122
|
+
return n;
|
|
123
|
+
case "s":
|
|
124
|
+
return n * 1_000;
|
|
125
|
+
case "m":
|
|
126
|
+
return n * 60_000;
|
|
127
|
+
case "h":
|
|
128
|
+
return n * 3_600_000;
|
|
129
|
+
case "d":
|
|
130
|
+
return n * 86_400_000;
|
|
131
|
+
case "w":
|
|
132
|
+
return n * 604_800_000;
|
|
133
|
+
default:
|
|
134
|
+
throw new Error(`invalid duration unit: ${m[2]}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function triggerDelayWaitpointId(runId) {
|
|
138
|
+
return `trigger-delay:${runId}`;
|
|
139
|
+
}
|
|
140
|
+
function cloneJournal(journal) {
|
|
141
|
+
return structuredClone(journal);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Build DriveOptions that carry a `triggerChild` hook bound to the
|
|
145
|
+
* same deps — so `ctx.invoke(child, ...)` recursively runs the child
|
|
146
|
+
* through the same orchestrator, store, and handler — plus a
|
|
147
|
+
* `beforeInvocation` hook that persists mid-flight progress and
|
|
148
|
+
* honors concurrent cancellations.
|
|
149
|
+
*/
|
|
150
|
+
function driveOptionsFor(deps) {
|
|
151
|
+
const now = deps.now ?? (() => Date.now());
|
|
152
|
+
return {
|
|
153
|
+
...deps,
|
|
154
|
+
triggerChild: async ({ parent, waitpoint }) => {
|
|
155
|
+
const childWorkflowId = String(waitpoint.meta.childWorkflowId);
|
|
156
|
+
const childInput = waitpoint.meta.childInput;
|
|
157
|
+
const detach = waitpoint.meta.detach === true;
|
|
158
|
+
return trigger({
|
|
159
|
+
workflowId: childWorkflowId,
|
|
160
|
+
// Children inherit the parent's workflow version slot unless
|
|
161
|
+
// lockToVersion is set; for v1 we always inherit.
|
|
162
|
+
workflowVersion: parent.workflowVersion,
|
|
163
|
+
input: childInput,
|
|
164
|
+
tenantMeta: parent.tenantMeta,
|
|
165
|
+
environment: parent.environment,
|
|
166
|
+
// Inherit the parent's trigger kind for run-tree observability.
|
|
167
|
+
triggeredBy: parent.triggeredBy,
|
|
168
|
+
tags: Array.isArray(waitpoint.meta.tags) ? waitpoint.meta.tags : [],
|
|
169
|
+
// Lineage pointer: if this child parks, its terminal status
|
|
170
|
+
// cascade-resumes the parent at this specific waitpoint.
|
|
171
|
+
parent: detach
|
|
172
|
+
? undefined
|
|
173
|
+
: { runId: parent.id, waitpointId: waitpoint.clientWaitpointId },
|
|
174
|
+
}, deps);
|
|
175
|
+
},
|
|
176
|
+
beforeInvocation: async (rec) => {
|
|
177
|
+
// Read-first: a concurrent `cancel()` may have flipped the
|
|
178
|
+
// stored status. If we saved first, we'd overwrite it.
|
|
179
|
+
const latest = await deps.store.get(rec.id);
|
|
180
|
+
if (latest && latest.status === "cancelled") {
|
|
181
|
+
rec.status = "cancelled";
|
|
182
|
+
rec.completedAt = latest.completedAt ?? now();
|
|
183
|
+
rec.pendingWaitpoints = [];
|
|
184
|
+
if (latest.error)
|
|
185
|
+
rec.error = latest.error;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
// Persist mid-flight progress so the dashboard sees updates and
|
|
189
|
+
// the next concurrent cancel() has an up-to-date target.
|
|
190
|
+
await deps.store.save(rec);
|
|
191
|
+
return true;
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export async function resume(args, deps) {
|
|
196
|
+
const existing = await deps.store.get(args.runId);
|
|
197
|
+
if (!existing) {
|
|
198
|
+
return { ok: false, status: "not_found", message: `run ${args.runId} not found` };
|
|
199
|
+
}
|
|
200
|
+
if (existing.status !== "waiting") {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
status: "not_parked",
|
|
204
|
+
message: `run ${args.runId} is not parked (status: ${existing.status})`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const ok = applyWaitpointInjection(existing, args.injection, deps.now);
|
|
208
|
+
if (!ok.ok) {
|
|
209
|
+
return { ok: false, status: "no_match", message: ok.message };
|
|
210
|
+
}
|
|
211
|
+
const abortCtrl = registerRunAbort(existing.id);
|
|
212
|
+
try {
|
|
213
|
+
await driveUntilPaused(existing, {
|
|
214
|
+
...driveOptionsFor(deps),
|
|
215
|
+
signal: abortCtrl.signal,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
unregisterRunAbort(existing.id);
|
|
220
|
+
}
|
|
221
|
+
if (abortCtrl.signal.aborted) {
|
|
222
|
+
const latest = await deps.store.get(existing.id);
|
|
223
|
+
if (latest?.status === "cancelled") {
|
|
224
|
+
const now = deps.now ?? (() => Date.now());
|
|
225
|
+
existing.status = "cancelled";
|
|
226
|
+
existing.completedAt = latest.completedAt ?? now();
|
|
227
|
+
existing.pendingWaitpoints = [];
|
|
228
|
+
existing.error = latest.error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const saved = await deps.store.save(existing);
|
|
232
|
+
// If this resume drove the run into a terminal state and it's a
|
|
233
|
+
// child of some parent, cascade the resolution.
|
|
234
|
+
if (isTerminalStatus(saved.status)) {
|
|
235
|
+
await cascadeResumeParent(saved, deps);
|
|
236
|
+
}
|
|
237
|
+
return { ok: true, record: saved };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Resolve every DATETIME waitpoint whose `wakeAt` has passed, drive
|
|
241
|
+
* the run forward, and persist. Returns the saved record, or null
|
|
242
|
+
* when the run isn't in `waiting` state (already terminal / running
|
|
243
|
+
* elsewhere), or when no DATETIME waitpoints are actually due yet —
|
|
244
|
+
* both are no-ops that the caller can treat as "nothing to do."
|
|
245
|
+
*
|
|
246
|
+
* Callers (local serve loop, CF DO alarm handler) are responsible for
|
|
247
|
+
* scheduling the actual wake-up timer. This function is transport-
|
|
248
|
+
* agnostic: given `now()`, it does the resolve + drive + save.
|
|
249
|
+
*/
|
|
250
|
+
export async function resumeDueAlarms(args, deps) {
|
|
251
|
+
const record = await deps.store.get(args.runId);
|
|
252
|
+
if (!record)
|
|
253
|
+
return null;
|
|
254
|
+
if (record.status !== "waiting")
|
|
255
|
+
return null;
|
|
256
|
+
const now = deps.now ?? (() => Date.now());
|
|
257
|
+
const at = now();
|
|
258
|
+
const stillPending = [];
|
|
259
|
+
let resolvedAny = false;
|
|
260
|
+
for (const wp of record.pendingWaitpoints) {
|
|
261
|
+
const wakeAt = typeof wp.meta.wakeAt === "number" ? wp.meta.wakeAt : undefined;
|
|
262
|
+
if (wp.kind === "DATETIME" && wakeAt !== undefined && wakeAt <= at) {
|
|
263
|
+
record.journal.waitpointsResolved[wp.clientWaitpointId] = {
|
|
264
|
+
kind: "DATETIME",
|
|
265
|
+
resolvedAt: at,
|
|
266
|
+
source: "replay",
|
|
267
|
+
};
|
|
268
|
+
resolvedAny = true;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
stillPending.push(wp);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (!resolvedAny)
|
|
275
|
+
return null;
|
|
276
|
+
record.pendingWaitpoints = stillPending;
|
|
277
|
+
if (record.pendingWaitpoints.length === 0)
|
|
278
|
+
record.status = "running";
|
|
279
|
+
const abortCtrl = registerRunAbort(record.id);
|
|
280
|
+
try {
|
|
281
|
+
await driveUntilPaused(record, {
|
|
282
|
+
...driveOptionsFor(deps),
|
|
283
|
+
signal: abortCtrl.signal,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
unregisterRunAbort(record.id);
|
|
288
|
+
}
|
|
289
|
+
const saved = await deps.store.save(record);
|
|
290
|
+
if (isTerminalStatus(saved.status)) {
|
|
291
|
+
await cascadeResumeParent(saved, deps);
|
|
292
|
+
}
|
|
293
|
+
return saved;
|
|
294
|
+
}
|
|
295
|
+
export async function cancel(args, deps) {
|
|
296
|
+
const existing = await deps.store.get(args.runId);
|
|
297
|
+
if (!existing) {
|
|
298
|
+
return { ok: false, status: "not_found", message: `run ${args.runId} not found` };
|
|
299
|
+
}
|
|
300
|
+
if (existing.status !== "waiting" && existing.status !== "running") {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
status: "already_terminal",
|
|
304
|
+
message: `run ${args.runId} is already terminal (status: ${existing.status})`,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const now = deps.now ?? (() => Date.now());
|
|
308
|
+
existing.status = "cancelled";
|
|
309
|
+
existing.completedAt = now();
|
|
310
|
+
existing.pendingWaitpoints = [];
|
|
311
|
+
if (args.reason) {
|
|
312
|
+
existing.error = {
|
|
313
|
+
category: "USER_ERROR",
|
|
314
|
+
code: "CANCELLED",
|
|
315
|
+
message: args.reason,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const saved = await deps.store.save(existing);
|
|
319
|
+
// Best-effort mid-step abort: if the run is in-flight in this
|
|
320
|
+
// process, fire its AbortSignal so step bodies that observe
|
|
321
|
+
// `ctx.signal` (fetches, sleeps, etc.) stop immediately. Returns
|
|
322
|
+
// `false` when no controller is registered (run is in another
|
|
323
|
+
// process, or drive has already exited) — that's fine; the
|
|
324
|
+
// status flip + between-invocation recheck cover that path.
|
|
325
|
+
signalRunAbort(existing.id, args.reason);
|
|
326
|
+
// If this cancel was on a child with a parked parent, surface the
|
|
327
|
+
// cancellation to the parent as a RUN-waitpoint error.
|
|
328
|
+
if (isTerminalStatus(saved.status)) {
|
|
329
|
+
await cascadeResumeParent(saved, deps);
|
|
330
|
+
}
|
|
331
|
+
return { ok: true, record: saved };
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* When a child run reaches a terminal state, look up its `parent`
|
|
335
|
+
* pointer and resume the parent's matching RUN waitpoint with the
|
|
336
|
+
* child's output / error. Best-effort: if the parent can't be found
|
|
337
|
+
* or is no longer parked, silently drop (the parent's own drive will
|
|
338
|
+
* observe the child's state on replay via a subsequent trigger).
|
|
339
|
+
*/
|
|
340
|
+
async function cascadeResumeParent(child, deps) {
|
|
341
|
+
if (!child.parent)
|
|
342
|
+
return;
|
|
343
|
+
const parent = await deps.store.get(child.parent.runId);
|
|
344
|
+
if (!parent)
|
|
345
|
+
return;
|
|
346
|
+
if (parent.status !== "waiting")
|
|
347
|
+
return;
|
|
348
|
+
const wpIdx = parent.pendingWaitpoints.findIndex((w) => w.clientWaitpointId === child.parent.waitpointId);
|
|
349
|
+
if (wpIdx < 0)
|
|
350
|
+
return;
|
|
351
|
+
const now = deps.now ?? (() => Date.now());
|
|
352
|
+
const at = now();
|
|
353
|
+
if (child.status === "completed") {
|
|
354
|
+
parent.journal.waitpointsResolved[child.parent.waitpointId] = {
|
|
355
|
+
kind: "RUN",
|
|
356
|
+
resolvedAt: at,
|
|
357
|
+
payload: child.output,
|
|
358
|
+
source: "replay",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
parent.journal.waitpointsResolved[child.parent.waitpointId] = {
|
|
363
|
+
kind: "RUN",
|
|
364
|
+
resolvedAt: at,
|
|
365
|
+
source: "replay",
|
|
366
|
+
error: {
|
|
367
|
+
category: child.error?.category ?? "USER_ERROR",
|
|
368
|
+
code: child.error?.code ?? "CHILD_RUN_ENDED",
|
|
369
|
+
message: child.error?.message ?? `child run ended with status ${child.status}`,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
parent.pendingWaitpoints.splice(wpIdx, 1);
|
|
374
|
+
if (parent.pendingWaitpoints.length === 0) {
|
|
375
|
+
parent.status = "running";
|
|
376
|
+
}
|
|
377
|
+
// Re-drive the parent. This nested drive goes through the same
|
|
378
|
+
// handler / store / hooks, so the parent's own parent (if any)
|
|
379
|
+
// will also cascade-resume when appropriate.
|
|
380
|
+
const abortCtrl = registerRunAbort(parent.id);
|
|
381
|
+
try {
|
|
382
|
+
await driveUntilPaused(parent, {
|
|
383
|
+
...driveOptionsFor(deps),
|
|
384
|
+
signal: abortCtrl.signal,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
unregisterRunAbort(parent.id);
|
|
389
|
+
}
|
|
390
|
+
await deps.store.save(parent);
|
|
391
|
+
// The parent might itself have a parent — recurse.
|
|
392
|
+
if (isTerminalStatus(parent.status)) {
|
|
393
|
+
await cascadeResumeParent(parent, deps);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function isTerminalStatus(s) {
|
|
397
|
+
return (s === "completed" ||
|
|
398
|
+
s === "failed" ||
|
|
399
|
+
s === "cancelled" ||
|
|
400
|
+
s === "compensated" ||
|
|
401
|
+
s === "compensation_failed");
|
|
402
|
+
}
|
|
403
|
+
function defaultRunId(now) {
|
|
404
|
+
const ts = now().toString(36);
|
|
405
|
+
// Non-cryptographic; orchestrator core exposes `idGenerator` for
|
|
406
|
+
// callers that want stronger guarantees.
|
|
407
|
+
const rand = Math.floor(Math.random() * 1_000_000)
|
|
408
|
+
.toString(36)
|
|
409
|
+
.padStart(4, "0");
|
|
410
|
+
return `run_${ts}_${rand}`;
|
|
411
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { JournalSlice, RunRecord } from "./types.js";
|
|
2
|
+
export interface BuildResumeJournalInput {
|
|
3
|
+
parent: RunRecord;
|
|
4
|
+
resumeFromStep?: string;
|
|
5
|
+
seedResults?: Record<string, unknown>;
|
|
6
|
+
now?: () => number;
|
|
7
|
+
}
|
|
8
|
+
export interface BuildResumeJournalResult {
|
|
9
|
+
resumeFromStep: string;
|
|
10
|
+
journal: JournalSlice;
|
|
11
|
+
metadataAppliedCount: number;
|
|
12
|
+
}
|
|
13
|
+
export interface BuildSeededResumeJournalInput {
|
|
14
|
+
parentRunId: string;
|
|
15
|
+
resumeFromStep: string;
|
|
16
|
+
seedResults: Record<string, unknown>;
|
|
17
|
+
metadataState?: Record<string, unknown>;
|
|
18
|
+
metadataAppliedCount?: number;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
}
|
|
21
|
+
export declare function buildResumeJournal(input: BuildResumeJournalInput): BuildResumeJournalResult;
|
|
22
|
+
export declare function buildSeededResumeJournal(input: BuildSeededResumeJournalInput): BuildResumeJournalResult;
|
|
23
|
+
export type SeedResultsValidation = {
|
|
24
|
+
ok: true;
|
|
25
|
+
seedResults: Record<string, unknown>;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Strict structural validation for caller-supplied `seedResults`
|
|
32
|
+
* (`POST /api/runs/:id/resume`). Seeded entries are written verbatim
|
|
33
|
+
* into the new run's journal as already-completed steps, so they let
|
|
34
|
+
* the caller assert "this step ran and produced this output" — they
|
|
35
|
+
* must be gated behind an operator credential AND shape-checked:
|
|
36
|
+
* a record of bounded, control-character-free step ids to
|
|
37
|
+
* JSON-serializable values, bounded in count and total size.
|
|
38
|
+
*/
|
|
39
|
+
export declare function validateSeedResults(value: unknown): SeedResultsValidation;
|
|
40
|
+
//# sourceMappingURL=resume-run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resume-run.d.ts","sourceRoot":"","sources":["../src/resume-run.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAoB,MAAM,YAAY,CAAA;AAE3E,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,SAAS,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,YAAY,CAAA;IACrB,oBAAoB,EAAE,MAAM,CAAA;CAC7B;AAED,MAAM,WAAW,6BAA6B;IAC5C,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACvC,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,wBAAwB,CAwC3F;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,6BAA6B,GACnC,wBAAwB,CAgB1B;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAClD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAQlC;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,CAsCzE"}
|