@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.
Files changed (81) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/README.md +31 -0
  3. package/dist/actors/coordinator.d.ts +114 -0
  4. package/dist/actors/coordinator.d.ts.map +1 -0
  5. package/dist/actors/coordinator.js +232 -0
  6. package/dist/actors/coordinator.js.map +1 -0
  7. package/dist/actors/db.d.ts +8 -0
  8. package/dist/actors/db.d.ts.map +1 -0
  9. package/dist/actors/db.js +282 -0
  10. package/dist/actors/db.js.map +1 -0
  11. package/dist/actors/dispatcher.d.ts +84 -0
  12. package/dist/actors/dispatcher.d.ts.map +1 -0
  13. package/dist/actors/dispatcher.js +498 -0
  14. package/dist/actors/dispatcher.js.map +1 -0
  15. package/dist/actors/hook-token.d.ts +26 -0
  16. package/dist/actors/hook-token.d.ts.map +1 -0
  17. package/dist/actors/hook-token.js +151 -0
  18. package/dist/actors/hook-token.js.map +1 -0
  19. package/dist/actors/shared.d.ts +366 -0
  20. package/dist/actors/shared.d.ts.map +1 -0
  21. package/dist/actors/shared.js +112 -0
  22. package/dist/actors/shared.js.map +1 -0
  23. package/dist/actors/streams.d.ts +26 -0
  24. package/dist/actors/streams.d.ts.map +1 -0
  25. package/dist/actors/streams.js +127 -0
  26. package/dist/actors/streams.js.map +1 -0
  27. package/dist/actors/workflow-run.d.ts +890 -0
  28. package/dist/actors/workflow-run.d.ts.map +1 -0
  29. package/dist/actors/workflow-run.js +863 -0
  30. package/dist/actors/workflow-run.js.map +1 -0
  31. package/dist/actors.d.ts +2204 -0
  32. package/dist/actors.d.ts.map +1 -0
  33. package/dist/actors.js +16 -0
  34. package/dist/actors.js.map +1 -0
  35. package/dist/codec.d.ts +4 -0
  36. package/dist/codec.d.ts.map +1 -0
  37. package/dist/codec.js +27 -0
  38. package/dist/codec.js.map +1 -0
  39. package/dist/index.d.ts +843 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +553 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/runtime.d.ts +2 -0
  44. package/dist/runtime.d.ts.map +1 -0
  45. package/dist/runtime.js +24 -0
  46. package/dist/runtime.js.map +1 -0
  47. package/package.json +51 -0
  48. package/scripts/conformance/run.ts +278 -0
  49. package/scripts/integration/run.ts +935 -0
  50. package/src/actors/coordinator.ts +360 -0
  51. package/src/actors/db.ts +291 -0
  52. package/src/actors/dispatcher.ts +787 -0
  53. package/src/actors/hook-token.ts +239 -0
  54. package/src/actors/shared.ts +153 -0
  55. package/src/actors/streams.ts +215 -0
  56. package/src/actors/workflow-run.ts +1466 -0
  57. package/src/actors.ts +18 -0
  58. package/src/codec.ts +28 -0
  59. package/src/index.ts +768 -0
  60. package/src/runtime.ts +29 -0
  61. package/tests/conformance.test.ts +8 -0
  62. package/tests/helpers/db.ts +62 -0
  63. package/tests/helpers/dispatcher-driver.ts +71 -0
  64. package/tests/helpers/harness.ts +161 -0
  65. package/tests/integration/crash-restart.test.ts +145 -0
  66. package/tests/integration/dispatcher-loop.test.ts +144 -0
  67. package/tests/integration/hook-token.test.ts +160 -0
  68. package/tests/integration/hooks.test.ts +123 -0
  69. package/tests/integration/streams.test.ts +178 -0
  70. package/tests/integration/workflow-events.test.ts +326 -0
  71. package/tests/setup.ts +10 -0
  72. package/tests/unit/codec.test.ts +73 -0
  73. package/tests/unit/coordinator-record.test.ts +177 -0
  74. package/tests/unit/db-migrations.test.ts +65 -0
  75. package/tests/unit/dispatcher-queue.test.ts +274 -0
  76. package/tests/unit/logging.test.ts +49 -0
  77. package/tests/unit/readiness.test.ts +102 -0
  78. package/tests/unit/transaction.test.ts +76 -0
  79. package/tsconfig.build.json +13 -0
  80. package/tsconfig.json +12 -0
  81. 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
+ });
@@ -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 });