@mokronos/wfkit 0.1.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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/authoring.d.ts +7 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.js +8 -0
- package/dist/authoring.js.map +1 -0
- package/dist/cli/integrations.d.ts +3 -0
- package/dist/cli/integrations.d.ts.map +1 -0
- package/dist/cli/integrations.js +40 -0
- package/dist/cli/integrations.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +591 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/core.d.ts +198 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1061 -0
- package/dist/core.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +154 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +24 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/replay.d.ts +3 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +28 -0
- package/dist/replay.js.map +1 -0
- package/dist/runtime.d.ts +70 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +183 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +18 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +19 -0
- package/dist/schema.js.map +1 -0
- package/dist/schemas.d.ts +1198 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +383 -0
- package/dist/schemas.js.map +1 -0
- package/dist/sdk/artifact.d.ts +41 -0
- package/dist/sdk/artifact.d.ts.map +1 -0
- package/dist/sdk/artifact.js +94 -0
- package/dist/sdk/artifact.js.map +1 -0
- package/dist/sdk/graph.d.ts +15 -0
- package/dist/sdk/graph.d.ts.map +1 -0
- package/dist/sdk/graph.js +388 -0
- package/dist/sdk/graph.js.map +1 -0
- package/dist/sdk/index.d.ts +14 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +8 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/integrations.d.ts +18 -0
- package/dist/sdk/integrations.d.ts.map +1 -0
- package/dist/sdk/integrations.js +20 -0
- package/dist/sdk/integrations.js.map +1 -0
- package/dist/sdk/json.d.ts +4 -0
- package/dist/sdk/json.d.ts.map +1 -0
- package/dist/sdk/json.js +31 -0
- package/dist/sdk/json.js.map +1 -0
- package/dist/sdk/loader.d.ts +10 -0
- package/dist/sdk/loader.d.ts.map +1 -0
- package/dist/sdk/loader.js +75 -0
- package/dist/sdk/loader.js.map +1 -0
- package/dist/sdk/sdk.d.ts +102 -0
- package/dist/sdk/sdk.d.ts.map +1 -0
- package/dist/sdk/sdk.js +669 -0
- package/dist/sdk/sdk.js.map +1 -0
- package/dist/sdk/sqlite.d.ts +8 -0
- package/dist/sdk/sqlite.d.ts.map +1 -0
- package/dist/sdk/sqlite.js +288 -0
- package/dist/sdk/sqlite.js.map +1 -0
- package/dist/signal.d.ts +17 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +78 -0
- package/dist/signal.js.map +1 -0
- package/dist/testing/index.d.ts +35 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +244 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +67 -0
- package/src/authoring.ts +32 -0
- package/src/cli/integrations.ts +75 -0
- package/src/cli/main.ts +793 -0
- package/src/core.ts +1515 -0
- package/src/errors.ts +19 -0
- package/src/events.ts +37 -0
- package/src/index.ts +109 -0
- package/src/replay.ts +29 -0
- package/src/runtime.ts +314 -0
- package/src/schema.ts +34 -0
- package/src/schemas.ts +473 -0
- package/src/sdk/artifact.ts +176 -0
- package/src/sdk/graph.ts +486 -0
- package/src/sdk/index.ts +64 -0
- package/src/sdk/integrations.ts +49 -0
- package/src/sdk/json.ts +40 -0
- package/src/sdk/loader.ts +99 -0
- package/src/sdk/sdk.ts +911 -0
- package/src/sdk/sqlite.ts +405 -0
- package/src/signal.ts +116 -0
- package/src/testing/index.ts +341 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { Schema } from "effect"
|
|
4
|
+
import {
|
|
5
|
+
WorkflowHistoryEvent as WorkflowHistoryEventSchema,
|
|
6
|
+
WorkflowRunEventRecord as WorkflowRunEventRecordSchema,
|
|
7
|
+
WorkflowRunRecord as WorkflowRunRecordSchema
|
|
8
|
+
} from "../schemas.ts"
|
|
9
|
+
import type {
|
|
10
|
+
WorkflowArtifact,
|
|
11
|
+
WorkflowRepository,
|
|
12
|
+
WorkflowRunEventRecord,
|
|
13
|
+
WorkflowRunRecord,
|
|
14
|
+
WorkflowRunStatus,
|
|
15
|
+
WorkflowStore
|
|
16
|
+
} from "./artifact.ts"
|
|
17
|
+
import { parseJsonText, toJsonText } from "./json.ts"
|
|
18
|
+
|
|
19
|
+
interface WorkflowRow {
|
|
20
|
+
readonly id: string
|
|
21
|
+
readonly name: string
|
|
22
|
+
readonly version: string
|
|
23
|
+
readonly source: string | null
|
|
24
|
+
readonly entrypoint?: string | null
|
|
25
|
+
readonly export_name: string | null
|
|
26
|
+
readonly created_at: string | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface WorkflowRunRow {
|
|
30
|
+
readonly id: string
|
|
31
|
+
readonly workflow_id: string
|
|
32
|
+
readonly workflow_version: string
|
|
33
|
+
readonly status: WorkflowRunStatus
|
|
34
|
+
readonly input_json: string
|
|
35
|
+
readonly result_json: string | null
|
|
36
|
+
readonly error_json: string | null
|
|
37
|
+
readonly started_at: string
|
|
38
|
+
readonly finished_at: string | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface WorkflowRunEventRow {
|
|
42
|
+
readonly id: number
|
|
43
|
+
readonly run_id: string
|
|
44
|
+
readonly sequence: number
|
|
45
|
+
readonly type: string
|
|
46
|
+
readonly event_json: string
|
|
47
|
+
readonly created_at: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface SqliteStatement<Row> {
|
|
51
|
+
get(...params: ReadonlyArray<unknown>): Row | null
|
|
52
|
+
all(...params: ReadonlyArray<unknown>): Row[]
|
|
53
|
+
run(...params: ReadonlyArray<unknown>): unknown
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface SqliteDatabase {
|
|
57
|
+
exec(sql: string): unknown
|
|
58
|
+
prepare<Row>(sql: string): SqliteStatement<Row>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SqliteWorkflowRepositoryOptions {
|
|
62
|
+
readonly databasePath?: string
|
|
63
|
+
readonly rootDir?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const defaultDatabasePath = (rootDir: string) => path.join(rootDir, ".wf", "wf.sqlite")
|
|
67
|
+
|
|
68
|
+
const nowIso = () => new Date().toISOString()
|
|
69
|
+
|
|
70
|
+
const optionalStringField = <K extends string>(
|
|
71
|
+
key: K,
|
|
72
|
+
value: string | undefined
|
|
73
|
+
): { readonly [P in K]?: string } => value === undefined ? {} : { [key]: value } as { readonly [P in K]?: string }
|
|
74
|
+
|
|
75
|
+
const toArtifact = (rootDir: string, row: WorkflowRow): WorkflowArtifact => ({
|
|
76
|
+
id: row.id,
|
|
77
|
+
name: row.name,
|
|
78
|
+
version: row.version,
|
|
79
|
+
source: row.source !== null && row.source.length > 0
|
|
80
|
+
? row.source
|
|
81
|
+
: legacySourceFromEntrypoint(rootDir, row.entrypoint),
|
|
82
|
+
...optionalStringField("exportName", row.export_name ?? undefined),
|
|
83
|
+
...optionalStringField("createdAt", row.created_at ?? undefined)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const legacySourceFromEntrypoint = (rootDir: string, entrypoint: string | null | undefined): string => {
|
|
87
|
+
if (entrypoint === undefined || entrypoint === null || entrypoint.length === 0) {
|
|
88
|
+
throw new Error("Stored workflow row is missing source")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const sourcePath = path.isAbsolute(entrypoint)
|
|
92
|
+
? entrypoint
|
|
93
|
+
: path.resolve(rootDir, entrypoint)
|
|
94
|
+
|
|
95
|
+
return readFileSync(sourcePath, "utf8")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const toRunRecord = (row: WorkflowRunRow): WorkflowRunRecord => Schema.decodeUnknownSync(WorkflowRunRecordSchema)({
|
|
99
|
+
id: row.id,
|
|
100
|
+
workflowId: row.workflow_id,
|
|
101
|
+
workflowVersion: row.workflow_version,
|
|
102
|
+
status: row.status,
|
|
103
|
+
input: JSON.parse(row.input_json),
|
|
104
|
+
result: parseJsonText(row.result_json),
|
|
105
|
+
error: parseJsonText(row.error_json),
|
|
106
|
+
startedAt: row.started_at,
|
|
107
|
+
...optionalStringField("finishedAt", row.finished_at ?? undefined)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const toRunEventRecord = (row: WorkflowRunEventRow): WorkflowRunEventRecord => Schema.decodeUnknownSync(
|
|
111
|
+
WorkflowRunEventRecordSchema
|
|
112
|
+
)({
|
|
113
|
+
id: row.id,
|
|
114
|
+
runId: row.run_id,
|
|
115
|
+
sequence: row.sequence,
|
|
116
|
+
type: row.type,
|
|
117
|
+
event: Schema.decodeUnknownSync(WorkflowHistoryEventSchema)(JSON.parse(row.event_json)),
|
|
118
|
+
createdAt: row.created_at
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const normalizeParams = (params: ReadonlyArray<unknown>) =>
|
|
122
|
+
params.map((param) => param === undefined ? null : param)
|
|
123
|
+
|
|
124
|
+
const createBunDatabase = async (databasePath: string): Promise<SqliteDatabase> => {
|
|
125
|
+
const { Database } = await import("bun:sqlite")
|
|
126
|
+
const db = new Database(databasePath, { create: true, readwrite: true })
|
|
127
|
+
return {
|
|
128
|
+
exec: (sql) => db.exec(sql),
|
|
129
|
+
prepare: <Row>(sql: string): SqliteStatement<Row> => {
|
|
130
|
+
const statement = db.query<Row, any>(sql)
|
|
131
|
+
return {
|
|
132
|
+
get: (...params) => statement.get(...params as any[]) ?? null,
|
|
133
|
+
all: (...params) => statement.all(...params as any[]),
|
|
134
|
+
run: (...params) => statement.run(...params as any[])
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const createNodeDatabase = async (databasePath: string): Promise<SqliteDatabase> => {
|
|
141
|
+
const { DatabaseSync } = await import("node:sqlite")
|
|
142
|
+
const db = new DatabaseSync(databasePath)
|
|
143
|
+
return {
|
|
144
|
+
exec: (sql) => db.exec(sql),
|
|
145
|
+
prepare: <Row>(sql: string): SqliteStatement<Row> => {
|
|
146
|
+
const statement = db.prepare(sql)
|
|
147
|
+
return {
|
|
148
|
+
get: (...params) => statement.get(...normalizeParams(params) as any[]) as Row | undefined ?? null,
|
|
149
|
+
all: (...params) => statement.all(...normalizeParams(params) as any[]) as Row[],
|
|
150
|
+
run: (...params) => statement.run(...normalizeParams(params) as any[])
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const createDatabase = async (databasePath: string): Promise<SqliteDatabase> =>
|
|
157
|
+
process.versions.bun === undefined
|
|
158
|
+
? createNodeDatabase(databasePath)
|
|
159
|
+
: createBunDatabase(databasePath)
|
|
160
|
+
|
|
161
|
+
const migrate = (db: SqliteDatabase) => {
|
|
162
|
+
db.exec(`
|
|
163
|
+
PRAGMA journal_mode = WAL;
|
|
164
|
+
PRAGMA foreign_keys = ON;
|
|
165
|
+
|
|
166
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
167
|
+
id TEXT NOT NULL,
|
|
168
|
+
name TEXT NOT NULL,
|
|
169
|
+
version TEXT NOT NULL,
|
|
170
|
+
source TEXT NOT NULL DEFAULT '',
|
|
171
|
+
entrypoint TEXT NOT NULL DEFAULT '',
|
|
172
|
+
export_name TEXT,
|
|
173
|
+
created_at TEXT NOT NULL,
|
|
174
|
+
PRIMARY KEY (name, version)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
178
|
+
id TEXT PRIMARY KEY,
|
|
179
|
+
workflow_id TEXT NOT NULL,
|
|
180
|
+
workflow_version TEXT NOT NULL,
|
|
181
|
+
status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'failed')),
|
|
182
|
+
input_json TEXT NOT NULL,
|
|
183
|
+
result_json TEXT,
|
|
184
|
+
error_json TEXT,
|
|
185
|
+
started_at TEXT NOT NULL,
|
|
186
|
+
finished_at TEXT
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
CREATE TABLE IF NOT EXISTS workflow_run_events (
|
|
190
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
191
|
+
run_id TEXT NOT NULL,
|
|
192
|
+
sequence INTEGER NOT NULL,
|
|
193
|
+
type TEXT NOT NULL,
|
|
194
|
+
event_json TEXT NOT NULL,
|
|
195
|
+
created_at TEXT NOT NULL,
|
|
196
|
+
FOREIGN KEY (run_id) REFERENCES workflow_runs(id),
|
|
197
|
+
UNIQUE (run_id, sequence)
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_workflow_id_idx
|
|
201
|
+
ON workflow_runs(workflow_id);
|
|
202
|
+
|
|
203
|
+
CREATE INDEX IF NOT EXISTS workflow_run_events_run_id_idx
|
|
204
|
+
ON workflow_run_events(run_id, sequence);
|
|
205
|
+
`)
|
|
206
|
+
|
|
207
|
+
const columns = db.prepare<{ name: string }>("PRAGMA table_info(workflows)").all()
|
|
208
|
+
const columnNames = new Set(columns.map((column) => column.name))
|
|
209
|
+
if (!columnNames.has("source")) {
|
|
210
|
+
db.prepare("ALTER TABLE workflows ADD COLUMN source TEXT NOT NULL DEFAULT ''").run()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
db.prepare("CREATE UNIQUE INDEX IF NOT EXISTS workflows_name_version_idx ON workflows(name, version)").run()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const createSqliteWorkflowRepository = (
|
|
217
|
+
options: SqliteWorkflowRepositoryOptions = {}
|
|
218
|
+
): WorkflowRepository => {
|
|
219
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd())
|
|
220
|
+
const databasePath = path.resolve(options.databasePath ?? defaultDatabasePath(rootDir))
|
|
221
|
+
mkdirSync(path.dirname(databasePath), { recursive: true })
|
|
222
|
+
|
|
223
|
+
const dbPromise = createDatabase(databasePath).then((db) => {
|
|
224
|
+
migrate(db)
|
|
225
|
+
return db
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
async upsertWorkflow(workflow) {
|
|
230
|
+
const db = await dbPromise
|
|
231
|
+
const existing = db.prepare<WorkflowRow>(`
|
|
232
|
+
SELECT id, name, version, source, entrypoint, export_name, created_at
|
|
233
|
+
FROM workflows
|
|
234
|
+
WHERE name = ?
|
|
235
|
+
AND version = ?
|
|
236
|
+
`).get(workflow.name, workflow.version)
|
|
237
|
+
|
|
238
|
+
if (existing !== null) {
|
|
239
|
+
const existingSource = existing.source !== null && existing.source.length > 0
|
|
240
|
+
? existing.source
|
|
241
|
+
: legacySourceFromEntrypoint(rootDir, existing.entrypoint)
|
|
242
|
+
if (existingSource !== workflow.source) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Workflow ${workflow.name}@v${workflow.version} is already cataloged with different source; register a new version instead`
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
db.prepare<unknown>(`
|
|
251
|
+
INSERT INTO workflows (id, name, version, source, entrypoint, export_name, created_at)
|
|
252
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
253
|
+
`).run(
|
|
254
|
+
workflow.id,
|
|
255
|
+
workflow.name,
|
|
256
|
+
workflow.version,
|
|
257
|
+
workflow.source,
|
|
258
|
+
"",
|
|
259
|
+
workflow.exportName ?? null,
|
|
260
|
+
workflow.createdAt ?? nowIso()
|
|
261
|
+
)
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
async deleteWorkflow(id) {
|
|
265
|
+
const db = await dbPromise
|
|
266
|
+
db.prepare<unknown>("DELETE FROM workflows WHERE id = ?").run(id)
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
async list() {
|
|
270
|
+
const db = await dbPromise
|
|
271
|
+
const rows = db.prepare<WorkflowRow>(`
|
|
272
|
+
SELECT id, name, version, source, entrypoint, export_name, created_at
|
|
273
|
+
FROM workflows
|
|
274
|
+
ORDER BY id
|
|
275
|
+
`).all()
|
|
276
|
+
return rows.map((row) => toArtifact(rootDir, row))
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
async get(id) {
|
|
280
|
+
const db = await dbPromise
|
|
281
|
+
const row = db.prepare<WorkflowRow>(`
|
|
282
|
+
SELECT id, name, version, source, entrypoint, export_name, created_at
|
|
283
|
+
FROM workflows
|
|
284
|
+
WHERE id = ?
|
|
285
|
+
`).get(id)
|
|
286
|
+
return row === null ? undefined : toArtifact(rootDir, row)
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
async getRun(id) {
|
|
290
|
+
const db = await dbPromise
|
|
291
|
+
const row = db.prepare<WorkflowRunRow>(`
|
|
292
|
+
SELECT *
|
|
293
|
+
FROM workflow_runs
|
|
294
|
+
WHERE id = ?
|
|
295
|
+
`).get(id)
|
|
296
|
+
return row === null ? undefined : toRunRecord(row)
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
async startRun({ id, workflow, input }) {
|
|
300
|
+
const db = await dbPromise
|
|
301
|
+
const startedAt = nowIso()
|
|
302
|
+
db.prepare<unknown>(`
|
|
303
|
+
INSERT INTO workflow_runs (
|
|
304
|
+
id,
|
|
305
|
+
workflow_id,
|
|
306
|
+
workflow_version,
|
|
307
|
+
status,
|
|
308
|
+
input_json,
|
|
309
|
+
started_at
|
|
310
|
+
)
|
|
311
|
+
VALUES (?, ?, ?, 'running', ?, ?)
|
|
312
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
313
|
+
status = 'running',
|
|
314
|
+
input_json = excluded.input_json,
|
|
315
|
+
result_json = NULL,
|
|
316
|
+
error_json = NULL,
|
|
317
|
+
started_at = excluded.started_at,
|
|
318
|
+
finished_at = NULL
|
|
319
|
+
`).run(id, workflow.id, workflow.version, toJsonText(input), startedAt)
|
|
320
|
+
|
|
321
|
+
const row = db.prepare<WorkflowRunRow>(`
|
|
322
|
+
SELECT *
|
|
323
|
+
FROM workflow_runs
|
|
324
|
+
WHERE id = ?
|
|
325
|
+
`).get(id)
|
|
326
|
+
|
|
327
|
+
if (row === null) {
|
|
328
|
+
throw new Error(`Failed to create workflow run ${id}`)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return toRunRecord(row)
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
async appendRunEvent({ runId, type, event }) {
|
|
335
|
+
const db = await dbPromise
|
|
336
|
+
const nextSequence = db.prepare<{ sequence: number }>(`
|
|
337
|
+
SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence
|
|
338
|
+
FROM workflow_run_events
|
|
339
|
+
WHERE run_id = ?
|
|
340
|
+
`).get(runId)?.sequence ?? 1
|
|
341
|
+
|
|
342
|
+
db.prepare<unknown>(`
|
|
343
|
+
INSERT INTO workflow_run_events (run_id, sequence, type, event_json, created_at)
|
|
344
|
+
VALUES (?, ?, ?, ?, ?)
|
|
345
|
+
`).run(runId, nextSequence, type, toJsonText(event), nowIso())
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
async completeRun({ runId, result }) {
|
|
349
|
+
const db = await dbPromise
|
|
350
|
+
db.prepare<unknown>(`
|
|
351
|
+
UPDATE workflow_runs
|
|
352
|
+
SET status = 'completed',
|
|
353
|
+
result_json = ?,
|
|
354
|
+
error_json = NULL,
|
|
355
|
+
finished_at = ?
|
|
356
|
+
WHERE id = ?
|
|
357
|
+
`).run(toJsonText(result), nowIso(), runId)
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
async failRun({ runId, error }) {
|
|
361
|
+
const db = await dbPromise
|
|
362
|
+
db.prepare<unknown>(`
|
|
363
|
+
UPDATE workflow_runs
|
|
364
|
+
SET status = 'failed',
|
|
365
|
+
error_json = ?,
|
|
366
|
+
finished_at = ?
|
|
367
|
+
WHERE id = ?
|
|
368
|
+
`).run(toJsonText(error), nowIso(), runId)
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
async listRuns() {
|
|
372
|
+
const db = await dbPromise
|
|
373
|
+
const rows = db.prepare<WorkflowRunRow>(`
|
|
374
|
+
SELECT *
|
|
375
|
+
FROM workflow_runs
|
|
376
|
+
ORDER BY started_at DESC
|
|
377
|
+
`).all()
|
|
378
|
+
return rows.map(toRunRecord)
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
async listRunEvents(runId) {
|
|
382
|
+
const db = await dbPromise
|
|
383
|
+
const rows = db.prepare<WorkflowRunEventRow>(`
|
|
384
|
+
SELECT *
|
|
385
|
+
FROM workflow_run_events
|
|
386
|
+
WHERE run_id = ?
|
|
387
|
+
ORDER BY sequence
|
|
388
|
+
`).all(runId)
|
|
389
|
+
return rows.map(toRunEventRecord)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export const seedSqliteWorkflowRepository = async (
|
|
395
|
+
repository: WorkflowRepository,
|
|
396
|
+
seedStore: WorkflowStore
|
|
397
|
+
) => {
|
|
398
|
+
if ((await repository.list()).length > 0) {
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const workflow of await seedStore.list()) {
|
|
403
|
+
await repository.upsertWorkflow(workflow)
|
|
404
|
+
}
|
|
405
|
+
}
|
package/src/signal.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
type AnySchema<A = any> = Schema.Codec<A, any, any, any>
|
|
4
|
+
|
|
5
|
+
export class SignalDeliveryError extends Error {
|
|
6
|
+
readonly _tag = "SignalDeliveryError"
|
|
7
|
+
|
|
8
|
+
constructor(message: string, options?: { readonly cause?: unknown }) {
|
|
9
|
+
super(message)
|
|
10
|
+
this.name = "SignalDeliveryError"
|
|
11
|
+
this.cause = options?.cause
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SignalWaiter {
|
|
16
|
+
readonly schema: AnySchema
|
|
17
|
+
readonly resolve: (value: unknown) => void
|
|
18
|
+
readonly reject: (error: unknown) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const schemas = new Map<string, AnySchema>()
|
|
22
|
+
const buffers = new Map<string, unknown[]>()
|
|
23
|
+
const waiters = new Map<string, SignalWaiter[]>()
|
|
24
|
+
|
|
25
|
+
const keyOf = (executionId: string, name: string): string => `${executionId}\0${name}`
|
|
26
|
+
|
|
27
|
+
export const decodeSignal = <T>(schema: AnySchema<T>, value: unknown): T => {
|
|
28
|
+
try {
|
|
29
|
+
return Schema.decodeUnknownSync(schema as any)(value) as T
|
|
30
|
+
} catch (cause) {
|
|
31
|
+
throw new SignalDeliveryError("Signal payload failed schema validation", { cause })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const getSignalSchema = (executionId: string, name: string): AnySchema | undefined =>
|
|
36
|
+
schemas.get(keyOf(executionId, name))
|
|
37
|
+
|
|
38
|
+
export const registerSignalSchema = <T>(
|
|
39
|
+
executionId: string,
|
|
40
|
+
name: string,
|
|
41
|
+
schema: AnySchema<T>
|
|
42
|
+
) => {
|
|
43
|
+
schemas.set(keyOf(executionId, name), schema)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const deliverSignal = async (
|
|
47
|
+
executionId: string,
|
|
48
|
+
name: string,
|
|
49
|
+
payload: unknown
|
|
50
|
+
): Promise<void> => {
|
|
51
|
+
const key = keyOf(executionId, name)
|
|
52
|
+
const queuedWaiters = waiters.get(key)
|
|
53
|
+
const waiter = queuedWaiters?.[0]
|
|
54
|
+
|
|
55
|
+
if (waiter !== undefined) {
|
|
56
|
+
const decoded = decodeSignal(waiter.schema, payload)
|
|
57
|
+
queuedWaiters!.shift()
|
|
58
|
+
waiter.resolve(decoded)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const schema = schemas.get(key)
|
|
63
|
+
const value = schema === undefined ? payload : decodeSignal(schema, payload)
|
|
64
|
+
const queue = buffers.get(key) ?? []
|
|
65
|
+
queue.push(value)
|
|
66
|
+
buffers.set(key, queue)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const takeBufferedSignal = <T>(
|
|
70
|
+
executionId: string,
|
|
71
|
+
name: string,
|
|
72
|
+
schema: AnySchema<T>
|
|
73
|
+
): T | undefined => {
|
|
74
|
+
registerSignalSchema(executionId, name, schema)
|
|
75
|
+
const key = keyOf(executionId, name)
|
|
76
|
+
const queue = buffers.get(key)
|
|
77
|
+
if (queue === undefined || queue.length === 0) {
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const value = queue.shift()
|
|
82
|
+
if (queue.length === 0) {
|
|
83
|
+
buffers.delete(key)
|
|
84
|
+
}
|
|
85
|
+
return decodeSignal(schema, value)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const awaitSignal = <T>(
|
|
89
|
+
executionId: string,
|
|
90
|
+
name: string,
|
|
91
|
+
schema: AnySchema<T>
|
|
92
|
+
): Promise<T> => {
|
|
93
|
+
const buffered = takeBufferedSignal(executionId, name, schema)
|
|
94
|
+
if (buffered !== undefined) {
|
|
95
|
+
return Promise.resolve(buffered)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const key = keyOf(executionId, name)
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const queue = waiters.get(key) ?? []
|
|
101
|
+
queue.push({ schema, resolve: resolve as (value: unknown) => void, reject })
|
|
102
|
+
waiters.set(key, queue)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const cancelSignalWaits = (executionId: string, error: unknown) => {
|
|
107
|
+
for (const [key, queuedWaiters] of waiters) {
|
|
108
|
+
if (!key.startsWith(`${executionId}\0`)) {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
waiters.delete(key)
|
|
112
|
+
for (const waiter of queuedWaiters) {
|
|
113
|
+
waiter.reject(error)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|