@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.
- 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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Postgres-backed RunRecordStore — implements the orchestrator's primary
|
|
2
|
+
// state-store interface (`@voyantjs/workflows-orchestrator/RunRecordStore`)
|
|
3
|
+
// against the existing `voyant_snapshot_runs` table.
|
|
4
|
+
//
|
|
5
|
+
// The snapshot table already carries a `run_record` JSONB column; this
|
|
6
|
+
// store uses that for the full RunRecord plus the indexed columns
|
|
7
|
+
// (`workflow_id`, `status`, `started_at`, etc.) for queries. Mode 2's
|
|
8
|
+
// `createNodeStandaloneDriver` plugs this into the orchestrator core's
|
|
9
|
+
// pure trigger/resume/cancel functions.
|
|
10
|
+
//
|
|
11
|
+
// The new `idempotency_key` column populated from `RunRecord.idempotencyKey`
|
|
12
|
+
// is enforced by the unique partial index added in migration 0003 — the
|
|
13
|
+
// orchestrator's deterministic-runId derivation from `idempotencyKey`
|
|
14
|
+
// dedups via the row primary key as well, so this index is a defensive
|
|
15
|
+
// safety net.
|
|
16
|
+
import { and, desc, eq } from "drizzle-orm";
|
|
17
|
+
import { snapshotRunsTable } from "./postgres-schema.js";
|
|
18
|
+
export function createPostgresRunRecordStore(opts) {
|
|
19
|
+
return {
|
|
20
|
+
async get(id) {
|
|
21
|
+
const rows = await opts.db
|
|
22
|
+
.select()
|
|
23
|
+
.from(snapshotRunsTable)
|
|
24
|
+
.where(eq(snapshotRunsTable.id, id))
|
|
25
|
+
.limit(1);
|
|
26
|
+
const row = rows[0];
|
|
27
|
+
if (!row)
|
|
28
|
+
return undefined;
|
|
29
|
+
// The full state lives on `run_record`. Older rows persisted by
|
|
30
|
+
// `createPostgresSnapshotRunStore` may lack it; fall back to the
|
|
31
|
+
// denormalized columns so reads stay backwards-compatible.
|
|
32
|
+
const stored = asRunRecord(row.runRecord);
|
|
33
|
+
if (stored)
|
|
34
|
+
return stored;
|
|
35
|
+
return undefined;
|
|
36
|
+
},
|
|
37
|
+
async save(record) {
|
|
38
|
+
const values = recordToValues(record);
|
|
39
|
+
// Upsert by id — last-write-wins. Used for state mutations after
|
|
40
|
+
// the run is created (resume / cancel / drive). Idempotency on
|
|
41
|
+
// *creation* is enforced separately via `tryInsert` below; this
|
|
42
|
+
// path is the steady-state save.
|
|
43
|
+
await opts.db.insert(snapshotRunsTable).values(values).onConflictDoUpdate({
|
|
44
|
+
target: snapshotRunsTable.id,
|
|
45
|
+
set: values,
|
|
46
|
+
});
|
|
47
|
+
return record;
|
|
48
|
+
},
|
|
49
|
+
async tryInsert(record) {
|
|
50
|
+
const values = recordToValues(record);
|
|
51
|
+
// Atomic at the DB level: INSERT … ON CONFLICT DO NOTHING returns
|
|
52
|
+
// the row only if it was created, empty otherwise. When empty, we
|
|
53
|
+
// re-SELECT to load the existing record. This closes the race
|
|
54
|
+
// window between `get(id)` and `save(record)` that the previous
|
|
55
|
+
// get-then-upsert pattern left open — concurrent triggers with
|
|
56
|
+
// the same idempotency-derived runId now see deterministic
|
|
57
|
+
// "first writer wins" semantics.
|
|
58
|
+
const inserted = await opts.db
|
|
59
|
+
.insert(snapshotRunsTable)
|
|
60
|
+
.values(values)
|
|
61
|
+
.onConflictDoNothing({ target: snapshotRunsTable.id })
|
|
62
|
+
.returning({ id: snapshotRunsTable.id });
|
|
63
|
+
if (inserted.length > 0) {
|
|
64
|
+
return { record, created: true };
|
|
65
|
+
}
|
|
66
|
+
// Conflict — load whoever won the race.
|
|
67
|
+
const existingRows = await opts.db
|
|
68
|
+
.select()
|
|
69
|
+
.from(snapshotRunsTable)
|
|
70
|
+
.where(eq(snapshotRunsTable.id, record.id))
|
|
71
|
+
.limit(1);
|
|
72
|
+
const existingRow = existingRows[0];
|
|
73
|
+
if (!existingRow) {
|
|
74
|
+
// Pathological case: the conflict happened but we can't read it
|
|
75
|
+
// back. Surface as a write that became a no-op so the caller
|
|
76
|
+
// doesn't proceed to drive a non-existent run.
|
|
77
|
+
return { record, created: false };
|
|
78
|
+
}
|
|
79
|
+
const existing = asRunRecord(existingRow.runRecord);
|
|
80
|
+
return {
|
|
81
|
+
record: existing ?? record,
|
|
82
|
+
created: false,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
async list(filter = {}) {
|
|
86
|
+
const conditions = [];
|
|
87
|
+
if (filter.workflowId) {
|
|
88
|
+
conditions.push(eq(snapshotRunsTable.workflowId, filter.workflowId));
|
|
89
|
+
}
|
|
90
|
+
if (filter.status) {
|
|
91
|
+
conditions.push(eq(snapshotRunsTable.status, filter.status));
|
|
92
|
+
}
|
|
93
|
+
let query = opts.db.select().from(snapshotRunsTable).$dynamic();
|
|
94
|
+
if (conditions.length === 1) {
|
|
95
|
+
query = query.where(conditions[0]);
|
|
96
|
+
}
|
|
97
|
+
else if (conditions.length > 1) {
|
|
98
|
+
query = query.where(and(...conditions));
|
|
99
|
+
}
|
|
100
|
+
query = query.orderBy(desc(snapshotRunsTable.startedAt));
|
|
101
|
+
if (filter.limit !== undefined) {
|
|
102
|
+
query = query.limit(filter.limit);
|
|
103
|
+
}
|
|
104
|
+
const rows = await query;
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const row of rows) {
|
|
107
|
+
const stored = asRunRecord(row.runRecord);
|
|
108
|
+
if (stored)
|
|
109
|
+
out.push(stored);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// ---- Helpers (parallel to the snapshot store's; kept private here to
|
|
116
|
+
// avoid coupling between the two stores' representations) ----
|
|
117
|
+
function recordToValues(record) {
|
|
118
|
+
return {
|
|
119
|
+
id: record.id,
|
|
120
|
+
workflowId: record.workflowId,
|
|
121
|
+
status: record.status,
|
|
122
|
+
startedAt: record.startedAt,
|
|
123
|
+
completedAt: record.completedAt ?? null,
|
|
124
|
+
durationMs: record.completedAt !== undefined ? record.completedAt - record.startedAt : null,
|
|
125
|
+
tags: [...record.tags],
|
|
126
|
+
// `result` mirrors the snapshot-store convention: the run's public
|
|
127
|
+
// outcome view. We snapshot output + error here so the dashboard's
|
|
128
|
+
// reads remain consistent across both stores.
|
|
129
|
+
result: normalizeRequiredJson({
|
|
130
|
+
status: record.status,
|
|
131
|
+
output: record.output,
|
|
132
|
+
error: record.error,
|
|
133
|
+
startedAt: record.startedAt,
|
|
134
|
+
completedAt: record.completedAt,
|
|
135
|
+
}),
|
|
136
|
+
input: normalizeJson(record.input),
|
|
137
|
+
runRecord: normalizeRequiredJson(record),
|
|
138
|
+
entryFile: null,
|
|
139
|
+
replayOf: null,
|
|
140
|
+
idempotencyKey: record.idempotencyKey ?? null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function asRunRecord(value) {
|
|
144
|
+
if (typeof value !== "object" || value === null)
|
|
145
|
+
return undefined;
|
|
146
|
+
// Sanity check: every RunRecord has at least { id, status, journal }.
|
|
147
|
+
const v = value;
|
|
148
|
+
if (typeof v.id !== "string")
|
|
149
|
+
return undefined;
|
|
150
|
+
if (typeof v.status !== "string")
|
|
151
|
+
return undefined;
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function normalizeJson(value) {
|
|
155
|
+
if (value === undefined)
|
|
156
|
+
return null;
|
|
157
|
+
return JSON.parse(JSON.stringify(value, (_key, nested) => (typeof nested === "bigint" ? Number(nested) : nested)));
|
|
158
|
+
}
|
|
159
|
+
function normalizeRequiredJson(value) {
|
|
160
|
+
return JSON.parse(JSON.stringify(value, (_key, nested) => (typeof nested === "bigint" ? Number(nested) : nested)));
|
|
161
|
+
}
|
|
@@ -214,6 +214,23 @@ export declare const snapshotRunsTable: import("drizzle-orm/pg-core").PgTableWit
|
|
|
214
214
|
identity: undefined;
|
|
215
215
|
generated: undefined;
|
|
216
216
|
}, {}, {}>;
|
|
217
|
+
idempotencyKey: import("drizzle-orm/pg-core").PgColumn<{
|
|
218
|
+
name: "idempotency_key";
|
|
219
|
+
tableName: "voyant_snapshot_runs";
|
|
220
|
+
dataType: "string";
|
|
221
|
+
columnType: "PgText";
|
|
222
|
+
data: string;
|
|
223
|
+
driverParam: string;
|
|
224
|
+
notNull: false;
|
|
225
|
+
hasDefault: false;
|
|
226
|
+
isPrimaryKey: false;
|
|
227
|
+
isAutoincrement: false;
|
|
228
|
+
hasRuntimeDefault: false;
|
|
229
|
+
enumValues: [string, ...string[]];
|
|
230
|
+
baseColumn: never;
|
|
231
|
+
identity: undefined;
|
|
232
|
+
generated: undefined;
|
|
233
|
+
}, {}, {}>;
|
|
217
234
|
};
|
|
218
235
|
dialect: "pg";
|
|
219
236
|
}>;
|
|
@@ -309,4 +326,108 @@ export declare const wakeupsTable: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
309
326
|
};
|
|
310
327
|
dialect: "pg";
|
|
311
328
|
}>;
|
|
329
|
+
/**
|
|
330
|
+
* Manifest store. Holds workflow + event-filter manifests pushed at
|
|
331
|
+
* `createApp()` boot via `driver.registerManifest(...)`. Last N versions
|
|
332
|
+
* retained per environment; `is_current` points to the active version.
|
|
333
|
+
*
|
|
334
|
+
* One row is "current" per environment, enforced by the partial unique
|
|
335
|
+
* index `voyant_workflow_manifests_current_idx`.
|
|
336
|
+
*
|
|
337
|
+
* See architecture doc §14 for the manifest lifecycle.
|
|
338
|
+
*/
|
|
339
|
+
export declare const workflowManifestsTable: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
340
|
+
name: "voyant_workflow_manifests";
|
|
341
|
+
schema: undefined;
|
|
342
|
+
columns: {
|
|
343
|
+
environment: import("drizzle-orm/pg-core").PgColumn<{
|
|
344
|
+
name: "environment";
|
|
345
|
+
tableName: "voyant_workflow_manifests";
|
|
346
|
+
dataType: "string";
|
|
347
|
+
columnType: "PgText";
|
|
348
|
+
data: string;
|
|
349
|
+
driverParam: string;
|
|
350
|
+
notNull: true;
|
|
351
|
+
hasDefault: false;
|
|
352
|
+
isPrimaryKey: false;
|
|
353
|
+
isAutoincrement: false;
|
|
354
|
+
hasRuntimeDefault: false;
|
|
355
|
+
enumValues: [string, ...string[]];
|
|
356
|
+
baseColumn: never;
|
|
357
|
+
identity: undefined;
|
|
358
|
+
generated: undefined;
|
|
359
|
+
}, {}, {}>;
|
|
360
|
+
versionId: import("drizzle-orm/pg-core").PgColumn<{
|
|
361
|
+
name: "version_id";
|
|
362
|
+
tableName: "voyant_workflow_manifests";
|
|
363
|
+
dataType: "string";
|
|
364
|
+
columnType: "PgText";
|
|
365
|
+
data: string;
|
|
366
|
+
driverParam: string;
|
|
367
|
+
notNull: true;
|
|
368
|
+
hasDefault: false;
|
|
369
|
+
isPrimaryKey: false;
|
|
370
|
+
isAutoincrement: false;
|
|
371
|
+
hasRuntimeDefault: false;
|
|
372
|
+
enumValues: [string, ...string[]];
|
|
373
|
+
baseColumn: never;
|
|
374
|
+
identity: undefined;
|
|
375
|
+
generated: undefined;
|
|
376
|
+
}, {}, {}>;
|
|
377
|
+
manifest: import("drizzle-orm/pg-core").PgColumn<{
|
|
378
|
+
name: "manifest";
|
|
379
|
+
tableName: "voyant_workflow_manifests";
|
|
380
|
+
dataType: "json";
|
|
381
|
+
columnType: "PgJsonb";
|
|
382
|
+
data: Record<string, unknown>;
|
|
383
|
+
driverParam: unknown;
|
|
384
|
+
notNull: true;
|
|
385
|
+
hasDefault: false;
|
|
386
|
+
isPrimaryKey: false;
|
|
387
|
+
isAutoincrement: false;
|
|
388
|
+
hasRuntimeDefault: false;
|
|
389
|
+
enumValues: undefined;
|
|
390
|
+
baseColumn: never;
|
|
391
|
+
identity: undefined;
|
|
392
|
+
generated: undefined;
|
|
393
|
+
}, {}, {
|
|
394
|
+
$type: Record<string, unknown>;
|
|
395
|
+
}>;
|
|
396
|
+
registeredAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
397
|
+
name: "registered_at";
|
|
398
|
+
tableName: "voyant_workflow_manifests";
|
|
399
|
+
dataType: "date";
|
|
400
|
+
columnType: "PgTimestamp";
|
|
401
|
+
data: Date;
|
|
402
|
+
driverParam: string;
|
|
403
|
+
notNull: true;
|
|
404
|
+
hasDefault: true;
|
|
405
|
+
isPrimaryKey: false;
|
|
406
|
+
isAutoincrement: false;
|
|
407
|
+
hasRuntimeDefault: false;
|
|
408
|
+
enumValues: undefined;
|
|
409
|
+
baseColumn: never;
|
|
410
|
+
identity: undefined;
|
|
411
|
+
generated: undefined;
|
|
412
|
+
}, {}, {}>;
|
|
413
|
+
isCurrent: import("drizzle-orm/pg-core").PgColumn<{
|
|
414
|
+
name: "is_current";
|
|
415
|
+
tableName: "voyant_workflow_manifests";
|
|
416
|
+
dataType: "boolean";
|
|
417
|
+
columnType: "PgBoolean";
|
|
418
|
+
data: boolean;
|
|
419
|
+
driverParam: boolean;
|
|
420
|
+
notNull: true;
|
|
421
|
+
hasDefault: true;
|
|
422
|
+
isPrimaryKey: false;
|
|
423
|
+
isAutoincrement: false;
|
|
424
|
+
hasRuntimeDefault: false;
|
|
425
|
+
enumValues: undefined;
|
|
426
|
+
baseColumn: never;
|
|
427
|
+
identity: undefined;
|
|
428
|
+
generated: undefined;
|
|
429
|
+
}, {}, {}>;
|
|
430
|
+
};
|
|
431
|
+
dialect: "pg";
|
|
432
|
+
}>;
|
|
312
433
|
//# sourceMappingURL=postgres-schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-schema.d.ts","sourceRoot":"","sources":["../src/postgres-schema.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"postgres-schema.d.ts","sourceRoot":"","sources":["../src/postgres-schema.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0C7B,CAAA;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAaxB,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAelC,CAAA"}
|
package/dist/postgres-schema.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { bigint, boolean, index, integer, jsonb, pgTable, primaryKey, text, timestamp, uniqueIndex, } from "drizzle-orm/pg-core";
|
|
2
3
|
export const snapshotRunsTable = pgTable("voyant_snapshot_runs", {
|
|
3
4
|
id: text("id").primaryKey(),
|
|
4
5
|
workflowId: text("workflow_id").notNull(),
|
|
@@ -12,9 +13,25 @@ export const snapshotRunsTable = pgTable("voyant_snapshot_runs", {
|
|
|
12
13
|
runRecord: jsonb("run_record").$type(),
|
|
13
14
|
entryFile: text("entry_file"),
|
|
14
15
|
replayOf: text("replay_of"),
|
|
16
|
+
/**
|
|
17
|
+
* Caller-supplied idempotency token, mirrored from
|
|
18
|
+
* `RunRecord.idempotencyKey` / `TriggerArgs.idempotencyKey`.
|
|
19
|
+
* The unique partial index below enforces dedup on
|
|
20
|
+
* `(workflow_id, idempotency_key)`; null values don't participate.
|
|
21
|
+
*/
|
|
22
|
+
idempotencyKey: text("idempotency_key"),
|
|
15
23
|
}, (table) => ({
|
|
16
24
|
workflowStartedIdx: index("voyant_snapshot_runs_workflow_started_idx").on(table.workflowId, table.startedAt),
|
|
17
25
|
statusStartedIdx: index("voyant_snapshot_runs_status_started_idx").on(table.status, table.startedAt),
|
|
26
|
+
/**
|
|
27
|
+
* Unique partial index — enforces idempotency dedup on
|
|
28
|
+
* `(workflow_id, idempotency_key)` while letting null keys coexist.
|
|
29
|
+
* Read in `createPostgresSnapshotRunStore` via `INSERT … ON CONFLICT
|
|
30
|
+
* DO NOTHING RETURNING id`.
|
|
31
|
+
*/
|
|
32
|
+
idempotencyIdx: uniqueIndex("voyant_snapshot_runs_idempotency_idx")
|
|
33
|
+
.on(table.workflowId, table.idempotencyKey)
|
|
34
|
+
.where(sql `${table.idempotencyKey} IS NOT NULL`),
|
|
18
35
|
}));
|
|
19
36
|
export const wakeupsTable = pgTable("voyant_wakeups", {
|
|
20
37
|
runId: text("run_id").primaryKey(),
|
|
@@ -26,3 +43,25 @@ export const wakeupsTable = pgTable("voyant_wakeups", {
|
|
|
26
43
|
dueIdx: index("voyant_wakeups_due_idx").on(table.wakeAt),
|
|
27
44
|
leaseIdx: index("voyant_wakeups_lease_idx").on(table.leaseExpiresAt),
|
|
28
45
|
}));
|
|
46
|
+
/**
|
|
47
|
+
* Manifest store. Holds workflow + event-filter manifests pushed at
|
|
48
|
+
* `createApp()` boot via `driver.registerManifest(...)`. Last N versions
|
|
49
|
+
* retained per environment; `is_current` points to the active version.
|
|
50
|
+
*
|
|
51
|
+
* One row is "current" per environment, enforced by the partial unique
|
|
52
|
+
* index `voyant_workflow_manifests_current_idx`.
|
|
53
|
+
*
|
|
54
|
+
* See architecture doc §14 for the manifest lifecycle.
|
|
55
|
+
*/
|
|
56
|
+
export const workflowManifestsTable = pgTable("voyant_workflow_manifests", {
|
|
57
|
+
environment: text("environment").notNull(),
|
|
58
|
+
versionId: text("version_id").notNull(),
|
|
59
|
+
manifest: jsonb("manifest").$type().notNull(),
|
|
60
|
+
registeredAt: timestamp("registered_at", { withTimezone: true }).notNull().defaultNow(),
|
|
61
|
+
isCurrent: boolean("is_current").notNull().default(false),
|
|
62
|
+
}, (table) => ({
|
|
63
|
+
pk: primaryKey({ columns: [table.environment, table.versionId] }),
|
|
64
|
+
currentIdx: uniqueIndex("voyant_workflow_manifests_current_idx")
|
|
65
|
+
.on(table.environment)
|
|
66
|
+
.where(sql `${table.isCurrent}`),
|
|
67
|
+
}));
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
ALTER TABLE "voyant_snapshot_runs" ADD COLUMN "idempotency_key" text;--> statement-breakpoint
|
|
2
|
+
CREATE UNIQUE INDEX "voyant_snapshot_runs_idempotency_idx" ON "voyant_snapshot_runs" USING btree ("workflow_id","idempotency_key") WHERE "voyant_snapshot_runs"."idempotency_key" IS NOT NULL;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
CREATE TABLE "voyant_workflow_manifests" (
|
|
2
|
+
"environment" text NOT NULL,
|
|
3
|
+
"version_id" text NOT NULL,
|
|
4
|
+
"manifest" jsonb NOT NULL,
|
|
5
|
+
"registered_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
6
|
+
"is_current" boolean DEFAULT false NOT NULL,
|
|
7
|
+
CONSTRAINT "voyant_workflow_manifests_environment_version_id_pk" PRIMARY KEY("environment","version_id")
|
|
8
|
+
);
|
|
9
|
+
--> statement-breakpoint
|
|
10
|
+
CREATE UNIQUE INDEX "voyant_workflow_manifests_current_idx" ON "voyant_workflow_manifests" USING btree ("environment") WHERE "voyant_workflow_manifests"."is_current";
|
|
@@ -22,6 +22,20 @@
|
|
|
22
22
|
"when": 1776506061874,
|
|
23
23
|
"tag": "0002_allow_null_input",
|
|
24
24
|
"breakpoints": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"idx": 3,
|
|
28
|
+
"version": "7",
|
|
29
|
+
"when": 1779000000000,
|
|
30
|
+
"tag": "0003_idempotency_key",
|
|
31
|
+
"breakpoints": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"idx": 4,
|
|
35
|
+
"version": "7",
|
|
36
|
+
"when": 1779000001000,
|
|
37
|
+
"tag": "0004_workflow_manifests",
|
|
38
|
+
"breakpoints": true
|
|
25
39
|
}
|
|
26
40
|
]
|
|
27
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyantjs/workflows-orchestrator-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Node/Docker runtime primitives for @voyantjs/workflows-orchestrator, including a file-backed run store and local scheduler.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"drizzle-orm": "^0.45.2",
|
|
31
31
|
"pg": "^8.20.0",
|
|
32
|
-
"@voyantjs/workflows-orchestrator": "0.
|
|
33
|
-
"@voyantjs/workflows": "0.
|
|
32
|
+
"@voyantjs/workflows-orchestrator": "0.29.0",
|
|
33
|
+
"@voyantjs/workflows": "0.29.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^20.12.0",
|
|
@@ -25,6 +25,27 @@ export function createFsRunRecordStore(opts: FsRunRecordStoreOptions = {}): RunR
|
|
|
25
25
|
return record
|
|
26
26
|
},
|
|
27
27
|
|
|
28
|
+
async tryInsert(record) {
|
|
29
|
+
// Single-process FS store: atomicity comes from `mkdir` without
|
|
30
|
+
// `recursive: true` — succeeds exactly once per path, EEXIST
|
|
31
|
+
// otherwise. The orchestrator only enters this branch for
|
|
32
|
+
// idempotency-derived runIds, so the directory's prior existence
|
|
33
|
+
// means another caller already created the run.
|
|
34
|
+
try {
|
|
35
|
+
await mkdir(rootDir, { recursive: true })
|
|
36
|
+
await mkdir(join(rootDir, record.id))
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
39
|
+
if (code !== "EEXIST") throw err
|
|
40
|
+
const existing = await readRunFile(join(rootDir, record.id, "run-record.json"))
|
|
41
|
+
if (existing) return { record: existing, created: false }
|
|
42
|
+
// Dir exists but no record file — partial-write race window;
|
|
43
|
+
// fall through and write our record.
|
|
44
|
+
}
|
|
45
|
+
await writeFile(join(rootDir, record.id, "run-record.json"), JSON.stringify(record, null, 2))
|
|
46
|
+
return { record, created: true }
|
|
47
|
+
},
|
|
48
|
+
|
|
28
49
|
async list(filter = {}) {
|
|
29
50
|
const entries = await safeReaddir(rootDir)
|
|
30
51
|
const runs: RunRecord[] = []
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,10 @@ export {
|
|
|
39
39
|
type RunPostgresMigrationsOptions,
|
|
40
40
|
runPostgresMigrations,
|
|
41
41
|
} from "./migrate.js"
|
|
42
|
+
export {
|
|
43
|
+
createNodeStandaloneDriver,
|
|
44
|
+
type NodeStandaloneDriverOptions,
|
|
45
|
+
} from "./node-standalone-driver.js"
|
|
42
46
|
export {
|
|
43
47
|
createPersistentWakeupManager,
|
|
44
48
|
type PersistentWakeupManager,
|
|
@@ -49,9 +53,20 @@ export {
|
|
|
49
53
|
createPostgresConnection,
|
|
50
54
|
type PostgresConnection,
|
|
51
55
|
} from "./postgres.js"
|
|
56
|
+
export {
|
|
57
|
+
createPostgresManifestStore,
|
|
58
|
+
type ManifestEnvelope,
|
|
59
|
+
type ManifestStore,
|
|
60
|
+
type PostgresManifestStoreOptions,
|
|
61
|
+
} from "./postgres-manifest-store.js"
|
|
62
|
+
export {
|
|
63
|
+
createPostgresRunRecordStore,
|
|
64
|
+
type PostgresRunRecordStoreOptions,
|
|
65
|
+
} from "./postgres-run-record-store.js"
|
|
52
66
|
export {
|
|
53
67
|
snapshotRunsTable,
|
|
54
68
|
wakeupsTable,
|
|
69
|
+
workflowManifestsTable,
|
|
55
70
|
} from "./postgres-schema.js"
|
|
56
71
|
export {
|
|
57
72
|
createPostgresSnapshotRunStore,
|