queasy 0.2.0 → 0.3.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.
Files changed (98) hide show
  1. package/.github/workflows/check.yml +3 -0
  2. package/.github/workflows/publish.yml +3 -0
  3. package/CLAUDE.md +5 -4
  4. package/Readme.md +9 -4
  5. package/biome.json +5 -1
  6. package/dist/client.d.ts +33 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +199 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/constants.d.ts +10 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/{src → dist}/constants.js +2 -10
  13. package/dist/constants.js.map +1 -0
  14. package/dist/errors.d.ts +7 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/{src → dist}/errors.js +1 -13
  17. package/dist/errors.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +3 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/manager.d.ts +19 -0
  23. package/dist/manager.d.ts.map +1 -0
  24. package/dist/manager.js +67 -0
  25. package/dist/manager.js.map +1 -0
  26. package/dist/pool.d.ts +29 -0
  27. package/dist/pool.d.ts.map +1 -0
  28. package/{src → dist}/pool.js +23 -82
  29. package/dist/pool.js.map +1 -0
  30. package/dist/queasy.lua +390 -0
  31. package/dist/queue.d.ts +22 -0
  32. package/dist/queue.d.ts.map +1 -0
  33. package/dist/queue.js +81 -0
  34. package/dist/queue.js.map +1 -0
  35. package/dist/types.d.ts +92 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +2 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/utils.d.ts +4 -0
  40. package/dist/utils.d.ts.map +1 -0
  41. package/dist/utils.js +24 -0
  42. package/dist/utils.js.map +1 -0
  43. package/dist/worker.d.ts +2 -0
  44. package/dist/worker.d.ts.map +1 -0
  45. package/dist/worker.js +42 -0
  46. package/dist/worker.js.map +1 -0
  47. package/docker-compose.yml +0 -2
  48. package/fuzztest/Readme.md +185 -0
  49. package/fuzztest/fuzz.ts +356 -0
  50. package/fuzztest/handlers/cascade-a.ts +90 -0
  51. package/fuzztest/handlers/cascade-b.ts +71 -0
  52. package/fuzztest/handlers/fail-handler.ts +47 -0
  53. package/fuzztest/handlers/periodic.ts +89 -0
  54. package/fuzztest/process.ts +100 -0
  55. package/fuzztest/shared/chaos.ts +29 -0
  56. package/fuzztest/shared/stream.ts +40 -0
  57. package/package.json +8 -7
  58. package/plans/redis-options.md +279 -0
  59. package/src/client.ts +246 -0
  60. package/src/constants.ts +33 -0
  61. package/src/errors.ts +13 -0
  62. package/src/index.ts +2 -0
  63. package/src/manager.ts +78 -0
  64. package/src/pool.ts +129 -0
  65. package/src/queasy.lua +2 -3
  66. package/src/queue.ts +108 -0
  67. package/src/types.ts +16 -0
  68. package/src/{utils.js → utils.ts} +3 -20
  69. package/src/{worker.js → worker.ts} +5 -12
  70. package/test/{client.test.js → client.test.ts} +6 -7
  71. package/test/{errors.test.js → errors.test.ts} +1 -1
  72. package/test/fixtures/always-fail-handler.ts +5 -0
  73. package/test/fixtures/data-logger-handler.ts +11 -0
  74. package/test/fixtures/failure-handler.ts +6 -0
  75. package/test/fixtures/permanent-error-handler.ts +6 -0
  76. package/test/fixtures/slow-handler.ts +6 -0
  77. package/test/fixtures/success-handler.js +0 -5
  78. package/test/fixtures/success-handler.ts +6 -0
  79. package/test/fixtures/with-failure-handler.ts +5 -0
  80. package/test/{guards.test.js → guards.test.ts} +21 -34
  81. package/test/{manager.test.js → manager.test.ts} +26 -34
  82. package/test/{pool.test.js → pool.test.ts} +14 -16
  83. package/test/{queue.test.js → queue.test.ts} +21 -21
  84. package/test/{redis-functions.test.js → redis-functions.test.ts} +14 -20
  85. package/test/{utils.test.js → utils.test.ts} +1 -1
  86. package/tsconfig.json +20 -0
  87. package/jsconfig.json +0 -17
  88. package/src/client.js +0 -258
  89. package/src/index.js +0 -2
  90. package/src/manager.js +0 -94
  91. package/src/queue.js +0 -154
  92. package/test/fixtures/always-fail-handler.js +0 -8
  93. package/test/fixtures/data-logger-handler.js +0 -19
  94. package/test/fixtures/failure-handler.js +0 -9
  95. package/test/fixtures/permanent-error-handler.js +0 -10
  96. package/test/fixtures/slow-handler.js +0 -9
  97. package/test/fixtures/with-failure-handler.js +0 -8
  98. /package/test/fixtures/{no-handle-handler.js → no-handle-handler.ts} +0 -0
