effect-mq 0.4.2 → 0.6.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.
Files changed (64) hide show
  1. package/README.md +85 -17
  2. package/dist/Flow.d.ts +381 -0
  3. package/dist/Flow.d.ts.map +1 -0
  4. package/dist/Flow.js +340 -0
  5. package/dist/Flow.js.map +1 -0
  6. package/dist/Job.d.ts +37 -6
  7. package/dist/Job.d.ts.map +1 -1
  8. package/dist/Job.js +17 -2
  9. package/dist/Job.js.map +1 -1
  10. package/dist/JobSchedules.d.ts +112 -0
  11. package/dist/JobSchedules.d.ts.map +1 -0
  12. package/dist/JobSchedules.js +106 -0
  13. package/dist/JobSchedules.js.map +1 -0
  14. package/dist/JobStore.d.ts +320 -10
  15. package/dist/JobStore.d.ts.map +1 -1
  16. package/dist/JobStore.js.map +1 -1
  17. package/dist/MemoryJobStore.d.ts.map +1 -1
  18. package/dist/MemoryJobStore.js +336 -8
  19. package/dist/MemoryJobStore.js.map +1 -1
  20. package/dist/Metrics.d.ts +31 -0
  21. package/dist/Metrics.d.ts.map +1 -1
  22. package/dist/Metrics.js +39 -0
  23. package/dist/Metrics.js.map +1 -1
  24. package/dist/Worker.d.ts +120 -11
  25. package/dist/Worker.d.ts.map +1 -1
  26. package/dist/Worker.js +452 -26
  27. package/dist/Worker.js.map +1 -1
  28. package/dist/drizzle-postgres/DrizzleJobStore.d.ts +19 -1
  29. package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -1
  30. package/dist/drizzle-postgres/DrizzleJobStore.js +662 -81
  31. package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -1
  32. package/dist/drizzle-postgres/schema.d.ts +310 -3
  33. package/dist/drizzle-postgres/schema.d.ts.map +1 -1
  34. package/dist/drizzle-postgres/schema.js +68 -1
  35. package/dist/drizzle-postgres/schema.js.map +1 -1
  36. package/dist/index.d.ts +14 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +14 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/redis/RedisJobStore.d.ts.map +1 -1
  41. package/dist/redis/RedisJobStore.js +221 -19
  42. package/dist/redis/RedisJobStore.js.map +1 -1
  43. package/dist/redis/scripts.d.ts +118 -11
  44. package/dist/redis/scripts.d.ts.map +1 -1
  45. package/dist/redis/scripts.js +497 -28
  46. package/dist/redis/scripts.js.map +1 -1
  47. package/dist/testing/conformance.d.ts +6 -0
  48. package/dist/testing/conformance.d.ts.map +1 -1
  49. package/dist/testing/conformance.js +765 -1
  50. package/dist/testing/conformance.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/Flow.ts +778 -0
  53. package/src/Job.ts +42 -11
  54. package/src/JobSchedules.ts +223 -0
  55. package/src/JobStore.ts +347 -9
  56. package/src/MemoryJobStore.ts +372 -8
  57. package/src/Metrics.ts +43 -0
  58. package/src/Worker.ts +726 -37
  59. package/src/drizzle-postgres/DrizzleJobStore.ts +827 -82
  60. package/src/drizzle-postgres/schema.ts +94 -0
  61. package/src/index.ts +16 -0
  62. package/src/redis/RedisJobStore.ts +291 -8
  63. package/src/redis/scripts.ts +529 -26
  64. package/src/testing/conformance.ts +989 -1
package/src/JobStore.ts CHANGED
@@ -67,6 +67,8 @@ export const ScheduleKey: Brand.Constructor<ScheduleKey> = Brand.nominal<Schedul
67
67
  * - `waiting`: runnable now, ordered by (priority desc, enqueue order asc)
68
68
  * - `delayed`: must not run before `runAt`
69
69
  * - `active`: claimed by a worker holding a lock token
