@boringnode/queue 0.0.1-alpha → 0.0.1-alpha.1
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 +334 -0
- package/build/index.d.ts +2 -3
- package/build/index.js +98 -100
- package/build/index.js.map +1 -1
- package/build/{job-CcAUWe8j.d.ts → job-Bd_c2lFK.d.ts} +45 -21
- package/build/src/contracts/adapter.d.ts +1 -2
- package/build/src/drivers/knex_adapter.d.ts +43 -0
- package/build/src/drivers/knex_adapter.js +176 -0
- package/build/src/drivers/knex_adapter.js.map +1 -0
- package/build/src/drivers/redis_adapter.d.ts +19 -6
- package/build/src/drivers/redis_adapter.js +155 -35
- package/build/src/drivers/redis_adapter.js.map +1 -1
- package/build/src/drivers/sync_adapter.d.ts +11 -5
- package/build/src/drivers/sync_adapter.js +11 -3
- package/build/src/drivers/sync_adapter.js.map +1 -1
- package/build/src/types/main.d.ts +1 -2
- package/package.json +11 -3
- package/build/src/contracts/lease_manager.d.ts +0 -8
- package/build/src/contracts/lease_manager.js +0 -1
- package/build/src/contracts/lease_manager.js.map +0 -1
|
@@ -1,34 +1,102 @@
|
|
|
1
1
|
// src/drivers/redis_adapter.ts
|
|
2
|
-
import { VerrouLeaseManager } from "#lease_managers/verrou";
|
|
3
2
|
import { Redis } from "ioredis";
|
|
4
3
|
var redisKey = "jobs";
|
|
5
|
-
var
|
|
6
|
-
local
|
|
7
|
-
local
|
|
8
|
-
local
|
|
4
|
+
var ACQUIRE_JOB_SCRIPT = `
|
|
5
|
+
local pending_key = KEYS[1]
|
|
6
|
+
local active_key = KEYS[2]
|
|
7
|
+
local delayed_key = KEYS[3]
|
|
8
|
+
local worker_id = ARGV[1]
|
|
9
|
+
local now = ARGV[2]
|
|
9
10
|
|
|
10
|
-
--
|
|
11
|
+
-- First, process delayed jobs
|
|
11
12
|
local ready_jobs = redis.call('ZRANGEBYSCORE', delayed_key, 0, now)
|
|
12
|
-
|
|
13
13
|
if #ready_jobs > 0 then
|
|
14
|
-
-- Move jobs to priority queue and remove from delayed queue atomically
|
|
15
14
|
for i = 1, #ready_jobs do
|
|
16
15
|
local job_data = ready_jobs[i]
|
|
17
16
|
local job = cjson.decode(job_data)
|
|
18
17
|
local priority = job.priority or 5
|
|
19
|
-
|
|
18
|
+
local timestamp = tonumber(now)
|
|
19
|
+
local score = priority * 10000000000000 + timestamp
|
|
20
|
+
redis.call('ZADD', pending_key, score, job_data)
|
|
20
21
|
redis.call('ZREM', delayed_key, job_data)
|
|
21
22
|
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
-- Pop highest priority job (lowest score)
|
|
26
|
+
local result = redis.call('ZPOPMIN', pending_key)
|
|
27
|
+
if not result or #result == 0 then
|
|
28
|
+
return nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
local job_data = result[1]
|
|
32
|
+
local job = cjson.decode(job_data)
|
|
33
|
+
|
|
34
|
+
-- Store in active hash: jobId -> {workerId, acquiredAt, data}
|
|
35
|
+
local active_data = cjson.encode({
|
|
36
|
+
workerId = worker_id,
|
|
37
|
+
acquiredAt = tonumber(now),
|
|
38
|
+
data = job
|
|
39
|
+
})
|
|
40
|
+
redis.call('HSET', active_key, job.id, active_data)
|
|
41
|
+
|
|
42
|
+
-- Return job with acquiredAt
|
|
43
|
+
job.acquiredAt = tonumber(now)
|
|
44
|
+
return cjson.encode(job)
|
|
45
|
+
`;
|
|
46
|
+
var COMPLETE_JOB_SCRIPT = `
|
|
47
|
+
local active_key = KEYS[1]
|
|
48
|
+
local job_id = ARGV[1]
|
|
49
|
+
|
|
50
|
+
redis.call('HDEL', active_key, job_id)
|
|
51
|
+
return 1
|
|
52
|
+
`;
|
|
53
|
+
var FAIL_JOB_SCRIPT = `
|
|
54
|
+
local active_key = KEYS[1]
|
|
55
|
+
local job_id = ARGV[1]
|
|
56
|
+
|
|
57
|
+
redis.call('HDEL', active_key, job_id)
|
|
58
|
+
return 1
|
|
59
|
+
`;
|
|
60
|
+
var RETRY_JOB_SCRIPT = `
|
|
61
|
+
local active_key = KEYS[1]
|
|
62
|
+
local pending_key = KEYS[2]
|
|
63
|
+
local delayed_key = KEYS[3]
|
|
64
|
+
local job_id = ARGV[1]
|
|
65
|
+
local retry_at = tonumber(ARGV[2])
|
|
66
|
+
local now = tonumber(ARGV[3])
|
|
22
67
|
|
|
23
|
-
|
|
68
|
+
-- Get job from active hash
|
|
69
|
+
local active_data = redis.call('HGET', active_key, job_id)
|
|
70
|
+
if not active_data then
|
|
71
|
+
return 0
|
|
24
72
|
end
|
|
25
73
|
|
|
26
|
-
|
|
74
|
+
local active = cjson.decode(active_data)
|
|
75
|
+
local job = active.data
|
|
76
|
+
|
|
77
|
+
-- Remove from active
|
|
78
|
+
redis.call('HDEL', active_key, job_id)
|
|
79
|
+
|
|
80
|
+
-- Increment attempts
|
|
81
|
+
job.attempts = (job.attempts or 0) + 1
|
|
82
|
+
|
|
83
|
+
local job_data = cjson.encode(job)
|
|
84
|
+
|
|
85
|
+
-- Add back to pending or delayed
|
|
86
|
+
if retry_at and retry_at > now then
|
|
87
|
+
redis.call('ZADD', delayed_key, retry_at, job_data)
|
|
88
|
+
else
|
|
89
|
+
local priority = job.priority or 5
|
|
90
|
+
local score = priority * 10000000000000 + now
|
|
91
|
+
redis.call('ZADD', pending_key, score, job_data)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return 1
|
|
27
95
|
`;
|
|
28
96
|
function redis(config) {
|
|
29
97
|
return () => {
|
|
30
98
|
if (config instanceof Redis) {
|
|
31
|
-
return new RedisAdapter(config);
|
|
99
|
+
return new RedisAdapter(config, false);
|
|
32
100
|
}
|
|
33
101
|
const options = {
|
|
34
102
|
host: "localhost",
|
|
@@ -38,30 +106,95 @@ function redis(config) {
|
|
|
38
106
|
...config
|
|
39
107
|
};
|
|
40
108
|
const connection = new Redis(options);
|
|
41
|
-
return new RedisAdapter(connection);
|
|
109
|
+
return new RedisAdapter(connection, true);
|
|
42
110
|
};
|
|
43
111
|
}
|
|
44
112
|
var RedisAdapter = class {
|
|
45
113
|
#connection;
|
|
46
|
-
|
|
114
|
+
#ownsConnection;
|
|
115
|
+
#workerId = "";
|
|
116
|
+
constructor(connection, ownsConnection = false) {
|
|
47
117
|
this.#connection = connection;
|
|
118
|
+
this.#ownsConnection = ownsConnection;
|
|
48
119
|
}
|
|
49
|
-
|
|
50
|
-
|
|
120
|
+
setWorkerId(workerId) {
|
|
121
|
+
this.#workerId = workerId;
|
|
51
122
|
}
|
|
52
123
|
async destroy() {
|
|
53
|
-
|
|
124
|
+
if (this.#ownsConnection) {
|
|
125
|
+
await this.#connection.quit();
|
|
126
|
+
}
|
|
54
127
|
}
|
|
55
128
|
pop() {
|
|
56
129
|
return this.popFrom("default");
|
|
57
130
|
}
|
|
58
131
|
async popFrom(queue) {
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const pendingKey = `${redisKey}::${queue}`;
|
|
134
|
+
const activeKey = `${redisKey}::${queue}::active`;
|
|
135
|
+
const delayedKey = `${redisKey}::delayed::${queue}`;
|
|
136
|
+
const result = await this.#connection.eval(
|
|
137
|
+
ACQUIRE_JOB_SCRIPT,
|
|
138
|
+
3,
|
|
139
|
+
pendingKey,
|
|
140
|
+
activeKey,
|
|
141
|
+
delayedKey,
|
|
142
|
+
this.#workerId,
|
|
143
|
+
now.toString()
|
|
144
|
+
);
|
|
145
|
+
if (!result) {
|
|
146
|
+
return null;
|
|
63
147
|
}
|
|
64
|
-
return
|
|
148
|
+
return JSON.parse(result);
|
|
149
|
+
}
|
|
150
|
+
async popAndWait(queue, timeout) {
|
|
151
|
+
const immediate = await this.popFrom(queue);
|
|
152
|
+
if (immediate) {
|
|
153
|
+
return immediate;
|
|
154
|
+
}
|
|
155
|
+
const pendingKey = `${redisKey}::${queue}`;
|
|
156
|
+
const activeKey = `${redisKey}::${queue}::active`;
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const result = await this.#connection.bzpopmin(pendingKey, timeout / 1e3);
|
|
159
|
+
if (!result) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const [, jobData] = result;
|
|
163
|
+
const job = JSON.parse(jobData);
|
|
164
|
+
const activeData = JSON.stringify({
|
|
165
|
+
workerId: this.#workerId,
|
|
166
|
+
acquiredAt: now,
|
|
167
|
+
data: job
|
|
168
|
+
});
|
|
169
|
+
await this.#connection.hset(activeKey, job.id, activeData);
|
|
170
|
+
return {
|
|
171
|
+
...job,
|
|
172
|
+
acquiredAt: now
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async completeJob(jobId, queue) {
|
|
176
|
+
const activeKey = `${redisKey}::${queue}::active`;
|
|
177
|
+
await this.#connection.eval(COMPLETE_JOB_SCRIPT, 1, activeKey, jobId);
|
|
178
|
+
}
|
|
179
|
+
async failJob(jobId, queue, _error) {
|
|
180
|
+
const activeKey = `${redisKey}::${queue}::active`;
|
|
181
|
+
await this.#connection.eval(FAIL_JOB_SCRIPT, 1, activeKey, jobId);
|
|
182
|
+
}
|
|
183
|
+
async retryJob(jobId, queue, retryAt) {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const activeKey = `${redisKey}::${queue}::active`;
|
|
186
|
+
const pendingKey = `${redisKey}::${queue}`;
|
|
187
|
+
const delayedKey = `${redisKey}::delayed::${queue}`;
|
|
188
|
+
await this.#connection.eval(
|
|
189
|
+
RETRY_JOB_SCRIPT,
|
|
190
|
+
3,
|
|
191
|
+
activeKey,
|
|
192
|
+
pendingKey,
|
|
193
|
+
delayedKey,
|
|
194
|
+
jobId,
|
|
195
|
+
retryAt ? retryAt.getTime().toString() : "0",
|
|
196
|
+
now.toString()
|
|
197
|
+
);
|
|
65
198
|
}
|
|
66
199
|
push(jobData) {
|
|
67
200
|
return this.pushOn("default", jobData);
|
|
@@ -86,19 +219,6 @@ var RedisAdapter = class {
|
|
|
86
219
|
sizeOf(queue) {
|
|
87
220
|
return this.#connection.zcard(`${redisKey}::${queue}`);
|
|
88
221
|
}
|
|
89
|
-
async #processDelayedJobs(queue) {
|
|
90
|
-
const now = Date.now();
|
|
91
|
-
const delayedKey = `${redisKey}::delayed::${queue}`;
|
|
92
|
-
const queueKey = `${redisKey}::${queue}`;
|
|
93
|
-
return await this.#connection.eval(
|
|
94
|
-
PROCESS_DELAYED_JOBS_SCRIPT,
|
|
95
|
-
2,
|
|
96
|
-
// number of keys
|
|
97
|
-
delayedKey,
|
|
98
|
-
queueKey,
|
|
99
|
-
now.toString()
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
222
|
};
|
|
103
223
|
export {
|
|
104
224
|
RedisAdapter,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/drivers/redis_adapter.ts"],"sourcesContent":["import { VerrouLeaseManager } from '#lease_managers/verrou'\nimport { Redis, type RedisOptions } from 'ioredis'\nimport type { Adapter } from '#contracts/adapter'\nimport type { JobData, LeaseConfig } from '#types/main'\nimport type { LeaseManager } from '#contracts/lease_manager'\n\nconst redisKey = 'jobs'\ntype RedisConfig = Redis | RedisOptions\n\n// Lua script for atomic delayed job processing\nconst PROCESS_DELAYED_JOBS_SCRIPT = `\n local delayed_key = KEYS[1]\n local queue_key = KEYS[2]\n local now = ARGV[1]\n\n -- Get ready jobs (score <= now)\n local ready_jobs = redis.call('ZRANGEBYSCORE', delayed_key, 0, now)\n\n if #ready_jobs > 0 then\n -- Move jobs to priority queue and remove from delayed queue atomically\n for i = 1, #ready_jobs do\n local job_data = ready_jobs[i]\n local job = cjson.decode(job_data)\n local priority = job.priority or 5\n redis.call('ZADD', queue_key, priority, job_data)\n redis.call('ZREM', delayed_key, job_data)\n end\n\n return #ready_jobs\n end\n\n return 0\n`\n\nexport function redis(config?: RedisConfig) {\n return () => {\n if (config instanceof Redis) {\n return new RedisAdapter(config)\n }\n\n // Create new Redis instance from options\n const options: RedisOptions = {\n host: 'localhost',\n port: 6379,\n keyPrefix: 'boringnode::queue::',\n db: 0,\n ...config,\n }\n\n const connection = new Redis(options)\n return new RedisAdapter(connection)\n }\n}\n\nexport class RedisAdapter implements Adapter {\n readonly #connection: Redis\n\n constructor(connection: Redis) {\n this.#connection = connection\n }\n\n createLeaseManager(config: LeaseConfig): LeaseManager {\n return new VerrouLeaseManager(config, this.#connection)\n }\n\n async destroy(): Promise<void> {\n await this.#connection.quit()\n }\n\n pop(): Promise<JobData | null> {\n return this.popFrom('default')\n }\n\n async popFrom(queue: string): Promise<JobData | null> {\n // First, move any ready delayed jobs to the regular queue\n await this.#processDelayedJobs(queue)\n\n // Pop from priority queue (sorted set) - highest priority (lowest score) first\n const queueContent = await this.#connection.zpopmin(`${redisKey}::${queue}`)\n\n if (queueContent && queueContent.length > 0) {\n return JSON.parse(queueContent[0])\n }\n\n return null\n }\n\n push(jobData: JobData): Promise<void> {\n return this.pushOn('default', jobData)\n }\n\n pushLater(jobData: JobData, delay: number): Promise<void> {\n return this.pushLaterOn('default', jobData, delay)\n }\n\n async pushLaterOn(queue: string, jobData: JobData, delay: number): Promise<void> {\n const executeAt = Date.now() + delay\n const delayedKey = `${redisKey}::delayed::${queue}`\n\n await this.#connection.zadd(delayedKey, executeAt, JSON.stringify(jobData))\n }\n\n async pushOn(queue: string, jobData: JobData): Promise<void> {\n const priority = jobData.priority ?? 5\n\n // Use priority as primary score, add timestamp for FIFO order within same priority\n // Date.now() precision is sufficient but perfect FIFO within the same millisecond is not guaranteed\n const timestamp = Date.now()\n const score = priority * 1e13 + timestamp\n\n await this.#connection.zadd(`${redisKey}::${queue}`, score, JSON.stringify(jobData))\n }\n\n size(): Promise<number> {\n return this.sizeOf('default')\n }\n\n sizeOf(queue: string): Promise<number> {\n return this.#connection.zcard(`${redisKey}::${queue}`)\n }\n\n async #processDelayedJobs(queue: string): Promise<number> {\n const now = Date.now()\n const delayedKey = `${redisKey}::delayed::${queue}`\n const queueKey = `${redisKey}::${queue}`\n\n // Use Lua script for atomic operation - much faster than pipeline\n return (await this.#connection.eval(\n PROCESS_DELAYED_JOBS_SCRIPT,\n 2, // number of keys\n delayedKey,\n queueKey,\n now.toString()\n )) as number\n }\n}\n"],"mappings":";AAAA,SAAS,0BAA0B;AACnC,SAAS,aAAgC;AAKzC,IAAM,WAAW;AAIjB,IAAM,8BAA8B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwB7B,SAAS,MAAM,QAAsB;AAC1C,SAAO,MAAM;AACX,QAAI,kBAAkB,OAAO;AAC3B,aAAO,IAAI,aAAa,MAAM;AAAA,IAChC;AAGA,UAAM,UAAwB;AAAA,MAC5B,MAAM;AAAA,MACN,MAAM;AAAA,MACN,WAAW;AAAA,MACX,IAAI;AAAA,MACJ,GAAG;AAAA,IACL;AAEA,UAAM,aAAa,IAAI,MAAM,OAAO;AACpC,WAAO,IAAI,aAAa,UAAU;AAAA,EACpC;AACF;AAEO,IAAM,eAAN,MAAsC;AAAA,EAClC;AAAA,EAET,YAAY,YAAmB;AAC7B,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,mBAAmB,QAAmC;AACpD,WAAO,IAAI,mBAAmB,QAAQ,KAAK,WAAW;AAAA,EACxD;AAAA,EAEA,MAAM,UAAyB;AAC7B,UAAM,KAAK,YAAY,KAAK;AAAA,EAC9B;AAAA,EAEA,MAA+B;AAC7B,WAAO,KAAK,QAAQ,SAAS;AAAA,EAC/B;AAAA,EAEA,MAAM,QAAQ,OAAwC;AAEpD,UAAM,KAAK,oBAAoB,KAAK;AAGpC,UAAM,eAAe,MAAM,KAAK,YAAY,QAAQ,GAAG,QAAQ,KAAK,KAAK,EAAE;AAE3E,QAAI,gBAAgB,aAAa,SAAS,GAAG;AAC3C,aAAO,KAAK,MAAM,aAAa,CAAC,CAAC;AAAA,IACnC;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,KAAK,SAAiC;AACpC,WAAO,KAAK,OAAO,WAAW,OAAO;AAAA,EACvC;AAAA,EAEA,UAAU,SAAkB,OAA8B;AACxD,WAAO,KAAK,YAAY,WAAW,SAAS,KAAK;AAAA,EACnD;AAAA,EAEA,MAAM,YAAY,OAAe,SAAkB,OAA8B;AAC/E,UAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,UAAM,aAAa,GAAG,QAAQ,cAAc,KAAK;AAEjD,UAAM,KAAK,YAAY,KAAK,YAAY,WAAW,KAAK,UAAU,OAAO,CAAC;AAAA,EAC5E;AAAA,EAEA,MAAM,OAAO,OAAe,SAAiC;AAC3D,UAAM,WAAW,QAAQ,YAAY;AAIrC,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,QAAQ,WAAW,OAAO;AAEhC,UAAM,KAAK,YAAY,KAAK,GAAG,QAAQ,KAAK,KAAK,IAAI,OAAO,KAAK,UAAU,OAAO,CAAC;AAAA,EACrF;AAAA,EAEA,OAAwB;AACtB,WAAO,KAAK,OAAO,SAAS;AAAA,EAC9B;AAAA,EAEA,OAAO,OAAgC;AACrC,WAAO,KAAK,YAAY,MAAM,GAAG,QAAQ,KAAK,KAAK,EAAE;AAAA,EACvD;AAAA,EAEA,MAAM,oBAAoB,OAAgC;AACxD,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,aAAa,GAAG,QAAQ,cAAc,KAAK;AACjD,UAAM,WAAW,GAAG,QAAQ,KAAK,KAAK;AAGtC,WAAQ,MAAM,KAAK,YAAY;AAAA,MAC7B;AAAA,MACA;AAAA;AAAA,MACA;AAAA,MACA;AAAA,MACA,IAAI,SAAS;AAAA,IACf;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/drivers/redis_adapter.ts"],"sourcesContent":["import { Redis, type RedisOptions } from 'ioredis'\nimport type { Adapter, AcquiredJob } from '#contracts/adapter'\nimport type { JobData } from '#types/main'\n\nconst redisKey = 'jobs'\ntype RedisConfig = Redis | RedisOptions\n\n/**\n * Lua script for atomic job acquisition.\n * 1. Check and process delayed jobs\n * 2. Pop from pending queue\n * 3. Add to active hash with worker info\n * 4. Return job data\n */\nconst ACQUIRE_JOB_SCRIPT = `\n local pending_key = KEYS[1]\n local active_key = KEYS[2]\n local delayed_key = KEYS[3]\n local worker_id = ARGV[1]\n local now = ARGV[2]\n\n -- First, process delayed jobs\n local ready_jobs = redis.call('ZRANGEBYSCORE', delayed_key, 0, now)\n if #ready_jobs > 0 then\n for i = 1, #ready_jobs do\n local job_data = ready_jobs[i]\n local job = cjson.decode(job_data)\n local priority = job.priority or 5\n local timestamp = tonumber(now)\n local score = priority * 10000000000000 + timestamp\n redis.call('ZADD', pending_key, score, job_data)\n redis.call('ZREM', delayed_key, job_data)\n end\n end\n\n -- Pop highest priority job (lowest score)\n local result = redis.call('ZPOPMIN', pending_key)\n if not result or #result == 0 then\n return nil\n end\n\n local job_data = result[1]\n local job = cjson.decode(job_data)\n\n -- Store in active hash: jobId -> {workerId, acquiredAt, data}\n local active_data = cjson.encode({\n workerId = worker_id,\n acquiredAt = tonumber(now),\n data = job\n })\n redis.call('HSET', active_key, job.id, active_data)\n\n -- Return job with acquiredAt\n job.acquiredAt = tonumber(now)\n return cjson.encode(job)\n`\n\n/**\n * Lua script for completing a job.\n * Removes the job from active hash.\n */\nconst COMPLETE_JOB_SCRIPT = `\n local active_key = KEYS[1]\n local job_id = ARGV[1]\n\n redis.call('HDEL', active_key, job_id)\n return 1\n`\n\n/**\n * Lua script for failing a job permanently.\n * Removes from active hash.\n */\nconst FAIL_JOB_SCRIPT = `\n local active_key = KEYS[1]\n local job_id = ARGV[1]\n\n redis.call('HDEL', active_key, job_id)\n return 1\n`\n\n/**\n * Lua script for retrying a job.\n * 1. Get job from active hash\n * 2. Remove from active hash\n * 3. Increment attempts\n * 4. Add back to pending (or delayed if retryAt is set)\n */\nconst RETRY_JOB_SCRIPT = `\n local active_key = KEYS[1]\n local pending_key = KEYS[2]\n local delayed_key = KEYS[3]\n local job_id = ARGV[1]\n local retry_at = tonumber(ARGV[2])\n local now = tonumber(ARGV[3])\n\n -- Get job from active hash\n local active_data = redis.call('HGET', active_key, job_id)\n if not active_data then\n return 0\n end\n\n local active = cjson.decode(active_data)\n local job = active.data\n\n -- Remove from active\n redis.call('HDEL', active_key, job_id)\n\n -- Increment attempts\n job.attempts = (job.attempts or 0) + 1\n\n local job_data = cjson.encode(job)\n\n -- Add back to pending or delayed\n if retry_at and retry_at > now then\n redis.call('ZADD', delayed_key, retry_at, job_data)\n else\n local priority = job.priority or 5\n local score = priority * 10000000000000 + now\n redis.call('ZADD', pending_key, score, job_data)\n end\n\n return 1\n`\n\n/**\n * Create a new Redis adapter factory.\n * Accepts either a Redis instance or Redis options.\n *\n * When passing options, the adapter will create and manage\n * the connection lifecycle (closing it on destroy).\n *\n * When passing a Redis instance, the caller is responsible for\n * managing the connection lifecycle.\n */\nexport function redis(config?: RedisConfig) {\n return () => {\n if (config instanceof Redis) {\n return new RedisAdapter(config, false)\n }\n\n const options: RedisOptions = {\n host: 'localhost',\n port: 6379,\n keyPrefix: 'boringnode::queue::',\n db: 0,\n ...config,\n }\n\n const connection = new Redis(options)\n return new RedisAdapter(connection, true)\n }\n}\n\nexport class RedisAdapter implements Adapter {\n readonly #connection: Redis\n readonly #ownsConnection: boolean\n #workerId: string = ''\n\n constructor(connection: Redis, ownsConnection: boolean = false) {\n this.#connection = connection\n this.#ownsConnection = ownsConnection\n }\n\n setWorkerId(workerId: string): void {\n this.#workerId = workerId\n }\n\n async destroy(): Promise<void> {\n if (this.#ownsConnection) {\n await this.#connection.quit()\n }\n }\n\n pop(): Promise<AcquiredJob | null> {\n return this.popFrom('default')\n }\n\n async popFrom(queue: string): Promise<AcquiredJob | null> {\n const now = Date.now()\n const pendingKey = `${redisKey}::${queue}`\n const activeKey = `${redisKey}::${queue}::active`\n const delayedKey = `${redisKey}::delayed::${queue}`\n\n const result = await this.#connection.eval(\n ACQUIRE_JOB_SCRIPT,\n 3,\n pendingKey,\n activeKey,\n delayedKey,\n this.#workerId,\n now.toString()\n )\n\n if (!result) {\n return null\n }\n\n return JSON.parse(result as string)\n }\n\n async popAndWait(queue: string, timeout: number): Promise<AcquiredJob | null> {\n // First try immediate pop\n const immediate = await this.popFrom(queue)\n if (immediate) {\n return immediate\n }\n\n // Wait for new job using BZPOPMIN on pending queue\n const pendingKey = `${redisKey}::${queue}`\n const activeKey = `${redisKey}::${queue}::active`\n const now = Date.now()\n\n // BZPOPMIN returns [key, member, score] or null\n const result = await this.#connection.bzpopmin(pendingKey, timeout / 1000)\n\n if (!result) {\n return null\n }\n\n const [, jobData] = result\n const job = JSON.parse(jobData)\n\n // Store in active hash\n const activeData = JSON.stringify({\n workerId: this.#workerId,\n acquiredAt: now,\n data: job,\n })\n await this.#connection.hset(activeKey, job.id, activeData)\n\n return {\n ...job,\n acquiredAt: now,\n }\n }\n\n async completeJob(jobId: string, queue: string): Promise<void> {\n const activeKey = `${redisKey}::${queue}::active`\n\n await this.#connection.eval(COMPLETE_JOB_SCRIPT, 1, activeKey, jobId)\n }\n\n async failJob(jobId: string, queue: string, _error?: Error): Promise<void> {\n const activeKey = `${redisKey}::${queue}::active`\n\n await this.#connection.eval(FAIL_JOB_SCRIPT, 1, activeKey, jobId)\n }\n\n async retryJob(jobId: string, queue: string, retryAt?: Date): Promise<void> {\n const now = Date.now()\n const activeKey = `${redisKey}::${queue}::active`\n const pendingKey = `${redisKey}::${queue}`\n const delayedKey = `${redisKey}::delayed::${queue}`\n\n await this.#connection.eval(\n RETRY_JOB_SCRIPT,\n 3,\n activeKey,\n pendingKey,\n delayedKey,\n jobId,\n retryAt ? retryAt.getTime().toString() : '0',\n now.toString()\n )\n }\n\n push(jobData: JobData): Promise<void> {\n return this.pushOn('default', jobData)\n }\n\n pushLater(jobData: JobData, delay: number): Promise<void> {\n return this.pushLaterOn('default', jobData, delay)\n }\n\n async pushLaterOn(queue: string, jobData: JobData, delay: number): Promise<void> {\n const executeAt = Date.now() + delay\n const delayedKey = `${redisKey}::delayed::${queue}`\n\n await this.#connection.zadd(delayedKey, executeAt, JSON.stringify(jobData))\n }\n\n async pushOn(queue: string, jobData: JobData): Promise<void> {\n const priority = jobData.priority ?? 5\n\n // Use priority as primary score, add timestamp for FIFO order within same priority\n // Date.now() precision is sufficient but perfect FIFO within the same millisecond is not guaranteed\n const timestamp = Date.now()\n const score = priority * 1e13 + timestamp\n\n await this.#connection.zadd(`${redisKey}::${queue}`, score, JSON.stringify(jobData))\n }\n\n size(): Promise<number> {\n return this.sizeOf('default')\n }\n\n sizeOf(queue: string): Promise<number> {\n return this.#connection.zcard(`${redisKey}::${queue}`)\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
import { A as Adapter,
|
|
2
|
-
import { LeaseManager } from '../contracts/lease_manager.js';
|
|
1
|
+
import { A as Adapter, b as JobData, a as AcquiredJob } from '../../job-Bd_c2lFK.js';
|
|
3
2
|
|
|
4
3
|
declare function sync(): () => SyncAdapter;
|
|
4
|
+
/**
|
|
5
|
+
* Sync adapter executes jobs immediately when pushed.
|
|
6
|
+
* Pop/complete/fail/retry are not supported as jobs are executed synchronously.
|
|
7
|
+
*/
|
|
5
8
|
declare class SyncAdapter implements Adapter {
|
|
6
9
|
#private;
|
|
7
|
-
|
|
10
|
+
setWorkerId(_workerId: string): void;
|
|
8
11
|
push(jobData: JobData): Promise<void>;
|
|
9
12
|
pushOn(_queue: string, jobData: JobData): Promise<void>;
|
|
10
13
|
pushLater(jobData: JobData, delay: number): Promise<void>;
|
|
11
14
|
pushLaterOn(_queue: string, jobData: JobData, delay: number): Promise<void>;
|
|
12
15
|
size(): Promise<number>;
|
|
13
16
|
sizeOf(_queue: string): Promise<number>;
|
|
14
|
-
pop(): Promise<
|
|
15
|
-
popFrom(_queue: string): Promise<
|
|
17
|
+
pop(): Promise<AcquiredJob | null>;
|
|
18
|
+
popFrom(_queue: string): Promise<AcquiredJob | null>;
|
|
19
|
+
completeJob(_jobId: string, _queue: string): Promise<void>;
|
|
20
|
+
failJob(_jobId: string, _queue: string, _error?: Error): Promise<void>;
|
|
21
|
+
retryJob(_jobId: string, _queue: string, _retryAt?: Date): Promise<void>;
|
|
16
22
|
destroy(): Promise<void>;
|
|
17
23
|
}
|
|
18
24
|
|
|
@@ -4,8 +4,7 @@ function sync() {
|
|
|
4
4
|
return () => new SyncAdapter();
|
|
5
5
|
}
|
|
6
6
|
var SyncAdapter = class {
|
|
7
|
-
|
|
8
|
-
throw new Error("Method not implemented.");
|
|
7
|
+
setWorkerId(_workerId) {
|
|
9
8
|
}
|
|
10
9
|
push(jobData) {
|
|
11
10
|
return this.pushOn("default", jobData);
|
|
@@ -32,7 +31,16 @@ var SyncAdapter = class {
|
|
|
32
31
|
return this.popFrom("default");
|
|
33
32
|
}
|
|
34
33
|
popFrom(_queue) {
|
|
35
|
-
throw new Error("
|
|
34
|
+
throw new Error("SyncAdapter does not support pop - jobs are executed immediately on push");
|
|
35
|
+
}
|
|
36
|
+
completeJob(_jobId, _queue) {
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
}
|
|
39
|
+
failJob(_jobId, _queue, _error) {
|
|
40
|
+
return Promise.resolve();
|
|
41
|
+
}
|
|
42
|
+
retryJob(_jobId, _queue, _retryAt) {
|
|
43
|
+
return Promise.resolve();
|
|
36
44
|
}
|
|
37
45
|
destroy() {
|
|
38
46
|
return Promise.resolve();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/drivers/sync_adapter.ts"],"sourcesContent":["import { Locator } from '#src/locator'\nimport type { Adapter } from '#contracts/adapter'\nimport type { JobData
|
|
1
|
+
{"version":3,"sources":["../../../src/drivers/sync_adapter.ts"],"sourcesContent":["import { Locator } from '#src/locator'\nimport type { Adapter, AcquiredJob } from '#contracts/adapter'\nimport type { JobData } from '#types/main'\n\nexport function sync() {\n return () => new SyncAdapter()\n}\n\n/**\n * Sync adapter executes jobs immediately when pushed.\n * Pop/complete/fail/retry are not supported as jobs are executed synchronously.\n */\nexport class SyncAdapter implements Adapter {\n setWorkerId(_workerId: string): void {}\n\n push(jobData: JobData): Promise<void> {\n return this.pushOn('default', jobData)\n }\n\n pushOn(_queue: string, jobData: JobData): Promise<void> {\n return this.#execute(jobData.name, jobData.payload)\n }\n\n pushLater(jobData: JobData, delay: number): Promise<void> {\n return this.pushLaterOn('default', jobData, delay)\n }\n\n pushLaterOn(_queue: string, jobData: JobData, delay: number): Promise<void> {\n setTimeout(() => {\n void this.#execute(jobData.name, jobData.payload)\n }, delay)\n\n return Promise.resolve()\n }\n\n size(): Promise<number> {\n return this.sizeOf('default')\n }\n\n sizeOf(_queue: string): Promise<number> {\n return Promise.resolve(0)\n }\n\n pop(): Promise<AcquiredJob | null> {\n return this.popFrom('default')\n }\n\n popFrom(_queue: string): Promise<AcquiredJob | null> {\n throw new Error('SyncAdapter does not support pop - jobs are executed immediately on push')\n }\n\n completeJob(_jobId: string, _queue: string): Promise<void> {\n return Promise.resolve()\n }\n\n failJob(_jobId: string, _queue: string, _error?: Error): Promise<void> {\n return Promise.resolve()\n }\n\n retryJob(_jobId: string, _queue: string, _retryAt?: Date): Promise<void> {\n return Promise.resolve()\n }\n\n destroy(): Promise<void> {\n return Promise.resolve()\n }\n\n async #execute(jobName: string, payload: any): Promise<any> {\n const JobClass = Locator.get(jobName)\n\n if (!JobClass) {\n throw new Error(`Job class ${jobName} not found.`)\n }\n\n const jobInstance = new JobClass(payload)\n await jobInstance.execute()\n }\n}\n"],"mappings":";AAAA,SAAS,eAAe;AAIjB,SAAS,OAAO;AACrB,SAAO,MAAM,IAAI,YAAY;AAC/B;AAMO,IAAM,cAAN,MAAqC;AAAA,EAC1C,YAAY,WAAyB;AAAA,EAAC;AAAA,EAEtC,KAAK,SAAiC;AACpC,WAAO,KAAK,OAAO,WAAW,OAAO;AAAA,EACvC;AAAA,EAEA,OAAO,QAAgB,SAAiC;AACtD,WAAO,KAAK,SAAS,QAAQ,MAAM,QAAQ,OAAO;AAAA,EACpD;AAAA,EAEA,UAAU,SAAkB,OAA8B;AACxD,WAAO,KAAK,YAAY,WAAW,SAAS,KAAK;AAAA,EACnD;AAAA,EAEA,YAAY,QAAgB,SAAkB,OAA8B;AAC1E,eAAW,MAAM;AACf,WAAK,KAAK,SAAS,QAAQ,MAAM,QAAQ,OAAO;AAAA,IAClD,GAAG,KAAK;AAER,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,OAAwB;AACtB,WAAO,KAAK,OAAO,SAAS;AAAA,EAC9B;AAAA,EAEA,OAAO,QAAiC;AACtC,WAAO,QAAQ,QAAQ,CAAC;AAAA,EAC1B;AAAA,EAEA,MAAmC;AACjC,WAAO,KAAK,QAAQ,SAAS;AAAA,EAC/B;AAAA,EAEA,QAAQ,QAA6C;AACnD,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AAAA,EAEA,YAAY,QAAgB,QAA+B;AACzD,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,QAAQ,QAAgB,QAAgB,QAA+B;AACrE,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,SAAS,QAAgB,QAAgB,UAAgC;AACvE,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,UAAyB;AACvB,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEA,MAAM,SAAS,SAAiB,SAA4B;AAC1D,UAAM,WAAW,QAAQ,IAAI,OAAO;AAEpC,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,aAAa,OAAO,aAAa;AAAA,IACnD;AAEA,UAAM,cAAc,IAAI,SAAS,OAAO;AACxC,UAAM,YAAY,QAAQ;AAAA,EAC5B;AACF;","names":[]}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
import '../contracts/lease_manager.js';
|
|
1
|
+
export { k as AdapterFactory, h as BackoffConfig, B as BackoffStrategy, D as Duration, g as JobClass, b as JobData, d as JobOptions, i as QueueConfig, Q as QueueManagerConfig, R as RetryConfig, j as WorkerConfig, W as WorkerCycle } from '../../job-Bd_c2lFK.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boringnode/queue",
|
|
3
3
|
"description": "A simple and efficient queue system for Node.js applications",
|
|
4
|
-
"version": "0.0.1-alpha",
|
|
4
|
+
"version": "0.0.1-alpha.1",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"#types/*": "./src/types/*.ts"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
+
"benchmark": "node benchmark/run.ts",
|
|
26
|
+
"benchmark:quick": "node benchmark/run.ts --quick",
|
|
27
|
+
"benchmark:full": "node benchmark/run.ts --full",
|
|
25
28
|
"build": "yarn clean && tsup-node",
|
|
26
29
|
"clean": "del-cli build",
|
|
27
30
|
"format": "prettier --write .",
|
|
@@ -33,8 +36,7 @@
|
|
|
33
36
|
},
|
|
34
37
|
"dependencies": {
|
|
35
38
|
"@lukeed/ms": "^2.0.2",
|
|
36
|
-
"@poppinss/utils": "^6.10.1"
|
|
37
|
-
"@verrou/core": "^0.5.1"
|
|
39
|
+
"@poppinss/utils": "^6.10.1"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@adonisjs/eslint-config": "^2.1.2",
|
|
@@ -44,11 +46,17 @@
|
|
|
44
46
|
"@japa/expect-type": "^2.0.3",
|
|
45
47
|
"@japa/file-system": "^2.3.2",
|
|
46
48
|
"@japa/runner": "^4.4.0",
|
|
49
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
47
50
|
"@types/node": "^24.3.1",
|
|
51
|
+
"@types/pg": "^8",
|
|
52
|
+
"better-sqlite3": "^12.5.0",
|
|
53
|
+
"bullmq": "^5.65.1",
|
|
48
54
|
"c8": "^10.1.3",
|
|
49
55
|
"del-cli": "^7.0.0",
|
|
50
56
|
"eslint": "^9.35.0",
|
|
51
57
|
"ioredis": "^5.7.0",
|
|
58
|
+
"knex": "^3.1.0",
|
|
59
|
+
"pg": "^8.16.3",
|
|
52
60
|
"prettier": "^3.6.2",
|
|
53
61
|
"release-it": "^19.0.4",
|
|
54
62
|
"testcontainers": "^11.5.1",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//# sourceMappingURL=lease_manager.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|