queen-mq 0.3.1 โ†’ 0.4.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 (39) hide show
  1. package/README.md +212 -144
  2. package/client-js/client-v2/Queen.js +28 -0
  3. package/client-js/client-v2/README.md +6 -2
  4. package/client-js/client-v2/builders/QueueBuilder.js +7 -1
  5. package/client-js/client-v2/builders/TransactionBuilder.js +20 -4
  6. package/client-js/client-v2/stream/StreamBuilder.js +72 -0
  7. package/client-js/client-v2/stream/StreamConsumer.js +156 -0
  8. package/client-js/client-v2/stream/Window.js +140 -0
  9. package/client-js/test-v2/MAINTENANCE_TEST.md +148 -0
  10. package/client-js/test-v2/consume.js +1 -0
  11. package/client-js/test-v2/maintenance.js +261 -0
  12. package/client-js/test-v2/retention.js +69 -0
  13. package/client-js/test-v2/run.js +7 -1
  14. package/package.json +1 -1
  15. package/client-js/client/client.js +0 -1536
  16. package/client-js/client/index.js +0 -4
  17. package/client-js/client/utils/http.js +0 -173
  18. package/client-js/client/utils/loadBalancer.js +0 -152
  19. package/client-js/client/utils/retry.js +0 -41
  20. package/client-js/services/encryptionService.js +0 -82
  21. package/client-js/services/evictionService.js +0 -160
  22. package/client-js/services/retentionService.js +0 -162
  23. package/client-js/services/startupSync.js +0 -35
  24. package/client-js/test/README.md +0 -224
  25. package/client-js/test/advanced-client-tests.js +0 -761
  26. package/client-js/test/advanced-pattern-tests.js +0 -1137
  27. package/client-js/test/bus-mode-tests.js +0 -361
  28. package/client-js/test/core-tests.js +0 -457
  29. package/client-js/test/edge-case-tests.js +0 -562
  30. package/client-js/test/enterprise-tests.js +0 -637
  31. package/client-js/test/human.js +0 -162
  32. package/client-js/test/partition-locking-tests.js +0 -545
  33. package/client-js/test/partition-transaction-tests.js +0 -482
  34. package/client-js/test/qos0-tests.js +0 -334
  35. package/client-js/test/test-new.js +0 -370
  36. package/client-js/test/utils.js +0 -169
  37. package/client-js/test/window-buffer-test.js +0 -114
  38. package/client-js/utils/logger.js +0 -44
  39. package/client-js/utils/uuid.js +0 -5