70
+ * - `waiting-children`: a flow parent parked until its children settle (see
71
+ * `AckOutcome`'s `FanOut`); never claimable, not terminal
70
72
  * - `completed` / `failed`: terminal, with an encoded `Exit` stored
71
73
  *
72
74
  * @since 0.1.0
@@ -75,6 +77,7 @@ export type JobState =
75
77
  | "waiting"
76
78
  | "delayed"
77
79
  | "active"
80
+ | "waiting-children"
78
81
  | "completed"
79
82
  | "failed"
80
83
  | "cancelled"
@@ -205,8 +208,12 @@ export interface AttemptRecord {
205
208
  readonly startedAt: number | undefined
206
209
  /** Ack/recovery time of this run (epoch millis). */
207
210
  readonly finishedAt: number
208
- readonly outcome: "completed" | "retried" | "failed" | "stalled" | "cancelled"
209
- /** Schema-encoded `Exit`; undefined for `stalled`. */
211
+ readonly outcome: "completed" | "retried" | "failed" | "stalled" | "cancelled" | "fanned-out"
212
+ /**
213
+ * Schema-encoded `Exit`; undefined for `stalled`, `cancelled`, and
214
+ * `fanned-out` entries, and for `failed` entries written by a store-side
215
+ * settle (a fail-fast flow parent) rather than a handler run.
216
+ */
210
217
  readonly exit: unknown
211
218
  }
212
219
 
@@ -244,6 +251,10 @@ export interface JobRecord {
244
251
  readonly dedupeKey: string | undefined
245
252
  /** The producer's span context, restored as the handler span's parent. */
246
253
  readonly trace: TraceContext | undefined
254
+ /** Present on flow children: the link back to their parent flow. */
255
+ readonly parent: ParentEnvelope | undefined
256
+ /** Present on flow parents once their manifest landed (see `FlowState`). */
257
+ readonly flow: FlowState | undefined
247
258
  /** Epoch millis before which the job must not be claimed. */
248
259
  readonly runAt: number
249
260
  readonly enqueuedAt: number
@@ -304,6 +315,171 @@ export interface TraceContext {
304
315
  readonly delayed: boolean
305
316
  }
306
317
 
318
+ /**
319
+ * The persisted link from a flow child job to its parent flow. Attached by
320
+ * the flow runtime at fan-out (never by producers). Its presence puts the
321
+ * job under the outbox invariant: the child's store appends its report to
322
+ * the outbox with every terminal transition (see `OutboxEntry`), and a
323
+ * worker's relay delivers it into the parent's store (see
324
+ * `Worker.layer({ flows })`).
325
+ *
326
+ * @since 0.6.0
327
+ */
328
+ export interface ParentEnvelope {
329
+ /** The flow definition's name (`Flow.make(name, ...)`). */
330
+ readonly flowName: string
331
+ /** The parent job's id in the parent store. */
332
+ readonly flowId: JobId
333
+ /** This child's key, unique within the flow (the idempotency mechanism). */
334
+ readonly childKey: string
335
+ /** The parent store's context-key string, for cross-store report routing. */
336
+ readonly parentStoreKey: string
337
+ /**
338
+ * This child's nesting level: 1 for children of a top-level flow, one
339
+ * more per level of nesting. Carried explicitly (never parsed out of
340
+ * ids — user keys are arbitrary strings) so the fan-out depth cap can
341
+ * catch cyclic definitions.
342
+ */
343
+ readonly depth: number
344
+ }
345
+
346
+ /**
347
+ * Flow bookkeeping persisted on a parent job by the `FanOut` ack. Its
348
+ * presence IS the phase marker: absent means the parent has not fanned out
349
+ * yet (a claim dispatches `fanOut`); present means the manifest landed (a
350
+ * claim dispatches `collect`, and a re-run can never fan out twice).
351
+ *
352
+ * The four counters always sum to the manifest size: applied reports move
353
+ * one child from `pending` to its outcome bucket, and settle-time marking
354
+ * (fail-fast, parent cancel) moves every remaining `pending` child to
355
+ * `cancelled` in the same atomic op. `collect` reads its tallies from here
356
+ * without touching a single dependency row.
357
+ *
358
+ * @since 0.6.0
359
+ */
360
+ export interface FlowState {
361
+ /** When true, the first failed child report settles the parent as failed. */
362
+ readonly failFast: boolean
363
+ /** Children whose result has not been recorded yet. */
364
+ readonly pending: number
365
+ readonly completed: number
366
+ readonly failed: number
367
+ readonly cancelled: number
368
+ }
369
+
370
+ /**
371
+ * One child of a flow fan-out, persisted on its dependency row so the flow
372
+ * sweeper can (re-)enqueue the child from storage alone after any crash.
373
+ * `request.id` is the deterministic flow child id — derived from the parent
374
+ * store key, flow id, and child key, so re-enqueues are idempotent — and
375
+ * `request.parent` carries the envelope.
376
+ *
377
+ * @since 0.6.0
378
+ */
379
+ export interface FlowChildSpec {
380
+ readonly childKey: string
381
+ /** The CHILD store's context-key string (children may live elsewhere). */
382
+ readonly storeKey: string
383
+ readonly request: EnqueueRequest
384
+ }
385
+
386
+ /**
387
+ * A dependency row: one child's status and result as recorded in the PARENT
388
+ * store. `exit` is the child's schema-encoded exit; `failedReason` carries
389
+ * store-side child failures (stall exhaustion, a nested parent's fail-fast
390
+ * settle) that never produced an exit. `cascaded` marks that a cancel no
391
+ * longer needs to be delivered into the child's store (set by
392
+ * `markChildrenCascaded`, or immediately when the recorded outcome came
393
+ * FROM the child's store).
394
+ *
395
+ * @since 0.6.0
396
+ */
397
+ export interface FlowChildRecord {
398
+ readonly flowId: JobId
399
+ readonly childKey: string
400
+ readonly name: string
401
+ readonly storeKey: string
402
+ readonly childJobId: JobId
403
+ readonly status: "pending" | "completed" | "failed" | "cancelled"
404
+ readonly exit: unknown
405
+ readonly failedReason: string | undefined
406
+ readonly cascaded: boolean
407
+ }
408
+
409
+ /**
410
+ * An idempotent child-result report delivered into the parent store — by a
411
+ * worker's outbox relay (the push path) or synthesized by the flow sweeper
412
+ * from child-store state (the reconcile path). Both may deliver the same
413
+ * report; the dependency row's state dedups them.
414
+ *
415
+ * @since 0.6.0
416
+ */
417
+ export interface FlowChildReport {
418
+ readonly flowId: JobId
419
+ readonly childKey: string
420
+ readonly outcome: "completed" | "failed" | "cancelled"
421
+ /** The child's schema-encoded exit; undefined for store-side failures. */
422
+ readonly exit: unknown
423
+ /** Present when the child was failed store-side (no exit exists). */
424
+ readonly failedReason: string | undefined
425
+ }
426
+
427
+ /**
428
+ * One undelivered child-result report in a CHILD store's outbox.
429
+ *
430
+ * The outbox invariant every driver must uphold: whenever a store operation
431
+ * moves a job carrying a `parent` envelope INTO a terminal state — a
432
+ * `Complete`/`Fail`/`Cancelled` ack, stall exhaustion, a direct `cancel` of
433
+ * a waiting/delayed child, a cancel honoured during release or stall
434
+ * recovery, or a fail-fast settle of a NESTED flow parent (its terminal
435
+ * transition happens store-side, with no ack) — the same atomic operation
436
+ * appends the corresponding report here. The worker's relay then drains
437
+ * the outbox into the parent store in batches and deletes what it
438
+ * delivered. Because dependency rows dedup redelivery, the relay needs no
439
+ * leases: crash anywhere and the entries are simply delivered again.
440
+ *
441
+ * `remove` appends nothing (an operator override), and a `FanOut` ack
442
+ * appends nothing (`waiting-children` is not terminal).
443
+ *
444
+ * @since 0.6.0
445
+ */
446
+ export interface OutboxEntry {
447
+ /** Store-assigned, opaque; pass back to `deleteOutbox` verbatim. */
448
+ readonly id: string
449
+ readonly flowName: string
450
+ /** The parent store's context-key string, for relay routing. */
451
+ readonly parentStoreKey: string
452
+ readonly report: FlowChildReport
453
+ }
454
+
455
+ /**
456
+ * The flow sweeper's work list, scoped by parent state so settled flows
457
+ * never re-drive work:
458
+ *
459
+ * - `reconcile`: for parents still in `waiting-children`, dependency rows
460
+ * `pending` longer than the caller's threshold, with their stored specs —
461
+ * the sweeper checks the child's store and either enqueues the child
462
+ * (missing) or synthesizes its report (terminal).
463
+ * - `cascade`: rows marked `cancelled` by a settle but not yet delivered as
464
+ * real cancels into their child store (any parent state).
465
+ *
466
+ * @since 0.6.0
467
+ */
468
+ export interface FlowSweepWork {
469
+ readonly reconcile: ReadonlyArray<{
470
+ readonly flowId: JobId
471
+ readonly children: ReadonlyArray<FlowChildSpec>
472
+ }>
473
+ readonly cascade: ReadonlyArray<{
474
+ readonly flowId: JobId
475
+ readonly children: ReadonlyArray<{
476
+ readonly childKey: string
477
+ readonly storeKey: string
478
+ readonly childJobId: JobId
479
+ }>
480
+ }>
481
+ }
482
+
307
483
  /**
308
484
  * @since 0.1.0
309
485
  */
@@ -327,6 +503,8 @@ export interface EnqueueRequest {
327
503
  readonly dedupe: DedupePolicy | undefined
328
504
  /** The producer's span context, for cross-process trace propagation. */
329
505
  readonly trace: TraceContext | undefined
506
+ /** Flow-parent link, set only by the flow runtime (opaque to the store). */
507
+ readonly parent: ParentEnvelope | undefined
330
508
  readonly delayMs: number
331
509
  }
332
510
 
@@ -389,7 +567,18 @@ export type ClaimResult =
389
567
  * attempts accounting) is computed by the worker; the store only applies it.
390
568
  *
391
569
  * Every outcome appends an `AttemptRecord` to the job's ledger (`Complete` →
392
- * completed, `Retry` → retried, `Fail` → failed).
570
+ * completed, `Retry` → retried, `Fail` → failed, `FanOut` → fanned-out).
571
+ *
572
+ * `FanOut` (flow parents only) atomically: persists `FlowState` (phase
573
+ * marker + policy + `pending = children.length`), inserts one `pending`
574
+ * dependency row per child (spec stored, so crash recovery needs only the
575
+ * parent store), and parks the parent in `waiting-children` — or, for an
576
+ * empty spec, settles it straight to `waiting`. It does NOT consume an
577
+ * attempt (a phase transition is not a completed run), and it does NOT
578
+ * enqueue the children (the flow runtime and sweeper own that). A parent
579
+ * whose `flow` is already present keeps its persisted manifest untouched —
580
+ * the new children are ignored and the state transition follows the existing
581
+ * `pending` count, so a double fan-out cannot duplicate children.
393
582
  *
