@rivet-dev/vercel-world 2.3.6
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/.turbo/turbo-build.log +4 -0
- package/README.md +31 -0
- package/dist/actors/coordinator.d.ts +114 -0
- package/dist/actors/coordinator.d.ts.map +1 -0
- package/dist/actors/coordinator.js +232 -0
- package/dist/actors/coordinator.js.map +1 -0
- package/dist/actors/db.d.ts +8 -0
- package/dist/actors/db.d.ts.map +1 -0
- package/dist/actors/db.js +282 -0
- package/dist/actors/db.js.map +1 -0
- package/dist/actors/dispatcher.d.ts +84 -0
- package/dist/actors/dispatcher.d.ts.map +1 -0
- package/dist/actors/dispatcher.js +498 -0
- package/dist/actors/dispatcher.js.map +1 -0
- package/dist/actors/hook-token.d.ts +26 -0
- package/dist/actors/hook-token.d.ts.map +1 -0
- package/dist/actors/hook-token.js +151 -0
- package/dist/actors/hook-token.js.map +1 -0
- package/dist/actors/shared.d.ts +366 -0
- package/dist/actors/shared.d.ts.map +1 -0
- package/dist/actors/shared.js +112 -0
- package/dist/actors/shared.js.map +1 -0
- package/dist/actors/streams.d.ts +26 -0
- package/dist/actors/streams.d.ts.map +1 -0
- package/dist/actors/streams.js +127 -0
- package/dist/actors/streams.js.map +1 -0
- package/dist/actors/workflow-run.d.ts +890 -0
- package/dist/actors/workflow-run.d.ts.map +1 -0
- package/dist/actors/workflow-run.js +863 -0
- package/dist/actors/workflow-run.js.map +1 -0
- package/dist/actors.d.ts +2204 -0
- package/dist/actors.d.ts.map +1 -0
- package/dist/actors.js +16 -0
- package/dist/actors.js.map +1 -0
- package/dist/codec.d.ts +4 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +27 -0
- package/dist/codec.js.map +1 -0
- package/dist/index.d.ts +843 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +553 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +24 -0
- package/dist/runtime.js.map +1 -0
- package/package.json +51 -0
- package/scripts/conformance/run.ts +278 -0
- package/scripts/integration/run.ts +935 -0
- package/src/actors/coordinator.ts +360 -0
- package/src/actors/db.ts +291 -0
- package/src/actors/dispatcher.ts +787 -0
- package/src/actors/hook-token.ts +239 -0
- package/src/actors/shared.ts +153 -0
- package/src/actors/streams.ts +215 -0
- package/src/actors/workflow-run.ts +1466 -0
- package/src/actors.ts +18 -0
- package/src/codec.ts +28 -0
- package/src/index.ts +768 -0
- package/src/runtime.ts +29 -0
- package/tests/conformance.test.ts +8 -0
- package/tests/helpers/db.ts +62 -0
- package/tests/helpers/dispatcher-driver.ts +71 -0
- package/tests/helpers/harness.ts +161 -0
- package/tests/integration/crash-restart.test.ts +145 -0
- package/tests/integration/dispatcher-loop.test.ts +144 -0
- package/tests/integration/hook-token.test.ts +160 -0
- package/tests/integration/hooks.test.ts +123 -0
- package/tests/integration/streams.test.ts +178 -0
- package/tests/integration/workflow-events.test.ts +326 -0
- package/tests/setup.ts +10 -0
- package/tests/unit/codec.test.ts +73 -0
- package/tests/unit/coordinator-record.test.ts +177 -0
- package/tests/unit/db-migrations.test.ts +65 -0
- package/tests/unit/dispatcher-queue.test.ts +274 -0
- package/tests/unit/logging.test.ts +49 -0
- package/tests/unit/readiness.test.ts +102 -0
- package/tests/unit/transaction.test.ts +76 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +12 -0
- package/vitest.config.ts +32 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ListHooksParams,
|
|
3
|
+
type ListWorkflowRunsParams,
|
|
4
|
+
type WorkflowRun,
|
|
5
|
+
} from "@workflow/world";
|
|
6
|
+
import { actor } from "rivetkit";
|
|
7
|
+
import { encodeValue } from "../codec.js";
|
|
8
|
+
import { coordinatorDb } from "./db.js";
|
|
9
|
+
import {
|
|
10
|
+
filterData,
|
|
11
|
+
one,
|
|
12
|
+
rowToRun,
|
|
13
|
+
toMs,
|
|
14
|
+
withActorTransaction,
|
|
15
|
+
type Ctx,
|
|
16
|
+
} from "./shared.js";
|
|
17
|
+
|
|
18
|
+
export type CoordinatorContext = Ctx;
|
|
19
|
+
|
|
20
|
+
type CorrelationKind = "step" | "hook" | "wait";
|
|
21
|
+
type Timestamp = Date | number | string;
|
|
22
|
+
|
|
23
|
+
export type CoordinatorIndexUpdate =
|
|
24
|
+
| { type: "run.upsert"; run: WorkflowRun; runRevision: number }
|
|
25
|
+
| {
|
|
26
|
+
type: "correlation.put" | "correlation.delete";
|
|
27
|
+
correlationId: string;
|
|
28
|
+
runId: string;
|
|
29
|
+
kind: CorrelationKind;
|
|
30
|
+
runRevision: number;
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
type: "hook.upsert" | "hook.delete";
|
|
34
|
+
hookId: string;
|
|
35
|
+
runId: string;
|
|
36
|
+
token: string;
|
|
37
|
+
runRevision: number;
|
|
38
|
+
createdAt: Timestamp;
|
|
39
|
+
updatedAt: Timestamp;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function assertCoordinatorKey(c: { key: readonly unknown[] }) {
|
|
43
|
+
if (String(c.key[0] ?? "") !== "coordinator" || c.key.length !== 1) {
|
|
44
|
+
throw new Error('coordinator actor key must be ["coordinator"]');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function assertRevision(revision: number) {
|
|
49
|
+
if (!Number.isSafeInteger(revision) || revision < 0) {
|
|
50
|
+
throw new Error("coordinator update requires a non-negative run revision");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function timestamp(value: Timestamp): number {
|
|
55
|
+
const result = toMs(value);
|
|
56
|
+
if (result == null || !Number.isFinite(result)) {
|
|
57
|
+
throw new Error("coordinator update requires a valid timestamp");
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function upsertRun(c: Ctx, run: WorkflowRun, runRevision: number) {
|
|
63
|
+
await c.db.execute(
|
|
64
|
+
`
|
|
65
|
+
INSERT INTO runs_index (
|
|
66
|
+
run_id, run_revision, status, deployment_id, workflow_name,
|
|
67
|
+
spec_version, execution_context, attributes, input, output, error,
|
|
68
|
+
expired_at, started_at, completed_at, created_at, updated_at
|
|
69
|
+
)
|
|
70
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
71
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
72
|
+
run_revision = excluded.run_revision,
|
|
73
|
+
status = excluded.status,
|
|
74
|
+
deployment_id = excluded.deployment_id,
|
|
75
|
+
workflow_name = excluded.workflow_name,
|
|
76
|
+
spec_version = excluded.spec_version,
|
|
77
|
+
execution_context = excluded.execution_context,
|
|
78
|
+
attributes = excluded.attributes,
|
|
79
|
+
input = excluded.input,
|
|
80
|
+
output = excluded.output,
|
|
81
|
+
error = excluded.error,
|
|
82
|
+
expired_at = excluded.expired_at,
|
|
83
|
+
started_at = excluded.started_at,
|
|
84
|
+
completed_at = excluded.completed_at,
|
|
85
|
+
created_at = excluded.created_at,
|
|
86
|
+
updated_at = excluded.updated_at
|
|
87
|
+
WHERE excluded.run_revision >= runs_index.run_revision
|
|
88
|
+
`,
|
|
89
|
+
run.runId,
|
|
90
|
+
runRevision,
|
|
91
|
+
run.status,
|
|
92
|
+
run.deploymentId,
|
|
93
|
+
run.workflowName,
|
|
94
|
+
run.specVersion ?? null,
|
|
95
|
+
encodeValue(run.executionContext),
|
|
96
|
+
encodeValue(run.attributes),
|
|
97
|
+
encodeValue(run.input),
|
|
98
|
+
encodeValue(run.output),
|
|
99
|
+
encodeValue(run.error),
|
|
100
|
+
toMs(run.expiredAt),
|
|
101
|
+
toMs(run.startedAt),
|
|
102
|
+
toMs(run.completedAt),
|
|
103
|
+
toMs(run.createdAt),
|
|
104
|
+
toMs(run.updatedAt),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function updateCorrelation(
|
|
109
|
+
c: Ctx,
|
|
110
|
+
input: Extract<
|
|
111
|
+
CoordinatorIndexUpdate,
|
|
112
|
+
{ type: "correlation.put" | "correlation.delete" }
|
|
113
|
+
>,
|
|
114
|
+
) {
|
|
115
|
+
const existing = await one(
|
|
116
|
+
c,
|
|
117
|
+
"SELECT run_id, kind FROM correlation_index WHERE correlation_id = ?",
|
|
118
|
+
input.correlationId,
|
|
119
|
+
);
|
|
120
|
+
if (
|
|
121
|
+
existing &&
|
|
122
|
+
(String(existing.run_id) !== input.runId ||
|
|
123
|
+
String(existing.kind) !== input.kind)
|
|
124
|
+
) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`correlation "${input.correlationId}" is already owned by another workflow entity`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const status = input.type === "correlation.put" ? "active" : "deleted";
|
|
131
|
+
await c.db.execute(
|
|
132
|
+
`
|
|
133
|
+
INSERT INTO correlation_index (
|
|
134
|
+
correlation_id, run_id, kind, run_revision, status
|
|
135
|
+
)
|
|
136
|
+
VALUES (?, ?, ?, ?, ?)
|
|
137
|
+
ON CONFLICT(correlation_id) DO UPDATE SET
|
|
138
|
+
run_revision = excluded.run_revision,
|
|
139
|
+
status = excluded.status
|
|
140
|
+
WHERE
|
|
141
|
+
NOT (
|
|
142
|
+
correlation_index.status = 'deleted'
|
|
143
|
+
AND excluded.status = 'active'
|
|
144
|
+
)
|
|
145
|
+
AND (
|
|
146
|
+
excluded.run_revision > correlation_index.run_revision
|
|
147
|
+
OR (
|
|
148
|
+
excluded.run_revision = correlation_index.run_revision
|
|
149
|
+
AND (
|
|
150
|
+
excluded.status = 'deleted'
|
|
151
|
+
OR correlation_index.status = excluded.status
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
`,
|
|
156
|
+
input.correlationId,
|
|
157
|
+
input.runId,
|
|
158
|
+
input.kind,
|
|
159
|
+
input.runRevision,
|
|
160
|
+
status,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function updateHook(
|
|
165
|
+
c: Ctx,
|
|
166
|
+
input: Extract<
|
|
167
|
+
CoordinatorIndexUpdate,
|
|
168
|
+
{ type: "hook.upsert" | "hook.delete" }
|
|
169
|
+
>,
|
|
170
|
+
) {
|
|
171
|
+
const existing = await one(
|
|
172
|
+
c,
|
|
173
|
+
"SELECT run_id FROM hooks_index WHERE hook_id = ?",
|
|
174
|
+
input.hookId,
|
|
175
|
+
);
|
|
176
|
+
if (existing && String(existing.run_id) !== input.runId) {
|
|
177
|
+
throw new Error(`hook "${input.hookId}" is already owned by another workflow run`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const status = input.type === "hook.upsert" ? "active" : "deleted";
|
|
181
|
+
await c.db.execute(
|
|
182
|
+
`
|
|
183
|
+
INSERT INTO hooks_index (
|
|
184
|
+
hook_id, run_id, run_revision, token, status, created_at, updated_at
|
|
185
|
+
)
|
|
186
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
187
|
+
ON CONFLICT(hook_id) DO UPDATE SET
|
|
188
|
+
run_revision = excluded.run_revision,
|
|
189
|
+
token = excluded.token,
|
|
190
|
+
status = excluded.status,
|
|
191
|
+
created_at = excluded.created_at,
|
|
192
|
+
updated_at = excluded.updated_at
|
|
193
|
+
WHERE
|
|
194
|
+
NOT (hooks_index.status = 'deleted' AND excluded.status = 'active')
|
|
195
|
+
AND (
|
|
196
|
+
excluded.run_revision > hooks_index.run_revision
|
|
197
|
+
OR (
|
|
198
|
+
excluded.run_revision = hooks_index.run_revision
|
|
199
|
+
AND (
|
|
200
|
+
excluded.status = 'deleted'
|
|
201
|
+
OR hooks_index.status = excluded.status
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
`,
|
|
206
|
+
input.hookId,
|
|
207
|
+
input.runId,
|
|
208
|
+
input.runRevision,
|
|
209
|
+
input.token,
|
|
210
|
+
status,
|
|
211
|
+
timestamp(input.createdAt),
|
|
212
|
+
timestamp(input.updatedAt),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Applies one derived index update. Canonical workflow progress never awaits this. */
|
|
217
|
+
export async function applyCoordinatorUpdate(
|
|
218
|
+
c: CoordinatorContext,
|
|
219
|
+
input: CoordinatorIndexUpdate,
|
|
220
|
+
) {
|
|
221
|
+
assertRevision(input.runRevision);
|
|
222
|
+
await withActorTransaction(c, async (tx) => {
|
|
223
|
+
switch (input.type) {
|
|
224
|
+
case "run.upsert":
|
|
225
|
+
await upsertRun(tx, input.run, input.runRevision);
|
|
226
|
+
break;
|
|
227
|
+
case "correlation.put":
|
|
228
|
+
case "correlation.delete":
|
|
229
|
+
await updateCorrelation(tx, input);
|
|
230
|
+
break;
|
|
231
|
+
case "hook.upsert":
|
|
232
|
+
case "hook.delete":
|
|
233
|
+
await updateHook(tx, input);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type IndexCursor = readonly [createdAt: number, id: string];
|
|
240
|
+
|
|
241
|
+
function encodeCursor(createdAt: unknown, id: unknown): string {
|
|
242
|
+
return JSON.stringify([Number(createdAt), String(id)] satisfies IndexCursor);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function decodeCursor(cursor: string | undefined): IndexCursor | undefined {
|
|
246
|
+
if (cursor == null) return undefined;
|
|
247
|
+
try {
|
|
248
|
+
const value: unknown = JSON.parse(cursor);
|
|
249
|
+
if (
|
|
250
|
+
!Array.isArray(value) ||
|
|
251
|
+
value.length !== 2 ||
|
|
252
|
+
!Number.isFinite(value[0]) ||
|
|
253
|
+
typeof value[1] !== "string"
|
|
254
|
+
) {
|
|
255
|
+
throw new Error();
|
|
256
|
+
}
|
|
257
|
+
return [Number(value[0]), value[1]];
|
|
258
|
+
} catch {
|
|
259
|
+
throw new Error("invalid coordinator pagination cursor");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export const coordinator = actor({
|
|
264
|
+
db: coordinatorDb,
|
|
265
|
+
actions: {
|
|
266
|
+
update: async (c, input: CoordinatorIndexUpdate) => {
|
|
267
|
+
assertCoordinatorKey(c);
|
|
268
|
+
await applyCoordinatorUpdate(c as unknown as CoordinatorContext, input);
|
|
269
|
+
},
|
|
270
|
+
getRunIdByCorrelation: async (c, correlationId: string) => {
|
|
271
|
+
assertCoordinatorKey(c);
|
|
272
|
+
const row = await one(
|
|
273
|
+
c,
|
|
274
|
+
`SELECT run_id FROM correlation_index
|
|
275
|
+
WHERE correlation_id = ? AND status = 'active'`,
|
|
276
|
+
correlationId,
|
|
277
|
+
);
|
|
278
|
+
return row?.run_id == null ? null : String(row.run_id);
|
|
279
|
+
},
|
|
280
|
+
getRunIdByHook: async (c, hookId: string) => {
|
|
281
|
+
assertCoordinatorKey(c);
|
|
282
|
+
const row = await one(
|
|
283
|
+
c,
|
|
284
|
+
"SELECT run_id FROM hooks_index WHERE hook_id = ? AND status = 'active'",
|
|
285
|
+
hookId,
|
|
286
|
+
);
|
|
287
|
+
return row?.run_id == null ? null : String(row.run_id);
|
|
288
|
+
},
|
|
289
|
+
listHookIndex: async (c, params?: ListHooksParams) => {
|
|
290
|
+
assertCoordinatorKey(c);
|
|
291
|
+
const limit = params?.pagination?.limit ?? 100;
|
|
292
|
+
const cursor = decodeCursor(params?.pagination?.cursor);
|
|
293
|
+
const where = ["status = 'active'"];
|
|
294
|
+
const args: unknown[] = [];
|
|
295
|
+
if (cursor) {
|
|
296
|
+
where.push("(created_at, hook_id) < (?, ?)");
|
|
297
|
+
args.push(...cursor);
|
|
298
|
+
}
|
|
299
|
+
const rows = await c.db.execute(
|
|
300
|
+
`
|
|
301
|
+
SELECT hook_id, run_id, created_at
|
|
302
|
+
FROM hooks_index
|
|
303
|
+
WHERE ${where.join(" AND ")}
|
|
304
|
+
ORDER BY created_at DESC, hook_id DESC
|
|
305
|
+
LIMIT ?
|
|
306
|
+
`,
|
|
307
|
+
...args,
|
|
308
|
+
limit + 1,
|
|
309
|
+
);
|
|
310
|
+
const values = rows.slice(0, limit);
|
|
311
|
+
const last = values.at(-1);
|
|
312
|
+
return {
|
|
313
|
+
data: values.map((row) => ({
|
|
314
|
+
hookId: String(row.hook_id),
|
|
315
|
+
runId: String(row.run_id),
|
|
316
|
+
})),
|
|
317
|
+
cursor: last ? encodeCursor(last.created_at, last.hook_id) : null,
|
|
318
|
+
hasMore: rows.length > limit,
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
listRuns: async (c, params?: ListWorkflowRunsParams) => {
|
|
322
|
+
assertCoordinatorKey(c);
|
|
323
|
+
const limit = params?.pagination?.limit ?? 20;
|
|
324
|
+
const cursor = decodeCursor(params?.pagination?.cursor);
|
|
325
|
+
const where: string[] = [];
|
|
326
|
+
const args: unknown[] = [];
|
|
327
|
+
if (cursor) {
|
|
328
|
+
where.push("(created_at, run_id) < (?, ?)");
|
|
329
|
+
args.push(...cursor);
|
|
330
|
+
}
|
|
331
|
+
if (params?.workflowName) {
|
|
332
|
+
where.push("workflow_name = ?");
|
|
333
|
+
args.push(params.workflowName);
|
|
334
|
+
}
|
|
335
|
+
if (params?.status) {
|
|
336
|
+
where.push("status = ?");
|
|
337
|
+
args.push(params.status);
|
|
338
|
+
}
|
|
339
|
+
const rows = await c.db.execute(
|
|
340
|
+
`
|
|
341
|
+
SELECT * FROM runs_index
|
|
342
|
+
${where.length > 0 ? `WHERE ${where.join(" AND ")}` : ""}
|
|
343
|
+
ORDER BY created_at DESC, run_id DESC
|
|
344
|
+
LIMIT ?
|
|
345
|
+
`,
|
|
346
|
+
...args,
|
|
347
|
+
limit + 1,
|
|
348
|
+
);
|
|
349
|
+
const values = rows.slice(0, limit);
|
|
350
|
+
const last = values.at(-1);
|
|
351
|
+
return {
|
|
352
|
+
data: values.map((row) =>
|
|
353
|
+
filterData(rowToRun(row), params?.resolveData),
|
|
354
|
+
),
|
|
355
|
+
cursor: last ? encodeCursor(last.created_at, last.run_id) : null,
|
|
356
|
+
hasMore: rows.length > limit,
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
});
|
package/src/actors/db.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { db } from "rivetkit/db";
|
|
2
|
+
import {
|
|
3
|
+
migrations,
|
|
4
|
+
type MigrationDatabase,
|
|
5
|
+
} from "rivetkit/unstable/migrations";
|
|
6
|
+
|
|
7
|
+
const bootstrapWorkflowDb = async (database: MigrationDatabase) => {
|
|
8
|
+
await database.execute(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
10
|
+
run_id TEXT PRIMARY KEY,
|
|
11
|
+
run_revision INTEGER NOT NULL DEFAULT 0,
|
|
12
|
+
status TEXT NOT NULL,
|
|
13
|
+
deployment_id TEXT NOT NULL,
|
|
14
|
+
workflow_name TEXT NOT NULL,
|
|
15
|
+
spec_version INTEGER,
|
|
16
|
+
execution_context BLOB,
|
|
17
|
+
attributes BLOB,
|
|
18
|
+
input BLOB,
|
|
19
|
+
output BLOB,
|
|
20
|
+
error BLOB,
|
|
21
|
+
expired_at INTEGER,
|
|
22
|
+
started_at INTEGER,
|
|
23
|
+
completed_at INTEGER,
|
|
24
|
+
created_at INTEGER NOT NULL,
|
|
25
|
+
updated_at INTEGER NOT NULL
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
await database.execute(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS workflow_events (
|
|
30
|
+
event_id TEXT PRIMARY KEY,
|
|
31
|
+
run_id TEXT NOT NULL,
|
|
32
|
+
event_type TEXT NOT NULL,
|
|
33
|
+
correlation_id TEXT,
|
|
34
|
+
event_data BLOB,
|
|
35
|
+
spec_version INTEGER,
|
|
36
|
+
created_at INTEGER NOT NULL
|
|
37
|
+
)
|
|
38
|
+
`);
|
|
39
|
+
await database.execute(`
|
|
40
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_events_create_step
|
|
41
|
+
ON workflow_events(correlation_id)
|
|
42
|
+
WHERE event_type = 'step_created'
|
|
43
|
+
`);
|
|
44
|
+
await database.execute(`
|
|
45
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_events_create_hook
|
|
46
|
+
ON workflow_events(correlation_id)
|
|
47
|
+
WHERE event_type = 'hook_created'
|
|
48
|
+
`);
|
|
49
|
+
await database.execute(`
|
|
50
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_events_create_wait
|
|
51
|
+
ON workflow_events(correlation_id)
|
|
52
|
+
WHERE event_type = 'wait_created'
|
|
53
|
+
`);
|
|
54
|
+
// Creation-event dedup (SPEC §3.2). IMPORTANT: every index here is scoped to a
|
|
55
|
+
// SINGLE workflowRun actor's own `c.db` — there is one `workflow_events` table
|
|
56
|
+
// per run. `uq_events_create_run` enforces "one run_created per database",
|
|
57
|
+
// which is correct ONLY because each run owns its database. Do NOT move these
|
|
58
|
+
// tables into a shared/global database without re-scoping these indexes by
|
|
59
|
+
// `run_id`, or the run-singleton guarantee silently collapses.
|
|
60
|
+
await database.execute(`
|
|
61
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_events_create_run
|
|
62
|
+
ON workflow_events(event_type)
|
|
63
|
+
WHERE event_type = 'run_created'
|
|
64
|
+
`);
|
|
65
|
+
await database.execute(`
|
|
66
|
+
CREATE TABLE IF NOT EXISTS workflow_steps (
|
|
67
|
+
step_id TEXT PRIMARY KEY,
|
|
68
|
+
run_id TEXT NOT NULL,
|
|
69
|
+
step_name TEXT NOT NULL,
|
|
70
|
+
status TEXT NOT NULL,
|
|
71
|
+
input BLOB,
|
|
72
|
+
output BLOB,
|
|
73
|
+
error BLOB,
|
|
74
|
+
attempt INTEGER NOT NULL,
|
|
75
|
+
started_at INTEGER,
|
|
76
|
+
completed_at INTEGER,
|
|
77
|
+
created_at INTEGER NOT NULL,
|
|
78
|
+
updated_at INTEGER NOT NULL,
|
|
79
|
+
retry_after INTEGER,
|
|
80
|
+
spec_version INTEGER
|
|
81
|
+
)
|
|
82
|
+
`);
|
|
83
|
+
await database.execute(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS workflow_hooks (
|
|
85
|
+
hook_id TEXT PRIMARY KEY,
|
|
86
|
+
run_id TEXT NOT NULL,
|
|
87
|
+
token TEXT NOT NULL,
|
|
88
|
+
token_generation INTEGER,
|
|
89
|
+
owner_id TEXT NOT NULL,
|
|
90
|
+
project_id TEXT NOT NULL,
|
|
91
|
+
environment TEXT NOT NULL,
|
|
92
|
+
metadata BLOB,
|
|
93
|
+
created_at INTEGER NOT NULL,
|
|
94
|
+
spec_version INTEGER,
|
|
95
|
+
is_webhook INTEGER
|
|
96
|
+
)
|
|
97
|
+
`);
|
|
98
|
+
await database.execute(`
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_hooks_token
|
|
100
|
+
ON workflow_hooks(token)
|
|
101
|
+
`);
|
|
102
|
+
await database.execute(`
|
|
103
|
+
CREATE TABLE IF NOT EXISTS workflow_waits (
|
|
104
|
+
wait_id TEXT PRIMARY KEY,
|
|
105
|
+
run_id TEXT NOT NULL,
|
|
106
|
+
status TEXT NOT NULL,
|
|
107
|
+
resume_at INTEGER,
|
|
108
|
+
runtime_url TEXT,
|
|
109
|
+
queue_name TEXT,
|
|
110
|
+
headers TEXT,
|
|
111
|
+
resume_enqueued_at INTEGER,
|
|
112
|
+
completed_at INTEGER,
|
|
113
|
+
created_at INTEGER NOT NULL,
|
|
114
|
+
updated_at INTEGER NOT NULL,
|
|
115
|
+
spec_version INTEGER
|
|
116
|
+
)
|
|
117
|
+
`);
|
|
118
|
+
await database.execute(`
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_waits_due
|
|
120
|
+
ON workflow_waits(status, resume_at)
|
|
121
|
+
`);
|
|
122
|
+
await database.execute(`
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_events_correlation
|
|
124
|
+
ON workflow_events(correlation_id, event_id)
|
|
125
|
+
`);
|
|
126
|
+
await database.execute(`
|
|
127
|
+
CREATE TABLE IF NOT EXISTS dispatcher_queue (
|
|
128
|
+
message_id TEXT PRIMARY KEY,
|
|
129
|
+
queue_name TEXT NOT NULL,
|
|
130
|
+
route TEXT NOT NULL,
|
|
131
|
+
runtime_url TEXT NOT NULL,
|
|
132
|
+
body TEXT NOT NULL,
|
|
133
|
+
headers TEXT,
|
|
134
|
+
idem_key TEXT,
|
|
135
|
+
status TEXT NOT NULL,
|
|
136
|
+
attempt INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
next_at INTEGER NOT NULL,
|
|
138
|
+
created_at INTEGER NOT NULL,
|
|
139
|
+
updated_at INTEGER NOT NULL,
|
|
140
|
+
started_at INTEGER,
|
|
141
|
+
finished_at INTEGER,
|
|
142
|
+
http_status INTEGER,
|
|
143
|
+
error TEXT
|
|
144
|
+
)
|
|
145
|
+
`);
|
|
146
|
+
await database.execute(`
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_dispatcher_queue_ready
|
|
148
|
+
ON dispatcher_queue(status, next_at, created_at)
|
|
149
|
+
`);
|
|
150
|
+
await database.execute(`
|
|
151
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_dispatcher_queue_idem
|
|
152
|
+
ON dispatcher_queue(idem_key) WHERE idem_key IS NOT NULL
|
|
153
|
+
`);
|
|
154
|
+
await database.execute(`
|
|
155
|
+
CREATE TABLE IF NOT EXISTS stream_chunks (
|
|
156
|
+
stream_id TEXT NOT NULL,
|
|
157
|
+
sequence INTEGER NOT NULL,
|
|
158
|
+
chunk_data BLOB,
|
|
159
|
+
eof INTEGER NOT NULL,
|
|
160
|
+
created_at INTEGER NOT NULL,
|
|
161
|
+
PRIMARY KEY (stream_id, sequence)
|
|
162
|
+
)
|
|
163
|
+
`);
|
|
164
|
+
await database.execute(`
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_stream_chunks_stream
|
|
166
|
+
ON stream_chunks(stream_id, sequence)
|
|
167
|
+
`);
|
|
168
|
+
await database.execute(`
|
|
169
|
+
CREATE TABLE IF NOT EXISTS run_streams (
|
|
170
|
+
stream_id TEXT PRIMARY KEY
|
|
171
|
+
)
|
|
172
|
+
`);
|
|
173
|
+
await database.execute(`
|
|
174
|
+
CREATE TABLE IF NOT EXISTS pending_ops (
|
|
175
|
+
op_id TEXT PRIMARY KEY,
|
|
176
|
+
op_type TEXT NOT NULL,
|
|
177
|
+
payload BLOB NOT NULL,
|
|
178
|
+
operation_revision INTEGER NOT NULL,
|
|
179
|
+
status TEXT NOT NULL CHECK (status IN ('ready', 'inflight')),
|
|
180
|
+
claim_id TEXT,
|
|
181
|
+
next_at INTEGER NOT NULL,
|
|
182
|
+
attempt INTEGER NOT NULL DEFAULT 0,
|
|
183
|
+
created_at INTEGER NOT NULL,
|
|
184
|
+
updated_at INTEGER NOT NULL
|
|
185
|
+
)
|
|
186
|
+
`);
|
|
187
|
+
await database.execute(`
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_pending_ops_due
|
|
189
|
+
ON pending_ops(status, next_at, created_at)
|
|
190
|
+
`);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const bootstrapCoordinatorDb = async (database: MigrationDatabase) => {
|
|
194
|
+
await database.execute(`
|
|
195
|
+
CREATE TABLE IF NOT EXISTS runs_index (
|
|
196
|
+
run_id TEXT PRIMARY KEY,
|
|
197
|
+
run_revision INTEGER NOT NULL,
|
|
198
|
+
status TEXT NOT NULL,
|
|
199
|
+
deployment_id TEXT NOT NULL,
|
|
200
|
+
workflow_name TEXT NOT NULL,
|
|
201
|
+
spec_version INTEGER,
|
|
202
|
+
execution_context BLOB,
|
|
203
|
+
attributes BLOB,
|
|
204
|
+
input BLOB,
|
|
205
|
+
output BLOB,
|
|
206
|
+
error BLOB,
|
|
207
|
+
expired_at INTEGER,
|
|
208
|
+
started_at INTEGER,
|
|
209
|
+
completed_at INTEGER,
|
|
210
|
+
created_at INTEGER NOT NULL,
|
|
211
|
+
updated_at INTEGER NOT NULL
|
|
212
|
+
)
|
|
213
|
+
`);
|
|
214
|
+
await database.execute(`
|
|
215
|
+
CREATE INDEX IF NOT EXISTS runs_index_list
|
|
216
|
+
ON runs_index(created_at DESC, run_id DESC)
|
|
217
|
+
`);
|
|
218
|
+
await database.execute(`
|
|
219
|
+
CREATE INDEX IF NOT EXISTS runs_index_name_list
|
|
220
|
+
ON runs_index(workflow_name, created_at DESC, run_id DESC)
|
|
221
|
+
`);
|
|
222
|
+
await database.execute(`
|
|
223
|
+
CREATE INDEX IF NOT EXISTS runs_index_status_list
|
|
224
|
+
ON runs_index(status, created_at DESC, run_id DESC)
|
|
225
|
+
`);
|
|
226
|
+
await database.execute(`
|
|
227
|
+
CREATE INDEX IF NOT EXISTS runs_index_name_status_list
|
|
228
|
+
ON runs_index(workflow_name, status, created_at DESC, run_id DESC)
|
|
229
|
+
`);
|
|
230
|
+
await database.execute(`
|
|
231
|
+
CREATE TABLE IF NOT EXISTS correlation_index (
|
|
232
|
+
correlation_id TEXT PRIMARY KEY,
|
|
233
|
+
run_id TEXT NOT NULL,
|
|
234
|
+
kind TEXT NOT NULL CHECK (kind IN ('step', 'hook', 'wait')),
|
|
235
|
+
run_revision INTEGER NOT NULL,
|
|
236
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'deleted'))
|
|
237
|
+
)
|
|
238
|
+
`);
|
|
239
|
+
await database.execute(`
|
|
240
|
+
CREATE TABLE IF NOT EXISTS hooks_index (
|
|
241
|
+
hook_id TEXT PRIMARY KEY,
|
|
242
|
+
run_id TEXT NOT NULL,
|
|
243
|
+
run_revision INTEGER NOT NULL,
|
|
244
|
+
token TEXT NOT NULL,
|
|
245
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'deleted')),
|
|
246
|
+
created_at INTEGER NOT NULL,
|
|
247
|
+
updated_at INTEGER NOT NULL
|
|
248
|
+
)
|
|
249
|
+
`);
|
|
250
|
+
await database.execute(`
|
|
251
|
+
CREATE INDEX IF NOT EXISTS hooks_index_list
|
|
252
|
+
ON hooks_index(status, created_at DESC, hook_id DESC)
|
|
253
|
+
`);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const bootstrapHookTokenDb = async (database: MigrationDatabase) => {
|
|
257
|
+
await database.execute(`
|
|
258
|
+
CREATE TABLE IF NOT EXISTS hook_token_state (
|
|
259
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
260
|
+
generation INTEGER NOT NULL,
|
|
261
|
+
run_id TEXT,
|
|
262
|
+
hook_id TEXT,
|
|
263
|
+
status TEXT NOT NULL CHECK (status IN ('empty', 'pending', 'confirmed')),
|
|
264
|
+
created_at INTEGER,
|
|
265
|
+
updated_at INTEGER NOT NULL,
|
|
266
|
+
expires_at INTEGER
|
|
267
|
+
)
|
|
268
|
+
`);
|
|
269
|
+
await database.execute(
|
|
270
|
+
`INSERT OR IGNORE INTO hook_token_state (
|
|
271
|
+
singleton, generation, status, updated_at
|
|
272
|
+
) VALUES (1, 0, 'empty', ?)`,
|
|
273
|
+
Date.now(),
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export const migrateWorkflowDb = migrations({
|
|
278
|
+
tableName: "vercel_world_workflow_schema_version",
|
|
279
|
+
migrations: [{ version: 1, up: bootstrapWorkflowDb }],
|
|
280
|
+
});
|
|
281
|
+
export const migrateCoordinatorDb = migrations({
|
|
282
|
+
tableName: "vercel_world_coordinator_schema_version",
|
|
283
|
+
migrations: [{ version: 1, up: bootstrapCoordinatorDb }],
|
|
284
|
+
});
|
|
285
|
+
export const migrateHookTokenDb = migrations({
|
|
286
|
+
tableName: "vercel_world_hook_token_schema_version",
|
|
287
|
+
migrations: [{ version: 1, up: bootstrapHookTokenDb }],
|
|
288
|
+
});
|
|
289
|
+
export const workflowDb = db({ onMigrate: migrateWorkflowDb });
|
|
290
|
+
export const coordinatorDb = db({ onMigrate: migrateCoordinatorDb });
|
|
291
|
+
export const hookTokenDb = db({ onMigrate: migrateHookTokenDb });
|