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