394
583
  * @since 0.1.0
395
584
  */
@@ -398,6 +587,11 @@ export type AckOutcome =
398
587
  | { readonly _tag: "Retry"; readonly delayMs: number; readonly exit: unknown }
399
588
  | { readonly _tag: "Fail"; readonly exit: unknown }
400
589
  | { readonly _tag: "Cancelled" }
590
+ | {
591
+ readonly _tag: "FanOut"
592
+ readonly failFast: boolean
593
+ readonly children: ReadonlyArray<FlowChildSpec>
594
+ }
401
595
 
402
596
  /**
403
597
  * Filters and pagination for `list`. Results are ordered newest-first
@@ -450,6 +644,12 @@ export interface ScheduleRecord {
450
644
  readonly backoff: BackoffPolicy | undefined
451
645
  readonly keep: KeepPolicy | undefined
452
646
  readonly timeoutMs: number | undefined
647
+ /**
648
+ * Ownership label for declarative reconciliation (`JobSchedules.layer`):
649
+ * a reconciler only ever prunes schedules carrying ITS group. Unlabeled
650
+ * schedules (plain `.schedule()` calls) are never pruned.
651
+ */
652
+ readonly group: string | undefined
453
653
  /** Epoch millis of the next occurrence to enqueue. */
454
654
  readonly nextRunAt: number
