@boringnode/queue 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -14
- package/build/chunk-6IO4P6RB.js +145 -0
- package/build/chunk-6IO4P6RB.js.map +1 -0
- package/build/{chunk-VRXHCWNK.js → chunk-AHUVTAI7.js} +220 -15
- package/build/chunk-AHUVTAI7.js.map +1 -0
- package/build/chunk-S37X3CBO.js +500 -0
- package/build/chunk-S37X3CBO.js.map +1 -0
- package/build/index.d.ts +10 -2
- package/build/index.js +187 -31
- package/build/index.js.map +1 -1
- package/build/{job-Z5fBSzRX.d.ts → job-C4oyCVxR.d.ts} +131 -10
- package/build/src/contracts/adapter.d.ts +1 -1
- package/build/src/drivers/fake_adapter.d.ts +7 -6
- package/build/src/drivers/fake_adapter.js +1 -1
- package/build/src/drivers/knex_adapter.d.ts +6 -5
- package/build/src/drivers/knex_adapter.js +112 -0
- package/build/src/drivers/knex_adapter.js.map +1 -1
- package/build/src/drivers/redis_adapter.d.ts +6 -5
- package/build/src/drivers/redis_adapter.js +134 -368
- package/build/src/drivers/redis_adapter.js.map +1 -1
- package/build/src/drivers/redis_job_storage.d.ts +17 -0
- package/build/src/drivers/redis_job_storage.js +14 -0
- package/build/src/drivers/redis_job_storage.js.map +1 -0
- package/build/src/drivers/redis_scripts.d.ts +87 -0
- package/build/src/drivers/redis_scripts.js +29 -0
- package/build/src/drivers/redis_scripts.js.map +1 -0
- package/build/src/drivers/sync_adapter.d.ts +2 -1
- package/build/src/drivers/sync_adapter.js +7 -1
- package/build/src/drivers/sync_adapter.js.map +1 -1
- package/build/src/otel.d.ts +2 -2
- package/build/src/types/index.d.ts +1 -1
- package/build/src/types/main.d.ts +1 -1
- package/build/src/types/tracing_channels.d.ts +7 -1
- package/package.json +17 -17
- package/build/chunk-VRXHCWNK.js.map +0 -1
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ npm install @boringnode/queue
|
|
|
26
26
|
- **Priority Queues**: Process high-priority jobs first
|
|
27
27
|
- **Bulk Dispatch**: Efficiently dispatch thousands of jobs at once
|
|
28
28
|
- **Job Grouping**: Organize related jobs for monitoring
|
|
29
|
+
- **Job Deduplication**: Prevent duplicate jobs with custom IDs
|
|
29
30
|
- **Retry with Backoff**: Exponential, linear, or fixed backoff strategies
|
|
30
31
|
- **Job Timeout**: Fail or retry jobs that exceed a time limit
|
|
31
32
|
- **Job History**: Retain completed/failed jobs for debugging
|
|
@@ -131,6 +132,85 @@ await SendEmailJob.dispatchMany(recipients).group('newsletter-jan-2025')
|
|
|
131
132
|
|
|
132
133
|
The `groupId` is stored with job data and accessible via `job.data.groupId`.
|
|
133
134
|
|
|
135
|
+
## Job Deduplication
|
|
136
|
+
|
|
137
|
+
Prevent the same job from being pushed multiple times. Four modes, all via `.dedup()`:
|
|
138
|
+
|
|
139
|
+
### Simple (skip while job exists)
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// First dispatch - job is created
|
|
143
|
+
await SendInvoiceJob.dispatch({ orderId: 123 }).dedup({ id: 'order-123' }).run()
|
|
144
|
+
|
|
145
|
+
// Second dispatch with same dedup ID - silently skipped
|
|
146
|
+
await SendInvoiceJob.dispatch({ orderId: 123 }).dedup({ id: 'order-123' }).run()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Throttle (skip within TTL window)
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// Within 5s, duplicates are skipped. After 5s, a new job is created.
|
|
153
|
+
await SendEmailJob.dispatch({ to: 'user@example.com' })
|
|
154
|
+
.dedup({ id: 'welcome-123', ttl: '5s' })
|
|
155
|
+
.run()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Extend (reset TTL on duplicate)
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// Each duplicate push resets the TTL timer.
|
|
162
|
+
await RateLimitJob.dispatch({ userId: 42 }).dedup({ id: 'rate-42', ttl: '1m', extend: true }).run()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Debounce (replace payload + reset TTL)
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// Within the 2s window, the latest payload overwrites the previous pending job.
|
|
169
|
+
await SaveDraftJob.dispatch({ content: 'latest draft' })
|
|
170
|
+
.dedup({ id: 'draft-42', ttl: '2s', replace: true, extend: true })
|
|
171
|
+
.run()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Inspecting the outcome
|
|
175
|
+
|
|
176
|
+
`DispatchResult` tells you what happened:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const { jobId, deduped } = await SaveDraftJob.dispatch({ content: '...' })
|
|
180
|
+
.dedup({ id: 'draft-42', ttl: '2s', replace: true })
|
|
181
|
+
.run()
|
|
182
|
+
|
|
183
|
+
// deduped: 'added' | 'skipped' | 'replaced' | 'extended'
|
|
184
|
+
// jobId: the UUID of the job (the existing one when deduped)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### How it works
|
|
188
|
+
|
|
189
|
+
- The dedup ID is automatically prefixed with the job name (`SendInvoiceJob::order-123`), so different job types can reuse the same key.
|
|
190
|
+
- The user-supplied `id` must be ≤ 400 characters, and the combined `<jobName>::<id>` key must be ≤ 510 characters (constrained by the Knex storage column). Both limits are validated at `.dedup()` time.
|
|
191
|
+
- `ttl` accepts a Duration (`'5s'`, `'1m'`) or milliseconds, and must be **positive** when provided. Use `0` or omit `ttl` if you want no expiry — `ttl: 0` is rejected to avoid an ambiguous "expired immediately vs no-expiry" interpretation across engines.
|
|
192
|
+
- `extend` and `replace` **require** `ttl` — calling them without `ttl` throws.
|
|
193
|
+
- `replace` only applies to jobs in `pending` or `delayed` state. Jobs that are active (executing) or retained in history (`completed`/`failed` with retention) are left alone; the dispatch returns `{ deduped: 'skipped' }`.
|
|
194
|
+
- `replace` swaps the **payload only** — priority, queue, delay, groupId, and stored dedup options of the existing job are retained. To change those, use a different dedup id or wait for the TTL to expire.
|
|
195
|
+
- `extend` resets the TTL clock but never changes the window length. The window length is fixed to the `ttl` from the first dispatch that created the dedup slot. Later dispatches that pass a different `ttl` only reset the clock; their `ttl` value is ignored. To resize the window, let the slot expire and start over with a new dispatch.
|
|
196
|
+
- `extend` works in **all states** — even when the existing job is `active` (executing) or retained in history. Unlike `replace` (which is no-op on non-replaceable states), `extend` always refreshes the dedup TTL window. Use this when you want the dedup slot to keep blocking new dispatches for the lifetime of a long-running job.
|
|
197
|
+
- `extend` requires the **first** dispatch to have set a `ttl`. If the slot was created without a `ttl`, later `extend` dispatches have no window to refresh and return `{ deduped: 'skipped' }` instead of `'extended'`.
|
|
198
|
+
- `retryJob` does not touch the dedup entry — a retried job continues to occupy the dedup slot. TTL runs on wall-clock time, so long-running retries may outlive the TTL window. Use a generous TTL or no TTL if retries must stay deduped.
|
|
199
|
+
- Atomic and race-free:
|
|
200
|
+
- **Redis**: a single Lua script per dispatch performs the dedup-key lookup, state check (pending/delayed ZSCORE), payload swap, and TTL refresh atomically.
|
|
201
|
+
- **Knex**: transactional `SELECT ... FOR UPDATE` + insert/update inside a transaction. A nested savepoint catches unique-constraint violations under concurrent inserts and returns `{ deduped: 'skipped' }` pointing at the winner.
|
|
202
|
+
- **SyncAdapter**: executes inline, no dedup support.
|
|
203
|
+
|
|
204
|
+
### Caveats
|
|
205
|
+
|
|
206
|
+
- Without `.dedup()`, jobs use auto-generated UUIDs and are never deduplicated.
|
|
207
|
+
- The **Sync adapter** ignores `.dedup()` entirely — every dispatch executes inline and `deduped` is always `undefined` on the result. Use Redis or Knex if you need real deduplication.
|
|
208
|
+
- `.dedup()` is only available on single dispatch. `dispatchMany` / `pushManyOn` reject jobs with a `dedup` field.
|
|
209
|
+
- Scheduled jobs (`.schedule()`) do not support dedup — each cron/interval fire is an independent dispatch.
|
|
210
|
+
- With no `ttl`, dedup persists until the job is removed (completed/failed without retention). When retention keeps the record, re-dispatch stays blocked until the record is pruned.
|
|
211
|
+
- With `ttl`, dedup expires after the window — a new job (new UUID) is created. The old job still runs.
|
|
212
|
+
- Knex MySQL concurrent race: MySQL does not support partial unique indexes, so two `pushOn` calls with the same dedup id firing at the exact same instant can both succeed. Serialize at the app layer if strict guarantees are required, or use Postgres / SQLite / Redis (all of which serialize correctly via the partial unique index or Lua atomicity).
|
|
213
|
+
|
|
134
214
|
## Job History & Retention
|
|
135
215
|
|
|
136
216
|
Keep completed and failed jobs for debugging:
|
|
@@ -536,7 +616,7 @@ import * as boringqueue from '@boringnode/queue'
|
|
|
536
616
|
|
|
537
617
|
const instrumentation = new QueueInstrumentation({
|
|
538
618
|
messagingSystem: 'boringqueue', // default
|
|
539
|
-
executionSpanLinkMode: 'link',
|
|
619
|
+
executionSpanLinkMode: 'link', // or 'parent'
|
|
540
620
|
})
|
|
541
621
|
|
|
542
622
|
instrumentation.enable()
|
|
@@ -549,19 +629,19 @@ The instrumentation patches `QueueManager.init()` to automatically inject its wr
|
|
|
549
629
|
|
|
550
630
|
The instrumentation uses standard [OTel messaging semantic conventions](https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/) where they map cleanly, plus a few queue-specific custom attributes.
|
|
551
631
|
|
|
552
|
-
| Attribute | Kind | Description
|
|
553
|
-
| ------------------------------- | ------- |
|
|
554
|
-
| `messaging.system` | Semconv | `'boringqueue'` (configurable)
|
|
555
|
-
| `messaging.operation.name` | Semconv | `'publish'` or `'process'`
|
|
556
|
-
| `messaging.destination.name` | Semconv | Queue name
|
|
557
|
-
| `messaging.message.id` | Semconv | Job ID for single-message spans
|
|
558
|
-
| `messaging.batch.message_count` | Semconv | Number of jobs in a batch dispatch
|
|
559
|
-
| `messaging.message.retry.count` | Custom | Retry count (0-based) for a job attempt
|
|
560
|
-
| `messaging.job.name` | Custom | Job class name (e.g. `SendEmailJob`)
|
|
561
|
-
| `messaging.job.status` | Custom | `'completed'`, `'failed'`, or `'retrying'`
|
|
562
|
-
| `messaging.job.group_id` | Custom | Queue-specific group identifier
|
|
563
|
-
| `messaging.job.priority` | Custom | Queue-specific job priority
|
|
564
|
-
| `messaging.job.delay_ms` | Custom | Delay before the job becomes available
|
|
632
|
+
| Attribute | Kind | Description |
|
|
633
|
+
| ------------------------------- | ------- | --------------------------------------------- |
|
|
634
|
+
| `messaging.system` | Semconv | `'boringqueue'` (configurable) |
|
|
635
|
+
| `messaging.operation.name` | Semconv | `'publish'` or `'process'` |
|
|
636
|
+
| `messaging.destination.name` | Semconv | Queue name |
|
|
637
|
+
| `messaging.message.id` | Semconv | Job ID for single-message spans |
|
|
638
|
+
| `messaging.batch.message_count` | Semconv | Number of jobs in a batch dispatch |
|
|
639
|
+
| `messaging.message.retry.count` | Custom | Retry count (0-based) for a job attempt |
|
|
640
|
+
| `messaging.job.name` | Custom | Job class name (e.g. `SendEmailJob`) |
|
|
641
|
+
| `messaging.job.status` | Custom | `'completed'`, `'failed'`, or `'retrying'` |
|
|
642
|
+
| `messaging.job.group_id` | Custom | Queue-specific group identifier |
|
|
643
|
+
| `messaging.job.priority` | Custom | Queue-specific job priority |
|
|
644
|
+
| `messaging.job.delay_ms` | Custom | Delay before the job becomes available |
|
|
565
645
|
| `messaging.job.queue_time_ms` | Custom | Time spent waiting in queue before processing |
|
|
566
646
|
|
|
567
647
|
### Trace Context Propagation
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/drivers/redis_job_storage.ts
|
|
2
|
+
function hydrateRedisJob(data, overlay) {
|
|
3
|
+
const jobData = JSON.parse(data);
|
|
4
|
+
const parsedOverlay = overlay ? JSON.parse(overlay) : void 0;
|
|
5
|
+
if (parsedOverlay?.payloadUndefined) {
|
|
6
|
+
jobData.payload = void 0;
|
|
7
|
+
} else if (parsedOverlay?.payload !== void 0) {
|
|
8
|
+
jobData.payload = JSON.parse(parsedOverlay.payload);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
...jobData,
|
|
12
|
+
...parsedOverlay?.attempts !== void 0 ? { attempts: parsedOverlay.attempts } : {},
|
|
13
|
+
...parsedOverlay?.stalledCount !== void 0 ? { stalledCount: parsedOverlay.stalledCount } : {}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function encodeRedisJobPayloadOverlay(payload) {
|
|
17
|
+
const encoded = JSON.stringify(payload);
|
|
18
|
+
return encoded === void 0 ? ["", "1"] : [encoded, "0"];
|
|
19
|
+
}
|
|
20
|
+
var REDIS_JOB_STORAGE_LUA = `
|
|
21
|
+
local function read_job_overlay(overlay_key, job_id)
|
|
22
|
+
local overlay_json = redis.call('HGET', overlay_key, job_id)
|
|
23
|
+
if overlay_json then
|
|
24
|
+
return cjson.decode(overlay_json)
|
|
25
|
+
end
|
|
26
|
+
return {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
local function write_job_overlay(overlay_key, job_id, overlay)
|
|
30
|
+
local has_overlay = false
|
|
31
|
+
for _ in pairs(overlay) do
|
|
32
|
+
has_overlay = true
|
|
33
|
+
break
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if has_overlay then
|
|
37
|
+
redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))
|
|
38
|
+
else
|
|
39
|
+
redis.call('HDEL', overlay_key, job_id)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
local function store_job_data(data_key, overlay_key, job_id, job_data)
|
|
44
|
+
redis.call('HDEL', overlay_key, job_id)
|
|
45
|
+
redis.call('HSET', data_key, job_id, job_data)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
local function delete_job_data(data_key, overlay_key, job_id)
|
|
49
|
+
redis.call('HDEL', data_key, job_id)
|
|
50
|
+
redis.call('HDEL', overlay_key, job_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
local function delete_jobs_data(data_key, overlay_key, job_ids)
|
|
54
|
+
if #job_ids > 0 then
|
|
55
|
+
redis.call('HDEL', data_key, unpack(job_ids))
|
|
56
|
+
redis.call('HDEL', overlay_key, unpack(job_ids))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
local function encode_job_result(job_data, overlay_key, job_id, result)
|
|
61
|
+
local encoded = result or {}
|
|
62
|
+
encoded.data = job_data
|
|
63
|
+
|
|
64
|
+
local overlay_json = redis.call('HGET', overlay_key, job_id)
|
|
65
|
+
if overlay_json then
|
|
66
|
+
encoded.overlay = overlay_json
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
return cjson.encode(encoded)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)
|
|
73
|
+
if payload_is_undefined == 1 then
|
|
74
|
+
overlay.payload = nil
|
|
75
|
+
overlay.payloadUndefined = true
|
|
76
|
+
else
|
|
77
|
+
overlay.payload = payload_data
|
|
78
|
+
overlay.payloadUndefined = nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
`;
|
|
82
|
+
var REDIS_DEDUP_LUA = `
|
|
83
|
+
local function resolve_dedup_existing_job(
|
|
84
|
+
data_key,
|
|
85
|
+
state_key_a,
|
|
86
|
+
state_key_b,
|
|
87
|
+
overlay_key,
|
|
88
|
+
dedup_key,
|
|
89
|
+
extend,
|
|
90
|
+
replace,
|
|
91
|
+
payload_data,
|
|
92
|
+
payload_is_undefined
|
|
93
|
+
)
|
|
94
|
+
local existing = redis.call('GET', dedup_key)
|
|
95
|
+
if not existing then
|
|
96
|
+
return nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
local existing_data = redis.call('HGET', data_key, existing)
|
|
100
|
+
if not existing_data then
|
|
101
|
+
return nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
local in_state_a = redis.call('ZSCORE', state_key_a, existing)
|
|
105
|
+
local in_state_b = redis.call('ZSCORE', state_key_b, existing)
|
|
106
|
+
local replaceable = in_state_a or in_state_b
|
|
107
|
+
local ok, existing_job = pcall(cjson.decode, existing_data)
|
|
108
|
+
local original_ttl = nil
|
|
109
|
+
|
|
110
|
+
if ok and type(existing_job) == 'table' and existing_job.dedup then
|
|
111
|
+
original_ttl = tonumber(existing_job.dedup.ttl)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if replace == 1 and replaceable then
|
|
115
|
+
if ok and existing_job then
|
|
116
|
+
local overlay = read_job_overlay(overlay_key, existing)
|
|
117
|
+
apply_payload_overlay(overlay, payload_data, payload_is_undefined)
|
|
118
|
+
write_job_overlay(overlay_key, existing, overlay)
|
|
119
|
+
|
|
120
|
+
if extend == 1 and original_ttl and original_ttl > 0 then
|
|
121
|
+
redis.call('PEXPIRE', dedup_key, original_ttl)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
return {'replaced', existing}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
return {'skipped', existing}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if extend == 1 and original_ttl and original_ttl > 0 then
|
|
131
|
+
redis.call('PEXPIRE', dedup_key, original_ttl)
|
|
132
|
+
return {'extended', existing}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
return {'skipped', existing}
|
|
136
|
+
end
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
hydrateRedisJob,
|
|
141
|
+
encodeRedisJobPayloadOverlay,
|
|
142
|
+
REDIS_JOB_STORAGE_LUA,
|
|
143
|
+
REDIS_DEDUP_LUA
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=chunk-6IO4P6RB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/drivers/redis_job_storage.ts"],"sourcesContent":["import type { JobData } from '../types/main.js'\n\n/**\n * Redis stores the original job JSON as an opaque payload in the data hash.\n * Lua scripts write only mutable Redis-side overrides in this overlay so\n * legacy jobs without overlays keep decoding from their original data.\n */\nexport type RedisJobOverlay = Partial<Pick<JobData, 'attempts' | 'stalledCount'>> & {\n payload?: string\n payloadUndefined?: boolean\n}\n\nexport function hydrateRedisJob(data: string, overlay?: string | null | false): JobData {\n const jobData = JSON.parse(data) as JobData\n const parsedOverlay = overlay ? (JSON.parse(overlay) as RedisJobOverlay) : undefined\n\n if (parsedOverlay?.payloadUndefined) {\n jobData.payload = undefined\n } else if (parsedOverlay?.payload !== undefined) {\n jobData.payload = JSON.parse(parsedOverlay.payload)\n }\n\n return {\n ...jobData,\n ...(parsedOverlay?.attempts !== undefined ? { attempts: parsedOverlay.attempts } : {}),\n ...(parsedOverlay?.stalledCount !== undefined\n ? { stalledCount: parsedOverlay.stalledCount }\n : {}),\n }\n}\n\nexport function encodeRedisJobPayloadOverlay(payload: JobData['payload']): [string, string] {\n const encoded = JSON.stringify(payload)\n\n return encoded === undefined ? ['', '1'] : [encoded, '0']\n}\n\nexport const REDIS_JOB_STORAGE_LUA = `\n local function read_job_overlay(overlay_key, job_id)\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n return cjson.decode(overlay_json)\n end\n return {}\n end\n\n local function write_job_overlay(overlay_key, job_id, overlay)\n local has_overlay = false\n for _ in pairs(overlay) do\n has_overlay = true\n break\n end\n\n if has_overlay then\n redis.call('HSET', overlay_key, job_id, cjson.encode(overlay))\n else\n redis.call('HDEL', overlay_key, job_id)\n end\n end\n\n local function store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('HDEL', overlay_key, job_id)\n redis.call('HSET', data_key, job_id, job_data)\n end\n\n local function delete_job_data(data_key, overlay_key, job_id)\n redis.call('HDEL', data_key, job_id)\n redis.call('HDEL', overlay_key, job_id)\n end\n\n local function delete_jobs_data(data_key, overlay_key, job_ids)\n if #job_ids > 0 then\n redis.call('HDEL', data_key, unpack(job_ids))\n redis.call('HDEL', overlay_key, unpack(job_ids))\n end\n end\n\n local function encode_job_result(job_data, overlay_key, job_id, result)\n local encoded = result or {}\n encoded.data = job_data\n\n local overlay_json = redis.call('HGET', overlay_key, job_id)\n if overlay_json then\n encoded.overlay = overlay_json\n end\n\n return cjson.encode(encoded)\n end\n\n local function apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n if payload_is_undefined == 1 then\n overlay.payload = nil\n overlay.payloadUndefined = true\n else\n overlay.payload = payload_data\n overlay.payloadUndefined = nil\n end\n end\n`\n\nexport const REDIS_DEDUP_LUA = `\n local function resolve_dedup_existing_job(\n data_key,\n state_key_a,\n state_key_b,\n overlay_key,\n dedup_key,\n extend,\n replace,\n payload_data,\n payload_is_undefined\n )\n local existing = redis.call('GET', dedup_key)\n if not existing then\n return nil\n end\n\n local existing_data = redis.call('HGET', data_key, existing)\n if not existing_data then\n return nil\n end\n\n local in_state_a = redis.call('ZSCORE', state_key_a, existing)\n local in_state_b = redis.call('ZSCORE', state_key_b, existing)\n local replaceable = in_state_a or in_state_b\n local ok, existing_job = pcall(cjson.decode, existing_data)\n local original_ttl = nil\n\n if ok and type(existing_job) == 'table' and existing_job.dedup then\n original_ttl = tonumber(existing_job.dedup.ttl)\n end\n\n if replace == 1 and replaceable then\n if ok and existing_job then\n local overlay = read_job_overlay(overlay_key, existing)\n apply_payload_overlay(overlay, payload_data, payload_is_undefined)\n write_job_overlay(overlay_key, existing, overlay)\n\n if extend == 1 and original_ttl and original_ttl > 0 then\n redis.call('PEXPIRE', dedup_key, original_ttl)\n end\n\n return {'replaced', existing}\n end\n\n return {'skipped', existing}\n end\n\n if extend == 1 and original_ttl and original_ttl > 0 then\n redis.call('PEXPIRE', dedup_key, original_ttl)\n return {'extended', existing}\n end\n\n return {'skipped', existing}\n end\n`\n"],"mappings":";AAYO,SAAS,gBAAgB,MAAc,SAA0C;AACtF,QAAM,UAAU,KAAK,MAAM,IAAI;AAC/B,QAAM,gBAAgB,UAAW,KAAK,MAAM,OAAO,IAAwB;AAE3E,MAAI,eAAe,kBAAkB;AACnC,YAAQ,UAAU;AAAA,EACpB,WAAW,eAAe,YAAY,QAAW;AAC/C,YAAQ,UAAU,KAAK,MAAM,cAAc,OAAO;AAAA,EACpD;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAI,eAAe,aAAa,SAAY,EAAE,UAAU,cAAc,SAAS,IAAI,CAAC;AAAA,IACpF,GAAI,eAAe,iBAAiB,SAChC,EAAE,cAAc,cAAc,aAAa,IAC3C,CAAC;AAAA,EACP;AACF;AAEO,SAAS,6BAA6B,SAA+C;AAC1F,QAAM,UAAU,KAAK,UAAU,OAAO;AAEtC,SAAO,YAAY,SAAY,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,GAAG;AAC1D;AAEO,IAAM,wBAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+D9B,IAAM,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;","names":[]}
|
|
@@ -603,6 +603,7 @@ var JobDispatcher = class {
|
|
|
603
603
|
#delay;
|
|
604
604
|
#priority;
|
|
605
605
|
#groupId;
|
|
606
|
+
#dedup;
|
|
606
607
|
/**
|
|
607
608
|
* Create a new job dispatcher.
|
|
608
609
|
*
|
|
@@ -694,6 +695,76 @@ var JobDispatcher = class {
|
|
|
694
695
|
this.#groupId = groupId;
|
|
695
696
|
return this;
|
|
696
697
|
}
|
|
698
|
+
/**
|
|
699
|
+
* Configure deduplication for this job.
|
|
700
|
+
*
|
|
701
|
+
* Modes:
|
|
702
|
+
* - **Simple** (`{ id }`): skip duplicates while the job exists.
|
|
703
|
+
* - **Throttle** (`{ id, ttl }`): skip duplicates within a TTL window.
|
|
704
|
+
* - **Extend** (`{ id, ttl, extend: true }`): reset the TTL clock on each duplicate.
|
|
705
|
+
* The window length stays at the original ttl from the first dispatch.
|
|
706
|
+
* - **Replace** (`{ id, ttl, replace: true }`): swap the payload of the existing
|
|
707
|
+
* pending/delayed job on duplicate within TTL. Active jobs and retained
|
|
708
|
+
* completed/failed jobs return `'skipped'`. Only `payload` changes —
|
|
709
|
+
* priority/queue/delay/groupId are preserved.
|
|
710
|
+
* - **Debounce** (`{ id, ttl, replace: true, extend: true }`): replace + reset TTL.
|
|
711
|
+
*
|
|
712
|
+
* The id is automatically prefixed with the job name to prevent collisions
|
|
713
|
+
* between different job types.
|
|
714
|
+
*
|
|
715
|
+
* @param options.id - Unique deduplication key
|
|
716
|
+
* @param options.ttl - TTL as Duration ('5s', 5000). Required for extend/replace.
|
|
717
|
+
* @param options.extend - Reset the TTL clock on duplicate within window. Window
|
|
718
|
+
* length stays at the original ttl; this option's `ttl` arg is ignored on extend.
|
|
719
|
+
* @param options.replace - Swap payload of existing pending/delayed job within
|
|
720
|
+
* window. Active and retained jobs are not modified.
|
|
721
|
+
*
|
|
722
|
+
* @example
|
|
723
|
+
* ```typescript
|
|
724
|
+
* // Simple dedup
|
|
725
|
+
* await SendInvoiceJob.dispatch({ orderId: 123 })
|
|
726
|
+
* .dedup({ id: 'order-123' })
|
|
727
|
+
*
|
|
728
|
+
* // Throttle: 5 second window
|
|
729
|
+
* await SendEmailJob.dispatch({ to: 'x' })
|
|
730
|
+
* .dedup({ id: 'welcome', ttl: '5s' })
|
|
731
|
+
*
|
|
732
|
+
* // Debounce: replace payload within window
|
|
733
|
+
* await SaveDraftJob.dispatch({ content: 'latest' })
|
|
734
|
+
* .dedup({ id: 'draft-42', ttl: '2s', replace: true, extend: true })
|
|
735
|
+
* ```
|
|
736
|
+
*/
|
|
737
|
+
dedup(options) {
|
|
738
|
+
if (!options.id) {
|
|
739
|
+
throw new Error("Dedup ID must be a non-empty string");
|
|
740
|
+
}
|
|
741
|
+
if (options.id.length > 400) {
|
|
742
|
+
throw new Error("Dedup ID must be 400 characters or less");
|
|
743
|
+
}
|
|
744
|
+
const prefixedLength = this.#name.length + 2 + options.id.length;
|
|
745
|
+
if (prefixedLength > 510) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
`Dedup ID combined with job name exceeds 510 characters (got ${prefixedLength}). Shorten either the job name or the dedup id.`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
if ((options.extend || options.replace) && options.ttl === void 0) {
|
|
751
|
+
throw new Error("dedup.ttl is required when extend or replace is set");
|
|
752
|
+
}
|
|
753
|
+
let parsedTtl;
|
|
754
|
+
if (options.ttl !== void 0) {
|
|
755
|
+
parsedTtl = parse(options.ttl);
|
|
756
|
+
if (!Number.isFinite(parsedTtl) || parsedTtl <= 0) {
|
|
757
|
+
throw new Error("dedup.ttl must be a positive duration");
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
this.#dedup = {
|
|
761
|
+
id: options.id,
|
|
762
|
+
ttl: parsedTtl,
|
|
763
|
+
extend: options.extend,
|
|
764
|
+
replace: options.replace
|
|
765
|
+
};
|
|
766
|
+
return this;
|
|
767
|
+
}
|
|
697
768
|
/**
|
|
698
769
|
* Use a specific adapter for this job.
|
|
699
770
|
*
|
|
@@ -726,6 +797,7 @@ var JobDispatcher = class {
|
|
|
726
797
|
*/
|
|
727
798
|
async run() {
|
|
728
799
|
const id = randomUUID();
|
|
800
|
+
const dedupId = this.#dedup ? `${this.#name}::${this.#dedup.id}` : void 0;
|
|
729
801
|
debug_default("dispatching job %s with id %s using payload %s", this.#name, id, this.#payload);
|
|
730
802
|
const adapter = this.#getAdapterInstance();
|
|
731
803
|
const wrapInternal = QueueManager.getInternalOperationWrapper();
|
|
@@ -737,16 +809,31 @@ var JobDispatcher = class {
|
|
|
737
809
|
attempts: 0,
|
|
738
810
|
priority: this.#priority,
|
|
739
811
|
groupId: this.#groupId,
|
|
740
|
-
createdAt: Date.now()
|
|
812
|
+
createdAt: Date.now(),
|
|
813
|
+
...dedupId ? {
|
|
814
|
+
dedup: {
|
|
815
|
+
id: dedupId,
|
|
816
|
+
ttl: this.#dedup.ttl,
|
|
817
|
+
extend: this.#dedup.extend,
|
|
818
|
+
replace: this.#dedup.replace
|
|
819
|
+
}
|
|
820
|
+
} : {}
|
|
741
821
|
};
|
|
742
822
|
const message = { jobs: [jobData], queue: this.#queue, delay: parsedDelay };
|
|
823
|
+
let pushResult;
|
|
743
824
|
await dispatchChannel.tracePromise(async () => {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
825
|
+
const result = parsedDelay !== void 0 ? await wrapInternal(() => adapter.pushLaterOn(this.#queue, jobData, parsedDelay)) : await wrapInternal(() => adapter.pushOn(this.#queue, jobData));
|
|
826
|
+
if (result && typeof result === "object" && "outcome" in result) {
|
|
827
|
+
pushResult = { outcome: result.outcome, jobId: result.jobId };
|
|
828
|
+
message.dedupOutcome = result.outcome;
|
|
748
829
|
}
|
|
749
830
|
}, message);
|
|
831
|
+
if (pushResult && this.#dedup) {
|
|
832
|
+
return {
|
|
833
|
+
jobId: pushResult.jobId,
|
|
834
|
+
deduped: pushResult.outcome
|
|
835
|
+
};
|
|
836
|
+
}
|
|
750
837
|
return { jobId: id };
|
|
751
838
|
}
|
|
752
839
|
/**
|
|
@@ -1297,7 +1384,9 @@ var FakeAdapter = class {
|
|
|
1297
1384
|
#pendingTimeouts = /* @__PURE__ */ new Set();
|
|
1298
1385
|
#schedules = /* @__PURE__ */ new Map();
|
|
1299
1386
|
#pushedJobs = [];
|
|
1387
|
+
#dedupIndex = /* @__PURE__ */ new Map();
|
|
1300
1388
|
#onDispose;
|
|
1389
|
+
#workerId = "";
|
|
1301
1390
|
/**
|
|
1302
1391
|
* Set the function to call when the fake is disposed
|
|
1303
1392
|
*/
|
|
@@ -1308,7 +1397,8 @@ var FakeAdapter = class {
|
|
|
1308
1397
|
[Symbol.dispose]() {
|
|
1309
1398
|
this.#onDispose?.();
|
|
1310
1399
|
}
|
|
1311
|
-
setWorkerId(
|
|
1400
|
+
setWorkerId(workerId) {
|
|
1401
|
+
this.#workerId = workerId;
|
|
1312
1402
|
}
|
|
1313
1403
|
getPushedJobs() {
|
|
1314
1404
|
return [...this.#pushedJobs];
|
|
@@ -1334,6 +1424,7 @@ var FakeAdapter = class {
|
|
|
1334
1424
|
this.#failedJobs.clear();
|
|
1335
1425
|
this.#schedules.clear();
|
|
1336
1426
|
this.#pushedJobs = [];
|
|
1427
|
+
this.#dedupIndex.clear();
|
|
1337
1428
|
}
|
|
1338
1429
|
assertPushed(matcher, query) {
|
|
1339
1430
|
const record = this.findPushed(matcher, query);
|
|
@@ -1366,21 +1457,33 @@ var FakeAdapter = class {
|
|
|
1366
1457
|
return this.pushOn("default", jobData);
|
|
1367
1458
|
}
|
|
1368
1459
|
async pushOn(queue, jobData) {
|
|
1460
|
+
const deduped = await this.#applyDedup(queue, jobData);
|
|
1461
|
+
if (deduped) return deduped;
|
|
1369
1462
|
this.#recordPush(queue, jobData);
|
|
1370
1463
|
this.#enqueue(queue, jobData);
|
|
1464
|
+
if (jobData.dedup) {
|
|
1465
|
+
return { outcome: "added", jobId: jobData.id };
|
|
1466
|
+
}
|
|
1371
1467
|
}
|
|
1372
1468
|
async pushLater(jobData, delay) {
|
|
1373
1469
|
return this.pushLaterOn("default", jobData, delay);
|
|
1374
1470
|
}
|
|
1375
|
-
pushLaterOn(queue, jobData, delay) {
|
|
1471
|
+
async pushLaterOn(queue, jobData, delay) {
|
|
1472
|
+
const deduped = await this.#applyDedup(queue, jobData);
|
|
1473
|
+
if (deduped) return deduped;
|
|
1376
1474
|
this.#recordPush(queue, jobData, delay);
|
|
1377
1475
|
this.#schedulePush(queue, jobData, delay);
|
|
1378
|
-
|
|
1476
|
+
if (jobData.dedup) {
|
|
1477
|
+
return { outcome: "added", jobId: jobData.id };
|
|
1478
|
+
}
|
|
1379
1479
|
}
|
|
1380
1480
|
async pushMany(jobs) {
|
|
1381
1481
|
return this.pushManyOn("default", jobs);
|
|
1382
1482
|
}
|
|
1383
1483
|
async pushManyOn(queue, jobs) {
|
|
1484
|
+
if (jobs.some((j) => j.dedup)) {
|
|
1485
|
+
throw new Error("dedup is not supported in batch dispatch; use single dispatch");
|
|
1486
|
+
}
|
|
1384
1487
|
for (const job of jobs) {
|
|
1385
1488
|
await this.pushOn(queue, job);
|
|
1386
1489
|
}
|
|
@@ -1407,7 +1510,7 @@ var FakeAdapter = class {
|
|
|
1407
1510
|
return null;
|
|
1408
1511
|
}
|
|
1409
1512
|
const acquiredAt = Date.now();
|
|
1410
|
-
this.#activeJobs.set(job.id, { job, acquiredAt, queue });
|
|
1513
|
+
this.#activeJobs.set(job.id, { job, acquiredAt, queue, workerId: this.#workerId });
|
|
1411
1514
|
return { ...job, acquiredAt };
|
|
1412
1515
|
}
|
|
1413
1516
|
async completeJob(jobId, queue, removeOnComplete) {
|
|
@@ -1415,6 +1518,7 @@ var FakeAdapter = class {
|
|
|
1415
1518
|
if (!active) return;
|
|
1416
1519
|
this.#activeJobs.delete(jobId);
|
|
1417
1520
|
if (removeOnComplete === void 0 || removeOnComplete === true) {
|
|
1521
|
+
this.#cleanupDedupForJob(queue, active.job);
|
|
1418
1522
|
return;
|
|
1419
1523
|
}
|
|
1420
1524
|
this.#storeHistory(queue, "completed", active.job, removeOnComplete);
|
|
@@ -1424,6 +1528,7 @@ var FakeAdapter = class {
|
|
|
1424
1528
|
if (!active) return;
|
|
1425
1529
|
this.#activeJobs.delete(jobId);
|
|
1426
1530
|
if (removeOnFail === void 0 || removeOnFail === true) {
|
|
1531
|
+
this.#cleanupDedupForJob(queue, active.job);
|
|
1427
1532
|
return;
|
|
1428
1533
|
}
|
|
1429
1534
|
this.#storeHistory(queue, "failed", active.job, removeOnFail, error);
|
|
@@ -1459,6 +1564,7 @@ var FakeAdapter = class {
|
|
|
1459
1564
|
const currentStalledCount = active.job.stalledCount ?? 0;
|
|
1460
1565
|
if (currentStalledCount >= maxStalledCount) {
|
|
1461
1566
|
this.#activeJobs.delete(jobId);
|
|
1567
|
+
this.#cleanupDedupForJob(active.queue, active.job);
|
|
1462
1568
|
continue;
|
|
1463
1569
|
}
|
|
1464
1570
|
this.#activeJobs.delete(jobId);
|
|
@@ -1471,6 +1577,18 @@ var FakeAdapter = class {
|
|
|
1471
1577
|
}
|
|
1472
1578
|
return recovered;
|
|
1473
1579
|
}
|
|
1580
|
+
async renewJobs(queue, jobIds) {
|
|
1581
|
+
const now = Date.now();
|
|
1582
|
+
let renewed = 0;
|
|
1583
|
+
for (const jobId of jobIds) {
|
|
1584
|
+
const active = this.#activeJobs.get(jobId);
|
|
1585
|
+
if (active && active.queue === queue && active.workerId === this.#workerId) {
|
|
1586
|
+
active.acquiredAt = now;
|
|
1587
|
+
renewed++;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return renewed;
|
|
1591
|
+
}
|
|
1474
1592
|
async getJob(jobId, queue) {
|
|
1475
1593
|
const active = this.#activeJobs.get(jobId);
|
|
1476
1594
|
if (active && active.queue === queue) {
|
|
@@ -1627,23 +1745,110 @@ var FakeAdapter = class {
|
|
|
1627
1745
|
const records = store.get(queue);
|
|
1628
1746
|
records.push(record);
|
|
1629
1747
|
if (retention && retention !== true) {
|
|
1630
|
-
this.#applyRetention(records, retention);
|
|
1748
|
+
this.#applyRetention(records, retention, queue);
|
|
1631
1749
|
}
|
|
1632
1750
|
}
|
|
1633
|
-
#applyRetention(records, retention) {
|
|
1751
|
+
#applyRetention(records, retention, queue) {
|
|
1634
1752
|
if (retention === false || retention === true) {
|
|
1635
1753
|
return;
|
|
1636
1754
|
}
|
|
1755
|
+
const pruned = [];
|
|
1637
1756
|
if (retention.age !== void 0) {
|
|
1638
1757
|
const maxAgeMs = parse(retention.age);
|
|
1639
1758
|
if (maxAgeMs > 0) {
|
|
1640
1759
|
const cutoff = Date.now() - maxAgeMs;
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1760
|
+
const kept = [];
|
|
1761
|
+
for (const record of records) {
|
|
1762
|
+
if ((record.finishedAt ?? 0) >= cutoff) {
|
|
1763
|
+
kept.push(record);
|
|
1764
|
+
} else {
|
|
1765
|
+
pruned.push(record);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
records.splice(0, records.length, ...kept);
|
|
1643
1769
|
}
|
|
1644
1770
|
}
|
|
1645
1771
|
if (retention.count !== void 0 && retention.count > 0 && records.length > retention.count) {
|
|
1646
|
-
|
|
1772
|
+
const excess = records.length - retention.count;
|
|
1773
|
+
pruned.push(...records.slice(0, excess));
|
|
1774
|
+
records.splice(0, excess);
|
|
1775
|
+
}
|
|
1776
|
+
for (const record of pruned) {
|
|
1777
|
+
this.#cleanupDedupForJob(queue, record.data);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
#applyDedup(queue, jobData) {
|
|
1781
|
+
if (!jobData.dedup) return null;
|
|
1782
|
+
const dedupId = jobData.dedup.id;
|
|
1783
|
+
const now = Date.now();
|
|
1784
|
+
const entry = this.#getDedupEntry(queue, dedupId);
|
|
1785
|
+
if (entry) {
|
|
1786
|
+
const withinTtl = !entry.ttl || now - entry.createdAt < entry.ttl;
|
|
1787
|
+
if (withinTtl) {
|
|
1788
|
+
const existing = this.#findJobById(queue, entry.jobId);
|
|
1789
|
+
if (existing) {
|
|
1790
|
+
const replaceable = existing.location === "pending" || existing.location === "delayed";
|
|
1791
|
+
if (jobData.dedup.replace && replaceable) {
|
|
1792
|
+
existing.job.payload = structuredClone(jobData.payload);
|
|
1793
|
+
if (jobData.dedup.extend && entry.ttl) {
|
|
1794
|
+
entry.createdAt = now;
|
|
1795
|
+
}
|
|
1796
|
+
return { outcome: "replaced", jobId: entry.jobId };
|
|
1797
|
+
}
|
|
1798
|
+
if (jobData.dedup.extend && entry.ttl) {
|
|
1799
|
+
entry.createdAt = now;
|
|
1800
|
+
return { outcome: "extended", jobId: entry.jobId };
|
|
1801
|
+
}
|
|
1802
|
+
return { outcome: "skipped", jobId: entry.jobId };
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
this.#setDedupEntry(queue, dedupId, {
|
|
1807
|
+
jobId: jobData.id,
|
|
1808
|
+
createdAt: now,
|
|
1809
|
+
ttl: jobData.dedup.ttl,
|
|
1810
|
+
replace: jobData.dedup.replace,
|
|
1811
|
+
extend: jobData.dedup.extend
|
|
1812
|
+
});
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
#findJobById(queue, jobId) {
|
|
1816
|
+
const active = this.#activeJobs.get(jobId);
|
|
1817
|
+
if (active && active.queue === queue) {
|
|
1818
|
+
return { job: active.job, location: "active" };
|
|
1819
|
+
}
|
|
1820
|
+
const pending = this.#queues.get(queue)?.find((j) => j.id === jobId);
|
|
1821
|
+
if (pending) {
|
|
1822
|
+
return { job: pending, location: "pending" };
|
|
1823
|
+
}
|
|
1824
|
+
const delayed = this.#delayedJobs.get(queue)?.get(jobId);
|
|
1825
|
+
if (delayed) {
|
|
1826
|
+
return { job: delayed.job, location: "delayed" };
|
|
1827
|
+
}
|
|
1828
|
+
const completed = this.#findHistory(this.#completedJobs, queue, jobId);
|
|
1829
|
+
if (completed) {
|
|
1830
|
+
return { job: completed.data, location: "completed" };
|
|
1831
|
+
}
|
|
1832
|
+
const failed = this.#findHistory(this.#failedJobs, queue, jobId);
|
|
1833
|
+
if (failed) {
|
|
1834
|
+
return { job: failed.data, location: "failed" };
|
|
1835
|
+
}
|
|
1836
|
+
return null;
|
|
1837
|
+
}
|
|
1838
|
+
#getDedupEntry(queue, dedupId) {
|
|
1839
|
+
return this.#dedupIndex.get(queue)?.get(dedupId);
|
|
1840
|
+
}
|
|
1841
|
+
#setDedupEntry(queue, dedupId, entry) {
|
|
1842
|
+
if (!this.#dedupIndex.has(queue)) {
|
|
1843
|
+
this.#dedupIndex.set(queue, /* @__PURE__ */ new Map());
|
|
1844
|
+
}
|
|
1845
|
+
this.#dedupIndex.get(queue).set(dedupId, entry);
|
|
1846
|
+
}
|
|
1847
|
+
#cleanupDedupForJob(queue, job) {
|
|
1848
|
+
if (!job.dedup) return;
|
|
1849
|
+
const entry = this.#getDedupEntry(queue, job.dedup.id);
|
|
1850
|
+
if (entry && entry.jobId === job.id) {
|
|
1851
|
+
this.#dedupIndex.get(queue)?.delete(job.dedup.id);
|
|
1647
1852
|
}
|
|
1648
1853
|
}
|
|
1649
1854
|
#findHistory(store, queue, jobId) {
|
|
@@ -1721,4 +1926,4 @@ export {
|
|
|
1721
1926
|
ScheduleBuilder,
|
|
1722
1927
|
Job
|
|
1723
1928
|
};
|
|
1724
|
-
//# sourceMappingURL=chunk-
|
|
1929
|
+
//# sourceMappingURL=chunk-AHUVTAI7.js.map
|