queen-mq 0.2.0 → 0.2.22

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 (85) hide show
  1. package/README.md +186 -763
  2. package/{src → client-js}/benchmark/consumer.js +6 -4
  3. package/{src → client-js}/benchmark/producer.js +9 -4
  4. package/{src → client-js}/client/client.js +277 -7
  5. package/{src → client-js}/client/index.js +1 -4
  6. package/{src → client-js}/client/utils/http.js +3 -2
  7. package/{src → client-js}/client/utils/retry.js +7 -1
  8. package/{src → client-js}/test/README.md +30 -9
  9. package/{src → client-js}/test/core-tests.js +7 -2
  10. package/{src → client-js}/test/edge-case-tests.js +2 -1
  11. package/client-js/test/human.js +162 -0
  12. package/client-js/test/partition-transaction-tests.js +482 -0
  13. package/client-js/test/qos0-tests.js +334 -0
  14. package/{src → client-js}/test/test-new.js +55 -0
  15. package/package.json +8 -17
  16. package/init-db.js +0 -20
  17. package/src/cluster-server.js +0 -242
  18. package/src/config.js +0 -229
  19. package/src/database/connection.js +0 -129
  20. package/src/database/poolManager.js +0 -199
  21. package/src/database/schema-v2.sql +0 -278
  22. package/src/managers/eventManager.js +0 -59
  23. package/src/managers/queueManagerOptimized.js +0 -1634
  24. package/src/managers/resourceCache.js +0 -96
  25. package/src/managers/systemEventManager.js +0 -132
  26. package/src/routes/ack.js +0 -26
  27. package/src/routes/configure.js +0 -46
  28. package/src/routes/messages.js +0 -368
  29. package/src/routes/pop.js +0 -69
  30. package/src/routes/push.js +0 -28
  31. package/src/routes/resources.js +0 -330
  32. package/src/routes/status.js +0 -1037
  33. package/src/server.js +0 -1633
  34. package/src/test/MIGRATION_ISSUES.md +0 -174
  35. package/src/test/test.js +0 -4524
  36. package/src/utils/streaming.js +0 -231
  37. package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
  38. package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
  39. package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
  40. package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
  41. package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
  42. package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
  43. package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
  44. package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
  45. package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
  46. package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
  47. package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
  48. package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
  49. package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
  50. package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
  51. package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
  52. package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
  53. package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
  54. package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
  55. package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
  56. package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
  57. package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
  58. package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
  59. package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
  60. package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
  61. package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
  62. package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
  63. package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
  64. package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
  65. package/src/webapp-dist/assets/queen-logo.svg +0 -263
  66. package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
  67. package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
  68. package/src/webapp-dist/index.html +0 -15
  69. package/src/websocket/wsServer.js +0 -228
  70. /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
  71. /package/{src → client-js}/benchmark/producer_multi.js +0 -0
  72. /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
  73. /package/{src → client-js}/services/encryptionService.js +0 -0
  74. /package/{src → client-js}/services/evictionService.js +0 -0
  75. /package/{src → client-js}/services/retentionService.js +0 -0
  76. /package/{src → client-js}/services/startupSync.js +0 -0
  77. /package/{src → client-js}/test/advanced-client-tests.js +0 -0
  78. /package/{src → client-js}/test/advanced-pattern-tests.js +0 -0
  79. /package/{src → client-js}/test/bus-mode-tests.js +0 -0
  80. /package/{src → client-js}/test/enterprise-tests.js +0 -0
  81. /package/{src → client-js}/test/partition-locking-tests.js +0 -0
  82. /package/{src → client-js}/test/utils.js +0 -0
  83. /package/{src → client-js}/test/window-buffer-test.js +0 -0
  84. /package/{src → client-js}/utils/logger.js +0 -0
  85. /package/{src → client-js}/utils/uuid.js +0 -0
@@ -3,7 +3,8 @@ import fs from 'fs';
3
3
 
4
4
  const QUEUE_NAME = 'benchmark-queue-001';
5
5
  const NUMBER_OF_CONSUMERS = 10;
6
- const BATCH_SIZE = 10;
6
+ const STARTING_PARTITION = parseInt(process.argv[2]) || 0;
7
+ const BATCH_SIZE = 10000;
7
8
  const CONSUME_MODE = 'partition';