455
655
  }
@@ -657,7 +857,8 @@ export interface Service {
657
857
  * Acknowledge a claimed job. Verifies the lock token, releases the lock,
658
858
  * increments `attemptsMade`, appends to the attempts ledger, then applies
659
859
  * the outcome (`Complete`/`Fail` are terminal and apply the record's `keep`
660
- * policy; `Retry` re-queues after `delayMs`).
860
+ * policy; `Retry` re-queues after `delayMs`). Terminal outcomes for jobs
861
+ * carrying a `parent` envelope also append an `OutboxEntry` atomically.
661
862
  */
662
863
  readonly ack: (
663
864
  id: JobId,
@@ -687,7 +888,8 @@ export interface Service {
687
888
  * Sweep active jobs whose lock has expired. Each recovered job gets
688
889
  * `stalledCount + 1` and a `stalled` ledger entry; jobs exceeding
689
890
  * `maxStalledCount` are failed (`failed: true` in the result), the rest
690
- * return to `waiting`.
891
+ * return to `waiting`. Stall-exhausting a job that carries a `parent`
892
+ * envelope appends its failed report to the outbox atomically.
691
893
  */
692
894
  readonly recoverStalled: (options: {
693
895
  readonly maxStalledCount: number
@@ -724,7 +926,10 @@ export interface Service {
724
926
  /**
725
927
  * Re-run a failed job: back to `waiting` with a fresh attempt budget
726
928
  * (`attemptsMade`/`stalledCount` reset, terminal fields cleared). The
727
- * attempts ledger is preserved and keeps numbering monotonically.
929
+ * attempts ledger is preserved and keeps numbering monotonically. A flow
930
+ * parent's `flow` field and dependency rows survive: a retried fail-fast
931
+ * flow re-enters the `collect` phase with its recorded (mixed) results —
932
+ * it can never fan out twice.
728
933
  */
729
934
  readonly retry: (
730
935
  id: JobId
@@ -736,8 +941,13 @@ export interface Service {
736
941
  /**
737
942
  * Cancel a job. Waiting/delayed jobs become terminal (`cancelled`)
738
943
  * immediately; active jobs get `cancelRequested` set, and the owning
739
- * worker interrupts the handler on its next heartbeat. Terminal jobs fail
740
- * with `JobNotCancellableError`.
944
+ * worker interrupts the handler on its next heartbeat. A `waiting-children`
945
+ * flow parent settles to `cancelled` AND flips its remaining `pending`
946
+ * dependency rows to `cancelled` (not `cascaded`) in the same atomic op —
947
+ * the flow sweeper then delivers real cancels into the child stores, and
948
+ * late child reports find their row terminal and drop. Cancelling a job
949
+ * that itself carries a `parent` envelope appends its cancelled report to
950
+ * the outbox atomically. Terminal jobs fail with `JobNotCancellableError`.
741
951
  */
742
952
  readonly cancel: (
743
953
  id: JobId
@@ -796,6 +1006,8 @@ export interface Service {
796
1006
  readonly listSchedules: (options?: {
797
1007
  readonly jobName?: string | undefined
798
1008
  readonly queue?: QueueName | undefined
1009
+ /** Only schedules carrying this ownership group. */
1010
+ readonly group?: string | undefined
799
1011
  }) => Effect.Effect<ReadonlyArray<ScheduleRecord>, JobStoreError>
800
1012
 
801
1013
  /** Schedules whose `nextRunAt` is due (per the Effect `Clock`). */
@@ -830,11 +1042,137 @@ export interface Service {
830
1042
  nextRunAt: number
831
1043
  ) => Effect.Effect<void, JobStoreError>
832
1044
 
1045
+ /**
1046
+ * Record a batch of child outcomes on their dependency rows — idempotent
1047
+ * and atomic, results positional. Each report applies only when its row
1048
+ * is still `pending` (`applied: false` for duplicate, late, or unknown
1049
+ * reports); an applied report moves the child from the parent's `pending`
1050
+ * counter to its outcome counter and marks the row `cascaded` (the
1051
+ * outcome came from the child's store — no cancel needs delivering). A
1052
+ * batch may span flows.
1053
+ *
1054
+ * Per flow, all row updates apply BEFORE the settle decision, and the
1055
+ * parent settles at most once per batch (`parentSettled: true` on the
1056
+ * report that decided it): when `pending` hits zero, `waiting-children` →
1057
+ * `waiting` (runnable now, phase `collect`) — or on the first applied
1058
+ * `failed` report in batch order under the fail-fast policy, which
1059
+ * instead settles the parent terminally `failed` (store-side,
1060
+ * `failedReason` set, no exit — like stall exhaustion) and flips every
1061
+ * remaining `pending` row to `cancelled`/not-`cascaded` in the same
1062
+ * atomic op. Fail-fast wins the tie when one report triggers both rules.
1063
+ *
1064
+ * Lock ordering (drivers MUST follow it): dependency rows first, the
1065
+ * parent row second — reports, fail-fast marking, and cancel marking all
1066
+ * take locks in this order, so report-vs-settle cannot deadlock.
1067
+ *
1068
+ * Drivers may process very large batches in atomic sub-batches (the
1069
+ * Redis driver chunks at 500); the apply-all-before-settle rule then
1070
+ * holds per sub-batch. Worker relays never exceed one page (500), so
1071
+ * this only shows on direct store calls with larger batches.
1072
+ *
1073
+ * @since 0.6.0
1074
+ */
1075
+ readonly recordChildResults: (
1076
+ reports: ReadonlyArray<FlowChildReport>
1077
+ ) => Effect.Effect<
1078
+ ReadonlyArray<{ readonly applied: boolean; readonly parentSettled: boolean }>,
1079
+ JobStoreError
1080
+ >
1081
+
1082
+ /**
1083
+ * The oldest undelivered outbox entries, up to `limit` (see
1084
+ * `OutboxEntry` for the append invariant). The relay peeks, delivers via
1085
+ * `recordChildResults` on the parent store, then deletes — redelivery
1086
+ * after a crash is safe because dependency rows dedup.
1087
+ *
1088
+ * `after` pages past a previously-returned entry id (exclusive), whether
1089
+ * or not that entry still exists — the relay walks the whole outbox this
1090
+ * way, so entries it cannot route (their parent store is not provided
1091
+ * here) never blockade the ones behind them. Anything other than an id
1092
+ * this store issued may be treated as unset.
1093
+ *
1094
+ * @since 0.6.0
1095
+ */
1096
+ readonly peekOutbox: (options: {
1097
+ readonly limit: number
1098
+ readonly after?: string | undefined
1099
+ }) => Effect.Effect<ReadonlyArray<OutboxEntry>, JobStoreError>
1100
+
1101
+ /**
1102
+ * Delete delivered outbox entries by id. Idempotent; unknown ids are
1103
+ * ignored.
1104
+ *
1105
+ * @since 0.6.0
1106
+ */
1107
+ readonly deleteOutbox: (
1108
+ ids: ReadonlyArray<string>
1109
+ ) => Effect.Effect<void, JobStoreError>
1110
+
1111
+ /**
1112
+ * A flow's dependency rows, ordered by child key; feeds `collect` and
1113
+ * dashboards. Pass the returned `cursor` back for the next page.
1114
+ *
1115
+ * @since 0.6.0
1116
+ */
1117
+ readonly listChildResults: (
1118
+ flowId: JobId,
1119
+ options?: {
1120
+ readonly cursor?: string | undefined
1121
+ /** Page size; default 1000. */
1122
+ readonly limit?: number | undefined
1123
+ } | undefined
1124
+ ) => Effect.Effect<
1125
+ {
1126
+ readonly items: ReadonlyArray<FlowChildRecord>
1127
+ readonly cursor: string | undefined
1128
+ },
1129
+ JobStoreError
1130
+ >
1131
+
1132
+ /**
1133
+ * The flow sweeper's work list (see `FlowSweepWork`). `pendingAgeMs`
1134
+ * scopes reconciliation to rows whose eligibility timestamp is at least
1135
+ * this old (giving the push path time); `limit` bounds the rows returned
1136
+ * per class per sweep.
1137
+ *
1138
+ * Returning a row for reconciliation re-arms its eligibility timestamp
1139
+ * (it is not returned again until another `pendingAgeMs` elapses), so a
1140
+ * full page ROTATES across sweeps: healthy in-flight children and rows
1141
+ * this sweeper cannot act on never pin the head of the page and starve
1142
+ * the rows behind them.
1143
+ *
1144
+ * @since 0.6.0
1145
+ */
1146
+ readonly flowSweepWork: (options: {
1147
+ readonly pendingAgeMs: number
1148
+ readonly limit?: number | undefined
1149
+ }) => Effect.Effect<FlowSweepWork, JobStoreError>
1150
+
1151
+ /**
1152
+ * Mark dependency rows as `cascaded` after their cancels were delivered
1153
+ * into (or confirmed unnecessary by) the child's store. Idempotent.
1154
+ *
1155
+ * @since 0.6.0
1156
+ */
1157
+ readonly markChildrenCascaded: (
1158
+ flowId: JobId,
1159
+ childKeys: ReadonlyArray<string>
1160
+ ) => Effect.Effect<void, JobStoreError>
1161
+
833
1162
  readonly counts: (
834
1163
  queue?: QueueName
835
1164
  ) => Effect.Effect<Record<JobState, number>, JobStoreError>
836
1165
 
837
- /** Remove a job (and its ledger). Refuses (returns false) when active. */
1166
+ /**
1167
+ * Remove a job (and its ledger; a flow parent's dependency rows go with
1168
+ * it). Refuses (returns false) when active or `waiting-children`.
1169
+ *
1170
+ * Note the retention asymmetry for flows: AUTOMATIC pruning (`keep`
1171
+ * policies, the `historyTtl` sweep) must skip a settled flow parent whose
1172
+ * rows still owe cascade cancels (`cancelled` and not `cascaded`) — those
1173
+ * rows are the only record that real cancels are still due in the child
1174
+ * stores. `remove` is the explicit operator override and deletes anyway.
1175
+ */
838
1176
  readonly remove: (id: JobId) => Effect.Effect<boolean, JobStoreError>
839
1177
  }
840
1178