queen-mq 0.3.0 → 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 +2 -2
  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
@@ -1,162 +0,0 @@
1
- // Functional retention service for message cleanup
2
- import { log, LogTypes } from '../utils/logger.js';
3
- import config from '../config.js';
4
-
5
- const RETENTION_INTERVAL = config.JOBS.RETENTION_INTERVAL;
6
-
7
- // Apply retention policy to a single queue
8
- const retentionQueue = async (client, queue) => {
9
- const {
10
- retention_seconds: retentionSeconds = 0,
11
- completed_retention_seconds: completedRetentionSeconds = 0,
12
- retention_enabled: retentionEnabled = false
13
- } = queue;
14
-
15
- if (!retentionEnabled) return 0;
16
-
17
- let totalDeleted = 0;
18
-
19
- // Delete old unconsumed messages from all partitions in this queue
20
- if (retentionSeconds > 0) {
21
- const result = await client.query(`
22
- DELETE FROM queen.messages m
23
- WHERE m.partition_id IN (
24
- SELECT id FROM queen.partitions WHERE queue_id = $1
25
- )
26
- AND m.created_at < NOW() - INTERVAL '1 second' * $2
27
- AND NOT EXISTS (
28
- -- Message has been consumed by at least one consumer
29
- SELECT 1
30
- FROM queen.partition_consumers pc
31
- WHERE pc.partition_id = m.partition_id
32
- AND (m.created_at, m.id) <= (pc.last_consumed_created_at, pc.last_consumed_id)
33
- )
34
- RETURNING id
35
- `, [queue.id, retentionSeconds]);
36
-
37
- totalDeleted += result.rowCount || 0;
38
- }
39
-
40
- // Delete old consumed messages from all partitions in this queue
41
- // (messages consumed by ALL consumer groups on that partition)
42
- if (completedRetentionSeconds > 0) {
43
- const result = await client.query(`
44
- DELETE FROM queen.messages m
45
- WHERE m.partition_id IN (
46
- SELECT id FROM queen.partitions WHERE queue_id = $1
47
- )
48
- AND m.created_at < NOW() - INTERVAL '1 second' * $2
49
- AND NOT EXISTS (
50
- -- No consumer group that hasn't consumed this message yet
51
- SELECT 1
52
- FROM queen.partition_consumers pc
53
- WHERE pc.partition_id = m.partition_id
54
- AND ((m.created_at, m.id) > (pc.last_consumed_created_at, pc.last_consumed_id)
55
- OR pc.last_consumed_id IS NULL)
56
- )
57
- RETURNING id
58
- `, [queue.id, completedRetentionSeconds]);
59
-
60
- totalDeleted += result.rowCount || 0;
61
- }
62
-
63
- // Log retention if messages were deleted
64
- if (totalDeleted > 0) {
65
- log(`${LogTypes.RETENTION} | Queue: ${queue.name} | Count: ${totalDeleted} | RetentionSeconds: ${retentionSeconds} | CompletedRetentionSeconds: ${completedRetentionSeconds}`);
66
- }
67
-
68
- return totalDeleted;
69
- };
70
-
71
- // Clean up empty partitions (no longer based on partition-level config)
72
- const cleanupEmptyPartitions = async (client) => {
73
- // For now, we keep Default partitions and only clean up truly empty non-default partitions
74
- // that haven't had activity for a long time (e.g., 7 days)
75
- const result = await client.query(`
76
- DELETE FROM queen.partitions p
77
- WHERE p.name != 'Default'
78
- AND NOT EXISTS (
79
- SELECT 1 FROM queen.messages m WHERE m.partition_id = p.id
80
- )
81
- AND p.last_activity < NOW() - INTERVAL '7 days'
82
- RETURNING id, name
83
- `);
84
-
85
- if (result.rowCount > 0) {
86
- log(`🗑️ Deleted ${result.rowCount} empty partitions`);
87
- }
88
-
89
- return result.rowCount;
90
- };
91
-
92
- // Clean up old metrics data (messages_consumed table)
93
- const cleanupOldMetrics = async (client) => {
94
- const retentionDays = config.JOBS.METRICS_RETENTION_DAYS;
95
-
96
- // Delete metrics data older than the retention period
97
- const result = await client.query(`
98
- DELETE FROM queen.messages_consumed
99
- WHERE acked_at < NOW() - INTERVAL '1 day' * $1
100
- `, [retentionDays]);
101
-
102
- if (result.rowCount > 0) {
103
- log(`📊 Deleted ${result.rowCount} old metrics records (older than ${retentionDays} days)`);
104
- }
105
-
106
- return result.rowCount;
107
- };
108
-
109
- // Main retention function
110
- const performRetention = async (pool) => {
111
- const client = await pool.connect();
112
-
113
- try {
114
- // Get queues with retention enabled
115
- const queuesResult = await client.query(`
116
- SELECT q.id, q.name, q.retention_enabled, q.retention_seconds,
117
- q.completed_retention_seconds
118
- FROM queen.queues q
119
- WHERE q.retention_enabled = true
120
- `);
121
-
122
- let totalDeleted = 0;
123
- for (const queue of queuesResult.rows) {
124
- totalDeleted += await retentionQueue(client, queue);
125
- }
126
-
127
- // Cleanup empty partitions
128
- await cleanupEmptyPartitions(client);
129
-
130
- // Cleanup old metrics data
131
- await cleanupOldMetrics(client);
132
-
133
- return totalDeleted;
134
- } catch (error) {
135
- log('Retention error:', error);
136
- return 0;
137
- } finally {
138
- client.release();
139
- }
140
- };
141
-
142
- // Start the retention job
143
- export const startRetentionJob = (pool) => {
144
- const intervalId = setInterval(async () => {
145
- try {
146
- await performRetention(pool);
147
- } catch (error) {
148
- log('Retention job error:', error);
149
- }
150
- }, RETENTION_INTERVAL);
151
-
152
- log(`♻️ Retention job started (interval: ${RETENTION_INTERVAL}ms)`);
153
-
154
- // Return cleanup function
155
- return () => {
156
- clearInterval(intervalId);
157
- log('Retention job stopped');
158
- };
159
- };
160
-
161
- // Export for manual execution
162
- export const runRetention = performRetention;
@@ -1,35 +0,0 @@
1
- import { SYSTEM_QUEUE } from '../managers/systemEventManager.js';
2
-
3
- export async function syncSystemEvents(client, eventManager, consumerGroup) {
4
- console.log('Synchronizing system events...');
5
-
6
- let processed = 0;
7
- let hasMore = true;
8
-
9
- while (hasMore) {
10
- // Pop and process system events in batches using the server's consumer group
11
- // This ensures we only process events we haven't seen before
12
- const result = await client.pop({
13
- queue: SYSTEM_QUEUE,
14
- consumerGroup: consumerGroup, // Use server's unique consumer group
15
- batch: 100,
16
- wait: false
17
- });
18
-
19
- if (!result.messages || result.messages.length === 0) {
20
- hasMore = false;
21
- break;
22
- }
23
-
24
- for (const message of result.messages) {
25
- await eventManager.processSystemEvent(message.data);
26
- await client.ack(message.transactionId, 'completed', null, consumerGroup);
27
- processed++;
28
- }
29
- }
30
-
31
- if (processed > 0) {
32
- console.log(`Synchronized ${processed} system events`);
33
- }
34
- return processed;
35
- }
@@ -1,224 +0,0 @@
1
- # Queen Message Queue Test Suite
2
-
3
- This directory contains the comprehensive test suite for the Queen Message Queue system, using the new minimalist `Queen` client interface.
4
-
5
- ## Test Structure
6
-
7
- The test suite is organized into focused, modular files:
8
-
9
- ### Core Files
10
-
11
- - **`test-new.js`** - Main test runner that orchestrates all tests
12
- - **`utils.js`** - Shared utilities (logging, database helpers, result tracking)
13
-
14
- ### Test Categories
15
-
16
- #### 1. Core Features (`core-tests.js`)
17
- - Queue creation policy
18
- - Single/batch message push
19
- - Queue configuration
20
- - Take and acknowledgment
21
- - Delayed processing
22
- - FIFO ordering within partitions
23
-
24
- #### 2. Partition Locking (`partition-locking-tests.js`)
25
- - Partition locking in queue mode
26
- - Partition locking in bus mode
27
- - Specific partition requests with locking
28
- - Namespace/task filtering with partition locking
29
-
30
- #### 3. Enterprise Features (`enterprise-tests.js`)
31
- - Message encryption/decryption
32
- - Retention policies (pending & completed messages)
33
- - Message eviction
34
- - Combined enterprise features
35
- - Enterprise error handling
36
-
37
- #### 4. Bus Mode Features (`bus-mode-tests.js`)
38
- - Consumer groups
39
- - Mixed mode (queue + bus)
40
- - Subscription modes (all vs new messages)
41
- - Consumer group isolation
42
-
43
- #### 5. Edge Cases (`edge-case-tests.js` & `partition-transaction-tests.js`)
44
- - Empty and null payloads
45
- - Very large payloads
46
- - Concurrent push/take operations
47
- - Retry limit exhaustion
48
- - Lease expiration and redelivery
49
- - SQL injection prevention
50
- - XSS prevention
51
- - **Partition-scoped transaction_id handling** (`partition-transaction-tests.js`):
52
- - Duplicate transaction IDs across partitions with correct ACK targeting
53
- - Batch ACK with duplicate transaction IDs
54
- - DLQ operations with duplicate transaction IDs
55
- - Analytics API with duplicate transaction IDs
56
-
57
- #### 6. Advanced Patterns (`advanced-pattern-tests.js`)
58
- - Multi-stage pipeline workflow
59
- - Fan-out/fan-in pattern
60
- - Dead letter queue pattern
61
- - Circuit breaker pattern
62
- - Message deduplication
63
-
64
- ## New vs Old Interface
65
-
66
- ### Old Interface (test.js)
67
- ```javascript
68
- import { createQueenClient } from '../client/queenClient.js';
69
-
70
- const client = createQueenClient({ baseUrls: [...] });
71
-
72
- // Configure
73
- await client.configure({ queue: 'myqueue', options: {} });
74
-
75
- // Push
76
- await client.push({ items: [{ queue: 'myqueue', partition: 'Default', payload: {...} }] });
77
-
78
- // Pop
79
- const result = await client.pop({ queue: 'myqueue', batch: 10 });
80
-
81
- // Ack
82
- await client.ack(transactionId, 'completed', null, consumerGroup);
83
- ```
84
-
85
- ### New Interface (test-new.js)
86
- ```javascript
87
- import { Queen } from '../client/client.js';
88
-
89
- const client = new Queen({ baseUrls: [...] });
90
-
91
- // Configure
92
- await client.queue('myqueue', {}, { namespace, task });
93
-
94
- // Push
95
- await client.push('myqueue/partition', payload);
96
-
97
- // Take (async iterator)
98
- for await (const msg of client.take('myqueue/partition@group', { limit: 10 })) {
99
- // Process message
100
- await client.ack(msg, true, { group: 'mygroup' });
101
- }
102
- ```
103
-
104
- ## Key Differences
105
-
106
- 1. **Address Format**: The new interface uses a unified address format:
107
- - `"queue"` - Simple queue
108
- - `"queue/partition"` - Specific partition
109
- - `"queue@group"` - Consumer group
110
- - `"queue/partition@group"` - Partition + group
111
- - `"namespace:name"` - Namespace filter
112
- - `"task:name"` - Task filter
113
- - `"namespace:billing/task:process"` - Combined filters
114
-
115
- 2. **Async Iterator**: `pop()` → `take()` using async iteration
116
- ```javascript
117
- for await (const msg of client.take(address, options)) {
118
- // Handle message
119
- }
120
- ```
121
-
122
- 3. **Simplified Methods**: 4 core methods instead of many:
123
- - `client.queue(name, options, metadata)` - Configure
124
- - `client.push(address, payload, options)` - Send
125
- - `client.take(address, options)` - Receive (async iterator)
126
- - `client.ack(message, status, context)` - Acknowledge
127
-
128
- 4. **Message Properties**: Can be in payload or options:
129
- ```javascript
130
- // In options
131
- await client.push('queue', { data }, { transactionId: 'txn-1' });
132
-
133
- // In payload
134
- await client.push('queue', { data, transactionId: 'txn-1' });
135
- ```
136
-
137
- ## Running Tests
138
-
139
- ```bash
140
- # Make sure Queen servers are running first
141
- # And database is accessible
142
-
143
- cd /Users/alice/Work/queen
144
-
145
- # Run all tests
146
- nvm use 22 && node client-js/test/test-new.js
147
-
148
- # Run specific test category
149
- node client-js/test/test-new.js core # Core features only
150
- node client-js/test/test-new.js partition # Partition locking tests only
151
- node client-js/test/test-new.js enterprise # Enterprise features only
152
- node client-js/test/test-new.js bus # Bus mode tests only
153
- node client-js/test/test-new.js edge # Edge cases (includes partition-scoped txn_id tests)
154
- node client-js/test/test-new.js advanced # Advanced patterns only
155
-
156
- # Show help
157
- node client-js/test/test-new.js help
158
- ```
159
-
160
- ### Testing Partition-Scoped Transaction ID Fix
161
-
162
- The `edge` test category includes critical tests for partition-scoped transaction_id handling:
163
-
164
- ```bash
165
- # Run all edge case tests (includes partition transaction_id tests)
166
- node client-js/test/test-new.js edge
167
- ```
168
-
169
- These tests verify that:
170
- - ✅ The same `transaction_id` can exist in multiple partitions
171
- - ✅ ACK operations correctly target messages using BOTH `partition_id` AND `transaction_id`
172
- - ✅ Batch ACK operations scope correctly by partition
173
- - ✅ DLQ operations only affect the intended partition
174
- - ✅ Analytics API (`GET /api/v1/messages/:partitionId/:transactionId`) returns the correct message
175
-
176
- ### Available Test Categories
177
-
178
- - **`core`** - Core features (queue creation, push, take, ack, delayed processing, FIFO)
179
- - **`partition`** (or `locking`) - Partition locking in queue and bus modes
180
- - **`enterprise`** - Enterprise features (encryption, retention, eviction)
181
- - **`bus`** - Bus mode features (consumer groups, mixed mode, subscription modes, isolation)
182
- - **`edge`** - Edge cases (null payloads, large payloads, concurrency, SQL injection, XSS)
183
- - **`advanced`** (or `pattern`) - Advanced patterns (pipeline, fan-out/fan-in, DLQ, circuit breaker, deduplication)
184
-
185
- ## Test Configuration
186
-
187
- Tests use environment variables for configuration:
188
- - `PG_HOST` - PostgreSQL host (default: localhost)
189
- - `PG_PORT` - PostgreSQL port (default: 5432)
190
- - `PG_DB` - Database name (default: postgres)
191
- - `PG_USER` - Database user (default: postgres)
192
- - `PG_PASSWORD` - Database password (default: postgres)
193
- - `QUEEN_ENCRYPTION_KEY` - Optional encryption key for encryption tests
194
-
195
- ## Adding New Tests
196
-
197
- 1. Create a test function in the appropriate category file:
198
- ```javascript
199
- export async function testMyFeature(client) {
200
- startTest('My Feature Test', 'category');
201
- try {
202
- // Test code here
203
- passTest('Feature works correctly');
204
- } catch (error) {
205
- failTest(error);
206
- }
207
- }
208
- ```
209
-
210
- 2. Import and add to `test-new.js`:
211
- ```javascript
212
- import { testMyFeature } from './category-tests.js';
213
-
214
- // In runTests():
215
- await runTest(() => testMyFeature(client));
216
- ```
217
-
218
- ## Notes
219
-
220
- - All tests automatically clean up test data before and after running
221
- - Tests use the `test-*`, `edge-*`, `pattern-*`, and `workflow-*` queue name prefixes
222
- - The test suite supports both single-server and multi-server configurations
223
- - Some enterprise tests (encryption, retention) are skipped if not configured
224
-