memcache 1.2.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.cjs CHANGED
@@ -22,8 +22,12 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  Memcache: () => Memcache,
24
24
  MemcacheEvents: () => MemcacheEvents,
25
+ MemcacheNode: () => MemcacheNode,
26
+ ModulaHash: () => ModulaHash,
25
27
  createNode: () => createNode,
26
- default: () => index_default
28
+ default: () => index_default,
29
+ defaultRetryBackoff: () => defaultRetryBackoff,
30
+ exponentialRetryBackoff: () => exponentialRetryBackoff
27
31
  });
28
32
  module.exports = __toCommonJS(index_exports);
29
33
  var import_hookified2 = require("hookified");
@@ -349,9 +353,390 @@ function binarySearchRing(ring, hash) {
349
353
  return lo;
350
354
  }
351
355
 
356
+ // src/modula.ts
357
+ var import_node_crypto2 = require("crypto");
358
+ var hashFunctionForBuiltin2 = (algorithm) => (value) => (0, import_node_crypto2.createHash)(algorithm).update(value).digest().readUInt32BE(0);
359
+ var ModulaHash = class {
360
+ /** The name of this distribution strategy */
361
+ name = "modula";
362
+ /** The hash function used to compute key hashes */
363
+ hashFn;
364
+ /** Map of node IDs to MemcacheNode instances */
365
+ nodeMap;
366
+ /**
367
+ * Weighted list of node IDs for modulo distribution.
368
+ * Nodes with higher weights appear multiple times.
369
+ */
370
+ nodeList;
371
+ /**
372
+ * Creates a new ModulaHash instance.
373
+ *
374
+ * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
375
+ *
376
+ * @example
377
+ * ```typescript
378
+ * // Use default SHA-1 hashing
379
+ * const distribution = new ModulaHash();
380
+ *
381
+ * // Use MD5 hashing
382
+ * const distribution = new ModulaHash('md5');
383
+ *
384
+ * // Use custom hash function
385
+ * const distribution = new ModulaHash((buf) => buf.readUInt32BE(0));
386
+ * ```
387
+ */
388
+ constructor(hashFn) {
389
+ this.hashFn = typeof hashFn === "string" ? hashFunctionForBuiltin2(hashFn) : hashFn ?? hashFunctionForBuiltin2("sha1");
390
+ this.nodeMap = /* @__PURE__ */ new Map();
391
+ this.nodeList = [];
392
+ }
393
+ /**
394
+ * Gets all nodes in the distribution.
395
+ * @returns Array of all MemcacheNode instances
396
+ */
397
+ get nodes() {
398
+ return Array.from(this.nodeMap.values());
399
+ }
400
+ /**
401
+ * Adds a node to the distribution with its weight.
402
+ * Weight determines how many times the node appears in the distribution list.
403
+ *
404
+ * @param node - The MemcacheNode to add
405
+ *
406
+ * @example
407
+ * ```typescript
408
+ * const node = new MemcacheNode('localhost', 11211, { weight: 2 });
409
+ * distribution.addNode(node);
410
+ * ```
411
+ */
412
+ addNode(node) {
413
+ this.nodeMap.set(node.id, node);
414
+ const weight = node.weight || 1;
415
+ for (let i = 0; i < weight; i++) {
416
+ this.nodeList.push(node.id);
417
+ }
418
+ }
419
+ /**
420
+ * Removes a node from the distribution by its ID.
421
+ *
422
+ * @param id - The node ID (e.g., "localhost:11211")
423
+ *
424
+ * @example
425
+ * ```typescript
426
+ * distribution.removeNode('localhost:11211');
427
+ * ```
428
+ */
429
+ removeNode(id) {
430
+ this.nodeMap.delete(id);
431
+ this.nodeList = this.nodeList.filter((nodeId) => nodeId !== id);
432
+ }
433
+ /**
434
+ * Gets a specific node by its ID.
435
+ *
436
+ * @param id - The node ID (e.g., "localhost:11211")
437
+ * @returns The MemcacheNode if found, undefined otherwise
438
+ *
439
+ * @example
440
+ * ```typescript
441
+ * const node = distribution.getNode('localhost:11211');
442
+ * if (node) {
443
+ * console.log(`Found node: ${node.uri}`);
444
+ * }
445
+ * ```
446
+ */
447
+ getNode(id) {
448
+ return this.nodeMap.get(id);
449
+ }
450
+ /**
451
+ * Gets the nodes responsible for a given key using modulo hashing.
452
+ * Uses `hash(key) % nodeCount` to determine the target node.
453
+ *
454
+ * @param key - The cache key to find the responsible node for
455
+ * @returns Array containing the responsible node(s), empty if no nodes available
456
+ *
457
+ * @example
458
+ * ```typescript
459
+ * const nodes = distribution.getNodesByKey('user:123');
460
+ * if (nodes.length > 0) {
461
+ * console.log(`Key will be stored on: ${nodes[0].id}`);
462
+ * }
463
+ * ```
464
+ */
465
+ getNodesByKey(key) {
466
+ if (this.nodeList.length === 0) {
467
+ return [];
468
+ }
469
+ const hash = this.hashFn(Buffer.from(key));
470
+ const index = hash % this.nodeList.length;
471
+ const nodeId = this.nodeList[index];
472
+ const node = this.nodeMap.get(nodeId);
473
+ return node ? [node] : [];
474
+ }
475
+ };
476
+
352
477
  // src/node.ts
