effect-mq 0.5.0 → 0.7.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.
- package/README.md +85 -17
- package/dist/Flow.d.ts +381 -0
- package/dist/Flow.d.ts.map +1 -0
- package/dist/Flow.js +340 -0
- package/dist/Flow.js.map +1 -0
- package/dist/Job.d.ts +31 -6
- package/dist/Job.d.ts.map +1 -1
- package/dist/Job.js +16 -2
- package/dist/Job.js.map +1 -1
- package/dist/JobStore.d.ts +353 -13
- package/dist/JobStore.d.ts.map +1 -1
- package/dist/JobStore.js +10 -0
- package/dist/JobStore.js.map +1 -1
- package/dist/MemoryJobStore.d.ts.map +1 -1
- package/dist/MemoryJobStore.js +361 -21
- package/dist/MemoryJobStore.js.map +1 -1
- package/dist/Metrics.d.ts +31 -0
- package/dist/Metrics.d.ts.map +1 -1
- package/dist/Metrics.js +39 -0
- package/dist/Metrics.js.map +1 -1
- package/dist/Worker.d.ts +120 -11
- package/dist/Worker.d.ts.map +1 -1
- package/dist/Worker.js +452 -26
- package/dist/Worker.js.map +1 -1
- package/dist/drizzle-postgres/DrizzleJobStore.d.ts +19 -1
- package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -1
- package/dist/drizzle-postgres/DrizzleJobStore.js +678 -80
- package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -1
- package/dist/drizzle-postgres/schema.d.ts +293 -3
- package/dist/drizzle-postgres/schema.d.ts.map +1 -1
- package/dist/drizzle-postgres/schema.js +66 -1
- package/dist/drizzle-postgres/schema.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/redis/RedisJobStore.d.ts +53 -0
- package/dist/redis/RedisJobStore.d.ts.map +1 -1
- package/dist/redis/RedisJobStore.js +402 -50
- package/dist/redis/RedisJobStore.js.map +1 -1
- package/dist/redis/scripts.d.ts +213 -35
- package/dist/redis/scripts.d.ts.map +1 -1
- package/dist/redis/scripts.js +689 -73
- package/dist/redis/scripts.js.map +1 -1
- package/dist/testing/conformance.d.ts +6 -0
- package/dist/testing/conformance.d.ts.map +1 -1
- package/dist/testing/conformance.js +855 -12
- package/dist/testing/conformance.js.map +1 -1
- package/package.json +1 -1
- package/src/Flow.ts +778 -0
- package/src/Job.ts +35 -11
- package/src/JobStore.ts +377 -12
- package/src/MemoryJobStore.ts +396 -25
- package/src/Metrics.ts +43 -0
- package/src/Worker.ts +726 -37
- package/src/drizzle-postgres/DrizzleJobStore.ts +844 -81
- package/src/drizzle-postgres/schema.ts +92 -0
- package/src/index.ts +8 -0
- package/src/redis/RedisJobStore.ts +540 -39
- package/src/redis/scripts.ts +751 -78
- package/src/testing/conformance.ts +1088 -12
package/src/Job.ts
CHANGED
|
@@ -63,7 +63,7 @@ export {
|
|
|
63
63
|
*/
|
|
64
64
|
unrecoverable
|
|
65
65
|
}
|
|
66
|
-
import { type
|
|
66
|
+
import { type CurrentJob, type RegisterOptions, Worker } from "./Worker.ts"
|
|
67
67
|
|
|
68
68
|
const TypeId = "~effect-mq/Job" as const
|
|
69
69
|
|
|
@@ -308,7 +308,12 @@ export type EnqueueManyOptions = Omit<JobOptions, "delay"> & RunTimeInput & {
|
|
|
308
308
|
readonly metadata?: Readonly<Record<string, string>> | undefined
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
|
|
311
|
+
/**
|
|
312
|
+
* A definition's `defaults`, normalized to store units.
|
|
313
|
+
*
|
|
314
|
+
* @since 0.6.0
|
|
315
|
+
*/
|
|
316
|
+
export interface ResolvedDefaults {
|
|
312
317
|
readonly delayMs: number
|
|
313
318
|
readonly priority: number
|
|
314
319
|
readonly attempts: number
|
|
@@ -384,8 +389,12 @@ export interface JobAttempt<A, E> {
|
|
|
384
389
|
readonly attempt: number
|
|
385
390
|
readonly startedAt: number | undefined
|
|
386
391
|
readonly finishedAt: number
|
|
387
|
-
readonly outcome: "completed" | "retried" | "failed" | "stalled" | "cancelled"
|
|
388
|
-
/**
|
|
392
|
+
readonly outcome: "completed" | "retried" | "failed" | "stalled" | "cancelled" | "fanned-out"
|
|
393
|
+
/**
|
|
394
|
+
* Absent for `stalled`, `cancelled`, and `fanned-out` entries — and for
|
|
395
|
+
* `failed` ones settled store-side without a handler exit (e.g. a
|
|
396
|
+
* fail-fast flow settle).
|
|
397
|
+
*/
|
|
389
398
|
readonly exit: Option.Option<Exit.Exit<A, E>>
|
|
390
399
|
}
|
|
391
400
|
|
|
@@ -554,19 +563,20 @@ export interface Job<
|
|
|
554
563
|
|
|
555
564
|
/**
|
|
556
565
|
* Attach the handler that processes this job, as a layer to provide on top
|
|
557
|
-
* of `Worker.layer` (bound to the same store).
|
|
566
|
+
* of `Worker.layer` (bound to the same store). The handler reads the
|
|
567
|
+
* running attempt from the `Worker.CurrentJob` service — the worker
|
|
568
|
+
* provides it per run, so it never appears in the layer's requirements.
|
|
558
569
|
*/
|
|
559
570
|
readonly toLayer: <R>(
|
|
560
571
|
handler: (
|
|
561
|
-
payload: Payload["Type"]
|
|
562
|
-
context: JobContext
|
|
572
|
+
payload: Payload["Type"]
|
|
563
573
|
) => Effect.Effect<Success["Type"], Error["Type"], R>,
|
|
564
574
|
options?: RegisterOptions | undefined
|
|
565
575
|
) => Layer.Layer<
|
|
566
576
|
never,
|
|
567
577
|
never,
|
|
568
578
|
| Worker
|
|
569
|
-
| R
|
|
579
|
+
| Exclude<R, CurrentJob>
|
|
570
580
|
| Payload["DecodingServices"]
|
|
571
581
|
| Success["EncodingServices"]
|
|
572
582
|
| Error["EncodingServices"]
|
|
@@ -589,7 +599,13 @@ const defaultPollSchedule = Schedule.min([
|
|
|
589
599
|
Schedule.spaced("1 second")
|
|
590
600
|
])
|
|
591
601
|
|
|
592
|
-
|
|
602
|
+
/**
|
|
603
|
+
* Normalize a user-facing `BackoffInput` to the persisted policy. Shared
|
|
604
|
+
* with the flow runtime's child-spec builder.
|
|
605
|
+
*
|
|
606
|
+
* @internal
|
|
607
|
+
*/
|
|
608
|
+
export const normalizeBackoff = (input: BackoffInput | undefined): BackoffPolicy | undefined =>
|
|
593
609
|
input === undefined ? undefined : {
|
|
594
610
|
_tag: input.type,
|
|
595
611
|
delayMs: Duration.toMillis(input.delay),
|
|
@@ -601,7 +617,13 @@ const normalizeKeepState = (input: KeepStateInput): KeepStatePolicy => ({
|
|
|
601
617
|
ageMs: input.age !== undefined ? Duration.toMillis(input.age) : undefined
|
|
602
618
|
})
|
|
603
619
|
|
|
604
|
-
|
|
620
|
+
/**
|
|
621
|
+
* Normalize a user-facing `KeepInput` to the persisted policy. Shared with
|
|
622
|
+
* the flow runtime's child-spec builder.
|
|
623
|
+
*
|
|
624
|
+
* @internal
|
|
625
|
+
*/
|
|
626
|
+
export const normalizeKeep = (input: KeepInput | undefined): KeepPolicy | undefined => {
|
|
605
627
|
if (input === undefined) return undefined
|
|
606
628
|
const split = "completed" in input || "failed" in input || "cancelled" in input
|
|
607
629
|
const flat = "count" in input || "age" in input
|
|
@@ -697,6 +719,7 @@ const Proto = {
|
|
|
697
719
|
trace: capturedSpan === undefined
|
|
698
720
|
? undefined
|
|
699
721
|
: { ...capturedSpan, delayed: resolvedDelayMs > 0 } satisfies TraceContext,
|
|
722
|
+
parent: undefined,
|
|
700
723
|
delayMs: resolvedDelayMs
|
|
701
724
|
}))
|
|
702
725
|
),
|
|
@@ -779,6 +802,7 @@ const Proto = {
|
|
|
779
802
|
trace: capturedSpan === undefined
|
|
780
803
|
? undefined
|
|
781
804
|
: { ...capturedSpan, delayed: resolvedDelayMs > 0 } satisfies TraceContext,
|
|
805
|
+
parent: undefined,
|
|
782
806
|
delayMs: resolvedDelayMs
|
|
783
807
|
})))
|
|
784
808
|
)
|
|
@@ -1010,7 +1034,7 @@ const Proto = {
|
|
|
1010
1034
|
|
|
1011
1035
|
toLayer(
|
|
1012
1036
|
this: AnyWithProps,
|
|
1013
|
-
handler: (payload: any
|
|
1037
|
+
handler: (payload: any) => Effect.Effect<any, any, any>,
|
|
1014
1038
|
options?: RegisterOptions
|
|
1015
1039
|
) {
|
|
1016
1040
|
return Layer.effectDiscard(
|
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
|
-
/**
|
|
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,11 +587,25 @@ 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
|
-
* Filters and pagination for `list`.
|
|
404
|
-
* (`enqueuedAt` desc, then id desc); pass the returned
|
|
405
|
-
* the next
|
|
597
|
+
* Filters, ordering, and pagination for `list`. The default order is
|
|
598
|
+
* newest-first (`enqueuedAt` desc, then id desc); pass the returned
|
|
599
|
+
* `cursor` back — with the SAME options that produced it — to get the next
|
|
600
|
+
* page.
|
|
601
|
+
*
|
|
602
|
+
* Ordering is not free on every storage layout, so the contract defines a
|
|
603
|
+
* REQUIRED (filter, orderBy) surface every driver serves
|
|
604
|
+
* (conformance-pinned): `enqueuedAt` with any filters; `runAt` for
|
|
605
|
+
* `states: ["delayed"]` within a `queue`; `finishedAt` for terminal
|
|
606
|
+
* `states`, with or without `name`/`queue`. Beyond that surface a driver
|
|
607
|
+
* either serves the query or dies with `ListOrderUnsupportedError` — never
|
|
608
|
+
* a silent full scan. Memory and Postgres serve every combination.
|
|
406
609
|
*
|
|
407
610
|
* @since 0.1.0
|
|
408
611
|
*/
|
|
@@ -412,11 +615,37 @@ export interface ListOptions {
|
|
|
412
615
|
readonly states?: ReadonlyArray<JobState> | undefined
|
|
413
616
|
/** Every entry must match the record's metadata exactly (AND semantics). */
|
|
414
617
|
readonly metadata?: Readonly<Record<string, string>> | undefined
|
|
618
|
+
/**
|
|
619
|
+
* The field to order by (default `enqueuedAt`). Jobs missing the field
|
|
620
|
+
* (`finishedAt` on non-terminal rows) sort as 0.
|
|
621
|
+
*
|
|
622
|
+
* @since 0.7.0
|
|
623
|
+
*/
|
|
624
|
+
readonly orderBy?: "enqueuedAt" | "runAt" | "finishedAt" | undefined
|
|
625
|
+
/**
|
|
626
|
+
* Direction (default `desc`). The id tiebreak follows the direction.
|
|
627
|
+
*
|
|
628
|
+
* @since 0.7.0
|
|
629
|
+
*/
|
|
630
|
+
readonly order?: "asc" | "desc" | undefined
|
|
415
631
|
readonly cursor?: string | undefined
|
|
416
632
|
/** Page size; default 50. */
|
|
417
633
|
readonly limit?: number | undefined
|
|
418
634
|
}
|
|
419
635
|
|
|
636
|
+
/**
|
|
637
|
+
* A driver received a (filter, `orderBy`) combination outside the surface
|
|
638
|
+
* it can serve without a full scan (see `ListOptions`). Delivered as a
|
|
639
|
+
* defect: the query contradicts the driver's documented matrix, which is a
|
|
640
|
+
* programming mistake, not a runtime condition to recover from.
|
|
641
|
+
*
|
|
642
|
+
* @since 0.7.0
|
|
643
|
+
*/
|
|
644
|
+
export class ListOrderUnsupportedError extends Data.TaggedError("ListOrderUnsupportedError")<{
|
|
645
|
+
readonly orderBy: string
|
|
646
|
+
readonly message: string
|
|
647
|
+
}> {}
|
|
648
|
+
|
|
420
649
|
/**
|
|
421
650
|
* @since 0.1.0
|
|
422
651
|
*/
|
|
@@ -663,7 +892,8 @@ export interface Service {
|
|
|
663
892
|
* Acknowledge a claimed job. Verifies the lock token, releases the lock,
|
|
664
893
|
* increments `attemptsMade`, appends to the attempts ledger, then applies
|
|
665
894
|
* the outcome (`Complete`/`Fail` are terminal and apply the record's `keep`
|
|
666
|
-
* policy; `Retry` re-queues after `delayMs`).
|
|
895
|
+
* policy; `Retry` re-queues after `delayMs`). Terminal outcomes for jobs
|
|
896
|
+
* carrying a `parent` envelope also append an `OutboxEntry` atomically.
|
|
667
897
|
*/
|
|
668
898
|
readonly ack: (
|
|
669
899
|
id: JobId,
|
|
@@ -693,7 +923,8 @@ export interface Service {
|
|
|
693
923
|
* Sweep active jobs whose lock has expired. Each recovered job gets
|
|
694
924
|
* `stalledCount + 1` and a `stalled` ledger entry; jobs exceeding
|
|
695
925
|
* `maxStalledCount` are failed (`failed: true` in the result), the rest
|
|
696
|
-
* return to `waiting`.
|
|
926
|
+
* return to `waiting`. Stall-exhausting a job that carries a `parent`
|
|
927
|
+
* envelope appends its failed report to the outbox atomically.
|
|
697
928
|
*/
|
|
698
929
|
readonly recoverStalled: (options: {
|
|
699
930
|
readonly maxStalledCount: number
|
|
@@ -730,7 +961,10 @@ export interface Service {
|
|
|
730
961
|
/**
|
|
731
962
|
* Re-run a failed job: back to `waiting` with a fresh attempt budget
|
|
732
963
|
* (`attemptsMade`/`stalledCount` reset, terminal fields cleared). The
|
|
733
|
-
* attempts ledger is preserved and keeps numbering monotonically.
|
|
964
|
+
* attempts ledger is preserved and keeps numbering monotonically. A flow
|
|
965
|
+
* parent's `flow` field and dependency rows survive: a retried fail-fast
|
|
966
|
+
* flow re-enters the `collect` phase with its recorded (mixed) results —
|
|
967
|
+
* it can never fan out twice.
|
|
734
968
|
*/
|
|
735
969
|
readonly retry: (
|
|
736
970
|
id: JobId
|
|
@@ -742,8 +976,13 @@ export interface Service {
|
|
|
742
976
|
/**
|
|
743
977
|
* Cancel a job. Waiting/delayed jobs become terminal (`cancelled`)
|
|
744
978
|
* immediately; active jobs get `cancelRequested` set, and the owning
|
|
745
|
-
* worker interrupts the handler on its next heartbeat.
|
|
746
|
-
*
|
|
979
|
+
* worker interrupts the handler on its next heartbeat. A `waiting-children`
|
|
980
|
+
* flow parent settles to `cancelled` AND flips its remaining `pending`
|
|
981
|
+
* dependency rows to `cancelled` (not `cascaded`) in the same atomic op —
|
|
982
|
+
* the flow sweeper then delivers real cancels into the child stores, and
|
|
983
|
+
* late child reports find their row terminal and drop. Cancelling a job
|
|
984
|
+
* that itself carries a `parent` envelope appends its cancelled report to
|
|
985
|
+
* the outbox atomically. Terminal jobs fail with `JobNotCancellableError`.
|
|
747
986
|
*/
|
|
748
987
|
readonly cancel: (
|
|
749
988
|
id: JobId
|
|
@@ -838,11 +1077,137 @@ export interface Service {
|
|
|
838
1077
|
nextRunAt: number
|
|
839
1078
|
) => Effect.Effect<void, JobStoreError>
|
|
840
1079
|
|
|
1080
|
+
/**
|
|
1081
|
+
* Record a batch of child outcomes on their dependency rows — idempotent
|
|
1082
|
+
* and atomic, results positional. Each report applies only when its row
|
|
1083
|
+
* is still `pending` (`applied: false` for duplicate, late, or unknown
|
|
1084
|
+
* reports); an applied report moves the child from the parent's `pending`
|
|
1085
|
+
* counter to its outcome counter and marks the row `cascaded` (the
|
|
1086
|
+
* outcome came from the child's store — no cancel needs delivering). A
|
|
1087
|
+
* batch may span flows.
|
|
1088
|
+
*
|
|
1089
|
+
* Per flow, all row updates apply BEFORE the settle decision, and the
|
|
1090
|
+
* parent settles at most once per batch (`parentSettled: true` on the
|
|
1091
|
+
* report that decided it): when `pending` hits zero, `waiting-children` →
|
|
1092
|
+
* `waiting` (runnable now, phase `collect`) — or on the first applied
|
|
1093
|
+
* `failed` report in batch order under the fail-fast policy, which
|
|
1094
|
+
* instead settles the parent terminally `failed` (store-side,
|
|
1095
|
+
* `failedReason` set, no exit — like stall exhaustion) and flips every
|
|
1096
|
+
* remaining `pending` row to `cancelled`/not-`cascaded` in the same
|
|
1097
|
+
* atomic op. Fail-fast wins the tie when one report triggers both rules.
|
|
1098
|
+
*
|
|
1099
|
+
* Lock ordering (drivers MUST follow it): dependency rows first, the
|
|
1100
|
+
* parent row second — reports, fail-fast marking, and cancel marking all
|
|
1101
|
+
* take locks in this order, so report-vs-settle cannot deadlock.
|
|
1102
|
+
*
|
|
1103
|
+
* Drivers may process very large batches in atomic sub-batches (the
|
|
1104
|
+
* Redis driver chunks at 500); the apply-all-before-settle rule then
|
|
1105
|
+
* holds per sub-batch. Worker relays never exceed one page (500), so
|
|
1106
|
+
* this only shows on direct store calls with larger batches.
|
|
1107
|
+
*
|
|
1108
|
+
* @since 0.6.0
|
|
1109
|
+
*/
|
|
1110
|
+
readonly recordChildResults: (
|
|
1111
|
+
reports: ReadonlyArray<FlowChildReport>
|
|
1112
|
+
) => Effect.Effect<
|
|
1113
|
+
ReadonlyArray<{ readonly applied: boolean; readonly parentSettled: boolean }>,
|
|
1114
|
+
JobStoreError
|
|
1115
|
+
>
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* The oldest undelivered outbox entries, up to `limit` (see
|
|
1119
|
+
* `OutboxEntry` for the append invariant). The relay peeks, delivers via
|
|
1120
|
+
* `recordChildResults` on the parent store, then deletes — redelivery
|
|
1121
|
+
* after a crash is safe because dependency rows dedup.
|
|
1122
|
+
*
|
|
1123
|
+
* `after` pages past a previously-returned entry id (exclusive), whether
|
|
1124
|
+
* or not that entry still exists — the relay walks the whole outbox this
|
|
1125
|
+
* way, so entries it cannot route (their parent store is not provided
|
|
1126
|
+
* here) never blockade the ones behind them. Anything other than an id
|
|
1127
|
+
* this store issued may be treated as unset.
|
|
1128
|
+
*
|
|
1129
|
+
* @since 0.6.0
|
|
1130
|
+
*/
|
|
1131
|
+
readonly peekOutbox: (options: {
|
|
1132
|
+
readonly limit: number
|
|
1133
|
+
readonly after?: string | undefined
|
|
1134
|
+
}) => Effect.Effect<ReadonlyArray<OutboxEntry>, JobStoreError>
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Delete delivered outbox entries by id. Idempotent; unknown ids are
|
|
1138
|
+
* ignored.
|
|
1139
|
+
*
|
|
1140
|
+
* @since 0.6.0
|
|
1141
|
+
*/
|
|
1142
|
+
readonly deleteOutbox: (
|
|
1143
|
+
ids: ReadonlyArray<string>
|
|
1144
|
+
) => Effect.Effect<void, JobStoreError>
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* A flow's dependency rows, ordered by child key; feeds `collect` and
|
|
1148
|
+
* dashboards. Pass the returned `cursor` back for the next page.
|
|
1149
|
+
*
|
|
1150
|
+
* @since 0.6.0
|
|
1151
|
+
*/
|
|
1152
|
+
readonly listChildResults: (
|
|
1153
|
+
flowId: JobId,
|
|
1154
|
+
options?: {
|
|
1155
|
+
readonly cursor?: string | undefined
|
|
1156
|
+
/** Page size; default 1000. */
|
|
1157
|
+
readonly limit?: number | undefined
|
|
1158
|
+
} | undefined
|
|
1159
|
+
) => Effect.Effect<
|
|
1160
|
+
{
|
|
1161
|
+
readonly items: ReadonlyArray<FlowChildRecord>
|
|
1162
|
+
readonly cursor: string | undefined
|
|
1163
|
+
},
|
|
1164
|
+
JobStoreError
|
|
1165
|
+
>
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* The flow sweeper's work list (see `FlowSweepWork`). `pendingAgeMs`
|
|
1169
|
+
* scopes reconciliation to rows whose eligibility timestamp is at least
|
|
1170
|
+
* this old (giving the push path time); `limit` bounds the rows returned
|
|
1171
|
+
* per class per sweep.
|
|
1172
|
+
*
|
|
1173
|
+
* Returning a row for reconciliation re-arms its eligibility timestamp
|
|
1174
|
+
* (it is not returned again until another `pendingAgeMs` elapses), so a
|
|
1175
|
+
* full page ROTATES across sweeps: healthy in-flight children and rows
|
|
1176
|
+
* this sweeper cannot act on never pin the head of the page and starve
|
|
1177
|
+
* the rows behind them.
|
|
1178
|
+
*
|
|
1179
|
+
* @since 0.6.0
|
|
1180
|
+
*/
|
|
1181
|
+
readonly flowSweepWork: (options: {
|
|
1182
|
+
readonly pendingAgeMs: number
|
|
1183
|
+
readonly limit?: number | undefined
|
|
1184
|
+
}) => Effect.Effect<FlowSweepWork, JobStoreError>
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Mark dependency rows as `cascaded` after their cancels were delivered
|
|
1188
|
+
* into (or confirmed unnecessary by) the child's store. Idempotent.
|
|
1189
|
+
*
|
|
1190
|
+
* @since 0.6.0
|
|
1191
|
+
*/
|
|
1192
|
+
readonly markChildrenCascaded: (
|
|
1193
|
+
flowId: JobId,
|
|
1194
|
+
childKeys: ReadonlyArray<string>
|
|
1195
|
+
) => Effect.Effect<void, JobStoreError>
|
|
1196
|
+
|
|
841
1197
|
readonly counts: (
|
|
842
1198
|
queue?: QueueName
|
|
843
1199
|
) => Effect.Effect<Record<JobState, number>, JobStoreError>
|
|
844
1200
|
|
|
845
|
-
/**
|
|
1201
|
+
/**
|
|
1202
|
+
* Remove a job (and its ledger; a flow parent's dependency rows go with
|
|
1203
|
+
* it). Refuses (returns false) when active or `waiting-children`.
|
|
1204
|
+
*
|
|
1205
|
+
* Note the retention asymmetry for flows: AUTOMATIC pruning (`keep`
|
|
1206
|
+
* policies, the `historyTtl` sweep) must skip a settled flow parent whose
|
|
1207
|
+
* rows still owe cascade cancels (`cancelled` and not `cascaded`) — those
|
|
1208
|
+
* rows are the only record that real cancels are still due in the child
|
|
1209
|
+
* stores. `remove` is the explicit operator override and deletes anyway.
|
|
1210
|
+
*/
|
|
846
1211
|
readonly remove: (id: JobId) => Effect.Effect<boolean, JobStoreError>
|
|
847
1212
|
}
|
|
848
1213
|
|