effect-mq 0.1.0 → 0.3.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 (67) hide show
  1. package/README.md +305 -25
  2. package/dist/Job.d.ts +118 -5
  3. package/dist/Job.d.ts.map +1 -1
  4. package/dist/Job.js +119 -4
  5. package/dist/Job.js.map +1 -1
  6. package/dist/JobStore.d.ts +260 -9
  7. package/dist/JobStore.d.ts.map +1 -1
  8. package/dist/JobStore.js +115 -1
  9. package/dist/JobStore.js.map +1 -1
  10. package/dist/MemoryJobStore.d.ts +38 -6
  11. package/dist/MemoryJobStore.d.ts.map +1 -1
  12. package/dist/MemoryJobStore.js +351 -47
  13. package/dist/MemoryJobStore.js.map +1 -1
  14. package/dist/Worker.d.ts +5 -1
  15. package/dist/Worker.d.ts.map +1 -1
  16. package/dist/Worker.js +117 -10
  17. package/dist/Worker.js.map +1 -1
  18. package/dist/{drizzle → drizzle-postgres}/DrizzleJobStore.d.ts +35 -2
  19. package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -0
  20. package/dist/drizzle-postgres/DrizzleJobStore.js +941 -0
  21. package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -0
  22. package/dist/drizzle-postgres/index.d.ts.map +1 -0
  23. package/dist/drizzle-postgres/index.js.map +1 -0
  24. package/dist/drizzle-postgres/schema.d.ts +670 -0
  25. package/dist/drizzle-postgres/schema.d.ts.map +1 -0
  26. package/dist/drizzle-postgres/schema.js +150 -0
  27. package/dist/drizzle-postgres/schema.js.map +1 -0
  28. package/dist/redis/RedisJobStore.d.ts +58 -0
  29. package/dist/redis/RedisJobStore.d.ts.map +1 -0
  30. package/dist/redis/RedisJobStore.js +424 -0
  31. package/dist/redis/RedisJobStore.js.map +1 -0
  32. package/dist/redis/index.d.ts +9 -0
  33. package/dist/redis/index.d.ts.map +1 -0
  34. package/dist/redis/index.js +9 -0
  35. package/dist/redis/index.js.map +1 -0
  36. package/dist/redis/scripts.d.ts +181 -0
  37. package/dist/redis/scripts.d.ts.map +1 -0
  38. package/dist/redis/scripts.js +940 -0
  39. package/dist/redis/scripts.js.map +1 -0
  40. package/dist/testing/conformance.d.ts.map +1 -1
  41. package/dist/testing/conformance.js +502 -8
  42. package/dist/testing/conformance.js.map +1 -1
  43. package/package.json +8 -4
  44. package/src/Job.ts +301 -10
  45. package/src/JobStore.ts +373 -9
  46. package/src/MemoryJobStore.ts +440 -53
  47. package/src/Worker.ts +153 -10
  48. package/src/drizzle-postgres/DrizzleJobStore.ts +1311 -0
  49. package/src/drizzle-postgres/schema.ts +279 -0
  50. package/src/redis/RedisJobStore.ts +652 -0
  51. package/src/redis/index.ts +8 -0
  52. package/src/redis/scripts.ts +1055 -0
  53. package/src/testing/conformance.ts +665 -8
  54. package/dist/drizzle/DrizzleJobStore.d.ts.map +0 -1
  55. package/dist/drizzle/DrizzleJobStore.js +0 -426
  56. package/dist/drizzle/DrizzleJobStore.js.map +0 -1
  57. package/dist/drizzle/index.d.ts.map +0 -1
  58. package/dist/drizzle/index.js.map +0 -1
  59. package/dist/drizzle/schema.d.ts +0 -464
  60. package/dist/drizzle/schema.d.ts.map +0 -1
  61. package/dist/drizzle/schema.js +0 -68
  62. package/dist/drizzle/schema.js.map +0 -1
  63. package/src/drizzle/DrizzleJobStore.ts +0 -599
  64. package/src/drizzle/schema.ts +0 -116
  65. /package/dist/{drizzle → drizzle-postgres}/index.d.ts +0 -0
  66. /package/dist/{drizzle → drizzle-postgres}/index.js +0 -0
  67. /package/src/{drizzle → drizzle-postgres}/index.ts +0 -0
package/src/JobStore.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * @since 0.1.0
18
18
  */
