memcache 1.1.0 → 1.3.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 CHANGED
@@ -322,9 +322,390 @@ function binarySearchRing(ring, hash) {
322
322
  return lo;
323
323
  }
324
324
 
325
+ // src/modula.ts
326
+ import { createHash as createHash2 } from "crypto";
327
+ var hashFunctionForBuiltin2 = (algorithm) => (value) => createHash2(algorithm).update(value).digest().readUInt32BE(0);
328
+ var ModulaHash = class {
329
+ /** The name of this distribution strategy */
330
+ name = "modula";
331
+ /** The hash function used to compute key hashes */
332
+ hashFn;
333
+ /** Map of node IDs to MemcacheNode instances */
334
+ nodeMap;
335
+ /**
336
+ * Weighted list of node IDs for modulo distribution.
337
+ * Nodes with higher weights appear multiple times.
338
+ */
339
+ nodeList;
340
+ /**
341
+ * Creates a new ModulaHash instance.
342
+ *
343
+ * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * // Use default SHA-1 hashing
348
+ * const distribution = new ModulaHash();
349
+ *
350
+ * // Use MD5 hashing
351
+ * const distribution = new ModulaHash('md5');
352
+ *
353
+ * // Use custom hash function
354
+ * const distribution = new ModulaHash((buf) => buf.readUInt32BE(0));
355
+ * ```
356
+ */
357
+ constructor(hashFn) {
358
+ this.hashFn = typeof hashFn === "string" ? hashFunctionForBuiltin2(hashFn) : hashFn ?? hashFunctionForBuiltin2("sha1");
359
+ this.nodeMap = /* @__PURE__ */ new Map();
360
+ this.nodeList = [];
361
+ }
362
+ /**
363
+ * Gets all nodes in the distribution.
364
+ * @returns Array of all MemcacheNode instances
365
+ */
366
+ get nodes() {
367
+ return Array.from(this.nodeMap.values());
368
+ }
369
+ /**
370
+ * Adds a node to the distribution with its weight.
371
+ * Weight determines how many times the node appears in the distribution list.
372
+ *
373
+ * @param node - The MemcacheNode to add
374
+ *
375
+ * @example
376
+ * ```typescript
377
+ * const node = new MemcacheNode('localhost', 11211, { weight: 2 });
378
+ * distribution.addNode(node);
379
+ * ```
380
+ */
381
+ addNode(node) {
382
+ this.nodeMap.set(node.id, node);
383
+ const weight = node.weight || 1;
384
+ for (let i = 0; i < weight; i++) {
385
+ this.nodeList.push(node.id);
386
+ }
387
+ }
388
+ /**
389
+ * Removes a node from the distribution by its ID.
390
+ *
391
+ * @param id - The node ID (e.g., "localhost:11211")
392
+ *
393
+ * @example
394
+ * ```typescript
395
+ * distribution.removeNode('localhost:11211');
396
+ * ```
397
+ */
398
+ removeNode(id) {
399
+ this.nodeMap.delete(id);
400
+ this.nodeList = this.nodeList.filter((nodeId) => nodeId !== id);
401
+ }
402
+ /**
403
+ * Gets a specific node by its ID.
404
+ *
405
+ * @param id - The node ID (e.g., "localhost:11211")
406
+ * @returns The MemcacheNode if found, undefined otherwise
407
+ *
408
+ * @example
409
+ * ```typescript
410
+ * const node = distribution.getNode('localhost:11211');
411
+ * if (node) {
412
+ * console.log(`Found node: ${node.uri}`);
413
+ * }
414
+ * ```
415
+ */
416
+ getNode(id) {
417
+ return this.nodeMap.get(id);
418
+ }
419
+ /**
420
+ * Gets the nodes responsible for a given key using modulo hashing.
421
+ * Uses `hash(key) % nodeCount` to determine the target node.
422
+ *
423
+ * @param key - The cache key to find the responsible node for
424
+ * @returns Array containing the responsible node(s), empty if no nodes available
425
+ *
426
+ * @example
427
+ * ```typescript
428
+ * const nodes = distribution.getNodesByKey('user:123');
429
+ * if (nodes.length > 0) {
430
+ * console.log(`Key will be stored on: ${nodes[0].id}`);
431
+ * }
432
+ * ```
433
+ */
434
+ getNodesByKey(key) {
435
+ if (this.nodeList.length === 0) {
436
+ return [];
437
+ }
438
+ const hash = this.hashFn(Buffer.from(key));
439
+ const index = hash % this.nodeList.length;
440
+ const nodeId = this.nodeList[index];
441
+ const node = this.nodeMap.get(nodeId);
442
+ return node ? [node] : [];
443
+ }
444
+ };
445
+
325
446
  // src/node.ts
326
447
  import { createConnection } from "net";
327
448
  import { Hookified } from "hookified";
