@emanuelepifani/nexo-client 0.1.1 → 0.2.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/README.md CHANGED
@@ -19,6 +19,7 @@ This exposes:
19
19
  npm install @emanuelepifani/nexo-client
20
20
  ```
21
21
 
22
+
22
23
  ### Connection
23
24
  ```typescript
24
25
  const client = await NexoClient.connect({ host: 'localhost', port: 7654 });
@@ -49,7 +50,7 @@ await mailQ.delete();
49
50
  ```
50
51
 
51
52
  <details>
52
- <summary><strong>Advanced Features (Persistence, Retry, Delay, Priority)</strong></summary>
53
+ <summary><strong>Advanced Features (Persistence, Retry, Delay, Priority, DLQ)</strong></summary>
53
54
 
54
55
  ```typescript
55
56
  // -------------------------------------------------------------
@@ -64,7 +65,6 @@ const criticalQueue = await client.queue<CriticalTask>('critical-tasks').create(
64
65
  ttlMs: 60000, // Message expires if not consumed in 60s (default=7days)
65
66
 
66
67
  // PERSISTENCE:
67
- // - 'memory': Volatile (Fastest, lost on restart)
68
68
  // - 'file_sync': Save every message (Safest, Slowest)
69
69
  // - 'file_async': Flush periodically (Fast & Durable) -
70
70
  // DEFAULT: 'file_async': Flush every 50ms or 5000 msgs
@@ -97,6 +97,44 @@ await criticalQueue.subscribe(
97
97
  waitMs: 5000 // Polling: If empty, wait 5s for new messages before retrying
98
98
  }
99
99
  );
100
+
101
+ // ---------------------------------------------------------
102
+ // 4. DEAD LETTER QUEUE (DLQ) - Failed Message Management
103
+ // ---------------------------------------------------------
104
+
105
+ // DLQ is automatically available for every main queue.
106
+ // When messages exceed maxRetries, they're moved to DLQ automatically
107
+
108
+ // Inspect failed messages
109
+ const failedMessages = await criticalQueue.dlq.peek(10);
110
+ console.log(`Found ${failedMessages.total} failed messages`);
111
+
112
+ for (const msg of failedMessages.items) {
113
+ console.log(`Message ${msg.id}: attempts=${msg.attempts}, reason=${msg.failureReason}`);
114
+ console.log(`Payload:`, msg.data);
115
+
116
+ // Decision logic based on failure reason
117
+ if (shouldRetry(msg.data)) {
118
+ // Replay: Move back to main queue (resets attempts to 0)
119
+ // The message will be immediately available for consumption
120
+ const moved = await criticalQueue.dlq.moveToQueue(msg.id);
121
+ console.log(`Replayed message ${msg.id}: ${moved}`);
122
+ } else {
123
+ // Discard: Permanently delete from DLQ
124
+ const deleted = await criticalQueue.dlq.delete(msg.id);
125
+ console.log(`Deleted message ${msg.id}: ${deleted}`);
126
+ }
127
+ }
128
+
129
+ // Bulk operations
130
+ const purgedCount = await criticalQueue.dlq.purge(); // Clear all DLQ messages
131
+ console.log(`Purged ${purgedCount} messages from DLQ`);
132
+
133
+ // DLQ API:
134
+ // - peek(limit, offset): Inspect messages without removing them. Returns { total, items: [] }
135
+ // - moveToQueue(messageId): Replay a specific message (returns false if not found)
136
+ // - delete(messageId): Permanently remove a specific message (returns false if not found)
137
+ // - purge(): Remove all messages (returns count)
100
138
  ```
101
139
  </details>
102
140
 
@@ -166,7 +204,6 @@ const orders = await client.stream<Order>('orders').create({
166
204
  partitions: 4, // Max concurrent consumers per group on same topic (default=8)
167
205
 
168
206
  // PERSISTENCE:
169
- // - 'memory': Volatile (Fastest, lost on restart)
170
207
  // - 'file_sync': Save every message (Safest, Slowest)
171
208
  // - 'file_async': Flush periodically (Fast & Durable) -
172
209
  // DEFAULT: 'file_async': Flush every 50ms or 5000 msgs
@@ -207,7 +244,7 @@ await orders.subscribe('audit-log', (order) => saveAudit(order));
207
244
  ### Binary Payloads (Zero-Overhead)
208
245
 
209
246
  All Nexo brokers (**Store, Queue, Stream, PubSub**) natively support raw binary data (`Buffer`).
210
- Bypassing JSON serialization drastically reduces Latency, increases Throughput, and saves Bandwidth (~30% smaller payloads) .
247
+ Bypassing JSON serialization drastically reduces Latency, increases Throughput, and saves Bandwidth.
211
248
 
212
249
  **Perfect for:** Video chunks, Images, Protobuf/MsgPack, Encrypted blobs.
213
250
 
@@ -217,13 +254,10 @@ const heavyPayload = Buffer.alloc(1024 * 1024);
217
254
 
218
255
  // 1. STREAM: Replayable Data (e.g. CCTV Recording, Event Sourcing)
219
256
  await client.stream('cctv-archive').publish(heavyPayload);
220
-
221
257
  // 2. PUBSUB: Ephemeral Live Data (e.g. VoIP, Real-time Sensor)
222
258
  await client.pubsub('live-audio-call').publish(heavyPayload);
223
-
224
259
  // 3. STORE (Cache Images)
225
260
  await client.store.map.set('user:avatar:1', heavyPayload);
226
-
227
261
  // 4. QUEUE (Process Files)
228
262
  await client.queue('pdf-processing').push(heavyPayload);
229
263
  ```
