workerflow 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.
@@ -0,0 +1,559 @@
1
+ export default `
2
+ -- All timestamps are INTEGER milliseconds since Unix epoch.
3
+
4
+ CREATE TABLE workflow_metadata (
5
+ id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
6
+
7
+ status TEXT NOT NULL CHECK (
8
+ status IN ('pending', 'running', 'paused', 'completed', 'failed', 'cancelled')
9
+ ),
10
+
11
+ created_at INTEGER NOT NULL
12
+ DEFAULT (CAST(unixepoch('subsecond') * 1000 AS INTEGER))
13
+ CHECK (created_at >= 0),
14
+
15
+ updated_at INTEGER NOT NULL
16
+ DEFAULT (CAST(unixepoch('subsecond') * 1000 AS INTEGER)),
17
+
18
+ definition_version TEXT
19
+ CHECK (definition_version IS NULL OR length(definition_version) > 0),
20
+ definition_input TEXT
21
+ CHECK (definition_input IS NULL OR json_valid(definition_input)),
22
+
23
+ CHECK (updated_at >= created_at),
24
+
25
+ -- definition_version and definition_input must be set together or not set at all
26
+ CHECK (definition_version IS NOT NULL OR definition_input IS NULL),
27
+
28
+ -- definition must be pinned before running/paused/completing/failing; cancelled is always allowed
29
+ CHECK (status IN ('pending', 'cancelled') OR definition_version IS NOT NULL)
30
+ ) STRICT;
31
+
32
+ CREATE TABLE steps (
33
+ id TEXT NOT NULL PRIMARY KEY CHECK (length(id) > 0),
34
+ type TEXT NOT NULL CHECK (type IN ('run', 'sleep', 'wait')),
35
+ state TEXT NOT NULL CHECK (state IN (
36
+ 'pending',
37
+ 'running',
38
+ 'succeeded',
39
+ 'failed',
40
+ 'waiting',
41
+ 'elapsed',
42
+ 'satisfied',
43
+ 'timed_out'
44
+ )),
45
+ created_at INTEGER NOT NULL
46
+ DEFAULT (CAST(unixepoch('subsecond') * 1000 AS INTEGER))
47
+ CHECK (created_at >= 0),
48
+
49
+ -- run-step fields
50
+ attempt_count INTEGER,
51
+ max_attempts INTEGER,
52
+ next_attempt_at INTEGER,
53
+ result TEXT,
54
+ error_message TEXT,
55
+ error_name TEXT,
56
+
57
+ -- sleep-step fields
58
+ wake_at INTEGER,
59
+
60
+ -- wait-step fields
61
+ event_name TEXT,
62
+ timeout_at INTEGER,
63
+ payload TEXT,
64
+
65
+ -- terminal timestamp
66
+ resolved_at INTEGER,
67
+
68
+ -- innermost enclosing run step when this row was created (nested run / sleep / wait under a run callback)
69
+ parent_step_id TEXT REFERENCES steps(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
70
+
71
+ CHECK (attempt_count IS NULL OR attempt_count >= 0),
72
+ CHECK (max_attempts IS NULL OR max_attempts >= 1),
73
+ CHECK (next_attempt_at IS NULL OR next_attempt_at >= 0),
74
+ CHECK (wake_at IS NULL OR wake_at >= 0),
75
+ CHECK (timeout_at IS NULL OR timeout_at >= 0),
76
+ CHECK (resolved_at IS NULL OR resolved_at >= created_at),
77
+ CHECK (error_name IS NULL OR length(error_name) > 0),
78
+ CHECK (event_name IS NULL OR length(event_name) > 0),
79
+
80
+ -- run steps may never exceed max_attempts
81
+ CHECK (
82
+ attempt_count IS NULL OR
83
+ max_attempts IS NULL OR
84
+ attempt_count <= max_attempts
85
+ ),
86
+
87
+ CHECK (
88
+ (
89
+ type = 'run' AND
90
+ state = 'pending' AND
91
+ attempt_count IS NOT NULL AND attempt_count >= 0 AND
92
+ (max_attempts IS NULL OR max_attempts >= 1) AND
93
+ (max_attempts IS NULL OR attempt_count < max_attempts) AND
94
+ next_attempt_at IS NOT NULL AND
95
+ result IS NULL AND
96
+ error_message IS NULL AND
97
+ error_name IS NULL AND
98
+ wake_at IS NULL AND
99
+ event_name IS NULL AND
100
+ timeout_at IS NULL AND
101
+ payload IS NULL AND
102
+ resolved_at IS NULL
103
+ )
104
+ OR
105
+ (
106
+ type = 'run' AND
107
+ state = 'running' AND
108
+ attempt_count IS NOT NULL AND attempt_count >= 1 AND
109
+ (max_attempts IS NULL OR max_attempts >= 1) AND
110
+ (max_attempts IS NULL OR attempt_count <= max_attempts) AND
111
+ next_attempt_at IS NULL AND
112
+ result IS NULL AND
113
+ error_message IS NULL AND
114
+ error_name IS NULL AND
115
+ wake_at IS NULL AND
116
+ event_name IS NULL AND
117
+ timeout_at IS NULL AND
118
+ payload IS NULL AND
119
+ resolved_at IS NULL
120
+ )
121
+ OR
122
+ (
123
+ type = 'run' AND
124
+ state = 'succeeded' AND
125
+ attempt_count IS NOT NULL AND attempt_count >= 1 AND
126
+ (max_attempts IS NULL OR max_attempts >= 1) AND
127
+ (max_attempts IS NULL OR attempt_count <= max_attempts) AND
128
+ next_attempt_at IS NULL AND
129
+ result IS NOT NULL AND
130
+ error_message IS NULL AND
131
+ error_name IS NULL AND
132
+ wake_at IS NULL AND
133
+ event_name IS NULL AND
134
+ timeout_at IS NULL AND
135
+ payload IS NULL AND
136
+ resolved_at IS NOT NULL
137
+ )
138
+ OR
139
+ (
140
+ type = 'run' AND
141
+ state = 'failed' AND
142
+ attempt_count IS NOT NULL AND attempt_count >= 1 AND
143
+ (max_attempts IS NULL OR max_attempts >= 1) AND
144
+ (max_attempts IS NULL OR attempt_count <= max_attempts) AND
145
+ next_attempt_at IS NULL AND
146
+ result IS NULL AND
147
+ error_message IS NOT NULL AND
148
+ wake_at IS NULL AND
149
+ event_name IS NULL AND
150
+ timeout_at IS NULL AND
151
+ payload IS NULL AND
152
+ resolved_at IS NOT NULL
153
+ )
154
+ OR
155
+ (
156
+ type = 'sleep' AND
157
+ state = 'waiting' AND
158
+ attempt_count IS NULL AND
159
+ max_attempts IS NULL AND
160
+ next_attempt_at IS NULL AND
161
+ result IS NULL AND
162
+ error_message IS NULL AND
163
+ error_name IS NULL AND
164
+ wake_at IS NOT NULL AND
165
+ event_name IS NULL AND
166
+ timeout_at IS NULL AND
167
+ payload IS NULL AND
168
+ resolved_at IS NULL
169
+ )
170
+ OR
171
+ (
172
+ type = 'sleep' AND
173
+ state = 'elapsed' AND
174
+ attempt_count IS NULL AND
175
+ max_attempts IS NULL AND
176
+ next_attempt_at IS NULL AND
177
+ result IS NULL AND
178
+ error_message IS NULL AND
179
+ error_name IS NULL AND
180
+ wake_at IS NULL AND
181
+ event_name IS NULL AND
182
+ timeout_at IS NULL AND
183
+ payload IS NULL AND
184
+ resolved_at IS NOT NULL
185
+ )
186
+ OR
187
+ (
188
+ type = 'wait' AND
189
+ state = 'waiting' AND
190
+ attempt_count IS NULL AND
191
+ max_attempts IS NULL AND
192
+ next_attempt_at IS NULL AND
193
+ result IS NULL AND
194
+ error_message IS NULL AND
195
+ error_name IS NULL AND
196
+ wake_at IS NULL AND
197
+ event_name IS NOT NULL AND
198
+ payload IS NULL AND
199
+ resolved_at IS NULL
200
+ )
201
+ OR
202
+ (
203
+ type = 'wait' AND
204
+ state = 'satisfied' AND
205
+ attempt_count IS NULL AND
206
+ max_attempts IS NULL AND
207
+ next_attempt_at IS NULL AND
208
+ result IS NULL AND
209
+ error_message IS NULL AND
210
+ error_name IS NULL AND
211
+ wake_at IS NULL AND
212
+ event_name IS NOT NULL AND
213
+ timeout_at IS NULL AND
214
+ payload IS NOT NULL AND
215
+ resolved_at IS NOT NULL
216
+ )
217
+ OR
218
+ (
219
+ type = 'wait' AND
220
+ state = 'timed_out' AND
221
+ attempt_count IS NULL AND
222
+ max_attempts IS NULL AND
223
+ next_attempt_at IS NULL AND
224
+ result IS NULL AND
225
+ error_message IS NULL AND
226
+ error_name IS NULL AND
227
+ wake_at IS NULL AND
228
+ event_name IS NOT NULL AND
229
+ timeout_at IS NULL AND
230
+ payload IS NULL AND
231
+ resolved_at IS NOT NULL
232
+ )
233
+ ),
234
+ CHECK (parent_step_id IS NULL OR parent_step_id <> id)
235
+ ) STRICT;
236
+
237
+ CREATE TABLE step_events (
238
+ id TEXT NOT NULL PRIMARY KEY
239
+ DEFAULT (lower(hex(randomblob(16))))
240
+ CHECK (length(id) > 0),
241
+
242
+ step_id TEXT NOT NULL,
243
+ recorded_at INTEGER NOT NULL
244
+ DEFAULT (CAST(unixepoch('subsecond') * 1000 AS INTEGER))
245
+ CHECK (recorded_at >= 0),
246
+
247
+ type TEXT NOT NULL CHECK (type IN (
248
+ 'attempt_started',
249
+ 'attempt_succeeded',
250
+ 'attempt_failed',
251
+ 'sleep_waiting',
252
+ 'sleep_elapsed',
253
+ 'wait_waiting',
254
+ 'wait_satisfied',
255
+ 'wait_timed_out'
256
+ )),
257
+
258
+ attempt_number INTEGER,
259
+ result TEXT,
260
+ error_message TEXT,
261
+ error_name TEXT,
262
+ next_attempt_at INTEGER,
263
+ wake_at INTEGER,
264
+ event_name TEXT,
265
+ timeout_at INTEGER,
266
+ payload TEXT,
267
+
268
+ FOREIGN KEY (step_id) REFERENCES steps(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
269
+
270
+ CHECK (attempt_number IS NULL OR attempt_number >= 1),
271
+ CHECK (next_attempt_at IS NULL OR next_attempt_at >= 0),
272
+ CHECK (wake_at IS NULL OR wake_at >= 0),
273
+ CHECK (timeout_at IS NULL OR timeout_at >= 0),
274
+ CHECK (error_name IS NULL OR length(error_name) > 0),
275
+ CHECK (event_name IS NULL OR length(event_name) > 0),
276
+
277
+ CHECK (
278
+ (
279
+ type = 'attempt_started' AND
280
+ attempt_number IS NOT NULL AND
281
+ result IS NULL AND
282
+ error_message IS NULL AND
283
+ error_name IS NULL AND
284
+ next_attempt_at IS NULL AND
285
+ wake_at IS NULL AND
286
+ event_name IS NULL AND
287
+ timeout_at IS NULL AND
288
+ payload IS NULL
289
+ )
290
+ OR
291
+ (
292
+ type = 'attempt_succeeded' AND
293
+ attempt_number IS NOT NULL AND
294
+ result IS NOT NULL AND
295
+ error_message IS NULL AND
296
+ error_name IS NULL AND
297
+ next_attempt_at IS NULL AND
298
+ wake_at IS NULL AND
299
+ event_name IS NULL AND
300
+ timeout_at IS NULL AND
301
+ payload IS NULL
302
+ )
303
+ OR
304
+ (
305
+ type = 'attempt_failed' AND
306
+ attempt_number IS NOT NULL AND
307
+ result IS NULL AND
308
+ error_message IS NOT NULL AND
309
+ wake_at IS NULL AND
310
+ event_name IS NULL AND
311
+ timeout_at IS NULL AND
312
+ payload IS NULL
313
+ )
314
+ OR
315
+ (
316
+ type = 'sleep_waiting' AND
317
+ attempt_number IS NULL AND
318
+ result IS NULL AND
319
+ error_message IS NULL AND
320
+ error_name IS NULL AND
321
+ next_attempt_at IS NULL AND
322
+ wake_at IS NOT NULL AND
323
+ event_name IS NULL AND
324
+ timeout_at IS NULL AND
325
+ payload IS NULL
326
+ )
327
+ OR
328
+ (
329
+ type = 'sleep_elapsed' AND
330
+ attempt_number IS NULL AND
331
+ result IS NULL AND
332
+ error_message IS NULL AND
333
+ error_name IS NULL AND
334
+ next_attempt_at IS NULL AND
335
+ wake_at IS NULL AND
336
+ event_name IS NULL AND
337
+ timeout_at IS NULL AND
338
+ payload IS NULL
339
+ )
340
+ OR
341
+ (
342
+ type = 'wait_waiting' AND
343
+ attempt_number IS NULL AND
344
+ result IS NULL AND
345
+ error_message IS NULL AND
346
+ error_name IS NULL AND
347
+ next_attempt_at IS NULL AND
348
+ wake_at IS NULL AND
349
+ event_name IS NOT NULL AND
350
+ payload IS NULL
351
+ )
352
+ OR
353
+ (
354
+ type = 'wait_satisfied' AND
355
+ attempt_number IS NULL AND
356
+ result IS NULL AND
357
+ error_message IS NULL AND
358
+ error_name IS NULL AND
359
+ next_attempt_at IS NULL AND
360
+ wake_at IS NULL AND
361
+ event_name IS NULL AND
362
+ timeout_at IS NULL AND
363
+ payload IS NOT NULL
364
+ )
365
+ OR
366
+ (
367
+ type = 'wait_timed_out' AND
368
+ attempt_number IS NULL AND
369
+ result IS NULL AND
370
+ error_message IS NULL AND
371
+ error_name IS NULL AND
372
+ next_attempt_at IS NULL AND
373
+ wake_at IS NULL AND
374
+ event_name IS NULL AND
375
+ timeout_at IS NULL AND
376
+ payload IS NULL
377
+ )
378
+ )
379
+ ) STRICT;
380
+
381
+ CREATE TABLE workflow_events (
382
+ id INTEGER NOT NULL PRIMARY KEY,
383
+
384
+ recorded_at INTEGER NOT NULL
385
+ DEFAULT (CAST(unixepoch('subsecond') * 1000 AS INTEGER))
386
+ CHECK (recorded_at >= 0),
387
+
388
+ type TEXT NOT NULL CHECK (type IN (
389
+ 'created',
390
+ 'started',
391
+ 'paused',
392
+ 'resumed',
393
+ 'completed',
394
+ 'failed',
395
+ 'cancelled'
396
+ )),
397
+
398
+ cancellation_reason TEXT,
399
+
400
+ CHECK (cancellation_reason IS NULL OR type = 'cancelled')
401
+ ) STRICT;
402
+
403
+ CREATE TABLE inbound_events (
404
+ id TEXT NOT NULL PRIMARY KEY
405
+ DEFAULT (lower(hex(randomblob(16))))
406
+ CHECK (length(id) > 0),
407
+ event_name TEXT NOT NULL CHECK (length(event_name) > 0),
408
+ payload TEXT CHECK (payload IS NULL OR json_valid(payload)),
409
+ created_at INTEGER NOT NULL
410
+ DEFAULT (CAST(unixepoch('subsecond') * 1000 AS INTEGER))
411
+ CHECK (created_at >= 0),
412
+ claimed_by TEXT,
413
+ claimed_at INTEGER,
414
+
415
+ FOREIGN KEY (claimed_by) REFERENCES steps(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
416
+
417
+ -- claimed_by and claimed_at must be set together or not set at all
418
+ CHECK (
419
+ (claimed_by IS NULL AND claimed_at IS NULL) OR
420
+ (claimed_by IS NOT NULL AND claimed_at IS NOT NULL)
421
+ )
422
+ ) STRICT;
423
+
424
+ -- scheduler/query indexes
425
+ CREATE INDEX steps_run_pending_by_time_idx
426
+ ON steps(next_attempt_at, id)
427
+ WHERE type = 'run' AND state = 'pending';
428
+
429
+ CREATE INDEX steps_sleep_waiting_by_time_idx
430
+ ON steps(wake_at, id)
431
+ WHERE type = 'sleep' AND state = 'waiting';
432
+
433
+ CREATE INDEX steps_wait_waiting_by_event_idx
434
+ ON steps(event_name, id)
435
+ WHERE type = 'wait' AND state = 'waiting';
436
+
437
+ CREATE INDEX steps_wait_waiting_by_timeout_idx
438
+ ON steps(timeout_at, id)
439
+ WHERE type = 'wait' AND state = 'waiting' AND timeout_at IS NOT NULL;
440
+
441
+ CREATE INDEX steps_by_parent_step_id_idx
442
+ ON steps(parent_step_id, id)
443
+ WHERE parent_step_id IS NOT NULL;
444
+
445
+ CREATE INDEX step_events_by_step_and_time_idx
446
+ ON step_events(step_id, recorded_at, id);
447
+
448
+ CREATE INDEX workflow_events_by_time_idx
449
+ ON workflow_events(recorded_at, id);
450
+
451
+ CREATE INDEX inbound_events_by_name_and_time_idx
452
+ ON inbound_events(event_name, created_at, id);
453
+
454
+ CREATE INDEX inbound_events_by_claimed_by_idx
455
+ ON inbound_events(claimed_by, id)
456
+ WHERE claimed_by IS NOT NULL;
457
+
458
+ -- immutable identity fields
459
+ CREATE TRIGGER workflow_metadata_immutable_fields
460
+ BEFORE UPDATE ON workflow_metadata
461
+ FOR EACH ROW
462
+ WHEN NEW.id <> OLD.id OR NEW.created_at <> OLD.created_at
463
+ BEGIN
464
+ SELECT RAISE(ABORT, 'workflow_metadata.id and workflow_metadata.created_at are immutable');
465
+ END;
466
+
467
+ -- valid status transitions
468
+ CREATE TRIGGER workflow_metadata_valid_transition
469
+ BEFORE UPDATE ON workflow_metadata
470
+ FOR EACH ROW
471
+ WHEN NEW.status <> OLD.status
472
+ BEGIN
473
+ SELECT CASE
474
+ WHEN OLD.status = 'pending' AND NEW.status NOT IN ('running', 'cancelled') THEN
475
+ RAISE(ABORT, 'pending can only transition to running or cancelled')
476
+ WHEN OLD.status = 'running' AND NEW.status NOT IN ('paused', 'completed', 'failed', 'cancelled') THEN
477
+ RAISE(ABORT, 'running can only transition to paused, completed, failed, or cancelled')
478
+ WHEN OLD.status = 'paused' AND NEW.status NOT IN ('running', 'cancelled') THEN
479
+ RAISE(ABORT, 'paused can only transition to running or cancelled')
480
+ WHEN OLD.status IN ('completed', 'failed', 'cancelled') THEN
481
+ RAISE(ABORT, 'terminal status cannot transition')
482
+ END;
483
+ END;
484
+
485
+ CREATE TRIGGER steps_immutable_identity_fields
486
+ BEFORE UPDATE ON steps
487
+ FOR EACH ROW
488
+ WHEN NEW.id <> OLD.id OR NEW.type <> OLD.type OR NEW.created_at <> OLD.created_at
489
+ BEGIN
490
+ SELECT RAISE(ABORT, 'steps.id, steps.type, and steps.created_at are immutable');
491
+ END;
492
+
493
+ CREATE TRIGGER steps_parent_step_id_immutable
494
+ BEFORE UPDATE ON steps
495
+ FOR EACH ROW
496
+ WHEN NEW.parent_step_id IS NOT OLD.parent_step_id
497
+ BEGIN
498
+ SELECT RAISE(ABORT, 'steps.parent_step_id is immutable');
499
+ END;
500
+
501
+ CREATE TRIGGER steps_parent_must_be_run
502
+ BEFORE INSERT ON steps
503
+ FOR EACH ROW
504
+ WHEN NEW.parent_step_id IS NOT NULL
505
+ AND (SELECT type FROM steps WHERE id = NEW.parent_step_id) IS NOT 'run'
506
+ BEGIN
507
+ SELECT RAISE(ABORT, 'steps.parent_step_id must reference a run step');
508
+ END;
509
+
510
+ -- append-only step events
511
+ CREATE TRIGGER step_events_append_only_update
512
+ BEFORE UPDATE ON step_events
513
+ FOR EACH ROW
514
+ BEGIN
515
+ SELECT RAISE(ABORT, 'step_events is append-only');
516
+ END;
517
+
518
+ CREATE TRIGGER step_events_append_only_delete
519
+ BEFORE DELETE ON step_events
520
+ FOR EACH ROW
521
+ BEGIN
522
+ SELECT RAISE(ABORT, 'step_events is append-only');
523
+ END;
524
+
525
+ -- append-only workflow events
526
+ CREATE TRIGGER workflow_events_append_only_update
527
+ BEFORE UPDATE ON workflow_events
528
+ FOR EACH ROW
529
+ BEGIN
530
+ SELECT RAISE(ABORT, 'workflow_events is append-only');
531
+ END;
532
+
533
+ CREATE TRIGGER workflow_events_append_only_delete
534
+ BEFORE DELETE ON workflow_events
535
+ FOR EACH ROW
536
+ BEGIN
537
+ SELECT RAISE(ABORT, 'workflow_events is append-only');
538
+ END;
539
+
540
+ -- step_events.type must match the referenced row in steps.type
541
+ CREATE TRIGGER step_events_parent_type_match
542
+ BEFORE INSERT ON step_events
543
+ FOR EACH ROW
544
+ BEGIN
545
+ SELECT CASE
546
+ WHEN NOT EXISTS (SELECT 1 FROM steps WHERE id = NEW.step_id) THEN
547
+ RAISE(ABORT, 'step_events.step_id does not reference an existing steps row')
548
+ WHEN NEW.type IN ('attempt_started', 'attempt_succeeded', 'attempt_failed')
549
+ AND (SELECT type FROM steps WHERE id = NEW.step_id) <> 'run' THEN
550
+ RAISE(ABORT, 'run attempt events require a run step')
551
+ WHEN NEW.type IN ('sleep_waiting', 'sleep_elapsed')
552
+ AND (SELECT type FROM steps WHERE id = NEW.step_id) <> 'sleep' THEN
553
+ RAISE(ABORT, 'sleep events require a sleep step')
554
+ WHEN NEW.type IN ('wait_waiting', 'wait_satisfied', 'wait_timed_out')
555
+ AND (SELECT type FROM steps WHERE id = NEW.step_id) <> 'wait' THEN
556
+ RAISE(ABORT, 'wait events require a wait step')
557
+ END;
558
+ END;
559
+ `;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Minimal typings for `node:async_hooks` without @types/node.
3
+ */
4
+ declare module "node:async_hooks" {
5
+ export interface AsyncLocalStorageOptions {
6
+ defaultValue?: unknown;
7
+ name?: string | undefined;
8
+ }
9
+
10
+ export class AsyncLocalStorage<T> {
11
+ constructor(options?: AsyncLocalStorageOptions);
12
+ getStore(): T | undefined;
13
+ run<R>(store: T, callback: () => R): R;
14
+ run<R, TArgs extends unknown[]>(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R;
15
+ }
16
+ }