effect-mq 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.
- package/README.md +167 -22
- package/dist/Job.d.ts +67 -4
- package/dist/Job.d.ts.map +1 -1
- package/dist/Job.js +76 -3
- package/dist/Job.js.map +1 -1
- package/dist/JobStore.d.ts +174 -3
- package/dist/JobStore.d.ts.map +1 -1
- package/dist/JobStore.js +89 -1
- package/dist/JobStore.js.map +1 -1
- package/dist/MemoryJobStore.d.ts +37 -6
- package/dist/MemoryJobStore.d.ts.map +1 -1
- package/dist/MemoryJobStore.js +201 -25
- package/dist/MemoryJobStore.js.map +1 -1
- package/dist/Worker.d.ts +5 -1
- package/dist/Worker.d.ts.map +1 -1
- package/dist/Worker.js +116 -10
- package/dist/Worker.js.map +1 -1
- package/dist/{drizzle → drizzle-postgres}/DrizzleJobStore.d.ts +18 -2
- package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -0
- package/dist/{drizzle → drizzle-postgres}/DrizzleJobStore.js +287 -40
- package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -0
- package/dist/drizzle-postgres/index.d.ts.map +1 -0
- package/dist/drizzle-postgres/index.js.map +1 -0
- package/dist/{drizzle → drizzle-postgres}/schema.d.ts +306 -4
- package/dist/drizzle-postgres/schema.d.ts.map +1 -0
- package/dist/{drizzle → drizzle-postgres}/schema.js +36 -2
- package/dist/drizzle-postgres/schema.js.map +1 -0
- package/dist/redis/RedisJobStore.d.ts +56 -0
- package/dist/redis/RedisJobStore.d.ts.map +1 -0
- package/dist/redis/RedisJobStore.js +385 -0
- package/dist/redis/RedisJobStore.js.map +1 -0
- package/dist/redis/index.d.ts +9 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +9 -0
- package/dist/redis/index.js.map +1 -0
- package/dist/redis/scripts.d.ts +168 -0
- package/dist/redis/scripts.d.ts.map +1 -0
- package/dist/redis/scripts.js +755 -0
- package/dist/redis/scripts.js.map +1 -0
- package/dist/testing/conformance.d.ts.map +1 -1
- package/dist/testing/conformance.js +332 -3
- package/dist/testing/conformance.js.map +1 -1
- package/package.json +8 -4
- package/src/Job.ts +189 -5
- package/src/JobStore.ts +252 -4
- package/src/MemoryJobStore.ts +273 -26
- package/src/Worker.ts +152 -10
- package/src/{drizzle → drizzle-postgres}/DrizzleJobStore.ts +412 -40
- package/src/{drizzle → drizzle-postgres}/schema.ts +52 -3
- package/src/redis/RedisJobStore.ts +597 -0
- package/src/redis/index.ts +8 -0
- package/src/redis/scripts.ts +862 -0
- package/src/testing/conformance.ts +421 -3
- package/dist/drizzle/DrizzleJobStore.d.ts.map +0 -1
- package/dist/drizzle/DrizzleJobStore.js.map +0 -1
- package/dist/drizzle/index.d.ts.map +0 -1
- package/dist/drizzle/index.js.map +0 -1
- package/dist/drizzle/schema.d.ts.map +0 -1
- package/dist/drizzle/schema.js.map +0 -1
- /package/dist/{drizzle → drizzle-postgres}/index.d.ts +0 -0
- /package/dist/{drizzle → drizzle-postgres}/index.js +0 -0
- /package/src/{drizzle → drizzle-postgres}/index.ts +0 -0
package/src/MemoryJobStore.ts
CHANGED
|
@@ -10,20 +10,27 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @since 0.1.0
|
|
12
12
|
*/
|
|
13
|
-
import { Clock, type Context, Deferred, Effect, Exit, Layer, Option } from "effect"
|
|
13
|
+
import { Clock, type Context, Deferred, Duration, Effect, Exit, Layer, Option, type Scope } from "effect"
|
|
14
14
|
import {
|
|
15
15
|
type AttemptRecord,
|
|
16
16
|
type ClaimResult,
|
|
17
17
|
type EnqueueRequest,
|
|
18
|
+
type ExtendLocksResult,
|
|
19
|
+
type IdGenerator,
|
|
18
20
|
JobId,
|
|
21
|
+
JobNotCancellableError,
|
|
19
22
|
JobNotFoundError,
|
|
23
|
+
JobNotPromotableError,
|
|
20
24
|
JobNotRetryableError,
|
|
21
25
|
type JobRecord,
|
|
22
26
|
type JobState,
|
|
23
27
|
JobStore,
|
|
24
28
|
type ListResult,
|
|
25
29
|
LockLostError,
|
|
30
|
+
JobStoreError,
|
|
26
31
|
type QueueName,
|
|
32
|
+
type ScheduleKey,
|
|
33
|
+
type ScheduleRecord,
|
|
27
34
|
type Service
|
|
28
35
|
} from "./JobStore.ts"
|
|
29
36
|
|
|
@@ -40,6 +47,8 @@ interface MemJob {
|
|
|
40
47
|
stalledCount: number
|
|
41
48
|
readonly backoff: JobRecord["backoff"]
|
|
42
49
|
readonly keep: JobRecord["keep"]
|
|
50
|
+
readonly timeoutMs: number | undefined
|
|
51
|
+
cancelRequested: boolean
|
|
43
52
|
runAt: number
|
|
44
53
|
readonly enqueuedAt: number
|
|
45
54
|
processedAt: number | undefined
|
|
@@ -52,6 +61,8 @@ interface MemJob {
|
|
|
52
61
|
lockExpiresAt: number | undefined
|
|
53
62
|
}
|
|
54
63
|
|
|
64
|
+
const TERMINAL_STATES: ReadonlySet<JobState> = new Set(["completed", "failed", "cancelled"])
|
|
65
|
+
|
|
55
66
|
const snapshot = (job: MemJob): JobRecord => ({
|
|
56
67
|
id: job.id,
|
|
57
68
|
name: job.name,
|
|
@@ -65,6 +76,8 @@ const snapshot = (job: MemJob): JobRecord => ({
|
|
|
65
76
|
stalledCount: job.stalledCount,
|
|
66
77
|
backoff: job.backoff,
|
|
67
78
|
keep: job.keep,
|
|
79
|
+
timeoutMs: job.timeoutMs,
|
|
80
|
+
cancelRequested: job.cancelRequested,
|
|
68
81
|
runAt: job.runAt,
|
|
69
82
|
enqueuedAt: job.enqueuedAt,
|
|
70
83
|
processedAt: job.processedAt,
|
|
@@ -83,13 +96,15 @@ const metadataMatches = (
|
|
|
83
96
|
return true
|
|
84
97
|
}
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
interface MemoryStore {
|
|
100
|
+
readonly service: Service
|
|
101
|
+
readonly sweepHistory: (now: number, ttlMs: number) => void
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemoryStore => {
|
|
92
105
|
const jobs = new Map<string, MemJob>()
|
|
106
|
+
const schedules = new Map<ScheduleKey, ScheduleRecord>()
|
|
107
|
+
const paused = new Set<QueueName>()
|
|
93
108
|
let seq = 0
|
|
94
109
|
let idCounter = 0
|
|
95
110
|
let wakeVersion = 0
|
|
@@ -133,6 +148,15 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
133
148
|
})
|
|
134
149
|
}
|
|
135
150
|
|
|
151
|
+
const markCancelled = (job: MemJob, now: number) => {
|
|
152
|
+
clearLock(job)
|
|
153
|
+
job.cancelRequested = false
|
|
154
|
+
job.state = "cancelled"
|
|
155
|
+
job.finishedAt = now
|
|
156
|
+
recordAttempt(job, "cancelled", now, undefined)
|
|
157
|
+
applyKeep(job, now)
|
|
158
|
+
}
|
|
159
|
+
|
|
136
160
|
// Terminal retention: keep at most `count` and drop older than `ageMs`
|
|
137
161
|
// among terminal jobs sharing this job's name + state.
|
|
138
162
|
const applyKeep = (job: MemJob, now: number) => {
|
|
@@ -158,16 +182,46 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
158
182
|
}
|
|
159
183
|
}
|
|
160
184
|
|
|
161
|
-
|
|
185
|
+
const sweepHistory = (now: number, ttlMs: number) => {
|
|
186
|
+
for (const job of jobs.values()) {
|
|
187
|
+
if (
|
|
188
|
+
TERMINAL_STATES.has(job.state) &&
|
|
189
|
+
job.finishedAt !== undefined &&
|
|
190
|
+
job.finishedAt <= now - ttlMs
|
|
191
|
+
) {
|
|
192
|
+
jobs.delete(job.id)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const service: Service = JobStore.of({
|
|
162
198
|
enqueue: (request: EnqueueRequest) =>
|
|
163
199
|
Effect.gen(function*() {
|
|
164
200
|
const now = yield* Clock.currentTimeMillis
|
|
165
201
|
let id = request.id
|
|
166
202
|
if (id === undefined) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
203
|
+
const generate = options?.idGenerator
|
|
204
|
+
if (generate === undefined) {
|
|
205
|
+
// Store-assigned ids must never collide with user-supplied ones.
|
|
206
|
+
do {
|
|
207
|
+
id = JobId(`j-${++idCounter}`)
|
|
208
|
+
} while (jobs.has(id))
|
|
209
|
+
} else {
|
|
210
|
+
// A user generator gets a bounded number of collision retries; a
|
|
211
|
+
// healthy generator's entropy makes even one retry pathological.
|
|
212
|
+
for (let i = 0; i < 5 && id === undefined; i++) {
|
|
213
|
+
const raw = generate(request)
|
|
214
|
+
const candidate = JobId(Effect.isEffect(raw) ? yield* raw : raw)
|
|
215
|
+
if (!jobs.has(candidate)) {
|
|
216
|
+
id = candidate
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (id === undefined) {
|
|
220
|
+
return yield* new JobStoreError({
|
|
221
|
+
message: "enqueue failed: could not generate a unique job id"
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
}
|
|
171
225
|
} else if (jobs.has(id)) {
|
|
172
226
|
return { id, duplicate: true }
|
|
173
227
|
}
|
|
@@ -184,6 +238,8 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
184
238
|
stalledCount: 0,
|
|
185
239
|
backoff: request.backoff,
|
|
186
240
|
keep: request.keep,
|
|
241
|
+
timeoutMs: request.timeoutMs,
|
|
242
|
+
cancelRequested: false,
|
|
187
243
|
runAt: now + Math.max(0, request.delayMs),
|
|
188
244
|
enqueuedAt: now,
|
|
189
245
|
processedAt: undefined,
|
|
@@ -222,7 +278,7 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
222
278
|
}
|
|
223
279
|
}
|
|
224
280
|
}
|
|
225
|
-
if (best === undefined) {
|
|
281
|
+
if (best === undefined || paused.has(options.queue)) {
|
|
226
282
|
const empty: ClaimResult = { _tag: "Empty", nextRunAt, wakeToken: wakeVersion }
|
|
227
283
|
return empty
|
|
228
284
|
}
|
|
@@ -248,6 +304,7 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
248
304
|
job.attemptsMade += 1
|
|
249
305
|
switch (outcome._tag) {
|
|
250
306
|
case "Complete": {
|
|
307
|
+
job.cancelRequested = false
|
|
251
308
|
job.state = "completed"
|
|
252
309
|
job.exit = outcome.exit
|
|
253
310
|
job.finishedAt = now
|
|
@@ -256,6 +313,7 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
256
313
|
break
|
|
257
314
|
}
|
|
258
315
|
case "Fail": {
|
|
316
|
+
job.cancelRequested = false
|
|
259
317
|
job.state = "failed"
|
|
260
318
|
job.exit = outcome.exit
|
|
261
319
|
job.finishedAt = now
|
|
@@ -263,7 +321,22 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
263
321
|
applyKeep(job, now)
|
|
264
322
|
break
|
|
265
323
|
}
|
|
324
|
+
case "Cancelled": {
|
|
325
|
+
job.cancelRequested = false
|
|
326
|
+
job.state = "cancelled"
|
|
327
|
+
job.finishedAt = now
|
|
328
|
+
recordAttempt(job, "cancelled", now, undefined)
|
|
329
|
+
applyKeep(job, now)
|
|
330
|
+
break
|
|
331
|
+
}
|
|
266
332
|
case "Retry": {
|
|
333
|
+
if (job.cancelRequested) {
|
|
334
|
+
// A cancel raced a natural failure before the heartbeat could
|
|
335
|
+
// interrupt the run: cancellation wins over revival (mirrors
|
|
336
|
+
// release/recoverStalled).
|
|
337
|
+
markCancelled(job, now)
|
|
338
|
+
break
|
|
339
|
+
}
|
|
267
340
|
recordAttempt(job, "retried", now, outcome.exit)
|
|
268
341
|
job.runAt = now + Math.max(0, outcome.delayMs)
|
|
269
342
|
job.state = outcome.delayMs > 0 ? "delayed" : "waiting"
|
|
@@ -276,6 +349,7 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
276
349
|
|
|
277
350
|
release: (id, token) =>
|
|
278
351
|
Effect.gen(function*() {
|
|
352
|
+
const now = yield* Clock.currentTimeMillis
|
|
279
353
|
const job = jobs.get(id)
|
|
280
354
|
if (job === undefined) {
|
|
281
355
|
return yield* new JobNotFoundError({ jobId: id })
|
|
@@ -283,6 +357,12 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
283
357
|
if (job.state !== "active" || job.lockToken !== token) {
|
|
284
358
|
return yield* new LockLostError({ jobId: id })
|
|
285
359
|
}
|
|
360
|
+
if (job.cancelRequested) {
|
|
361
|
+
// A cancel arrived while the worker was shutting down: honour it
|
|
362
|
+
// instead of reviving the job.
|
|
363
|
+
markCancelled(job, now)
|
|
364
|
+
return
|
|
365
|
+
}
|
|
286
366
|
clearLock(job)
|
|
287
367
|
job.state = "waiting"
|
|
288
368
|
signalWake()
|
|
@@ -292,19 +372,23 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
292
372
|
Effect.gen(function*() {
|
|
293
373
|
const now = yield* Clock.currentTimeMillis
|
|
294
374
|
const lost: Array<JobId> = []
|
|
375
|
+
const cancelRequested: Array<JobId> = []
|
|
295
376
|
for (const lock of locks) {
|
|
296
377
|
const job = jobs.get(lock.id)
|
|
297
378
|
if (
|
|
298
|
-
job
|
|
299
|
-
job.state
|
|
300
|
-
job.lockToken
|
|
379
|
+
job === undefined ||
|
|
380
|
+
job.state !== "active" ||
|
|
381
|
+
job.lockToken !== lock.token
|
|
301
382
|
) {
|
|
302
|
-
job.lockExpiresAt = now + durationMs
|
|
303
|
-
} else {
|
|
304
383
|
lost.push(lock.id)
|
|
384
|
+
} else if (job.cancelRequested) {
|
|
385
|
+
cancelRequested.push(lock.id)
|
|
386
|
+
} else {
|
|
387
|
+
job.lockExpiresAt = now + durationMs
|
|
305
388
|
}
|
|
306
389
|
}
|
|
307
|
-
|
|
390
|
+
const result: ExtendLocksResult = { lost, cancelRequested }
|
|
391
|
+
return result
|
|
308
392
|
}),
|
|
309
393
|
|
|
310
394
|
recoverStalled: (options) =>
|
|
@@ -319,6 +403,11 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
319
403
|
) {
|
|
320
404
|
continue
|
|
321
405
|
}
|
|
406
|
+
if (job.cancelRequested) {
|
|
407
|
+
// The owning worker died before honouring the cancel: finish it.
|
|
408
|
+
markCancelled(job, now)
|
|
409
|
+
continue
|
|
410
|
+
}
|
|
322
411
|
clearLock(job)
|
|
323
412
|
job.stalledCount += 1
|
|
324
413
|
recordAttempt(job, "stalled", now, undefined)
|
|
@@ -414,6 +503,7 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
414
503
|
job.state = "waiting"
|
|
415
504
|
job.attemptsMade = 0
|
|
416
505
|
job.stalledCount = 0
|
|
506
|
+
job.cancelRequested = false
|
|
417
507
|
job.exit = undefined
|
|
418
508
|
job.failedReason = undefined
|
|
419
509
|
job.finishedAt = undefined
|
|
@@ -423,6 +513,101 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
423
513
|
signalWake()
|
|
424
514
|
}),
|
|
425
515
|
|
|
516
|
+
cancel: (id) =>
|
|
517
|
+
Effect.gen(function*() {
|
|
518
|
+
const now = yield* Clock.currentTimeMillis
|
|
519
|
+
const job = jobs.get(id)
|
|
520
|
+
if (job === undefined) {
|
|
521
|
+
return yield* new JobNotFoundError({ jobId: id })
|
|
522
|
+
}
|
|
523
|
+
switch (job.state) {
|
|
524
|
+
case "waiting":
|
|
525
|
+
case "delayed": {
|
|
526
|
+
markCancelled(job, now)
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
case "active": {
|
|
530
|
+
job.cancelRequested = true
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
default: {
|
|
534
|
+
return yield* new JobNotCancellableError({ jobId: id, state: job.state })
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}),
|
|
538
|
+
|
|
539
|
+
promote: (id) =>
|
|
540
|
+
Effect.gen(function*() {
|
|
541
|
+
const now = yield* Clock.currentTimeMillis
|
|
542
|
+
const job = jobs.get(id)
|
|
543
|
+
if (job === undefined) {
|
|
544
|
+
return yield* new JobNotFoundError({ jobId: id })
|
|
545
|
+
}
|
|
546
|
+
if (job.state !== "delayed") {
|
|
547
|
+
return yield* new JobNotPromotableError({ jobId: id, state: job.state })
|
|
548
|
+
}
|
|
549
|
+
job.state = "waiting"
|
|
550
|
+
job.runAt = now
|
|
551
|
+
signalWake()
|
|
552
|
+
}),
|
|
553
|
+
|
|
554
|
+
pause: (queue) =>
|
|
555
|
+
Effect.sync(() => {
|
|
556
|
+
paused.add(queue)
|
|
557
|
+
}),
|
|
558
|
+
|
|
559
|
+
resume: (queue) =>
|
|
560
|
+
Effect.sync(() => {
|
|
561
|
+
if (paused.delete(queue)) {
|
|
562
|
+
signalWake()
|
|
563
|
+
}
|
|
564
|
+
}),
|
|
565
|
+
|
|
566
|
+
pausedQueues: () => Effect.sync(() => Array.from(paused)),
|
|
567
|
+
|
|
568
|
+
upsertSchedule: (schedule) =>
|
|
569
|
+
Effect.sync(() => {
|
|
570
|
+
// An unchanged cadence keeps its next occurrence: deploy-time
|
|
571
|
+
// re-registration must not re-anchor `every` grids or drop a pending
|
|
572
|
+
// catch-up run. A changed cadence takes the caller's fresh nextRunAt.
|
|
573
|
+
const existing = schedules.get(schedule.key)
|
|
574
|
+
const sameCadence = existing !== undefined &&
|
|
575
|
+
existing.cron === schedule.cron &&
|
|
576
|
+
existing.tz === schedule.tz &&
|
|
577
|
+
existing.everyMs === schedule.everyMs
|
|
578
|
+
schedules.set(
|
|
579
|
+
schedule.key,
|
|
580
|
+
sameCadence ? { ...schedule, nextRunAt: existing.nextRunAt } : schedule
|
|
581
|
+
)
|
|
582
|
+
signalWake()
|
|
583
|
+
}),
|
|
584
|
+
|
|
585
|
+
removeSchedule: (key) => Effect.sync(() => schedules.delete(key)),
|
|
586
|
+
|
|
587
|
+
listSchedules: (options) =>
|
|
588
|
+
Effect.sync(() =>
|
|
589
|
+
Array.from(schedules.values()).filter((schedule) =>
|
|
590
|
+
(options?.jobName === undefined || schedule.jobName === options.jobName) &&
|
|
591
|
+
(options?.queue === undefined || schedule.queue === options.queue)
|
|
592
|
+
)
|
|
593
|
+
),
|
|
594
|
+
|
|
595
|
+
dueSchedules: () =>
|
|
596
|
+
Effect.gen(function*() {
|
|
597
|
+
const now = yield* Clock.currentTimeMillis
|
|
598
|
+
return Array.from(schedules.values())
|
|
599
|
+
.filter((schedule) => schedule.nextRunAt <= now)
|
|
600
|
+
.toSorted((a, b) => a.nextRunAt - b.nextRunAt)
|
|
601
|
+
}),
|
|
602
|
+
|
|
603
|
+
advanceSchedule: (key, expectedRunAt, nextRunAt) =>
|
|
604
|
+
Effect.sync(() => {
|
|
605
|
+
const schedule = schedules.get(key)
|
|
606
|
+
if (schedule !== undefined && schedule.nextRunAt === expectedRunAt) {
|
|
607
|
+
schedules.set(key, { ...schedule, nextRunAt })
|
|
608
|
+
}
|
|
609
|
+
}),
|
|
610
|
+
|
|
426
611
|
counts: (queue) =>
|
|
427
612
|
Effect.sync(() => {
|
|
428
613
|
const counts = {
|
|
@@ -430,7 +615,8 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
430
615
|
delayed: 0,
|
|
431
616
|
active: 0,
|
|
432
617
|
completed: 0,
|
|
433
|
-
failed: 0
|
|
618
|
+
failed: 0,
|
|
619
|
+
cancelled: 0
|
|
434
620
|
} satisfies Record<JobState, number>
|
|
435
621
|
for (const job of jobs.values()) {
|
|
436
622
|
if (queue !== undefined && job.queue !== queue) continue
|
|
@@ -447,15 +633,75 @@ export const make: Effect.Effect<Service> = Effect.sync(() => {
|
|
|
447
633
|
return true
|
|
448
634
|
})
|
|
449
635
|
})
|
|
450
|
-
|
|
636
|
+
|
|
637
|
+
return { service, sweepHistory }
|
|
638
|
+
}
|
|
451
639
|
|
|
452
640
|
/**
|
|
453
|
-
*
|
|
454
|
-
|
|
641
|
+
* @since 0.2.0
|
|
642
|
+
*/
|
|
643
|
+
export interface MemoryJobStoreOptions {
|
|
644
|
+
/**
|
|
645
|
+
* Store-level retention ceiling: terminal records (completed, failed,
|
|
646
|
+
* cancelled) older than this are removed by a periodic sweep. Per-job
|
|
647
|
+
* `keep` policies may only be stricter.
|
|
648
|
+
*/
|
|
649
|
+
readonly historyTtl?: Duration.Input | undefined
|
|
650
|
+
/** Sweep cadence (default 1 minute). */
|
|
651
|
+
readonly historySweepInterval?: Duration.Input | undefined
|
|
652
|
+
/**
|
|
653
|
+
* Generator for store-assigned job ids (e.g. `() => \`job_${ulid()}\``).
|
|
654
|
+
* Default: a `j-<n>` counter. See `JobStore.IdGenerator`.
|
|
655
|
+
*/
|
|
656
|
+
readonly idGenerator?: IdGenerator | undefined
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Build a fresh in-memory `JobStore` implementation (no history sweeper —
|
|
661
|
+
* use `makeWith` for `historyTtl` support).
|
|
455
662
|
*
|
|
456
663
|
* @since 0.1.0
|
|
457
664
|
*/
|
|
458
|
-
export const
|
|
665
|
+
export const make: Effect.Effect<Service> = Effect.sync(() => makeStoreUnsafe().service)
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Build a fresh in-memory `JobStore` with options; the history sweeper (when
|
|
669
|
+
* configured) lives in the surrounding `Scope`.
|
|
670
|
+
*
|
|
671
|
+
* @since 0.2.0
|
|
672
|
+
*/
|
|
673
|
+
export const makeWith = (
|
|
674
|
+
options?: MemoryJobStoreOptions | undefined
|
|
675
|
+
): Effect.Effect<Service, never, Scope.Scope> =>
|
|
676
|
+
Effect.gen(function*() {
|
|
677
|
+
const { service, sweepHistory } = makeStoreUnsafe(options)
|
|
678
|
+
if (options?.historyTtl !== undefined) {
|
|
679
|
+
const ttlMs = Duration.toMillis(options.historyTtl)
|
|
680
|
+
const intervalMs = Duration.toMillis(options.historySweepInterval ?? "1 minute")
|
|
681
|
+
yield* Effect.gen(function*() {
|
|
682
|
+
yield* Effect.sleep(intervalMs)
|
|
683
|
+
const now = yield* Clock.currentTimeMillis
|
|
684
|
+
sweepHistory(now, ttlMs)
|
|
685
|
+
}).pipe(Effect.forever, Effect.forkScoped)
|
|
686
|
+
}
|
|
687
|
+
return service
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* A fresh in-memory `JobStore` layer.
|
|
692
|
+
*
|
|
693
|
+
* @since 0.1.0
|
|
694
|
+
*/
|
|
695
|
+
export const layer: Layer.Layer<JobStore> = Layer.effect(JobStore, makeWith())
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* An in-memory layer with options (e.g. `historyTtl`).
|
|
699
|
+
*
|
|
700
|
+
* @since 0.2.0
|
|
701
|
+
*/
|
|
702
|
+
export const layerWith = (
|
|
703
|
+
options?: MemoryJobStoreOptions | undefined
|
|
704
|
+
): Layer.Layer<JobStore> => Layer.effect(JobStore, makeWith(options))
|
|
459
705
|
|
|
460
706
|
/**
|
|
461
707
|
* An in-memory layer for a specific store key.
|
|
@@ -463,5 +709,6 @@ export const layer: Layer.Layer<JobStore> = Layer.effect(JobStore, make)
|
|
|
463
709
|
* @since 0.1.0
|
|
464
710
|
*/
|
|
465
711
|
export const layerFor = <Id>(
|
|
466
|
-
store: Context.Key<Id, Service
|
|
467
|
-
|
|
712
|
+
store: Context.Key<Id, Service>,
|
|
713
|
+
options?: MemoryJobStoreOptions | undefined
|
|
714
|
+
): Layer.Layer<Id> => Layer.effect(store, makeWith(options))
|