@@ -0,0 +1,156 @@
1
+ import { Window } from './Window.js';
2
+
3
+ /**
4
+ * StreamConsumer - Manages consuming windows from a stream
5
+ */
6
+ export class StreamConsumer {
7
+ constructor(httpClient, queen, streamName, consumerGroup) {
8
+ this.httpClient = httpClient;
9
+ this.queen = queen;
10
+ this.streamName = streamName;
11
+ this.consumerGroup = consumerGroup;
12
+ this.pollTimeout = 30000; // 30s long poll
13
+ this.leaseRenewInterval = 20000; // 20s renewal (before 60s timeout)
14
+ this.running = false;
15
+ }
16
+
17
+ /**
18
+ * Start processing windows with the provided callback
19
+ * Runs in an infinite loop until stopped
20
+ * @param {Function} callback - async function(window) to process each window
21
+ */
22
+ async process(callback) {
23
+ this.running = true;
24
+
25
+
26
+ while (this.running) {
27
+ let window = null;
28
+
29
+ try {
30
+ window = await this.pollWindow();
31
+
32
+ if (!window) {
33
+ // 204 No Content - no window available
34
+ continue;
35
+ }
36
+
37
+ await this.executeCallback(window, callback);
38
+
39
+ } catch (err) {
40
+ // Backoff on error
41
+ await new Promise(r => setTimeout(r, 1000));
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Stop the processing loop
48
+ */
49
+ stop() {
50
+ this.running = false;
51
+ }
52
+
53
+ /**
54
+ * Poll for a window (blocking call)
55
+ * @returns {Promise<Window|null>} Window or null if no content
56
+ */
57
+ async pollWindow() {
58
+ try {
59
+ const response = await this.httpClient.post('/api/v1/stream/poll', {
60
+ streamName: this.streamName,
61
+ consumerGroup: this.consumerGroup,
62
+ timeout: this.pollTimeout
63
+ });
64
+
65
+ // Handle 204 No Content (HttpClient returns null for 204)
66
+ if (!response) {
67
+ return null;
68
+ }
69
+
70
+ // HttpClient returns the JSON body directly (e.g., {"window": {...}})
71
+ if (!response.window) {
72
+ return null;
73
+ }
74
+
75
+ return new Window(response.window);
76
+
77
+ } catch (err) {
78
+ // HttpClient throws on errors, returns null on 204
79
+ // So any error here is a real network/server error
80
+ throw err;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Execute the callback with lease renewal
86
+ * @param {Window} window - The window to process
87
+ * @param {Function} callback - User callback
88
+ */
89
+ async executeCallback(window, callback) {
90
+ let leaseTimer = null;
91
+ let leaseExpired = false;
92
+
93
+ try {
94
+ // Start lease renewal timer
95
+ leaseTimer = setInterval(async () => {
96
+ try {
97
+ await this.httpClient.post('/api/v1/stream/renew-lease', {
98
+ leaseId: window.leaseId,
99
+ extend_ms: this.leaseRenewInterval + 10000 // Extend by renewal interval + buffer
100
+ });
101
+ } catch (e) {
102
+ leaseExpired = true;
103
+ }
104
+ }, this.leaseRenewInterval);
105
+
106
+ // Execute user callback
107
+ await callback(window);
108
+
109
+ // ACK if lease hasn't expired
110
+ if (!leaseExpired) {
111
+ await this.httpClient.post('/api/v1/stream/ack', {
112
+ windowId: window.id,
113
+ leaseId: window.leaseId,
114
+ success: true
115
+ });
116
+ }
117
+
118
+ } catch (err) {
119
+
120
+ // NACK if lease hasn't expired
121
+ if (!leaseExpired) {
122
+ try {
123
+ await this.httpClient.post('/api/v1/stream/ack', {
124
+ windowId: window.id,
125
+ leaseId: window.leaseId,
126
+ success: false // NACK
127
+ });
128
+ } catch (nackErr) {
129
+
130
+ }
131
+ }
132
+
133
+ // Re-throw to trigger backoff
134
+ throw err;
135
+
136
+ } finally {
137
+ // Stop lease renewal
138
+ if (leaseTimer) {
139
+ clearInterval(leaseTimer);
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Seek to a specific timestamp
146
+ * @param {string} timestamp - ISO timestamp to seek to
147
+ */
148
+ async seek(timestamp) {
149
+ return this.httpClient.post('/api/v1/stream/seek', {
150
+ streamName: this.streamName,
151
+ consumerGroup: this.consumerGroup,
152
+ timestamp
153
+ });
154
+ }
155
+ }
156
+
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Utility function to get nested property value from object using dot notation
3
+ */
4
+ const getPath = (obj, path) =>
5
+ path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : null), obj);
6
+
7
+ /**
8
+ * Window - Represents a time window of messages with utility methods
9
+ */
10
+ export class Window {
11
+ constructor(rawWindow) {
12
+ // Copy all properties from raw window
13
+ Object.assign(this, rawWindow);
14
+
15
+ // Store immutable original messages
16
+ this.allMessages = Object.freeze(rawWindow.messages || []);
17
+
18
+ // Working copy for transformations
19
+ this.messages = [...this.allMessages];
20
+ }
21
+
22
+ /**
23
+ * Filter messages based on a predicate function
24
+ * @param {Function} filterFn - Predicate function (msg) => boolean
25
+ * @returns {Window} this for chaining
26
+ */
27
+ filter(filterFn) {
28
+ this.messages = this.messages.filter(filterFn);
29
+ return this;
30
+ }
31
+
32
+ /**
33
+ * Group messages by a key path (dot notation supported)
34
+ * @param {string} keyPath - Path to key in message data (e.g., 'data.userId')
35
+ * @returns {Object} Object with keys as group names and values as arrays of messages
36
+ */
37
+ groupBy(keyPath) {
38
+ const groups = {};
39
+ for (const msg of this.messages) {
40
+ const key = getPath(msg, keyPath) || 'null_key';
41
+ if (!groups[key]) {
42
+ groups[key] = [];
43
+ }
44
+ groups[key].push(msg);
45
+ }
46
+ return groups;
47
+ }
48
+
49
+ /**
50
+ * Aggregate messages using various aggregation functions
51
+ * @param {Object} config - Aggregation configuration
52
+ * @param {boolean} config.count - Count messages
53
+ * @param {string[]} config.sum - Array of paths to sum
54
+ * @param {string[]} config.avg - Array of paths to average
55
+ * @param {string[]} config.min - Array of paths to find minimum
56
+ * @param {string[]} config.max - Array of paths to find maximum
57
+ * @returns {Object} Aggregation results
58
+ */
59
+ aggregate(config = {}) {
60
+ const results = {};
61
+
62
+ // Count
63
+ if (config.count) {
64
+ results.count = this.messages.length;
65
+ }
66
+
67
+ // Sum
68
+ if (config.sum) {
69
+ results.sum = {};
70
+ for (const path of config.sum) {
71
+ results.sum[path] = this.messages.reduce((total, msg) => {
72
+ const val = getPath(msg, path);
73
+ return total + (typeof val === 'number' ? val : 0);
74
+ }, 0);
75
+ }
76
+ }
77
+
78
+ // Average
79
+ if (config.avg) {
80
+ results.avg = {};
81
+ for (const path of config.avg) {
82
+ const sum = this.messages.reduce((total, msg) => {
83
+ const val = getPath(msg, path);
84
+ return total + (typeof val === 'number' ? val : 0);
85
+ }, 0);
86
+ results.avg[path] = this.messages.length > 0 ? sum / this.messages.length : 0;
87
+ }
88
+ }
89
+
90
+ // Min
91
+ if (config.min) {
92
+ results.min = {};
93
+ for (const path of config.min) {
94
+ const values = this.messages
95
+ .map(msg => getPath(msg, path))
96
+ .filter(val => typeof val === 'number');
97
+ results.min[path] = values.length > 0 ? Math.min(...values) : null;
98
+ }
99
+ }
100
+
101
+ // Max
102
+ if (config.max) {
103
+ results.max = {};
104
+ for (const path of config.max) {
105
+ const values = this.messages
106
+ .map(msg => getPath(msg, path))
107
+ .filter(val => typeof val === 'number');
108
+ results.max[path] = values.length > 0 ? Math.max(...values) : null;
109
+ }
110
+ }
111
+
112
+ return results;
113
+ }
114
+
115
+ /**
116
+ * Reset working messages to the original frozen set
117
+ * @returns {Window} this for chaining
118
+ */
119
+ reset() {
120
+ this.messages = [...this.allMessages];
121
+ return this;
122
+ }
123
+
124
+ /**
125
+ * Get count of messages in working set
126
+ * @returns {number}
127
+ */
128
+ size() {
129
+ return this.messages.length;
130
+ }
131
+
132
+ /**
133
+ * Get count of original messages
134
+ * @returns {number}
135
+ */
136
+ originalSize() {
137
+ return this.allMessages.length;
138
+ }
139
+ }
140
+
@@ -0,0 +1,148 @@
1
+ # Maintenance Mode Test
2
+
3
+ ## Overview
4
+
5
+ Comprehensive test for the maintenance mode feature with file buffer.
6
+
7
+ ## Test Scenario
8
+
9
+ 1. **Start consumer** - Begins consuming messages from queue
10
+ 2. **Start producer** - Produces 10 messages/second
11
+ 3. **Enable maintenance** - Triggers maintenance mode via API
12
+ 4. **Verify buffering** - Consumer stops receiving (messages go to file buffer)
13
+ 5. **Continue producing** - Producer keeps running for 10 seconds during maintenance
14
+ 6. **Disable maintenance** - Deactivates maintenance mode via API
15
+ 7. **Verify drain** - Consumer resumes receiving messages
16
+ 8. **Stop producer** - Halts message production
17
+ 9. **Wait for completion** - Waits 10 seconds for file buffer to drain
18
+ 10. **Verify counts** - Ensures all produced messages were received
19
+
20
+ ## Running the Test
21
+
22
+ ### Standalone
23
+
24
+ ```bash
25
+ cd /Users/alice/Work/queen/client-js/test-v2
26
+ node maintenance.js
27
+ ```
28
+
29
+ ### Via Test Runner
30
+
31
+ ```bash
32
+ # Run just this test
33
+ node run.js test_maintenance_mode
34
+
35
+ # Run all human tests (includes this one)
36
+ node run.js human
37
+
38
+ # Run all tests
39
+ node run.js
40
+ ```
41
+
42
+ ## Expected Output
43
+
44
+ ```
45
+ ๐Ÿงช Testing Maintenance Mode with File Buffer
46
+
47
+ ๐Ÿ“‹ Step 1: Configuring queue "test-maintenance-queue"...
48
+ โœ… Queue configured
49
+
50
+ ๐Ÿ“ฅ Step 2: Starting consumer...
51
+ โœ… Consumer started
52
+
53
+ ๐Ÿ“ค Step 3: Starting producer (10 msgs/sec)...
54
+ โœ… Producer started
55
+
56
+ โฑ๏ธ Step 4: Waiting 3 seconds for normal message flow...
57
+ ๐Ÿ“จ Received 10 messages (total: 10)
58
+ ๐Ÿ“จ Received 10 messages (total: 20)
59
+ ๐Ÿ“จ Received 10 messages (total: 30)
60
+ Produced: 30, Received: 30
61
+
62
+ ๐Ÿ”ง Step 5: Enabling MAINTENANCE MODE...
63
+ Response: { maintenanceMode: true, bufferedMessages: 0, ... }
64
+ โœ… Maintenance mode enabled
65
+
66
+ โฑ๏ธ Step 6: Waiting 2 seconds - consumer should stop receiving...
67
+ Messages received during maintenance: 0
68
+ Total: Produced=50, Received=30
69
+
70
+ โฑ๏ธ Step 7: Producing during maintenance for 10 seconds...
71
+ (Messages should go to file buffer)
72
+
73
+ Maintenance period complete:
74
+ - Total produced: 150
75
+ - Total received: 30
76
+ - Buffered (should be ~100): 120
77
+
78
+ ๐Ÿ“Š Step 8: Checking maintenance status...
79
+ Status: { maintenanceMode: true, bufferedMessages: 120, ... }
80
+
81
+ โœ… Step 9: Disabling MAINTENANCE MODE...
82
+ Response: { maintenanceMode: false, bufferedMessages: 120, ... }
83
+ (File buffer should start draining to database)
84
+
85
+ โฑ๏ธ Step 10: Waiting for messages to resume...
86
+ ๐Ÿ“จ Received 10 messages (total: 40)
87
+ Messages received after resuming: 10
88
+ โœ… Messages are flowing again!
89
+
90
+ ๐Ÿ›‘ Step 11: Stopping producer...
91
+ Final produced count: 150
92
+
93
+ โฑ๏ธ Step 12: Waiting 10 seconds for file buffer to drain...
94
+ 1s - Received: 50/150 (33.3%)
95
+ ๐Ÿ“จ Received 10 messages (total: 60)
96
+ 2s - Received: 70/150 (46.7%)
97
+ ๐Ÿ“จ Received 10 messages (total: 80)
98
+ 3s - Received: 90/150 (60.0%)
99
+ ๐Ÿ“จ Received 10 messages (total: 100)
100
+ 4s - Received: 110/150 (73.3%)
101
+ ๐Ÿ“จ Received 10 messages (total: 120)
102
+ 5s - Received: 130/150 (86.7%)
103
+ ๐Ÿ“จ Received 10 messages (total: 140)
104
+ 6s - Received: 150/150 (100.0%)
105
+ โœ… All messages received!
106
+
107
+ ๐Ÿ›‘ Step 13: Stopping consumer...
108
+ โœ… Consumer stopped
109
+
110
+ ๐Ÿ“Š Final Verification:
111
+
112
+ Total Produced: 150
113
+ Total Received: 150
114
+ Difference: 0
115
+
116
+ โœ… SUCCESS: All messages accounted for!
117
+ Maintenance mode works correctly with file buffer.
118
+
119
+ ๐Ÿ‘‹ Queen client closed
120
+
121
+ โœ… All tests passed!
122
+ ```
123
+
124
+ ## What It Tests
125
+
126
+ โœ… **Maintenance mode activation** - API responds correctly
127
+ โœ… **Message buffering** - Messages go to file buffer (consumer stops receiving)
128
+ โœ… **Continued production** - Can push messages during maintenance
129
+ โœ… **Maintenance mode deactivation** - API responds correctly
130
+ โœ… **Automatic drain** - File buffer drains to database
131
+ โœ… **Message ordering** - All messages received in order (FIFO)
132
+ โœ… **No message loss** - Produced count == Received count
133
+ โœ… **Multi-instance support** - Uses database for state (works across restarts)
134
+
135
+ ## Success Criteria
136
+
137
+ - All produced messages are eventually received
138
+ - Consumer stops receiving during maintenance
139
+ - Consumer resumes receiving after maintenance is disabled
140
+ - No duplicates or lost messages
141
+ - Drain completes within 10 seconds
142
+
143
+ ## Notes
144
+
145
+ - Uses `axios` for direct API calls (maintenance endpoints)
146
+ - Uses Queen client for queue operations (push/pop/ack)
147
+ - Test takes ~25 seconds total (3s warmup + 10s maintenance + 10s drain)
148
+
@@ -190,6 +190,7 @@ export async function testConsumerOrdering(client) {
190
190
  await client
191
191
  .queue('test-queue-v2-consume-batch')
192
192
  .push([{ data: { id: i } }])
193
+ console.log(new Date().toISOString(), 'Pushed message:', i)
193
194
  }
194
195
 
195
196