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