8
9
 
9
10
  // Global metrics tracking
@@ -21,7 +22,7 @@ const metrics = {
21
22
 
22
23
  async function consumer(consumerId, partition) {
23
24
  const q = new Queen({
24
- baseUrls: ['http://localhost:6632'],
25
+ baseUrls: [ 'http://localhost:6632'],
25
26
  timeout: 30000
26
27
  });
27
28
 
@@ -49,7 +50,7 @@ async function consumer(consumerId, partition) {
49
50
  try {
50
51
  for await (const messages of q.takeBatch(target, { //`namespace:benchmark`,
51
52
  wait: true,
52
- timeout: 15000,
53
+ timeout: 30000,
53
54
  batch: BATCH_SIZE,
54
55
  idleTimeout: 5000
55
56
  })) {
@@ -126,8 +127,9 @@ metrics.startTime = Date.now();
126
127
 
127
128
  // Launch all consumers
128
129
  const consumerPromises = [];
130
+ //
129
131
  for (let i = 0; i < NUMBER_OF_CONSUMERS; i++) {
130
- consumerPromises.push(consumer(i, i));
132
+ consumerPromises.push(consumer(i, i ));
131
133
  }
132
134
 
133
135
  // Wait for all consumers to complete
@@ -1,9 +1,9 @@
1
1
  import { Queen } from '../client/client.js';
2
2
  import fs from 'fs';
3
3
 
4
- const TOTAL_MESSAGES = 100000;
4
+ const TOTAL_MESSAGES = 1000000;
5
5
  const PARTITIONS = 10;
6
- const MAX_BATCH_SIZE = 1000; // Reduced from 10000
6
+ const MAX_BATCH_SIZE = 100; // Reduced from 10000
7
7
  const PUSH_PARALLEL = 10; // Reduced from 10 to avoid lock contention
8
8
  const QUEUE_NAME = 'benchmark-queue-001';
9
9
 
@@ -12,11 +12,16 @@ const q = new Queen({
12
12
  timeout: 120000
13
13
  });
14
14
 
15
- //await q.queueDelete(QUEUE_NAME);
16
- await q.queue(QUEUE_NAME, {
15
+
16
+ try {
17
+ await q.queueDelete(QUEUE_NAME);
18
+ await q.queue(QUEUE_NAME, {
17
19
  leaseTime: 10,
18
20
  retryLimit: 3
19
21
  }, { namespace: 'benchmark' });
22
+ } catch (e) {
23
+ console.error(e);
24
+ }
20
25
 
21
26
 
22
27
 
@@ -1,6 +1,7 @@
1
1
  import { createHttpClient, createLoadBalancedHttpClient } from './utils/http.js';
2
2
  import { withRetry } from './utils/retry.js';
3
3
  import { createLoadBalancer, LoadBalancingStrategy } from './utils/loadBalancer.js';
4
+ import { generateUUID } from '../utils/uuid.js';
4
5
 
5
6
  /**
6
7
  * Minimalist Queen Message Queue Client
@@ -16,6 +17,7 @@ export class Queen {
16
17
  #http;
17
18
  #loadBalancer;
18
19
  #connected = false;
20
+ #bufferManager;
19
21
 
20
22
  constructor(config = {}) {
21
23
  this.#config = {
@@ -27,6 +29,7 @@ export class Queen {
27
29
  retryDelay: 1000,
28
30
  ...config
29
31
  };
32
+ this.#bufferManager = new BufferManager(this);
30
33
  }
31
34
 
32
35
  /**
@@ -146,7 +149,7 @@ export class Queen {
146
149
  * Push messages to a queue
147
150
  * @param {string} address - Queue address (e.g., "myqueue" or "myqueue/partition")
148
151
  * @param {Object|Array} payload - Single message or array of messages
149
- * @param {Object} options - Optional message properties { transactionId, traceId }
152
+ * @param {Object} options - Optional message properties { transactionId, traceId, buffer }
150
153
  */
151
154
  async push(address, payload, options = {}) {
152
155
  await this.#ensureConnected();
@@ -167,7 +170,7 @@ export class Queen {
167
170
  queue,
168
171
  partition,
169
172
  payload: item._payload || item,
170
- transactionId: item._transactionId || item.transactionId
173
+ transactionId: item._transactionId || item.transactionId || generateUUID() // Always generate if missing
171
174
  };
172
175
 
173
176
  // Include traceId if provided and valid UUID
@@ -184,7 +187,9 @@ export class Queen {
184
187
  queue,
185
188
  partition,
186
189
  payload: item,
187
- transactionId: options.transactionId || (item && typeof item === 'object' ? item.transactionId : undefined)
190
+ transactionId: options.transactionId ||
191
+ (item && typeof item === 'object' ? item.transactionId : undefined) ||
192
+ generateUUID() // Always generate if missing
188
193
  };
189
194
 
190
195
  // Include traceId if provided and valid UUID
@@ -196,10 +201,26 @@ export class Queen {
196
201
  return result;
197
202
  });
198
203
 
204
+ // Check if client-side buffering is enabled
205
+ if (options.buffer && typeof options.buffer === 'object') {
206
+ return this.#pushBuffered(address, formattedItems, options.buffer);
207
+ }
208
+
209
+ // Build request body for immediate push
210
+ const requestBody = { items: formattedItems };
211
+
212
+ // Add server-side buffering flag if specified
213
+ if (options.buffer === true || options.qos0 === true) {
214
+ requestBody.qos0 = true; // Server still uses 'qos0' internally
215
+ }
216
+
217
+ // Legacy support: bufferMs/bufferMax are aliases for buffer
218
+ if (options.bufferMs !== undefined || options.bufferMax !== undefined) {
219
+ requestBody.qos0 = true;
220
+ }
221
+
199
222
  const result = await withRetry(
200
- () => this.#http.post('/api/v1/push', {
201
- items: formattedItems
202
- }),
223
+ () => this.#http.post('/api/v1/push', requestBody),
203
224
  this.#config.retryAttempts,
204
225
  this.#config.retryDelay
205
226
  );
@@ -211,6 +232,25 @@ export class Queen {
211
232
  return result;
212
233
  }
213
234
 
235
+ /**
236
+ * Push messages using client-side buffering
237
+ * @private
238
+ */
239
+ #pushBuffered(address, formattedItems, bufferOptions) {
240
+ // Validate buffer options
241
+ if (!bufferOptions.size || !bufferOptions.time) {
242
+ throw new Error('Buffer options must include both size and time');
243
+ }
244
+
245
+ // Add each formatted message to the buffer
246
+ for (const item of formattedItems) {
247
+ this.#bufferManager.addMessage(address, item, bufferOptions);
248
+ }
249
+
250
+ // Return immediately (non-blocking)
251
+ return { buffered: true, count: formattedItems.length };
252
+ }
253
+
214
254
  /**
215
255
  * Internal method that yields batches of messages (arrays)
216
256
  * @private
@@ -266,6 +306,9 @@ export class Queen {
266
306
  if (subscriptionMode) params.append('subscriptionMode', subscriptionMode);
267
307
  if (subscriptionFrom) params.append('subscriptionFrom', subscriptionFrom);
268
308
 
309
+ // Add auto-ack option if specified
310
+ if (options.autoAck) params.append('autoAck', 'true');
311
+
269
312
  // Determine the endpoint based on parameters
270
313
  if (queue) {
271
314
  if (partition && partition !== 'Default') {
@@ -668,10 +711,45 @@ export class Queen {
668
711
  return result;
669
712
  }
670
713
 
714
+ /**
715
+ * Manually flush a specific buffer
716
+ * @param {string} address - Queue address to flush
717
+ * @returns {Promise<void>}
718
+ */
719
+ async flushBuffer(address) {
720
+ return this.#bufferManager.flushBuffer(address);
721
+ }
722
+
723
+ /**
724
+ * Manually flush all buffers
725
+ * @returns {Promise<void>}
726
+ */
727
+ async flushAllBuffers() {
728
+ return this.#bufferManager.flushAllBuffers();
729
+ }
730
+
731
+ /**
732
+ * Get buffer statistics
733
+ * @returns {Object} Buffer statistics
734
+ */
735
+ getBufferStats() {
736
+ return this.#bufferManager.getStats();
737
+ }
738
+
671
739
  /**
672
740
  * Close the client connection
673
741
  */
674
742
  async close() {
743
+ // Flush all buffers before closing
744
+ try {
745
+ await this.#bufferManager.flushAllBuffers();
746
+ } catch (error) {
747
+ console.warn('Error flushing buffers during close:', error);
748
+ }
749
+
750
+ // Cleanup buffer manager
751
+ this.#bufferManager.cleanup();
752
+
675
753
  this.#connected = false;
676
754
  this.#http = null;
677
755
  this.#loadBalancer = null;
@@ -702,6 +780,194 @@ export class Queen {
702
780
  get _http() {
703
781
  return this.#http;
704
782
  }
783
+
784
+ get _config() {
785
+ return this.#config;
786
+ }
787
+ }
788
+
789
+ /**
790
+ * Message Buffer for client-side buffering
791
+ */
792
+ class MessageBuffer {
793
+ constructor(address, options, flushCallback) {
794
+ this.address = address;
795
+ this.messages = [];
796
+ this.options = options;
797
+ this.flushCallback = flushCallback;
798
+ this.timer = null;
799
+ this.firstMessageTime = null;
800
+ this.flushing = false;
801
+ }
802
+
803
+ add(formattedMessage) {
804
+ // Set first message time if this is the first message
805
+ if (this.messages.length === 0) {
806
+ this.firstMessageTime = Date.now();
807
+ this.#startTimer();
808
+ }
809
+
810
+ this.messages.push(formattedMessage);
811
+
812
+ // Check if we should flush based on size
813
+ if (this.messages.length >= this.options.size) {
814
+ this.#triggerFlush();
815
+ }
816
+ }
817
+
818
+ #startTimer() {
819
+ if (this.timer) return; // Timer already running
820
+
821
+ this.timer = setTimeout(() => {
822
+ this.#triggerFlush();
823
+ }, this.options.time);
824
+ }
825
+
826
+ #triggerFlush() {
827
+ if (this.flushing || this.messages.length === 0) return;
828
+
829
+ // Clear timer
830
+ if (this.timer) {
831
+ clearTimeout(this.timer);
832
+ this.timer = null;
833
+ }
834
+
835
+ // Trigger flush via callback
836
+ this.flushCallback(this.address);
837
+ }
838
+
839
+ extractMessages() {
840
+ const messages = [...this.messages];
841
+ this.messages = [];
842
+ this.firstMessageTime = null;
843
+ this.flushing = false;
844
+
845
+ if (this.timer) {
846
+ clearTimeout(this.timer);
847
+ this.timer = null;
848
+ }
849
+
850
+ return messages;
851
+ }
852
+
853
+ cleanup() {
854
+ if (this.timer) {
855
+ clearTimeout(this.timer);
856
+ this.timer = null;
857
+ }
858
+ this.messages = [];
859
+ this.firstMessageTime = null;
860
+ this.flushing = false;
861
+ }
862
+ }
863
+
864
+ /**
865
+ * Buffer Manager for client-side message buffering
866
+ */
867
+ class BufferManager {
868
+ constructor(client) {
869
+ this.client = client;
870
+ this.buffers = new Map(); // address -> MessageBuffer
871
+ this.flushCount = 0;
872
+ }
873
+
874
+ addMessage(address, formattedMessage, bufferOptions) {
875
+ const bufferKey = address;
876
+
877
+ if (!this.buffers.has(bufferKey)) {
878
+ this.buffers.set(bufferKey, new MessageBuffer(
879
+ address,
880
+ bufferOptions,
881
+ (addr) => this.#flushBuffer(addr)
882
+ ));
883
+ }
884
+
885
+ const buffer = this.buffers.get(bufferKey);
886
+ buffer.add(formattedMessage);
887
+ }
888
+
889
+ async #flushBuffer(address) {
890
+ const buffer = this.buffers.get(address);
891
+ if (!buffer || buffer.flushing || buffer.messages.length === 0) {
892
+ return;
893
+ }
894
+
895
+ buffer.flushing = true;
896
+
897
+ try {
898
+ const messages = buffer.extractMessages();
899
+
900
+ if (messages.length === 0) return;
901
+
902
+ // Call the original push logic with the buffered messages
903
+ const requestBody = { items: messages };
904
+
905
+ await withRetry(
906
+ () => this.client._http.post('/api/v1/push', requestBody),
907
+ this.client._config.retryAttempts,
908
+ this.client._config.retryDelay
909
+ );
910
+
911
+ this.flushCount++;
912
+
913
+ // Call onFlush callback if provided
914
+ if (buffer.options.onFlush) {
915
+ buffer.options.onFlush(address, messages.length);
916
+ }
917
+
918
+ // Remove empty buffer
919
+ this.buffers.delete(address);
920
+
921
+ } catch (error) {
922
+ buffer.flushing = false;
923
+
924
+ // Call onError callback if provided
925
+ if (buffer.options.onError) {
926
+ buffer.options.onError(address, error);
927
+ }
928
+
929
+ throw error;
930
+ }
931
+ }
932
+
933
+ async flushBuffer(address) {
934
+ return this.#flushBuffer(address);
935
+ }
936
+
937
+ async flushAllBuffers() {
938
+ const flushPromises = [];
939
+ for (const address of this.buffers.keys()) {
940
+ flushPromises.push(this.#flushBuffer(address));
941
+ }
942
+ await Promise.all(flushPromises);
943
+ }
944
+
945
+ getStats() {
946
+ let totalBufferedMessages = 0;
947
+ let oldestBufferAge = 0;
948
+
949
+ for (const buffer of this.buffers.values()) {
950
+ totalBufferedMessages += buffer.messages.length;
951
+ if (buffer.firstMessageTime) {
952
+ const age = Date.now() - buffer.firstMessageTime;
953
+ oldestBufferAge = Math.max(oldestBufferAge, age);
954
+ }
955
+ }
956
+
957
+ return {
958
+ activeBuffers: this.buffers.size,
959
+ totalBufferedMessages,
960
+ oldestBufferAge,
961
+ flushesPerformed: this.flushCount
962
+ };
963
+ }
964
+
965
+ cleanup() {
966
+ for (const buffer of this.buffers.values()) {
967
+ buffer.cleanup();
968
+ }
969
+ this.buffers.clear();
970
+ }
705
971
  }
706
972
 
707
973
  /**
@@ -978,7 +1244,11 @@ class PipelineBuilder {
978
1244
  await tx.commit();
979
1245
  } else {
980
1246
  // Default: just ACK the messages
981
- await this.#client.ack(messages, true);
1247
+ // Extract consumer group from queue address if present (format: queue@group)
1248
+ const atIndex = this.#queue.lastIndexOf('@');
1249
+ const consumerGroup = atIndex > 0 ? this.#queue.substring(atIndex + 1) : null;
1250
+ const context = consumerGroup ? { group: consumerGroup } : {};
1251
+ await this.#client.ack(messages, true, context);
982
1252
  }
983
1253
 
984
1254
  return {
@@ -1,7 +1,4 @@
1
1
  export { LoadBalancingStrategy } from './utils/loadBalancer.js';
2
2
 
3
3
  // New minimalist interface
4
- export { Queen } from './client.js';
5
-
6
- // Server
7
- export { QueenServer } from '../server.js';
4
+ export { Queen } from './client.js';
@@ -1,4 +1,5 @@
1
1
  export const httpRequest = async (url, options = {}) => {
2
+ //console.log('httpRequest', url, options);
2
3
  const response = await fetch(url, {
3
4
  ...options,
4
5
  headers: {
@@ -6,7 +7,7 @@ export const httpRequest = async (url, options = {}) => {
6
7
  ...options.headers
7
8
  }
8
9
  });
9
-
10
+ //console.log('response', response);
10
11
  // Handle 204 No Content
11
12
  if (response.status === 204) {
12
13
  return null; // No content
@@ -143,7 +144,7 @@ export const createLoadBalancedHttpClient = ({ baseUrls, loadBalancer, timeout =
143
144
  return await executeRequest(url);
144
145
  } catch (error) {
145
146
  lastError = error;
146
- console.warn(`Request failed for ${url}: ${error.message}`);
147
+ console.warn(`Request failed for ${url}: ${method} ${path} - ${error.message}`);
147
148
 
148
149
  // Don't retry on client errors (4xx)
149
150
  if (error.status && error.status >= 400 && error.status < 500) {
@@ -1,4 +1,10 @@
1
- import config from '../../config.js';
1
+ const config = {
2
+ CLIENT: {
3
+ DEFAULT_RETRY_ATTEMPTS: 3,
4
+ DEFAULT_RETRY_DELAY: 1000,
5
+ DEFAULT_RETRY_BACKOFF: 2
6
+ }
7
+ }
2
8
 
3
9
  export const withRetry = async (fn, attempts = config.CLIENT.DEFAULT_RETRY_ATTEMPTS, delay = config.CLIENT.DEFAULT_RETRY_DELAY, backoff = config.CLIENT.DEFAULT_RETRY_BACKOFF) => {
4
10
  let lastError;
@@ -40,7 +40,7 @@ The test suite is organized into focused, modular files:
40
40
  - Subscription modes (all vs new messages)
41
41
  - Consumer group isolation
42
42
 
43
- #### 5. Edge Cases (`edge-case-tests.js`)
43
+ #### 5. Edge Cases (`edge-case-tests.js` & `partition-transaction-tests.js`)
44
44
  - Empty and null payloads
45
45
  - Very large payloads
46
46
  - Concurrent push/take operations
@@ -48,6 +48,11 @@ The test suite is organized into focused, modular files:
48
48
  - Lease expiration and redelivery
49
49
  - SQL injection prevention
50
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
51
56
 
52
57
  #### 6. Advanced Patterns (`advanced-pattern-tests.js`)
53
58
  - Multi-stage pipeline workflow
@@ -138,20 +143,36 @@ for await (const msg of client.take('myqueue/partition@group', { limit: 10 })) {
138
143
  cd /Users/alice/Work/queen
139
144
 
140
145
  # Run all tests
141
- nvm use 22 && node src/test/test-new.js
146
+ nvm use 22 && node client-js/test/test-new.js
142
147
 
143
148
  # Run specific test category
144
- node src/test/test-new.js core # Core features only
145
- node src/test/test-new.js partition # Partition locking tests only
146
- node src/test/test-new.js enterprise # Enterprise features only
147
- node src/test/test-new.js bus # Bus mode tests only
148
- node src/test/test-new.js edge # Edge cases only
149
- node src/test/test-new.js advanced # Advanced patterns only
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
150
155
 
151
156
  # Show help
152
- node src/test/test-new.js help
157
+ node client-js/test/test-new.js help
153
158
  ```
154
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
+
155
176
  ### Available Test Categories
156
177
 
157
178
  - **`core`** - Core features (queue creation, push, take, ack, delayed processing, FIFO)
@@ -113,19 +113,23 @@ export async function testBatchMessagePush(client) {
113
113
 
114
114
  // Push batch
115
115
  await client.push(`${queue}/batch-partition`, messages);
116
-
116
+
117
117
  // Verify messages were stored
118
118
  const count = await getMessageCount(queue, 'batch-partition');
119
+
119
120
  if (count !== batchSize) {
120
121
  throw new Error(`Expected ${batchSize} messages in database, got ${count}`);
121
122
  }
122
-
123
+
123
124
  // Take all messages
124
125
  const received = [];
125
126
  for await (const msg of client.take(`${queue}/batch-partition`, { limit: batchSize })) {
126
127
  received.push(msg);
128
+
127
129
  await client.ack(msg);
128
130
  }
131
+
132
+
129
133
 
130
134
  if (received.length !== batchSize) {
131
135
  throw new Error(`Expected ${batchSize} messages, got ${received.length}`);
@@ -233,6 +237,7 @@ export async function testDelayedProcessing(client) {
233
237
  await client.queue(queue, {
234
238
  delayedProcessing: 2 // 2 seconds delay
235
239
  });
240
+
236
241
 
237
242
  const startTime = Date.now();
238
243
 
@@ -105,10 +105,11 @@ export async function testVeryLargePayloads(client) {
105
105
 
106
106
  // Take and verify
107
107
  let received = null;
108
- for await (const msg of client.take(queue, { limit: 1 })) {
108
+ for await (const msg of client.take(queue, { limit: 1, timeout: 20000 })) {
109
109
  received = msg.data;
110
110
  await client.ack(msg);
111
111
  }
112
+
112
113
 
113
114
  if (!received || !received.array || received.array.length !== 10000) {
114
115
  throw new Error('Large payload corrupted');