queasy 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import EventEmitter from 'node:events';
2
2
  import { readFileSync } from 'node:fs';
3
+ import os from 'node:os';
3
4
  import { dirname, join } from 'node:path';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { getEnvironmentData } from 'node:worker_threads';
7
+ import { createClient, createCluster } from 'redis';
6
8
  import { HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, LUA_FUNCTIONS_VERSION } from './constants.js';
7
9
  import { Manager } from './manager.js';
8
10
  import { Pool } from './pool.js';
@@ -18,7 +20,7 @@ const luaScript = readFileSync(join(__dirname, 'queasy.lua'), 'utf8').replace(
18
20
  /**
19
21
  * Check the installed version and load our Lua functions if needed.
20
22
  * Returns true if this client should be disconnected (newer major on server).
21
- * @param {RedisClient} redis
23
+ * @param {{ fCall: Function, sendCommand: Function }} redis
22
24
  * @returns {Promise<boolean>} Whether to disconnect.
23
25
  */
24
26
  async function installLuaFunctions(redis) {
@@ -38,9 +40,19 @@ async function installLuaFunctions(redis) {
38
40
  return installedVersion[0] > availableVersion[0];
39
41
  }
40
42
 
41
- /** @typedef {import('redis').RedisClientType} RedisClient */
43
+ /** @typedef {import('./types').RedisOptions} RedisOptions */
42
44
  /** @typedef {import('./types').Job} Job */
43
45
 
46
+ /**
47
+ * @param {RedisOptions} options
48
+ */
49
+ function buildRedisConnection(options) {
50
+ if ('rootNodes' in options) {
51
+ return createCluster(options);
52
+ }
53
+ return createClient(options);
54
+ }
55
+
44
56
  /** @typedef {{ queue: Queue, bumpTimer?: NodeJS.Timeout }} QueueEntry */
45
57
 
46
58
  /**
@@ -70,13 +82,13 @@ export function parseJob(jobArray) {
70
82
 
71
83
  export class Client extends EventEmitter {
72
84
  /**
73
- * @param {RedisClient} redis - Redis client
74
- * @param {number?} workerCount - Allow this client to dequeue jobs.
85
+ * @param {RedisOptions} options - Redis connection options
86
+ * @param {number} [workerCount] - Allow this client to dequeue jobs.
75
87
  * @param {((client: Client) => any)} [callback] - Callback when client is ready
76
88
  */
77
- constructor(redis, workerCount, callback) {
89
+ constructor(options = {}, workerCount = os.cpus().length, callback) {
78
90
  super();
79
- this.redis = redis;
91
+ this.redis = buildRedisConnection(options);
80
92
  this.clientId = generateId();
81
93
 
82
94
  /** @type {Record<string, QueueEntry>} */
@@ -90,11 +102,21 @@ export class Client extends EventEmitter {
90
102
  // Not awaited — the Lua script is read synchronously at module load,
91
103
  // so Redis' single-threaded ordering ensures the FUNCTION LOAD completes
92
104
  // before any subsequent fCalls from user code.
93
- installLuaFunctions(this.redis).then((disconnect) => {
94
- this.disconnected = disconnect;
95
- if (disconnect) this.emit('disconnected', 'Redis has incompatible queasy version.');
96
- else if (callback) callback(this);
97
- });
105
+ this.redis
106
+ .connect()
107
+ .then(() => installLuaFunctions(this.redis))
108
+ .then((disconnect) => {
109
+ this.disconnected = disconnect;
110
+ if (disconnect) this.emit('disconnected', 'Redis has incompatible queasy version.');
111
+ else {
112
+ this.emit('connected');
113
+ if (callback) callback(this);
114
+ }
115
+ })
116
+ .catch((err) => {
117
+ this.disconnected = true;
118
+ this.emit('disconnected', err.message);
119
+ });
98
120
  }
99
121
 
100
122
  /**
@@ -155,6 +177,7 @@ export class Client extends EventEmitter {
155
177
  this.queues = {};
156
178
  this.pool = undefined;
157
179
  this.disconnected = true;
180
+ await this.redis.quit().catch(() => {});
158
181
  }
159
182
 
160
183
  /**
@@ -211,7 +234,11 @@ export class Client extends EventEmitter {
211
234
  // Heartbeats should start with the first dequeue.
212
235
  this.scheduleBump(key);
213
236
 
214
- return /** @type Job[] */ (result.map((jobArray) => parseJob(jobArray)).filter(Boolean));
237
+ const jobs = /** @type Job[] */ (
238
+ result.map((jobArray) => parseJob(jobArray)).filter(Boolean)
239
+ );
240
+ for (const job of jobs) this.emit('dequeue', key, job);
241
+ return jobs;
215
242
  }
216
243
 
217
244
  /**
@@ -223,6 +250,7 @@ export class Client extends EventEmitter {
223
250
  keys: [key],
224
251
  arguments: [jobId, this.clientId, String(Date.now())],
225
252
  });
253
+ this.emit('finish', key, jobId);
226
254
  }
227
255
 
228
256
  /**
@@ -242,6 +270,7 @@ export class Client extends EventEmitter {
242
270
  String(Date.now()),
243
271
  ],
244
272
  });
273
+ this.emit('fail', key, jobId);
245
274
  }
246
275
 
247
276
  /**
@@ -254,5 +283,6 @@ export class Client extends EventEmitter {
254
283
  keys: [key],
255
284
  arguments: [jobId, this.clientId, String(retryAt), String(Date.now())],
256
285
  });
286
+ this.emit('retry', key, jobId);
257
287
  }
258
288
  }
package/src/constants.js CHANGED
@@ -30,7 +30,7 @@ export const FAILJOB_RETRY_OPTIONS = {
30
30
  priority: 100,
31
31
  };
32
32
 
33
- export const LUA_FUNCTIONS_VERSION = '1.0';
33
+ export const LUA_FUNCTIONS_VERSION = '1.0.1';
34
34
  export const HEARTBEAT_INTERVAL = 5000; // 5 seconds
35
35
  export const HEARTBEAT_TIMEOUT = 10000; // 10 seconds
36
36
  export const WORKER_CAPACITY = 10;
package/src/queasy.lua CHANGED
@@ -226,9 +226,8 @@ local function dequeue(queue_key, client_id, now, expiry, limit)
226
226
  table.insert(result, job)
227
227
  end
228
228
 
229
- if #result > 0 then
230
- redis.call('ZADD', expiry_key, expiry, client_id)
231
- end
229
+ -- Add this client to queue and bump its expiry
230
+ redis.call('ZADD', expiry_key, expiry, client_id)
232
231
 
233
232
  -- Sweep stalled clients
234
233
  sweep(queue_key, now)
package/src/types.ts CHANGED
@@ -1,3 +1,18 @@
1
+ import type { RedisClientOptions, RedisClusterOptions } from 'redis';
2
+
3
+ type SingleNodeOptions = Pick<
4
+ RedisClientOptions,
5
+ 'url' | 'socket' | 'username' | 'password' | 'database'
6
+ >;
7
+
8
+ export type RedisOptions =
9
+ | SingleNodeOptions
10
+ | {
11
+ rootNodes: SingleNodeOptions[];
12
+ defaults?: Partial<SingleNodeOptions>;
13
+ nodeAddressMap?: RedisClusterOptions['nodeAddressMap'];
14
+ };
15
+
1
16
  /**
2
17
  * Core job identification and data
3
18
  */
@@ -14,7 +14,7 @@ describe.skip('Client heartbeat', () => {
14
14
  beforeEach(async () => {
15
15
  redis = createClient();
16
16
  await redis.connect();
17
- client = new Client(redis, 1);
17
+ client = new Client({}, 1);
18
18
  client.scheduleBump = mock.fn();
19
19
  if (client.manager) client.manager.addQueue = mock.fn();
20
20
  });
@@ -4,22 +4,17 @@ import { createClient } from 'redis';
4
4
  import { Client } from '../src/index.js';
5
5
 
6
6
  describe('Client disconnect guard', () => {
7
- /** @type {import('redis').RedisClientType} */
8
- let redis;
9
7
  /** @type {import('../src/client.js').Client} */
10
8
  let client;
11
9
 
12
- beforeEach(async () => {
13
- redis = createClient();
14
- await redis.connect();
10
+ beforeEach(() => {
15
11
  return new Promise((res) => {
16
- client = new Client(redis, 0, res);
12
+ client = new Client({}, 0, res);
17
13
  });
18
14
  });
19
15
 
20
16
  afterEach(async () => {
21
17
  await client.close();
22
- await redis.quit();
23
18
  });
24
19
 
25
20
  it('should throw from queue() when disconnected', () => {
@@ -29,23 +24,18 @@ describe('Client disconnect guard', () => {
29
24
  });
30
25
 
31
26
  describe('Queue disconnect guards', () => {
32
- /** @type {import('redis').RedisClientType} */
33
- let redis;
34
27
  /** @type {import('../src/client.js').Client} */
35
28
  let client;
36
29
 
37
- beforeEach(async () => {
38
- redis = createClient();
39
- await redis.connect();
40
- client = new Client(redis, 1);
41
- if (client.manager) client.manager.addQueue = mock.fn();
42
- // Wait for installLuaFunctions to settle
43
- await new Promise((r) => setTimeout(r, 50));
30
+ beforeEach(() => {
31
+ return new Promise((res) => {
32
+ client = new Client({}, 1, res);
33
+ if (client.manager) client.manager.addQueue = mock.fn();
34
+ });
44
35
  });
45
36
 
46
37
  afterEach(async () => {
47
38
  await client.close();
48
- await redis.quit();
49
39
  });
50
40
 
51
41
  it('should throw from dispatch() when disconnected', async () => {
@@ -67,12 +57,10 @@ describe('Queue disconnect guards', () => {
67
57
  });
68
58
 
69
59
  it('should throw from listen() on non-processing client', async () => {
70
- const nonProcessingClient = new Client(redis, 0);
71
- // Wait for installLuaFunctions to settle
72
- await new Promise((r) => setTimeout(r, 50));
60
+ const nonProcessingClient = await new Promise((res) => new Client({}, 0, res));
73
61
  const q = nonProcessingClient.queue('guard-test');
74
62
  await assert.rejects(() => q.listen('/some/handler.js'), /non-processing/);
75
- await client.close();
63
+ await nonProcessingClient.close();
76
64
  });
77
65
  });
78
66
 
@@ -90,8 +78,10 @@ describe('Client bump lost lock', () => {
90
78
  const keys = await redis.keys(`{${QUEUE_NAME}}*`);
91
79
  if (keys.length > 0) await redis.del(keys);
92
80
 
93
- client = new Client(redis, 1);
94
- if (client.manager) client.manager.addQueue = mock.fn();
81
+ await new Promise((res) => {
82
+ client = new Client({}, 1, res);
83
+ if (client.manager) client.manager.addQueue = mock.fn();
84
+ });
95
85
  });
96
86
 
97
87
  afterEach(async () => {
@@ -163,7 +163,9 @@ describe('Manager scheduling', () => {
163
163
  if (keys.length > 0) await redis.del(keys);
164
164
 
165
165
  // Do NOT mock manager.addQueue — let the manager drive dequeuing
166
- client = new Client(redis, 1);
166
+ await new Promise((res) => {
167
+ client = new Client({}, 1, res);
168
+ });
167
169
  });
168
170
 
169
171
  afterEach(async () => {
package/test/pool.test.js CHANGED
@@ -111,8 +111,10 @@ describe('Pool stall and timeout', () => {
111
111
  const keys = await redis.keys(`{${QUEUE_NAME}}*`);
112
112
  if (keys.length > 0) await redis.del(keys);
113
113
 
114
- client = new Client(redis, 1);
115
- if (client.manager) client.manager.addQueue = mock.fn();
114
+ await new Promise((res) => {
115
+ client = new Client({}, 1, res);
116
+ if (client.manager) client.manager.addQueue = mock.fn();
117
+ });
116
118
  });
117
119
 
118
120
  afterEach(async () => {
@@ -19,10 +19,11 @@ describe('Queue E2E', () => {
19
19
  await redis.del(keys);
20
20
  }
21
21
 
22
- client = new Client(redis, 1);
23
-
24
- // Mock this so that no actual work is dequeued by the manager.
25
- if (client.manager) client.manager.addQueue = mock.fn();
22
+ await new Promise((res) => {
23
+ client = new Client({}, 1, res);
24
+ // Mock this so that no actual work is dequeued by the manager.
25
+ if (client.manager) client.manager.addQueue = mock.fn();
26
+ });
26
27
  });
27
28
 
28
29
  afterEach(async () => {