effect-mq 0.2.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.
- package/README.md +153 -18
- package/dist/Job.d.ts +51 -1
- package/dist/Job.d.ts.map +1 -1
- package/dist/Job.js +44 -2
- package/dist/Job.js.map +1 -1
- package/dist/JobStore.d.ts +86 -6
- package/dist/JobStore.d.ts.map +1 -1
- package/dist/JobStore.js +27 -1
- package/dist/JobStore.js.map +1 -1
- package/dist/MemoryJobStore.d.ts +6 -5
- package/dist/MemoryJobStore.d.ts.map +1 -1
- package/dist/MemoryJobStore.js +160 -32
- package/dist/MemoryJobStore.js.map +1 -1
- package/dist/Worker.d.ts.map +1 -1
- package/dist/Worker.js +1 -0
- package/dist/Worker.js.map +1 -1
- package/dist/drizzle-postgres/DrizzleJobStore.d.ts +20 -3
- package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -1
- package/dist/drizzle-postgres/DrizzleJobStore.js +352 -84
- package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -1
- package/dist/drizzle-postgres/schema.d.ts +255 -351
- package/dist/drizzle-postgres/schema.d.ts.map +1 -1
- package/dist/drizzle-postgres/schema.js +75 -27
- package/dist/drizzle-postgres/schema.js.map +1 -1
- package/dist/redis/RedisJobStore.d.ts +4 -2
- package/dist/redis/RedisJobStore.d.ts.map +1 -1
- package/dist/redis/RedisJobStore.js +67 -28
- package/dist/redis/RedisJobStore.js.map +1 -1
- package/dist/redis/scripts.d.ts +19 -6
- package/dist/redis/scripts.d.ts.map +1 -1
- package/dist/redis/scripts.js +208 -23
- package/dist/redis/scripts.js.map +1 -1
- package/dist/testing/conformance.d.ts.map +1 -1
- package/dist/testing/conformance.js +172 -7
- package/dist/testing/conformance.js.map +1 -1
- package/package.json +1 -1
- package/src/Job.ts +113 -6
- package/src/JobStore.ts +122 -6
- package/src/MemoryJobStore.ts +184 -44
- package/src/Worker.ts +1 -0
- package/src/drizzle-postgres/DrizzleJobStore.ts +431 -91
- package/src/drizzle-postgres/schema.ts +177 -63
- package/src/redis/RedisJobStore.ts +90 -35
- package/src/redis/scripts.ts +217 -24
- package/src/testing/conformance.ts +246 -7
package/src/MemoryJobStore.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
type ClaimResult,
|
|
17
17
|
type EnqueueRequest,
|
|
18
18
|
type ExtendLocksResult,
|
|
19
|
+
type HistoryTtlByState,
|
|
20
|
+
type HistoryTtlInput,
|
|
19
21
|
type IdGenerator,
|
|
20
22
|
JobId,
|
|
21
23
|
JobNotCancellableError,
|
|
@@ -24,10 +26,13 @@ import {
|
|
|
24
26
|
JobNotRetryableError,
|
|
25
27
|
type JobRecord,
|
|
26
28
|
type JobState,
|
|
29
|
+
type KeepStatePolicy,
|
|
27
30
|
JobStore,
|
|
28
31
|
type ListResult,
|
|
29
32
|
LockLostError,
|
|
30
33
|
JobStoreError,
|
|
34
|
+
normalizeHistoryTtl,
|
|
35
|
+
type TerminalState,
|
|
31
36
|
type QueueName,
|
|
32
37
|
type ScheduleKey,
|
|
33
38
|
type ScheduleRecord,
|
|
@@ -38,17 +43,18 @@ interface MemJob {
|
|
|
38
43
|
readonly id: JobId
|
|
39
44
|
readonly name: string
|
|
40
45
|
readonly queue: QueueName
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
payload: unknown
|
|
47
|
+
metadata: Readonly<Record<string, string>>
|
|
43
48
|
state: JobState
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
priority: number
|
|
50
|
+
attemptsMax: number
|
|
46
51
|
attemptsMade: number
|
|
47
52
|
stalledCount: number
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
backoff: JobRecord["backoff"]
|
|
54
|
+
keep: JobRecord["keep"]
|
|
55
|
+
timeoutMs: number | undefined
|
|
51
56
|
cancelRequested: boolean
|
|
57
|
+
readonly dedupeKey: string | undefined
|
|
52
58
|
runAt: number
|
|
53
59
|
readonly enqueuedAt: number
|
|
54
60
|
processedAt: number | undefined
|
|
@@ -78,6 +84,7 @@ const snapshot = (job: MemJob): JobRecord => ({
|
|
|
78
84
|
keep: job.keep,
|
|
79
85
|
timeoutMs: job.timeoutMs,
|
|
80
86
|
cancelRequested: job.cancelRequested,
|
|
87
|
+
dedupeKey: job.dedupeKey,
|
|
81
88
|
runAt: job.runAt,
|
|
82
89
|
enqueuedAt: job.enqueuedAt,
|
|
83
90
|
processedAt: job.processedAt,
|
|
@@ -98,26 +105,53 @@ const metadataMatches = (
|
|
|
98
105
|
|
|
99
106
|
interface MemoryStore {
|
|
100
107
|
readonly service: Service
|
|
101
|
-
readonly sweepHistory: (now: number,
|
|
108
|
+
readonly sweepHistory: (now: number, ttlByState: HistoryTtlByState) => void
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemoryStore => {
|
|
105
112
|
const jobs = new Map<string, MemJob>()
|
|
106
113
|
const schedules = new Map<ScheduleKey, ScheduleRecord>()
|
|
107
114
|
const paused = new Set<QueueName>()
|
|
115
|
+
// Dedup registry: one entry per (name, key). `expiresAt` is set for
|
|
116
|
+
// ttl/throttle windows; pending-mode entries live as long as their job.
|
|
117
|
+
const dedupes = new Map<string, { jobId: JobId; expiresAt: number | undefined }>()
|
|
118
|
+
const dedupeMapKey = (name: string, key: string) => `${name}\u0000${key}`
|
|
108
119
|
let seq = 0
|
|
109
120
|
let idCounter = 0
|
|
110
121
|
let wakeVersion = 0
|
|
111
|
-
let
|
|
122
|
+
let lastBroadcast = 0
|
|
123
|
+
const lastWake = new Map<QueueName, number>()
|
|
124
|
+
interface Waiter {
|
|
125
|
+
readonly queues: ReadonlySet<QueueName>
|
|
126
|
+
readonly deferred: Deferred.Deferred<void>
|
|
127
|
+
}
|
|
128
|
+
const waiters = new Set<Waiter>()
|
|
129
|
+
const lastWakeFor = (queue: QueueName) => Math.max(lastWake.get(queue) ?? 0, lastBroadcast)
|
|
112
130
|
|
|
113
131
|
// Synchronous on purpose: it is called inside the same synchronous block as
|
|
114
132
|
// the state mutation, so no effect-op boundary (where an interrupt could
|
|
115
|
-
// land) can separate a mutation from its wake-up signal.
|
|
116
|
-
|
|
133
|
+
// land) can separate a mutation from its wake-up signal. A queue targets
|
|
134
|
+
// only waiters watching it; no queue broadcasts (rare maintenance verbs).
|
|
135
|
+
const signalWake = (queue?: QueueName) => {
|
|
117
136
|
wakeVersion += 1
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
if (queue === undefined) {
|
|
138
|
+
lastBroadcast = wakeVersion
|
|
139
|
+
} else {
|
|
140
|
+
lastWake.set(queue, wakeVersion)
|
|
141
|
+
}
|
|
142
|
+
// Snapshot-and-clear BEFORE resolving: doneUnsafe resumes waiting fibers
|
|
143
|
+
// synchronously, and a woken taker that re-parks registers a NEW waiter —
|
|
144
|
+
// resolving inside the live Set iteration would visit it and livelock.
|
|
145
|
+
const toWake: Array<Waiter> = []
|
|
146
|
+
for (const waiter of waiters) {
|
|
147
|
+
if (queue === undefined || waiter.queues.has(queue)) {
|
|
148
|
+
waiters.delete(waiter)
|
|
149
|
+
toWake.push(waiter)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const waiter of toWake) {
|
|
153
|
+
Deferred.doneUnsafe(waiter.deferred, Exit.succeed<void>(void 0))
|
|
154
|
+
}
|
|
121
155
|
}
|
|
122
156
|
|
|
123
157
|
const promoteDue = (now: number) => {
|
|
@@ -148,32 +182,71 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
148
182
|
})
|
|
149
183
|
}
|
|
150
184
|
|
|
185
|
+
// A job leaving the pending states frees its pending-mode dedup entry;
|
|
186
|
+
// live throttle windows deliberately outlast the job.
|
|
187
|
+
const releaseDedupe = (job: MemJob, now: number) => {
|
|
188
|
+
if (job.dedupeKey === undefined) return
|
|
189
|
+
const key = dedupeMapKey(job.name, job.dedupeKey)
|
|
190
|
+
const entry = dedupes.get(key)
|
|
191
|
+
if (
|
|
192
|
+
entry !== undefined && entry.jobId === job.id &&
|
|
193
|
+
(entry.expiresAt === undefined || entry.expiresAt <= now)
|
|
194
|
+
) {
|
|
195
|
+
dedupes.delete(key)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
151
199
|
const markCancelled = (job: MemJob, now: number) => {
|
|
152
200
|
clearLock(job)
|
|
153
201
|
job.cancelRequested = false
|
|
154
202
|
job.state = "cancelled"
|
|
155
203
|
job.finishedAt = now
|
|
156
204
|
recordAttempt(job, "cancelled", now, undefined)
|
|
205
|
+
releaseDedupe(job, now)
|
|
157
206
|
applyKeep(job, now)
|
|
158
207
|
}
|
|
159
208
|
|
|
209
|
+
const keepPolicyFor = (keep: JobRecord["keep"], state: JobState) => {
|
|
210
|
+
if (keep === undefined) return undefined
|
|
211
|
+
const policy = state === "completed"
|
|
212
|
+
? keep.completed
|
|
213
|
+
: state === "failed"
|
|
214
|
+
? keep.failed
|
|
215
|
+
: state === "cancelled"
|
|
216
|
+
? keep.cancelled
|
|
217
|
+
: undefined
|
|
218
|
+
if (policy !== undefined) return policy
|
|
219
|
+
// Records persisted by 0.2.x carry the flat {count, ageMs} shape — honour
|
|
220
|
+
// it as an all-states policy so upgrades keep pruning.
|
|
221
|
+
if (
|
|
222
|
+
keep.completed === undefined && keep.failed === undefined && keep.cancelled === undefined &&
|
|
223
|
+
("count" in keep || "ageMs" in keep)
|
|
224
|
+
) {
|
|
225
|
+
// SAFETY: the flat legacy shape carries KeepStatePolicy fields.
|
|
226
|
+
return keep as KeepStatePolicy
|
|
227
|
+
}
|
|
228
|
+
return undefined
|
|
229
|
+
}
|
|
230
|
+
|
|
160
231
|
// Terminal retention: keep at most `count` and drop older than `ageMs`
|
|
161
|
-
// among terminal jobs sharing this job's name + state
|
|
232
|
+
// among terminal jobs sharing this job's name + state (policies are split
|
|
233
|
+
// per terminal state).
|
|
162
234
|
const applyKeep = (job: MemJob, now: number) => {
|
|
163
|
-
|
|
235
|
+
const policy = keepPolicyFor(job.keep, job.state)
|
|
236
|
+
if (policy === undefined) return
|
|
164
237
|
const peers = Array.from(jobs.values())
|
|
165
238
|
.filter((peer) => peer.name === job.name && peer.state === job.state)
|
|
166
239
|
.toSorted((a, b) => ((b.finishedAt ?? 0) - (a.finishedAt ?? 0)) || (b.seq - a.seq))
|
|
167
240
|
const remove = new Set<string>()
|
|
168
|
-
if (
|
|
241
|
+
if (policy.ageMs !== undefined) {
|
|
169
242
|
for (const peer of peers) {
|
|
170
|
-
if (peer.finishedAt !== undefined && peer.finishedAt <= now -
|
|
243
|
+
if (peer.finishedAt !== undefined && peer.finishedAt <= now - policy.ageMs) {
|
|
171
244
|
remove.add(peer.id)
|
|
172
245
|
}
|
|
173
246
|
}
|
|
174
247
|
}
|
|
175
|
-
if (
|
|
176
|
-
for (const peer of peers.slice(Math.max(0,
|
|
248
|
+
if (policy.count !== undefined) {
|
|
249
|
+
for (const peer of peers.slice(Math.max(0, policy.count))) {
|
|
177
250
|
remove.add(peer.id)
|
|
178
251
|
}
|
|
179
252
|
}
|
|
@@ -182,22 +255,75 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
182
255
|
}
|
|
183
256
|
}
|
|
184
257
|
|
|
185
|
-
const sweepHistory = (now: number,
|
|
258
|
+
const sweepHistory = (now: number, ttlByState: HistoryTtlByState) => {
|
|
186
259
|
for (const job of jobs.values()) {
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
)
|
|
260
|
+
if (!TERMINAL_STATES.has(job.state) || job.finishedAt === undefined) continue
|
|
261
|
+
// SAFETY: TERMINAL_STATES membership was checked above.
|
|
262
|
+
const state = job.state as TerminalState
|
|
263
|
+
const ttl = ttlByState[state]
|
|
264
|
+
const keepAge = keepPolicyFor(job.keep, state)?.ageMs
|
|
265
|
+
// The sweep honours min(per-row keep age, store ceiling) — a quiet job
|
|
266
|
+
// name is pruned on the timer, not only when its group is acked.
|
|
267
|
+
const effective = keepAge !== undefined && (ttl === undefined || keepAge < ttl) ? keepAge : ttl
|
|
268
|
+
if (effective !== undefined && job.finishedAt <= now - effective) {
|
|
192
269
|
jobs.delete(job.id)
|
|
193
270
|
}
|
|
194
271
|
}
|
|
272
|
+
// Dead dedup entries: expired window, or a pointer at a vanished job.
|
|
273
|
+
for (const [key, entry] of dedupes) {
|
|
274
|
+
const alive = entry.expiresAt !== undefined
|
|
275
|
+
? entry.expiresAt > now
|
|
276
|
+
: jobs.has(entry.jobId) && !TERMINAL_STATES.has(jobs.get(entry.jobId)?.state ?? "completed")
|
|
277
|
+
if (!alive) dedupes.delete(key)
|
|
278
|
+
}
|
|
195
279
|
}
|
|
196
280
|
|
|
197
281
|
const service: Service = JobStore.of({
|
|
198
282
|
enqueue: (request: EnqueueRequest) =>
|
|
199
283
|
Effect.gen(function*() {
|
|
200
284
|
const now = yield* Clock.currentTimeMillis
|
|
285
|
+
if (request.id !== undefined && jobs.has(request.id)) {
|
|
286
|
+
return { id: request.id, duplicate: true }
|
|
287
|
+
}
|
|
288
|
+
// The dedup decision tree runs BEFORE id generation, so a
|
|
289
|
+
// deduplicated enqueue never consults the id generator.
|
|
290
|
+
if (request.dedupe !== undefined) {
|
|
291
|
+
const mapKey = dedupeMapKey(request.name, request.dedupe.key)
|
|
292
|
+
const entry = dedupes.get(mapKey)
|
|
293
|
+
if (entry !== undefined) {
|
|
294
|
+
const keyed = jobs.get(entry.jobId)
|
|
295
|
+
const windowLive = entry.expiresAt !== undefined && now < entry.expiresAt
|
|
296
|
+
// Latest-wins while the keyed job is still delayed.
|
|
297
|
+
if (request.dedupe.replace && keyed !== undefined && keyed.state === "delayed") {
|
|
298
|
+
keyed.payload = request.payload
|
|
299
|
+
keyed.metadata = request.metadata
|
|
300
|
+
keyed.priority = request.priority
|
|
301
|
+
keyed.attemptsMax = request.attemptsMax
|
|
302
|
+
keyed.backoff = request.backoff
|
|
303
|
+
keyed.keep = request.keep
|
|
304
|
+
keyed.timeoutMs = request.timeoutMs
|
|
305
|
+
keyed.runAt = now + Math.max(0, request.delayMs)
|
|
306
|
+
// A landed replace re-arms the ttl window.
|
|
307
|
+
if (request.dedupe.ttlMs !== undefined) {
|
|
308
|
+
entry.expiresAt = now + request.dedupe.ttlMs
|
|
309
|
+
}
|
|
310
|
+
signalWake(keyed.queue)
|
|
311
|
+
return { id: keyed.id, duplicate: true }
|
|
312
|
+
}
|
|
313
|
+
if (windowLive) {
|
|
314
|
+
if (request.dedupe.extend && request.dedupe.ttlMs !== undefined) {
|
|
315
|
+
entry.expiresAt = now + request.dedupe.ttlMs
|
|
316
|
+
}
|
|
317
|
+
return { id: entry.jobId, duplicate: true }
|
|
318
|
+
}
|
|
319
|
+
const pending = keyed !== undefined && !TERMINAL_STATES.has(keyed.state)
|
|
320
|
+
if (entry.expiresAt === undefined && pending) {
|
|
321
|
+
return { id: entry.jobId, duplicate: true }
|
|
322
|
+
}
|
|
323
|
+
// Dead entry (expired window / finished job): fall through and
|
|
324
|
+
// let the new job take over the key below.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
201
327
|
let id = request.id
|
|
202
328
|
if (id === undefined) {
|
|
203
329
|
const generate = options?.idGenerator
|
|
@@ -222,8 +348,6 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
222
348
|
})
|
|
223
349
|
}
|
|
224
350
|
}
|
|
225
|
-
} else if (jobs.has(id)) {
|
|
226
|
-
return { id, duplicate: true }
|
|
227
351
|
}
|
|
228
352
|
jobs.set(id, {
|
|
229
353
|
id,
|
|
@@ -240,6 +364,7 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
240
364
|
keep: request.keep,
|
|
241
365
|
timeoutMs: request.timeoutMs,
|
|
242
366
|
cancelRequested: false,
|
|
367
|
+
dedupeKey: request.dedupe?.key,
|
|
243
368
|
runAt: now + Math.max(0, request.delayMs),
|
|
244
369
|
enqueuedAt: now,
|
|
245
370
|
processedAt: undefined,
|
|
@@ -251,7 +376,13 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
251
376
|
lockToken: undefined,
|
|
252
377
|
lockExpiresAt: undefined
|
|
253
378
|
})
|
|
254
|
-
|
|
379
|
+
if (request.dedupe !== undefined) {
|
|
380
|
+
dedupes.set(dedupeMapKey(request.name, request.dedupe.key), {
|
|
381
|
+
jobId: id,
|
|
382
|
+
expiresAt: request.dedupe.ttlMs !== undefined ? now + request.dedupe.ttlMs : undefined
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
signalWake(request.queue)
|
|
255
386
|
return { id, duplicate: false }
|
|
256
387
|
}),
|
|
257
388
|
|
|
@@ -309,6 +440,7 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
309
440
|
job.exit = outcome.exit
|
|
310
441
|
job.finishedAt = now
|
|
311
442
|
recordAttempt(job, "completed", now, outcome.exit)
|
|
443
|
+
releaseDedupe(job, now)
|
|
312
444
|
applyKeep(job, now)
|
|
313
445
|
break
|
|
314
446
|
}
|
|
@@ -318,6 +450,7 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
318
450
|
job.exit = outcome.exit
|
|
319
451
|
job.finishedAt = now
|
|
320
452
|
recordAttempt(job, "failed", now, outcome.exit)
|
|
453
|
+
releaseDedupe(job, now)
|
|
321
454
|
applyKeep(job, now)
|
|
322
455
|
break
|
|
323
456
|
}
|
|
@@ -326,6 +459,7 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
326
459
|
job.state = "cancelled"
|
|
327
460
|
job.finishedAt = now
|
|
328
461
|
recordAttempt(job, "cancelled", now, undefined)
|
|
462
|
+
releaseDedupe(job, now)
|
|
329
463
|
applyKeep(job, now)
|
|
330
464
|
break
|
|
331
465
|
}
|
|
@@ -341,7 +475,7 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
341
475
|
job.runAt = now + Math.max(0, outcome.delayMs)
|
|
342
476
|
job.state = outcome.delayMs > 0 ? "delayed" : "waiting"
|
|
343
477
|
job.seq = ++seq
|
|
344
|
-
signalWake()
|
|
478
|
+
signalWake(job.queue)
|
|
345
479
|
break
|
|
346
480
|
}
|
|
347
481
|
}
|
|
@@ -365,7 +499,7 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
365
499
|
}
|
|
366
500
|
clearLock(job)
|
|
367
501
|
job.state = "waiting"
|
|
368
|
-
signalWake()
|
|
502
|
+
signalWake(job.queue)
|
|
369
503
|
}),
|
|
370
504
|
|
|
371
505
|
extendLocks: (locks, durationMs) =>
|
|
@@ -415,6 +549,7 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
415
549
|
job.state = "failed"
|
|
416
550
|
job.finishedAt = now
|
|
417
551
|
job.failedReason = "job stalled more than allowable limit"
|
|
552
|
+
releaseDedupe(job, now)
|
|
418
553
|
recovered.push({ id: job.id, failed: true })
|
|
419
554
|
} else {
|
|
420
555
|
job.state = "waiting"
|
|
@@ -427,10 +562,14 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
427
562
|
return recovered
|
|
428
563
|
}),
|
|
429
564
|
|
|
430
|
-
awaitWake: (
|
|
565
|
+
awaitWake: (queues, wakeToken) =>
|
|
431
566
|
Effect.suspend(() => {
|
|
432
|
-
if (
|
|
433
|
-
|
|
567
|
+
if (queues.some((queue) => lastWakeFor(queue) > wakeToken)) return Effect.void
|
|
568
|
+
const waiter: Waiter = { queues: new Set(queues), deferred: Deferred.makeUnsafe<void>() }
|
|
569
|
+
waiters.add(waiter)
|
|
570
|
+
return Deferred.await(waiter.deferred).pipe(
|
|
571
|
+
Effect.ensuring(Effect.sync(() => waiters.delete(waiter)))
|
|
572
|
+
)
|
|
434
573
|
}),
|
|
435
574
|
|
|
436
575
|
getJob: (id) =>
|
|
@@ -510,7 +649,7 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
510
649
|
job.processedAt = undefined
|
|
511
650
|
job.runAt = now
|
|
512
651
|
job.seq = ++seq
|
|
513
|
-
signalWake()
|
|
652
|
+
signalWake(job.queue)
|
|
514
653
|
}),
|
|
515
654
|
|
|
516
655
|
cancel: (id) =>
|
|
@@ -548,7 +687,7 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
548
687
|
}
|
|
549
688
|
job.state = "waiting"
|
|
550
689
|
job.runAt = now
|
|
551
|
-
signalWake()
|
|
690
|
+
signalWake(job.queue)
|
|
552
691
|
}),
|
|
553
692
|
|
|
554
693
|
pause: (queue) =>
|
|
@@ -559,7 +698,7 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
559
698
|
resume: (queue) =>
|
|
560
699
|
Effect.sync(() => {
|
|
561
700
|
if (paused.delete(queue)) {
|
|
562
|
-
signalWake()
|
|
701
|
+
signalWake(queue)
|
|
563
702
|
}
|
|
564
703
|
}),
|
|
565
704
|
|
|
@@ -579,7 +718,7 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
579
718
|
schedule.key,
|
|
580
719
|
sameCadence ? { ...schedule, nextRunAt: existing.nextRunAt } : schedule
|
|
581
720
|
)
|
|
582
|
-
signalWake()
|
|
721
|
+
signalWake(schedule.queue)
|
|
583
722
|
}),
|
|
584
723
|
|
|
585
724
|
removeSchedule: (key) => Effect.sync(() => schedules.delete(key)),
|
|
@@ -642,11 +781,12 @@ const makeStoreUnsafe = (options?: MemoryJobStoreOptions | undefined): MemorySto
|
|
|
642
781
|
*/
|
|
643
782
|
export interface MemoryJobStoreOptions {
|
|
644
783
|
/**
|
|
645
|
-
* Store-level retention ceiling: terminal records
|
|
646
|
-
*
|
|
647
|
-
* `
|
|
784
|
+
* Store-level retention ceiling: terminal records older than this are
|
|
785
|
+
* removed by a periodic sweep — one duration for all terminal states or a
|
|
786
|
+
* per-state split (`{ completed: "1 day", failed: "30 days" }`). The sweep
|
|
787
|
+
* also honours stricter per-job `keep.age` rules.
|
|
648
788
|
*/
|
|
649
|
-
readonly historyTtl?:
|
|
789
|
+
readonly historyTtl?: HistoryTtlInput | undefined
|
|
650
790
|
/** Sweep cadence (default 1 minute). */
|
|
651
791
|
readonly historySweepInterval?: Duration.Input | undefined
|
|
652
792
|
/**
|
|
@@ -676,12 +816,12 @@ export const makeWith = (
|
|
|
676
816
|
Effect.gen(function*() {
|
|
677
817
|
const { service, sweepHistory } = makeStoreUnsafe(options)
|
|
678
818
|
if (options?.historyTtl !== undefined) {
|
|
679
|
-
const
|
|
819
|
+
const ttlByState = normalizeHistoryTtl(options.historyTtl)
|
|
680
820
|
const intervalMs = Duration.toMillis(options.historySweepInterval ?? "1 minute")
|
|
681
821
|
yield* Effect.gen(function*() {
|
|
682
822
|
yield* Effect.sleep(intervalMs)
|
|
683
823
|
const now = yield* Clock.currentTimeMillis
|
|
684
|
-
sweepHistory(now,
|
|
824
|
+
sweepHistory(now, ttlByState)
|
|
685
825
|
}).pipe(Effect.forever, Effect.forkScoped)
|
|
686
826
|
}
|
|
687
827
|
return service
|
package/src/Worker.ts
CHANGED