@@ -1,6 +1,6 @@
1
1
  import { NexoConnection } from '../connection';
2
2
  import { Logger } from '../utils/logger';
3
- export type PersistenceStrategy = 'memory' | 'file_sync' | 'file_async';
3
+ export type PersistenceStrategy = 'file_sync' | 'file_async';
4
4
  export interface QueueConfig {
5
5
  visibilityTimeoutMs?: number;
6
6
  maxRetries?: number;
@@ -16,12 +16,61 @@ export interface QueuePushOptions {
16
16
  priority?: number;
17
17
  delayMs?: number;
18
18
  }
19
+ /**
20
+ * Dead Letter Queue (DLQ) management for a queue.
21
+ * Provides methods to inspect, replay, delete, and purge failed messages.
22
+ */
23
+ export declare class NexoDLQ<T = any> {
24
+ private conn;
25
+ private queueName;
26
+ private logger;
27
+ constructor(conn: NexoConnection, queueName: string, logger: Logger);
28
+ /**
29
+ * Peek messages in the DLQ without consuming them.
30
+ * @param limit Maximum number of messages to return (default: 10)
31
+ * @param offset Pagination offset (default: 0)
32
+ * @returns Object containing total count and array of messages
33
+ */
34
+ peek(limit?: number, offset?: number): Promise<{
35
+ total: number;
36
+ items: {
37
+ id: string;
38
+ data: T;
39
+ attempts: number;
40
+ failureReason: string;
41
+ }[];
42
+ }>;
43
+ /**
44
+ * Move a message from DLQ back to the main queue (replay/retry).
45
+ * The message will be reset with attempts = 0 and become available for consumption.
46
+ * @param messageId ID of the message to move
47
+ * @returns true if the message was moved, false if not found
48
+ */
49
+ moveToQueue(messageId: string): Promise<boolean>;
50
+ /**
51
+ * Delete a specific message from the DLQ.
52
+ * @param messageId ID of the message to delete
53
+ * @returns true if the message was deleted, false if not found
54
+ */
55
+ delete(messageId: string): Promise<boolean>;
56
+ /**
57
+ * Purge all messages from the DLQ.
58
+ * @returns Number of messages purged
59
+ */
60
+ purge(): Promise<number>;
61
+ }
19
62
  export declare class NexoQueue<T = any> {
20
63
  private conn;
21
64
  readonly name: string;
22
65
  private logger;
23
66
  private isSubscribed;
67
+ private _dlq;
24
68
  constructor(conn: NexoConnection, name: string, logger: Logger);
69
+ /**
70
+ * Access the Dead Letter Queue (DLQ) for this queue.
71
+ * Use this to inspect, replay, delete, or purge failed messages.
72
+ */
73
+ get dlq(): NexoDLQ<T>;
25
74
  create(config?: QueueConfig): Promise<this>;
26
75
  exists(): Promise<boolean>;
27
76
  delete(): Promise<void>;
@@ -30,4 +79,5 @@ export declare class NexoQueue<T = any> {
30
79
  stop: () => void;
31
80
  }>;
32
81
  private ack;
82
+ private nack;
33
83
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NexoQueue = void 0;
3
+ exports.NexoQueue = exports.NexoDLQ = void 0;
4
4
  const codec_1 = require("../codec");
5
5
  const errors_1 = require("../errors");
6
6
  var QueueOpcode;
@@ -11,6 +11,11 @@ var QueueOpcode;
11
11
  QueueOpcode[QueueOpcode["Q_ACK"] = 19] = "Q_ACK";
12
12
  QueueOpcode[QueueOpcode["Q_EXISTS"] = 20] = "Q_EXISTS";
13
13
  QueueOpcode[QueueOpcode["Q_DELETE"] = 21] = "Q_DELETE";
14
+ QueueOpcode[QueueOpcode["Q_PEEK_DLQ"] = 22] = "Q_PEEK_DLQ";
15
+ QueueOpcode[QueueOpcode["Q_MOVE_TO_QUEUE"] = 23] = "Q_MOVE_TO_QUEUE";
16
+ QueueOpcode[QueueOpcode["Q_DELETE_DLQ"] = 24] = "Q_DELETE_DLQ";
17
+ QueueOpcode[QueueOpcode["Q_PURGE_DLQ"] = 25] = "Q_PURGE_DLQ";
18
+ QueueOpcode[QueueOpcode["Q_NACK"] = 26] = "Q_NACK";
14
19
  })(QueueOpcode || (QueueOpcode = {}));