19
- import { Brand, Context, Data, type Effect, type Option, Predicate } from "effect"
19
+ import { Brand, Context, Cron, Data, Duration, type Effect, type Option, Predicate, Result } from "effect"
20
20
 
21
21
  /**
22
22
  * The identifier of an enqueued job. Produced by `enqueue` (either
@@ -47,6 +47,20 @@ export type QueueName = Brand.Branded<string, "effect-mq/QueueName">
47
47
  */
48
48
  export const QueueName: Brand.Constructor<QueueName> = Brand.nominal<QueueName>()
49
49
 
50
+ /**
51
+ * The identifier of a repeatable-job schedule.
52
+ *
53
+ * @since 0.2.0
54
+ */
55
+ export type ScheduleKey = Brand.Branded<string, "effect-mq/ScheduleKey">
56
+
57
+ /**
58
+ * Brand a raw string as a `ScheduleKey`.
59
+ *
60
+ * @since 0.2.0
61
+ */
62
+ export const ScheduleKey: Brand.Constructor<ScheduleKey> = Brand.nominal<ScheduleKey>()
63
+
50
64
  /**
51
65
  * The lifecycle states of a job.
52
66
  *
@@ -57,7 +71,13 @@ export const QueueName: Brand.Constructor<QueueName> = Brand.nominal<QueueName>(
57
71
  *
58
72
  * @since 0.1.0
59
73
  */
60
- export type JobState = "waiting" | "delayed" | "active" | "completed" | "failed"
74
+ export type JobState =
75
+ | "waiting"
76
+ | "delayed"
77
+ | "active"
78
+ | "completed"
79
+ | "failed"
80
+ | "cancelled"
61
81
 
62
82
  /**
63
83
  * Retry backoff policy, persisted on the job record so any worker can route
@@ -75,19 +95,101 @@ export interface BackoffPolicy {
75
95
  }
76
96
 
77
97
  /**
78
- * Retention policy for terminal (completed/failed) jobs, persisted on the
79
- * record. Applied by the store after terminal acks, scoped to jobs with the
80
- * same name and state. Default (undefined) keeps records forever.
98
+ * Retention rule for one terminal state's records (per job name).
81
99
  *
82
- * @since 0.1.0
100
+ * @since 0.3.0
83
101
  */
