qrusty-client 0.3.0 → 0.5.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/index.js +244 -2
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -23,7 +23,7 @@ class QrustyClient {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Create
|
|
26
|
+
* Create a new queue. Throws an error if the queue already exists.
|
|
27
27
|
* @param {string} name
|
|
28
28
|
* @param {string} [ordering="MaxFirst"] - One of: Fifo | MinFirst | MaxFirst
|
|
29
29
|
* @param {boolean} [allowDuplicates=true]
|
|
@@ -189,4 +189,246 @@ class QrustyClient {
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// WsSession – WebSocket client (WS-0019)
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* DeliveredMessage – a message pushed by the server over a WebSocket subscription.
|
|
198
|
+
* @typedef {Object} DeliveredMessage
|
|
199
|
+
* @property {string} queue
|
|
200
|
+
* @property {string} id
|
|
201
|
+
* @property {string} payload
|
|
202
|
+
* @property {number} priority
|
|
203
|
+
* @property {string} createdAt
|
|
204
|
+
*/
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Persistent WebSocket session to a Qrusty server.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* const session = new WsSession('ws://localhost:6784');
|
|
211
|
+
* await session.connect();
|
|
212
|
+
*
|
|
213
|
+
* // Publish
|
|
214
|
+
* const id = await session.publish('orders', 'hello', 10);
|
|
215
|
+
*
|
|
216
|
+
* // Subscribe (AsyncIterable)
|
|
217
|
+
* for await (const msg of session.subscribe('orders')) {
|
|
218
|
+
* console.log(msg.payload);
|
|
219
|
+
* await session.ack(msg.queue, msg.id);
|
|
220
|
+
* break;
|
|
221
|
+
* }
|
|
222
|
+
*
|
|
223
|
+
* await session.close();
|
|
224
|
+
*
|
|
225
|
+
* @requires ws (npm install ws)
|
|
226
|
+
*/
|
|
227
|
+
class WsSession {
|
|
228
|
+
/**
|
|
229
|
+
* @param {string} addr - Server WebSocket base URL, e.g. 'ws://localhost:6784'
|
|
230
|
+
*/
|
|
231
|
+
constructor(addr) {
|
|
232
|
+
this.addr = addr.replace(/\/$/, "");
|
|
233
|
+
this._ws = null;
|
|
234
|
+
this._reqCounter = 0;
|
|
235
|
+
this._pending = new Map(); // req_id → { resolve, reject }
|
|
236
|
+
this._deliverQueues = new Map(); // queue → Array of waiting resolvers
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// -------------------------------------------------------------------------
|
|
240
|
+
// connect / close
|
|
241
|
+
// -------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
/** Establish the WebSocket connection. */
|
|
244
|
+
async connect() {
|
|
245
|
+
const WebSocket = require("ws");
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
this._ws = new WebSocket(`${this.addr}/ws`);
|
|
248
|
+
this._ws.once("open", resolve);
|
|
249
|
+
this._ws.once("error", reject);
|
|
250
|
+
this._ws.on("message", (raw) => this._onMessage(raw));
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Close the connection gracefully. */
|
|
255
|
+
async close() {
|
|
256
|
+
return new Promise((resolve) => {
|
|
257
|
+
if (!this._ws) return resolve();
|
|
258
|
+
this._ws.once("close", resolve);
|
|
259
|
+
this._ws.close();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// -------------------------------------------------------------------------
|
|
264
|
+
// Internal helpers
|
|
265
|
+
// -------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
_nextReqId() {
|
|
268
|
+
return `req-${++this._reqCounter}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_onMessage(raw) {
|
|
272
|
+
let frame;
|
|
273
|
+
try {
|
|
274
|
+
frame = JSON.parse(raw.toString());
|
|
275
|
+
} catch {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const reqId = frame.req_id;
|
|
280
|
+
if (reqId && this._pending.has(reqId)) {
|
|
281
|
+
const { resolve, reject } = this._pending.get(reqId);
|
|
282
|
+
this._pending.delete(reqId);
|
|
283
|
+
if (frame.type === "error") {
|
|
284
|
+
reject(new Error(`server error [${frame.code}]: ${frame.message}`));
|
|
285
|
+
} else {
|
|
286
|
+
resolve(frame);
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (frame.type === "deliver") {
|
|
292
|
+
const queue = frame.queue || "";
|
|
293
|
+
if (this._deliverQueues.has(queue)) {
|
|
294
|
+
const { resolve } = this._deliverQueues.get(queue).shift() || {};
|
|
295
|
+
if (resolve) {
|
|
296
|
+
resolve({
|
|
297
|
+
queue,
|
|
298
|
+
id: frame.id,
|
|
299
|
+
payload: frame.payload,
|
|
300
|
+
priority: frame.priority,
|
|
301
|
+
createdAt: frame.created_at,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async _request(frame) {
|
|
309
|
+
const reqId = this._nextReqId();
|
|
310
|
+
frame.req_id = reqId;
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
this._pending.set(reqId, { resolve, reject });
|
|
313
|
+
this._ws.send(JSON.stringify(frame), (err) => {
|
|
314
|
+
if (err) {
|
|
315
|
+
this._pending.delete(reqId);
|
|
316
|
+
reject(err);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// -------------------------------------------------------------------------
|
|
323
|
+
// Public API (WS-0019)
|
|
324
|
+
// -------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Publish a message to a queue.
|
|
328
|
+
* @param {string} queue
|
|
329
|
+
* @param {string} payload
|
|
330
|
+
* @param {number} [priority=0]
|
|
331
|
+
* @returns {Promise<string>} The assigned message id.
|
|
332
|
+
*/
|
|
333
|
+
async publish(queue, payload, priority = 0) {
|
|
334
|
+
const resp = await this._request({
|
|
335
|
+
type: "publish",
|
|
336
|
+
queue,
|
|
337
|
+
payload,
|
|
338
|
+
priority,
|
|
339
|
+
});
|
|
340
|
+
return resp.id;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Subscribe to a queue.
|
|
345
|
+
* Returns an AsyncIterable that yields DeliveredMessage objects.
|
|
346
|
+
* @param {string} queue
|
|
347
|
+
* @returns {AsyncIterable<DeliveredMessage>}
|
|
348
|
+
*/
|
|
349
|
+
subscribe(queue) {
|
|
350
|
+
// Ensure delivery queue exists.
|
|
351
|
+
if (!this._deliverQueues.has(queue)) {
|
|
352
|
+
this._deliverQueues.set(queue, []);
|
|
353
|
+
}
|
|
354
|
+
// Send subscribe frame (fire-and-forget; ok response is consumed internally).
|
|
355
|
+
this._request({ type: "subscribe", queue }).catch(() => {});
|
|
356
|
+
|
|
357
|
+
const self = this;
|
|
358
|
+
return {
|
|
359
|
+
[Symbol.asyncIterator]() {
|
|
360
|
+
return {
|
|
361
|
+
next() {
|
|
362
|
+
return new Promise((resolve, reject) => {
|
|
363
|
+
const waiters = self._deliverQueues.get(queue) || [];
|
|
364
|
+
waiters.push({
|
|
365
|
+
resolve: (msg) => resolve({ value: msg, done: false }),
|
|
366
|
+
reject,
|
|
367
|
+
});
|
|
368
|
+
if (!self._deliverQueues.has(queue)) {
|
|
369
|
+
self._deliverQueues.set(queue, waiters);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
},
|
|
373
|
+
return() {
|
|
374
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Unsubscribe from a queue.
|
|
383
|
+
* @param {string} queue
|
|
384
|
+
* @returns {Promise<void>}
|
|
385
|
+
*/
|
|
386
|
+
async unsubscribe(queue) {
|
|
387
|
+
await this._request({ type: "unsubscribe", queue });
|
|
388
|
+
this._deliverQueues.delete(queue);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Acknowledge a message.
|
|
393
|
+
* @param {string} queue
|
|
394
|
+
* @param {string} id
|
|
395
|
+
* @returns {Promise<void>}
|
|
396
|
+
*/
|
|
397
|
+
async ack(queue, id) {
|
|
398
|
+
await this._request({ type: "ack", queue, id });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Negative-acknowledge a message.
|
|
403
|
+
* @param {string} queue
|
|
404
|
+
* @param {string} id
|
|
405
|
+
* @returns {Promise<void>}
|
|
406
|
+
*/
|
|
407
|
+
async nack(queue, id) {
|
|
408
|
+
await this._request({ type: "nack", queue, id });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Batch-acknowledge multiple messages.
|
|
413
|
+
* @param {string} queue
|
|
414
|
+
* @param {string[]} ids
|
|
415
|
+
* @returns {Promise<number>} Count of acked messages.
|
|
416
|
+
*/
|
|
417
|
+
async batchAck(queue, ids) {
|
|
418
|
+
const resp = await this._request({ type: "batch-ack", queue, ids });
|
|
419
|
+
return resp.acked;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Batch-negative-acknowledge multiple messages.
|
|
424
|
+
* @param {string} queue
|
|
425
|
+
* @param {string[]} ids
|
|
426
|
+
* @returns {Promise<{unlocked: number, dropped: number}>}
|
|
427
|
+
*/
|
|
428
|
+
async batchNack(queue, ids) {
|
|
429
|
+
const resp = await this._request({ type: "batch-nack", queue, ids });
|
|
430
|
+
return { unlocked: resp.unlocked, dropped: resp.dropped };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
module.exports = { QrustyClient, WsSession };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qrusty-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Node.js client for the qrusty priority queue server API.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Gordon Greene <greeng3@obscure-reference.com>",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"docs": "jsdoc -c jsdoc.json"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"axios": "^1.13.
|
|
13
|
+
"axios": "^1.13.5"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"jest": "30.2.0",
|