15
20
  const QueueCommands = {
16
21
  create: (conn, name, config) => conn.send(QueueOpcode.Q_CREATE, codec_1.FrameCodec.string(name), codec_1.FrameCodec.string(JSON.stringify(config || {}))),
@@ -41,6 +46,36 @@ const QueueCommands = {
41
46
  return messages;
42
47
  },
43
48
  ack: (conn, name, id) => conn.send(QueueOpcode.Q_ACK, codec_1.FrameCodec.uuid(id), codec_1.FrameCodec.string(name)),
49
+ nack: (conn, name, id, reason) => conn.send(QueueOpcode.Q_NACK, codec_1.FrameCodec.uuid(id), codec_1.FrameCodec.string(name), codec_1.FrameCodec.string(reason)),
50
+ // DLQ Commands
51
+ peekDLQ: async (conn, name, limit, offset) => {
52
+ const res = await conn.send(QueueOpcode.Q_PEEK_DLQ, codec_1.FrameCodec.string(name), codec_1.FrameCodec.u32(limit), codec_1.FrameCodec.u32(offset));
53
+ const total = res.cursor.readU32();
54
+ const count = res.cursor.readU32();
55
+ const items = [];
56
+ for (let i = 0; i < count; i++) {
57
+ const idHex = res.cursor.readUUID();
58
+ const payloadLen = res.cursor.readU32();
59
+ const payloadBuf = res.cursor.readBuffer(payloadLen);
60
+ const data = codec_1.FrameCodec.decodeAny(new codec_1.Cursor(payloadBuf));
61
+ const attempts = res.cursor.readU32();
62
+ const failureReason = res.cursor.readString();
63
+ items.push({ id: idHex, data, attempts, failureReason });
64
+ }
65
+ return { total, items };
66
+ },
67
+ moveToQueue: async (conn, name, messageId) => {
68
+ const res = await conn.send(QueueOpcode.Q_MOVE_TO_QUEUE, codec_1.FrameCodec.string(name), codec_1.FrameCodec.uuid(messageId));
69
+ return res.cursor.readU8() === 1;
70
+ },
71
+ deleteDLQ: async (conn, name, messageId) => {
72
+ const res = await conn.send(QueueOpcode.Q_DELETE_DLQ, codec_1.FrameCodec.string(name), codec_1.FrameCodec.uuid(messageId));
73
+ return res.cursor.readU8() === 1;
74
+ },
75
+ purgeDLQ: async (conn, name) => {
76
+ const res = await conn.send(QueueOpcode.Q_PURGE_DLQ, codec_1.FrameCodec.string(name));
77
+ return res.cursor.readU32();
78
+ },
44
79
  };
