@voyantjs/workflows-orchestrator-node 0.28.1 → 0.29.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/fs-run-record-store.d.ts.map +1 -1
- package/dist/fs-run-record-store.js +23 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/node-standalone-driver.d.ts +72 -0
- package/dist/node-standalone-driver.d.ts.map +1 -0
- package/dist/node-standalone-driver.js +357 -0
- package/dist/postgres-manifest-store.d.ts +35 -0
- package/dist/postgres-manifest-store.d.ts.map +1 -0
- package/dist/postgres-manifest-store.js +82 -0
- package/dist/postgres-run-record-store.d.ts +9 -0
- package/dist/postgres-run-record-store.d.ts.map +1 -0
- package/dist/postgres-run-record-store.js +161 -0
- package/dist/postgres-schema.d.ts +121 -0
- package/dist/postgres-schema.d.ts.map +1 -1
- package/dist/postgres-schema.js +40 -1
- package/drizzle/0003_idempotency_key.sql +2 -0
- package/drizzle/0004_workflow_manifests.sql +10 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +3 -3
- package/src/fs-run-record-store.ts +21 -0
- package/src/index.ts +15 -0
- package/src/node-standalone-driver.ts +491 -0
- package/src/postgres-manifest-store.ts +144 -0
- package/src/postgres-run-record-store.ts +187 -0
- package/src/postgres-schema.ts +56 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs-run-record-store.d.ts","sourceRoot":"","sources":["../src/fs-run-record-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACT,cAAc,EACf,MAAM,kCAAkC,CAAA;AAEzC,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,uBAA4B,GAAG,cAAc,
|
|
1
|
+
{"version":3,"file":"fs-run-record-store.d.ts","sourceRoot":"","sources":["../src/fs-run-record-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACT,cAAc,EACf,MAAM,kCAAkC,CAAA;AAEzC,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,uBAA4B,GAAG,cAAc,CAkDzF;AA+BD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,SAAS,EAAE,EAC1B,MAAM,GAAE;IACN,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,qBAAqB,CAAA;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;CACV,GACL,SAAS,EAAE,CASb"}
|
|
@@ -11,6 +11,29 @@ export function createFsRunRecordStore(opts = {}) {
|
|
|
11
11
|
await writeFile(join(rootDir, record.id, "run-record.json"), JSON.stringify(record, null, 2));
|
|
12
12
|
return record;
|
|
13
13
|
},
|
|
14
|
+
async tryInsert(record) {
|
|
15
|
+
// Single-process FS store: atomicity comes from `mkdir` without
|
|
16
|
+
// `recursive: true` — succeeds exactly once per path, EEXIST
|
|
17
|
+
// otherwise. The orchestrator only enters this branch for
|
|
18
|
+
// idempotency-derived runIds, so the directory's prior existence
|
|
19
|
+
// means another caller already created the run.
|
|
20
|
+
try {
|
|
21
|
+
await mkdir(rootDir, { recursive: true });
|
|
22
|
+
await mkdir(join(rootDir, record.id));
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const code = err.code;
|
|
26
|
+
if (code !== "EEXIST")
|
|
27
|
+
throw err;
|
|
28
|
+
const existing = await readRunFile(join(rootDir, record.id, "run-record.json"));
|
|
29
|
+
if (existing)
|
|
30
|
+
return { record: existing, created: false };
|
|
31
|
+
// Dir exists but no record file — partial-write race window;
|
|
32
|
+
// fall through and write our record.
|
|
33
|
+
}
|
|
34
|
+
await writeFile(join(rootDir, record.id, "run-record.json"), JSON.stringify(record, null, 2));
|
|
35
|
+
return { record, created: true };
|
|
36
|
+
},
|
|
14
37
|
async list(filter = {}) {
|
|
15
38
|
const entries = await safeReaddir(rootDir);
|
|
16
39
|
const runs = [];
|
package/dist/index.d.ts
CHANGED
|
@@ -3,9 +3,12 @@ export { type EntryFile, type LoadEntryOptions, loadEntryFile, } from "./entry-l
|
|
|
3
3
|
export { createFsRunRecordStore, type FsRunRecordStoreOptions, filterRunRecords, } from "./fs-run-record-store.js";
|
|
4
4
|
export { durationToMs, generateLocalRunId, } from "./local-runtime.js";
|
|
5
5
|
export { defaultMigrationsDir, loadPostgresMigrations, type PostgresMigration, type RunPostgresMigrationsOptions, runPostgresMigrations, } from "./migrate.js";
|
|
6
|
+
export { createNodeStandaloneDriver, type NodeStandaloneDriverOptions, } from "./node-standalone-driver.js";
|
|
6
7
|
export { createPersistentWakeupManager, type PersistentWakeupManager, type PersistentWakeupManagerDeps, } from "./persistent-wakeup-manager.js";
|
|
7
8
|
export { type CreatePostgresConnectionOptions, createPostgresConnection, type PostgresConnection, } from "./postgres.js";
|
|
8
|
-
export {
|
|
9
|
+
export { createPostgresManifestStore, type ManifestEnvelope, type ManifestStore, type PostgresManifestStoreOptions, } from "./postgres-manifest-store.js";
|
|
10
|
+
export { createPostgresRunRecordStore, type PostgresRunRecordStoreOptions, } from "./postgres-run-record-store.js";
|
|
11
|
+
export { snapshotRunsTable, wakeupsTable, workflowManifestsTable, } from "./postgres-schema.js";
|
|
9
12
|
export { createPostgresSnapshotRunStore, type PostgresSnapshotRunStoreOptions, rowToStoredRun, storedRunToRow, } from "./postgres-snapshot-run-store.js";
|
|
10
13
|
export { createPostgresWakeupStore, type PostgresWakeupStoreOptions, rowToWakeupRecord, wakeupToRow, } from "./postgres-wakeup-store.js";
|
|
11
14
|
export { type BuildResumeJournalInput, type BuildResumeJournalResult, type BuildSeededResumeJournalInput, buildResumeJournal, buildSeededResumeJournal, } from "./resume-run.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,cAAc,EACd,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,eAAe,EACpB,aAAa,EACb,kBAAkB,EAClB,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACvB,aAAa,EACb,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,uBAAuB,EACvB,WAAW,GACZ,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,aAAa,GACd,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,sBAAsB,EACtB,KAAK,uBAAuB,EAC5B,gBAAgB,GACjB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,YAAY,EACZ,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,iBAAiB,EACtB,KAAK,4BAA4B,EACjC,qBAAqB,GACtB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,6BAA6B,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,GACjC,MAAM,gCAAgC,CAAA;AACvC,OAAO,EACL,KAAK,+BAA+B,EACpC,wBAAwB,EACxB,KAAK,kBAAkB,GACxB,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,iBAAiB,EACjB,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -3,9 +3,12 @@ export { loadEntryFile, } from "./entry-loader.js";
|
|
|
3
3
|
export { createFsRunRecordStore, filterRunRecords, } from "./fs-run-record-store.js";
|
|
4
4
|
export { durationToMs, generateLocalRunId, } from "./local-runtime.js";
|
|
5
5
|
export { defaultMigrationsDir, loadPostgresMigrations, runPostgresMigrations, } from "./migrate.js";
|
|
6
|
+
export { createNodeStandaloneDriver, } from "./node-standalone-driver.js";
|
|
6
7
|
export { createPersistentWakeupManager, } from "./persistent-wakeup-manager.js";
|
|
7
8
|
export { createPostgresConnection, } from "./postgres.js";
|
|
8
|
-
export {
|
|
9
|
+
export { createPostgresManifestStore, } from "./postgres-manifest-store.js";
|
|
10
|
+
export { createPostgresRunRecordStore, } from "./postgres-run-record-store.js";
|
|
11
|
+
export { snapshotRunsTable, wakeupsTable, workflowManifestsTable, } from "./postgres-schema.js";
|
|
9
12
|
export { createPostgresSnapshotRunStore, rowToStoredRun, storedRunToRow, } from "./postgres-snapshot-run-store.js";
|
|
10
13
|
export { createPostgresWakeupStore, rowToWakeupRecord, wakeupToRow, } from "./postgres-wakeup-store.js";
|
|
11
14
|
export { buildResumeJournal, buildSeededResumeJournal, } from "./resume-run.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { DriverFactory } from "@voyantjs/workflows/driver";
|
|
2
|
+
import { type RunRecord, type StepHandler } from "@voyantjs/workflows-orchestrator";
|
|
3
|
+
import type { drizzle } from "drizzle-orm/node-postgres";
|
|
4
|
+
type Db = ReturnType<typeof drizzle>;
|
|
5
|
+
export interface NodeStandaloneDriverOptions {
|
|
6
|
+
/** Long-lived Postgres connection (drizzle-orm `node-postgres` adapter). */
|
|
7
|
+
db: Db;
|
|
8
|
+
/** Default environment for `trigger()` calls that don't specify one. */
|
|
9
|
+
defaultEnvironment?: "production" | "preview" | "development";
|
|
10
|
+
/** Tenant metadata stamped onto every triggered run. */
|
|
11
|
+
tenantMeta?: RunRecord["tenantMeta"];
|
|
12
|
+
/** Injectable clock; defaults to `Date.now`. */
|
|
13
|
+
now?: () => number;
|
|
14
|
+
/**
|
|
15
|
+
* Step handler override. Defaults to in-process `handleStepRequest`
|
|
16
|
+
* with the framework-supplied `services` container plumbed through
|
|
17
|
+
* (so step bodies can resolve via `ctx.services.resolve(...)`).
|
|
18
|
+
*/
|
|
19
|
+
handler?: StepHandler;
|
|
20
|
+
/**
|
|
21
|
+
* Latest N manifest versions to retain per environment after each
|
|
22
|
+
* registerManifest. Defaults to 3 (per architecture doc §14.2). Set to
|
|
23
|
+
* a high number to disable pruning effectively.
|
|
24
|
+
*/
|
|
25
|
+
manifestVersionsToKeep?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Time-wheel poll interval, ms. The wakeup manager (architecture doc
|
|
28
|
+
* §7.2) polls `voyant_wakeups` for due alarms and resumes parked runs
|
|
29
|
+
* via the orchestrator. Defaults to 1_000 ms. Lower values reduce
|
|
30
|
+
* sleep-resume latency at the cost of DB load.
|
|
31
|
+
*/
|
|
32
|
+
wakeupPollIntervalMs?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Wakeup lease TTL, ms. A poll instance leases a due wakeup for this
|
|
35
|
+
* long; if the process dies mid-process, another instance picks the
|
|
36
|
+
* wakeup back up after the lease expires. Defaults to 4× the poll
|
|
37
|
+
* interval (or 5_000 ms, whichever is greater).
|
|
38
|
+
*/
|
|
39
|
+
wakeupLeaseMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Lease owner identifier. Used to disambiguate poller instances
|
|
42
|
+
* across processes. Defaults to a random per-driver token.
|
|
43
|
+
*/
|
|
44
|
+
wakeupLeaseOwner?: string;
|
|
45
|
+
/**
|
|
46
|
+
* When `true`, the wakeup poller does NOT auto-start on construction.
|
|
47
|
+
* Callers must invoke the returned driver's lifecycle hooks
|
|
48
|
+
* themselves — useful for tests that want to control the poll
|
|
49
|
+
* cadence. Defaults to `false` (poller starts immediately).
|
|
50
|
+
*/
|
|
51
|
+
disableTimeWheel?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the Mode 2 driver factory. The factory closes over its options
|
|
55
|
+
* and returns a fresh `WorkflowDriver` when `createApp()` (or a test)
|
|
56
|
+
* calls it with `DriverFactoryDeps`.
|
|
57
|
+
*
|
|
58
|
+
* Usage:
|
|
59
|
+
*
|
|
60
|
+
* createApp({
|
|
61
|
+
* workflows: {
|
|
62
|
+
* driver: createNodeStandaloneDriver({ db, defaultEnvironment: "production" }),
|
|
63
|
+
* },
|
|
64
|
+
* })
|
|
65
|
+
*
|
|
66
|
+
* Or in compliance tests:
|
|
67
|
+
*
|
|
68
|
+
* const driver = createNodeStandaloneDriver({ db: testDb })(testFactoryDeps())
|
|
69
|
+
*/
|
|
70
|
+
export declare function createNodeStandaloneDriver(opts: NodeStandaloneDriverOptions): DriverFactory;
|
|
71
|
+
export {};
|
|
72
|
+
//# sourceMappingURL=node-standalone-driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-standalone-driver.d.ts","sourceRoot":"","sources":["../src/node-standalone-driver.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EACV,aAAa,EAOd,MAAM,4BAA4B,CAAA;AAInC,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,WAAW,EACjB,MAAM,kCAAkC,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAWxD,KAAK,EAAE,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAA;AAIpC,MAAM,WAAW,2BAA2B;IAC1C,4EAA4E;IAC5E,EAAE,EAAE,EAAE,CAAA;IACN,wEAAwE;IACxE,kBAAkB,CAAC,EAAE,YAAY,GAAG,SAAS,GAAG,aAAa,CAAA;IAC7D,wDAAwD;IACxD,UAAU,CAAC,EAAE,SAAS,CAAC,YAAY,CAAC,CAAA;IACpC,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAUD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,2BAA2B,GAAG,aAAa,CA6R3F"}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// Mode 2 driver — pure Node, Postgres-backed.
|
|
2
|
+
//
|
|
3
|
+
// Returns a `DriverFactory` (per architecture doc §6.3) that the framework
|
|
4
|
+
// invokes after `createApp()` has assembled its `ModuleContainer`. Composes:
|
|
5
|
+
//
|
|
6
|
+
// * `createPostgresRunRecordStore` — primary state, against
|
|
7
|
+
// `voyant_snapshot_runs.run_record` JSONB.
|
|
8
|
+
// * `createPostgresManifestStore` — manifest history, against
|
|
9
|
+
// `voyant_workflow_manifests`.
|
|
10
|
+
// * In-process step handler glued to `handleStepRequest` from
|
|
11
|
+
// `@voyantjs/workflows/handler` — the workflow body executes in the
|
|
12
|
+
// same Node process as the driver.
|
|
13
|
+
//
|
|
14
|
+
// The Postgres time wheel (`createPersistentWakeupManager`) is started via
|
|
15
|
+
// the returned driver's `start()` lifecycle helper or — when used from
|
|
16
|
+
// `createApp()` — by the framework's bootstrap.
|
|
17
|
+
//
|
|
18
|
+
// See architecture doc §7 for the full Mode 2 design.
|
|
19
|
+
import { deriveStableEventId } from "@voyantjs/workflows/events";
|
|
20
|
+
import { handleStepRequest } from "@voyantjs/workflows/handler";
|
|
21
|
+
import { cancel as orchestratorCancel, trigger as orchestratorTrigger, routeEvent, } from "@voyantjs/workflows-orchestrator";
|
|
22
|
+
import { createPersistentWakeupManager, } from "./persistent-wakeup-manager.js";
|
|
23
|
+
import { createPostgresManifestStore } from "./postgres-manifest-store.js";
|
|
24
|
+
import { createPostgresRunRecordStore } from "./postgres-run-record-store.js";
|
|
25
|
+
import { createPostgresWakeupStore } from "./postgres-wakeup-store.js";
|
|
26
|
+
import { syncWakeupFromRecord } from "./wakeup-store.js";
|
|
27
|
+
const DEFAULT_TENANT_META = {
|
|
28
|
+
tenantId: "default",
|
|
29
|
+
projectId: "default",
|
|
30
|
+
organizationId: "default",
|
|
31
|
+
};
|
|
32
|
+
const DEFAULT_MANIFEST_KEEP = 3;
|
|
33
|
+
/**
|
|
34
|
+
* Build the Mode 2 driver factory. The factory closes over its options
|
|
35
|
+
* and returns a fresh `WorkflowDriver` when `createApp()` (or a test)
|
|
36
|
+
* calls it with `DriverFactoryDeps`.
|
|
37
|
+
*
|
|
38
|
+
* Usage:
|
|
39
|
+
*
|
|
40
|
+
* createApp({
|
|
41
|
+
* workflows: {
|
|
42
|
+
* driver: createNodeStandaloneDriver({ db, defaultEnvironment: "production" }),
|
|
43
|
+
* },
|
|
44
|
+
* })
|
|
45
|
+
*
|
|
46
|
+
* Or in compliance tests:
|
|
47
|
+
*
|
|
48
|
+
* const driver = createNodeStandaloneDriver({ db: testDb })(testFactoryDeps())
|
|
49
|
+
*/
|
|
50
|
+
export function createNodeStandaloneDriver(opts) {
|
|
51
|
+
return (deps) => {
|
|
52
|
+
const runStore = createPostgresRunRecordStore({ db: opts.db });
|
|
53
|
+
const manifestStore = createPostgresManifestStore({ db: opts.db });
|
|
54
|
+
const wakeupStore = createPostgresWakeupStore({ db: opts.db });
|
|
55
|
+
const now = opts.now ?? deps.now ?? (() => Date.now());
|
|
56
|
+
const tenantMeta = opts.tenantMeta ?? DEFAULT_TENANT_META;
|
|
57
|
+
const defaultEnv = opts.defaultEnvironment ?? "development";
|
|
58
|
+
const keep = opts.manifestVersionsToKeep ?? DEFAULT_MANIFEST_KEEP;
|
|
59
|
+
const leaseOwner = opts.wakeupLeaseOwner ?? `node-standalone-${randomToken()}`;
|
|
60
|
+
// Wire the framework-supplied service container through to step bodies.
|
|
61
|
+
// The handler closes over `deps.services` so every step invocation
|
|
62
|
+
// surfaces it as `ctx.services` inside the workflow body.
|
|
63
|
+
const handler = opts.handler ??
|
|
64
|
+
(async (req, stepOpts) => handleStepRequest(req, { services: deps.services }, stepOpts));
|
|
65
|
+
// Persistent wakeup manager — polls `voyant_wakeups` for runs
|
|
66
|
+
// parked on DATETIME waitpoints and resumes them via the orchestrator's
|
|
67
|
+
// `resumeDueAlarms`. This is what makes `ctx.sleep(...)` actually
|
|
68
|
+
// wake up in Mode 2 (architecture doc §7.2).
|
|
69
|
+
const wakeupManager = createPersistentWakeupManager({
|
|
70
|
+
wakeupStore,
|
|
71
|
+
handler,
|
|
72
|
+
leaseOwner,
|
|
73
|
+
leaseMs: opts.wakeupLeaseMs,
|
|
74
|
+
intervalMs: opts.wakeupPollIntervalMs,
|
|
75
|
+
now,
|
|
76
|
+
logger: (level, message, data) => deps.logger(level, message, data),
|
|
77
|
+
// For Mode 2 the "stored" representation IS the RunRecord — the
|
|
78
|
+
// postgres-run-record-store carries the full state on `run_record`
|
|
79
|
+
// JSONB. So toRecord/fromRecord are identity.
|
|
80
|
+
async getRun(runId) {
|
|
81
|
+
return runStore.get(runId);
|
|
82
|
+
},
|
|
83
|
+
async saveRun(record) {
|
|
84
|
+
await runStore.save(record);
|
|
85
|
+
return record;
|
|
86
|
+
},
|
|
87
|
+
toRecord: (record) => record,
|
|
88
|
+
fromRecord: (record) => record,
|
|
89
|
+
async listRuns() {
|
|
90
|
+
// Bootstrap-time list of currently-parked runs to seed the wakeup
|
|
91
|
+
// store. Mode 2 uses status="waiting" filter on the run-record store.
|
|
92
|
+
return runStore.list({ status: "waiting" });
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
if (!opts.disableTimeWheel) {
|
|
96
|
+
// Auto-start the poller. Callers can opt out via `disableTimeWheel`
|
|
97
|
+
// for tests that want to control the cadence manually (poll explicitly
|
|
98
|
+
// via `manager.poll()`).
|
|
99
|
+
wakeupManager.start();
|
|
100
|
+
}
|
|
101
|
+
let shuttingDown = false;
|
|
102
|
+
// ---- WorkflowDriver implementation ----
|
|
103
|
+
async function registerManifest(args) {
|
|
104
|
+
assertNotShutdown(shuttingDown);
|
|
105
|
+
const result = await manifestStore.registerManifest({
|
|
106
|
+
environment: args.environment,
|
|
107
|
+
versionId: args.manifest.versionId,
|
|
108
|
+
manifest: args.manifest,
|
|
109
|
+
});
|
|
110
|
+
// Best-effort prune; failures here shouldn't fail boot.
|
|
111
|
+
try {
|
|
112
|
+
await manifestStore.pruneToVersions(args.environment, keep);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
deps.logger("warn", "manifest prune failed (non-fatal)", {
|
|
116
|
+
environment: args.environment,
|
|
117
|
+
error: err instanceof Error ? err.message : String(err),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
async function getManifest(args) {
|
|
123
|
+
const envelope = await manifestStore.getCurrent(args.environment);
|
|
124
|
+
if (!envelope)
|
|
125
|
+
return null;
|
|
126
|
+
return envelope.manifest;
|
|
127
|
+
}
|
|
128
|
+
async function trigger(workflow, input, triggerOpts) {
|
|
129
|
+
assertNotShutdown(shuttingDown);
|
|
130
|
+
const workflowId = typeof workflow === "string" ? workflow : workflow.id;
|
|
131
|
+
const env = triggerOpts?.environment ?? defaultEnv;
|
|
132
|
+
const record = await orchestratorTrigger({
|
|
133
|
+
workflowId,
|
|
134
|
+
workflowVersion: triggerOpts?.lockToVersion ?? "v1",
|
|
135
|
+
input: input,
|
|
136
|
+
tenantMeta,
|
|
137
|
+
environment: env,
|
|
138
|
+
tags: triggerOpts?.tags,
|
|
139
|
+
idempotencyKey: triggerOpts?.idempotencyKey,
|
|
140
|
+
}, { store: runStore, handler, now });
|
|
141
|
+
// Sync wakeup row so the time-wheel can resume DATETIME-parked runs.
|
|
142
|
+
// No-op if the run completed inline (status !== "waiting").
|
|
143
|
+
await syncWakeupFromRecord(wakeupStore, record);
|
|
144
|
+
return runRecordToRun(record);
|
|
145
|
+
}
|
|
146
|
+
async function ingestEvent(args) {
|
|
147
|
+
assertNotShutdown(shuttingDown);
|
|
148
|
+
const stored = await manifestStore.getCurrent(args.environment);
|
|
149
|
+
if (!stored) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
reason: "manifest_not_registered",
|
|
153
|
+
message: `No manifest is registered for environment "${args.environment}".`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const eventId = await ensureEventId(args.envelope);
|
|
157
|
+
const manifest = stored.manifest;
|
|
158
|
+
const routed = routeEvent({
|
|
159
|
+
manifest,
|
|
160
|
+
envelope: args.envelope,
|
|
161
|
+
eventId,
|
|
162
|
+
idempotencyOverride: args.idempotencyKey,
|
|
163
|
+
});
|
|
164
|
+
const matches = [];
|
|
165
|
+
let anyTriggered = false;
|
|
166
|
+
let anyFailed = false;
|
|
167
|
+
for (const entry of routed) {
|
|
168
|
+
if (entry.status === "skipped") {
|
|
169
|
+
matches.push({
|
|
170
|
+
filterId: entry.filterId,
|
|
171
|
+
status: "skipped",
|
|
172
|
+
reason: entry.reason,
|
|
173
|
+
details: entry.details,
|
|
174
|
+
});
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const record = await orchestratorTrigger({
|
|
179
|
+
workflowId: entry.targetWorkflowId,
|
|
180
|
+
workflowVersion: "v1",
|
|
181
|
+
input: entry.input,
|
|
182
|
+
tenantMeta,
|
|
183
|
+
environment: args.environment,
|
|
184
|
+
idempotencyKey: entry.idempotencyKey,
|
|
185
|
+
triggeredBy: {
|
|
186
|
+
kind: "event",
|
|
187
|
+
eventId,
|
|
188
|
+
eventType: args.envelope.name,
|
|
189
|
+
filterId: entry.filterId,
|
|
190
|
+
},
|
|
191
|
+
}, { store: runStore, handler, now });
|
|
192
|
+
await syncWakeupFromRecord(wakeupStore, record);
|
|
193
|
+
matches.push({
|
|
194
|
+
filterId: entry.filterId,
|
|
195
|
+
targetWorkflowId: entry.targetWorkflowId,
|
|
196
|
+
runId: record.id,
|
|
197
|
+
idempotencyKey: entry.idempotencyKey,
|
|
198
|
+
status: "queued",
|
|
199
|
+
});
|
|
200
|
+
anyTriggered = true;
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
matches.push({
|
|
204
|
+
filterId: entry.filterId,
|
|
205
|
+
targetWorkflowId: entry.targetWorkflowId,
|
|
206
|
+
status: "error",
|
|
207
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
208
|
+
});
|
|
209
|
+
anyFailed = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (matches.length > 0 && !anyTriggered && anyFailed) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
reason: "trigger_failed_for_all_matches",
|
|
216
|
+
message: "every matched filter failed to trigger",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { ok: true, eventId, matches };
|
|
220
|
+
}
|
|
221
|
+
async function shutdown() {
|
|
222
|
+
shuttingDown = true;
|
|
223
|
+
// Stop the time-wheel poller so the process can exit cleanly.
|
|
224
|
+
// Idempotent — calling stop() on an already-stopped manager is a
|
|
225
|
+
// no-op.
|
|
226
|
+
wakeupManager.stop();
|
|
227
|
+
}
|
|
228
|
+
// ---- WorkflowAdmin (full; Mode 2 has Postgres-native query support) ----
|
|
229
|
+
const admin = {
|
|
230
|
+
async listRuns(listOpts) {
|
|
231
|
+
const filterStatus = normalizeStatusFilter(listOpts?.status);
|
|
232
|
+
const filterEnv = listOpts?.environment;
|
|
233
|
+
const filterWorkflow = listOpts?.workflowId;
|
|
234
|
+
const filterTag = listOpts?.tag;
|
|
235
|
+
const filterSince = toEpoch(listOpts?.since);
|
|
236
|
+
const filterUntil = toEpoch(listOpts?.until);
|
|
237
|
+
const limit = listOpts?.limit ?? 100;
|
|
238
|
+
// Take a generous fetch window; in-memory filter for fields the
|
|
239
|
+
// store doesn't index (env, tag, since/until). For real load we'd
|
|
240
|
+
// push these down into the query — out of scope for PR1 step 6.
|
|
241
|
+
const records = await runStore.list({
|
|
242
|
+
workflowId: filterWorkflow,
|
|
243
|
+
status: filterStatus?.[0],
|
|
244
|
+
limit: limit * 2,
|
|
245
|
+
});
|
|
246
|
+
const results = [];
|
|
247
|
+
for (const rec of records) {
|
|
248
|
+
if (filterStatus && !filterStatus.includes(rec.status))
|
|
249
|
+
continue;
|
|
250
|
+
if (filterEnv && rec.environment !== filterEnv)
|
|
251
|
+
continue;
|
|
252
|
+
if (filterTag && !rec.tags.includes(filterTag))
|
|
253
|
+
continue;
|
|
254
|
+
if (filterSince !== undefined && rec.startedAt < filterSince)
|
|
255
|
+
continue;
|
|
256
|
+
if (filterUntil !== undefined && rec.startedAt > filterUntil)
|
|
257
|
+
continue;
|
|
258
|
+
results.push(runRecordToSummary(rec));
|
|
259
|
+
}
|
|
260
|
+
const page = results.slice(0, limit);
|
|
261
|
+
const nextCursor = results.length > limit ? String(limit) : undefined;
|
|
262
|
+
return { runs: page, nextCursor };
|
|
263
|
+
},
|
|
264
|
+
async getRun(runId) {
|
|
265
|
+
const rec = await runStore.get(runId);
|
|
266
|
+
return rec ? runRecordToDetail(rec) : null;
|
|
267
|
+
},
|
|
268
|
+
async cancelRun(runId, cancelOpts) {
|
|
269
|
+
// The orchestrator core's cancel() does NOT run compensations by
|
|
270
|
+
// default (architecture doc §21.21). The `compensate` flag is
|
|
271
|
+
// accepted but no-ops in v1.
|
|
272
|
+
void cancelOpts?.compensate;
|
|
273
|
+
await orchestratorCancel({ runId, reason: cancelOpts?.reason }, { store: runStore, handler, now });
|
|
274
|
+
},
|
|
275
|
+
streamRun(runId) {
|
|
276
|
+
// Live journal-event streaming is a follow-up — needs LISTEN/NOTIFY
|
|
277
|
+
// wired against the run store or a polling source. PR1 ships
|
|
278
|
+
// listRuns + getRun; streamRun returns an immediately-exhausted
|
|
279
|
+
// iterable so dashboards probing it get a clean empty stream
|
|
280
|
+
// instead of an undefined.
|
|
281
|
+
void runId;
|
|
282
|
+
return {
|
|
283
|
+
[Symbol.asyncIterator]() {
|
|
284
|
+
return {
|
|
285
|
+
next: async () => ({ value: undefined, done: true }),
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
registerManifest,
|
|
293
|
+
trigger,
|
|
294
|
+
ingestEvent,
|
|
295
|
+
getManifest,
|
|
296
|
+
shutdown,
|
|
297
|
+
admin,
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// ---- Helpers ----
|
|
302
|
+
function assertNotShutdown(shuttingDown) {
|
|
303
|
+
if (shuttingDown) {
|
|
304
|
+
throw new Error("NodeStandaloneDriver: shutdown() has been called; new operations are refused.");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function randomToken() {
|
|
308
|
+
return Math.floor(Math.random() * 1_000_000_000)
|
|
309
|
+
.toString(36)
|
|
310
|
+
.padStart(6, "0");
|
|
311
|
+
}
|
|
312
|
+
async function ensureEventId(envelope) {
|
|
313
|
+
if (envelope.metadata?.eventId)
|
|
314
|
+
return envelope.metadata.eventId;
|
|
315
|
+
// Content-derived fallback per architecture doc §15.2 — closes the
|
|
316
|
+
// dedup hole reviewer P2.2 flagged.
|
|
317
|
+
return deriveStableEventId(envelope);
|
|
318
|
+
}
|
|
319
|
+
function runRecordToRun(rec) {
|
|
320
|
+
return {
|
|
321
|
+
id: rec.id,
|
|
322
|
+
workflowId: rec.workflowId,
|
|
323
|
+
status: rec.status,
|
|
324
|
+
startedAt: rec.startedAt,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function runRecordToSummary(rec) {
|
|
328
|
+
return {
|
|
329
|
+
id: rec.id,
|
|
330
|
+
workflowId: rec.workflowId,
|
|
331
|
+
status: rec.status,
|
|
332
|
+
startedAt: rec.startedAt,
|
|
333
|
+
completedAt: rec.completedAt,
|
|
334
|
+
tags: [...rec.tags],
|
|
335
|
+
environment: rec.environment,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function runRecordToDetail(rec) {
|
|
339
|
+
return {
|
|
340
|
+
...runRecordToSummary(rec),
|
|
341
|
+
version: rec.workflowVersion,
|
|
342
|
+
input: rec.input,
|
|
343
|
+
output: rec.output,
|
|
344
|
+
error: rec.error,
|
|
345
|
+
durationMs: rec.completedAt !== undefined ? Math.max(0, rec.completedAt - rec.startedAt) : undefined,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function normalizeStatusFilter(s) {
|
|
349
|
+
if (s === undefined)
|
|
350
|
+
return undefined;
|
|
351
|
+
return Array.isArray(s) ? s : [s];
|
|
352
|
+
}
|
|
353
|
+
function toEpoch(v) {
|
|
354
|
+
if (v === undefined)
|
|
355
|
+
return undefined;
|
|
356
|
+
return typeof v === "number" ? v : v.getTime();
|
|
357
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { drizzle } from "drizzle-orm/node-postgres";
|
|
2
|
+
type ManifestDb = ReturnType<typeof drizzle>;
|
|
3
|
+
/**
|
|
4
|
+
* Structural view of `WorkflowManifest` (from `@voyantjs/workflows/protocol`).
|
|
5
|
+
* Declared locally to avoid pulling the workflows package's protocol export
|
|
6
|
+
* into this store — every consumer satisfies the shape via TypeScript
|
|
7
|
+
* structural compat, same pattern Voyant uses elsewhere.
|
|
8
|
+
*/
|
|
9
|
+
export interface ManifestEnvelope {
|
|
10
|
+
environment: string;
|
|
11
|
+
versionId: string;
|
|
12
|
+
manifest: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
export interface ManifestStore {
|
|
15
|
+
/**
|
|
16
|
+
* Idempotent. Same `(environment, versionId)` returns without re-write.
|
|
17
|
+
* New `versionId` for an existing environment marks the new row
|
|
18
|
+
* `is_current = true` and the previous current `is_current = false`.
|
|
19
|
+
*/
|
|
20
|
+
registerManifest(envelope: ManifestEnvelope): Promise<{
|
|
21
|
+
versionId: string;
|
|
22
|
+
}>;
|
|
23
|
+
/** Returns the current manifest for the environment, or null. */
|
|
24
|
+
getCurrent(environment: string): Promise<ManifestEnvelope | null>;
|
|
25
|
+
/** Retain the latest `keep` versions per environment; delete older. */
|
|
26
|
+
pruneToVersions(environment: string, keep: number): Promise<{
|
|
27
|
+
deleted: number;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export interface PostgresManifestStoreOptions {
|
|
31
|
+
db: ManifestDb;
|
|
32
|
+
}
|
|
33
|
+
export declare function createPostgresManifestStore(opts: PostgresManifestStoreOptions): ManifestStore;
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=postgres-manifest-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-manifest-store.d.ts","sourceRoot":"","sources":["../src/postgres-manifest-store.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAIxD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,gBAAgB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAE5E,iEAAiE;IACjE,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IAEjE,uEAAuE;IACvE,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACjF;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,UAAU,CAAA;CACf;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,4BAA4B,GAAG,aAAa,CA8F7F"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Postgres-backed manifest store. Holds the serialized WorkflowManifest
|
|
2
|
+
// pushed at `createApp()` boot via `driver.registerManifest(...)` and read
|
|
3
|
+
// by `driver.getManifest(...)` for boot-time mismatch detection and the
|
|
4
|
+
// dashboard's filter inspector.
|
|
5
|
+
//
|
|
6
|
+
// One row is "current" per environment, enforced by the partial unique
|
|
7
|
+
// index `voyant_workflow_manifests_current_idx` (migration 0004). History
|
|
8
|
+
// is retained — `pruneToVersions(n)` keeps the latest N per environment.
|
|
9
|
+
//
|
|
10
|
+
// See architecture doc §14 for the manifest lifecycle.
|
|
11
|
+
import { and, desc, eq, ne, sql } from "drizzle-orm";
|
|
12
|
+
import { workflowManifestsTable } from "./postgres-schema.js";
|
|
13
|
+
export function createPostgresManifestStore(opts) {
|
|
14
|
+
const db = opts.db;
|
|
15
|
+
return {
|
|
16
|
+
async registerManifest(envelope) {
|
|
17
|
+
// Atomically: insert new row, flip is_current to false on every other
|
|
18
|
+
// row for this environment, mark this row is_current = true. Single
|
|
19
|
+
// transaction so concurrent registrations don't leave two rows current.
|
|
20
|
+
await db.transaction(async (tx) => {
|
|
21
|
+
await tx
|
|
22
|
+
.insert(workflowManifestsTable)
|
|
23
|
+
.values({
|
|
24
|
+
environment: envelope.environment,
|
|
25
|
+
versionId: envelope.versionId,
|
|
26
|
+
manifest: envelope.manifest,
|
|
27
|
+
isCurrent: true,
|
|
28
|
+
})
|
|
29
|
+
.onConflictDoNothing({
|
|
30
|
+
target: [workflowManifestsTable.environment, workflowManifestsTable.versionId],
|
|
31
|
+
});
|
|
32
|
+
// Demote any existing current row that isn't this versionId.
|
|
33
|
+
await tx
|
|
34
|
+
.update(workflowManifestsTable)
|
|
35
|
+
.set({ isCurrent: false })
|
|
36
|
+
.where(and(eq(workflowManifestsTable.environment, envelope.environment), eq(workflowManifestsTable.isCurrent, true), ne(workflowManifestsTable.versionId, envelope.versionId)));
|
|
37
|
+
// Promote the just-registered row in case it already existed
|
|
38
|
+
// (re-register of the same versionId).
|
|
39
|
+
await tx
|
|
40
|
+
.update(workflowManifestsTable)
|
|
41
|
+
.set({ isCurrent: true })
|
|
42
|
+
.where(and(eq(workflowManifestsTable.environment, envelope.environment), eq(workflowManifestsTable.versionId, envelope.versionId)));
|
|
43
|
+
});
|
|
44
|
+
return { versionId: envelope.versionId };
|
|
45
|
+
},
|
|
46
|
+
async getCurrent(environment) {
|
|
47
|
+
const rows = await db
|
|
48
|
+
.select()
|
|
49
|
+
.from(workflowManifestsTable)
|
|
50
|
+
.where(and(eq(workflowManifestsTable.environment, environment), eq(workflowManifestsTable.isCurrent, true)))
|
|
51
|
+
.limit(1);
|
|
52
|
+
const row = rows[0];
|
|
53
|
+
if (!row)
|
|
54
|
+
return null;
|
|
55
|
+
return {
|
|
56
|
+
environment: row.environment,
|
|
57
|
+
versionId: row.versionId,
|
|
58
|
+
manifest: row.manifest,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
async pruneToVersions(environment, keep) {
|
|
62
|
+
if (keep < 1) {
|
|
63
|
+
throw new Error(`pruneToVersions: keep must be >= 1, got ${keep}`);
|
|
64
|
+
}
|
|
65
|
+
// Get the IDs of the latest `keep` rows for this environment, then
|
|
66
|
+
// delete everything else.
|
|
67
|
+
const newest = await db
|
|
68
|
+
.select({ versionId: workflowManifestsTable.versionId })
|
|
69
|
+
.from(workflowManifestsTable)
|
|
70
|
+
.where(eq(workflowManifestsTable.environment, environment))
|
|
71
|
+
.orderBy(desc(workflowManifestsTable.registeredAt))
|
|
72
|
+
.limit(keep);
|
|
73
|
+
const keepIds = newest.map((r) => r.versionId);
|
|
74
|
+
if (keepIds.length === 0)
|
|
75
|
+
return { deleted: 0 };
|
|
76
|
+
const result = await db.execute(sql `DELETE FROM ${workflowManifestsTable}
|
|
77
|
+
WHERE environment = ${environment}
|
|
78
|
+
AND version_id NOT IN (${sql.join(keepIds.map((id) => sql `${id}`), sql `, `)})`);
|
|
79
|
+
return { deleted: result.rowCount ?? 0 };
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OrchestratorRunStatus, RunRecordStore } from "@voyantjs/workflows-orchestrator";
|
|
2
|
+
import type { drizzle } from "drizzle-orm/node-postgres";
|
|
3
|
+
type SnapshotDb = ReturnType<typeof drizzle>;
|
|
4
|
+
export interface PostgresRunRecordStoreOptions {
|
|
5
|
+
db: SnapshotDb;
|
|
6
|
+
}
|
|
7
|
+
export declare function createPostgresRunRecordStore(opts: PostgresRunRecordStoreOptions): RunRecordStore;
|
|
8
|
+
export type { OrchestratorRunStatus };
|
|
9
|
+
//# sourceMappingURL=postgres-run-record-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-run-record-store.d.ts","sourceRoot":"","sources":["../src/postgres-run-record-store.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,qBAAqB,EAErB,cAAc,EACf,MAAM,kCAAkC,CAAA;AAEzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAIxD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAA;AAE5C,MAAM,WAAW,6BAA6B;IAC5C,EAAE,EAAE,UAAU,CAAA;CACf;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,6BAA6B,GAAG,cAAc,CAkGhG;AAwDD,YAAY,EAAE,qBAAqB,EAAE,CAAA"}
|