@usermetrics/queuebit 1.0.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.
@@ -0,0 +1,573 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>QueueBit Browser Example</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ max-width: 1200px;
11
+ margin: 20px auto;
12
+ padding: 10px;
13
+ font-size: 14px;
14
+ }
15
+ h1 {
16
+ margin: 0 0 15px 0;
17
+ font-size: 24px;
18
+ }
19
+ h2 {
20
+ margin: 0 0 8px 0;
21
+ font-size: 16px;
22
+ }
23
+ .row {
24
+ display: flex;
25
+ gap: 10px;
26
+ margin-bottom: 10px;
27
+ }
28
+ .col {
29
+ flex: 1;
30
+ }
31
+ .container {
32
+ border: 1px solid #ccc;
33
+ padding: 10px;
34
+ border-radius: 3px;
35
+ background: #f9f9f9;
36
+ }
37
+ button {
38
+ padding: 6px 12px;
39
+ margin: 2px;
40
+ cursor: pointer;
41
+ font-size: 12px;
42
+ }
43
+ button:disabled {
44
+ opacity: 0.5;
45
+ cursor: not-allowed;
46
+ }
47
+ #messages {
48
+ border: 1px solid #ddd;
49
+ padding: 8px;
50
+ height: 300px;
51
+ overflow-y: auto;
52
+ background: white;
53
+ font-size: 12px;
54
+ }
55
+ .message {
56
+ padding: 3px;
57
+ margin: 2px 0;
58
+ background: #f0f0f0;
59
+ border-left: 3px solid #4CAF50;
60
+ padding-left: 8px;
61
+ }
62
+ .perf-message {
63
+ border-left-color: #2196F3;
64
+ font-weight: bold;
65
+ }
66
+ input, select {
67
+ padding: 5px;
68
+ font-size: 12px;
69
+ margin: 2px;
70
+ }
71
+ input.small {
72
+ width: 150px;
73
+ }
74
+ input.medium {
75
+ width: 200px;
76
+ }
77
+ select {
78
+ width: 120px;
79
+ }
80
+ #perfStats {
81
+ background: #e3f2fd;
82
+ padding: 8px;
83
+ margin: 5px 0;
84
+ border-radius: 3px;
85
+ display: none;
86
+ font-size: 12px;
87
+ }
88
+ #status {
89
+ margin: 5px 0;
90
+ font-weight: bold;
91
+ }
92
+ #serverVersion {
93
+ font-size: 11px;
94
+ color: #666;
95
+ margin-top: 3px;
96
+ }
97
+ </style>
98
+ </head>
99
+ <body>
100
+ <h1>QueueBit Browser Client</h1>
101
+
102
+ <div class="row">
103
+ <div class="col container">
104
+ <h2>Connection</h2>
105
+ <input type="text" id="serverUrl" value="http://localhost:3333" class="medium">
106
+ <button onclick="connect()">Connect</button>
107
+ <button onclick="disconnect()">Disconnect</button>
108
+ <div id="status">Not connected</div>
109
+ <div id="serverVersion"></div>
110
+ </div>
111
+
112
+ <div class="col container">
113
+ <h2>Publish</h2>
114
+ <input type="text" id="messageText" placeholder="Message text" class="medium">
115
+ <input type="text" id="subject" placeholder="Subject" class="small">
116
+ <br>
117
+ <button onclick="publishMessage()">Publish</button>
118
+ <button onclick="publishEphemeral()">Ephemeral</button>
119
+ </div>
120
+ </div>
121
+
122
+ <div class="row">
123
+ <div class="col container">
124
+ <h2>Subscribe</h2>
125
+ <input type="text" id="subscribeSubject" placeholder="Subject" class="small">
126
+ <button onclick="subscribe()">Subscribe</button>
127
+ <button onclick="subscribeQueue()">Queue Group</button>
128
+ </div>
129
+
130
+ <div class="col container">
131
+ <h2>Queue Operations</h2>
132
+ <input type="text" id="getMessagesSubject" placeholder="Subject" class="small">
133
+ <button onclick="getAllMessages()">Get All</button>
134
+ <button onclick="clearMessages()">Clear Display</button>
135
+ <br>
136
+ <select id="testMessageCount">
137
+ <option value="100">100 messages</option>
138
+ <option value="1000">1,000 messages</option>
139
+ <option value="10000">10,000 messages</option>
140
+ <option value="100000">100,000 messages</option>
141
+ <option value="1000000" selected>1,000,000 messages</option>
142
+ </select>
143
+ <button id="perfTestBtn" onclick="runPerformanceTest()">Run Test</button>
144
+ </div>
145
+ </div>
146
+
147
+ <div class="container">
148
+ <h2>Messages</h2>
149
+ <div id="perfStats"></div>
150
+ <div id="messages"></div>
151
+ </div>
152
+
153
+ <!-- Include Socket.IO client from CDN -->
154
+ <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
155
+ <!-- Include QueueBit client -->
156
+ <script src="../src/client-browser.js"></script>
157
+
158
+ <script>
159
+ let client = null;
160
+ let perfTestRunning = false;
161
+ let receivedCount = 0;
162
+ let lastUpdateTime = 0;
163
+ let publishEndTime = 0;
164
+ let isConnected = false;
165
+
166
+ function connect() {
167
+ const url = document.getElementById('serverUrl').value;
168
+ client = new QueueBitClient(url);
169
+
170
+ // Listen for connection status
171
+ client.socket.on('connect', () => {
172
+ isConnected = true;
173
+ document.getElementById('status').textContent = 'Connected to ' + url;
174
+ document.getElementById('status').style.color = 'green';
175
+ });
176
+
177
+ client.socket.on('disconnect', () => {
178
+ isConnected = false;
179
+ document.getElementById('status').textContent = 'Disconnected';
180
+ document.getElementById('status').style.color = 'red';
181
+ });
182
+
183
+ // Listen for server info
184
+ client.socket.on('serverInfo', (info) => {
185
+ document.getElementById('serverVersion').textContent = `Server: v${info.version}`;
186
+ });
187
+
188
+ document.getElementById('status').textContent = 'Connecting...';
189
+ document.getElementById('status').style.color = 'orange';
190
+ }
191
+
192
+ function disconnect() {
193
+ if (client) {
194
+ client.disconnect();
195
+ client = null;
196
+ isConnected = false;
197
+ document.getElementById('status').textContent = 'Disconnected';
198
+ document.getElementById('status').style.color = 'black';
199
+ document.getElementById('serverVersion').textContent = '';
200
+ }
201
+ }
202
+
203
+ async function publishMessage() {
204
+ if (!client || !isConnected) {
205
+ alert('Please connect first and wait for connection to be established');
206
+ return;
207
+ }
208
+
209
+ const text = document.getElementById('messageText').value;
210
+ const subject = document.getElementById('subject').value;
211
+
212
+ const options = {};
213
+ if (subject) options.subject = subject;
214
+
215
+ try {
216
+ const result = await client.publish({ text, timestamp: new Date() }, options);
217
+ console.log('Published:', result);
218
+
219
+ document.getElementById('messageText').value = '';
220
+ } catch (error) {
221
+ console.error('Publish error:', error);
222
+ alert('Failed to publish: ' + error.message);
223
+ }
224
+ }
225
+
226
+ async function publishEphemeral() {
227
+ if (!client || !isConnected) {
228
+ alert('Please connect first and wait for connection to be established');
229
+ return;
230
+ }
231
+
232
+ const text = document.getElementById('messageText').value;
233
+ const subject = document.getElementById('subject').value;
234
+
235
+ const options = { removeAfterRead: true };
236
+ if (subject) options.subject = subject;
237
+
238
+ try {
239
+ const result = await client.publish({ text, timestamp: new Date() }, options);
240
+ console.log('Published ephemeral:', result);
241
+
242
+ document.getElementById('messageText').value = '';
243
+ } catch (error) {
244
+ console.error('Publish error:', error);
245
+ alert('Failed to publish: ' + error.message);
246
+ }
247
+ }
248
+
249
+ async function subscribe() {
250
+ if (!client || !isConnected) {
251
+ alert('Please connect first and wait for connection to be established');
252
+ return;
253
+ }
254
+
255
+ const subject = document.getElementById('subscribeSubject').value || 'default';
256
+
257
+ await client.subscribe((message) => {
258
+ if (!perfTestRunning) {
259
+ addMessage(`[${subject}] ${JSON.stringify(message.data)}`);
260
+ }
261
+ }, { subject });
262
+
263
+ console.log('Subscribed to:', subject);
264
+ }
265
+
266
+ async function subscribeQueue() {
267
+ if (!client || !isConnected) {
268
+ alert('Please connect first and wait for connection to be established');
269
+ return;
270
+ }
271
+
272
+ const subject = document.getElementById('subscribeSubject').value || 'default';
273
+
274
+ await client.subscribe((message) => {
275
+ if (!perfTestRunning) {
276
+ addMessage(`[Queue: ${subject}] ${JSON.stringify(message.data)}`);
277
+ }
278
+ }, { subject, queue: 'browser-workers' });
279
+
280
+ console.log('Subscribed to queue group:', subject);
281
+ }
282
+
283
+ function addMessage(text, isPerf = false) {
284
+ const messagesDiv = document.getElementById('messages');
285
+ const messageDiv = document.createElement('div');
286
+ messageDiv.className = isPerf ? 'message perf-message' : 'message';
287
+ messageDiv.textContent = new Date().toLocaleTimeString() + ' - ' + text;
288
+ messagesDiv.appendChild(messageDiv);
289
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
290
+ }
291
+
292
+ function clearMessages() {
293
+ document.getElementById('messages').innerHTML = '';
294
+ }
295
+
296
+ async function getAllMessages() {
297
+ if (!client || !isConnected) {
298
+ alert('Please connect first and wait for connection to be established');
299
+ return;
300
+ }
301
+
302
+ const subject = document.getElementById('getMessagesSubject').value || 'default';
303
+
304
+ try {
305
+ const result = await client.getMessages({ subject });
306
+
307
+ if (result.success) {
308
+ clearMessages();
309
+ addMessage(`=== Retrieved ${result.count} messages from queue "${subject}" ===`);
310
+
311
+ result.messages.forEach((msg, index) => {
312
+ const timestamp = new Date(msg.timestamp).toLocaleString();
313
+ const expiry = msg.expiry ? ` | Expires: ${new Date(msg.expiry).toLocaleString()}` : '';
314
+ const ephemeral = msg.removeAfterRead ? ' | [Ephemeral]' : '';
315
+ addMessage(`${index + 1}. [${timestamp}${expiry}${ephemeral}] ${JSON.stringify(msg.data)}`);
316
+ });
317
+
318
+ if (result.count === 0) {
319
+ addMessage('No messages in queue');
320
+ }
321
+ } else {
322
+ addMessage('Error retrieving messages');
323
+ }
324
+ } catch (error) {
325
+ console.error('Error:', error);
326
+ addMessage('Error: ' + error.message);
327
+ }
328
+ }
329
+
330
+ async function runPerformanceTest() {
331
+ console.log('runPerformanceTest called');
332
+ console.log('client:', client);
333
+ console.log('isConnected:', isConnected);
334
+
335
+ if (!client) {
336
+ alert('Please connect first');
337
+ return;
338
+ }
339
+
340
+ if (!isConnected) {
341
+ alert('Waiting for connection to be established. Please try again in a moment.');
342
+ return;
343
+ }
344
+
345
+ if (perfTestRunning) {
346
+ alert('Performance test already running');
347
+ return;
348
+ }
349
+
350
+ const totalMessages = parseInt(document.getElementById('testMessageCount').value);
351
+ console.log('totalMessages:', totalMessages);
352
+
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
+ console.log('Starting performance test...');
360
+ perfTestRunning = true;
361
+ receivedCount = 0;
362
+ lastUpdateTime = 0;
363
+ publishEndTime = 0;
364
+
365
+ // Larger batch sizes for better throughput
366
+ const batchSize = totalMessages >= 100000 ? 500 : (totalMessages >= 10000 ? 250 : 100);
367
+ const subject = 'perftest';
368
+
369
+ const perfBtn = document.getElementById('perfTestBtn');
370
+ const perfStats = document.getElementById('perfStats');
371
+
372
+ perfBtn.disabled = true;
373
+ perfStats.style.display = 'block';
374
+ perfStats.innerHTML = 'Starting performance test...';
375
+
376
+ clearMessages();
377
+ addMessage('=== Performance Test Started ===', true);
378
+ addMessage(`Testing with ${totalMessages.toLocaleString()} messages in batches of ${batchSize}`, true);
379
+
380
+ // Subscribe to performance test messages BEFORE publishing
381
+ addMessage('Subscribing to performance test subject...', true);
382
+ await client.subscribe((message) => {
383
+ receivedCount++;
384
+
385
+ // Update frequency based on message count
386
+ const updateInterval = totalMessages >= 100000 ? 5000 : (totalMessages >= 10000 ? 1000 : 100);
387
+
388
+ // Update stats at intervals or every 500ms
389
+ const now = performance.now();
390
+ if (receivedCount % updateInterval === 0 || (now - lastUpdateTime > 500)) {
391
+ lastUpdateTime = now;
392
+ if (publishEndTime > 0) {
393
+ const receiveElapsed = ((now - publishEndTime) / 1000);
394
+ const receiveRate = Math.round(receivedCount / (receiveElapsed || 0.1));
395
+ perfStats.innerHTML = `
396
+ <strong>Status:</strong> Receiving messages <br>
397
+ <strong>Received:</strong> ${receivedCount.toLocaleString()} / ${totalMessages.toLocaleString()} (${(receivedCount/totalMessages*100).toFixed(1)}%)<br>
398
+ <strong>Pending:</strong> ${(totalMessages - receivedCount).toLocaleString()} <br>
399
+ <strong>Receive Rate:</strong> ${receiveRate.toLocaleString()} msg/s
400
+ `;
401
+ }
402
+ }
403
+ }, { subject });
404
+
405
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Give subscription time to register
406
+
407
+ const startTime = performance.now();
408
+ let publishStartTime = performance.now();
409
+ let publishedCount = 0;
410
+ let failedCount = 0;
411
+
412
+ try {
413
+ addMessage('Publishing messages...', true);
414
+
415
+ // Send messages in larger batches
416
+ for (let i = 0; i < totalMessages; i += batchSize) {
417
+ const batchPromises = [];
418
+ const currentBatchSize = Math.min(batchSize, totalMessages - i);
419
+
420
+ for (let j = 0; j < currentBatchSize; j++) {
421
+ const promise = client.publish({
422
+ id: i + j,
423
+ data: `Message ${i + j}`,
424
+ timestamp: Date.now()
425
+ }, { subject }).then(result => {
426
+ if (result && result.success) {
427
+ publishedCount++;
428
+ } else {
429
+ failedCount++;
430
+ if (!result) {
431
+ console.error('No response from server for message', i + j);
432
+ }
433
+ }
434
+ return result;
435
+ }).catch(err => {
436
+ failedCount++;
437
+ console.error('Publish error:', err);
438
+ });
439
+
440
+ batchPromises.push(promise);
441
+ }
442
+
443
+ await Promise.all(batchPromises);
444
+
445
+ // Reduce delay for better throughput
446
+ if (totalMessages >= 100000 && i % 5000 === 0) {
447
+ await new Promise(resolve => setTimeout(resolve, 1));
448
+ }
449
+
450
+ // Update progress every batch
451
+ const progress = ((i + currentBatchSize) / totalMessages * 100).toFixed(1);
452
+ const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
453
+ const messagesPerSec = Math.round(publishedCount / (elapsed || 1));
454
+
455
+ perfStats.innerHTML = `
456
+ <strong>Progress:</strong> ${progress}% (${(i + currentBatchSize).toLocaleString()} / ${totalMessages.toLocaleString()}) <br>
457
+ <strong>Elapsed:</strong> ${elapsed}s <br>
458
+ <strong>Published:</strong> ${publishedCount.toLocaleString()} <br>
459
+ ${failedCount > 0 ? `<strong style="color: red;">Failed:</strong> ${failedCount.toLocaleString()} <br>` : ''}
460
+ <strong>Publish Throughput:</strong> ${messagesPerSec.toLocaleString()} msg/s <br>
461
+ <strong>Received So Far:</strong> ${receivedCount.toLocaleString()}
462
+ `;
463
+ }
464
+
465
+ publishEndTime = performance.now();
466
+ const publishDuration = (publishEndTime - publishStartTime) / 1000;
467
+
468
+ addMessage(`Published ${publishedCount.toLocaleString()} messages in ${publishDuration.toFixed(2)}s! Waiting for delivery...`, true);
469
+ if (failedCount > 0) {
470
+ addMessage(`⚠ ${failedCount.toLocaleString()} messages failed to publish`, true);
471
+ }
472
+
473
+ // Wait for all messages to be received with timeout
474
+ const maxWaitTime = 300000; // 5 minutes timeout
475
+ const waitStartTime = performance.now();
476
+ let lastReceivedCount = 0;
477
+ let stuckCounter = 0;
478
+
479
+ while (receivedCount < publishedCount) {
480
+ await new Promise(resolve => setTimeout(resolve, 500));
481
+
482
+ // Check if we're stuck
483
+ if (receivedCount === lastReceivedCount) {
484
+ stuckCounter++;
485
+ if (stuckCounter > 20) { // Stuck for 10 seconds
486
+ addMessage(`⚠ Appears stuck at ${receivedCount.toLocaleString()} messages`, true);
487
+ break;
488
+ }
489
+ } else {
490
+ stuckCounter = 0;
491
+ }
492
+ lastReceivedCount = receivedCount;
493
+
494
+ const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
495
+ const waitTime = ((performance.now() - waitStartTime) / 1000).toFixed(2);
496
+ const receiveElapsed = ((performance.now() - publishEndTime) / 1000);
497
+ const receiveRate = Math.round(receivedCount / (receiveElapsed || 0.1));
498
+
499
+ perfStats.innerHTML = `
500
+ <strong>Status:</strong> Waiting for delivery completion <br>
501
+ <strong>Total Elapsed:</strong> ${elapsed}s <br>
502
+ <strong>Wait Time:</strong> ${waitTime}s <br>
503
+ <strong>Received:</strong> ${receivedCount.toLocaleString()} / ${publishedCount.toLocaleString()} (${(receivedCount/publishedCount*100).toFixed(1)}%)<br>
504
+ <strong>Pending:</strong> ${(publishedCount - receivedCount).toLocaleString()} <br>
505
+ <strong>Receive Rate:</strong> ${receiveRate.toLocaleString()} msg/s
506
+ `;
507
+
508
+ // Timeout check
509
+ if (performance.now() - waitStartTime > maxWaitTime) {
510
+ addMessage(`⚠ Timeout after ${maxWaitTime/1000}s - only ${receivedCount.toLocaleString()} messages received`, true);
511
+ break;
512
+ }
513
+ }
514
+
515
+ const endTime = performance.now();
516
+ const totalDuration = (endTime - startTime) / 1000;
517
+ const receiveDuration = (endTime - publishEndTime) / 1000;
518
+
519
+ // Calculate statistics
520
+ const avgPublishRate = Math.round(publishedCount / publishDuration);
521
+ const avgReceiveRate = Math.round(receivedCount / receiveDuration);
522
+ const avgTotalRate = Math.round(receivedCount / totalDuration);
523
+ const successRate = ((receivedCount / publishedCount) * 100).toFixed(2);
524
+
525
+ addMessage('=== Performance Test Complete ===', true);
526
+ addMessage(`Total Messages Attempted: ${totalMessages.toLocaleString()}`, true);
527
+ addMessage(`Messages Published: ${publishedCount.toLocaleString()}`, true);
528
+ addMessage(`Messages Received: ${receivedCount.toLocaleString()}`, true);
529
+ addMessage(`Success Rate: ${successRate}%`, true);
530
+ if (failedCount > 0) {
531
+ addMessage(`⚠ Failed to Publish: ${failedCount.toLocaleString()}`, true);
532
+ }
533
+ if (receivedCount < publishedCount) {
534
+ addMessage(`⚠ Missing Messages: ${(publishedCount - receivedCount).toLocaleString()}`, true);
535
+ }
536
+ addMessage(`Total Time: ${totalDuration.toFixed(2)}s`, true);
537
+ addMessage(`Publish Time: ${publishDuration.toFixed(2)}s`, true);
538
+ addMessage(`Receive Time: ${receiveDuration.toFixed(2)}s`, true);
539
+ addMessage(`Average Publish Rate: ${avgPublishRate.toLocaleString()} msg/s`, true);
540
+ addMessage(`Average Receive Rate: ${avgReceiveRate.toLocaleString()} msg/s`, true);
541
+ addMessage(`Overall Throughput: ${avgTotalRate.toLocaleString()} msg/s`, true);
542
+
543
+ const statusIcon = receivedCount === publishedCount ? '✓' : '⚠';
544
+ perfStats.innerHTML = `
545
+ <strong>${statusIcon} Test Complete!</strong><br>
546
+ <strong>Messages Attempted:</strong> ${totalMessages.toLocaleString()}<br>
547
+ <strong>Messages Published:</strong> ${publishedCount.toLocaleString()}<br>
548
+ <strong>Messages Received:</strong> ${receivedCount.toLocaleString()}<br>
549
+ <strong>Success Rate:</strong> ${successRate}%<br>
550
+ ${failedCount > 0 ? `<strong style="color: red;">Failed to Publish:</strong> ${failedCount.toLocaleString()}<br>` : ''}
551
+ ${receivedCount < publishedCount ? `<strong style="color: orange;">Missing:</strong> ${(publishedCount - receivedCount).toLocaleString()}<br>` : ''}
552
+ <strong>Total Time:</strong> ${totalDuration.toFixed(2)}s<br>
553
+ <strong>Publish Time:</strong> ${publishDuration.toFixed(2)}s<br>
554
+ <strong>Receive Time:</strong> ${receiveDuration.toFixed(2)}s<br>
555
+ <strong>Publish Rate:</strong> ${avgPublishRate.toLocaleString()} msg/s<br>
556
+ <strong>Receive Rate:</strong> ${avgReceiveRate.toLocaleString()} msg/s<br>
557
+ <strong>Overall Throughput:</strong> ${avgTotalRate.toLocaleString()} msg/s
558
+ `;
559
+
560
+ } catch (error) {
561
+ addMessage('Error during performance test: ' + error.message, true);
562
+ console.error('Performance test error:', error);
563
+ } finally {
564
+ perfTestRunning = false;
565
+ perfBtn.disabled = false;
566
+
567
+ // Unsubscribe from perftest subject
568
+ await client.unsubscribe({ subject });
569
+ }
570
+ }
571
+ </script>
572
+ </body>
573
+ </html>
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@usermetrics/queuebit",
3
+ "version": "1.0.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "A socket server API for using a queue with guaranteed delivery",
8
+ "main": "src/index.js",
9
+ "browser": "src/client-browser.js",
10
+ "scripts": {
11
+ "test": "node test/test-harness.js",
12
+ "start": "node src/index.js",
13
+ "server": "node src/server-runner.js",
14
+ "prepublishOnly": "npm test"
15
+ },
16
+ "keywords": [
17
+ "queue",
18
+ "socket",
19
+ "nats",
20
+ "message-queue",
21
+ "websocket",
22
+ "pubsub",
23
+ "realtime"
24
+ ],
25
+ "author": "Karl Lilje <karl@karllilje.com>",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "socket.io": "^4.7.2",
29
+ "uuid": "^9.0.1"
30
+ },
31
+ "devDependencies": {
32
+ "socket.io-client": "^4.7.2"
33
+ },
34
+ "files": [
35
+ "src/",
36
+ "docs/",
37
+ "examples/",
38
+ "LICENSE",
39
+ "README.md"
40
+ ],
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/bigfun123/queuebit.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/bigfun123/queuebit/issues"
47
+ },
48
+ "homepage": "https://github.com/bigfun123/queuebit#readme",
49
+ "engines": {
50
+ "node": ">=14.0.0"
51
+ }
52
+ }