84
- export interface KeepPolicy {
102
+ export interface KeepStatePolicy {
85
103
  /** Keep at most this many terminal records (per name + state). */
86
104
  readonly count?: number | undefined
87
105
  /** Remove terminal records older than this many milliseconds. */
88
106
  readonly ageMs?: number | undefined
89
107
  }
90
108
 
109
+ /**
110
+ * Retention policy persisted on the record, split by terminal state —
111
+ * completed jobs are usually noise, failed ones evidence. Applied by the
112
+ * store after terminal acks (scoped to jobs with the same name and state),
113
+ * and honoured by the store's periodic history sweep when one is configured.
114
+ * An absent state keeps its records until the store's `historyTtl` ceiling
115
+ * (forever without one). Producers accept a flat `{ count, age }` shorthand
116
+ * and normalize it to all three states before it reaches a driver.
117
+ *
118
+ * @since 0.1.0
119
+ */
120
+ export interface KeepPolicy {
121
+ readonly completed?: KeepStatePolicy | undefined
122
+ readonly failed?: KeepStatePolicy | undefined
123
+ readonly cancelled?: KeepStatePolicy | undefined
124
+ }
125
+
126
+ /**
127
+ * The terminal states retention applies to.
128
+ *
129
+ * @since 0.3.0
130
+ */
131
+ export type TerminalState = "completed" | "failed" | "cancelled"
132
+
133
+ /**
134
+ * Store-level retention ceiling: one duration for all terminal states, or a
135
+ * per-state split (an absent state is never swept by the timer).
136
+ *
137
+ * @since 0.3.0
138
+ */
139
+ export type HistoryTtlInput =
140
+ | Duration.Input
141
+ | {
142
+ readonly completed?: Duration.Input | undefined
143
+ readonly failed?: Duration.Input | undefined
144
+ readonly cancelled?: Duration.Input | undefined
145
+ }
146
+
147
+ /**
148
+ * `HistoryTtlInput` normalized to milliseconds per terminal state.
149
+ *
150
+ * @since 0.3.0
151
+ */
152
+ export interface HistoryTtlByState {
153
+ readonly completed?: number | undefined
154
+ readonly failed?: number | undefined
155
+ readonly cancelled?: number | undefined
156
+ }
157
+
158
+ /**
159
+ * Normalize a `HistoryTtlInput` to milliseconds per terminal state.
160
+ *
161
+ * @internal
162
+ */
163
+ export const normalizeHistoryTtl = (
164
+ input: HistoryTtlInput
165
+ ): HistoryTtlByState => {
166
+ if (Predicate.isObject(input) && !Duration.isDuration(input)) {
167
+ if (!("completed" in input || "failed" in input || "cancelled" in input)) {
168
+ // {} or a DurationObject would silently normalize to a 0ms ceiling and
169
+ // wipe all history — refuse loudly; use "90 days"-style inputs.
170
+ throw new Error(
171
+ "effect-mq: historyTtl object must set at least one of completed/failed/cancelled"
172
+ )
173
+ }
174
+ // SAFETY: Duration inputs (numbers, strings, bigints, tuples, Duration
175
+ // instances, DurationObjects) never carry terminal-state keys, so this
176
+ // is the per-state split form.
177
+ const split = input as {
178
+ readonly completed?: Duration.Input | undefined
179
+ readonly failed?: Duration.Input | undefined
180
+ readonly cancelled?: Duration.Input | undefined
181
+ }
182
+ return {
183
+ completed: split.completed !== undefined ? Duration.toMillis(split.completed) : undefined,
184
+ failed: split.failed !== undefined ? Duration.toMillis(split.failed) : undefined,
185
+ cancelled: split.cancelled !== undefined ? Duration.toMillis(split.cancelled) : undefined
186
+ }
187
+ }
188
+ // SAFETY: not the split form (checked above), so a plain Duration input.
189
+ const ms = Duration.toMillis(input as Duration.Input)
190
+ return { completed: ms, failed: ms, cancelled: ms }
191
+ }
192
+
91
193
  /**
92
194
  * One run of a job, persisted so the full run history is durable and
93
195
  * inspectable — the storage-level analogue of `tapError` before a rerun,
@@ -103,7 +205,7 @@ export interface AttemptRecord {
103
205
  readonly startedAt: number | undefined
104
206
  /** Ack/recovery time of this run (epoch millis). */
105
207
  readonly finishedAt: number
106
- readonly outcome: "completed" | "retried" | "failed" | "stalled"
208
+ readonly outcome: "completed" | "retried" | "failed" | "stalled" | "cancelled"
107
209
  /** Schema-encoded `Exit`; undefined for `stalled`. */
108
210
  readonly exit: unknown
109
211
  }
