@usermetrics/queuebit 1.0.0 → 1.0.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.
- package/README.md +7 -88
- package/examples/inprocessserver.js +60 -0
- package/examples/loadbalancer.js +56 -0
- package/examples/{browser-example.html → qpanel.html} +18 -11
- package/examples/server2server.js +56 -0
- package/examples/start_load_balancer.cmd +1 -0
- package/examples/start_node_client.cmd +1 -0
- package/examples/start_node_inprocess.cmd +1 -0
- package/package.json +3 -2
- package/src/client-browser.js +12 -6
- package/src/client-node.js +14 -8
- package/src/server.js +64 -52
package/README.md
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# QueueBit
|
|
2
2
|
|
|
3
|
-
A high performance socket-based message queue server with guaranteed delivery, compatible with NATS queue patterns.
|
|
3
|
+
A high performance socket-based message queue server with guaranteed delivery, compatible with NATS queue patterns.
|
|
4
|
+
Built in Load Balancer. (see examples).
|
|
4
5
|
|
|
5
|
-
It can run in-process in an existing nodejs app, separately as a nodejs server,
|
|
6
|
-
|
|
6
|
+
It can run in-process in an existing nodejs app, separately as a nodejs server, or run clients
|
|
7
|
+
in the backend and/or frontend.
|
|
7
8
|
|
|
8
9
|
## Features
|
|
9
10
|
|
|
10
11
|
- WebSocket-based message queue
|
|
11
12
|
- Subject-based message routing
|
|
12
|
-
-
|
|
13
|
+
- Load-balancer
|
|
13
14
|
- Message expiry support
|
|
14
15
|
- Remove after read (ephemeral messages)
|
|
15
16
|
- Guaranteed delivery to all subscribers
|
|
@@ -82,7 +83,8 @@ Include Socket.IO and QueueBit client in your HTML:
|
|
|
82
83
|
</script>
|
|
83
84
|
```
|
|
84
85
|
|
|
85
|
-
See `examples/
|
|
86
|
+
See `examples/qpanel.html` for a complete browser example.
|
|
87
|
+
Open it with Live Server in vscode to test.
|
|
86
88
|
|
|
87
89
|
### Server
|
|
88
90
|
|
|
@@ -185,89 +187,6 @@ Unsubscribe from messages.
|
|
|
185
187
|
##### `disconnect()`
|
|
186
188
|
Disconnect from the server.
|
|
187
189
|
|
|
188
|
-
## Publishing to NPM
|
|
189
|
-
|
|
190
|
-
### First Time Setup
|
|
191
|
-
|
|
192
|
-
1. Create an NPM account at https://www.npmjs.com/signup
|
|
193
|
-
2. Run authentication setup:
|
|
194
|
-
```cmd
|
|
195
|
-
setup-npm-auth.cmd
|
|
196
|
-
```
|
|
197
|
-
3. Update package.json with your username:
|
|
198
|
-
- Change `@yourusername/queuebit` to `@YOUR_NPM_USERNAME/queuebit`
|
|
199
|
-
- Or use an unscoped name like `queuebit-yourname` if available
|
|
200
|
-
- Update author field with your information
|
|
201
|
-
|
|
202
|
-
### Publishing
|
|
203
|
-
|
|
204
|
-
1. Update version number:
|
|
205
|
-
```cmd
|
|
206
|
-
update-version.cmd
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
2. **Without 2FA:**
|
|
210
|
-
```cmd
|
|
211
|
-
publish.cmd
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
3. **With 2FA enabled:**
|
|
215
|
-
```cmd
|
|
216
|
-
publish-with-otp.cmd
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
### Troubleshooting
|
|
220
|
-
|
|
221
|
-
- **403 Forbidden / 2FA Required**: Use `publish-with-otp.cmd`
|
|
222
|
-
- **Package name taken**: Change name in package.json to something unique
|
|
223
|
-
- **Not logged in**: Run `npm login` or `setup-npm-auth.cmd`
|
|
224
|
-
- **Version already exists**: Increment version with `update-version.cmd`
|
|
225
|
-
|
|
226
|
-
## Development
|
|
227
|
-
|
|
228
|
-
### Install Dependencies
|
|
229
|
-
```bash
|
|
230
|
-
npm install
|
|
231
|
-
```
|
|
232
|
-
Or on Windows:
|
|
233
|
-
```cmd
|
|
234
|
-
install-deps.cmd
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### Run Tests
|
|
238
|
-
```bash
|
|
239
|
-
npm test
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### Publishing
|
|
243
|
-
|
|
244
|
-
#### Update Version
|
|
245
|
-
```cmd
|
|
246
|
-
update-version.cmd
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
#### Dry Run (test without publishing)
|
|
250
|
-
```cmd
|
|
251
|
-
publish-dry-run.cmd
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
#### Publish to NPM
|
|
255
|
-
```cmd
|
|
256
|
-
publish.cmd
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
Or manually:
|
|
260
|
-
```bash
|
|
261
|
-
npm login
|
|
262
|
-
npm test
|
|
263
|
-
npm publish
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
## Testing
|
|
267
|
-
|
|
268
|
-
```bash
|
|
269
|
-
npm test
|
|
270
|
-
```
|
|
271
190
|
|
|
272
191
|
## License
|
|
273
192
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process QueueBit demo.
|
|
3
|
+
* Starts the QueueBit server and an HTTP server in the same process.
|
|
4
|
+
* Use the qpanel.html dashboard to publish messages to the queue and see them received by the HTTP server.
|
|
5
|
+
* Run this example with /examples/start_node_inprocess.cmd
|
|
6
|
+
* No need to run a separate QueueBit server.
|
|
7
|
+
*/
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const { QueueBitServer } = require('../src/server');
|
|
10
|
+
const { QueueBitClient } = require('../src/client-node');
|
|
11
|
+
|
|
12
|
+
const webserverPORT = 3000;
|
|
13
|
+
const queuebitPORT = 3333;
|
|
14
|
+
|
|
15
|
+
// Start the QueueBit server in-process (constructor starts listening immediately)
|
|
16
|
+
const queuebitServer = new QueueBitServer({ port: queuebitPORT });
|
|
17
|
+
|
|
18
|
+
// Connect a client to the in-process QueueBit server
|
|
19
|
+
// this is just for testing. typically you would connect from another process/frontend/server
|
|
20
|
+
const messageQueue = new QueueBitClient(`http://localhost:${queuebitPORT}`);
|
|
21
|
+
|
|
22
|
+
messageQueue.subscribe((msg) => {
|
|
23
|
+
console.log('Received message from queue:', msg);
|
|
24
|
+
}, { subject: 'default' });
|
|
25
|
+
|
|
26
|
+
// Create an HTTP server
|
|
27
|
+
const server = http.createServer((req, res) => {
|
|
28
|
+
res.setHeader('Content-Type', 'application/json');
|
|
29
|
+
|
|
30
|
+
if (req.method === 'POST' && req.url === '/enqueue') {
|
|
31
|
+
let body = '';
|
|
32
|
+
req.on('data', chunk => { body += chunk.toString(); });
|
|
33
|
+
req.on('end', async () => {
|
|
34
|
+
try {
|
|
35
|
+
const message = JSON.parse(body);
|
|
36
|
+
const result = await messageQueue.publish(message);
|
|
37
|
+
res.writeHead(200);
|
|
38
|
+
res.end(JSON.stringify({ success: true, result }));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
res.writeHead(400);
|
|
41
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
} else if (req.method === 'GET' && req.url === '/messages') {
|
|
45
|
+
messageQueue.getMessages({ subject: 'default' }).then((result) => {
|
|
46
|
+
res.writeHead(200);
|
|
47
|
+
res.end(JSON.stringify(result));
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
res.writeHead(404);
|
|
51
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
server.listen(webserverPORT, () => {
|
|
56
|
+
console.log(`HTTP server running at http://localhost:${webserverPORT}`);
|
|
57
|
+
console.log('Endpoints:');
|
|
58
|
+
console.log(' POST /enqueue - Publish message to QueueBit');
|
|
59
|
+
console.log(' GET /messages - Get messages from QueueBit');
|
|
60
|
+
});
|
|
@@ -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()">
|
|
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
|
-
|
|
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:
|
|
289
|
+
}, { subject, queue: uniqueQueueName });
|
|
279
290
|
|
|
280
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usermetrics/queuebit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"browser": "src/client-browser.js",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node test/test-harness.js",
|
|
12
|
-
"start": "node src/
|
|
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": [
|
package/src/client-browser.js
CHANGED
|
@@ -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(
|
|
84
|
-
this.messageHandlers.set(
|
|
85
|
+
if (!this.messageHandlers.has(handlerKey)) {
|
|
86
|
+
this.messageHandlers.set(handlerKey, new Set());
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
this.messageHandlers.get(
|
|
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
|
-
|
|
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
|
|
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);
|
package/src/client-node.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { io } = require('socket.io-client');
|
|
2
2
|
|
|
3
3
|
class QueueBitClient {
|
|
4
|
-
constructor(url = 'http://localhost:
|
|
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(
|
|
65
|
-
this.messageHandlers.set(
|
|
66
|
+
if (!this.messageHandlers.has(handlerKey)) {
|
|
67
|
+
this.messageHandlers.set(handlerKey, new Set());
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
this.messageHandlers.get(
|
|
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
|
-
|
|
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
|
|
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/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.
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
//
|
|
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
|
|
171
|
+
const lbName = options.queue;
|
|
165
172
|
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
173
|
-
if (!
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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({
|
|
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
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
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
|
|
234
|
-
for (const
|
|
235
|
-
for (const
|
|
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
|
}
|