353
478
  var import_node_net = require("net");
354
479
  var import_hookified = require("hookified");
480
+
481
+ // src/binary-protocol.ts
482
+ var REQUEST_MAGIC = 128;
483
+ var OPCODE_SASL_AUTH = 33;
484
+ var OPCODE_GET = 0;
485
+ var OPCODE_SET = 1;
486
+ var OPCODE_ADD = 2;
487
+ var OPCODE_REPLACE = 3;
488
+ var OPCODE_DELETE = 4;
489
+ var OPCODE_INCREMENT = 5;
490
+ var OPCODE_DECREMENT = 6;
491
+ var OPCODE_QUIT = 7;
492
+ var OPCODE_FLUSH = 8;
493
+ var OPCODE_VERSION = 11;
494
+ var OPCODE_APPEND = 14;
495
+ var OPCODE_PREPEND = 15;
496
+ var OPCODE_STAT = 16;
497
+ var OPCODE_TOUCH = 28;
498
+ var STATUS_SUCCESS = 0;
499
+ var STATUS_KEY_NOT_FOUND = 1;
500
+ var STATUS_AUTH_ERROR = 32;
501
+ var HEADER_SIZE = 24;
502
+ function serializeHeader(header) {
503
+ const buf = Buffer.alloc(HEADER_SIZE);
504
+ buf.writeUInt8(header.magic ?? REQUEST_MAGIC, 0);
505
+ buf.writeUInt8(header.opcode ?? 0, 1);
506
+ buf.writeUInt16BE(header.keyLength ?? 0, 2);
507
+ buf.writeUInt8(header.extrasLength ?? 0, 4);
508
+ buf.writeUInt8(header.dataType ?? 0, 5);
509
+ buf.writeUInt16BE(header.status ?? 0, 6);
510
+ buf.writeUInt32BE(header.totalBodyLength ?? 0, 8);
511
+ buf.writeUInt32BE(header.opaque ?? 0, 12);
512
+ if (header.cas) {
513
+ header.cas.copy(buf, 16);
514
+ }
515
+ return buf;
516
+ }
517
+ function deserializeHeader(buf) {
518
+ return {
519
+ magic: buf.readUInt8(0),
520
+ opcode: buf.readUInt8(1),
521
+ keyLength: buf.readUInt16BE(2),
522
+ extrasLength: buf.readUInt8(4),
523
+ dataType: buf.readUInt8(5),
524
+ status: buf.readUInt16BE(6),
525
+ totalBodyLength: buf.readUInt32BE(8),
526
+ opaque: buf.readUInt32BE(12),
527
+ cas: buf.subarray(16, 24)
528
+ };
529
+ }
530
+ function buildSaslPlainRequest(username, password) {
531
+ const mechanism = "PLAIN";
532
+ const authData = `\0${username}\0${password}`;
533
+ const keyBuf = Buffer.from(mechanism, "utf8");
534
+ const valueBuf = Buffer.from(authData, "utf8");
535
+ const header = serializeHeader({
536
+ magic: REQUEST_MAGIC,
537
+ opcode: OPCODE_SASL_AUTH,
538
+ keyLength: keyBuf.length,
539
+ totalBodyLength: keyBuf.length + valueBuf.length
540
+ });
541
+ return Buffer.concat([header, keyBuf, valueBuf]);
542
+ }
543
+ function buildGetRequest(key) {
544
+ const keyBuf = Buffer.from(key, "utf8");
545
+ const header = serializeHeader({
546
+ magic: REQUEST_MAGIC,
547
+ opcode: OPCODE_GET,
548
+ keyLength: keyBuf.length,
549
+ totalBodyLength: keyBuf.length
550
+ });
551
+ return Buffer.concat([header, keyBuf]);
552
+ }
553
+ function buildSetRequest(key, value, flags = 0, exptime = 0) {
554
+ const keyBuf = Buffer.from(key, "utf8");
555
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
556
+ const extras = Buffer.alloc(8);
557
+ extras.writeUInt32BE(flags, 0);
558
+ extras.writeUInt32BE(exptime, 4);
559
+ const header = serializeHeader({
560
+ magic: REQUEST_MAGIC,
561
+ opcode: OPCODE_SET,
562
+ keyLength: keyBuf.length,
563
+ extrasLength: 8,
564
+ totalBodyLength: 8 + keyBuf.length + valueBuf.length
565
+ });
566
+ return Buffer.concat([header, extras, keyBuf, valueBuf]);
567
+ }
568
+ function buildAddRequest(key, value, flags = 0, exptime = 0) {
569
+ const keyBuf = Buffer.from(key, "utf8");
570
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
571
+ const extras = Buffer.alloc(8);
572
+ extras.writeUInt32BE(flags, 0);
573
+ extras.writeUInt32BE(exptime, 4);
574
+ const header = serializeHeader({
575
+ magic: REQUEST_MAGIC,
576
+ opcode: OPCODE_ADD,
577
+ keyLength: keyBuf.length,
578
+ extrasLength: 8,
579
+ totalBodyLength: 8 + keyBuf.length + valueBuf.length
580
+ });
581
+ return Buffer.concat([header, extras, keyBuf, valueBuf]);
582
+ }
583
+ function buildReplaceRequest(key, value, flags = 0, exptime = 0) {
584
+ const keyBuf = Buffer.from(key, "utf8");
585
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
586
+ const extras = Buffer.alloc(8);
587
+ extras.writeUInt32BE(flags, 0);
588
+ extras.writeUInt32BE(exptime, 4);
589
+ const header = serializeHeader({
590
+ magic: REQUEST_MAGIC,
591
+ opcode: OPCODE_REPLACE,
592
+ keyLength: keyBuf.length,
593
+ extrasLength: 8,
594
+ totalBodyLength: 8 + keyBuf.length + valueBuf.length
595
+ });
596
+ return Buffer.concat([header, extras, keyBuf, valueBuf]);
597
+ }
598
+ function buildDeleteRequest(key) {
599
+ const keyBuf = Buffer.from(key, "utf8");
600
+ const header = serializeHeader({
601
+ magic: REQUEST_MAGIC,
602
+ opcode: OPCODE_DELETE,
603
+ keyLength: keyBuf.length,
604
+ totalBodyLength: keyBuf.length
605
+ });
606
+ return Buffer.concat([header, keyBuf]);
607
+ }
608
+ function buildIncrementRequest(key, delta = 1, initial = 0, exptime = 0) {
609
+ const keyBuf = Buffer.from(key, "utf8");
610
+ const extras = Buffer.alloc(20);
611
+ extras.writeUInt32BE(Math.floor(delta / 4294967296), 0);
612
+ extras.writeUInt32BE(delta >>> 0, 4);
613
+ extras.writeUInt32BE(Math.floor(initial / 4294967296), 8);
614
+ extras.writeUInt32BE(initial >>> 0, 12);
615
+ extras.writeUInt32BE(exptime, 16);
616
+ const header = serializeHeader({
617
+ magic: REQUEST_MAGIC,
618
+ opcode: OPCODE_INCREMENT,
619
+ keyLength: keyBuf.length,
620
+ extrasLength: 20,
621
+ totalBodyLength: 20 + keyBuf.length
622
+ });
623
+ return Buffer.concat([header, extras, keyBuf]);
624
+ }
625
+ function buildDecrementRequest(key, delta = 1, initial = 0, exptime = 0) {
626
+ const keyBuf = Buffer.from(key, "utf8");
627
+ const extras = Buffer.alloc(20);
628
+ extras.writeUInt32BE(Math.floor(delta / 4294967296), 0);
629
+ extras.writeUInt32BE(delta >>> 0, 4);
630
+ extras.writeUInt32BE(Math.floor(initial / 4294967296), 8);
631
+ extras.writeUInt32BE(initial >>> 0, 12);
632
+ extras.writeUInt32BE(exptime, 16);
633
+ const header = serializeHeader({
634
+ magic: REQUEST_MAGIC,
635
+ opcode: OPCODE_DECREMENT,
636
+ keyLength: keyBuf.length,
637
+ extrasLength: 20,
638
+ totalBodyLength: 20 + keyBuf.length
639
+ });
640
+ return Buffer.concat([header, extras, keyBuf]);
641
+ }
642
+ function buildAppendRequest(key, value) {
643
+ const keyBuf = Buffer.from(key, "utf8");
644
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
645
+ const header = serializeHeader({
646
+ magic: REQUEST_MAGIC,
647
+ opcode: OPCODE_APPEND,
648
+ keyLength: keyBuf.length,
649
+ totalBodyLength: keyBuf.length + valueBuf.length
650
+ });
651
+ return Buffer.concat([header, keyBuf, valueBuf]);
652
+ }
653
+ function buildPrependRequest(key, value) {
654
+ const keyBuf = Buffer.from(key, "utf8");
655
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
656
+ const header = serializeHeader({
657
+ magic: REQUEST_MAGIC,
658
+ opcode: OPCODE_PREPEND,
659
+ keyLength: keyBuf.length,
660
+ totalBodyLength: keyBuf.length + valueBuf.length
661
+ });
662
+ return Buffer.concat([header, keyBuf, valueBuf]);
663
+ }
664
+ function buildTouchRequest(key, exptime) {
665
+ const keyBuf = Buffer.from(key, "utf8");
666
+ const extras = Buffer.alloc(4);
667
+ extras.writeUInt32BE(exptime, 0);
668
+ const header = serializeHeader({
669
+ magic: REQUEST_MAGIC,
670
+ opcode: OPCODE_TOUCH,
671
+ keyLength: keyBuf.length,
672
+ extrasLength: 4,
673
+ totalBodyLength: 4 + keyBuf.length
674
+ });
675
+ return Buffer.concat([header, extras, keyBuf]);
676
+ }
677
+ function buildFlushRequest(exptime = 0) {
678
+ const extras = Buffer.alloc(4);
679
+ extras.writeUInt32BE(exptime, 0);
680
+ const header = serializeHeader({
681
+ magic: REQUEST_MAGIC,
682
+ opcode: OPCODE_FLUSH,
683
+ extrasLength: 4,
684
+ totalBodyLength: 4
685
+ });
686
+ return Buffer.concat([header, extras]);
687
+ }
688
+ function buildVersionRequest() {
689
+ return serializeHeader({
690
+ magic: REQUEST_MAGIC,
691
+ opcode: OPCODE_VERSION
692
+ });
693
+ }
694
+ function buildStatRequest(key) {
695
+ if (key) {
696
+ const keyBuf = Buffer.from(key, "utf8");
697
+ const header = serializeHeader({
698
+ magic: REQUEST_MAGIC,
699
+ opcode: OPCODE_STAT,
700
+ keyLength: keyBuf.length,
701
+ totalBodyLength: keyBuf.length
702
+ });
703
+ return Buffer.concat([header, keyBuf]);
704
+ }
705
+ return serializeHeader({
706
+ magic: REQUEST_MAGIC,
707
+ opcode: OPCODE_STAT
708
+ });
709
+ }
710
+ function buildQuitRequest() {
711
+ return serializeHeader({
712
+ magic: REQUEST_MAGIC,
713
+ opcode: OPCODE_QUIT
714
+ });
715
+ }
716
+ function parseGetResponse(buf) {
717
+ const header = deserializeHeader(buf);
718
+ if (header.status !== STATUS_SUCCESS) {
719
+ return { header, value: void 0, key: void 0 };
720
+ }
721
+ const extrasEnd = HEADER_SIZE + header.extrasLength;
722
+ const keyEnd = extrasEnd + header.keyLength;
723
+ const valueEnd = HEADER_SIZE + header.totalBodyLength;
724
+ const key = header.keyLength > 0 ? buf.subarray(extrasEnd, keyEnd).toString("utf8") : void 0;
725
+ const value = valueEnd > keyEnd ? buf.subarray(keyEnd, valueEnd) : void 0;
726
+ return { header, value, key };
727
+ }
728
+ function parseIncrDecrResponse(buf) {
729
+ const header = deserializeHeader(buf);
730
+ if (header.status !== STATUS_SUCCESS || header.totalBodyLength < 8) {
731
+ return { header, value: void 0 };
732
+ }
733
+ const high = buf.readUInt32BE(HEADER_SIZE);
734
+ const low = buf.readUInt32BE(HEADER_SIZE + 4);
735
+ const value = high * 4294967296 + low;
736
+ return { header, value };
737
+ }
738
+
739
+ // src/node.ts
355
740
  var MemcacheNode = class extends import_hookified.Hookified {
356
741
  _host;
357
742
  _port;
@@ -366,6 +751,9 @@ var MemcacheNode = class extends import_hookified.Hookified {
366
751
  _currentCommand = void 0;
367
752
  _multilineData = [];
368
753
  _pendingValueBytes = 0;
754
+ _sasl;
755
+ _authenticated = false;
756
+ _binaryBuffer = Buffer.alloc(0);
369
757
  constructor(host, port, options) {
370
758
  super();
371
759
  this._host = host;
@@ -374,6 +762,7 @@ var MemcacheNode = class extends import_hookified.Hookified {
374
762
  this._keepAlive = options?.keepAlive !== false;
375
763
  this._keepAliveDelay = options?.keepAliveDelay || 1e3;
376
764
  this._weight = options?.weight || 1;
765
+ this._sasl = options?.sasl;
377
766
  }
378
767
  /**
379
768
  * Get the host of this node
@@ -447,6 +836,18 @@ var MemcacheNode = class extends import_hookified.Hookified {
447
836
  get commandQueue() {
448
837
  return this._commandQueue;
449
838
  }
839
+ /**
840
+ * Get whether SASL authentication is configured
841
+ */
842
+ get hasSaslCredentials() {
843
+ return !!this._sasl?.username && !!this._sasl?.password;
844
+ }
845
+ /**
846
+ * Get whether the node is authenticated (only relevant if SASL is configured)
847
+ */
848
+ get isAuthenticated() {
849
+ return this._authenticated;
850
+ }
450
851
  /**
451
852
  * Connect to the memcache server
452
853
  */
@@ -463,14 +864,31 @@ var MemcacheNode = class extends import_hookified.Hookified {
463
864
  keepAliveInitialDelay: this._keepAliveDelay
464
865
  });
465
866
  this._socket.setTimeout(this._timeout);
466
- this._socket.setEncoding("utf8");
467
- this._socket.on("connect", () => {
867
+ if (!this._sasl) {
868
+ this._socket.setEncoding("utf8");
869
+ }
870
+ this._socket.on("connect", async () => {
468
871
  this._connected = true;
469
- this.emit("connect");
470
- resolve();
872
+ if (this._sasl) {
873
+ try {
874
+ await this.performSaslAuth();
875
+ this.emit("connect");
876
+ resolve();
877
+ } catch (error) {
878
+ this._socket?.destroy();
879
+ this._connected = false;
880
+ this._authenticated = false;
881
+ reject(error);
882
+ }
883
+ } else {
884
+ this.emit("connect");
885
+ resolve();
886
+ }
471
887
  });
472
888
  this._socket.on("data", (data) => {
473
- this.handleData(data);
889
+ if (typeof data === "string") {
890
+ this.handleData(data);
891
+ }
474
892
  });
475
893
  this._socket.on("error", (error) => {
476
894
  this.emit("error", error);
@@ -480,6 +898,7 @@ var MemcacheNode = class extends import_hookified.Hookified {
480
898
  });
481
899
  this._socket.on("close", () => {
482
900
  this._connected = false;
901
+ this._authenticated = false;
483
902
  this.emit("close");
484
903
  this.rejectPendingCommands(new Error("Connection closed"));
485
904
  });
@@ -513,9 +932,257 @@ var MemcacheNode = class extends import_hookified.Hookified {
513
932
  this._currentCommand = void 0;
514
933
  this._multilineData = [];
515
934
  this._pendingValueBytes = 0;
935
+ this._authenticated = false;
936
+ this._binaryBuffer = Buffer.alloc(0);
516
937
  }
517
938
  await this.connect();
518
939
  }
940
+ /**
941
+ * Perform SASL PLAIN authentication using the binary protocol
942
+ */
943
+ async performSaslAuth() {
944
+ if (!this._sasl || !this._socket) {
945
+ throw new Error("SASL credentials not configured");
946
+ }
947
+ const socket = this._socket;
948
+ const sasl = this._sasl;
949
+ return new Promise((resolve, reject) => {
950
+ this._binaryBuffer = Buffer.alloc(0);
951
+ const authPacket = buildSaslPlainRequest(sasl.username, sasl.password);
952
+ const binaryHandler = (data) => {
953
+ this._binaryBuffer = Buffer.concat([this._binaryBuffer, data]);
954
+ if (this._binaryBuffer.length < HEADER_SIZE) {
955
+ return;
956
+ }
957
+ const header = deserializeHeader(this._binaryBuffer);
958
+ const totalLength = HEADER_SIZE + header.totalBodyLength;
959
+ if (this._binaryBuffer.length < totalLength) {
960
+ return;
961
+ }
962
+ socket.removeListener("data", binaryHandler);
963
+ if (header.status === STATUS_SUCCESS) {
964
+ this._authenticated = true;
965
+ this.emit("authenticated");
966
+ resolve();
967
+ } else if (header.status === STATUS_AUTH_ERROR) {
968
+ const body = this._binaryBuffer.subarray(HEADER_SIZE, totalLength);
969
+ reject(
970
+ new Error(
971
+ `SASL authentication failed: ${body.toString() || "Invalid credentials"}`
972
+ )
973
+ );
974
+ } else {
975
+ reject(
976
+ new Error(
977
+ `SASL authentication failed with status: 0x${header.status.toString(16)}`
978
+ )
979
+ );
980
+ }
981
+ };
982
+ socket.on("data", binaryHandler);
983
+ socket.write(authPacket);
984
+ });
985
+ }
986
+ /**
987
+ * Send a binary protocol request and wait for response.
988
+ * Used internally for SASL-authenticated connections.
989
+ */
990
+ async binaryRequest(packet) {
991
+ if (!this._socket) {
992
+ throw new Error("Not connected");
993
+ }
994
+ const socket = this._socket;
995
+ return new Promise((resolve) => {
996
+ let buffer = Buffer.alloc(0);
997
+ const dataHandler = (data) => {
998
+ buffer = Buffer.concat([buffer, data]);
999
+ if (buffer.length < HEADER_SIZE) {
1000
+ return;
1001
+ }
1002
+ const header = deserializeHeader(buffer);
1003
+ const totalLength = HEADER_SIZE + header.totalBodyLength;
1004
+ if (buffer.length < totalLength) {
1005
+ return;
1006
+ }
1007
+ socket.removeListener("data", dataHandler);
1008
+ resolve(buffer.subarray(0, totalLength));
1009
+ };
1010
+ socket.on("data", dataHandler);
1011
+ socket.write(packet);
1012
+ });
1013
+ }
1014
+ /**
1015
+ * Binary protocol GET operation
1016
+ */
1017
+ async binaryGet(key) {
1018
+ const response = await this.binaryRequest(buildGetRequest(key));
1019
+ const { header, value } = parseGetResponse(response);
1020
+ if (header.status === STATUS_KEY_NOT_FOUND) {
1021
+ this.emit("miss", key);
1022
+ return void 0;
1023
+ }
1024
+ if (header.status !== STATUS_SUCCESS || !value) {
1025
+ return void 0;
1026
+ }
1027
+ const result = value.toString("utf8");
1028
+ this.emit("hit", key, result);
1029
+ return result;
1030
+ }
1031
+ /**
1032
+ * Binary protocol SET operation
1033
+ */
1034
+ async binarySet(key, value, exptime = 0, flags = 0) {
1035
+ const response = await this.binaryRequest(
1036
+ buildSetRequest(key, value, flags, exptime)
1037
+ );
1038
+ const header = deserializeHeader(response);
1039
+ return header.status === STATUS_SUCCESS;
1040
+ }
1041
+ /**
1042
+ * Binary protocol ADD operation
1043
+ */
1044
+ async binaryAdd(key, value, exptime = 0, flags = 0) {
1045
+ const response = await this.binaryRequest(
1046
+ buildAddRequest(key, value, flags, exptime)
1047
+ );
1048
+ const header = deserializeHeader(response);
1049
+ return header.status === STATUS_SUCCESS;
1050
+ }
1051
+ /**
1052
+ * Binary protocol REPLACE operation
1053
+ */
1054
+ async binaryReplace(key, value, exptime = 0, flags = 0) {
1055
+ const response = await this.binaryRequest(
1056
+ buildReplaceRequest(key, value, flags, exptime)
1057
+ );
1058
+ const header = deserializeHeader(response);
1059
+ return header.status === STATUS_SUCCESS;
1060
+ }
1061
+ /**
1062
+ * Binary protocol DELETE operation
1063
+ */
1064
+ async binaryDelete(key) {
1065
+ const response = await this.binaryRequest(buildDeleteRequest(key));
1066
+ const header = deserializeHeader(response);
1067
+ return header.status === STATUS_SUCCESS || header.status === STATUS_KEY_NOT_FOUND;
1068
+ }
1069
+ /**
1070
+ * Binary protocol INCREMENT operation
1071
+ */
1072
+ async binaryIncr(key, delta = 1, initial = 0, exptime = 0) {
1073
+ const response = await this.binaryRequest(
1074
+ buildIncrementRequest(key, delta, initial, exptime)
1075
+ );
1076
+ const { header, value } = parseIncrDecrResponse(response);
1077
+ if (header.status !== STATUS_SUCCESS) {
1078
+ return void 0;
1079
+ }
1080
+ return value;
1081
+ }
1082
+ /**
1083
+ * Binary protocol DECREMENT operation
1084
+ */
1085
+ async binaryDecr(key, delta = 1, initial = 0, exptime = 0) {
1086
+ const response = await this.binaryRequest(
1087
+ buildDecrementRequest(key, delta, initial, exptime)
1088
+ );
1089
+ const { header, value } = parseIncrDecrResponse(response);
1090
+ if (header.status !== STATUS_SUCCESS) {
1091
+ return void 0;
1092
+ }
1093
+ return value;
1094
+ }
1095
+ /**
1096
+ * Binary protocol APPEND operation
1097
+ */
1098
+ async binaryAppend(key, value) {
1099
+ const response = await this.binaryRequest(buildAppendRequest(key, value));
1100
+ const header = deserializeHeader(response);
1101
+ return header.status === STATUS_SUCCESS;
1102
+ }
1103
+ /**
1104
+ * Binary protocol PREPEND operation
1105
+ */
1106
+ async binaryPrepend(key, value) {
1107
+ const response = await this.binaryRequest(buildPrependRequest(key, value));
1108
+ const header = deserializeHeader(response);
1109
+ return header.status === STATUS_SUCCESS;
1110
+ }
1111
+ /**
1112
+ * Binary protocol TOUCH operation
1113
+ */
1114
+ async binaryTouch(key, exptime) {
1115
+ const response = await this.binaryRequest(buildTouchRequest(key, exptime));
1116
+ const header = deserializeHeader(response);
1117
+ return header.status === STATUS_SUCCESS;
1118
+ }
1119
+ /**
1120
+ * Binary protocol FLUSH operation
1121
+ */
1122
+ /* v8 ignore next -- @preserve */
1123
+ async binaryFlush(exptime = 0) {
1124
+ const response = await this.binaryRequest(buildFlushRequest(exptime));
1125
+ const header = deserializeHeader(response);
1126
+ return header.status === STATUS_SUCCESS;
1127
+ }
1128
+ /**
1129
+ * Binary protocol VERSION operation
1130
+ */
1131
+ async binaryVersion() {
1132
+ const response = await this.binaryRequest(buildVersionRequest());
1133
+ const header = deserializeHeader(response);
1134
+ if (header.status !== STATUS_SUCCESS) {
1135
+ return void 0;
1136
+ }
1137
+ return response.subarray(HEADER_SIZE, HEADER_SIZE + header.totalBodyLength).toString("utf8");
1138
+ }
1139
+ /**
1140
+ * Binary protocol STATS operation
1141
+ */
1142
+ async binaryStats() {
1143
+ if (!this._socket) {
1144
+ throw new Error("Not connected");
1145
+ }
1146
+ const socket = this._socket;
1147
+ const stats = {};
1148
+ return new Promise((resolve) => {
1149
+ let buffer = Buffer.alloc(0);
1150
+ const dataHandler = (data) => {
1151
+ buffer = Buffer.concat([buffer, data]);
1152
+ while (buffer.length >= HEADER_SIZE) {
1153
+ const header = deserializeHeader(buffer);
1154
+ const totalLength = HEADER_SIZE + header.totalBodyLength;
1155
+ if (buffer.length < totalLength) {
1156
+ return;
1157
+ }
1158
+ if (header.keyLength === 0 && header.totalBodyLength === 0) {
1159
+ socket.removeListener("data", dataHandler);
1160
+ resolve(stats);
1161
+ return;
1162
+ }
1163
+ if (header.opcode === OPCODE_STAT && header.status === STATUS_SUCCESS) {
1164
+ const keyStart = HEADER_SIZE;
1165
+ const keyEnd = keyStart + header.keyLength;
1166
+ const valueEnd = HEADER_SIZE + header.totalBodyLength;
1167
+ const key = buffer.subarray(keyStart, keyEnd).toString("utf8");
1168
+ const value = buffer.subarray(keyEnd, valueEnd).toString("utf8");
1169
+ stats[key] = value;
1170
+ }
1171
+ buffer = buffer.subarray(totalLength);
1172
+ }
1173
+ };
1174
+ socket.on("data", dataHandler);
1175
+ socket.write(buildStatRequest());
1176
+ });
1177
+ }
1178
+ /**
1179
+ * Binary protocol QUIT operation
1180
+ */
1181
+ async binaryQuit() {
1182
+ if (this._socket) {
1183
+ this._socket.write(buildQuitRequest());
1184
+ }
1185
+ }
519
1186
  /**
520
1187
  * Gracefully quit the connection (send quit command then disconnect)
521
1188
  */
@@ -680,7 +1347,7 @@ function createNode(host, port, options) {
680
1347
  return new MemcacheNode(host, port, options);
681
1348
  }
682
1349
 
683
- // src/index.ts
1350
+ // src/types.ts
684
1351
  var MemcacheEvents = /* @__PURE__ */ ((MemcacheEvents2) => {
685
1352
  MemcacheEvents2["CONNECT"] = "connect";
686
1353
  MemcacheEvents2["QUIT"] = "quit";
@@ -693,24 +1360,44 @@ var MemcacheEvents = /* @__PURE__ */ ((MemcacheEvents2) => {
693
1360
  MemcacheEvents2["CLOSE"] = "close";
694
1361
  return MemcacheEvents2;
695
1362
  })(MemcacheEvents || {});
1363
+
1364
+ // src/index.ts
1365
+ var defaultRetryBackoff = (_attempt, baseDelay) => baseDelay;
1366
+ var exponentialRetryBackoff = (attempt, baseDelay) => baseDelay * 2 ** attempt;
696
1367
  var Memcache = class extends import_hookified2.Hookified {
697
1368
  _nodes = [];
698
1369
  _timeout;
699
1370
  _keepAlive;
700
1371
  _keepAliveDelay;
701
1372
  _hash;
1373
+ _retries;
1374
+ _retryDelay;
1375
+ _retryBackoff;
1376
+ _retryOnlyIdempotent;
1377
+ _sasl;
702
1378
  constructor(options) {
703
1379
  super();
704
- this._hash = new KetamaHash();
705
1380
  if (typeof options === "string") {
1381
+ this._hash = new KetamaHash();
706
1382
  this._timeout = 5e3;
707
1383
  this._keepAlive = true;
708
1384
  this._keepAliveDelay = 1e3;
1385
+ this._retries = 0;
1386
+ this._retryDelay = 100;
1387
+ this._retryBackoff = defaultRetryBackoff;
1388
+ this._retryOnlyIdempotent = true;
1389
+ this._sasl = void 0;
709
1390
  this.addNode(options);
710
1391
  } else {
1392
+ this._hash = options?.hash ?? new KetamaHash();
711
1393
  this._timeout = options?.timeout || 5e3;
712
1394
  this._keepAlive = options?.keepAlive !== false;
713
1395
  this._keepAliveDelay = options?.keepAliveDelay || 1e3;
1396
+ this._retries = options?.retries ?? 0;
1397
+ this._retryDelay = options?.retryDelay ?? 100;
1398
+ this._retryBackoff = options?.retryBackoff ?? defaultRetryBackoff;
1399
+ this._retryOnlyIdempotent = options?.retryOnlyIdempotent ?? true;
1400
+ this._sasl = options?.sasl;
714
1401
  const nodeUris = options?.nodes || ["localhost:11211"];
715
1402
  for (const nodeUri of nodeUris) {
716
1403
  this.addNode(nodeUri);
@@ -815,6 +1502,74 @@ var Memcache = class extends import_hookified2.Hookified {
815
1502
  this._keepAliveDelay = value;
816
1503
  this.updateNodes();
817
1504
  }
1505
+ /**
1506
+ * Get the number of retry attempts for failed commands.
1507
+ * @returns {number}
1508
+ * @default 0
1509
+ */
1510
+ get retries() {
1511
+ return this._retries;
1512
+ }
1513
+ /**
1514
+ * Set the number of retry attempts for failed commands.
1515
+ * Set to 0 to disable retries.
1516
+ * @param {number} value
1517
+ * @default 0
1518
+ */
1519
+ set retries(value) {
1520
+ this._retries = Math.max(0, Math.floor(value));
1521
+ }
1522
+ /**
1523
+ * Get the base delay in milliseconds between retry attempts.
1524
+ * @returns {number}
1525
+ * @default 100
1526
+ */
1527
+ get retryDelay() {
1528
+ return this._retryDelay;
1529
+ }
1530
+ /**
1531
+ * Set the base delay in milliseconds between retry attempts.
1532
+ * @param {number} value
1533
+ * @default 100
1534
+ */
1535
+ set retryDelay(value) {
1536
+ this._retryDelay = Math.max(0, value);
1537
+ }
1538
+ /**
1539
+ * Get the backoff function for retry delays.
1540
+ * @returns {RetryBackoffFunction}
1541
+ * @default defaultRetryBackoff
1542
+ */
1543
+ get retryBackoff() {
1544
+ return this._retryBackoff;
1545
+ }
1546
+ /**
1547
+ * Set the backoff function for retry delays.
1548
+ * @param {RetryBackoffFunction} value
1549
+ * @default defaultRetryBackoff
1550
+ */
1551
+ set retryBackoff(value) {
1552
+ this._retryBackoff = value;
1553
+ }
1554
+ /**
1555
+ * Get whether retries are restricted to idempotent commands only.
1556
+ * @returns {boolean}
1557
+ * @default true
1558
+ */
1559
+ get retryOnlyIdempotent() {
1560
+ return this._retryOnlyIdempotent;
1561
+ }
1562
+ /**
1563
+ * Set whether retries are restricted to idempotent commands only.
1564
+ * When true (default), retries only occur for commands explicitly marked
1565
+ * as idempotent via ExecuteOptions. This prevents accidental double-execution
1566
+ * of non-idempotent operations like incr, decr, append, etc.
1567
+ * @param {boolean} value
1568
+ * @default true
1569
+ */
1570
+ set retryOnlyIdempotent(value) {
1571
+ this._retryOnlyIdempotent = value;
1572
+ }
818
1573
  /**
819
1574
  * Get an array of all MemcacheNode instances
820
1575
  * @returns {MemcacheNode[]}
@@ -848,7 +1603,8 @@ var Memcache = class extends import_hookified2.Hookified {
848
1603
  timeout: this._timeout,
849
1604
  keepAlive: this._keepAlive,
850
1605
  keepAliveDelay: this._keepAliveDelay,
851
- weight
1606
+ weight,
1607
+ sasl: this._sasl
852
1608
  });
853
1609
  } else {
854
1610
  node = uri;
@@ -1366,19 +2122,27 @@ ${valueStr}`;
1366
2122
  return nodes;
1367
2123
  }
1368
2124
  /**
1369
- * Execute a command on the specified nodes.
2125
+ * Execute a command on the specified nodes with retry support.
1370
2126
  * @param {string} command - The memcache command string to execute
1371
2127
  * @param {MemcacheNode[]} nodes - Array of MemcacheNode instances to execute on
1372
- * @param {ExecuteOptions} options - Optional execution options
2128
+ * @param {ExecuteOptions} options - Optional execution options including retry overrides
1373
2129
  * @returns {Promise<unknown[]>} Promise resolving to array of results from each node
1374
2130
  */
1375
2131
  async execute(command, nodes, options) {
2132
+ const configuredRetries = options?.retries ?? this._retries;
2133
+ const retryDelay = options?.retryDelay ?? this._retryDelay;
2134
+ const retryBackoff = options?.retryBackoff ?? this._retryBackoff;
2135
+ const isIdempotent = options?.idempotent === true;
2136
+ const maxRetries = this._retryOnlyIdempotent && !isIdempotent ? 0 : configuredRetries;
1376
2137
  const promises = nodes.map(async (node) => {
1377
- try {
1378
- return await node.command(command, options?.commandOptions);
1379
- } catch {
1380
- return void 0;
1381
- }
2138
+ return this.executeWithRetry(
2139
+ node,
2140
+ command,
2141
+ options?.commandOptions,
2142
+ maxRetries,
2143
+ retryDelay,
2144
+ retryBackoff
2145
+ );
1382
2146
  });
1383
2147
  return Promise.all(promises);
1384
2148
  }
@@ -1409,6 +2173,46 @@ ${valueStr}`;
1409
2173
  }
1410
2174
  }
1411
2175
  // Private methods
2176
+ /**
2177
+ * Sleep utility for retry delays.
2178
+ * @param {number} ms - Milliseconds to sleep
2179
+ * @returns {Promise<void>}
2180
+ */
2181
+ sleep(ms) {
2182
+ return new Promise((resolve) => setTimeout(resolve, ms));
2183
+ }
2184
+ /**
2185
+ * Execute a command on a single node with retry logic.
2186
+ * @param {MemcacheNode} node - The node to execute on
2187
+ * @param {string} command - The command string
2188
+ * @param {CommandOptions} commandOptions - Optional command options
2189
+ * @param {number} maxRetries - Maximum number of retry attempts
2190
+ * @param {number} retryDelay - Base delay between retries in milliseconds
2191
+ * @param {RetryBackoffFunction} retryBackoff - Function to calculate backoff delay
2192
+ * @returns {Promise<unknown>} Result or undefined on failure
2193
+ */
2194
+ async executeWithRetry(node, command, commandOptions, maxRetries, retryDelay, retryBackoff) {
2195
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
2196
+ try {
2197
+ return await node.command(command, commandOptions);
2198
+ } catch {
2199
+ if (attempt >= maxRetries) {
2200
+ break;
2201
+ }
2202
+ const delay = retryBackoff(attempt, retryDelay);
2203
+ if (delay > 0) {
2204
+ await this.sleep(delay);
2205
+ }
2206
+ if (!node.isConnected()) {
2207
+ try {
2208
+ await node.connect();
2209
+ } catch {
2210
+ }
2211
+ }
2212
+ }
2213
+ }
2214
+ return void 0;
2215
+ }
1412
2216
  /**
1413
2217
  * Update all nodes with current keepAlive settings
1414
2218
  */
@@ -1441,6 +2245,11 @@ var index_default = Memcache;
1441
2245
  0 && (module.exports = {
1442
2246
  Memcache,
1443
2247
  MemcacheEvents,
1444
- createNode
2248
+ MemcacheNode,
2249
+ ModulaHash,
2250
+ createNode,
2251
+ defaultRetryBackoff,
2252
+ exponentialRetryBackoff
1445
2253
  });
1446
2254
  /* v8 ignore next -- @preserve */
2255
+ /* v8 ignore next 3 -- @preserve */