@usermetrics/queuebit 1.0.0 → 1.0.5

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.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Load Balancer demo - load balanced message delivery.
3
+ * Messages are distributed round-robin across workers in the same load balancer.
4
+ * Only ONE worker receives each message (unlike regular subscribe where ALL receive).
5
+ *
6
+ * Run this example with: node examples/queuegroup.js
7
+ */
8
+ const { QueueBitServer } = require('../src/server');
9
+ const { QueueBitClient } = require('../src/client-node');
10
+
11
+ const PORT = 3333;
12
+
13
+ // Start QueueBit server in-process
14
+ new QueueBitServer({ port: PORT });
15
+
16
+ async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
17
+
18
+ async function main() {
19
+ await sleep(500); // wait for server to start
20
+
21
+ // Create 3 worker clients - all in the same load balancer 'workers' on subject 'jobs'
22
+ const worker1 = new QueueBitClient(`http://localhost:${PORT}`);
23
+ const worker2 = new QueueBitClient(`http://localhost:${PORT}`);
24
+ const worker3 = new QueueBitClient(`http://localhost:${PORT}`);
25
+
26
+ await sleep(500); // wait for clients to connect
27
+
28
+ await worker1.subscribe((msg) => {
29
+ console.log(`Worker 1 received:`, msg.data);
30
+ }, { subject: 'jobs', queue: 'workers' });
31
+
32
+ await worker2.subscribe((msg) => {
33
+ console.log(`Worker 2 received:`, msg.data);
34
+ }, { subject: 'jobs', queue: 'workers' });
35
+
36
+ await worker3.subscribe((msg) => {
37
+ console.log(`Worker 3 received:`, msg.data);
38
+ }, { subject: 'jobs', queue: 'workers' });
39
+
40
+ // Publisher client
41
+ const publisher = new QueueBitClient(`http://localhost:${PORT}`);
42
+ await sleep(500);
43
+
44
+ console.log('\nPublishing 6 jobs - each worker should receive 2 (round-robin):\n');
45
+
46
+ for (let i = 1; i <= 6; i++) {
47
+ await publisher.publish({ job: `task-${i}`, payload: `data-${i}` }, { subject: 'jobs' });
48
+ await sleep(100);
49
+ }
50
+
51
+ await sleep(500);
52
+ console.log('\nDone. Each worker received ~2 messages.');
53
+ process.exit(0);
54
+ }
55
+
56
+ main();
@@ -124,7 +124,7 @@
124
124
  <h2>Subscribe</h2>
125
125
  <input type="text" id="subscribeSubject" placeholder="Subject" class="small">
126
126
  <button onclick="subscribe()">Subscribe</button>
127
- <button onclick="subscribeQueue()">Queue Group</button>
127
+ <button onclick="subscribeQueue()">Add Load Balancer</button>
128
128
  </div>
129
129
 
130
130
  <div class="col container">
@@ -162,6 +162,7 @@
162
162
  let lastUpdateTime = 0;
163
163
  let publishEndTime = 0;
164
164
  let isConnected = false;
165
+ let lbSubscriptionCount = 0; // track how many LB subscriptions added
165
166
 
166
167
  function connect() {
167
168
  const url = document.getElementById('serverUrl').value;
@@ -194,6 +195,7 @@
194
195
  client.disconnect();
195
196
  client = null;
196
197
  isConnected = false;
198
+ lbSubscriptionCount = 0;
197
199
  document.getElementById('status').textContent = 'Disconnected';
198
200
  document.getElementById('status').style.color = 'black';
199
201
  document.getElementById('serverVersion').textContent = '';
@@ -270,14 +272,25 @@
270
272
  }
271
273
 
272
274
  const subject = document.getElementById('subscribeSubject').value || 'default';
275
+ lbSubscriptionCount++;
276
+ const workerNum = lbSubscriptionCount;
277
+ const uniqueQueueName = `browser-worker-${workerNum}`; // unique name per worker
273
278
 
