memcache 0.2.0 → 1.0.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/dist/index.cjs ADDED
@@ -0,0 +1,1502 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Memcache: () => Memcache,
24
+ MemcacheEvents: () => MemcacheEvents,
25
+ createNode: () => createNode,
26
+ default: () => index_default
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var import_hookified2 = require("hookified");
30
+
31
+ // src/ketama.ts
32
+ var import_node_crypto = require("crypto");
33
+ var hashFunctionForBuiltin = (algorithm) => (value) => (0, import_node_crypto.createHash)(algorithm).update(value).digest().readInt32BE();
34
+ var keyFor = (node) => typeof node === "string" ? node : node.key;
35
+ var HashRing = class _HashRing {
36
+ /**
37
+ * Base weight of each node in the hash ring. Having a base weight of 1 is
38
+ * not very desirable, since then, due to the ketama-style "clock", it's
39
+ * possible to end up with a clock that's very skewed when dealing with a
40
+ * small number of nodes. Setting to 50 nodes seems to give a better
41
+ * distribution, so that load is spread roughly evenly to +/- 5%.
42
+ */
43
+ static baseWeight = 50;
44
+ /** The hash function used to compute node positions on the ring */
45
+ hashFn;
46
+ /** The sorted array of [hash, node key] tuples representing virtual nodes on the ring */
47
+ _clock = [];
48
+ /** Map of node keys to actual node objects */
49
+ _nodes = /* @__PURE__ */ new Map();
50
+ /**
51
+ * Gets the sorted array of [hash, node key] tuples representing virtual nodes on the ring.
52
+ * @returns The hash clock array
53
+ */
54
+ get clock() {
55
+ return this._clock;
56
+ }
57
+ /**
58
+ * Gets the map of node keys to actual node objects.
59
+ * @returns The nodes map
60
+ */
61
+ get nodes() {
62
+ return this._nodes;
63
+ }
64
+ /**
65
+ * Creates a new HashRing instance.
66
+ *
67
+ * @param initialNodes - Array of nodes to add to the ring, optionally with weights
68
+ * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * // Simple ring with default SHA-1 hashing
73
+ * const ring = new HashRing(['node1', 'node2']);
74
+ *
75
+ * // Ring with custom hash function
76
+ * const customRing = new HashRing(['node1', 'node2'], 'md5');
77
+ *
78
+ * // Ring with weighted nodes
79
+ * const weightedRing = new HashRing([
80
+ * { node: 'heavy-server', weight: 3 },
81
+ * { node: 'light-server', weight: 1 }
82
+ * ]);
83
+ * ```
84
+ */
85
+ constructor(initialNodes = [], hashFn = "sha1") {
86
+ this.hashFn = typeof hashFn === "string" ? hashFunctionForBuiltin(hashFn) : hashFn;
87
+ for (const node of initialNodes) {
88
+ if (typeof node === "object" && "weight" in node && "node" in node) {
89
+ this.addNode(node.node, node.weight);
90
+ } else {
91
+ this.addNode(node);
92
+ }
93
+ }
94
+ }
95
+ /**
96
+ * Add a new node to the ring. If the node already exists in the ring, it
97
+ * will be updated. For example, you can use this to update the node's weight.
98
+ *
99
+ * @param node - The node to add to the ring
100
+ * @param weight - The relative weight of this node (default: 1). Higher weights mean more keys will be assigned to this node. A weight of 0 removes the node.
101
+ * @throws {RangeError} If weight is negative
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * const ring = new HashRing();
106
+ * ring.addNode('server1'); // Add with default weight of 1
107
+ * ring.addNode('server2', 2); // Add with weight of 2 (will handle ~2x more keys)
108
+ * ring.addNode('server1', 3); // Update server1's weight to 3
109
+ * ring.addNode('server2', 0); // Remove server2
110
+ * ```
111
+ */
112
+ addNode(node, weight = 1) {
113
+ if (weight === 0) {
114
+ this.removeNode(node);
115
+ } else if (weight < 0) {
116
+ throw new RangeError("Cannot add a node to the hashring with weight < 0");
117
+ } else {
118
+ this.removeNode(node);
119
+ const key = keyFor(node);
120
+ this._nodes.set(key, node);
121
+ this.addNodeToClock(key, Math.round(weight * _HashRing.baseWeight));
122
+ }
123
+ }
124
+ /**
125
+ * Removes the node from the ring. No-op if the node does not exist.
126
+ *
127
+ * @param node - The node to remove from the ring
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * const ring = new HashRing(['server1', 'server2']);
132
+ * ring.removeNode('server1'); // Removes server1 from the ring
133
+ * ring.removeNode('nonexistent'); // Safe to call with non-existent node
134
+ * ```
135
+ */
136
+ removeNode(node) {
137
+ const key = keyFor(node);
138
+ if (this._nodes.delete(key)) {
139
+ this._clock = this._clock.filter(([, n]) => n !== key);
140
+ }
141
+ }
142
+ /**
143
+ * Gets the node which should handle the given input key. Returns undefined if
144
+ * the hashring has no nodes.
145
+ *
146
+ * Uses consistent hashing to ensure the same input always maps to the same node,
147
+ * and minimizes redistribution when nodes are added or removed.
148
+ *
149
+ * @param input - The key to find the responsible node for (string or Buffer)
150
+ * @returns The node responsible for this key, or undefined if ring is empty
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const ring = new HashRing(['server1', 'server2', 'server3']);
155
+ * const node = ring.getNode('user:123'); // Returns e.g., 'server2'
156
+ * const sameNode = ring.getNode('user:123'); // Always returns 'server2'
157
+ *
158
+ * // Also accepts Buffer input
159
+ * const bufferNode = ring.getNode(Buffer.from('user:123'));
160
+ * ```
161
+ */
162
+ getNode(input) {
163
+ if (this._clock.length === 0) {
164
+ return void 0;
165
+ }
166
+ const index = this.getIndexForInput(input);
167
+ const key = index === this._clock.length ? this._clock[0][1] : this._clock[index][1];
168
+ return this._nodes.get(key);
169
+ }
170
+ /**
171
+ * Finds the index in the clock for the given input by hashing it and performing binary search.
172
+ *
173
+ * @param input - The input to find the clock position for
174
+ * @returns The index in the clock array
175
+ */
176
+ getIndexForInput(input) {
177
+ const hash = this.hashFn(
178
+ typeof input === "string" ? Buffer.from(input) : input
179
+ );
180
+ return binarySearchRing(this._clock, hash);
181
+ }
182
+ /**
183
+ * Gets multiple replica nodes that should handle the given input. Useful for
184
+ * implementing replication strategies where you want to store data on multiple nodes.
185
+ *
186
+ * The returned array will contain unique nodes in the order they appear on the ring
187
+ * starting from the primary node. If there are fewer nodes than replicas requested,
188
+ * all nodes are returned.
189
+ *
190
+ * @param input - The key to find replica nodes for (string or Buffer)
191
+ * @param replicas - The number of replica nodes to return
192
+ * @returns Array of nodes that should handle this key (length ≤ replicas)
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * const ring = new HashRing(['server1', 'server2', 'server3', 'server4']);
197
+ *
198
+ * // Get 3 replicas for a key
199
+ * const replicas = ring.getNodes('user:123', 3);
200
+ * // Returns e.g., ['server2', 'server4', 'server1']
201
+ *
202
+ * // If requesting more replicas than nodes, returns all nodes
203
+ * const allNodes = ring.getNodes('user:123', 10);
204
+ * // Returns ['server1', 'server2', 'server3', 'server4']
205
+ * ```
206
+ */
207
+ getNodes(input, replicas) {
208
+ if (this._clock.length === 0) {
209
+ return [];
210
+ }
211
+ if (replicas >= this._nodes.size) {
212
+ return [...this._nodes.values()];
213
+ }
214
+ const chosen = /* @__PURE__ */ new Set();
215
+ for (let i = this.getIndexForInput(input); chosen.size < replicas; i++) {
216
+ chosen.add(this._clock[i % this._clock.length][1]);
217
+ }
218
+ return [...chosen].map((c) => this._nodes.get(c));
219
+ }
220
+ /**
221
+ * Adds virtual nodes to the clock for the given node key.
222
+ * Creates multiple positions on the ring for better distribution.
223
+ *
224
+ * @param key - The node key to add to the clock
225
+ * @param weight - The number of virtual nodes to create (weight * baseWeight)
226
+ */
227
+ addNodeToClock(key, weight) {
228
+ for (let i = weight; i > 0; i--) {
229
+ const hash = this.hashFn(Buffer.from(`${key}\0${i}`));
230
+ this._clock.push([hash, key]);
231
+ }
232
+ this._clock.sort((a, b) => a[0] - b[0]);
233
+ }
234
+ };
235
+ var KetamaHash = class {
236
+ /** The name of this distribution strategy */
237
+ name = "ketama";
238
+ /** Internal hash ring for consistent hashing */
239
+ hashRing;
240
+ /** Map of node IDs to MemcacheNode instances */
241
+ nodeMap;
242
+ /**
243
+ * Creates a new KetamaDistributionHash instance.
244
+ *
245
+ * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * // Use default SHA-1 hashing
250
+ * const distribution = new KetamaDistributionHash();
251
+ *
252
+ * // Use MD5 hashing
253
+ * const distribution = new KetamaDistributionHash('md5');
254
+ * ```
255
+ */
256
+ constructor(hashFn) {
257
+ this.hashRing = new HashRing([], hashFn);
258
+ this.nodeMap = /* @__PURE__ */ new Map();
259
+ }
260
+ /**
261
+ * Gets all nodes in the distribution.
262
+ * @returns Array of all MemcacheNode instances
263
+ */
264
+ get nodes() {
265
+ return Array.from(this.nodeMap.values());
266
+ }
267
+ /**
268
+ * Adds a node to the distribution with its weight for consistent hashing.
269
+ *
270
+ * @param node - The MemcacheNode to add
271
+ *
272
+ * @example
273
+ * ```typescript
274
+ * const node = new MemcacheNode('localhost', 11211, { weight: 2 });
275
+ * distribution.addNode(node);
276
+ * ```
277
+ */
278
+ addNode(node) {
279
+ this.nodeMap.set(node.id, node);
280
+ this.hashRing.addNode(node.id, node.weight);
281
+ }
282
+ /**
283
+ * Removes a node from the distribution by its ID.
284
+ *
285
+ * @param id - The node ID (e.g., "localhost:11211")
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * distribution.removeNode('localhost:11211');
290
+ * ```
291
+ */
292
+ removeNode(id) {
293
+ this.nodeMap.delete(id);
294
+ this.hashRing.removeNode(id);
295
+ }
296
+ /**
297
+ * Gets a specific node by its ID.
298
+ *
299
+ * @param id - The node ID (e.g., "localhost:11211")
300
+ * @returns The MemcacheNode if found, undefined otherwise
301
+ *
302
+ * @example
303
+ * ```typescript
304
+ * const node = distribution.getNode('localhost:11211');
305
+ * if (node) {
306
+ * console.log(`Found node: ${node.uri}`);
307
+ * }
308
+ * ```
309
+ */
310
+ getNode(id) {
311
+ return this.nodeMap.get(id);
312
+ }
313
+ /**
314
+ * Gets the nodes responsible for a given key using consistent hashing.
315
+ * Currently returns a single node (the primary node for the key).
316
+ *
317
+ * @param key - The cache key to find the responsible node for
318
+ * @returns Array containing the responsible node(s), empty if no nodes available
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * const nodes = distribution.getNodesByKey('user:123');
323
+ * if (nodes.length > 0) {
324
+ * console.log(`Key will be stored on: ${nodes[0].id}`);
325
+ * }
326
+ * ```
327
+ */
328
+ getNodesByKey(key) {
329
+ const nodeId = this.hashRing.getNode(key);
330
+ if (!nodeId) {
331
+ return [];
332
+ }
333
+ const node = this.nodeMap.get(nodeId);
334
+ return node ? [node] : [];
335
+ }
336
+ };
337
+ function binarySearchRing(ring, hash) {
338
+ let mid;
339
+ let lo = 0;
340
+ let hi = ring.length - 1;
341
+ while (lo <= hi) {
342
+ mid = Math.floor((lo + hi) / 2);
343
+ if (ring[mid][0] >= hash) {
344
+ hi = mid - 1;
345
+ } else {
346
+ lo = mid + 1;
347
+ }
348
+ }
349
+ return lo;
350
+ }
351
+
352
+ // src/node.ts
353
+ var import_node_net = require("net");
354
+ var import_hookified = require("hookified");
355
+ var MemcacheNode = class extends import_hookified.Hookified {
356
+ _host;
357
+ _port;
358
+ _socket = void 0;
359
+ _timeout;
360
+ _keepAlive;
361
+ _keepAliveDelay;
362
+ _weight;
363
+ _connected = false;
364
+ _commandQueue = [];
365
+ _buffer = "";
366
+ _currentCommand = void 0;
367
+ _multilineData = [];
368
+ _pendingValueBytes = 0;
369
+ constructor(host, port, options) {
370
+ super();
371
+ this._host = host;
372
+ this._port = port;
373
+ this._timeout = options?.timeout || 5e3;
374
+ this._keepAlive = options?.keepAlive !== false;
375
+ this._keepAliveDelay = options?.keepAliveDelay || 1e3;
376
+ this._weight = options?.weight || 1;
377
+ }
378
+ /**
379
+ * Get the host of this node
380
+ */
381
+ get host() {
382
+ return this._host;
383
+ }
384
+ /**
385
+ * Get the port of this node
386
+ */
387
+ get port() {
388
+ return this._port;
389
+ }
390
+ /**
391
+ * Get the unique identifier for this node (host:port format)
392
+ */
393
+ get id() {
394
+ return this._port === 0 ? this._host : `${this._host}:${this._port}`;
395
+ }
396
+ /**
397
+ * Get the full uri like memcache://localhost:11211
398
+ */
399
+ get uri() {
400
+ return `memcache://${this.id}`;
401
+ }
402
+ /**
403
+ * Get the socket connection
404
+ */
405
+ get socket() {
406
+ return this._socket;
407
+ }
408
+ /**
409
+ * Get the weight of this node (used for consistent hashing distribution)
410
+ */
411
+ get weight() {
412
+ return this._weight;
413
+ }
414
+ /**
415
+ * Set the weight of this node (used for consistent hashing distribution)
416
+ */
417
+ set weight(value) {
418
+ this._weight = value;
419
+ }
420
+ /**
421
+ * Get the keepAlive setting for this node
422
+ */
423
+ get keepAlive() {
424
+ return this._keepAlive;
425
+ }
426
+ /**
427
+ * Set the keepAlive setting for this node
428
+ */
429
+ set keepAlive(value) {
430
+ this._keepAlive = value;
431
+ }
432
+ /**
433
+ * Get the keepAliveDelay setting for this node
434
+ */
435
+ get keepAliveDelay() {
436
+ return this._keepAliveDelay;
437
+ }
438
+ /**
439
+ * Set the keepAliveDelay setting for this node
440
+ */
441
+ set keepAliveDelay(value) {
442
+ this._keepAliveDelay = value;
443
+ }
444
+ /**
445
+ * Get the command queue
446
+ */
447
+ get commandQueue() {
448
+ return this._commandQueue;
449
+ }
450
+ /**
451
+ * Connect to the memcache server
452
+ */
453
+ async connect() {
454
+ return new Promise((resolve, reject) => {
455
+ if (this._connected) {
456
+ resolve();
457
+ return;
458
+ }
459
+ this._socket = (0, import_node_net.createConnection)({
460
+ host: this._host,
461
+ port: this._port,
462
+ keepAlive: this._keepAlive,
463
+ keepAliveInitialDelay: this._keepAliveDelay
464
+ });
465
+ this._socket.setTimeout(this._timeout);
466
+ this._socket.setEncoding("utf8");
467
+ this._socket.on("connect", () => {
468
+ this._connected = true;
469
+ this.emit("connect");
470
+ resolve();
471
+ });
472
+ this._socket.on("data", (data) => {
473
+ this.handleData(data);
474
+ });
475
+ this._socket.on("error", (error) => {
476
+ this.emit("error", error);
477
+ if (!this._connected) {
478
+ reject(error);
479
+ }
480
+ });
481
+ this._socket.on("close", () => {
482
+ this._connected = false;
483
+ this.emit("close");
484
+ this.rejectPendingCommands(new Error("Connection closed"));
485
+ });
486
+ this._socket.on("timeout", () => {
487
+ this.emit("timeout");
488
+ this._socket?.destroy();
489
+ reject(new Error("Connection timeout"));
490
+ });
491
+ });
492
+ }
493
+ /**
494
+ * Disconnect from the memcache server
495
+ */
496
+ async disconnect() {
497
+ if (this._socket) {
498
+ this._socket.destroy();
499
+ this._socket = void 0;
500
+ this._connected = false;
501
+ }
502
+ }
503
+ /**
504
+ * Reconnect to the memcache server by disconnecting and connecting again
505
+ */
506
+ async reconnect() {
507
+ if (this._connected || this._socket) {
508
+ await this.disconnect();
509
+ this.rejectPendingCommands(
510
+ new Error("Connection reset for reconnection")
511
+ );
512
+ this._buffer = "";
513
+ this._currentCommand = void 0;
514
+ this._multilineData = [];
515
+ this._pendingValueBytes = 0;
516
+ }
517
+ await this.connect();
518
+ }
519
+ /**
520
+ * Gracefully quit the connection (send quit command then disconnect)
521
+ */
522
+ async quit() {
523
+ if (this._connected && this._socket) {
524
+ try {
525
+ await this.command("quit");
526
+ } catch (error) {
527
+ }
528
+ await this.disconnect();
529
+ }
530
+ }
531
+ /**
532
+ * Check if connected to the memcache server
533
+ */
534
+ isConnected() {
535
+ return this._connected;
536
+ }
537
+ /**
538
+ * Send a generic command to the memcache server
539
+ * @param cmd The command string to send (without trailing \r\n)
540
+ * @param options Command options for response parsing
541
+ */
542
+ async command(cmd, options) {
543
+ if (!this._connected || !this._socket) {
544
+ throw new Error(`Not connected to memcache server ${this.id}`);
545
+ }
546
+ return new Promise((resolve, reject) => {
547
+ this._commandQueue.push({
548
+ command: cmd,
549
+ resolve,
550
+ reject,
551
+ isMultiline: options?.isMultiline,
552
+ isStats: options?.isStats,
553
+ requestedKeys: options?.requestedKeys
554
+ });
555
+ this._socket.write(`${cmd}\r
556
+ `);
557
+ });
558
+ }
559
+ handleData(data) {
560
+ this._buffer += data;
561
+ while (true) {
562
+ if (this._pendingValueBytes > 0) {
563
+ if (this._buffer.length >= this._pendingValueBytes + 2) {
564
+ const value = this._buffer.substring(0, this._pendingValueBytes);
565
+ this._buffer = this._buffer.substring(this._pendingValueBytes + 2);
566
+ this._multilineData.push(value);
567
+ this._pendingValueBytes = 0;
568
+ } else {
569
+ break;
570
+ }
571
+ }
572
+ const lineEnd = this._buffer.indexOf("\r\n");
573
+ if (lineEnd === -1) break;
574
+ const line = this._buffer.substring(0, lineEnd);
575
+ this._buffer = this._buffer.substring(lineEnd + 2);
576
+ this.processLine(line);
577
+ }
578
+ }
579
+ processLine(line) {
580
+ if (!this._currentCommand) {
581
+ this._currentCommand = this._commandQueue.shift();
582
+ if (!this._currentCommand) return;
583
+ }
584
+ if (this._currentCommand.isStats) {
585
+ if (line === "END") {
586
+ const stats = {};
587
+ for (const statLine of this._multilineData) {
588
+ const [, key, value] = statLine.split(" ");
589
+ if (key && value) {
590
+ stats[key] = value;
591
+ }
592
+ }
593
+ this._currentCommand.resolve(stats);
594
+ this._multilineData = [];
595
+ this._currentCommand = void 0;
596
+ return;
597
+ }
598
+ if (line.startsWith("STAT ")) {
599
+ this._multilineData.push(line);
600
+ return;
601
+ }
602
+ if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
603
+ this._currentCommand.reject(new Error(line));
604
+ this._currentCommand = void 0;
605
+ return;
606
+ }
607
+ return;
608
+ }
609
+ if (this._currentCommand.isMultiline) {
610
+ if (this._currentCommand.requestedKeys && !this._currentCommand.foundKeys) {
611
+ this._currentCommand.foundKeys = [];
612
+ }
613
+ if (line.startsWith("VALUE ")) {
614
+ const parts = line.split(" ");
615
+ const key = parts[1];
616
+ const bytes = parseInt(parts[3], 10);
617
+ if (this._currentCommand.requestedKeys) {
618
+ this._currentCommand.foundKeys?.push(key);
619
+ }
620
+ this._pendingValueBytes = bytes;
621
+ } else if (line === "END") {
622
+ let result;
623
+ if (this._currentCommand.requestedKeys && this._currentCommand.foundKeys) {
624
+ result = {
625
+ values: this._multilineData.length > 0 ? this._multilineData : void 0,
626
+ foundKeys: this._currentCommand.foundKeys
627
+ };
628
+ } else {
629
+ result = this._multilineData.length > 0 ? this._multilineData : void 0;
630
+ }
631
+ if (this._currentCommand.requestedKeys && this._currentCommand.foundKeys) {
632
+ const foundKeys = this._currentCommand.foundKeys;
633
+ for (let i = 0; i < foundKeys.length; i++) {
634
+ this.emit("hit", foundKeys[i], this._multilineData[i]);
635
+ }
636
+ const missedKeys = this._currentCommand.requestedKeys.filter(
637
+ (key) => !foundKeys.includes(key)
638
+ );
639
+ for (const key of missedKeys) {
640
+ this.emit("miss", key);
641
+ }
642
+ }
643
+ this._currentCommand.resolve(result);
644
+ this._multilineData = [];
645
+ this._currentCommand = void 0;
646
+ } else if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
647
+ this._currentCommand.reject(new Error(line));
648
+ this._multilineData = [];
649
+ this._currentCommand = void 0;
650
+ }
651
+ } else {
652
+ if (line === "STORED" || line === "DELETED" || line === "OK" || line === "TOUCHED" || line === "EXISTS" || line === "NOT_FOUND") {
653
+ this._currentCommand.resolve(line);
654
+ } else if (line === "NOT_STORED") {
655
+ this._currentCommand.resolve(false);
656
+ } else if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
657
+ this._currentCommand.reject(new Error(line));
658
+ } else if (/^\d+$/.test(line)) {
659
+ this._currentCommand.resolve(parseInt(line, 10));
660
+ } else {
661
+ this._currentCommand.resolve(line);
662
+ }
663
+ this._currentCommand = void 0;
664
+ }
665
+ }
666
+ rejectPendingCommands(error) {
667
+ if (this._currentCommand) {
668
+ this._currentCommand.reject(error);
669
+ this._currentCommand = void 0;
670
+ }
671
+ while (this._commandQueue.length > 0) {
672
+ const cmd = this._commandQueue.shift();
673
+ if (cmd) {
674
+ cmd.reject(error);
675
+ }
676
+ }
677
+ }
678
+ };
679
+ function createNode(host, port, options) {
680
+ return new MemcacheNode(host, port, options);
681
+ }
682
+
683
+ // src/index.ts
684
+ var MemcacheEvents = /* @__PURE__ */ ((MemcacheEvents2) => {
685
+ MemcacheEvents2["CONNECT"] = "connect";
686
+ MemcacheEvents2["QUIT"] = "quit";
687
+ MemcacheEvents2["HIT"] = "hit";
688
+ MemcacheEvents2["MISS"] = "miss";
689
+ MemcacheEvents2["ERROR"] = "error";
690
+ MemcacheEvents2["WARN"] = "warn";
691
+ MemcacheEvents2["INFO"] = "info";
692
+ MemcacheEvents2["TIMEOUT"] = "timeout";
693
+ MemcacheEvents2["CLOSE"] = "close";
694
+ return MemcacheEvents2;
695
+ })(MemcacheEvents || {});
696
+ var Memcache = class extends import_hookified2.Hookified {
697
+ _nodes = [];
698
+ _timeout;
699
+ _keepAlive;
700
+ _keepAliveDelay;
701
+ _hash;
702
+ constructor(options) {
703
+ super();
704
+ this._hash = new KetamaHash();
705
+ this._timeout = options?.timeout || 5e3;
706
+ this._keepAlive = options?.keepAlive !== false;
707
+ this._keepAliveDelay = options?.keepAliveDelay || 1e3;
708
+ const nodeUris = options?.nodes || ["localhost:11211"];
709
+ for (const nodeUri of nodeUris) {
710
+ this.addNode(nodeUri);
711
+ }
712
+ }
713
+ /**
714
+ * Get the list of nodes
715
+ * @returns {MemcacheNode[]} Array of MemcacheNode
716
+ */
717
+ get nodes() {
718
+ return this._nodes;
719
+ }
720
+ /**
721
+ * Get the list of node IDs (e.g., ["localhost:11211", "127.0.0.1:11212"])
722
+ * @returns {string[]} Array of node ID strings
723
+ */
724
+ get nodeIds() {
725
+ return this._nodes.map((node) => node.id);
726
+ }
727
+ /**
728
+ * Get the hash provider used for consistent hashing distribution.
729
+ * @returns {HashProvider} The current hash provider instance
730
+ * @default KetamaHash
731
+ *
732
+ * @example
733
+ * ```typescript
734
+ * const client = new Memcache();
735
+ * const hashProvider = client.hash;
736
+ * console.log(hashProvider.name); // "ketama"
737
+ * ```
738
+ */
739
+ get hash() {
740
+ return this._hash;
741
+ }
742
+ /**
743
+ * Set the hash provider used for consistent hashing distribution.
744
+ * This allows you to customize the hashing strategy for distributing keys across nodes.
745
+ * @param {HashProvider} hash - The hash provider instance to use
746
+ *
747
+ * @example
748
+ * ```typescript
749
+ * const client = new Memcache();
750
+ * const customHashProvider = new KetamaHash();
751
+ * client.hash = customHashProvider;
752
+ * ```
753
+ */
754
+ set hash(hash) {
755
+ this._hash = hash;
756
+ }
757
+ /**
758
+ * Get the timeout for Memcache operations.
759
+ * @returns {number}
760
+ * @default 5000
761
+ */
762
+ get timeout() {
763
+ return this._timeout;
764
+ }
765
+ /**
766
+ * Set the timeout for Memcache operations.
767
+ * @param {number} value
768
+ * @default 5000
769
+ */
770
+ set timeout(value) {
771
+ this._timeout = value;
772
+ }
773
+ /**
774
+ * Get the keepAlive setting for the Memcache connection.
775
+ * @returns {boolean}
776
+ * @default true
777
+ */
778
+ get keepAlive() {
779
+ return this._keepAlive;
780
+ }
781
+ /**
782
+ * Set the keepAlive setting for the Memcache connection.
783
+ * Updates all existing nodes with the new value.
784
+ * Note: To apply the new value, you need to call reconnect() on the nodes.
785
+ * @param {boolean} value
786
+ * @default true
787
+ */
788
+ set keepAlive(value) {
789
+ this._keepAlive = value;
790
+ this.updateNodes();
791
+ }
792
+ /**
793
+ * Get the delay before the connection is kept alive.
794
+ * @returns {number}
795
+ * @default 1000
796
+ */
797
+ get keepAliveDelay() {
798
+ return this._keepAliveDelay;
799
+ }
800
+ /**
801
+ * Set the delay before the connection is kept alive.
802
+ * Updates all existing nodes with the new value.
803
+ * Note: To apply the new value, you need to call reconnect() on the nodes.
804
+ * @param {number} value
805
+ * @default 1000
806
+ */
807
+ set keepAliveDelay(value) {
808
+ this._keepAliveDelay = value;
809
+ this.updateNodes();
810
+ }
811
+ /**
812
+ * Get an array of all MemcacheNode instances
813
+ * @returns {MemcacheNode[]}
814
+ */
815
+ getNodes() {
816
+ return [...this._nodes];
817
+ }
818
+ /**
819
+ * Get a specific node by its ID
820
+ * @param {string} id - The node ID (e.g., "localhost:11211")
821
+ * @returns {MemcacheNode | undefined}
822
+ */
823
+ getNode(id) {
824
+ return this._nodes.find((n) => n.id === id);
825
+ }
826
+ /**
827
+ * Add a new node to the cluster
828
+ * @param {string | MemcacheNode} uri - Node URI (e.g., "localhost:11212") or a MemcacheNode instance
829
+ * @param {number} weight - Optional weight for consistent hashing (only used for string URIs)
830
+ */
831
+ async addNode(uri, weight) {
832
+ let node;
833
+ let nodeKey;
834
+ if (typeof uri === "string") {
835
+ const { host, port } = this.parseUri(uri);
836
+ nodeKey = port === 0 ? host : `${host}:${port}`;
837
+ if (this._nodes.some((n) => n.id === nodeKey)) {
838
+ throw new Error(`Node ${nodeKey} already exists`);
839
+ }
840
+ node = new MemcacheNode(host, port, {
841
+ timeout: this._timeout,
842
+ keepAlive: this._keepAlive,
843
+ keepAliveDelay: this._keepAliveDelay,
844
+ weight
845
+ });
846
+ } else {
847
+ node = uri;
848
+ nodeKey = node.id;
849
+ if (this._nodes.some((n) => n.id === nodeKey)) {
850
+ throw new Error(`Node ${nodeKey} already exists`);
851
+ }
852
+ }
853
+ this.forwardNodeEvents(node);
854
+ this._nodes.push(node);
855
+ this._hash.addNode(node);
856
+ }
857
+ /**
858
+ * Remove a node from the cluster
859
+ * @param {string} uri - Node URI (e.g., "localhost:11212")
860
+ */
861
+ async removeNode(uri) {
862
+ const { host, port } = this.parseUri(uri);
863
+ const nodeKey = port === 0 ? host : `${host}:${port}`;
864
+ const node = this._nodes.find((n) => n.id === nodeKey);
865
+ if (!node) return;
866
+ await node.disconnect();
867
+ this._nodes = this._nodes.filter((n) => n.id !== nodeKey);
868
+ this._hash.removeNode(node.id);
869
+ }
870
+ /**
871
+ * Parse a URI string into host and port.
872
+ * Supports multiple formats:
873
+ * - Simple: "localhost:11211" or "localhost"
874
+ * - Protocol: "memcache://localhost:11211", "memcached://localhost:11211", "tcp://localhost:11211"
875
+ * - IPv6: "[::1]:11211" or "memcache://[2001:db8::1]:11212"
876
+ * - Unix socket: "/var/run/memcached.sock" or "unix:///var/run/memcached.sock"
877
+ *
878
+ * @param {string} uri - URI string
879
+ * @returns {{ host: string; port: number }} Object containing host and port (port is 0 for Unix sockets)
880
+ * @throws {Error} If URI format is invalid
881
+ */
882
+ parseUri(uri) {
883
+ if (uri.startsWith("unix://")) {
884
+ return { host: uri.slice(7), port: 0 };
885
+ }
886
+ if (uri.startsWith("/")) {
887
+ return { host: uri, port: 0 };
888
+ }
889
+ let cleanUri = uri;
890
+ if (uri.includes("://")) {
891
+ const protocolParts = uri.split("://");
892
+ const protocol = protocolParts[0];
893
+ if (!["memcache", "memcached", "tcp"].includes(protocol)) {
894
+ throw new Error(
895
+ `Invalid protocol '${protocol}'. Supported protocols: memcache://, memcached://, tcp://, unix://`
896
+ );
897
+ }
898
+ cleanUri = protocolParts[1];
899
+ }
900
+ if (cleanUri.startsWith("[")) {
901
+ const bracketEnd = cleanUri.indexOf("]");
902
+ if (bracketEnd === -1) {
903
+ throw new Error("Invalid IPv6 format: missing closing bracket");
904
+ }
905
+ const host2 = cleanUri.slice(1, bracketEnd);
906
+ if (!host2) {
907
+ throw new Error("Invalid URI format: host is required");
908
+ }
909
+ const remainder = cleanUri.slice(bracketEnd + 1);
910
+ if (remainder === "") {
911
+ return { host: host2, port: 11211 };
912
+ }
913
+ if (!remainder.startsWith(":")) {
914
+ throw new Error("Invalid IPv6 format: expected ':' after bracket");
915
+ }
916
+ const portStr = remainder.slice(1);
917
+ const port2 = Number.parseInt(portStr, 10);
918
+ if (Number.isNaN(port2) || port2 <= 0 || port2 > 65535) {
919
+ throw new Error("Invalid port number");
920
+ }
921
+ return { host: host2, port: port2 };
922
+ }
923
+ const parts = cleanUri.split(":");
924
+ if (parts.length === 0 || parts.length > 2) {
925
+ throw new Error("Invalid URI format");
926
+ }
927
+ const host = parts[0];
928
+ if (!host) {
929
+ throw new Error("Invalid URI format: host is required");
930
+ }
931
+ const port = parts.length === 2 ? Number.parseInt(parts[1], 10) : 11211;
932
+ if (Number.isNaN(port) || port < 0 || port > 65535) {
933
+ throw new Error("Invalid port number");
934
+ }
935
+ if (port === 0) {
936
+ throw new Error("Invalid port number");
937
+ }
938
+ return { host, port };
939
+ }
940
+ /**
941
+ * Connect to all Memcache servers or a specific node.
942
+ * @param {string} nodeId - Optional node ID to connect to (e.g., "localhost:11211")
943
+ * @returns {Promise<void>}
944
+ */
945
+ async connect(nodeId) {
946
+ if (nodeId) {
947
+ const node = this._nodes.find((n) => n.id === nodeId);
948
+ if (!node) throw new Error(`Node ${nodeId} not found`);
949
+ await node.connect();
950
+ return;
951
+ }
952
+ await Promise.all(this._nodes.map((node) => node.connect()));
953
+ }
954
+ /**
955
+ * Get a value from the Memcache server.
956
+ * When multiple nodes are returned by the hash provider (for replication),
957
+ * queries all nodes and returns the first successful result.
958
+ * @param {string} key
959
+ * @returns {Promise<string | undefined>}
960
+ */
961
+ async get(key) {
962
+ await this.beforeHook("get", { key });
963
+ this.validateKey(key);
964
+ const nodes = await this.getNodesByKey(key);
965
+ const promises = nodes.map(async (node) => {
966
+ try {
967
+ const result = await node.command(`get ${key}`, {
968
+ isMultiline: true,
969
+ requestedKeys: [key]
970
+ });
971
+ if (result?.values && result.values.length > 0) {
972
+ return result.values[0];
973
+ }
974
+ return void 0;
975
+ } catch {
976
+ return void 0;
977
+ }
978
+ });
979
+ const results = await Promise.all(promises);
980
+ const value = results.find((v) => v !== void 0);
981
+ await this.afterHook("get", { key, value });
982
+ return value;
983
+ }
984
+ /**
985
+ * Get multiple values from the Memcache server.
986
+ * When multiple nodes are returned by the hash provider (for replication),
987
+ * queries all replica nodes and returns the first successful result for each key.
988
+ * @param keys {string[]}
989
+ * @returns {Promise<Map<string, string>>}
990
+ */
991
+ async gets(keys) {
992
+ await this.beforeHook("gets", { keys });
993
+ for (const key of keys) {
994
+ this.validateKey(key);
995
+ }
996
+ const keysByNode = /* @__PURE__ */ new Map();
997
+ for (const key of keys) {
998
+ const nodes = this._hash.getNodesByKey(key);
999
+ if (nodes.length === 0) {
1000
+ throw new Error(`No node available for key: ${key}`);
1001
+ }
1002
+ for (const node of nodes) {
1003
+ if (!keysByNode.has(node)) {
1004
+ keysByNode.set(node, []);
1005
+ }
1006
+ keysByNode.get(node).push(key);
1007
+ }
1008
+ }
1009
+ const promises = Array.from(keysByNode.entries()).map(
1010
+ async ([node, nodeKeys]) => {
1011
+ try {
1012
+ if (!node.isConnected()) await node.connect();
1013
+ const keysStr = nodeKeys.join(" ");
1014
+ const result = await node.command(`get ${keysStr}`, {
1015
+ isMultiline: true,
1016
+ requestedKeys: nodeKeys
1017
+ });
1018
+ return result;
1019
+ } catch {
1020
+ return void 0;
1021
+ }
1022
+ }
1023
+ );
1024
+ const results = await Promise.all(promises);
1025
+ const map = /* @__PURE__ */ new Map();
1026
+ for (const result of results) {
1027
+ if (result?.foundKeys && result.values) {
1028
+ for (let i = 0; i < result.foundKeys.length; i++) {
1029
+ if (result.values[i] !== void 0) {
1030
+ if (!map.has(result.foundKeys[i])) {
1031
+ map.set(result.foundKeys[i], result.values[i]);
1032
+ }
1033
+ }
1034
+ }
1035
+ }
1036
+ }
1037
+ await this.afterHook("gets", { keys, values: map });
1038
+ return map;
1039
+ }
1040
+ /**
1041
+ * Check-And-Set: Store a value only if it hasn't been modified since last fetch.
1042
+ * When multiple nodes are returned by the hash provider (for replication),
1043
+ * executes on all nodes and returns true only if all succeed.
1044
+ * @param key {string}
1045
+ * @param value {string}
1046
+ * @param casToken {string}
1047
+ * @param exptime {number}
1048
+ * @param flags {number}
1049
+ * @returns {Promise<boolean>}
1050
+ */
1051
+ async cas(key, value, casToken, exptime = 0, flags = 0) {
1052
+ await this.beforeHook("cas", { key, value, casToken, exptime, flags });
1053
+ this.validateKey(key);
1054
+ const valueStr = String(value);
1055
+ const bytes = Buffer.byteLength(valueStr);
1056
+ const command = `cas ${key} ${flags} ${exptime} ${bytes} ${casToken}\r
1057
+ ${valueStr}`;
1058
+ const nodes = await this.getNodesByKey(key);
1059
+ const promises = nodes.map(async (node) => {
1060
+ try {
1061
+ const result = await node.command(command);
1062
+ return result === "STORED";
1063
+ } catch {
1064
+ return false;
1065
+ }
1066
+ });
1067
+ const results = await Promise.all(promises);
1068
+ const success = results.every((result) => result === true);
1069
+ await this.afterHook("cas", {
1070
+ key,
1071
+ value,
1072
+ casToken,
1073
+ exptime,
1074
+ flags,
1075
+ success
1076
+ });
1077
+ return success;
1078
+ }
1079
+ /**
1080
+ * Set a value in the Memcache server.
1081
+ * When multiple nodes are returned by the hash provider (for replication),
1082
+ * executes on all nodes and returns true only if all succeed.
1083
+ * @param key {string}
1084
+ * @param value {string}
1085
+ * @param exptime {number}
1086
+ * @param flags {number}
1087
+ * @returns {Promise<boolean>}
1088
+ */
1089
+ async set(key, value, exptime = 0, flags = 0) {
1090
+ await this.beforeHook("set", { key, value, exptime, flags });
1091
+ this.validateKey(key);
1092
+ const valueStr = String(value);
1093
+ const bytes = Buffer.byteLength(valueStr);
1094
+ const command = `set ${key} ${flags} ${exptime} ${bytes}\r
1095
+ ${valueStr}`;
1096
+ const nodes = await this.getNodesByKey(key);
1097
+ const promises = nodes.map(async (node) => {
1098
+ try {
1099
+ const result = await node.command(command);
1100
+ return result === "STORED";
1101
+ } catch {
1102
+ return false;
1103
+ }
1104
+ });
1105
+ const results = await Promise.all(promises);
1106
+ const success = results.every((result) => result === true);
1107
+ await this.afterHook("set", { key, value, exptime, flags, success });
1108
+ return success;
1109
+ }
1110
+ /**
1111
+ * Add a value to the Memcache server (only if key doesn't exist).
1112
+ * When multiple nodes are returned by the hash provider (for replication),
1113
+ * executes on all nodes and returns true only if all succeed.
1114
+ * @param key {string}
1115
+ * @param value {string}
1116
+ * @param exptime {number}
1117
+ * @param flags {number}
1118
+ * @returns {Promise<boolean>}
1119
+ */
1120
+ async add(key, value, exptime = 0, flags = 0) {
1121
+ await this.beforeHook("add", { key, value, exptime, flags });
1122
+ this.validateKey(key);
1123
+ const valueStr = String(value);
1124
+ const bytes = Buffer.byteLength(valueStr);
1125
+ const command = `add ${key} ${flags} ${exptime} ${bytes}\r
1126
+ ${valueStr}`;
1127
+ const nodes = await this.getNodesByKey(key);
1128
+ const promises = nodes.map(async (node) => {
1129
+ try {
1130
+ const result = await node.command(command);
1131
+ return result === "STORED";
1132
+ } catch {
1133
+ return false;
1134
+ }
1135
+ });
1136
+ const results = await Promise.all(promises);
1137
+ const success = results.every((result) => result === true);
1138
+ await this.afterHook("add", { key, value, exptime, flags, success });
1139
+ return success;
1140
+ }
1141
+ /**
1142
+ * Replace a value in the Memcache server (only if key exists).
1143
+ * When multiple nodes are returned by the hash provider (for replication),
1144
+ * executes on all nodes and returns true only if all succeed.
1145
+ * @param key {string}
1146
+ * @param value {string}
1147
+ * @param exptime {number}
1148
+ * @param flags {number}
1149
+ * @returns {Promise<boolean>}
1150
+ */
1151
+ async replace(key, value, exptime = 0, flags = 0) {
1152
+ await this.beforeHook("replace", { key, value, exptime, flags });
1153
+ this.validateKey(key);
1154
+ const valueStr = String(value);
1155
+ const bytes = Buffer.byteLength(valueStr);
1156
+ const command = `replace ${key} ${flags} ${exptime} ${bytes}\r
1157
+ ${valueStr}`;
1158
+ const nodes = await this.getNodesByKey(key);
1159
+ const promises = nodes.map(async (node) => {
1160
+ try {
1161
+ const result = await node.command(command);
1162
+ return result === "STORED";
1163
+ } catch {
1164
+ return false;
1165
+ }
1166
+ });
1167
+ const results = await Promise.all(promises);
1168
+ const success = results.every((result) => result === true);
1169
+ await this.afterHook("replace", { key, value, exptime, flags, success });
1170
+ return success;
1171
+ }
1172
+ /**
1173
+ * Append a value to an existing key in the Memcache server.
1174
+ * When multiple nodes are returned by the hash provider (for replication),
1175
+ * executes on all nodes and returns true only if all succeed.
1176
+ * @param key {string}
1177
+ * @param value {string}
1178
+ * @returns {Promise<boolean>}
1179
+ */
1180
+ async append(key, value) {
1181
+ await this.beforeHook("append", { key, value });
1182
+ this.validateKey(key);
1183
+ const valueStr = String(value);
1184
+ const bytes = Buffer.byteLength(valueStr);
1185
+ const command = `append ${key} 0 0 ${bytes}\r
1186
+ ${valueStr}`;
1187
+ const nodes = await this.getNodesByKey(key);
1188
+ const promises = nodes.map(async (node) => {
1189
+ try {
1190
+ const result = await node.command(command);
1191
+ return result === "STORED";
1192
+ } catch {
1193
+ return false;
1194
+ }
1195
+ });
1196
+ const results = await Promise.all(promises);
1197
+ const success = results.every((result) => result === true);
1198
+ await this.afterHook("append", { key, value, success });
1199
+ return success;
1200
+ }
1201
+ /**
1202
+ * Prepend a value to an existing key in the Memcache server.
1203
+ * When multiple nodes are returned by the hash provider (for replication),
1204
+ * executes on all nodes and returns true only if all succeed.
1205
+ * @param key {string}
1206
+ * @param value {string}
1207
+ * @returns {Promise<boolean>}
1208
+ */
1209
+ async prepend(key, value) {
1210
+ await this.beforeHook("prepend", { key, value });
1211
+ this.validateKey(key);
1212
+ const valueStr = String(value);
1213
+ const bytes = Buffer.byteLength(valueStr);
1214
+ const command = `prepend ${key} 0 0 ${bytes}\r
1215
+ ${valueStr}`;
1216
+ const nodes = await this.getNodesByKey(key);
1217
+ const promises = nodes.map(async (node) => {
1218
+ try {
1219
+ const result = await node.command(command);
1220
+ return result === "STORED";
1221
+ } catch {
1222
+ return false;
1223
+ }
1224
+ });
1225
+ const results = await Promise.all(promises);
1226
+ const success = results.every((result) => result === true);
1227
+ await this.afterHook("prepend", { key, value, success });
1228
+ return success;
1229
+ }
1230
+ /**
1231
+ * Delete a value from the Memcache server.
1232
+ * When multiple nodes are returned by the hash provider (for replication),
1233
+ * executes on all nodes and returns true only if all succeed.
1234
+ * @param key {string}
1235
+ * @returns {Promise<boolean>}
1236
+ */
1237
+ async delete(key) {
1238
+ await this.beforeHook("delete", { key });
1239
+ this.validateKey(key);
1240
+ const nodes = await this.getNodesByKey(key);
1241
+ const promises = nodes.map(async (node) => {
1242
+ try {
1243
+ const result = await node.command(`delete ${key}`);
1244
+ return result === "DELETED";
1245
+ } catch {
1246
+ return false;
1247
+ }
1248
+ });
1249
+ const results = await Promise.all(promises);
1250
+ const success = results.every((result) => result === true);
1251
+ await this.afterHook("delete", { key, success });
1252
+ return success;
1253
+ }
1254
+ /**
1255
+ * Increment a value in the Memcache server.
1256
+ * When multiple nodes are returned by the hash provider (for replication),
1257
+ * executes on all nodes and returns the first successful result.
1258
+ * @param key {string}
1259
+ * @param value {number}
1260
+ * @returns {Promise<number | undefined>}
1261
+ */
1262
+ async incr(key, value = 1) {
1263
+ await this.beforeHook("incr", { key, value });
1264
+ this.validateKey(key);
1265
+ const nodes = await this.getNodesByKey(key);
1266
+ const promises = nodes.map(async (node) => {
1267
+ try {
1268
+ const result = await node.command(`incr ${key} ${value}`);
1269
+ return typeof result === "number" ? result : void 0;
1270
+ } catch {
1271
+ return void 0;
1272
+ }
1273
+ });
1274
+ const results = await Promise.all(promises);
1275
+ const newValue = results.find((v) => v !== void 0);
1276
+ await this.afterHook("incr", { key, value, newValue });
1277
+ return newValue;
1278
+ }
1279
+ /**
1280
+ * Decrement a value in the Memcache server.
1281
+ * When multiple nodes are returned by the hash provider (for replication),
1282
+ * executes on all nodes and returns the first successful result.
1283
+ * @param key {string}
1284
+ * @param value {number}
1285
+ * @returns {Promise<number | undefined>}
1286
+ */
1287
+ async decr(key, value = 1) {
1288
+ await this.beforeHook("decr", { key, value });
1289
+ this.validateKey(key);
1290
+ const nodes = await this.getNodesByKey(key);
1291
+ const promises = nodes.map(async (node) => {
1292
+ try {
1293
+ const result = await node.command(`decr ${key} ${value}`);
1294
+ return typeof result === "number" ? result : void 0;
1295
+ } catch {
1296
+ return void 0;
1297
+ }
1298
+ });
1299
+ const results = await Promise.all(promises);
1300
+ const newValue = results.find((v) => v !== void 0);
1301
+ await this.afterHook("decr", { key, value, newValue });
1302
+ return newValue;
1303
+ }
1304
+ /**
1305
+ * Touch a value in the Memcache server (update expiration time).
1306
+ * When multiple nodes are returned by the hash provider (for replication),
1307
+ * executes on all nodes and returns true only if all succeed.
1308
+ * @param key {string}
1309
+ * @param exptime {number}
1310
+ * @returns {Promise<boolean>}
1311
+ */
1312
+ async touch(key, exptime) {
1313
+ await this.beforeHook("touch", { key, exptime });
1314
+ this.validateKey(key);
1315
+ const nodes = await this.getNodesByKey(key);
1316
+ const promises = nodes.map(async (node) => {
1317
+ try {
1318
+ const result = await node.command(`touch ${key} ${exptime}`);
1319
+ return result === "TOUCHED";
1320
+ } catch {
1321
+ return false;
1322
+ }
1323
+ });
1324
+ const results = await Promise.all(promises);
1325
+ const success = results.every((result) => result === true);
1326
+ await this.afterHook("touch", { key, exptime, success });
1327
+ return success;
1328
+ }
1329
+ /**
1330
+ * Flush all values from all Memcache servers.
1331
+ * @param delay {number}
1332
+ * @returns {Promise<boolean>}
1333
+ */
1334
+ async flush(delay) {
1335
+ let command = "flush_all";
1336
+ if (delay !== void 0) {
1337
+ command += ` ${delay}`;
1338
+ }
1339
+ const results = await Promise.all(
1340
+ this._nodes.map(async (node) => {
1341
+ if (!node.isConnected()) {
1342
+ await node.connect();
1343
+ }
1344
+ return node.command(command);
1345
+ })
1346
+ );
1347
+ return results.every((r) => r === "OK");
1348
+ }
1349
+ /**
1350
+ * Get statistics from all Memcache servers.
1351
+ * @param type {string}
1352
+ * @returns {Promise<Map<string, MemcacheStats>>}
1353
+ */
1354
+ async stats(type) {
1355
+ const command = type ? `stats ${type}` : "stats";
1356
+ const results = /* @__PURE__ */ new Map();
1357
+ await Promise.all(
1358
+ /* v8 ignore next -- @preserve */
1359
+ this._nodes.map(async (node) => {
1360
+ if (!node.isConnected()) {
1361
+ await node.connect();
1362
+ }
1363
+ const stats = await node.command(command, { isStats: true });
1364
+ results.set(node.id, stats);
1365
+ })
1366
+ );
1367
+ return results;
1368
+ }
1369
+ /**
1370
+ * Get the Memcache server version from all nodes.
1371
+ * @returns {Promise<Map<string, string>>} Map of node IDs to version strings
1372
+ */
1373
+ async version() {
1374
+ const results = /* @__PURE__ */ new Map();
1375
+ await Promise.all(
1376
+ /* v8 ignore next -- @preserve */
1377
+ this._nodes.map(async (node) => {
1378
+ if (!node.isConnected()) {
1379
+ await node.connect();
1380
+ }
1381
+ const version = await node.command("version");
1382
+ results.set(node.id, version);
1383
+ })
1384
+ );
1385
+ return results;
1386
+ }
1387
+ /**
1388
+ * Quit all connections gracefully.
1389
+ * @returns {Promise<void>}
1390
+ */
1391
+ async quit() {
1392
+ await Promise.all(
1393
+ this._nodes.map(async (node) => {
1394
+ if (node.isConnected()) {
1395
+ await node.quit();
1396
+ }
1397
+ })
1398
+ );
1399
+ }
1400
+ /**
1401
+ * Disconnect all connections.
1402
+ * @returns {Promise<void>}
1403
+ */
1404
+ async disconnect() {
1405
+ await Promise.all(this._nodes.map((node) => node.disconnect()));
1406
+ }
1407
+ /**
1408
+ * Reconnect all nodes by disconnecting and connecting them again.
1409
+ * @returns {Promise<void>}
1410
+ */
1411
+ async reconnect() {
1412
+ await Promise.all(this._nodes.map((node) => node.reconnect()));
1413
+ }
1414
+ /**
1415
+ * Check if any node is connected to a Memcache server.
1416
+ * @returns {boolean}
1417
+ */
1418
+ isConnected() {
1419
+ return this._nodes.some((node) => node.isConnected());
1420
+ }
1421
+ /**
1422
+ * Get the nodes for a given key using consistent hashing, with lazy connection.
1423
+ * This method will automatically connect to the nodes if they're not already connected.
1424
+ * Returns an array to support replication strategies.
1425
+ * @param {string} key - The cache key
1426
+ * @returns {Promise<Array<MemcacheNode>>} The nodes responsible for this key
1427
+ * @throws {Error} If no nodes are available for the key
1428
+ */
1429
+ async getNodesByKey(key) {
1430
+ const nodes = this._hash.getNodesByKey(key);
1431
+ if (nodes.length === 0) {
1432
+ throw new Error(`No node available for key: ${key}`);
1433
+ }
1434
+ for (const node of nodes) {
1435
+ if (!node.isConnected()) {
1436
+ await node.connect();
1437
+ }
1438
+ }
1439
+ return nodes;
1440
+ }
1441
+ /**
1442
+ * Validates a Memcache key according to protocol requirements.
1443
+ * @param {string} key - The key to validate
1444
+ * @throws {Error} If the key is empty, exceeds 250 characters, or contains invalid characters
1445
+ *
1446
+ * @example
1447
+ * ```typescript
1448
+ * client.validateKey("valid-key"); // OK
1449
+ * client.validateKey(""); // Throws: Key cannot be empty
1450
+ * client.validateKey("a".repeat(251)); // Throws: Key length cannot exceed 250 characters
1451
+ * client.validateKey("key with spaces"); // Throws: Key cannot contain spaces, newlines, or null characters
1452
+ * ```
1453
+ */
1454
+ validateKey(key) {
1455
+ if (!key || key.length === 0) {
1456
+ throw new Error("Key cannot be empty");
1457
+ }
1458
+ if (key.length > 250) {
1459
+ throw new Error("Key length cannot exceed 250 characters");
1460
+ }
1461
+ if (/[\s\r\n\0]/.test(key)) {
1462
+ throw new Error(
1463
+ "Key cannot contain spaces, newlines, or null characters"
1464
+ );
1465
+ }
1466
+ }
1467
+ // Private methods
1468
+ /**
1469
+ * Update all nodes with current keepAlive settings
1470
+ */
1471
+ updateNodes() {
1472
+ for (const node of this._nodes) {
1473
+ node.keepAlive = this._keepAlive;
1474
+ node.keepAliveDelay = this._keepAliveDelay;
1475
+ }
1476
+ }
1477
+ /**
1478
+ * Forward events from a MemcacheNode to the Memcache instance
1479
+ */
1480
+ forwardNodeEvents(node) {
1481
+ node.on("connect", () => this.emit("connect" /* CONNECT */, node.id));
1482
+ node.on("close", () => this.emit("close" /* CLOSE */, node.id));
1483
+ node.on(
1484
+ "error",
1485
+ (err) => this.emit("error" /* ERROR */, node.id, err)
1486
+ );
1487
+ node.on("timeout", () => this.emit("timeout" /* TIMEOUT */, node.id));
1488
+ node.on(
1489
+ "hit",
1490
+ (key, value) => this.emit("hit" /* HIT */, key, value)
1491
+ );
1492
+ node.on("miss", (key) => this.emit("miss" /* MISS */, key));
1493
+ }
1494
+ };
1495
+ var index_default = Memcache;
1496
+ // Annotate the CommonJS export names for ESM import in node:
1497
+ 0 && (module.exports = {
1498
+ Memcache,
1499
+ MemcacheEvents,
1500
+ createNode
1501
+ });
1502
+ /* v8 ignore next -- @preserve */