@voyantjs/workflows-orchestrator-node 0.28.3 → 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.
@@ -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,CA6BzF;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"}
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 { snapshotRunsTable, wakeupsTable, } from "./postgres-schema.js";
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";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
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 { snapshotRunsTable, wakeupsTable, } from "./postgres-schema.js";
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"}