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.
Files changed (2) hide show
  1. package/index.js +244 -2
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -23,7 +23,7 @@ class QrustyClient {
23
23
  }
24
24
 
25
25
  /**
26
- * Create or configure a queue
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
- module.exports = QrustyClient;
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.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.2"
13
+ "axios": "^1.13.5"
14
14
  },
15
15
  "devDependencies": {
16
16
  "jest": "30.2.0",