45
80
  async function runConcurrent(items, concurrency, fn) {
46
81
  if (concurrency === 1) {
@@ -55,12 +90,69 @@ async function runConcurrent(items, concurrency, fn) {
55
90
  });
56
91
  await Promise.all(workers);
57
92
  }
93
+ /**
94
+ * Dead Letter Queue (DLQ) management for a queue.
95
+ * Provides methods to inspect, replay, delete, and purge failed messages.
96
+ */
97
+ class NexoDLQ {
98
+ constructor(conn, queueName, logger) {
99
+ this.conn = conn;
100
+ this.queueName = queueName;
101
+ this.logger = logger;
102
+ }
103
+ /**
104
+ * Peek messages in the DLQ without consuming them.
105
+ * @param limit Maximum number of messages to return (default: 10)
106
+ * @param offset Pagination offset (default: 0)
107
+ * @returns Object containing total count and array of messages
108
+ */
109
+ async peek(limit = 10, offset = 0) {
110
+ this.logger.debug(`[DLQ:${this.queueName}] Peeking ${limit} messages at offset ${offset}`);
111
+ return QueueCommands.peekDLQ(this.conn, this.queueName, limit, offset);
112
+ }
113
+ /**
114
+ * Move a message from DLQ back to the main queue (replay/retry).
115
+ * The message will be reset with attempts = 0 and become available for consumption.
116
+ * @param messageId ID of the message to move
117
+ * @returns true if the message was moved, false if not found
118
+ */
119
+ async moveToQueue(messageId) {
120
+ this.logger.debug(`[DLQ:${this.queueName}] Moving message ${messageId} to main queue`);
121
+ return QueueCommands.moveToQueue(this.conn, this.queueName, messageId);
122
+ }
123
+ /**
124
+ * Delete a specific message from the DLQ.
125
+ * @param messageId ID of the message to delete
126
+ * @returns true if the message was deleted, false if not found
127
+ */
128
+ async delete(messageId) {
129
+ this.logger.debug(`[DLQ:${this.queueName}] Deleting message ${messageId}`);
130
+ return QueueCommands.deleteDLQ(this.conn, this.queueName, messageId);
131
+ }
132
+ /**
133
+ * Purge all messages from the DLQ.
134
+ * @returns Number of messages purged
135
+ */
136
+ async purge() {
137
+ this.logger.debug(`[DLQ:${this.queueName}] Purging all messages`);
138
+ return QueueCommands.purgeDLQ(this.conn, this.queueName);
139
+ }
140
+ }
141
+ exports.NexoDLQ = NexoDLQ;
58
142
  class NexoQueue {
59
143
  constructor(conn, name, logger) {
60
144
  this.conn = conn;
61
145
  this.name = name;
62
146
  this.logger = logger;
63
147
  this.isSubscribed = false;
148
+ this._dlq = new NexoDLQ(conn, name, logger);
149
+ }
150
+ /**
151
+ * Access the Dead Letter Queue (DLQ) for this queue.
152
+ * Use this to inspect, replay, delete, or purge failed messages.
153
+ */
154
+ get dlq() {
155
+ return this._dlq;
64
156
  }
65
157
  async create(config = {}) {
66
158
  await QueueCommands.create(this.conn, this.name, config);
@@ -93,8 +185,6 @@ class NexoQueue {
93
185
  await new Promise(r => setTimeout(r, 500));
94
186
  continue;
95
187
  }
96
- // Add small jitter to avoid thundering herd on reconnect
97
- await new Promise(r => setTimeout(r, Math.random() * 500));
98
188
  try {
99
189
  // Double check before sending
100
190
  if (!this.conn.isConnected)
@@ -103,8 +193,13 @@ class NexoQueue {
103
193
  if (messages.length === 0)
104
194
  continue;
105
195
  await runConcurrent(messages, concurrency, async (msg) => {
106
- if (!active)
196
+ if (!active) {
197
+ if (this.conn.isConnected) {
198
+ // If stopped while consuming, release message immediately so it's available for others
199
+ await this.nack(msg.id, "Consumer stopped").catch(() => { });
200
+ }
107
201
  return;
202
+ }
108
203
  try {
109
204
  await callback(msg.data);
110
205
  await this.ack(msg.id);
@@ -112,7 +207,9 @@ class NexoQueue {
112
207
  catch (e) {
113
208
  if (!this.conn.isConnected)
114
209
  return;
115
- this.logger.error(`Callback error in queue ${this.name}:`, e);
210
+ const reason = e instanceof Error ? e.message : String(e);
211
+ this.logger.error(`[Queue:${this.name}] Consumer error, sending NACK. Reason: ${reason}`);
212
+ await this.nack(msg.id, reason);
116
213
  }
117
214
  });
118
215
  }
@@ -133,10 +230,18 @@ class NexoQueue {
133
230
  loop().catch(err => {
134
231
  this.logger.error(`[CRITICAL] Queue loop crashed for ${this.name}`, err);
135
232
  });
136
- return { stop: () => { active = false; } };
233
+ return {
234
+ stop: () => {
235
+ active = false;
236
+ this.isSubscribed = false;
237
+ }
238
+ };
137
239
  }
138
240
  async ack(id) {
139
241
  await QueueCommands.ack(this.conn, this.name, id);
140
242
  }
243
+ async nack(id, reason) {
244
+ await QueueCommands.nack(this.conn, this.name, id, reason);
245
+ }
141
246
  }
142
247
  exports.NexoQueue = NexoQueue;
@@ -1,6 +1,6 @@
1
1
  import { NexoConnection } from '../connection';
2
2
  import { Logger } from '../utils/logger';
3
- export type PersistenceStrategy = 'memory' | 'file_sync' | 'file_async';
3
+ export type PersistenceStrategy = 'file_sync' | 'file_async';
4
4
  export interface RetentionOptions {
5
5
  maxAgeMs: number;
6
6
  maxBytes: number;
@@ -85,8 +85,6 @@ class StreamSubscription {
85
85
  await this.backoff(500);
86
86
  continue;
87
87
  }
88
- // Jitter to avoid thundering herd
89
- await this.backoff(Math.random() * 500);
90
88
  try {
91
89
  if (!this.conn.isConnected)
92
90
  continue;
package/dist/codec.js CHANGED
@@ -51,7 +51,10 @@ class FrameCodec {
51
51
  return result;
52
52
  }
53
53
  static uuid(hex) {
54
- return Buffer.from(hex, 'hex');
54
+ // Remove dashes from UUID format (e.g., "550e8400-e29b-41d4-a716-446655440000")
55
+ // This handles both formats: with dashes (from crypto.randomUUID()) and without (from server)
56
+ const cleanHex = hex.replace(/-/g, '');
57
+ return Buffer.from(cleanHex, 'hex');
55
58
  }
56
59
  static any(data) {
57
60
  let type = protocol_1.DataType.RAW;
@@ -74,25 +77,24 @@ class FrameCodec {
74
77
  return result;
75
78
  }
76
79
  static packRequest(id, opcode, ...parts) {
77
- // 1. Calcolo dimensione PAYLOAD (Opcode [1 byte] + Argomenti)
78
- let payloadSize = 1;
80
+ // 1. Calcolo dimensione PAYLOAD
81
+ let payloadSize = 0;
79
82
  for (const part of parts)
80
83
  payloadSize += part.length;
81
- // 2. Allocazione buffer TOTALE (Header [9 bytes] + Payload)
82
- // Header è sempre fisso a 9 bytes: [Type:1][ID:4][Len:4]
83
- const result = Buffer.allocUnsafe(9 + payloadSize);
84
- // --- SCRITTURA HEADER (Bytes 0-8) ---
84
+ // 2. Allocazione buffer TOTALE (Header [10 bytes] + Payload)
85
+ // Header: [Type:1][Opcode:1][ID:4][Len:4]
86
+ const result = Buffer.allocUnsafe(10 + payloadSize);
87
+ // --- SCRITTURA HEADER (Bytes 0-9) ---
85
88
  // Byte 0: Tipo di Frame (REQUEST = 0x01)
86
89
  result.writeUInt8(protocol_1.FrameType.REQUEST, 0);
87
- // Bytes 1-4: ID della richiesta (UInt32 Big Endian)
88
- result.writeUInt32BE(id, 1);
89
- // Bytes 5-8: Lunghezza del Payload (UInt32 Big Endian)
90
- // Questo dice al server quanti byte leggere dopo l'header
91
- result.writeUInt32BE(payloadSize, 5);
92
- // --- SCRITTURA PAYLOAD (Bytes 9+) ---
93
- // Byte 9: Opcode (Il primo byte del payload è sempre l'operazione)
94
- result.writeUInt8(opcode, 9);
95
- // Bytes 10+: Copia degli argomenti (dati effettivi)
90
+ // Byte 1: Opcode
91
+ result.writeUInt8(opcode, 1);
92
+ // Bytes 2-5: ID della richiesta (UInt32 Big Endian)
93
+ result.writeUInt32BE(id, 2);
94
+ // Bytes 6-9: Lunghezza del Payload (UInt32 Big Endian)
95
+ result.writeUInt32BE(payloadSize, 6);
96
+ // --- SCRITTURA PAYLOAD (Bytes 10+) ---
97
+ // Copia degli argomenti (dati effettivi)
96
98
  let offset = 10;
97
99
  for (const part of parts) {
98
100
  part.copy(result, offset);
@@ -54,7 +54,7 @@ class NexoConnection extends events_1.EventEmitter {
54
54
  this.port = options.port;
55
55
  this.logger = logger;
56
56
  this.socket = new net.Socket();
57
- this.requestTimeoutMs = options.requestTimeoutMs ?? 10000;
57
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 15000;
58
58
  }
59
59
  async connect() {
60
60
  this.shouldReconnect = true;
@@ -126,11 +126,11 @@ class NexoConnection extends events_1.EventEmitter {
126
126
  this.chunks = [];
127
127
  }
128
128
  while (true) {
129
- // Need at least header (9 bytes)
130
- if (this.buffer.length < 9)
129
+ // Need at least header (10 bytes): [Type:1][Opcode:1][ID:4][Len:4]
130
+ if (this.buffer.length < 10)
131
131
  break;
132
- const payloadLen = this.buffer.readUInt32BE(5);
133
- const totalFrameLen = 9 + payloadLen;
132
+ const payloadLen = this.buffer.readUInt32BE(6);
133
+ const totalFrameLen = 10 + payloadLen;
134
134
  if (this.buffer.length < totalFrameLen)
135
135
  break;
136
136
  // 2. Slice Frame
@@ -142,6 +142,7 @@ class NexoConnection extends events_1.EventEmitter {
142
142
  handleFrame(frame) {
143
143
  const cursor = new codec_1.Cursor(frame);
144
144
  const type = cursor.readU8();
145
+ const opcodeOrStatus = cursor.readU8(); // Opcode for requests, Status for responses
145
146
  const id = cursor.readU32();
146
147
  cursor.readU32(); // Skip payloadLen
147
148
  const payload = cursor.buf.subarray(cursor.offset);
@@ -150,8 +151,8 @@ class NexoConnection extends events_1.EventEmitter {
150
151
  const req = this.pending.get(id);
151
152
  if (req) {
152
153
  this.pending.delete(id);
153
- // Payload: [Status:1][Data...]
154
- req.resolve({ status: payload[0], data: payload.subarray(1) });
154
+ // Status is in header (byte 1), payload is clean data
155
+ req.resolve({ status: opcodeOrStatus, data: payload });
155
156
  }
156
157
  break;
157
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emanuelepifani/nexo-client",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "High-performance TypeScript Client for Nexo Broker",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,6 +13,8 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "tsc",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
16
18
  "prepublishOnly": "npm run build"
17
19
  },
18
20
  "repository": {
@@ -37,6 +39,9 @@
37
39
  ],
38
40
  "devDependencies": {
39
41
  "@types/node": "^20.10.0",
40
- "typescript": "^5.3.0"
42
+ "typescript": "^5.3.0",
43
+ "vitest": "^1.2.0",
44
+ "tsx": "^4.7.0",
45
+ "zod": "^4.2.1"
41
46
  }
42
47
  }
@@ -1,2 +0,0 @@
1
- declare function performanceTest(): void;
2
- export { performanceTest };
@@ -1,58 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.performanceTest = performanceTest;
4
- const ringbuffer_1 = require("./ringbuffer");
5
- // Simple performance test for RingBuffer vs Buffer.concat
6
- function performanceTest() {
7
- const iterations = 10000;
8
- const chunkSize = 1024;
9
- const totalData = iterations * chunkSize;
10
- console.log(`Testing performance with ${iterations} chunks of ${chunkSize} bytes each...`);
11
- // Test 1: Buffer.concat (old method)
12
- console.time('Buffer.concat');
13
- const chunks = [];
14
- let totalBuffer = Buffer.alloc(0);
15
- for (let i = 0; i < iterations; i++) {
16
- const chunk = Buffer.allocUnsafe(chunkSize);
17
- chunk.fill(Math.floor(Math.random() * 256));
18
- chunks.push(chunk);
19
- // Simulate periodic concatenation (like the old code)
20
- if (chunks.length > 10) {
21
- totalBuffer = Buffer.concat([totalBuffer, ...chunks]);
22
- chunks.length = 0;
23
- }
24
- }
25
- // Final concat
26
- if (chunks.length > 0) {
27
- totalBuffer = Buffer.concat([totalBuffer, ...chunks]);
28
- }
29
- console.timeEnd('Buffer.concat');
30
- // Test 2: RingBuffer (new method)
31
- console.time('RingBuffer');
32
- const ringBuffer = new ringbuffer_1.RingBuffer();
33
- for (let i = 0; i < iterations; i++) {
34
- const chunk = Buffer.allocUnsafe(chunkSize);
35
- chunk.fill(Math.floor(Math.random() * 256));
36
- ringBuffer.write(chunk);
37
- }
38
- // Read all data
39
- const result = ringBuffer.consume(ringBuffer.available);
40
- console.timeEnd('RingBuffer');
41
- console.log(`Total data processed: ${(totalData / 1024 / 1024).toFixed(2)} MB`);
42
- console.log(`Buffer.concat result length: ${totalBuffer.length}`);
43
- console.log(`RingBuffer result length: ${result.length}`);
44
- // Memory usage comparison
45
- const memBefore = process.memoryUsage();
46
- // Test memory efficiency
47
- const largeRingBuffer = new ringbuffer_1.RingBuffer(1024 * 1024); // 1MB initial
48
- for (let i = 0; i < 1000; i++) {
49
- const chunk = Buffer.allocUnsafe(4096);
50
- largeRingBuffer.write(chunk);
51
- }
52
- const memAfter = process.memoryUsage();
53
- console.log(`Memory usage increase: ${((memAfter.heapUsed - memBefore.heapUsed) / 1024 / 1024).toFixed(2)} MB`);
54
- }
55
- // Run the test
56
- if (require.main === module) {
57
- performanceTest();
58
- }
@@ -1,27 +0,0 @@
1
- /** @internal */
2
- export declare class RingBuffer {
3
- private buffer;
4
- private head;
5
- private tail;
6
- private size;
7
- private capacity;
8
- constructor(initialCapacity?: number);
9
- /** Write data to ring buffer */
10
- write(data: Buffer): number;
11
- /** Read data from ring buffer without consuming it */
12
- peek(length: number): Buffer;
13
- /** Consume data from ring buffer */
14
- consume(length: number): Buffer;
15
- /** Get current data size */
16
- get available(): number;
17
- /** Get available space for writing */
18
- get freeSpace(): number;
19
- /** Check if buffer is empty */
20
- get isEmpty(): boolean;
21
- /** Clear all data */
22
- clear(): void;
23
- /** Ensure capacity for new data */
24
- private ensureCapacity;
25
- /** Get next power of 2 */
26
- private nextPowerOf2;
27
- }
@@ -1,109 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RingBuffer = void 0;
4
- /** @internal */
5
- class RingBuffer {
6
- constructor(initialCapacity = 8192) {
7
- this.head = 0; // write position
8
- this.tail = 0; // read position
9
- this.size = 0; // current data size
10
- this.capacity = this.nextPowerOf2(initialCapacity);
11
- this.buffer = Buffer.allocUnsafe(this.capacity);
12
- }
13
- /** Write data to ring buffer */
14
- write(data) {
15
- const dataLen = data.length;
16
- // Ensure we have enough space
17
- this.ensureCapacity(this.size + dataLen);
18
- // Write data in potentially two chunks (if wrapping around)
19
- const availableToEnd = this.capacity - this.head;
20
- if (dataLen <= availableToEnd) {
21
- // Fits in one chunk
22
- data.copy(this.buffer, this.head);
23
- this.head = (this.head + dataLen) & (this.capacity - 1);
24
- }
25
- else {
26
- // Needs to wrap around
27
- const firstChunk = data.subarray(0, availableToEnd);
28
- const secondChunk = data.subarray(availableToEnd);
29
- firstChunk.copy(this.buffer, this.head);
30
- secondChunk.copy(this.buffer, 0);
31
- this.head = secondChunk.length;
32
- }
33
- this.size += dataLen;
34
- return dataLen;
35
- }
36
- /** Read data from ring buffer without consuming it */
37
- peek(length) {
38
- if (length > this.size) {
39
- length = this.size;
40
- }
41
- if (length === 0)
42
- return Buffer.alloc(0);
43
- const availableToEnd = this.capacity - this.tail;
44
- if (length <= availableToEnd) {
45
- // Data is contiguous
46
- return this.buffer.subarray(this.tail, this.tail + length);
47
- }
48
- else {
49
- // Data wraps around, need to concatenate
50
- const firstChunk = this.buffer.subarray(this.tail, this.capacity);
51
- const secondChunk = this.buffer.subarray(0, length - availableToEnd);
52
- return Buffer.concat([firstChunk, secondChunk]);
53
- }
54
- }
55
- /** Consume data from ring buffer */
56
- consume(length) {
57
- const data = this.peek(length);
58
- this.tail = (this.tail + length) & (this.capacity - 1);
59
- this.size -= length;
60
- return data;
61
- }
62
- /** Get current data size */
63
- get available() {
64
- return this.size;
65
- }
66
- /** Get available space for writing */
67
- get freeSpace() {
68
- return this.capacity - this.size;
69
- }
70
- /** Check if buffer is empty */
71
- get isEmpty() {
72
- return this.size === 0;
73
- }
74
- /** Clear all data */
75
- clear() {
76
- this.head = 0;
77
- this.tail = 0;
78
- this.size = 0;
79
- }
80
- /** Ensure capacity for new data */
81
- ensureCapacity(required) {
82
- if (required <= this.capacity)
83
- return;
84
- const newCapacity = this.nextPowerOf2(required);
85
- const newBuffer = Buffer.allocUnsafe(newCapacity);
86
- // Copy existing data to new buffer
87
- if (this.size > 0) {
88
- const data = this.peek(this.size);
89
- data.copy(newBuffer, 0);
90
- }
91
- this.buffer = newBuffer;
92
- this.capacity = newCapacity;
93
- this.head = this.size;
94
- this.tail = 0;
95
- }
96
- /** Get next power of 2 */
97
- nextPowerOf2(n) {
98
- if (n <= 0)
99
- return 1;
100
- n--;
101
- n |= n >> 1;
102
- n |= n >> 2;
103
- n |= n >> 4;
104
- n |= n >> 8;
105
- n |= n >> 16;
106
- return n + 1;
107
- }
108
- }
109
- exports.RingBuffer = RingBuffer;