@@ -131,6 +233,15 @@ export interface JobRecord {
131
233
  readonly stalledCount: number
132
234
  readonly backoff: BackoffPolicy | undefined
133
235
  readonly keep: KeepPolicy | undefined
236
+ /** Per-run execution time limit; the worker interrupts the handler past it. */
237
+ readonly timeoutMs: number | undefined
238
+ /**
239
+ * Set by `cancel` on an active job; the owning worker interrupts the
240
+ * handler on its next heartbeat and acks `Cancelled`.
241
+ */
242
+ readonly cancelRequested: boolean
243
+ /** The dedup key this job was enqueued under, if any (see `DedupePolicy`). */
244
+ readonly dedupeKey: string | undefined
134
245
  /** Epoch millis before which the job must not be claimed. */
135
246
  readonly runAt: number
136
247
  readonly enqueuedAt: number
@@ -142,6 +253,36 @@ export interface JobRecord {
142
253
  readonly failedReason: string | undefined
143
254
  }
144
255
 
256
+ /**
257
+ * Deduplication policy carried on an enqueue request, scoped per job name.
258
+ * Dedup NEVER changes the job id — ids come from the caller, the idempotency
259
+ * key, or the store's id generator; the dedup key is a separate value with
260
+ * its own lifecycle:
261
+ *
262
+ * - `{ key }` (no ttl): dedupe while the keyed job is pending (waiting,
263
+ * delayed, or active); a terminal keyed job frees the key.
264
+ * - `{ key, ttlMs }`: throttle — at most one job per key per window,
265
+ * regardless of the keyed job's completion.
266
+ * - `extend` (requires `ttlMs`): each deduplicated enqueue pushes the window
267
+ * out (debounce).
268
+ * - `replace`: while the keyed job is still *delayed*, the new enqueue's
269
+ * payload/metadata/priority/attempts/backoff/keep/timeout/delay replace the
270
+ * existing job's (id and ledger preserved), and a `ttlMs` window is
271
+ * re-armed from the replace; in every other state normal dedup applies.
272
+ *
273
+ * A deduplicated (or replaced) enqueue returns the keyed job's id with
274
+ * `duplicate: true`.
275
+ *
276
+ * @since 0.3.0
277
+ */
278
+ export interface DedupePolicy {
279
+ /** Non-empty; producers validate before the request reaches a driver. */
280
+ readonly key: string
281
+ readonly ttlMs: number | undefined
282
+ readonly extend: boolean
283
+ readonly replace: boolean
284
+ }
285
+
145
286
  /**
146
287
  * @since 0.1.0
147
288
  */
@@ -160,9 +301,29 @@ export interface EnqueueRequest {
160
301
  readonly attemptsMax: number
161
302
  readonly backoff: BackoffPolicy | undefined
162
303
  readonly keep: KeepPolicy | undefined
304
+ readonly timeoutMs: number | undefined
305
+ /** Deduplicate against other enqueues sharing `dedupe.key` (same name). */
306
+ readonly dedupe: DedupePolicy | undefined
163
307
  readonly delayMs: number
164
308
  }
165
309
 
310
+ /**
311
+ * Generator for store-assigned job ids, used when `EnqueueRequest.id` is
312
+ * undefined (custom ids and idempotency keys always win, and schedule tick
313
+ * ids stay slot-deterministic). May be effectful (e.g. draw from Effect's
314
+ * `Random`). The store retries a bounded number of times when a generated id
315
+ * collides with an existing job, then fails the enqueue with
316
+ * `JobStoreError` — generators must have enough entropy that collisions are
317
+ * pathological, not routine.
318
+ *
319
+ * @example `({ name }) => \`${name}_${ulid()}\``
320
+ *
321
+ * @since 0.2.0
322
+ */
323
+ export type IdGenerator = (
324
+ request: EnqueueRequest
325
+ ) => string | Effect.Effect<string>
326
+
166
327
  /**
167
328
  * @since 0.1.0
168
329
  */
@@ -213,6 +374,7 @@ export type AckOutcome =
213
374
  | { readonly _tag: "Complete"; readonly exit: unknown }
214
375
  | { readonly _tag: "Retry"; readonly delayMs: number; readonly exit: unknown }
215
376
  | { readonly _tag: "Fail"; readonly exit: unknown }
377
+ | { readonly _tag: "Cancelled" }
216
378
 
217
379
  /**
218
380
  * Filters and pagination for `list`. Results are ordered newest-first
@@ -241,6 +403,45 @@ export interface ListResult {
241
403
  readonly cursor: string | undefined
242
404
  }
243
405
 
406
+ /**
407
+ * A repeatable-job schedule as persisted by the store. Exactly one of `cron`
408
+ * (with optional IANA `tz`) or `everyMs` is set. The payload is stored
409
+ * schema-encoded, like job payloads. `nextRunAt` is maintained by the worker
410
+ * sweep via `advanceSchedule`; ticks enqueue with the deterministic id
411
+ * `sched/<key>/<slot>`, so concurrent sweepers dedup naturally.
412
+ *
413
+ * @since 0.2.0
414
+ */
415
+ export interface ScheduleRecord {
416
+ readonly key: ScheduleKey
417
+ readonly jobName: string
418
+ readonly queue: QueueName
419
+ readonly cron: string | undefined
420
+ readonly tz: string | undefined
421
+ readonly everyMs: number | undefined
422
+ readonly payload: unknown
423
+ readonly metadata: Readonly<Record<string, string>>
424
+ readonly priority: number
425
+ readonly attemptsMax: number
426
+ readonly backoff: BackoffPolicy | undefined
427
+ readonly keep: KeepPolicy | undefined
428
+ readonly timeoutMs: number | undefined
429
+ /** Epoch millis of the next occurrence to enqueue. */
430
+ readonly nextRunAt: number
431
+ }
432
+
433
+ /**
434
+ * Result of a heartbeat: locks that could not be extended (lost to stall
435
+ * recovery or another worker) and active jobs with a pending cancel request
436
+ * (the worker must interrupt them and ack `Cancelled`).
437
+ *
438
+ * @since 0.2.0
439
+ */
440
+ export interface ExtendLocksResult {
441
+ readonly lost: ReadonlyArray<JobId>
442
+ readonly cancelRequested: ReadonlyArray<JobId>
443
+ }
444
+
244
445
  /**
245
446
  * A transient or fatal driver error (connection loss, serialization, etc.).
246
447
  *
@@ -260,6 +461,73 @@ export class JobStoreError extends Data.TaggedError("JobStoreError")<{
260
461
  export const isJobStoreError = (u: unknown): u is JobStoreError =>
261
462
  Predicate.hasProperty(u, "_tag") && u._tag === "JobStoreError"
262
463
 
464
+ // Identity-based marking (WeakSet, works on frozen error instances) so the
465
+ // error keeps its declared type and schema round-trip untouched.
466
+ const unrecoverableRegistry = new WeakSet<object>()
467
+
468
+ /**
469
+ * Mark an error value as unrecoverable: when a handler fails with it, the
470
+ * worker skips the remaining retry budget and fails the job immediately. The
471
+ * error itself is returned unchanged, so typed error channels and schemas
472
+ * are unaffected. Also exported as `Job.unrecoverable`.
473
+ *
474
+ * Marking is identity-based, so it only works for object errors — a
475
+ * primitive (string/number) failure is returned unmarked and retries
476
+ * normally; use the definition-level `retryable` predicate for those.
477
+ *
478
+ * @since 0.2.0
479
+ */
480
+ export const unrecoverable = <E>(error: E): E => {
481
+ if (Object(error) === error) {
482
+ // SAFETY: `Object(x) === x` is true exactly for object values, which is
483
+ // what WeakSet membership requires.
484
+ unrecoverableRegistry.add(error as object)
485
+ }
486
+ return error
487
+ }
488
+
489
+ /**
490
+ * Whether `unrecoverable` marked this value.
491
+ *
492
+ * @internal
493
+ */
494
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- boundary classifier over arbitrary error values
495
+ export const isMarkedUnrecoverable = (u: unknown): boolean =>
496
+ // SAFETY: `Object(u) === u` short-circuits to false for primitives, so the
497
+ // assertion only ever passes object values to the WeakSet.
498
+ Object(u) === u && unrecoverableRegistry.has(u as object)
499
+
500
+ /**
501
+ * The next occurrence of a schedule strictly after `now`. `fromSlot` anchors
502
+ * `everyMs` schedules (occurrences stay on the `slot + k * every` grid).
503
+ * Returns undefined for an invalid cron expression.
504
+ *
505
+ * @internal
506
+ */
507
+ export const nextOccurrence = (
508
+ schedule: Pick<ScheduleRecord, "cron" | "tz" | "everyMs">,
509
+ fromSlot: number,
510
+ now: number
511
+ ): number | undefined => {
512
+ if (schedule.cron !== undefined) {
513
+ const parsed = Cron.parse(schedule.cron, schedule.tz)
514
+ if (Result.isFailure(parsed)) return undefined
515
+ // Cron.next throws for parseable-but-unsatisfiable expressions (e.g.
516
+ // "0 0 30 2 *"); treat those as invalid rather than defecting the sweep.
517
+ try {
518
+ return Cron.next(parsed.success, new Date(Math.max(fromSlot, now))).getTime()
519
+ } catch {
520
+ return undefined
521
+ }
522
+ }
523
+ if (schedule.everyMs !== undefined && schedule.everyMs > 0) {
524
+ const behind = Math.max(0, now - fromSlot)
525
+ const steps = Math.floor(behind / schedule.everyMs) + 1
526
+ return fromSlot + steps * schedule.everyMs
527
+ }
528
+ return undefined
529
+ }
530
+
263
531
 
264
532
  /**
265
533
  * The presented lock token no longer owns the job (it stalled and was
@@ -288,6 +556,35 @@ export class JobNotRetryableError extends Data.TaggedError("JobNotRetryableError
288
556
  readonly state: JobState
289
557
  }> {}
290
558
 
559
+ /**
560
+ * `cancel` was called on a job that is already terminal.
561
+ *
562
+ * @since 0.2.0
563
+ */
564
+ export class JobNotCancellableError extends Data.TaggedError("JobNotCancellableError")<{
565
+ readonly jobId: JobId
566
+ readonly state: JobState
567
+ }> {}
568
+
569
+ /**
570
+ * `promote` was called on a job that is not in the `delayed` state.
571
+ *
572
+ * @since 0.2.0
573
+ */
574
+ export class JobNotPromotableError extends Data.TaggedError("JobNotPromotableError")<{
575
+ readonly jobId: JobId
576
+ readonly state: JobState
577
+ }> {}
578
+
579
+ /**
580
+ * Raised (as a defect) by `awaitResult` when the awaited job was cancelled.
581
+ *
582
+ * @since 0.2.0
583
+ */
584
+ export class JobCancelledError extends Data.TaggedError("JobCancelledError")<{
585
+ readonly jobId: JobId
586
+ }> {}
587
+
291
588
  /**
292
589
  * The service shape every store implements.
293
590
  *
@@ -339,7 +636,7 @@ export interface Service {
339
636
  readonly extendLocks: (
340
637
  locks: ReadonlyArray<{ readonly id: JobId; readonly token: string }>,
341
638
  durationMs: number
342
- ) => Effect.Effect<ReadonlyArray<JobId>, JobStoreError>
639
+ ) => Effect.Effect<ExtendLocksResult, JobStoreError>
343
640
 
344
641
  /**
345
642
  * Sweep active jobs whose lock has expired. Each recovered job gets
@@ -391,6 +688,73 @@ export interface Service {
391
688
  JobStoreError | JobNotFoundError | JobNotRetryableError
392
689
  >
393
690
 
691
+ /**
692
+ * Cancel a job. Waiting/delayed jobs become terminal (`cancelled`)
693
+ * immediately; active jobs get `cancelRequested` set, and the owning
694
+ * worker interrupts the handler on its next heartbeat. Terminal jobs fail
695
+ * with `JobNotCancellableError`.
696
+ */
697
+ readonly cancel: (
698
+ id: JobId
699
+ ) => Effect.Effect<
700
+ void,
701
+ JobStoreError | JobNotFoundError | JobNotCancellableError
702
+ >
703
+
704
+ /** Move a delayed job to `waiting` now. */
705
+ readonly promote: (
706
+ id: JobId
707
+ ) => Effect.Effect<
708
+ void,
709
+ JobStoreError | JobNotFoundError | JobNotPromotableError
710
+ >
711
+
712
+ /**
713
+ * Durably pause a queue: claims return `Empty` until `resume` (delayed
714
+ * jobs still promote to `waiting`, they just aren't handed out). Affects
715
+ * every worker on the store.
716
+ */
717
+ readonly pause: (queue: QueueName) => Effect.Effect<void, JobStoreError>
718
+
719
+ /** Undo `pause` and wake idle workers. */
720
+ readonly resume: (queue: QueueName) => Effect.Effect<void, JobStoreError>
721
+
722
+ readonly pausedQueues: () => Effect.Effect<
723
+ ReadonlyArray<QueueName>,
724
+ JobStoreError
725
+ >
726
+
727
+ /** Create or replace a repeatable-job schedule (keyed by `schedule.key`). */
728
+ readonly upsertSchedule: (
729
+ schedule: ScheduleRecord
730
+ ) => Effect.Effect<void, JobStoreError>
731
+
732
+ /** Remove a schedule. Returns false when the key does not exist. */
733
+ readonly removeSchedule: (
734
+ key: ScheduleKey
735
+ ) => Effect.Effect<boolean, JobStoreError>
736
+
737
+ readonly listSchedules: (options?: {
738
+ readonly jobName?: string | undefined
739
+ readonly queue?: QueueName | undefined
740
+ }) => Effect.Effect<ReadonlyArray<ScheduleRecord>, JobStoreError>
741
+
742
+ /** Schedules whose `nextRunAt` is due (per the Effect `Clock`). */
743
+ readonly dueSchedules: () => Effect.Effect<
744
+ ReadonlyArray<ScheduleRecord>,
745
+ JobStoreError
746
+ >
747
+
748
+ /**
749
+ * Advance a schedule's `nextRunAt` from `expectedRunAt` to `nextRunAt`
750
+ * (conditional, so concurrent sweepers cannot regress it).
751
+ */
752
+ readonly advanceSchedule: (
753
+ key: ScheduleKey,
754
+ expectedRunAt: number,
755
+ nextRunAt: number
756
+ ) => Effect.Effect<void, JobStoreError>
757
+
394
758
  readonly counts: (
395
759
  queue?: QueueName
396
760
  ) => Effect.Effect<Record<JobState, number>, JobStoreError>