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/README.md +405 -1
- package/dist/index.cjs +856 -110
- package/dist/index.d.cts +406 -40
- package/dist/index.d.ts +406 -40
- package/dist/index.js +851 -109
- 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;
|
|
@@ -1063,16 +1819,8 @@ var Memcache = class extends import_hookified2.Hookified {
|
|
|
1063
1819
|
const command = `cas ${key} ${flags} ${exptime} ${bytes} ${casToken}\r
|
|
1064
1820
|
${valueStr}`;
|
|
1065
1821
|
const nodes = await this.getNodesByKey(key);
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
const result = await node.command(command);
|
|
1069
|
-
return result === "STORED";
|
|
1070
|
-
} catch {
|
|
1071
|
-
return false;
|
|
1072
|
-
}
|
|
1073
|
-
});
|
|
1074
|
-
const results = await Promise.all(promises);
|
|
1075
|
-
const success = results.every((result) => result === true);
|
|
1822
|
+
const results = await this.execute(command, nodes);
|
|
1823
|
+
const success = results.every((result) => result === "STORED");
|
|
1076
1824
|
await this.afterHook("cas", {
|
|
1077
1825
|
key,
|
|
1078
1826
|
value,
|
|
@@ -1101,16 +1849,8 @@ ${valueStr}`;
|
|
|
1101
1849
|
const command = `set ${key} ${flags} ${exptime} ${bytes}\r
|
|
1102
1850
|
${valueStr}`;
|
|
1103
1851
|
const nodes = await this.getNodesByKey(key);
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
const result = await node.command(command);
|
|
1107
|
-
return result === "STORED";
|
|
1108
|
-
} catch {
|
|
1109
|
-
return false;
|
|
1110
|
-
}
|
|
1111
|
-
});
|
|
1112
|
-
const results = await Promise.all(promises);
|
|
1113
|
-
const success = results.every((result) => result === true);
|
|
1852
|
+
const results = await this.execute(command, nodes);
|
|
1853
|
+
const success = results.every((result) => result === "STORED");
|
|
1114
1854
|
await this.afterHook("set", { key, value, exptime, flags, success });
|
|
1115
1855
|
return success;
|
|
1116
1856
|
}
|
|
@@ -1132,16 +1872,8 @@ ${valueStr}`;
|
|
|
1132
1872
|
const command = `add ${key} ${flags} ${exptime} ${bytes}\r
|
|
1133
1873
|
${valueStr}`;
|
|
1134
1874
|
const nodes = await this.getNodesByKey(key);
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
const result = await node.command(command);
|
|
1138
|
-
return result === "STORED";
|
|
1139
|
-
} catch {
|
|
1140
|
-
return false;
|
|
1141
|
-
}
|
|
1142
|
-
});
|
|
1143
|
-
const results = await Promise.all(promises);
|
|
1144
|
-
const success = results.every((result) => result === true);
|
|
1875
|
+
const results = await this.execute(command, nodes);
|
|
1876
|
+
const success = results.every((result) => result === "STORED");
|
|
1145
1877
|
await this.afterHook("add", { key, value, exptime, flags, success });
|
|
1146
1878
|
return success;
|
|
1147
1879
|
}
|
|
@@ -1163,16 +1895,8 @@ ${valueStr}`;
|
|
|
1163
1895
|
const command = `replace ${key} ${flags} ${exptime} ${bytes}\r
|
|
1164
1896
|
${valueStr}`;
|
|
1165
1897
|
const nodes = await this.getNodesByKey(key);
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
const result = await node.command(command);
|
|
1169
|
-
return result === "STORED";
|
|
1170
|
-
} catch {
|
|
1171
|
-
return false;
|
|
1172
|
-
}
|
|
1173
|
-
});
|
|
1174
|
-
const results = await Promise.all(promises);
|
|
1175
|
-
const success = results.every((result) => result === true);
|
|
1898
|
+
const results = await this.execute(command, nodes);
|
|
1899
|
+
const success = results.every((result) => result === "STORED");
|
|
1176
1900
|
await this.afterHook("replace", { key, value, exptime, flags, success });
|
|
1177
1901
|
return success;
|
|
1178
1902
|
}
|
|
@@ -1192,16 +1916,8 @@ ${valueStr}`;
|
|
|
1192
1916
|
const command = `append ${key} 0 0 ${bytes}\r
|
|
1193
1917
|
${valueStr}`;
|
|
1194
1918
|
const nodes = await this.getNodesByKey(key);
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
const result = await node.command(command);
|
|
1198
|
-
return result === "STORED";
|
|
1199
|
-
} catch {
|
|
1200
|
-
return false;
|
|
1201
|
-
}
|
|
1202
|
-
});
|
|
1203
|
-
const results = await Promise.all(promises);
|
|
1204
|
-
const success = results.every((result) => result === true);
|
|
1919
|
+
const results = await this.execute(command, nodes);
|
|
1920
|
+
const success = results.every((result) => result === "STORED");
|
|
1205
1921
|
await this.afterHook("append", { key, value, success });
|
|
1206
1922
|
return success;
|
|
1207
1923
|
}
|
|
@@ -1221,16 +1937,8 @@ ${valueStr}`;
|
|
|
1221
1937
|
const command = `prepend ${key} 0 0 ${bytes}\r
|
|
1222
1938
|
${valueStr}`;
|
|
1223
1939
|
const nodes = await this.getNodesByKey(key);
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
const result = await node.command(command);
|
|
1227
|
-
return result === "STORED";
|
|
1228
|
-
} catch {
|
|
1229
|
-
return false;
|
|
1230
|
-
}
|
|
1231
|
-
});
|
|
1232
|
-
const results = await Promise.all(promises);
|
|
1233
|
-
const success = results.every((result) => result === true);
|
|
1940
|
+
const results = await this.execute(command, nodes);
|
|
1941
|
+
const success = results.every((result) => result === "STORED");
|
|
1234
1942
|
await this.afterHook("prepend", { key, value, success });
|
|
1235
1943
|
return success;
|
|
1236
1944
|
}
|
|
@@ -1245,16 +1953,8 @@ ${valueStr}`;
|
|
|
1245
1953
|
await this.beforeHook("delete", { key });
|
|
1246
1954
|
this.validateKey(key);
|
|
1247
1955
|
const nodes = await this.getNodesByKey(key);
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
const result = await node.command(`delete ${key}`);
|
|
1251
|
-
return result === "DELETED";
|
|
1252
|
-
} catch {
|
|
1253
|
-
return false;
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
const results = await Promise.all(promises);
|
|
1257
|
-
const success = results.every((result) => result === true);
|
|
1956
|
+
const results = await this.execute(`delete ${key}`, nodes);
|
|
1957
|
+
const success = results.every((result) => result === "DELETED");
|
|
1258
1958
|
await this.afterHook("delete", { key, success });
|
|
1259
1959
|
return success;
|
|
1260
1960
|
}
|
|
@@ -1270,16 +1970,8 @@ ${valueStr}`;
|
|
|
1270
1970
|
await this.beforeHook("incr", { key, value });
|
|
1271
1971
|
this.validateKey(key);
|
|
1272
1972
|
const nodes = await this.getNodesByKey(key);
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
const result = await node.command(`incr ${key} ${value}`);
|
|
1276
|
-
return typeof result === "number" ? result : void 0;
|
|
1277
|
-
} catch {
|
|
1278
|
-
return void 0;
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1281
|
-
const results = await Promise.all(promises);
|
|
1282
|
-
const newValue = results.find((v) => v !== void 0);
|
|
1973
|
+
const results = await this.execute(`incr ${key} ${value}`, nodes);
|
|
1974
|
+
const newValue = results.find((v) => typeof v === "number");
|
|
1283
1975
|
await this.afterHook("incr", { key, value, newValue });
|
|
1284
1976
|
return newValue;
|
|
1285
1977
|
}
|
|
@@ -1295,16 +1987,8 @@ ${valueStr}`;
|
|
|
1295
1987
|
await this.beforeHook("decr", { key, value });
|
|
1296
1988
|
this.validateKey(key);
|
|
1297
1989
|
const nodes = await this.getNodesByKey(key);
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
const result = await node.command(`decr ${key} ${value}`);
|
|
1301
|
-
return typeof result === "number" ? result : void 0;
|
|
1302
|
-
} catch {
|
|
1303
|
-
return void 0;
|
|
1304
|
-
}
|
|
1305
|
-
});
|
|
1306
|
-
const results = await Promise.all(promises);
|
|
1307
|
-
const newValue = results.find((v) => v !== void 0);
|
|
1990
|
+
const results = await this.execute(`decr ${key} ${value}`, nodes);
|
|
1991
|
+
const newValue = results.find((v) => typeof v === "number");
|
|
1308
1992
|
await this.afterHook("decr", { key, value, newValue });
|
|
1309
1993
|
return newValue;
|
|
1310
1994
|
}
|
|
@@ -1320,16 +2004,8 @@ ${valueStr}`;
|
|
|
1320
2004
|
await this.beforeHook("touch", { key, exptime });
|
|
1321
2005
|
this.validateKey(key);
|
|
1322
2006
|
const nodes = await this.getNodesByKey(key);
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
const result = await node.command(`touch ${key} ${exptime}`);
|
|
1326
|
-
return result === "TOUCHED";
|
|
1327
|
-
} catch {
|
|
1328
|
-
return false;
|
|
1329
|
-
}
|
|
1330
|
-
});
|
|
1331
|
-
const results = await Promise.all(promises);
|
|
1332
|
-
const success = results.every((result) => result === true);
|
|
2007
|
+
const results = await this.execute(`touch ${key} ${exptime}`, nodes);
|
|
2008
|
+
const success = results.every((result) => result === "TOUCHED");
|
|
1333
2009
|
await this.afterHook("touch", { key, exptime, success });
|
|
1334
2010
|
return success;
|
|
1335
2011
|
}
|
|
@@ -1445,6 +2121,31 @@ ${valueStr}`;
|
|
|
1445
2121
|
}
|
|
1446
2122
|
return nodes;
|
|
1447
2123
|
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Execute a command on the specified nodes with retry support.
|
|
2126
|
+
* @param {string} command - The memcache command string to execute
|
|
2127
|
+
* @param {MemcacheNode[]} nodes - Array of MemcacheNode instances to execute on
|
|
2128
|
+
* @param {ExecuteOptions} options - Optional execution options including retry overrides
|
|
2129
|
+
* @returns {Promise<unknown[]>} Promise resolving to array of results from each node
|
|
2130
|
+
*/
|
|
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;
|
|
2137
|
+
const promises = nodes.map(async (node) => {
|
|
2138
|
+
return this.executeWithRetry(
|
|
2139
|
+
node,
|
|
2140
|
+
command,
|
|
2141
|
+
options?.commandOptions,
|
|
2142
|
+
maxRetries,
|
|
2143
|
+
retryDelay,
|
|
2144
|
+
retryBackoff
|
|
2145
|
+
);
|
|
2146
|
+
});
|
|
2147
|
+
return Promise.all(promises);
|
|
2148
|
+
}
|
|
1448
2149
|
/**
|
|
1449
2150
|
* Validates a Memcache key according to protocol requirements.
|
|
1450
2151
|
* @param {string} key - The key to validate
|
|
@@ -1472,6 +2173,46 @@ ${valueStr}`;
|
|
|
1472
2173
|
}
|
|
1473
2174
|
}
|
|
1474
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
|
+
}
|
|
1475
2216
|
/**
|
|
1476
2217
|
* Update all nodes with current keepAlive settings
|
|
1477
2218
|
*/
|
|
@@ -1504,6 +2245,11 @@ var index_default = Memcache;
|
|
|
1504
2245
|
0 && (module.exports = {
|
|
1505
2246
|
Memcache,
|
|
1506
2247
|
MemcacheEvents,
|
|
1507
|
-
|
|
2248
|
+
MemcacheNode,
|
|
2249
|
+
ModulaHash,
|
|
2250
|
+
createNode,
|
|
2251
|
+
defaultRetryBackoff,
|
|
2252
|
+
exponentialRetryBackoff
|
|
1508
2253
|
});
|
|
1509
2254
|
/* v8 ignore next -- @preserve */
|
|
2255
|
+
/* v8 ignore next 3 -- @preserve */
|