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.
Files changed (90) hide show
  1. package/API.md +1116 -0
  2. package/CACHE.md +519 -0
  3. package/DASHBOARD-V3.md +478 -0
  4. package/DASHBOARD.md +382 -0
  5. package/MOD_QUEUE.md +453 -0
  6. package/PARTITION_LOCKING_DESIGN.md +989 -0
  7. package/PLAN.md +707 -0
  8. package/QUERY_ANALSYS.md +72 -0
  9. package/QUEUE_BUS.md +334 -0
  10. package/README.md +1495 -0
  11. package/V2-PLAN.md +236 -0
  12. package/assets/dashboard.png +0 -0
  13. package/dashboard/.vscode/extensions.json +3 -0
  14. package/dashboard/README.md +5 -0
  15. package/dashboard/index.html +14 -0
  16. package/dashboard/package-lock.json +1458 -0
  17. package/dashboard/package.json +25 -0
  18. package/dashboard/public/vite.svg +1 -0
  19. package/dashboard/src/App.vue +29 -0
  20. package/dashboard/src/assets/styles/main.css +908 -0
  21. package/dashboard/src/assets/vue.svg +1 -0
  22. package/dashboard/src/components/cards/MetricCard.vue +298 -0
  23. package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
  24. package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
  25. package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
  26. package/dashboard/src/components/common/ActivityFeed.vue +251 -0
  27. package/dashboard/src/components/layout/AppHeader.vue +208 -0
  28. package/dashboard/src/components/layout/AppLayout.vue +88 -0
  29. package/dashboard/src/components/layout/AppSidebar.vue +261 -0
  30. package/dashboard/src/main.js +44 -0
  31. package/dashboard/src/router.js +54 -0
  32. package/dashboard/src/services/api.js +187 -0
  33. package/dashboard/src/services/websocket.js +167 -0
  34. package/dashboard/src/utils/constants.js +56 -0
  35. package/dashboard/src/utils/helpers.js +118 -0
  36. package/dashboard/src/views/Analytics.vue +912 -0
  37. package/dashboard/src/views/Dashboard.vue +906 -0
  38. package/dashboard/src/views/Messages.vue +437 -0
  39. package/dashboard/src/views/QueueDetail.vue +501 -0
  40. package/dashboard/src/views/Queues.vue +333 -0
  41. package/dashboard/vite.config.js +30 -0
  42. package/debug-namespace.js +110 -0
  43. package/docs/long-polling.md +159 -0
  44. package/docs/multi-server-cache-solutions.md +185 -0
  45. package/docs/performance-tuning.md +222 -0
  46. package/examples/bus-mode.js +239 -0
  47. package/examples/continuous-consumer-optimized.js +215 -0
  48. package/examples/continuous-consumer.js +159 -0
  49. package/examples/continuous-producer.js +343 -0
  50. package/examples/mixed-mode.js +277 -0
  51. package/examples/multi-server-test.js +305 -0
  52. package/examples/single.js +64 -0
  53. package/examples/smartchat-dealyed.js +42 -0
  54. package/examples/smartchat.js +52 -0
  55. package/examples/test-cache-invalidation.js +119 -0
  56. package/examples/test-cache-multi-server.js +245 -0
  57. package/examples/test-minimal-client.js +112 -0
  58. package/examples/test-queue-creation-policy.js +137 -0
  59. package/init-db.js +20 -0
  60. package/package.json +36 -0
  61. package/src/client/client.js +291 -0
  62. package/src/client/index.js +6 -0
  63. package/src/client/queenClient.js +513 -0
  64. package/src/client/utils/http.js +172 -0
  65. package/src/client/utils/loadBalancer.js +152 -0
  66. package/src/client/utils/retry.js +35 -0
  67. package/src/config.js +215 -0
  68. package/src/database/connection.js +103 -0
  69. package/src/database/poolManager.js +192 -0
  70. package/src/database/schema-v2.sql +214 -0
  71. package/src/managers/eventManager.js +59 -0
  72. package/src/managers/queueManagerOptimized.js +1512 -0
  73. package/src/managers/resourceCache.js +96 -0
  74. package/src/managers/systemEventManager.js +127 -0
  75. package/src/routes/ack.js +26 -0
  76. package/src/routes/analytics.js +812 -0
  77. package/src/routes/configure.js +46 -0
  78. package/src/routes/messages.js +298 -0
  79. package/src/routes/pop.js +85 -0
  80. package/src/routes/push.js +28 -0
  81. package/src/routes/resources.js +296 -0
  82. package/src/server.js +1286 -0
  83. package/src/services/encryptionService.js +82 -0
  84. package/src/services/evictionService.js +131 -0
  85. package/src/services/retentionService.js +129 -0
  86. package/src/services/startupSync.js +35 -0
  87. package/src/test/test.js +4521 -0
  88. package/src/utils/logger.js +44 -0
  89. package/src/utils/uuid.js +5 -0
  90. 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';