queen-mq 0.1.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/API.md +1116 -0
- package/CACHE.md +519 -0
- package/DASHBOARD-V3.md +478 -0
- package/DASHBOARD.md +382 -0
- package/MOD_QUEUE.md +453 -0
- package/PARTITION_LOCKING_DESIGN.md +989 -0
- package/PLAN.md +707 -0
- package/QUERY_ANALSYS.md +72 -0
- package/QUEUE_BUS.md +334 -0
- package/README.md +1495 -0
- package/V2-PLAN.md +236 -0
- package/assets/dashboard.png +0 -0
- package/dashboard/.vscode/extensions.json +3 -0
- package/dashboard/README.md +5 -0
- package/dashboard/index.html +14 -0
- package/dashboard/package-lock.json +1458 -0
- package/dashboard/package.json +25 -0
- package/dashboard/public/vite.svg +1 -0
- package/dashboard/src/App.vue +29 -0
- package/dashboard/src/assets/styles/main.css +908 -0
- package/dashboard/src/assets/vue.svg +1 -0
- package/dashboard/src/components/cards/MetricCard.vue +298 -0
- package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
- package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
- package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
- package/dashboard/src/components/common/ActivityFeed.vue +251 -0
- package/dashboard/src/components/layout/AppHeader.vue +208 -0
- package/dashboard/src/components/layout/AppLayout.vue +88 -0
- package/dashboard/src/components/layout/AppSidebar.vue +261 -0
- package/dashboard/src/main.js +44 -0
- package/dashboard/src/router.js +54 -0
- package/dashboard/src/services/api.js +187 -0
- package/dashboard/src/services/websocket.js +167 -0
- package/dashboard/src/utils/constants.js +56 -0
- package/dashboard/src/utils/helpers.js +118 -0
- package/dashboard/src/views/Analytics.vue +912 -0
- package/dashboard/src/views/Dashboard.vue +906 -0
- package/dashboard/src/views/Messages.vue +437 -0
- package/dashboard/src/views/QueueDetail.vue +501 -0
- package/dashboard/src/views/Queues.vue +333 -0
- package/dashboard/vite.config.js +30 -0
- package/debug-namespace.js +110 -0
- package/docs/long-polling.md +159 -0
- package/docs/multi-server-cache-solutions.md +185 -0
- package/docs/performance-tuning.md +222 -0
- package/examples/bus-mode.js +239 -0
- package/examples/continuous-consumer-optimized.js +215 -0
- package/examples/continuous-consumer.js +159 -0
- package/examples/continuous-producer.js +343 -0
- package/examples/mixed-mode.js +277 -0
- package/examples/multi-server-test.js +305 -0
- package/examples/single.js +64 -0
- package/examples/smartchat-dealyed.js +42 -0
- package/examples/smartchat.js +52 -0
- package/examples/test-cache-invalidation.js +119 -0
- package/examples/test-cache-multi-server.js +245 -0
- package/examples/test-minimal-client.js +112 -0
- package/examples/test-queue-creation-policy.js +137 -0
- package/init-db.js +20 -0
- package/package.json +36 -0
- package/src/client/client.js +291 -0
- package/src/client/index.js +6 -0
- package/src/client/queenClient.js +513 -0
- package/src/client/utils/http.js +172 -0
- package/src/client/utils/loadBalancer.js +152 -0
- package/src/client/utils/retry.js +35 -0
- package/src/config.js +215 -0
- package/src/database/connection.js +103 -0
- package/src/database/poolManager.js +192 -0
- package/src/database/schema-v2.sql +214 -0
- package/src/managers/eventManager.js +59 -0
- package/src/managers/queueManagerOptimized.js +1512 -0
- package/src/managers/resourceCache.js +96 -0
- package/src/managers/systemEventManager.js +127 -0
- package/src/routes/ack.js +26 -0
- package/src/routes/analytics.js +812 -0
- package/src/routes/configure.js +46 -0
- package/src/routes/messages.js +298 -0
- package/src/routes/pop.js +85 -0
- package/src/routes/push.js +28 -0
- package/src/routes/resources.js +296 -0
- package/src/server.js +1286 -0
- package/src/services/encryptionService.js +82 -0
- package/src/services/evictionService.js +131 -0
- package/src/services/retentionService.js +129 -0
- package/src/services/startupSync.js +35 -0
- package/src/test/test.js +4521 -0
- package/src/utils/logger.js +44 -0
- package/src/utils/uuid.js +5 -0
- package/src/websocket/wsServer.js +221 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { createHttpClient, createLoadBalancedHttpClient } from './utils/http.js';
|
|
2
|
+
import { withRetry } from './utils/retry.js';
|
|
3
|
+
import { createLoadBalancer, LoadBalancingStrategy } from './utils/loadBalancer.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimalist Queen Message Queue Client
|
|
7
|
+
*
|
|
8
|
+
* Simple, powerful API with just 4 methods:
|
|
9
|
+
* - queue: Configure a queue
|
|
10
|
+
* - push: Send messages
|
|
11
|
+
* - take: Receive messages (async iterator)
|
|
12
|
+
* - ack: Acknowledge messages
|
|
13
|
+
*/
|
|
14
|
+
export class Queen {
|
|
15
|
+
#config;
|
|
16
|
+
#http;
|
|
17
|
+
#loadBalancer;
|
|
18
|
+
#connected = false;
|
|
19
|
+
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
this.#config = {
|
|
22
|
+
baseUrls: null,
|
|
23
|
+
loadBalancingStrategy: LoadBalancingStrategy.ROUND_ROBIN,
|
|
24
|
+
enableFailover: true,
|
|
25
|
+
timeout: 30000,
|
|
26
|
+
retryAttempts: 3,
|
|
27
|
+
retryDelay: 1000,
|
|
28
|
+
...config
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ensure HTTP client is connected
|
|
34
|
+
*/
|
|
35
|
+
async #ensureConnected() {
|
|
36
|
+
if (this.#connected) return;
|
|
37
|
+
|
|
38
|
+
const { baseUrl, baseUrls, loadBalancingStrategy, enableFailover, timeout } = this.#config;
|
|
39
|
+
|
|
40
|
+
if (baseUrls && Array.isArray(baseUrls) && baseUrls.length > 0) {
|
|
41
|
+
// Multiple servers with load balancing
|
|
42
|
+
this.#loadBalancer = createLoadBalancer(baseUrls, loadBalancingStrategy);
|
|
43
|
+
this.#http = createLoadBalancedHttpClient({
|
|
44
|
+
baseUrls,
|
|
45
|
+
loadBalancer: this.#loadBalancer,
|
|
46
|
+
timeout,
|
|
47
|
+
enableFailover
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
// Single server
|
|
51
|
+
const singleUrl = baseUrls && baseUrls.length === 1 ? baseUrls[0] : baseUrl;
|
|
52
|
+
this.#http = createHttpClient({ baseUrl: singleUrl, timeout });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.#connected = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse address string into components
|
|
60
|
+
* Examples:
|
|
61
|
+
* - "myqueue" -> { queue: "myqueue" }
|
|
62
|
+
* - "myqueue/urgent" -> { queue: "myqueue", partition: "urgent" }
|
|
63
|
+
* - "myqueue@workers" -> { queue: "myqueue", consumerGroup: "workers" }
|
|
64
|
+
* - "myqueue/urgent@workers" -> { queue: "myqueue", partition: "urgent", consumerGroup: "workers" }
|
|
65
|
+
*/
|
|
66
|
+
#parseAddress(address) {
|
|
67
|
+
// Match: queue[/partition][@group]
|
|
68
|
+
const match = address.match(/^([^/@]+)(?:\/([^@]+))?(?:@(.+))?$/);
|
|
69
|
+
if (!match) {
|
|
70
|
+
throw new Error(`Invalid address format: ${address}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
queue: match[1],
|
|
75
|
+
partition: match[2] || 'Default',
|
|
76
|
+
consumerGroup: match[3] || null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Configure a queue with options
|
|
82
|
+
* @param {string} name - Queue name
|
|
83
|
+
* @param {Object} options - Queue configuration options
|
|
84
|
+
*/
|
|
85
|
+
async queue(name, options = {}) {
|
|
86
|
+
await this.#ensureConnected();
|
|
87
|
+
|
|
88
|
+
const result = await withRetry(
|
|
89
|
+
() => this.#http.post('/api/v1/configure', {
|
|
90
|
+
queue: name,
|
|
91
|
+
options
|
|
92
|
+
}),
|
|
93
|
+
this.#config.retryAttempts,
|
|
94
|
+
this.#config.retryDelay
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (result && result.error) {
|
|
98
|
+
throw new Error(result.error);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Push messages to a queue
|
|
106
|
+
* @param {string} address - Queue address (e.g., "myqueue" or "myqueue/partition")
|
|
107
|
+
* @param {Object|Array} payload - Single message or array of messages
|
|
108
|
+
*/
|
|
109
|
+
async push(address, payload) {
|
|
110
|
+
await this.#ensureConnected();
|
|
111
|
+
|
|
112
|
+
const { queue, partition } = this.#parseAddress(address);
|
|
113
|
+
|
|
114
|
+
// Handle both single and array inputs
|
|
115
|
+
const items = Array.isArray(payload) ? payload : [payload];
|
|
116
|
+
|
|
117
|
+
// Format items for the API
|
|
118
|
+
const formattedItems = items.map(item => ({
|
|
119
|
+
queue,
|
|
120
|
+
partition,
|
|
121
|
+
payload: item,
|
|
122
|
+
transactionId: item.transactionId // Optional, for idempotency
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
const result = await withRetry(
|
|
126
|
+
() => this.#http.post('/api/v1/push', {
|
|
127
|
+
items: formattedItems
|
|
128
|
+
}),
|
|
129
|
+
this.#config.retryAttempts,
|
|
130
|
+
this.#config.retryDelay
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (result && result.error) {
|
|
134
|
+
throw new Error(result.error);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Take messages from a queue (async iterator)
|
|
142
|
+
* @param {string} address - Queue address (e.g., "myqueue", "myqueue/partition", "myqueue@group")
|
|
143
|
+
* @param {Object} options - Options for taking messages
|
|
144
|
+
* @yields {Object} Message objects
|
|
145
|
+
*/
|
|
146
|
+
async *take(address, options = {}) {
|
|
147
|
+
await this.#ensureConnected();
|
|
148
|
+
|
|
149
|
+
const { queue, partition, consumerGroup } = this.#parseAddress(address);
|
|
150
|
+
const {
|
|
151
|
+
limit = null,
|
|
152
|
+
batch = 1,
|
|
153
|
+
wait = false,
|
|
154
|
+
timeout = 30000,
|
|
155
|
+
subscriptionMode = null,
|
|
156
|
+
subscriptionFrom = null
|
|
157
|
+
} = options;
|
|
158
|
+
|
|
159
|
+
let count = 0;
|
|
160
|
+
let consecutiveEmptyResponses = 0;
|
|
161
|
+
const maxConsecutiveEmpty = 3;
|
|
162
|
+
|
|
163
|
+
while (true) {
|
|
164
|
+
// Check if we've reached the limit
|
|
165
|
+
if (limit && count >= limit) break;
|
|
166
|
+
|
|
167
|
+
// Build the request path and parameters
|
|
168
|
+
let path;
|
|
169
|
+
const params = new URLSearchParams({
|
|
170
|
+
wait: wait.toString(),
|
|
171
|
+
timeout: timeout.toString(),
|
|
172
|
+
batch: Math.min(batch, limit ? limit - count : batch).toString()
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Add consumer group parameters if provided
|
|
176
|
+
if (consumerGroup) params.append('consumerGroup', consumerGroup);
|
|
177
|
+
if (subscriptionMode) params.append('subscriptionMode', subscriptionMode);
|
|
178
|
+
if (subscriptionFrom) params.append('subscriptionFrom', subscriptionFrom);
|
|
179
|
+
|
|
180
|
+
// Determine the endpoint based on parameters
|
|
181
|
+
if (partition && partition !== 'Default') {
|
|
182
|
+
path = `/api/v1/pop/queue/${queue}/partition/${partition}`;
|
|
183
|
+
} else {
|
|
184
|
+
path = `/api/v1/pop/queue/${queue}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// For long polling, use a slightly longer client timeout
|
|
189
|
+
const clientTimeout = wait ? timeout + 5000 : timeout;
|
|
190
|
+
|
|
191
|
+
const result = await this.#http.get(`${path}?${params}`, clientTimeout);
|
|
192
|
+
|
|
193
|
+
// Handle empty response
|
|
194
|
+
if (!result || !result.messages || result.messages.length === 0) {
|
|
195
|
+
if (wait) {
|
|
196
|
+
// For long polling, immediately retry
|
|
197
|
+
continue;
|
|
198
|
+
} else {
|
|
199
|
+
// For non-waiting mode, stop after several empty responses
|
|
200
|
+
consecutiveEmptyResponses++;
|
|
201
|
+
if (consecutiveEmptyResponses >= maxConsecutiveEmpty) {
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
// Small delay before retry
|
|
205
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Reset empty counter on successful fetch
|
|
211
|
+
consecutiveEmptyResponses = 0;
|
|
212
|
+
|
|
213
|
+
// Yield each message
|
|
214
|
+
for (const message of result.messages) {
|
|
215
|
+
yield message;
|
|
216
|
+
count++;
|
|
217
|
+
if (limit && count >= limit) return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
} catch (error) {
|
|
221
|
+
// Check if this is a timeout error (expected for long polling)
|
|
222
|
+
const isTimeoutError = error.name === 'AbortError' ||
|
|
223
|
+
error.message?.includes('abort') ||
|
|
224
|
+
error.message?.includes('timeout');
|
|
225
|
+
|
|
226
|
+
if (isTimeoutError && wait) {
|
|
227
|
+
// For long polling timeout, immediately retry
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// For other errors, throw
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Acknowledge a message
|
|
239
|
+
* @param {Object|string} message - Message object or transaction ID
|
|
240
|
+
* @param {boolean|string} status - true for success, false for failure, or 'retry'
|
|
241
|
+
* @param {Object} context - Optional context (e.g., { group: 'workers', error: 'reason' })
|
|
242
|
+
*/
|
|
243
|
+
async ack(message, status = true, context = {}) {
|
|
244
|
+
await this.#ensureConnected();
|
|
245
|
+
|
|
246
|
+
// Extract transaction ID
|
|
247
|
+
const transactionId = typeof message === 'object' ?
|
|
248
|
+
(message.transactionId || message.id) :
|
|
249
|
+
message;
|
|
250
|
+
|
|
251
|
+
// Determine status string
|
|
252
|
+
let statusStr;
|
|
253
|
+
if (typeof status === 'boolean') {
|
|
254
|
+
statusStr = status ? 'completed' : 'failed';
|
|
255
|
+
} else {
|
|
256
|
+
statusStr = status; // Allow custom status like 'retry'
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Build request body
|
|
260
|
+
const body = {
|
|
261
|
+
transactionId,
|
|
262
|
+
status: statusStr,
|
|
263
|
+
error: context.error || null,
|
|
264
|
+
consumerGroup: context.group || null
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const result = await withRetry(
|
|
268
|
+
() => this.#http.post('/api/v1/ack', body),
|
|
269
|
+
this.#config.retryAttempts,
|
|
270
|
+
this.#config.retryDelay
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (result && result.error) {
|
|
274
|
+
throw new Error(result.error);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Close the client connection
|
|
282
|
+
*/
|
|
283
|
+
async close() {
|
|
284
|
+
this.#connected = false;
|
|
285
|
+
this.#http = null;
|
|
286
|
+
this.#loadBalancer = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Export the class as default as well for convenience
|
|
291
|
+
export default Queen;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Legacy exports (for backward compatibility)
|
|
2
|
+
export { createQueenClient, createConsumer, createProducer } from './queenClient.js';
|
|
3
|
+
export { LoadBalancingStrategy } from './utils/loadBalancer.js';
|
|
4
|
+
|
|
5
|
+
// New minimalist interface
|
|
6
|
+
export { Queen, default as default } from './client.js';
|