@@ -1,42 +1,23 @@
1
1
  import { availableParallelism } from 'node:os';
2
2
  import { Worker } from 'node:worker_threads';
3
- import { WORKER_CAPACITY } from './constants.js';
4
- import { generateId } from './utils.js';
5
-
6
- /** @typedef {import('./types').DoneMessage} DoneMessage */
7
- /** @typedef {import('./types').Job} Job */
8
-
9
- /** @typedef {{
10
- * worker: Worker,
11
- * capacity: number,
12
- * id: string,
13
- * jobCount: number,
14
- * stalledJobs: Set<string>
15
- * }} WorkerEntry */
16
-
17
- /** @typedef {{
18
- * resolve: (value: DoneMessage) => void,
19
- * reject: (reason: DoneMessage) => void,
20
- * size: number,
21
- * timer: NodeJS.Timeout
22
- * }} JobEntry */
23
-
3
+ import { WORKER_CAPACITY } from "./constants.js";
4
+ import { generateId } from "./utils.js";
24
5
  export class Pool {
25
- /** @param {number?} targetCount - Number of desired workers */
6
+ workers;
7
+ activeJobs;
8
+ capacity;
26
9
  constructor(targetCount) {
27
- /** @type {Set<WorkerEntry>} */
28
10
  this.workers = new Set();
29
- /** @type {Map<string, JobEntry>} */
30
11
  this.activeJobs = new Map();
31
-
32
12
  this.capacity = 0;
33
-
34
13
  const count = targetCount ?? availableParallelism();
35
- for (let i = 0; i < count; i++) this.createWorker();
14
+ for (let i = 0; i < count; i++)
15
+ this.createWorker();
36
16
  }
37
-
38
17
  createWorker() {
39
- const worker = new Worker(new URL('./worker.js', import.meta.url));
18
+ // Copy file extension from current file so it works in src and dist.
19
+ const workerFilename = `./worker${import.meta.url.slice(-3)}`;
20
+ const worker = new Worker(new URL(workerFilename, import.meta.url));
40
21
  const entry = {
41
22
  worker,
42
23
  capacity: WORKER_CAPACITY,
@@ -48,11 +29,6 @@ export class Pool {
48
29
  worker.on('message', (message) => this.handleWorkerMessage(entry, message));
49
30
  this.workers.add(entry);
50
31
  }
51
-
52
- /**
53
- * @param {WorkerEntry} workerEntry
54
- * @param {DoneMessage} message
55
- */
56
32
  handleWorkerMessage(workerEntry, message) {
57
33
  const { jobId, error } = message;
58
34
  const jobEntry = this.activeJobs.get(jobId);
@@ -64,44 +40,24 @@ export class Pool {
64
40
  workerEntry.capacity += jobEntry.size;
65
41
  this.capacity += jobEntry.size;
66
42
  workerEntry.jobCount -= 1;
67
-
68
- // If this job was previously marked as stalled, unmark it.
69
- if (workerEntry.stalledJobs.has(jobId)) workerEntry.stalledJobs.delete(jobId);
70
-
43
+ if (workerEntry.stalledJobs.has(jobId))
44
+ workerEntry.stalledJobs.delete(jobId);
71
45
  this.activeJobs.delete(jobId);
72
46
  jobEntry[error ? 'reject' : 'resolve'](message);
73
-
74
- // If this worker is no longer in the the pool, check if it can be terminated.
75
- if (!this.workers.has(workerEntry)) this.terminateIfEmpty(workerEntry);
47
+ if (!this.workers.has(workerEntry))
48
+ this.terminateIfEmpty(workerEntry);
76
49
  }
77
-
78
- /**
79
- *
80
- * @param {WorkerEntry} workerEntry
81
- * @param {string} jobId
82
- */
83
50
  handleTimeout(workerEntry, jobId) {
84
51
  workerEntry.stalledJobs.add(jobId);
85
-
86
- // Remove and replace this worker in the pool (if it wasn’t already).
87
- if (this.workers.delete(workerEntry)) this.createWorker();
52
+ if (this.workers.delete(workerEntry))
53
+ this.createWorker();
88
54
  this.capacity -= workerEntry.capacity;
89
-
90
- // If this is the last job in this worker, terminate it.
91
55
  this.terminateIfEmpty(workerEntry);
92
56
  }
93
-
94
- /**
95
- * Stops adding new jobs to a worker if it has stalled jobs.
96
- * Terminates workers if all remaining jobs are stalled.
97
- * @param {WorkerEntry} workerEntry
98
- * @returns
99
- */
100
57
  async terminateIfEmpty({ stalledJobs, jobCount, worker }) {
101
- // Don't destroy if there are still non-stalled jobs running.
102
- if (jobCount > stalledJobs.size) return;
58
+ if (jobCount > stalledJobs.size)
59
+ return;
103
60
  await worker.terminate();
104
-
105
61
  for (const jobId of stalledJobs) {
106
62
  const jobEntry = this.activeJobs.get(jobId);
107
63
  this.activeJobs.delete(jobId);
@@ -112,26 +68,15 @@ export class Pool {
112
68
  });
113
69
  }
114
70
  }
115
-
116
- /**
117
- * Processes a job to the most free worker
118
- * @param {string} handlerPath
119
- * @param {Job} job
120
- * @param {number} size
121
- * @param {number} timeout - Maximum time in ms
122
- * @returns {Promise<DoneMessage>}
123
- */
124
71
  process(handlerPath, job, size, timeout) {
125
- // Find worker with most capacity
126
72
  let workerEntry = null;
127
73
  for (const entry of this.workers) {
128
- if (!workerEntry || entry.capacity > workerEntry.capacity) workerEntry = entry;
74
+ if (!workerEntry || entry.capacity > workerEntry.capacity)
75
+ workerEntry = entry;
129
76
  }
130
-
131
- if (!workerEntry) throw Error('Can’t process job without workers');
132
-
77
+ if (!workerEntry)
78
+ throw Error("Can't process job without workers");
133
79
  const timer = setTimeout(() => this.handleTimeout(workerEntry, job.id), timeout);
134
-
135
80
  return new Promise((resolve, reject) => {
136
81
  this.activeJobs.set(job.id, { resolve, reject, size, timer });
137
82
  workerEntry.capacity -= size;
@@ -140,10 +85,6 @@ export class Pool {
140
85
  workerEntry.worker.postMessage({ op: 'exec', handlerPath, job });
141
86
  });
142
87
  }
143
-
144
- /**
145
- * Terminates all workers
146
- */
147
88
  async close() {
148
89
  await Promise.all([...this.workers].map(async ({ worker }) => worker.terminate()));
149
90
  for (const [jobId, { reject, timer }] of this.activeJobs.entries()) {
@@ -154,8 +95,8 @@ export class Pool {
154
95
  error: { name: 'StallError', message: 'Pool is closing', kind: 'stall' },
155
96
  });
156
97
  }
157
-
158
98
  this.workers = new Set();
159
99
  this.activeJobs.clear();
160
100
  }
161
101
  }
102
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,390 @@
1
+ #!lua name=queasy
2
+
3
+ --[[
4
+ Queasy: Redis Lua functions for job queue management
5
+
6
+ Key structure:
7
+ - {queue} - sorted set of waiting job IDs (score = run_at or -run_at if blocked)
8
+ - {queue}:expiry - sorted set of client heartbeat expiries (member = client_id, score = expiry)
9
+ - {queue}:checkouts:{client_id} - set of job IDs checked out by this client
10
+ - {queue}:waiting_job:{id} - hash with job data for waiting jobs
11
+ - {queue}:active_job:{id} - hash with job data for active jobs
12
+ ]]
13
+
14
+ -- Key helpers
15
+ local function get_waiting_job_key(queue_key, id)
16
+ return queue_key .. ':waiting_job:' .. id
17
+ end
18
+
19
+ local function get_active_job_key(queue_key, id)
20
+ return queue_key .. ':active_job:' .. id
21
+ end
22
+
23
+ local function get_expiry_key(queue_key)
24
+ return queue_key .. ':expiry'
25
+ end
26
+
27
+ local function get_checkouts_key(queue_key, client_id)
28
+ return queue_key .. ':checkouts:' .. client_id
29
+ end
30
+
31
+ -- Helper: Add job to waiting queue with appropriate flags
32
+ local function add_to_waiting(queue_key, id, score, update_run_at)
33
+ local flag = nil
34
+
35
+ if update_run_at == 'false' then
36
+ flag = 'NX'
37
+ elseif update_run_at == 'if_later' then
38
+ flag = score >= 0 and 'GT' or 'LT'
39
+ elseif update_run_at == 'if_earlier' then
40
+ flag = score >= 0 and 'LT' or 'GT'
41
+ end
42
+
43
+ if flag then
44
+ redis.call('ZADD', queue_key, flag, score, id)
45
+ else
46
+ redis.call('ZADD', queue_key, score, id)
47
+ end
48
+ end
49
+
50
+ -- Helper: Upsert job to waiting queue
51
+ local function dispatch(
52
+ queue_key,
53
+ id, run_at, data,
54
+ update_data, update_run_at, reset_counts
55
+ )
56
+ local waiting_job_key = get_waiting_job_key(queue_key, id)
57
+ local active_job_key = get_active_job_key(queue_key, id)
58
+
59
+ -- id is always stored so that HGETALL (e.g. during dequeue) includes it
60
+ redis.call('HSET', waiting_job_key, 'id', id)
61
+
62
+ -- If reset_counts is true, reset counters to 0, otherwise initialize them
63
+ redis.call(reset_counts and 'HSET' or 'HSETNX', waiting_job_key, 'retry_count', '0')
64
+ redis.call(reset_counts and 'HSET' or 'HSETNX', waiting_job_key, 'stall_count', '0')
65
+
66
+ -- Handle data
67
+ redis.call(update_data and 'HSET' or 'HSETNX', waiting_job_key, 'data', data)
68
+
69
+ -- Check if there's an active job with this ID
70
+ local is_blocked = redis.call('EXISTS', active_job_key) == 1
71
+ local score = is_blocked and -tonumber(run_at) or tonumber(run_at)
72
+
73
+ if is_blocked then
74
+ -- save these flags in case they need to be applied later
75
+ redis.call('HSET', waiting_job_key,
76
+ 'reset_counts', tostring(reset_counts),
77
+ 'update_data', tostring(update_data),
78
+ 'update_run_at', update_run_at)
79
+ end
80
+
81
+ -- Add to waiting queue
82
+ add_to_waiting(queue_key, id, score, update_run_at)
83
+
84
+ return { ok = 'OK' }
85
+ end
86
+
87
+ -- Helper: Move job back to waiting for retry
88
+ local function do_retry(queue_key, id, retry_at)
89
+ local waiting_job_key = get_waiting_job_key(queue_key, id)
90
+ local active_job_key = get_active_job_key(queue_key, id)
91
+
92
+ local existing_score = redis.call('ZSCORE', queue_key, id)
93
+
94
+ if existing_score then
95
+ local run_at = -existing_score.double
96
+ local job = redis.call('HGETALL', waiting_job_key)['map']
97
+
98
+ redis.call('RENAME', active_job_key, waiting_job_key)
99
+ redis.call('ZADD', queue_key, retry_at, id)
100
+
101
+ if next(job) then
102
+ dispatch(
103
+ queue_key,
104
+ id, run_at, job.data,
105
+ job.update_data == 'true', job.update_run_at, job.reset_counts == 'true'
106
+ )
107
+ end
108
+ else
109
+ redis.call('RENAME', active_job_key, waiting_job_key)
110
+ redis.call('ZADD', queue_key, retry_at, id)
111
+ end
112
+
113
+ return { ok = 'OK' }
114
+ end
115
+
116
+ -- Helper: Clear active job and unblock waiting job
117
+ local function finish(queue_key, id, client_id, now)
118
+ local waiting_job_key = get_waiting_job_key(queue_key, id)
119
+ local active_job_key = get_active_job_key(queue_key, id)
120
+ local checkouts_key = get_checkouts_key(queue_key, client_id)
121
+
122
+ redis.call('SREM', checkouts_key, id)
123
+ redis.call('DEL', active_job_key)
124
+
125
+ local score = redis.call('ZSCORE', queue_key, id)
126
+
127
+ if score then
128
+ score = tonumber(score.double)
129
+ if score < 0 then
130
+ score = -score
131
+ end
132
+
133
+ local update_run_at = redis.call('HGET', waiting_job_key, 'update_run_at') or 'true'
134
+ add_to_waiting(queue_key, id, score, update_run_at)
135
+ end
136
+
137
+ return { ok = 'OK' }
138
+ end
139
+
140
+ -- Helper: Handle permanent failure
141
+ -- Creates a fail job and finishes the original job
142
+ local function fail(queue_key, fail_queue_key, id, client_id, fail_job_id, fail_job_data, now)
143
+ -- Dispatch the fail job
144
+ dispatch(fail_queue_key,
145
+ fail_job_id, 0, fail_job_data,
146
+ 'false', 'false', 'false')
147
+
148
+ -- Finish the original job
149
+ finish(queue_key, id, client_id, now)
150
+
151
+ return { ok = 'OK' }
152
+ end
153
+
154
+ -- Helper: Handle retriable failure
155
+ local function retry(queue_key, id, client_id, retry_at, now)
156
+ local active_job_key = get_active_job_key(queue_key, id)
157
+ local checkouts_key = get_checkouts_key(queue_key, client_id)
158
+
159
+ redis.call('SREM', checkouts_key, id)
160
+
161
+ local retry_count = tonumber(redis.call('HGET', active_job_key, 'retry_count'))
162
+
163
+ retry_count = retry_count + 1
164
+ redis.call('HSET', active_job_key, 'retry_count', retry_count)
165
+
166
+ local result = do_retry(queue_key, id, retry_at)
167
+
168
+ return result
169
+ end
170
+
171
+ -- Helper: Handle stalled job
172
+ local function handle_stall(queue_key, id, retry_at)
173
+ local active_job_key = get_active_job_key(queue_key, id)
174
+
175
+ local stall_count = tonumber(redis.call('HGET', active_job_key, 'stall_count'))
176
+
177
+ stall_count = stall_count + 1
178
+ redis.call('HSET', active_job_key, 'stall_count', stall_count)
179
+
180
+ return do_retry(queue_key, id, retry_at)
181
+ end
182
+
183
+ -- Sweep stalled clients
184
+ local function sweep(queue_key, now)
185
+ local expiry_key = get_expiry_key(queue_key)
186
+
187
+ -- Find first stalled client
188
+ local stalled = redis.call('ZRANGEBYSCORE', expiry_key, 0, now, 'LIMIT', 0, 1)
189
+
190
+ if #stalled == 0 then return 0 end
191
+
192
+ local stalled_client_id = stalled[1]
193
+ local checkouts_key = get_checkouts_key(queue_key, stalled_client_id)
194
+
195
+ -- Get all job IDs checked out by this client
196
+ -- RESP3 returns SMEMBERS as { set = { id1 = true, id2 = true, ... } }
197
+ local members_resp = redis.call('SMEMBERS', checkouts_key)
198
+
199
+ for id, _ in pairs(members_resp['set']) do
200
+ handle_stall(queue_key, id, 0)
201
+ end
202
+
203
+ -- Clean up the stalled client
204
+ redis.call('ZREM', expiry_key, stalled_client_id)
205
+ redis.call('DEL', checkouts_key)
206
+
207
+ return 1
208
+ end
209
+
210
+ -- Dequeue jobs from waiting queue
211
+ local function dequeue(queue_key, client_id, now, expiry, limit)
212
+ local expiry_key = get_expiry_key(queue_key)
213
+ local checkouts_key = get_checkouts_key(queue_key, client_id)
214
+ local jobs = redis.call('ZRANGEBYSCORE', queue_key, 0, now, 'LIMIT', 0, limit)
215
+ local result = {}
216
+
217
+ for _, id in ipairs(jobs) do
218
+ redis.call('ZREM', queue_key, id)
219
+ local waiting_job_key = get_waiting_job_key(queue_key, id)
220
+ local active_job_key = get_active_job_key(queue_key, id)
221
+
222
+ redis.call('RENAME', waiting_job_key, active_job_key)
223
+ local job = redis.call('HGETALL', active_job_key)
224
+
225
+ redis.call('SADD', checkouts_key, id)
226
+ table.insert(result, job)
227
+ end
228
+
229
+ -- Add this client to queue and bump its expiry
230
+ redis.call('ZADD', expiry_key, expiry, client_id)
231
+
232
+ -- Sweep stalled clients
233
+ sweep(queue_key, now)
234
+
235
+ return result
236
+ end
237
+
238
+ -- Cancel a waiting job
239
+ local function cancel(queue_key, id)
240
+ local waiting_job_key = get_waiting_job_key(queue_key, id)
241
+ local removed = redis.call('ZREM', queue_key, id)
242
+ if removed == 1 then
243
+ redis.call('DEL', waiting_job_key)
244
+ end
245
+ return removed
246
+ end
247
+
248
+ -- Bump heartbeat for client and sweep stalled clients
249
+ local function bump(queue_key, client_id, now, expiry)
250
+ local expiry_key = get_expiry_key(queue_key)
251
+
252
+ -- Check if this client exists in expiry set
253
+ -- This can’t be skipped in favour of ZADD XX CH — when a client's new expiry
254
+ -- is the same as the old one, XX CH returns 0 but we need it to return 1
255
+ if not redis.call('ZSCORE', expiry_key, client_id) then
256
+ return 0
257
+ end
258
+
259
+ -- Update expiry
260
+ redis.call('ZADD', expiry_key, 'XX', expiry, client_id)
261
+
262
+ -- Sweep stalled clients
263
+ sweep(queue_key, now)
264
+
265
+ return 1
266
+ end
267
+
268
+ -- Register: queasy_dispatch
269
+ redis.register_function {
270
+ function_name = 'queasy_dispatch',
271
+ callback = function(keys, args)
272
+ local queue_key = keys[1]
273
+ local id = args[1]
274
+ local run_at = tonumber(args[2])
275
+ local data = args[3]
276
+ local update_data = args[4] == 'true'
277
+ local update_run_at = args[5]
278
+ local reset_counts = args[6] == 'true'
279
+
280
+ redis.setresp(3)
281
+ return dispatch(
282
+ queue_key,
283
+ id, run_at, data,
284
+ update_data, update_run_at, reset_counts
285
+ )
286
+ end,
287
+ flags = {}
288
+ }
289
+
290
+ -- Register: queasy_dequeue
291
+ redis.register_function {
292
+ function_name = 'queasy_dequeue',
293
+ callback = function(keys, args)
294
+ local queue_key = keys[1]
295
+ local client_id = args[1]
296
+ local now = tonumber(args[2])
297
+ local expiry = tonumber(args[3])
298
+ local limit = tonumber(args[4])
299
+
300
+ redis.setresp(3)
301
+ return dequeue(queue_key, client_id, now, expiry, limit)
302
+ end,
303
+ flags = {}
304
+ }
305
+
306
+ -- Register: queasy_cancel
307
+ redis.register_function {
308
+ function_name = 'queasy_cancel',
309
+ callback = function(keys, args)
310
+ local queue_key = keys[1]
311
+ local id = args[1]
312
+
313
+ redis.setresp(3)
314
+ return cancel(queue_key, id)
315
+ end,
316
+ flags = {}
317
+ }
318
+
319
+ -- Register: queasy_bump
320
+ redis.register_function {
321
+ function_name = 'queasy_bump',
322
+ callback = function(keys, args)
323
+ local queue_key = keys[1]
324
+ local client_id = args[1]
325
+ local now = tonumber(args[2])
326
+ local expiry = tonumber(args[3])
327
+
328
+ redis.setresp(3)
329
+ return bump(queue_key, client_id, now, expiry)
330
+ end,
331
+ flags = {}
332
+ }
333
+
334
+ -- Register: queasy_finish
335
+ redis.register_function {
336
+ function_name = 'queasy_finish',
337
+ callback = function(keys, args)
338
+ local queue_key = keys[1]
339
+ local id = args[1]
340
+ local client_id = args[2]
341
+ local now = tonumber(args[3])
342
+
343
+ redis.setresp(3)
344
+ return finish(queue_key, id, client_id, now)
345
+ end,
346
+ flags = {}
347
+ }
348
+
349
+ -- Register: queasy_retry
350
+ redis.register_function {
351
+ function_name = 'queasy_retry',
352
+ callback = function(keys, args)
353
+ local queue_key = keys[1]
354
+ local id = args[1]
355
+ local client_id = args[2]
356
+ local retry_at = tonumber(args[3])
357
+ local now = tonumber(args[5])
358
+
359
+ redis.setresp(3)
360
+ return retry(queue_key, id, client_id, retry_at, now)
361
+ end,
362
+ flags = {}
363
+ }
364
+
365
+ -- Register: queasy_fail
366
+ redis.register_function {
367
+ function_name = 'queasy_fail',
368
+ callback = function(keys, args)
369
+ local queue_key = keys[1]
370
+ local fail_queue_key = keys[2]
371
+ local id = args[1]
372
+ local client_id = args[2]
373
+ local fail_job_id = args[3]
374
+ local fail_job_data = args[4]
375
+ local now = tonumber(args[5])
376
+
377
+ redis.setresp(3)
378
+ return fail(queue_key, fail_queue_key, id, client_id, fail_job_id, fail_job_data, now)
379
+ end,
380
+ flags = {}
381
+ }
382
+
383
+ -- Register: queasy_version
384
+ redis.register_function {
385
+ function_name = 'queasy_version',
386
+ callback = function(keys, args)
387
+ return '__QUEASY_VERSION__'
388
+ end,
389
+ flags = {}
390
+ }
@@ -0,0 +1,22 @@
1
+ import type { Client } from './client.ts';
2
+ import type { Manager } from './manager.ts';
3
+ import type { Pool } from './pool.ts';
4
+ import type { HandlerOptions, JobOptions, ListenOptions } from './types.ts';
5
+ export declare class Queue {
6
+ key: string;
7
+ client: Client;
8
+ pool: Pool | undefined;
9
+ manager: Manager | undefined;
10
+ handlerOptions: Required<HandlerOptions> | undefined;
11
+ handlerPath: string | undefined;
12
+ failKey: string | undefined;
13
+ constructor(key: string, client: Client, pool: Pool | undefined, manager: Manager | undefined);
14
+ listen(handlerPath: string, options?: ListenOptions): Promise<void>;
15
+ dispatch(data: any, options?: JobOptions): Promise<string>;
16
+ cancel(id: string): Promise<boolean>;
17
+ dequeue(count: number): Promise<{
18
+ count: number;
19
+ promise: Promise<unknown[]>;
20
+ }>;
21
+ }
22
+ //# sourceMappingURL=queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,KAAK,EAAe,cAAc,EAAO,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG9F,qBAAa,KAAK;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,IAAI,GAAG,SAAS,CAAC;IACvB,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,cAAc,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IACrD,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;gBAEhB,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS;IAUvF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBvE,QAAQ,CAEV,IAAI,EAAE,GAAG,EACT,OAAO,GAAE,UAAe,GACzB,OAAO,CAAC,MAAM,CAAC;IAcZ,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKpC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KAAE,CAAC;CAyCxF"}
package/dist/queue.js ADDED
@@ -0,0 +1,81 @@
1
+ import { DEFAULT_RETRY_OPTIONS, FAILJOB_RETRY_OPTIONS } from "./constants.js";
2
+ import { generateId } from "./utils.js";
3
+ export class Queue {
4
+ key;
5
+ client;
6
+ pool;
7
+ manager;
8
+ handlerOptions;
9
+ handlerPath;
10
+ failKey;
11
+ constructor(key, client, pool, manager) {
12
+ this.key = key;
13
+ this.client = client;
14
+ this.pool = pool;
15
+ this.manager = manager;
16
+ this.handlerOptions = undefined;
17
+ this.handlerPath = undefined;
18
+ this.failKey = undefined;
19
+ }
20
+ async listen(handlerPath, options = {}) {
21
+ const { failHandler, failRetryOptions, ...retryOptions } = options;
22
+ if (this.client.disconnected)
23
+ throw new Error("Can't listen: client disconnected");
24
+ if (!this.pool || !this.manager)
25
+ throw new Error("Can't listen: non-processing client");
26
+ this.handlerPath = handlerPath;
27
+ this.handlerOptions = { ...DEFAULT_RETRY_OPTIONS, ...retryOptions };
28
+ if (failHandler) {
29
+ this.failKey = `${this.key}-fail`;
30
+ const failQueue = this.client.queue(this.failKey, true);
31
+ failQueue.listen(failHandler, { ...FAILJOB_RETRY_OPTIONS, ...failRetryOptions });
32
+ }
33
+ this.manager.addQueue(this);
34
+ }
35
+ async dispatch(
36
+ // biome-ignore lint/suspicious/noExplicitAny: Data is any serializable value
37
+ data, options = {}) {
38
+ if (this.client.disconnected)
39
+ throw new Error("Can't dispatch: client disconnected");
40
+ const { id = generateId(), runAt = 0, updateData = false, updateRunAt = false, resetCounts = false, } = options;
41
+ await this.client.dispatch(this.key, id, runAt, data, updateData, updateRunAt, resetCounts);
42
+ return id;
43
+ }
44
+ async cancel(id) {
45
+ if (this.client.disconnected)
46
+ throw new Error("Can't cancel: client disconnected");
47
+ return await this.client.cancel(this.key, id);
48
+ }
49
+ async dequeue(count) {
50
+ const pool = this.pool;
51
+ const handlerPath = this.handlerPath;
52
+ const { maxRetries, maxStalls, maxBackoff, minBackoff, size, timeout } = this.handlerOptions;
53
+ const jobs = await this.client.dequeue(this.key, count);
54
+ const promise = Promise.all(jobs.map(async (job) => {
55
+ if (job.stallCount >= maxStalls) {
56
+ if (!this.failKey)
57
+ return this.client.finish(this.key, job.id);
58
+ const failJobData = [job.id, job.data, { message: 'Max stalls exceeded' }];
59
+ return this.client.fail(this.key, this.failKey, job.id, failJobData);
60
+ }
61
+ try {
62
+ await pool.process(handlerPath, job, size, timeout);
63
+ await this.client.finish(this.key, job.id);
64
+ }
65
+ catch (message) {
66
+ const { error } = message;
67
+ const { retryAt = 0, kind } = error;
68
+ if (kind === 'permanent' || job.retryCount >= maxRetries) {
69
+ if (!this.failKey)
70
+ return this.client.finish(this.key, job.id);
71
+ const failJobData = [job.id, job.data, error];
72
+ return this.client.fail(this.key, this.failKey, job.id, failJobData);
73
+ }
74
+ const backoffUntil = Date.now() + Math.min(maxBackoff, minBackoff * 2 ** job.retryCount);
75
+ await this.client.retry(this.key, job.id, Math.max(retryAt, backoffUntil));
76
+ }
77
+ }));
78
+ return { count: jobs.length, promise };
79
+ }
80
+ }
81
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"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"}