workerflow 0.1.0 → 0.2.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.
@@ -22,7 +22,7 @@ export default `
22
22
 
23
23
  CHECK (updated_at >= created_at),
24
24
 
25
- -- definition_version and definition_input must be set together or not set at all
25
+ -- definition_input must be NULL if definition_version is NULL
26
26
  CHECK (definition_version IS NOT NULL OR definition_input IS NULL),
27
27
 
28
28
  -- definition must be pinned before running/paused/completing/failing; cancelled is always allowed
@@ -32,348 +32,157 @@ export default `
32
32
  CREATE TABLE steps (
33
33
  id TEXT NOT NULL PRIMARY KEY CHECK (length(id) > 0),
34
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',
35
+ created_at INTEGER NOT NULL
36
+ DEFAULT (CAST(unixepoch('subsecond') * 1000 AS INTEGER))
37
+ CHECK (created_at >= 0),
38
+
39
+ -- sleep / wait only: run rows use run_step_attempts for lifecycle
40
+ state TEXT CHECK (state IN (
40
41
  'waiting',
41
42
  'elapsed',
42
43
  'satisfied',
43
44
  'timed_out'
44
45
  )),
45
- created_at INTEGER NOT NULL
46
- DEFAULT (CAST(unixepoch('subsecond') * 1000 AS INTEGER))
47
- CHECK (created_at >= 0),
48
46
 
49
- -- run-step fields
50
- attempt_count INTEGER,
51
47
  max_attempts INTEGER,
52
- next_attempt_at INTEGER,
53
- result TEXT,
54
- error_message TEXT,
55
- error_name TEXT,
56
48
 
57
- -- sleep-step fields
58
- wake_at INTEGER,
49
+ target_wake_at INTEGER,
59
50
 
60
- -- wait-step fields
61
51
  event_name TEXT,
62
52
  timeout_at INTEGER,
63
- payload TEXT,
64
53
 
65
- -- terminal timestamp
66
54
  resolved_at INTEGER,
67
55
 
68
- -- innermost enclosing run step when this row was created (nested run / sleep / wait under a run callback)
69
56
  parent_step_id TEXT REFERENCES steps(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
70
57
 
71
- CHECK (attempt_count IS NULL OR attempt_count >= 0),
72
58
  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),
59
+ CHECK (target_wake_at IS NULL OR target_wake_at >= 0),
75
60
  CHECK (timeout_at IS NULL OR timeout_at >= 0),
76
61
  CHECK (resolved_at IS NULL OR resolved_at >= created_at),
77
- CHECK (error_name IS NULL OR length(error_name) > 0),
78
62
  CHECK (event_name IS NULL OR length(event_name) > 0),
79
63
 
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
64
  CHECK (
88
65
  (
89
66
  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
67
+ state IS NULL AND
109
68
  (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
69
+ target_wake_at IS NULL AND
116
70
  event_name IS NULL AND
117
71
  timeout_at IS NULL AND
118
- payload IS NULL AND
119
72
  resolved_at IS NULL
120
73
  )
121
74
  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
75
  (
156
76
  type = 'sleep' AND
157
77
  state = 'waiting' AND
158
- attempt_count IS NULL AND
159
78
  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
79
+ target_wake_at IS NOT NULL AND
165
80
  event_name IS NULL AND
166
81
  timeout_at IS NULL AND
167
- payload IS NULL AND
168
82
  resolved_at IS NULL
169
83
  )
170
84
  OR
171
85
  (
172
86
  type = 'sleep' AND
173
87
  state = 'elapsed' AND
174
- attempt_count IS NULL AND
175
88
  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
89
+ target_wake_at IS NOT NULL AND
181
90
  event_name IS NULL AND
182
91
  timeout_at IS NULL AND
183
- payload IS NULL AND
184
92
  resolved_at IS NOT NULL
185
93
  )
186
94
  OR
187
95
  (
188
96
  type = 'wait' AND
189
97
  state = 'waiting' AND
190
- attempt_count IS NULL AND
191
98
  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
99
+ target_wake_at IS NULL AND
197
100
  event_name IS NOT NULL AND
198
- payload IS NULL AND
199
101
  resolved_at IS NULL
200
102
  )
201
103
  OR
202
104
  (
203
105
  type = 'wait' AND
204
106
  state = 'satisfied' AND
205
- attempt_count IS NULL AND
206
107
  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
108
+ target_wake_at IS NULL AND
212
109
  event_name IS NOT NULL AND
213
- timeout_at IS NULL AND
214
- payload IS NOT NULL AND
215
110
  resolved_at IS NOT NULL
216
111
  )
217
112
  OR
218
113
  (
219
114
  type = 'wait' AND
220
115
  state = 'timed_out' AND
221
- attempt_count IS NULL AND
222
116
  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
117
+ target_wake_at IS NULL AND
228
118
  event_name IS NOT NULL AND
229
- timeout_at IS NULL AND
230
- payload IS NULL AND
119
+ timeout_at IS NOT NULL AND
231
120
  resolved_at IS NOT NULL
232
121
  )
233
122
  ),
234
123
  CHECK (parent_step_id IS NULL OR parent_step_id <> id)
235
124
  ) STRICT;
236
125
 
237
- CREATE TABLE step_events (
126
+ CREATE TABLE run_step_attempts (
238
127
  id TEXT NOT NULL PRIMARY KEY
239
128
  DEFAULT (lower(hex(randomblob(16))))
240
129
  CHECK (length(id) > 0),
241
130
 
242
- step_id TEXT NOT NULL,
243
- recorded_at INTEGER NOT NULL
131
+ step_id TEXT NOT NULL REFERENCES steps(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
132
+
133
+ started_at INTEGER NOT NULL
244
134
  DEFAULT (CAST(unixepoch('subsecond') * 1000 AS INTEGER))
245
- CHECK (recorded_at >= 0),
135
+ CHECK (started_at >= 0),
246
136
 
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
- )),
137
+ state TEXT NOT NULL CHECK (state IN ('started', 'succeeded', 'failed')),
138
+
139
+ ended_at INTEGER CHECK (ended_at IS NULL OR ended_at >= started_at),
140
+
141
+ -- Discriminator for the shape of a succeeded result.
142
+ -- 'json' → result_json holds the raw JSON value (never NULL)
143
+ -- 'none' → callback returned undefined/void, result_json IS NULL
144
+ result_type TEXT CHECK (result_type IN ('json', 'none')),
145
+
146
+ -- Raw JSON value. No wrapper objects.
147
+ -- NULL when result_type is 'none', or when the attempt hasn't succeeded yet.
148
+ result_json TEXT CHECK (result_json IS NULL OR json_valid(result_json)),
257
149
 
258
- attempt_number INTEGER,
259
- result TEXT,
260
150
  error_message TEXT,
261
151
  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,
152
+ next_attempt_at INTEGER CHECK (next_attempt_at IS NULL OR next_attempt_at >= 0),
269
153
 
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
154
  CHECK (error_name IS NULL OR length(error_name) > 0),
275
- CHECK (event_name IS NULL OR length(event_name) > 0),
276
155
 
277
156
  CHECK (
278
157
  (
279
- type = 'attempt_started' AND
280
- attempt_number IS NOT NULL AND
281
- result IS NULL AND
158
+ state = 'started' AND
159
+ ended_at IS NULL AND
160
+ result_type IS NULL AND
161
+ result_json IS NULL AND
282
162
  error_message IS NULL AND
283
163
  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
164
+ next_attempt_at IS NULL
289
165
  )
290
166
  OR
291
167
  (
292
- type = 'attempt_succeeded' AND
293
- attempt_number IS NOT NULL AND
294
- result IS NOT NULL AND
168
+ state = 'succeeded' AND
169
+ ended_at IS NOT NULL AND
170
+ result_type IS NOT NULL AND
171
+ (
172
+ (result_type = 'none' AND result_json IS NULL) OR
173
+ (result_type = 'json' AND result_json IS NOT NULL)
174
+ ) AND
295
175
  error_message IS NULL AND
296
176
  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
177
+ next_attempt_at IS NULL
302
178
  )
303
179
  OR
304
180
  (
305
- type = 'attempt_failed' AND
306
- attempt_number IS NOT NULL AND
307
- result IS NULL AND
181
+ state = 'failed' AND
182
+ ended_at IS NOT NULL AND
308
183
  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
184
+ result_type IS NULL AND
185
+ result_json IS NULL
377
186
  )
378
187
  )
379
188
  ) STRICT;
@@ -405,6 +214,8 @@ export default `
405
214
  DEFAULT (lower(hex(randomblob(16))))
406
215
  CHECK (length(id) > 0),
407
216
  event_name TEXT NOT NULL CHECK (length(event_name) > 0),
217
+ -- Raw JSON value, or SQL NULL when no payload was provided (undefined).
218
+ -- JSON null is stored as the TEXT literal 'null', distinct from SQL NULL.
408
219
  payload TEXT CHECK (payload IS NULL OR json_valid(payload)),
409
220
  created_at INTEGER NOT NULL
410
221
  DEFAULT (CAST(unixepoch('subsecond') * 1000 AS INTEGER))
@@ -414,20 +225,21 @@ export default `
414
225
 
415
226
  FOREIGN KEY (claimed_by) REFERENCES steps(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
416
227
 
417
- -- claimed_by and claimed_at must be set together or not set at all
418
228
  CHECK (
419
229
  (claimed_by IS NULL AND claimed_at IS NULL) OR
420
230
  (claimed_by IS NOT NULL AND claimed_at IS NOT NULL)
421
231
  )
422
232
  ) STRICT;
423
233
 
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';
234
+ CREATE INDEX run_step_attempts_by_step_time_idx
235
+ ON run_step_attempts(step_id, started_at, id);
236
+
237
+ CREATE INDEX run_step_attempts_started_one_idx
238
+ ON run_step_attempts(step_id)
239
+ WHERE state = 'started';
428
240
 
429
241
  CREATE INDEX steps_sleep_waiting_by_time_idx
430
- ON steps(wake_at, id)
242
+ ON steps(target_wake_at, id)
431
243
  WHERE type = 'sleep' AND state = 'waiting';
432
244
 
433
245
  CREATE INDEX steps_wait_waiting_by_event_idx
@@ -442,20 +254,16 @@ export default `
442
254
  ON steps(parent_step_id, id)
443
255
  WHERE parent_step_id IS NOT NULL;
444
256
 
445
- CREATE INDEX step_events_by_step_and_time_idx
446
- ON step_events(step_id, recorded_at, id);
447
-
448
257
  CREATE INDEX workflow_events_by_time_idx
449
258
  ON workflow_events(recorded_at, id);
450
259
 
451
260
  CREATE INDEX inbound_events_by_name_and_time_idx
452
261
  ON inbound_events(event_name, created_at, id);
453
262
 
454
- CREATE INDEX inbound_events_by_claimed_by_idx
455
- ON inbound_events(claimed_by, id)
263
+ CREATE UNIQUE INDEX inbound_events_claimed_by_unique
264
+ ON inbound_events(claimed_by)
456
265
  WHERE claimed_by IS NOT NULL;
457
266
 
458
- -- immutable identity fields
459
267
  CREATE TRIGGER workflow_metadata_immutable_fields
460
268
  BEFORE UPDATE ON workflow_metadata
461
269
  FOR EACH ROW
@@ -464,7 +272,6 @@ export default `
464
272
  SELECT RAISE(ABORT, 'workflow_metadata.id and workflow_metadata.created_at are immutable');
465
273
  END;
466
274
 
467
- -- valid status transitions
468
275
  CREATE TRIGGER workflow_metadata_valid_transition
469
276
  BEFORE UPDATE ON workflow_metadata
470
277
  FOR EACH ROW
@@ -507,22 +314,34 @@ export default `
507
314
  SELECT RAISE(ABORT, 'steps.parent_step_id must reference a run step');
508
315
  END;
509
316
 
510
- -- append-only step events
511
- CREATE TRIGGER step_events_append_only_update
512
- BEFORE UPDATE ON step_events
513
- FOR EACH ROW
317
+ CREATE TRIGGER run_step_attempts_step_must_be_run
318
+ BEFORE INSERT ON run_step_attempts
319
+ WHEN (SELECT type FROM steps WHERE id = NEW.step_id) IS NOT 'run'
320
+ BEGIN
321
+ SELECT RAISE(ABORT, 'run_step_attempts.step_id must reference a run step');
322
+ END;
323
+
324
+ CREATE TRIGGER run_step_attempts_at_most_one_started_ins
325
+ BEFORE INSERT ON run_step_attempts
326
+ WHEN NEW.state = 'started'
327
+ AND EXISTS (SELECT 1 FROM run_step_attempts WHERE step_id = NEW.step_id AND state = 'started')
514
328
  BEGIN
515
- SELECT RAISE(ABORT, 'step_events is append-only');
329
+ SELECT RAISE(ABORT, 'run step already has an in-flight attempt');
516
330
  END;
517
331
 
518
- CREATE TRIGGER step_events_append_only_delete
519
- BEFORE DELETE ON step_events
332
+ CREATE TRIGGER run_step_attempts_valid_transition
333
+ BEFORE UPDATE ON run_step_attempts
520
334
  FOR EACH ROW
335
+ WHEN NEW.state <> OLD.state
521
336
  BEGIN
522
- SELECT RAISE(ABORT, 'step_events is append-only');
337
+ SELECT CASE
338
+ WHEN OLD.state = 'started' AND NEW.state NOT IN ('succeeded', 'failed') THEN
339
+ RAISE(ABORT, 'started can only transition to succeeded or failed')
340
+ WHEN OLD.state IN ('succeeded', 'failed') THEN
341
+ RAISE(ABORT, 'terminal attempt state cannot transition')
342
+ END;
523
343
  END;
524
344
 
525
- -- append-only workflow events
526
345
  CREATE TRIGGER workflow_events_append_only_update
527
346
  BEFORE UPDATE ON workflow_events
528
347
  FOR EACH ROW
@@ -536,24 +355,4 @@ export default `
536
355
  BEGIN
537
356
  SELECT RAISE(ABORT, 'workflow_events is append-only');
538
357
  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
358
  `;