274
- await client.subscribe((message) => {
279
+ const response = await client.subscribe((message) => {
275
280
  if (!perfTestRunning) {
276
- addMessage(`[Queue: ${subject}] ${JSON.stringify(message.data)}`);
281
+ console.log('LB message received:', JSON.stringify({
282
+ queueName: message.queueName,
283
+ loadBalancerId: message.loadBalancerId,
284
+ subject: message.subject
285
+ }));
286
+ const lbId = message.loadBalancerId !== undefined ? ` LB#${message.loadBalancerId}` : '';
287
+ addMessage(`[Worker #${workerNum}${lbId}: ${subject}] ${JSON.stringify(message.data)}`);
277
288
  }
278
- }, { subject, queue: 'browser-workers' });
289
+ }, { subject, queue: uniqueQueueName });
279
290
 
280
- console.log('Subscribed to queue group:', subject);
291
+ const lbId = response?.loadBalancerId !== undefined ? ` (LB#${response.loadBalancerId})` : '';
292
+ addMessage(`✓ Load Balancer Worker #${workerNum}${lbId} added for subject "${subject}"`);
293
+ console.log(`Worker #${workerNum} subscribed to load balancer: ${subject} (queue: ${uniqueQueueName})`);
281
294
  }
282
295
 
283
296
  function addMessage(text, isPerf = false) {
@@ -350,12 +363,6 @@
350
363
  const totalMessages = parseInt(document.getElementById('testMessageCount').value);
351
364
  console.log('totalMessages:', totalMessages);
352
365
 
353
- const confirmed = confirm(`This will send ${totalMessages.toLocaleString()} messages to the queue. Continue?`);
354
- if (!confirmed) {
355
- console.log('Test cancelled by user');
356
- return;
357
- }
358
-
359
366
  console.log('Starting performance test...');
360
367
  perfTestRunning = true;
361
368
  receivedCount = 0;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Server to server queue.
3
+ * This adds queue functionality to a simple http server (the queue server is running elsewhere)
4
+ * Use the qpanel.html dashboard to publish messages to the queue and see them received by the HTTP server.
5
+ * run the server with /start_server.cmd and then run this example with /examples/start_node_client.cmd
6
+ */
7
+ const http = require('http');
8
+ const { QueueBitClient } = require('../src/client-node');
9
+
10
+ const webserverPORT = 3000;
11
+ const queuebitPORT = 3333;
12
+
13
+ // Initialize a queuebit message queue. It will connect to the server running on port 3333
14
+ const messageQueue = new QueueBitClient(`http://localhost:${queuebitPORT}`);
15
+
16
+ // Create an HTTP server
17
+ const server = http.createServer((req, res) => {
18
+ res.setHeader('Content-Type', 'application/json');
19
+
20
+ if (req.method === 'POST' && req.url === '/enqueue') {
21
+ let body = '';
22
+ req.on('data', chunk => {
23
+ body += chunk.toString();
24
+ });
25
+
26
+ req.on('end', () => {
27
+ try {
28
+ const message = JSON.parse(body);
29
+ messageQueue.enqueue(message);
30
+ res.writeHead(200);
31
+ res.end(JSON.stringify({ success: true, message: 'Message enqueued' }));
32
+ } catch (error) {
33
+ res.writeHead(400);
34
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
35
+ }
36
+ });
37
+ } else if (req.method === 'GET' && req.url === '/dequeue') {
38
+ const message = messageQueue.dequeue();
39
+ res.writeHead(200);
40
+ res.end(JSON.stringify({ message: message || 'Queue is empty' }));
41
+ } else if (req.method === 'GET' && req.url === '/size') {
42
+ res.writeHead(200);
43
+ res.end(JSON.stringify({ size: messageQueue.size() }));
44
+ } else {
45
+ res.writeHead(404);
46
+ res.end(JSON.stringify({ error: 'Not found' }));
47
+ }
48
+ });
49
+
50
+ server.listen(webserverPORT, () => {
51
+ console.log(`Server running at http://localhost:${webserverPORT}`);
52
+ console.log('Endpoints:');
53
+ console.log(' POST /enqueue - Add message to queue');
54
+ console.log(' GET /dequeue - Remove and return message from queue');
55
+ console.log(' GET /size - Get current queue size');
56
+ });
@@ -0,0 +1 @@
1
+ node ./loadbalancer.js
@@ -0,0 +1 @@
1
+ node ./server2server.js
@@ -0,0 +1 @@
1
+ call node inprocessserver.js
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@usermetrics/queuebit",
3
- "version": "1.0.0",
3
+ "version": "1.0.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "A socket server API for using a queue with guaranteed delivery",
7
+ "description": "A high performance queue with guaranteed delivery and built-in load balancer. Runs in-process or as a standalone server. Supports browser clients.",
8
8
  "main": "src/index.js",
9
9
  "browser": "src/client-browser.js",
10
10
  "scripts": {
11
11
  "test": "node test/test-harness.js",
12
- "start": "node src/index.js",
12
+ "start": "node src/server-runner.js",
13
13
  "server": "node src/server-runner.js",
14
+ "auto": "nodemon src/server-runner.js",
14
15
  "prepublishOnly": "npm test"
15
16
  },
16
17
  "keywords": [
@@ -26,11 +27,10 @@
26
27
  "license": "MIT",
27
28
  "dependencies": {
28
29
  "socket.io": "^4.7.2",
30
+ "socket.io-client": "^4.7.2",
29
31
  "uuid": "^9.0.1"
30
32
  },
31
- "devDependencies": {
32
- "socket.io-client": "^4.7.2"
33
- },
33
+ "devDependencies": {},
34
34
  "files": [
35
35
  "src/",
36
36
  "docs/",
@@ -79,12 +79,14 @@ class QueueBitClient {
79
79
 
80
80
  subscribe(callback, options = {}) {
81
81
  const subject = options.subject || 'default';
82
+ const queueName = options.queue || null;
83
+ const handlerKey = queueName ? `${subject}:${queueName}` : subject;
82
84
 
83
- if (!this.messageHandlers.has(subject)) {
84
- this.messageHandlers.set(subject, new Set());
85
+ if (!this.messageHandlers.has(handlerKey)) {
86
+ this.messageHandlers.set(handlerKey, new Set());
85
87
  }
86
88
 
87
- this.messageHandlers.get(subject).add(callback);
89
+ this.messageHandlers.get(handlerKey).add(callback);
88
90
 
89
91
  return new Promise((resolve) => {
90
92
  this.socket.emit('subscribe', options, (response) => {
@@ -95,7 +97,9 @@ class QueueBitClient {
95
97
 
96
98
  unsubscribe(options = {}) {
97
99
  const subject = options.subject || 'default';
98
- this.messageHandlers.delete(subject);
100
+ const queueName = options.queue || null;
101
+ const handlerKey = queueName ? `${subject}:${queueName}` : subject;
102
+ this.messageHandlers.delete(handlerKey);
99
103
 
100
104
  return new Promise((resolve) => {
101
105
  this.socket.emit('unsubscribe', options, (response) => {
@@ -114,8 +118,10 @@ class QueueBitClient {
114
118
 
115
119
  handleMessage(message) {
116
120
  const subject = message.subject || 'default';
117
- const handlers = this.messageHandlers.get(subject);
118
-
121
+ const queueName = message.queueName || null;
122
+ const handlerKey = queueName ? `${subject}:${queueName}` : subject;
123
+
124
+ const handlers = this.messageHandlers.get(handlerKey);
119
125
  if (handlers) {
120
126
  for (const handler of handlers) {
121
127
  handler(message);
@@ -1,7 +1,7 @@
1
1
  const { io } = require('socket.io-client');
2
2
 
3
3
  class QueueBitClient {
4
- constructor(url = 'http://localhost:3000') {
4
+ constructor(url = 'http://localhost:3333') {
5
5
  this.socket = io(url, {
6
6
  transports: ['websocket'],
7
7
  upgrade: false,
@@ -60,12 +60,14 @@ class QueueBitClient {
60
60
 
61
61
  subscribe(callback, options = {}) {
62
62
  const subject = options.subject || 'default';
63
+ const queueName = options.queue || null;
64
+ const handlerKey = queueName ? `${subject}:${queueName}` : subject;
63
65
 
64
- if (!this.messageHandlers.has(subject)) {
65
- this.messageHandlers.set(subject, new Set());
66
+ if (!this.messageHandlers.has(handlerKey)) {
67
+ this.messageHandlers.set(handlerKey, new Set());
66
68
  }
67
69
 
68
- this.messageHandlers.get(subject).add(callback);
70
+ this.messageHandlers.get(handlerKey).add(callback);
69
71
 
70
72
  return new Promise((resolve) => {
71
73
  this.socket.emit('subscribe', options, (response) => {
@@ -76,7 +78,9 @@ class QueueBitClient {
76
78
 
77
79
  unsubscribe(options = {}) {
78
80
  const subject = options.subject || 'default';
79
- this.messageHandlers.delete(subject);
81
+ const queueName = options.queue || null;
82
+ const handlerKey = queueName ? `${subject}:${queueName}` : subject;
83
+ this.messageHandlers.delete(handlerKey);
80
84
 
81
85
  return new Promise((resolve) => {
82
86
  this.socket.emit('unsubscribe', options, (response) => {
@@ -95,8 +99,10 @@ class QueueBitClient {
95
99
 
96
100
  handleMessage(message) {
97
101
  const subject = message.subject || 'default';
98
- const handlers = this.messageHandlers.get(subject);
99
-
102
+ const queueName = message.queueName || null;
103
+ const handlerKey = queueName ? `${subject}:${queueName}` : subject;
104
+
105
+ const handlers = this.messageHandlers.get(handlerKey);
100
106
  if (handlers) {
101
107
  for (const handler of handlers) {
102
108
  handler(message);
@@ -109,4 +115,4 @@ class QueueBitClient {
109
115
  }
110
116
  }
111
117
 
112
- module.exports = { QueueBitClient };
118
+ module.exports = { QueueBitClient, Queue: QueueBitClient };
package/src/client.js CHANGED
@@ -68,6 +68,3 @@ class QueueBitClient {
68
68
  this.socket.disconnect();
69
69
  }
70
70
  }
71
-
72
- // Node.js client (default export for npm package)
73
- module.exports = require('./client-node');
package/src/server.js CHANGED
@@ -10,7 +10,8 @@ class QueueBitServer {
10
10
 
11
11
  this.messages = new Map();
12
12
  this.subscribers = new Map();
13
- this.queueGroups = new Map();
13
+ this.loadBalancers = new Map();
14
+ this.loadBalancerIdCounter = 0;
14
15
  this.deliveryQueue = [];
15
16
  this.deliveryBatchSize = 100;
16
17
  this.isDelivering = false;
@@ -124,36 +125,42 @@ class QueueBitServer {
124
125
 
125
126
  deliverMessage(message) {
126
127
  const subject = message.subject || 'default';
127
- let delivered = false;
128
-
129
- // Deliver to queue groups (load balanced)
130
- const queueGroups = this.queueGroups.get(subject);
131
- if (queueGroups) {
132
- for (const [queueName, sockets] of queueGroups.entries()) {
133
- if (sockets.length > 0) {
134
- const socket = sockets[0];
135
- sockets.push(sockets.shift());
136
-
137
- socket.emit('message', message);
138
- delivered = true;
139
-
140
- if (message.removeAfterRead) {
141
- this.removeMessage(message.id, subject);
142
- return;
143
- }
128
+
129
+ const loadBalancers = this.loadBalancers.get(subject);
130
+ if (loadBalancers && loadBalancers.size > 0) {
131
+ const activeLBs = [];
132
+ for (const [lbName, lb] of loadBalancers.entries()) {
133
+ if (lb.sockets.length > 0) {
134
+ activeLBs.push(lb);
144
135
  }
145
136
  }
137
+
138
+ if (activeLBs.length > 0) {
139
+ if (!this._lbRoundRobinIndex) this._lbRoundRobinIndex = {};
140
+ if (this._lbRoundRobinIndex[subject] === undefined) this._lbRoundRobinIndex[subject] = 0;
141
+
142
+ const idx = this._lbRoundRobinIndex[subject] % activeLBs.length;
143
+ this._lbRoundRobinIndex[subject]++;
144
+
145
+ const lb = activeLBs[idx];
146
+ const socket = lb.sockets[0];
147
+ lb.sockets.push(lb.sockets.shift()); // rotate within LB
148
+
149
+ socket.emit('message', { ...message, loadBalancerId: lb.id, queueName: lb.name });
150
+
151
+ // Always remove from store after LB delivery - LB messages are consumed, not persistent
152
+ this.removeMessage(message.id, subject);
153
+ return;
154
+ }
146
155
  }
147
156
 
148
- // Deliver to regular subscribers (all receive)
157
+ // Only deliver to regular subscribers if no load balancer handled it
149
158
  const subscribers = this.subscribers.get(subject);
150
159
  if (subscribers) {
151
160
  for (const socket of subscribers) {
152
161
  socket.emit('message', message);
153
- delivered = true;
154
162
  }
155
-
156
- if (message.removeAfterRead && !queueGroups) {
163
+ if (message.removeAfterRead) {
157
164
  this.removeMessage(message.id, subject);
158
165
  }
159
166
  }
@@ -161,20 +168,20 @@ class QueueBitServer {
161
168
 
162
169
  handleSubscribe(socket, options, callback) {
163
170
  const subject = options.subject || 'default';
164
- const queueName = options.queue;
171
+ const lbName = options.queue;
165
172
 
166
- if (queueName) {
167
- // Queue group subscription (load balanced)
168
- if (!this.queueGroups.has(subject)) {
169
- this.queueGroups.set(subject, new Map());
173
+ if (lbName) {
174
+ if (!this.loadBalancers.has(subject)) {
175
+ this.loadBalancers.set(subject, new Map());
170
176
  }
171
177
 
172
- const queues = this.queueGroups.get(subject);
173
- if (!queues.has(queueName)) {
174
- queues.set(queueName, []);
178
+ const lbs = this.loadBalancers.get(subject);
179
+ if (!lbs.has(lbName)) {
180
+ lbs.set(lbName, { id: ++this.loadBalancerIdCounter, name: lbName, sockets: [] });
175
181
  }
176
182
 
177
- queues.get(queueName).push(socket);
183
+ lbs.get(lbName).sockets.push(socket);
184
+ // Do NOT replay existing messages to load balancer subscribers
178
185
  } else {
179
186
  // Regular subscription (all subscribers get messages)
180
187
  if (!this.subscribers.has(subject)) {
@@ -182,33 +189,38 @@ class QueueBitServer {
182
189
  }
183
190
 
184
191
  this.subscribers.get(subject).add(socket);
185
- }
186
192
 
187
- // Deliver any existing messages
188
- const messages = this.messages.get(subject) || [];
189
- for (const message of messages) {
190
- if (!message.removeAfterRead) {
191
- socket.emit('message', message);
193
+ // Replay existing messages only for regular subscribers
194
+ const messages = this.messages.get(subject) || [];
195
+ for (const message of messages) {
196
+ if (!message.removeAfterRead) {
197
+ socket.emit('message', message);
198
+ }
192
199
  }
193
200
  }
194
201
 
195
202
  if (callback) {
196
- callback({ success: true, subject, queue: queueName });
203
+ callback({
204
+ success: true,
205
+ subject,
206
+ loadBalancer: lbName,
207
+ loadBalancerId: lbName ? this.loadBalancers.get(subject)?.get(lbName)?.id : undefined
208
+ });
197
209
  }
198
210
  }
199
211
 
200
212
  handleUnsubscribe(socket, options, callback) {
201
213
  const subject = options.subject || 'default';
202
- const queueName = options.queue;
203
-
204
- if (queueName) {
205
- const queues = this.queueGroups.get(subject);
206
- if (queues) {
207
- const sockets = queues.get(queueName);
208
- if (sockets) {
209
- const index = sockets.indexOf(socket);
214
+ const lbName = options.queue;
215
+
216
+ if (lbName) {
217
+ const lbs = this.loadBalancers.get(subject);
218
+ if (lbs) {
219
+ const lb = lbs.get(lbName);
220
+ if (lb) {
221
+ const index = lb.sockets.indexOf(socket);
210
222
  if (index > -1) {
211
- sockets.splice(index, 1);
223
+ lb.sockets.splice(index, 1);
212
224
  }
213
225
  }
214
226
  }
@@ -230,12 +242,12 @@ class QueueBitServer {
230
242
  subscribers.delete(socket);
231
243
  }
232
244
 
233
- // Remove from all queue groups
234
- for (const queues of this.queueGroups.values()) {
235
- for (const sockets of queues.values()) {
236
- const index = sockets.indexOf(socket);
245
+ // Remove from all load balancers
246
+ for (const lbs of this.loadBalancers.values()) {
247
+ for (const lb of lbs.values()) {
248
+ const index = lb.sockets.indexOf(socket);
237
249
  if (index > -1) {
238
- sockets.splice(index, 1);
250
+ lb.sockets.splice(index, 1);
239
251
  }
240
252
  }
241
253
  }