449
+
450
+ // src/binary-protocol.ts
451
+ var REQUEST_MAGIC = 128;
452
+ var OPCODE_SASL_AUTH = 33;
453
+ var OPCODE_GET = 0;
454
+ var OPCODE_SET = 1;
455
+ var OPCODE_ADD = 2;
456
+ var OPCODE_REPLACE = 3;
457
+ var OPCODE_DELETE = 4;
458
+ var OPCODE_INCREMENT = 5;
459
+ var OPCODE_DECREMENT = 6;
460
+ var OPCODE_QUIT = 7;
461
+ var OPCODE_FLUSH = 8;
462
+ var OPCODE_VERSION = 11;
463
+ var OPCODE_APPEND = 14;
464
+ var OPCODE_PREPEND = 15;
465
+ var OPCODE_STAT = 16;
466
+ var OPCODE_TOUCH = 28;
467
+ var STATUS_SUCCESS = 0;
468
+ var STATUS_KEY_NOT_FOUND = 1;
469
+ var STATUS_AUTH_ERROR = 32;
470
+ var HEADER_SIZE = 24;
471
+ function serializeHeader(header) {
472
+ const buf = Buffer.alloc(HEADER_SIZE);
473
+ buf.writeUInt8(header.magic ?? REQUEST_MAGIC, 0);
474
+ buf.writeUInt8(header.opcode ?? 0, 1);
475
+ buf.writeUInt16BE(header.keyLength ?? 0, 2);
476
+ buf.writeUInt8(header.extrasLength ?? 0, 4);
477
+ buf.writeUInt8(header.dataType ?? 0, 5);
478
+ buf.writeUInt16BE(header.status ?? 0, 6);
479
+ buf.writeUInt32BE(header.totalBodyLength ?? 0, 8);
480
+ buf.writeUInt32BE(header.opaque ?? 0, 12);
481
+ if (header.cas) {
482
+ header.cas.copy(buf, 16);
483
+ }
484
+ return buf;
485
+ }
486
+ function deserializeHeader(buf) {
487
+ return {
488
+ magic: buf.readUInt8(0),
489
+ opcode: buf.readUInt8(1),
490
+ keyLength: buf.readUInt16BE(2),
491
+ extrasLength: buf.readUInt8(4),
492
+ dataType: buf.readUInt8(5),
493
+ status: buf.readUInt16BE(6),
494
+ totalBodyLength: buf.readUInt32BE(8),
495
+ opaque: buf.readUInt32BE(12),
496
+ cas: buf.subarray(16, 24)
497
+ };
498
+ }
499
+ function buildSaslPlainRequest(username, password) {
500
+ const mechanism = "PLAIN";
501
+ const authData = `\0${username}\0${password}`;
502
+ const keyBuf = Buffer.from(mechanism, "utf8");
503
+ const valueBuf = Buffer.from(authData, "utf8");
504
+ const header = serializeHeader({
505
+ magic: REQUEST_MAGIC,
506
+ opcode: OPCODE_SASL_AUTH,
507
+ keyLength: keyBuf.length,
508
+ totalBodyLength: keyBuf.length + valueBuf.length
509
+ });
510
+ return Buffer.concat([header, keyBuf, valueBuf]);
511
+ }
512
+ function buildGetRequest(key) {
513
+ const keyBuf = Buffer.from(key, "utf8");
514
+ const header = serializeHeader({
515
+ magic: REQUEST_MAGIC,
516
+ opcode: OPCODE_GET,
517
+ keyLength: keyBuf.length,
518
+ totalBodyLength: keyBuf.length
519
+ });
520
+ return Buffer.concat([header, keyBuf]);
521
+ }
522
+ function buildSetRequest(key, value, flags = 0, exptime = 0) {
523
+ const keyBuf = Buffer.from(key, "utf8");
524
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
525
+ const extras = Buffer.alloc(8);
526
+ extras.writeUInt32BE(flags, 0);
527
+ extras.writeUInt32BE(exptime, 4);
528
+ const header = serializeHeader({
529
+ magic: REQUEST_MAGIC,
530
+ opcode: OPCODE_SET,
531
+ keyLength: keyBuf.length,
532
+ extrasLength: 8,
533
+ totalBodyLength: 8 + keyBuf.length + valueBuf.length
534
+ });
535
+ return Buffer.concat([header, extras, keyBuf, valueBuf]);
536
+ }
537
+ function buildAddRequest(key, value, flags = 0, exptime = 0) {
538
+ const keyBuf = Buffer.from(key, "utf8");
539
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
540
+ const extras = Buffer.alloc(8);
541
+ extras.writeUInt32BE(flags, 0);
542
+ extras.writeUInt32BE(exptime, 4);
543
+ const header = serializeHeader({
544
+ magic: REQUEST_MAGIC,
545
+ opcode: OPCODE_ADD,
546
+ keyLength: keyBuf.length,
547
+ extrasLength: 8,
548
+ totalBodyLength: 8 + keyBuf.length + valueBuf.length
549
+ });
550
+ return Buffer.concat([header, extras, keyBuf, valueBuf]);
551
+ }
552
+ function buildReplaceRequest(key, value, flags = 0, exptime = 0) {
553
+ const keyBuf = Buffer.from(key, "utf8");
554
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
555
+ const extras = Buffer.alloc(8);
556
+ extras.writeUInt32BE(flags, 0);
557
+ extras.writeUInt32BE(exptime, 4);
558
+ const header = serializeHeader({
559
+ magic: REQUEST_MAGIC,
560
+ opcode: OPCODE_REPLACE,
561
+ keyLength: keyBuf.length,
562
+ extrasLength: 8,
563
+ totalBodyLength: 8 + keyBuf.length + valueBuf.length
564
+ });
565
+ return Buffer.concat([header, extras, keyBuf, valueBuf]);
566
+ }
567
+ function buildDeleteRequest(key) {
568
+ const keyBuf = Buffer.from(key, "utf8");
569
+ const header = serializeHeader({
570
+ magic: REQUEST_MAGIC,
571
+ opcode: OPCODE_DELETE,
572
+ keyLength: keyBuf.length,
573
+ totalBodyLength: keyBuf.length
574
+ });
575
+ return Buffer.concat([header, keyBuf]);
576
+ }
577
+ function buildIncrementRequest(key, delta = 1, initial = 0, exptime = 0) {
578
+ const keyBuf = Buffer.from(key, "utf8");
579
+ const extras = Buffer.alloc(20);
580
+ extras.writeUInt32BE(Math.floor(delta / 4294967296), 0);
581
+ extras.writeUInt32BE(delta >>> 0, 4);
582
+ extras.writeUInt32BE(Math.floor(initial / 4294967296), 8);
583
+ extras.writeUInt32BE(initial >>> 0, 12);
584
+ extras.writeUInt32BE(exptime, 16);
585
+ const header = serializeHeader({
586
+ magic: REQUEST_MAGIC,
587
+ opcode: OPCODE_INCREMENT,
588
+ keyLength: keyBuf.length,
589
+ extrasLength: 20,
590
+ totalBodyLength: 20 + keyBuf.length
591
+ });
592
+ return Buffer.concat([header, extras, keyBuf]);
593
+ }
594
+ function buildDecrementRequest(key, delta = 1, initial = 0, exptime = 0) {
595
+ const keyBuf = Buffer.from(key, "utf8");
596
+ const extras = Buffer.alloc(20);
597
+ extras.writeUInt32BE(Math.floor(delta / 4294967296), 0);
598
+ extras.writeUInt32BE(delta >>> 0, 4);
599
+ extras.writeUInt32BE(Math.floor(initial / 4294967296), 8);
600
+ extras.writeUInt32BE(initial >>> 0, 12);
601
+ extras.writeUInt32BE(exptime, 16);
602
+ const header = serializeHeader({
603
+ magic: REQUEST_MAGIC,
604
+ opcode: OPCODE_DECREMENT,
605
+ keyLength: keyBuf.length,
606
+ extrasLength: 20,
607
+ totalBodyLength: 20 + keyBuf.length
608
+ });
609
+ return Buffer.concat([header, extras, keyBuf]);
610
+ }
611
+ function buildAppendRequest(key, value) {
612
+ const keyBuf = Buffer.from(key, "utf8");
613
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
614
+ const header = serializeHeader({
615
+ magic: REQUEST_MAGIC,
616
+ opcode: OPCODE_APPEND,
617
+ keyLength: keyBuf.length,
618
+ totalBodyLength: keyBuf.length + valueBuf.length
619
+ });
620
+ return Buffer.concat([header, keyBuf, valueBuf]);
621
+ }
622
+ function buildPrependRequest(key, value) {
623
+ const keyBuf = Buffer.from(key, "utf8");
624
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
625
+ const header = serializeHeader({
626
+ magic: REQUEST_MAGIC,
627
+ opcode: OPCODE_PREPEND,
628
+ keyLength: keyBuf.length,
629
+ totalBodyLength: keyBuf.length + valueBuf.length
630
+ });
631
+ return Buffer.concat([header, keyBuf, valueBuf]);
632
+ }
633
+ function buildTouchRequest(key, exptime) {
634
+ const keyBuf = Buffer.from(key, "utf8");
635
+ const extras = Buffer.alloc(4);
636
+ extras.writeUInt32BE(exptime, 0);
637
+ const header = serializeHeader({
638
+ magic: REQUEST_MAGIC,
639
+ opcode: OPCODE_TOUCH,
640
+ keyLength: keyBuf.length,
641
+ extrasLength: 4,
642
+ totalBodyLength: 4 + keyBuf.length
643
+ });
644
+ return Buffer.concat([header, extras, keyBuf]);
645
+ }
646
+ function buildFlushRequest(exptime = 0) {
647
+ const extras = Buffer.alloc(4);
648
+ extras.writeUInt32BE(exptime, 0);
649
+ const header = serializeHeader({
650
+ magic: REQUEST_MAGIC,
651
+ opcode: OPCODE_FLUSH,
652
+ extrasLength: 4,
653
+ totalBodyLength: 4
654
+ });
655
+ return Buffer.concat([header, extras]);
656
+ }
657
+ function buildVersionRequest() {
658
+ return serializeHeader({
659
+ magic: REQUEST_MAGIC,
660
+ opcode: OPCODE_VERSION
661
+ });
662
+ }
663
+ function buildStatRequest(key) {
664
+ if (key) {
665
+ const keyBuf = Buffer.from(key, "utf8");
666
+ const header = serializeHeader({
667
+ magic: REQUEST_MAGIC,
668
+ opcode: OPCODE_STAT,
669
+ keyLength: keyBuf.length,
670
+ totalBodyLength: keyBuf.length
671
+ });
672
+ return Buffer.concat([header, keyBuf]);
673
+ }
674
+ return serializeHeader({
675
+ magic: REQUEST_MAGIC,
676
+ opcode: OPCODE_STAT
677
+ });
678
+ }
679
+ function buildQuitRequest() {
680
+ return serializeHeader({
681
+ magic: REQUEST_MAGIC,
682
+ opcode: OPCODE_QUIT
683
+ });
684
+ }
685
+ function parseGetResponse(buf) {
686
+ const header = deserializeHeader(buf);
687
+ if (header.status !== STATUS_SUCCESS) {
688
+ return { header, value: void 0, key: void 0 };
689
+ }
690
+ const extrasEnd = HEADER_SIZE + header.extrasLength;
691
+ const keyEnd = extrasEnd + header.keyLength;
692
+ const valueEnd = HEADER_SIZE + header.totalBodyLength;
693
+ const key = header.keyLength > 0 ? buf.subarray(extrasEnd, keyEnd).toString("utf8") : void 0;
694
+ const value = valueEnd > keyEnd ? buf.subarray(keyEnd, valueEnd) : void 0;
695
+ return { header, value, key };
696
+ }
697
+ function parseIncrDecrResponse(buf) {
698
+ const header = deserializeHeader(buf);
699
+ if (header.status !== STATUS_SUCCESS || header.totalBodyLength < 8) {
700
+ return { header, value: void 0 };
701
+ }
702
+ const high = buf.readUInt32BE(HEADER_SIZE);
703
+ const low = buf.readUInt32BE(HEADER_SIZE + 4);
704
+ const value = high * 4294967296 + low;
705
+ return { header, value };
706
+ }
707
+
708
+ // src/node.ts
328
709
  var MemcacheNode = class extends Hookified {
329
710
  _host;
330
711
  _port;
@@ -339,6 +720,9 @@ var MemcacheNode = class extends Hookified {
339
720
  _currentCommand = void 0;
340
721
  _multilineData = [];
341
722
  _pendingValueBytes = 0;
723
+ _sasl;
724
+ _authenticated = false;
725
+ _binaryBuffer = Buffer.alloc(0);
342
726
  constructor(host, port, options) {
343
727
  super();
344
728
  this._host = host;
@@ -347,6 +731,7 @@ var MemcacheNode = class extends Hookified {
347
731
  this._keepAlive = options?.keepAlive !== false;
348
732
  this._keepAliveDelay = options?.keepAliveDelay || 1e3;
349
733
  this._weight = options?.weight || 1;
734
+ this._sasl = options?.sasl;
350
735
  }
351
736
  /**
352
737
  * Get the host of this node
@@ -420,6 +805,18 @@ var MemcacheNode = class extends Hookified {
420
805
  get commandQueue() {
421
806
  return this._commandQueue;
422
807
  }
808
+ /**
809
+ * Get whether SASL authentication is configured
810
+ */
811
+ get hasSaslCredentials() {
812
+ return !!this._sasl?.username && !!this._sasl?.password;
813
+ }
814
+ /**
815
+ * Get whether the node is authenticated (only relevant if SASL is configured)
816
+ */
817
+ get isAuthenticated() {
818
+ return this._authenticated;
819
+ }
423
820
  /**
424
821
  * Connect to the memcache server
425
822
  */
@@ -436,14 +833,31 @@ var MemcacheNode = class extends Hookified {
436
833
  keepAliveInitialDelay: this._keepAliveDelay
437
834
  });
438
835
  this._socket.setTimeout(this._timeout);
439
- this._socket.setEncoding("utf8");
440
- this._socket.on("connect", () => {
836
+ if (!this._sasl) {
837
+ this._socket.setEncoding("utf8");
838
+ }
839
+ this._socket.on("connect", async () => {
441
840
  this._connected = true;
442
- this.emit("connect");
443
- resolve();
841
+ if (this._sasl) {
842
+ try {
843
+ await this.performSaslAuth();
844
+ this.emit("connect");
845
+ resolve();
846
+ } catch (error) {
847
+ this._socket?.destroy();
848
+ this._connected = false;
849
+ this._authenticated = false;
850
+ reject(error);
851
+ }
852
+ } else {
853
+ this.emit("connect");
854
+ resolve();
855
+ }
444
856
  });
445
857
  this._socket.on("data", (data) => {
446
- this.handleData(data);
858
+ if (typeof data === "string") {
859
+ this.handleData(data);
860
+ }
447
861
  });
448
862
  this._socket.on("error", (error) => {
449
863
  this.emit("error", error);
@@ -453,6 +867,7 @@ var MemcacheNode = class extends Hookified {
453
867
  });
454
868
  this._socket.on("close", () => {
455
869
  this._connected = false;
870
+ this._authenticated = false;
456
871
  this.emit("close");
457
872
  this.rejectPendingCommands(new Error("Connection closed"));
458
873
  });
@@ -486,9 +901,257 @@ var MemcacheNode = class extends Hookified {
486
901
  this._currentCommand = void 0;
487
902
  this._multilineData = [];
488
903
  this._pendingValueBytes = 0;
904
+ this._authenticated = false;
905
+ this._binaryBuffer = Buffer.alloc(0);
489
906
  }
490
907
  await this.connect();
491
908
  }
909
+ /**
910
+ * Perform SASL PLAIN authentication using the binary protocol
911
+ */
912
+ async performSaslAuth() {
913
+ if (!this._sasl || !this._socket) {
914
+ throw new Error("SASL credentials not configured");
915
+ }
916
+ const socket = this._socket;
917
+ const sasl = this._sasl;
918
+ return new Promise((resolve, reject) => {
919
+ this._binaryBuffer = Buffer.alloc(0);
920
+ const authPacket = buildSaslPlainRequest(sasl.username, sasl.password);
921
+ const binaryHandler = (data) => {
922
+ this._binaryBuffer = Buffer.concat([this._binaryBuffer, data]);
923
+ if (this._binaryBuffer.length < HEADER_SIZE) {
924
+ return;
925
+ }
926
+ const header = deserializeHeader(this._binaryBuffer);
927
+ const totalLength = HEADER_SIZE + header.totalBodyLength;
928
+ if (this._binaryBuffer.length < totalLength) {
929
+ return;
930
+ }
931
+ socket.removeListener("data", binaryHandler);
932
+ if (header.status === STATUS_SUCCESS) {
933
+ this._authenticated = true;
934
+ this.emit("authenticated");
935
+ resolve();
936
+ } else if (header.status === STATUS_AUTH_ERROR) {
937
+ const body = this._binaryBuffer.subarray(HEADER_SIZE, totalLength);
938
+ reject(
939
+ new Error(
940
+ `SASL authentication failed: ${body.toString() || "Invalid credentials"}`
941
+ )
942
+ );
943
+ } else {
944
+ reject(
945
+ new Error(
946
+ `SASL authentication failed with status: 0x${header.status.toString(16)}`
947
+ )
948
+ );
949
+ }
950
+ };
951
+ socket.on("data", binaryHandler);
952
+ socket.write(authPacket);
953
+ });
954
+ }
955
+ /**
956
+ * Send a binary protocol request and wait for response.
957
+ * Used internally for SASL-authenticated connections.
958
+ */
959
+ async binaryRequest(packet) {
960
+ if (!this._socket) {
961
+ throw new Error("Not connected");
962
+ }
963
+ const socket = this._socket;
964
+ return new Promise((resolve) => {
965
+ let buffer = Buffer.alloc(0);
966
+ const dataHandler = (data) => {
967
+ buffer = Buffer.concat([buffer, data]);
968
+ if (buffer.length < HEADER_SIZE) {
969
+ return;
970
+ }
971
+ const header = deserializeHeader(buffer);
972
+ const totalLength = HEADER_SIZE + header.totalBodyLength;
973
+ if (buffer.length < totalLength) {
974
+ return;
975
+ }
976
+ socket.removeListener("data", dataHandler);
977
+ resolve(buffer.subarray(0, totalLength));
978
+ };
979
+ socket.on("data", dataHandler);
980
+ socket.write(packet);
981
+ });
982
+ }
983
+ /**
984
+ * Binary protocol GET operation
985
+ */
986
+ async binaryGet(key) {
987
+ const response = await this.binaryRequest(buildGetRequest(key));
988
+ const { header, value } = parseGetResponse(response);
989
+ if (header.status === STATUS_KEY_NOT_FOUND) {
990
+ this.emit("miss", key);
991
+ return void 0;
992
+ }
993
+ if (header.status !== STATUS_SUCCESS || !value) {
994
+ return void 0;
995
+ }
996
+ const result = value.toString("utf8");
997
+ this.emit("hit", key, result);
998
+ return result;
999
+ }
1000
+ /**
1001
+ * Binary protocol SET operation
1002
+ */
1003
+ async binarySet(key, value, exptime = 0, flags = 0) {
1004
+ const response = await this.binaryRequest(
1005
+ buildSetRequest(key, value, flags, exptime)
1006
+ );
1007
+ const header = deserializeHeader(response);
1008
+ return header.status === STATUS_SUCCESS;
1009
+ }
1010
+ /**
1011
+ * Binary protocol ADD operation
1012
+ */
1013
+ async binaryAdd(key, value, exptime = 0, flags = 0) {
1014
+ const response = await this.binaryRequest(
1015
+ buildAddRequest(key, value, flags, exptime)
1016
+ );
1017
+ const header = deserializeHeader(response);
1018
+ return header.status === STATUS_SUCCESS;
1019
+ }
1020
+ /**
1021
+ * Binary protocol REPLACE operation
1022
+ */
1023
+ async binaryReplace(key, value, exptime = 0, flags = 0) {
1024
+ const response = await this.binaryRequest(
1025
+ buildReplaceRequest(key, value, flags, exptime)
1026
+ );
1027
+ const header = deserializeHeader(response);
1028
+ return header.status === STATUS_SUCCESS;
1029
+ }
1030
+ /**
1031
+ * Binary protocol DELETE operation
1032
+ */
1033
+ async binaryDelete(key) {
1034
+ const response = await this.binaryRequest(buildDeleteRequest(key));
1035
+ const header = deserializeHeader(response);
1036
+ return header.status === STATUS_SUCCESS || header.status === STATUS_KEY_NOT_FOUND;
1037
+ }
1038
+ /**
1039
+ * Binary protocol INCREMENT operation
1040
+ */
1041
+ async binaryIncr(key, delta = 1, initial = 0, exptime = 0) {
1042
+ const response = await this.binaryRequest(
1043
+ buildIncrementRequest(key, delta, initial, exptime)
1044
+ );
1045
+ const { header, value } = parseIncrDecrResponse(response);
1046
+ if (header.status !== STATUS_SUCCESS) {
1047
+ return void 0;
1048
+ }
1049
+ return value;
1050
+ }
1051
+ /**
1052
+ * Binary protocol DECREMENT operation
1053
+ */
1054
+ async binaryDecr(key, delta = 1, initial = 0, exptime = 0) {
1055
+ const response = await this.binaryRequest(
1056
+ buildDecrementRequest(key, delta, initial, exptime)
1057
+ );
1058
+ const { header, value } = parseIncrDecrResponse(response);
1059
+ if (header.status !== STATUS_SUCCESS) {
1060
+ return void 0;
1061
+ }
1062
+ return value;
1063
+ }
1064
+ /**
1065
+ * Binary protocol APPEND operation
1066
+ */
1067
+ async binaryAppend(key, value) {
1068
+ const response = await this.binaryRequest(buildAppendRequest(key, value));
1069
+ const header = deserializeHeader(response);
1070
+ return header.status === STATUS_SUCCESS;
1071
+ }
1072
+ /**
1073
+ * Binary protocol PREPEND operation
1074
+ */
1075
+ async binaryPrepend(key, value) {
1076
+ const response = await this.binaryRequest(buildPrependRequest(key, value));
1077
+ const header = deserializeHeader(response);
1078
+ return header.status === STATUS_SUCCESS;
1079
+ }
1080
+ /**
1081
+ * Binary protocol TOUCH operation
1082
+ */
1083
+ async binaryTouch(key, exptime) {
1084
+ const response = await this.binaryRequest(buildTouchRequest(key, exptime));
1085
+ const header = deserializeHeader(response);
1086
+ return header.status === STATUS_SUCCESS;
1087
+ }
1088
+ /**
1089
+ * Binary protocol FLUSH operation
1090
+ */
1091
+ /* v8 ignore next -- @preserve */
1092
+ async binaryFlush(exptime = 0) {
1093
+ const response = await this.binaryRequest(buildFlushRequest(exptime));
1094
+ const header = deserializeHeader(response);
1095
+ return header.status === STATUS_SUCCESS;
1096
+ }
1097
+ /**
1098
+ * Binary protocol VERSION operation
1099
+ */
1100
+ async binaryVersion() {
1101
+ const response = await this.binaryRequest(buildVersionRequest());
1102
+ const header = deserializeHeader(response);
1103
+ if (header.status !== STATUS_SUCCESS) {
1104
+ return void 0;
1105
+ }
1106
+ return response.subarray(HEADER_SIZE, HEADER_SIZE + header.totalBodyLength).toString("utf8");
1107
+ }
1108
+ /**
1109
+ * Binary protocol STATS operation
1110
+ */
1111
+ async binaryStats() {
1112
+ if (!this._socket) {
1113
+ throw new Error("Not connected");
1114
+ }
1115
+ const socket = this._socket;
1116
+ const stats = {};
1117
+ return new Promise((resolve) => {
1118
+ let buffer = Buffer.alloc(0);
1119
+ const dataHandler = (data) => {
1120
+ buffer = Buffer.concat([buffer, data]);
1121
+ while (buffer.length >= HEADER_SIZE) {
1122
+ const header = deserializeHeader(buffer);
1123
+ const totalLength = HEADER_SIZE + header.totalBodyLength;
1124
+ if (buffer.length < totalLength) {
1125
+ return;
1126
+ }
1127
+ if (header.keyLength === 0 && header.totalBodyLength === 0) {
1128
+ socket.removeListener("data", dataHandler);
1129
+ resolve(stats);
1130
+ return;
1131
+ }
1132
+ if (header.opcode === OPCODE_STAT && header.status === STATUS_SUCCESS) {
1133
+ const keyStart = HEADER_SIZE;
1134
+ const keyEnd = keyStart + header.keyLength;
1135
+ const valueEnd = HEADER_SIZE + header.totalBodyLength;
1136
+ const key = buffer.subarray(keyStart, keyEnd).toString("utf8");
1137
+ const value = buffer.subarray(keyEnd, valueEnd).toString("utf8");
1138
+ stats[key] = value;
1139
+ }
1140
+ buffer = buffer.subarray(totalLength);
1141
+ }
1142
+ };
1143
+ socket.on("data", dataHandler);
1144
+ socket.write(buildStatRequest());
1145
+ });
1146
+ }
1147
+ /**
1148
+ * Binary protocol QUIT operation
1149
+ */
1150
+ async binaryQuit() {
1151
+ if (this._socket) {
1152
+ this._socket.write(buildQuitRequest());
1153
+ }
1154
+ }
492
1155
  /**
493
1156
  * Gracefully quit the connection (send quit command then disconnect)
494
1157
  */
@@ -653,7 +1316,7 @@ function createNode(host, port, options) {
653
1316
  return new MemcacheNode(host, port, options);
654
1317
  }
655
1318
 
656
- // src/index.ts
1319
+ // src/types.ts
657
1320
  var MemcacheEvents = /* @__PURE__ */ ((MemcacheEvents2) => {
658
1321
  MemcacheEvents2["CONNECT"] = "connect";
659
1322
  MemcacheEvents2["QUIT"] = "quit";
@@ -666,24 +1329,44 @@ var MemcacheEvents = /* @__PURE__ */ ((MemcacheEvents2) => {
666
1329
  MemcacheEvents2["CLOSE"] = "close";
667
1330
  return MemcacheEvents2;
668
1331
  })(MemcacheEvents || {});
1332
+
1333
+ // src/index.ts
1334
+ var defaultRetryBackoff = (_attempt, baseDelay) => baseDelay;
1335
+ var exponentialRetryBackoff = (attempt, baseDelay) => baseDelay * 2 ** attempt;
669
1336
  var Memcache = class extends Hookified2 {
670
1337
  _nodes = [];
671
1338
  _timeout;
672
1339
  _keepAlive;
673
1340
  _keepAliveDelay;
674
1341
  _hash;
1342
+ _retries;
1343
+ _retryDelay;
1344
+ _retryBackoff;
1345
+ _retryOnlyIdempotent;
1346
+ _sasl;
675
1347
  constructor(options) {
676
1348
  super();
677
- this._hash = new KetamaHash();
678
1349
  if (typeof options === "string") {
1350
+ this._hash = new KetamaHash();
679
1351
  this._timeout = 5e3;
680
1352
  this._keepAlive = true;
681
1353
  this._keepAliveDelay = 1e3;
1354
+ this._retries = 0;
1355
+ this._retryDelay = 100;
1356
+ this._retryBackoff = defaultRetryBackoff;
1357
+ this._retryOnlyIdempotent = true;
1358
+ this._sasl = void 0;
682
1359
  this.addNode(options);
683
1360
  } else {
1361
+ this._hash = options?.hash ?? new KetamaHash();
684
1362
  this._timeout = options?.timeout || 5e3;
685
1363
  this._keepAlive = options?.keepAlive !== false;
686
1364
  this._keepAliveDelay = options?.keepAliveDelay || 1e3;
1365
+ this._retries = options?.retries ?? 0;
1366
+ this._retryDelay = options?.retryDelay ?? 100;
1367
+ this._retryBackoff = options?.retryBackoff ?? defaultRetryBackoff;
1368
+ this._retryOnlyIdempotent = options?.retryOnlyIdempotent ?? true;
1369
+ this._sasl = options?.sasl;
687
1370
  const nodeUris = options?.nodes || ["localhost:11211"];
688
1371
  for (const nodeUri of nodeUris) {
689
1372
  this.addNode(nodeUri);
@@ -788,6 +1471,74 @@ var Memcache = class extends Hookified2 {
788
1471
  this._keepAliveDelay = value;
789
1472
  this.updateNodes();
790
1473
  }
1474
+ /**
1475
+ * Get the number of retry attempts for failed commands.
1476
+ * @returns {number}
1477
+ * @default 0
1478
+ */
1479
+ get retries() {
1480
+ return this._retries;
1481
+ }
1482
+ /**
1483
+ * Set the number of retry attempts for failed commands.
1484
+ * Set to 0 to disable retries.
1485
+ * @param {number} value
1486
+ * @default 0
1487
+ */
1488
+ set retries(value) {
1489
+ this._retries = Math.max(0, Math.floor(value));
1490
+ }
1491
+ /**
1492
+ * Get the base delay in milliseconds between retry attempts.
1493
+ * @returns {number}
1494
+ * @default 100
1495
+ */
1496
+ get retryDelay() {
1497
+ return this._retryDelay;
1498
+ }
1499
+ /**
1500
+ * Set the base delay in milliseconds between retry attempts.
1501
+ * @param {number} value
1502
+ * @default 100
1503
+ */
1504
+ set retryDelay(value) {
1505
+ this._retryDelay = Math.max(0, value);
1506
+ }
1507
+ /**
1508
+ * Get the backoff function for retry delays.
1509
+ * @returns {RetryBackoffFunction}
1510
+ * @default defaultRetryBackoff
1511
+ */
1512
+ get retryBackoff() {
1513
+ return this._retryBackoff;
1514
+ }
1515
+ /**
1516
+ * Set the backoff function for retry delays.
1517
+ * @param {RetryBackoffFunction} value
1518
+ * @default defaultRetryBackoff
1519
+ */
1520
+ set retryBackoff(value) {
1521
+ this._retryBackoff = value;
1522
+ }
1523
+ /**
1524
+ * Get whether retries are restricted to idempotent commands only.
1525
+ * @returns {boolean}
1526
+ * @default true
1527
+ */
1528
+ get retryOnlyIdempotent() {
1529
+ return this._retryOnlyIdempotent;
1530
+ }
1531
+ /**
1532
+ * Set whether retries are restricted to idempotent commands only.
1533
+ * When true (default), retries only occur for commands explicitly marked
1534
+ * as idempotent via ExecuteOptions. This prevents accidental double-execution
1535
+ * of non-idempotent operations like incr, decr, append, etc.
1536
+ * @param {boolean} value
1537
+ * @default true
1538
+ */
1539
+ set retryOnlyIdempotent(value) {
1540
+ this._retryOnlyIdempotent = value;
1541
+ }
791
1542
  /**
792
1543
  * Get an array of all MemcacheNode instances
793
1544
  * @returns {MemcacheNode[]}
@@ -821,7 +1572,8 @@ var Memcache = class extends Hookified2 {
821
1572
  timeout: this._timeout,
822
1573
  keepAlive: this._keepAlive,
823
1574
  keepAliveDelay: this._keepAliveDelay,
824
- weight
1575
+ weight,
1576
+ sasl: this._sasl
825
1577
  });
826
1578
  } else {
827
1579
  node = uri;
@@ -1036,16 +1788,8 @@ var Memcache = class extends Hookified2 {
1036
1788
  const command = `cas ${key} ${flags} ${exptime} ${bytes} ${casToken}\r
1037
1789
  ${valueStr}`;
1038
1790
  const nodes = await this.getNodesByKey(key);
1039
- const promises = nodes.map(async (node) => {
1040
- try {
1041
- const result = await node.command(command);
1042
- return result === "STORED";
1043
- } catch {
1044
- return false;
1045
- }
1046
- });
1047
- const results = await Promise.all(promises);
1048
- const success = results.every((result) => result === true);
1791
+ const results = await this.execute(command, nodes);
1792
+ const success = results.every((result) => result === "STORED");
1049
1793
  await this.afterHook("cas", {
1050
1794
  key,
1051
1795
  value,
@@ -1074,16 +1818,8 @@ ${valueStr}`;
1074
1818
  const command = `set ${key} ${flags} ${exptime} ${bytes}\r
1075
1819
  ${valueStr}`;
1076
1820
  const nodes = await this.getNodesByKey(key);
1077
- const promises = nodes.map(async (node) => {
1078
- try {
1079
- const result = await node.command(command);
1080
- return result === "STORED";
1081
- } catch {
1082
- return false;
1083
- }
1084
- });
1085
- const results = await Promise.all(promises);
1086
- const success = results.every((result) => result === true);
1821
+ const results = await this.execute(command, nodes);
1822
+ const success = results.every((result) => result === "STORED");
1087
1823
  await this.afterHook("set", { key, value, exptime, flags, success });
1088
1824
  return success;
1089
1825
  }
@@ -1105,16 +1841,8 @@ ${valueStr}`;
1105
1841
  const command = `add ${key} ${flags} ${exptime} ${bytes}\r
1106
1842
  ${valueStr}`;
1107
1843
  const nodes = await this.getNodesByKey(key);
1108
- const promises = nodes.map(async (node) => {
1109
- try {
1110
- const result = await node.command(command);
1111
- return result === "STORED";
1112
- } catch {
1113
- return false;
1114
- }
1115
- });
1116
- const results = await Promise.all(promises);
1117
- const success = results.every((result) => result === true);
1844
+ const results = await this.execute(command, nodes);
1845
+ const success = results.every((result) => result === "STORED");
1118
1846
  await this.afterHook("add", { key, value, exptime, flags, success });
1119
1847
  return success;
1120
1848
  }
@@ -1136,16 +1864,8 @@ ${valueStr}`;
1136
1864
  const command = `replace ${key} ${flags} ${exptime} ${bytes}\r
1137
1865
  ${valueStr}`;
1138
1866
  const nodes = await this.getNodesByKey(key);
1139
- const promises = nodes.map(async (node) => {
1140
- try {
1141
- const result = await node.command(command);
1142
- return result === "STORED";
1143
- } catch {
1144
- return false;
1145
- }
1146
- });
1147
- const results = await Promise.all(promises);
1148
- const success = results.every((result) => result === true);
1867
+ const results = await this.execute(command, nodes);
1868
+ const success = results.every((result) => result === "STORED");
1149
1869
  await this.afterHook("replace", { key, value, exptime, flags, success });
1150
1870
  return success;
1151
1871
  }
@@ -1165,16 +1885,8 @@ ${valueStr}`;
1165
1885
  const command = `append ${key} 0 0 ${bytes}\r
1166
1886
  ${valueStr}`;
1167
1887
  const nodes = await this.getNodesByKey(key);
1168
- const promises = nodes.map(async (node) => {
1169
- try {
1170
- const result = await node.command(command);
1171
- return result === "STORED";
1172
- } catch {
1173
- return false;
1174
- }
1175
- });
1176
- const results = await Promise.all(promises);
1177
- const success = results.every((result) => result === true);
1888
+ const results = await this.execute(command, nodes);
1889
+ const success = results.every((result) => result === "STORED");
1178
1890
  await this.afterHook("append", { key, value, success });
1179
1891
  return success;
1180
1892
  }
@@ -1194,16 +1906,8 @@ ${valueStr}`;
1194
1906
  const command = `prepend ${key} 0 0 ${bytes}\r
1195
1907
  ${valueStr}`;
1196
1908
  const nodes = await this.getNodesByKey(key);
1197
- const promises = nodes.map(async (node) => {
1198
- try {
1199
- const result = await node.command(command);
1200
- return result === "STORED";
1201
- } catch {
1202
- return false;
1203
- }
1204
- });
1205
- const results = await Promise.all(promises);
1206
- const success = results.every((result) => result === true);
1909
+ const results = await this.execute(command, nodes);
1910
+ const success = results.every((result) => result === "STORED");
1207
1911
  await this.afterHook("prepend", { key, value, success });
1208
1912
  return success;
1209
1913
  }
@@ -1218,16 +1922,8 @@ ${valueStr}`;
1218
1922
  await this.beforeHook("delete", { key });
1219
1923
  this.validateKey(key);
1220
1924
  const nodes = await this.getNodesByKey(key);
1221
- const promises = nodes.map(async (node) => {
1222
- try {
1223
- const result = await node.command(`delete ${key}`);
1224
- return result === "DELETED";
1225
- } catch {
1226
- return false;
1227
- }
1228
- });
1229
- const results = await Promise.all(promises);
1230
- const success = results.every((result) => result === true);
1925
+ const results = await this.execute(`delete ${key}`, nodes);
1926
+ const success = results.every((result) => result === "DELETED");
1231
1927
  await this.afterHook("delete", { key, success });
1232
1928
  return success;
1233
1929
  }
@@ -1243,16 +1939,8 @@ ${valueStr}`;
1243
1939
  await this.beforeHook("incr", { key, value });
1244
1940
  this.validateKey(key);
1245
1941
  const nodes = await this.getNodesByKey(key);
1246
- const promises = nodes.map(async (node) => {
1247
- try {
1248
- const result = await node.command(`incr ${key} ${value}`);
1249
- return typeof result === "number" ? result : void 0;
1250
- } catch {
1251
- return void 0;
1252
- }
1253
- });
1254
- const results = await Promise.all(promises);
1255
- const newValue = results.find((v) => v !== void 0);
1942
+ const results = await this.execute(`incr ${key} ${value}`, nodes);
1943
+ const newValue = results.find((v) => typeof v === "number");
1256
1944
  await this.afterHook("incr", { key, value, newValue });
1257
1945
  return newValue;
1258
1946
  }
@@ -1268,16 +1956,8 @@ ${valueStr}`;
1268
1956
  await this.beforeHook("decr", { key, value });
1269
1957
  this.validateKey(key);
1270
1958
  const nodes = await this.getNodesByKey(key);
1271
- const promises = nodes.map(async (node) => {
1272
- try {
1273
- const result = await node.command(`decr ${key} ${value}`);
1274
- return typeof result === "number" ? result : void 0;
1275
- } catch {
1276
- return void 0;
1277
- }
1278
- });
1279
- const results = await Promise.all(promises);
1280
- const newValue = results.find((v) => v !== void 0);
1959
+ const results = await this.execute(`decr ${key} ${value}`, nodes);
1960
+ const newValue = results.find((v) => typeof v === "number");
1281
1961
  await this.afterHook("decr", { key, value, newValue });
1282
1962
  return newValue;
1283
1963
  }
@@ -1293,16 +1973,8 @@ ${valueStr}`;
1293
1973
  await this.beforeHook("touch", { key, exptime });
1294
1974
  this.validateKey(key);
1295
1975
  const nodes = await this.getNodesByKey(key);
1296
- const promises = nodes.map(async (node) => {
1297
- try {
1298
- const result = await node.command(`touch ${key} ${exptime}`);
1299
- return result === "TOUCHED";
1300
- } catch {
1301
- return false;
1302
- }
1303
- });
1304
- const results = await Promise.all(promises);
1305
- const success = results.every((result) => result === true);
1976
+ const results = await this.execute(`touch ${key} ${exptime}`, nodes);
1977
+ const success = results.every((result) => result === "TOUCHED");
1306
1978
  await this.afterHook("touch", { key, exptime, success });
1307
1979
  return success;
1308
1980
  }
@@ -1418,6 +2090,31 @@ ${valueStr}`;
1418
2090
  }
1419
2091
  return nodes;
1420
2092
  }
2093
+ /**
2094
+ * Execute a command on the specified nodes with retry support.
2095
+ * @param {string} command - The memcache command string to execute
2096
+ * @param {MemcacheNode[]} nodes - Array of MemcacheNode instances to execute on
2097
+ * @param {ExecuteOptions} options - Optional execution options including retry overrides
2098
+ * @returns {Promise<unknown[]>} Promise resolving to array of results from each node
2099
+ */
2100
+ async execute(command, nodes, options) {
2101
+ const configuredRetries = options?.retries ?? this._retries;
2102
+ const retryDelay = options?.retryDelay ?? this._retryDelay;
2103
+ const retryBackoff = options?.retryBackoff ?? this._retryBackoff;
2104
+ const isIdempotent = options?.idempotent === true;
2105
+ const maxRetries = this._retryOnlyIdempotent && !isIdempotent ? 0 : configuredRetries;
2106
+ const promises = nodes.map(async (node) => {
2107
+ return this.executeWithRetry(
2108
+ node,
2109
+ command,
2110
+ options?.commandOptions,
2111
+ maxRetries,
2112
+ retryDelay,
2113
+ retryBackoff
2114
+ );
2115
+ });
2116
+ return Promise.all(promises);
2117
+ }
1421
2118
  /**
1422
2119
  * Validates a Memcache key according to protocol requirements.
1423
2120
  * @param {string} key - The key to validate
@@ -1445,6 +2142,46 @@ ${valueStr}`;
1445
2142
  }
1446
2143
  }
1447
2144
  // Private methods
2145
+ /**
2146
+ * Sleep utility for retry delays.
2147
+ * @param {number} ms - Milliseconds to sleep
2148
+ * @returns {Promise<void>}
2149
+ */
2150
+ sleep(ms) {
2151
+ return new Promise((resolve) => setTimeout(resolve, ms));
2152
+ }
2153
+ /**
2154
+ * Execute a command on a single node with retry logic.
2155
+ * @param {MemcacheNode} node - The node to execute on
2156
+ * @param {string} command - The command string
2157
+ * @param {CommandOptions} commandOptions - Optional command options
2158
+ * @param {number} maxRetries - Maximum number of retry attempts
2159
+ * @param {number} retryDelay - Base delay between retries in milliseconds
2160
+ * @param {RetryBackoffFunction} retryBackoff - Function to calculate backoff delay
2161
+ * @returns {Promise<unknown>} Result or undefined on failure
2162
+ */
2163
+ async executeWithRetry(node, command, commandOptions, maxRetries, retryDelay, retryBackoff) {
2164
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
2165
+ try {
2166
+ return await node.command(command, commandOptions);
2167
+ } catch {
2168
+ if (attempt >= maxRetries) {
2169
+ break;
2170
+ }
2171
+ const delay = retryBackoff(attempt, retryDelay);
2172
+ if (delay > 0) {
2173
+ await this.sleep(delay);
2174
+ }
2175
+ if (!node.isConnected()) {
2176
+ try {
2177
+ await node.connect();
2178
+ } catch {
2179
+ }
2180
+ }
2181
+ }
2182
+ }
2183
+ return void 0;
2184
+ }
1448
2185
  /**
1449
2186
  * Update all nodes with current keepAlive settings
1450
2187
  */
@@ -1476,7 +2213,12 @@ var index_default = Memcache;
1476
2213
  export {
1477
2214
  Memcache,
1478
2215
  MemcacheEvents,
2216
+ MemcacheNode,
2217
+ ModulaHash,
1479
2218
  createNode,
1480
- index_default as default
2219
+ index_default as default,
2220
+ defaultRetryBackoff,
2221
+ exponentialRetryBackoff
1481
2222
  };
1482
2223
  /* v8 ignore next -- @preserve */
2224
+ /* v8 ignore next 3 -- @preserve */