@boringnode/queue 0.5.1 → 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 +103 -20
- package/build/chunk-6IO4P6RB.js +145 -0
- package/build/chunk-6IO4P6RB.js.map +1 -0
- package/build/{chunk-VHN3XZDC.js → chunk-AHUVTAI7.js} +278 -29
- 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 +34 -8
- package/build/index.js +187 -31
- package/build/index.js.map +1 -1
- package/build/{job-DImdhRFO.d.ts → job-C4oyCVxR.d.ts} +275 -15
- package/build/src/contracts/adapter.d.ts +1 -1
- package/build/src/drivers/fake_adapter.d.ts +12 -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 +166 -402
- 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/otel.js +3 -0
- package/build/src/otel.js.map +1 -1
- 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 +18 -19
- package/build/chunk-VHN3XZDC.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:
|
|
@@ -270,20 +350,22 @@ await QueueManager.init({
|
|
|
270
350
|
locations: ['./app/jobs/**/*.ts'],
|
|
271
351
|
})
|
|
272
352
|
|
|
273
|
-
|
|
353
|
+
// The `using` keyword automatically restores the real adapters when
|
|
354
|
+
// the variable goes out of scope (at the end of the test function).
|
|
355
|
+
using fake = QueueManager.fake()
|
|
274
356
|
|
|
275
357
|
await SendEmailJob.dispatch({ to: 'user@example.com' })
|
|
276
358
|
|
|
277
|
-
|
|
278
|
-
|
|
359
|
+
fake.assertPushed(SendEmailJob)
|
|
360
|
+
fake.assertPushed(SendEmailJob, {
|
|
279
361
|
queue: 'default',
|
|
280
362
|
payload: (payload) => payload.to === 'user@example.com',
|
|
281
363
|
})
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
QueueManager.restore()
|
|
364
|
+
fake.assertPushedCount(1)
|
|
285
365
|
```
|
|
286
366
|
|
|
367
|
+
You can also call `QueueManager.restore()` manually if you need more control over when the real adapters are restored.
|
|
368
|
+
|
|
287
369
|
### Sync (for testing)
|
|
288
370
|
|
|
289
371
|
```typescript
|
|
@@ -534,7 +616,7 @@ import * as boringqueue from '@boringnode/queue'
|
|
|
534
616
|
|
|
535
617
|
const instrumentation = new QueueInstrumentation({
|
|
536
618
|
messagingSystem: 'boringqueue', // default
|
|
537
|
-
executionSpanLinkMode: 'link',
|
|
619
|
+
executionSpanLinkMode: 'link', // or 'parent'
|
|
538
620
|
})
|
|
539
621
|
|
|
540
622
|
instrumentation.enable()
|
|
@@ -547,19 +629,20 @@ The instrumentation patches `QueueManager.init()` to automatically inject its wr
|
|
|
547
629
|
|
|
548
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.
|
|
549
631
|
|
|
550
|
-
| Attribute | Kind | Description
|
|
551
|
-
| ------------------------------- | ------- |
|
|
552
|
-
| `messaging.system` | Semconv | `'boringqueue'` (configurable)
|
|
553
|
-
| `messaging.operation.name` | Semconv | `'publish'` or `'process'`
|
|
554
|
-
| `messaging.destination.name` | Semconv | Queue name
|
|
555
|
-
| `messaging.message.id` | Semconv | Job ID for single-message spans
|
|
556
|
-
| `messaging.batch.message_count` | Semconv | Number of jobs in a batch dispatch
|
|
557
|
-
| `messaging.message.retry.count` | Custom | Retry count (0-based) for a job attempt
|
|
558
|
-
| `messaging.job.name` | Custom | Job class name (e.g. `SendEmailJob`)
|
|
559
|
-
| `messaging.job.status` | Custom | `'completed'`, `'failed'`, or `'retrying'`
|
|
560
|
-
| `messaging.job.group_id` | Custom | Queue-specific group identifier
|
|
561
|
-
| `messaging.job.priority` | Custom | Queue-specific job priority
|
|
562
|
-
| `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 |
|
|
645
|
+
| `messaging.job.queue_time_ms` | Custom | Time spent waiting in queue before processing |
|
|
563
646
|
|
|
564
647
|
### Trace Context Propagation
|
|
565
648
|
|
|
@@ -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":[]}
|