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/README.md +405 -1
- package/dist/index.cjs +826 -17
- package/dist/index.d.cts +400 -46
- package/dist/index.d.ts +400 -46
- package/dist/index.js +821 -16
- package/package.json +20 -18
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.
|
|
467
|
-
|
|
867
|
+
if (!this._sasl) {
|
|
868
|
+
this._socket.setEncoding("utf8");
|
|
869
|
+
}
|
|
870
|
+
this._socket.on("connect", async () => {
|
|
468
871
|
this._connected = true;
|
|
469
|
-
this.
|
|
470
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
-
|
|
2248
|
+
MemcacheNode,
|
|
2249
|
+
ModulaHash,
|
|
2250
|
+
createNode,
|
|
2251
|
+
defaultRetryBackoff,
|
|
2252
|
+
exponentialRetryBackoff
|
|
1445
2253
|
});
|
|
1446
2254
|
/* v8 ignore next -- @preserve */
|
|
2255
|
+
/* v8 ignore next 3 -- @preserve */
|