memcache 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -6,10 +6,13 @@ interface MemcacheNodeOptions {
6
6
  keepAlive?: boolean;
7
7
  keepAliveDelay?: number;
8
8
  weight?: number;
9
+ /** SASL authentication credentials */
10
+ sasl?: SASLCredentials;
9
11
  }
10
12
  interface CommandOptions {
11
13
  isMultiline?: boolean;
12
14
  isStats?: boolean;
15
+ isConfig?: boolean;
13
16
  requestedKeys?: string[];
14
17
  }
15
18
  type CommandQueueItem = {
@@ -18,6 +21,7 @@ type CommandQueueItem = {
18
21
  reject: (reason?: any) => void;
19
22
  isMultiline?: boolean;
20
23
  isStats?: boolean;
24
+ isConfig?: boolean;
21
25
  requestedKeys?: string[];
22
26
  foundKeys?: string[];
23
27
  };
@@ -39,6 +43,9 @@ declare class MemcacheNode extends Hookified {
39
43
  private _currentCommand;
40
44
  private _multilineData;
41
45
  private _pendingValueBytes;
46
+ private _sasl;
47
+ private _authenticated;
48
+ private _binaryBuffer;
42
49
  constructor(host: string, port: number, options?: MemcacheNodeOptions);
43
50
  /**
44
51
  * Get the host of this node
@@ -88,6 +95,14 @@ declare class MemcacheNode extends Hookified {
88
95
  * Get the command queue
89
96
  */
90
97
  get commandQueue(): CommandQueueItem[];
98
+ /**
99
+ * Get whether SASL authentication is configured
100
+ */
101
+ get hasSaslCredentials(): boolean;
102
+ /**
103
+ * Get whether the node is authenticated (only relevant if SASL is configured)
104
+ */
105
+ get isAuthenticated(): boolean;
91
106
  /**
92
107
  * Connect to the memcache server
93
108
  */
@@ -100,6 +115,71 @@ declare class MemcacheNode extends Hookified {
100
115
  * Reconnect to the memcache server by disconnecting and connecting again
101
116
  */
102
117
  reconnect(): Promise<void>;
118
+ /**
119
+ * Perform SASL PLAIN authentication using the binary protocol
120
+ */
121
+ private performSaslAuth;
122
+ /**
123
+ * Send a binary protocol request and wait for response.
124
+ * Used internally for SASL-authenticated connections.
125
+ */
126
+ private binaryRequest;
127
+ /**
128
+ * Binary protocol GET operation
129
+ */
130
+ binaryGet(key: string): Promise<string | undefined>;
131
+ /**
132
+ * Binary protocol SET operation
133
+ */
134
+ binarySet(key: string, value: string, exptime?: number, flags?: number): Promise<boolean>;
135
+ /**
136
+ * Binary protocol ADD operation
137
+ */
138
+ binaryAdd(key: string, value: string, exptime?: number, flags?: number): Promise<boolean>;
139
+ /**
140
+ * Binary protocol REPLACE operation
141
+ */
142
+ binaryReplace(key: string, value: string, exptime?: number, flags?: number): Promise<boolean>;
143
+ /**
144
+ * Binary protocol DELETE operation
145
+ */
146
+ binaryDelete(key: string): Promise<boolean>;
147
+ /**
148
+ * Binary protocol INCREMENT operation
149
+ */
150
+ binaryIncr(key: string, delta?: number, initial?: number, exptime?: number): Promise<number | undefined>;
151
+ /**
152
+ * Binary protocol DECREMENT operation
153
+ */
154
+ binaryDecr(key: string, delta?: number, initial?: number, exptime?: number): Promise<number | undefined>;
155
+ /**
156
+ * Binary protocol APPEND operation
157
+ */
158
+ binaryAppend(key: string, value: string): Promise<boolean>;
159
+ /**
160
+ * Binary protocol PREPEND operation
161
+ */
162
+ binaryPrepend(key: string, value: string): Promise<boolean>;
163
+ /**
164
+ * Binary protocol TOUCH operation
165
+ */
166
+ binaryTouch(key: string, exptime: number): Promise<boolean>;
167
+ /**
168
+ * Binary protocol FLUSH operation
169
+ */
170
+ binaryFlush(exptime?: number): Promise<boolean>;
171
+ /**
172
+ * Binary protocol VERSION operation
173
+ */
174
+ binaryVersion(): Promise<string | undefined>;
175
+ /**
176
+ * Binary protocol STATS operation
177
+ */
178
+ binaryStats(): Promise<Record<string, string>>;
179
+ /**
180
+ * Binary protocol QUIT operation
181
+ */
182
+ binaryQuit(): Promise<void>;
103
183
  /**
104
184
  * Gracefully quit the connection (send quit command then disconnect)
105
185
  */
@@ -137,6 +217,24 @@ declare class MemcacheNode extends Hookified {
137
217
  */
138
218
  declare function createNode(host: string, port: number, options?: MemcacheNodeOptions): MemcacheNode;
139
219
 
220
+ /**
221
+ * SASL authentication credentials for connecting to a secured memcache server.
222
+ */
223
+ interface SASLCredentials {
224
+ /**
225
+ * Username for SASL authentication
226
+ */
227
+ username: string;
228
+ /**
229
+ * Password for SASL authentication
230
+ */
231
+ password: string;
232
+ /**
233
+ * SASL mechanism to use (default: 'PLAIN')
234
+ * Currently only 'PLAIN' is supported.
235
+ */
236
+ mechanism?: "PLAIN";
237
+ }
140
238
  declare enum MemcacheEvents {
141
239
  CONNECT = "connect",
142
240
  QUIT = "quit",
@@ -146,8 +244,18 @@ declare enum MemcacheEvents {
146
244
  WARN = "warn",
147
245
  INFO = "info",
148
246
  TIMEOUT = "timeout",
149
- CLOSE = "close"
247
+ CLOSE = "close",
248
+ AUTO_DISCOVER = "autoDiscover",
249
+ AUTO_DISCOVER_ERROR = "autoDiscoverError",
250
+ AUTO_DISCOVER_UPDATE = "autoDiscoverUpdate"
150
251
  }
252
+ /**
253
+ * Function to calculate delay between retry attempts.
254
+ * @param attempt - The current attempt number (0-indexed)
255
+ * @param baseDelay - The base delay in milliseconds
256
+ * @returns The delay in milliseconds before the next retry
257
+ */
258
+ type RetryBackoffFunction = (attempt: number, baseDelay: number) => number;
151
259
  interface HashProvider {
152
260
  name: string;
153
261
  nodes: Array<MemcacheNode>;
@@ -183,20 +291,327 @@ interface MemcacheOptions {
183
291
  * on the number of nodes and hashing. By default it uses KetamaHash as the provider
184
292
  */
185
293
  hash?: HashProvider;
294
+ /**
295
+ * The number of retry attempts for failed commands.
296
+ * Set to 0 to disable retries.
297
+ * @default 0
298
+ */
299
+ retries?: number;
300
+ /**
301
+ * The base delay in milliseconds between retry attempts.
302
+ * @default 100
303
+ */
304
+ retryDelay?: number;
305
+ /**
306
+ * Function to calculate backoff delay between retries.
307
+ * Receives (attempt, baseDelay) and returns delay in ms.
308
+ * @default defaultRetryBackoff (fixed delay)
309
+ */
310
+ retryBackoff?: RetryBackoffFunction;
311
+ /**
312
+ * When true, retries are only performed for commands marked as idempotent.
313
+ * This prevents accidental double-execution of non-idempotent operations
314
+ * (like incr, decr, append) if the server applies the command but the
315
+ * client doesn't receive the response before a timeout/disconnect.
316
+ * @default true
317
+ */
318
+ retryOnlyIdempotent?: boolean;
319
+ /**
320
+ * SASL authentication credentials for all nodes.
321
+ * When provided, nodes will authenticate using SASL PLAIN mechanism
322
+ * before accepting commands.
323
+ */
324
+ sasl?: SASLCredentials;
325
+ /**
326
+ * AWS ElastiCache Auto Discovery configuration.
327
+ * When enabled, the client will periodically poll the configuration endpoint
328
+ * to detect cluster topology changes and automatically update the node list.
329
+ */
330
+ autoDiscover?: AutoDiscoverOptions;
186
331
  }
187
332
  interface MemcacheStats {
188
333
  [key: string]: string;
189
334
  }
335
+ /**
336
+ * Configuration for AWS ElastiCache Auto Discovery.
337
+ * When enabled, the client connects to a configuration endpoint and periodically
338
+ * polls for cluster topology changes, automatically adding/removing nodes.
339
+ */
340
+ interface AutoDiscoverOptions {
341
+ /**
342
+ * Enable auto discovery of cluster nodes.
343
+ */
344
+ enabled: boolean;
345
+ /**
346
+ * How often to poll for topology changes, in milliseconds.
347
+ * @default 60000
348
+ */
349
+ pollingInterval?: number;
350
+ /**
351
+ * The configuration endpoint to use for discovery.
352
+ * This is typically the .cfg endpoint from ElastiCache.
353
+ * If not specified, the first node in the nodes array will be used.
354
+ */
355
+ configEndpoint?: string;
356
+ /**
357
+ * Use the legacy `get AmazonElastiCache:cluster` command
358
+ * instead of `config get cluster` (for engine versions < 1.4.14).
359
+ * @default false
360
+ */
361
+ useLegacyCommand?: boolean;
362
+ }
363
+ /**
364
+ * Represents a discovered node from the ElastiCache cluster configuration.
365
+ */
366
+ interface DiscoveredNode {
367
+ /** The hostname of the node */
368
+ hostname: string;
369
+ /** The IP address of the node (may be empty string) */
370
+ ip: string;
371
+ /** The port number of the node */
372
+ port: number;
373
+ }
374
+ /**
375
+ * Represents the parsed result of an ElastiCache cluster config response.
376
+ */
377
+ interface ClusterConfig {
378
+ /** The config version number (increments on topology changes) */
379
+ version: number;
380
+ /** The list of discovered nodes */
381
+ nodes: DiscoveredNode[];
382
+ }
190
383
  interface ExecuteOptions {
191
384
  /** Command options passed to node.command() */
192
385
  commandOptions?: CommandOptions;
386
+ /**
387
+ * Override the number of retries for this specific execution.
388
+ * If undefined, uses the instance-level retries setting.
389
+ */
390
+ retries?: number;
391
+ /**
392
+ * Override the retry delay for this specific execution.
393
+ * If undefined, uses the instance-level retryDelay setting.
394
+ */
395
+ retryDelay?: number;
396
+ /**
397
+ * Override the backoff function for this specific execution.
398
+ * If undefined, uses the instance-level retryBackoff setting.
399
+ */
400
+ retryBackoff?: RetryBackoffFunction;
401
+ /**
402
+ * Mark this command as idempotent, allowing retries even when
403
+ * retryOnlyIdempotent is true. Set this for read operations (get, gets)
404
+ * or operations that are safe to repeat (set with same value).
405
+ * @default false
406
+ */
407
+ idempotent?: boolean;
408
+ }
409
+
410
+ interface AutoDiscoveryOptions {
411
+ configEndpoint: string;
412
+ pollingInterval: number;
413
+ useLegacyCommand: boolean;
414
+ timeout: number;
415
+ keepAlive: boolean;
416
+ keepAliveDelay: number;
417
+ sasl?: SASLCredentials;
418
+ }
419
+ /**
420
+ * Handles AWS ElastiCache Auto Discovery for memcache clusters.
421
+ * Connects to a configuration endpoint, periodically polls for cluster
422
+ * topology changes, and emits events when nodes are added or removed.
423
+ */
424
+ declare class AutoDiscovery extends Hookified {
425
+ private _configEndpoint;
426
+ private _pollingInterval;
427
+ private _useLegacyCommand;
428
+ private _configVersion;
429
+ private _pollingTimer;
430
+ private _configNode;
431
+ private _timeout;
432
+ private _keepAlive;
433
+ private _keepAliveDelay;
434
+ private _sasl;
435
+ private _isRunning;
436
+ private _isPolling;
437
+ constructor(options: AutoDiscoveryOptions);
438
+ /** Current config version. -1 means no config has been fetched yet. */
439
+ get configVersion(): number;
440
+ /** Whether auto discovery is currently running. */
441
+ get isRunning(): boolean;
442
+ /** The configuration endpoint being used. */
443
+ get configEndpoint(): string;
444
+ /**
445
+ * Start the auto discovery process.
446
+ * Performs an initial discovery, then starts the polling timer.
447
+ */
448
+ start(): Promise<ClusterConfig>;
449
+ /**
450
+ * Stop the auto discovery process.
451
+ */
452
+ stop(): Promise<void>;
453
+ /**
454
+ * Perform a single discovery cycle.
455
+ * Returns the ClusterConfig if the version has changed, or undefined if unchanged.
456
+ */
457
+ discover(): Promise<ClusterConfig | undefined>;
458
+ /**
459
+ * Parse the raw response data from a config get cluster command.
460
+ * The raw data is the value content between the CONFIG/VALUE header and END.
461
+ * Format: "<version>\n<host1>|<ip1>|<port1> <host2>|<ip2>|<port2>\n"
462
+ */
463
+ static parseConfigResponse(rawData: string[]): ClusterConfig;
464
+ /**
465
+ * Parse a single node entry in the format "hostname|ip|port".
466
+ */
467
+ static parseNodeEntry(entry: string): DiscoveredNode;
468
+ /**
469
+ * Build a node ID from a DiscoveredNode.
470
+ * Prefers IP when available, falls back to hostname.
471
+ */
472
+ static nodeId(node: DiscoveredNode): string;
473
+ private ensureConfigNode;
474
+ private fetchConfig;
475
+ private poll;
476
+ private parseEndpoint;
477
+ }
478
+
479
+ /**
480
+ * Function that returns an unsigned 32-bit hash of the input.
481
+ */
482
+ type HashFunction = (input: Buffer) => number;
483
+ /**
484
+ * A distribution hash implementation using modulo-based hashing.
485
+ * This class provides a simple key distribution strategy where keys are
486
+ * assigned to nodes using `hash(key) % nodeCount`.
487
+ *
488
+ * Unlike consistent hashing (Ketama), modulo hashing redistributes all keys
489
+ * when nodes are added or removed. This makes it suitable for:
490
+ * - Fixed-size clusters
491
+ * - Testing environments
492
+ * - Scenarios where simplicity is preferred over minimal redistribution
493
+ *
494
+ * @example
495
+ * ```typescript
496
+ * const distribution = new ModulaHash();
497
+ * distribution.addNode(node1);
498
+ * distribution.addNode(node2);
499
+ * const targetNode = distribution.getNodesByKey('my-key')[0];
500
+ * ```
501
+ */
502
+ declare class ModulaHash implements HashProvider {
503
+ /** The name of this distribution strategy */
504
+ readonly name = "modula";
505
+ /** The hash function used to compute key hashes */
506
+ private readonly hashFn;
507
+ /** Map of node IDs to MemcacheNode instances */
508
+ private nodeMap;
509
+ /**
510
+ * Weighted list of node IDs for modulo distribution.
511
+ * Nodes with higher weights appear multiple times.
512
+ */
513
+ private nodeList;
514
+ /**
515
+ * Creates a new ModulaHash instance.
516
+ *
517
+ * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
518
+ *
519
+ * @example
520
+ * ```typescript
521
+ * // Use default SHA-1 hashing
522
+ * const distribution = new ModulaHash();
523
+ *
524
+ * // Use MD5 hashing
525
+ * const distribution = new ModulaHash('md5');
526
+ *
527
+ * // Use custom hash function
528
+ * const distribution = new ModulaHash((buf) => buf.readUInt32BE(0));
529
+ * ```
530
+ */
531
+ constructor(hashFn?: string | HashFunction);
532
+ /**
533
+ * Gets all nodes in the distribution.
534
+ * @returns Array of all MemcacheNode instances
535
+ */
536
+ get nodes(): Array<MemcacheNode>;
537
+ /**
538
+ * Adds a node to the distribution with its weight.
539
+ * Weight determines how many times the node appears in the distribution list.
540
+ *
541
+ * @param node - The MemcacheNode to add
542
+ *
543
+ * @example
544
+ * ```typescript
545
+ * const node = new MemcacheNode('localhost', 11211, { weight: 2 });
546
+ * distribution.addNode(node);
547
+ * ```
548
+ */
549
+ addNode(node: MemcacheNode): void;
550
+ /**
551
+ * Removes a node from the distribution by its ID.
552
+ *
553
+ * @param id - The node ID (e.g., "localhost:11211")
554
+ *
555
+ * @example
556
+ * ```typescript
557
+ * distribution.removeNode('localhost:11211');
558
+ * ```
559
+ */
560
+ removeNode(id: string): void;
561
+ /**
562
+ * Gets a specific node by its ID.
563
+ *
564
+ * @param id - The node ID (e.g., "localhost:11211")
565
+ * @returns The MemcacheNode if found, undefined otherwise
566
+ *
567
+ * @example
568
+ * ```typescript
569
+ * const node = distribution.getNode('localhost:11211');
570
+ * if (node) {
571
+ * console.log(`Found node: ${node.uri}`);
572
+ * }
573
+ * ```
574
+ */
575
+ getNode(id: string): MemcacheNode | undefined;
576
+ /**
577
+ * Gets the nodes responsible for a given key using modulo hashing.
578
+ * Uses `hash(key) % nodeCount` to determine the target node.
579
+ *
580
+ * @param key - The cache key to find the responsible node for
581
+ * @returns Array containing the responsible node(s), empty if no nodes available
582
+ *
583
+ * @example
584
+ * ```typescript
585
+ * const nodes = distribution.getNodesByKey('user:123');
586
+ * if (nodes.length > 0) {
587
+ * console.log(`Key will be stored on: ${nodes[0].id}`);
588
+ * }
589
+ * ```
590
+ */
591
+ getNodesByKey(key: string): Array<MemcacheNode>;
193
592
  }
593
+
594
+ /**
595
+ * Default backoff function - returns fixed delay
596
+ */
597
+ declare const defaultRetryBackoff: RetryBackoffFunction;
598
+ /**
599
+ * Exponential backoff function - doubles delay each attempt
600
+ */
601
+ declare const exponentialRetryBackoff: RetryBackoffFunction;
194
602
  declare class Memcache extends Hookified {
195
603
  private _nodes;
196
604
  private _timeout;
197
605
  private _keepAlive;
198
606
  private _keepAliveDelay;
199
607
  private _hash;
608
+ private _retries;
609
+ private _retryDelay;
610
+ private _retryBackoff;
611
+ private _retryOnlyIdempotent;
612
+ private _sasl;
613
+ private _autoDiscovery;
614
+ private _autoDiscoverOptions;
200
615
  constructor(options?: string | MemcacheOptions);
201
616
  /**
202
617
  * Get the list of nodes
@@ -274,6 +689,58 @@ declare class Memcache extends Hookified {
274
689
  * @default 1000
275
690
  */
276
691
  set keepAliveDelay(value: number);
692
+ /**
693
+ * Get the number of retry attempts for failed commands.
694
+ * @returns {number}
695
+ * @default 0
696
+ */
697
+ get retries(): number;
698
+ /**
699
+ * Set the number of retry attempts for failed commands.
700
+ * Set to 0 to disable retries.
701
+ * @param {number} value
702
+ * @default 0
703
+ */
704
+ set retries(value: number);
705
+ /**
706
+ * Get the base delay in milliseconds between retry attempts.
707
+ * @returns {number}
708
+ * @default 100
709
+ */
710
+ get retryDelay(): number;
711
+ /**
712
+ * Set the base delay in milliseconds between retry attempts.
713
+ * @param {number} value
714
+ * @default 100
715
+ */
716
+ set retryDelay(value: number);
717
+ /**
718
+ * Get the backoff function for retry delays.
719
+ * @returns {RetryBackoffFunction}
720
+ * @default defaultRetryBackoff
721
+ */
722
+ get retryBackoff(): RetryBackoffFunction;
723
+ /**
724
+ * Set the backoff function for retry delays.
725
+ * @param {RetryBackoffFunction} value
726
+ * @default defaultRetryBackoff
727
+ */
728
+ set retryBackoff(value: RetryBackoffFunction);
729
+ /**
730
+ * Get whether retries are restricted to idempotent commands only.
731
+ * @returns {boolean}
732
+ * @default true
733
+ */
734
+ get retryOnlyIdempotent(): boolean;
735
+ /**
736
+ * Set whether retries are restricted to idempotent commands only.
737
+ * When true (default), retries only occur for commands explicitly marked
738
+ * as idempotent via ExecuteOptions. This prevents accidental double-execution
739
+ * of non-idempotent operations like incr, decr, append, etc.
740
+ * @param {boolean} value
741
+ * @default true
742
+ */
743
+ set retryOnlyIdempotent(value: boolean);
277
744
  /**
278
745
  * Get an array of all MemcacheNode instances
279
746
  * @returns {MemcacheNode[]}
@@ -479,10 +946,10 @@ declare class Memcache extends Hookified {
479
946
  */
480
947
  getNodesByKey(key: string): Promise<Array<MemcacheNode>>;
481
948
  /**
482
- * Execute a command on the specified nodes.
949
+ * Execute a command on the specified nodes with retry support.
483
950
  * @param {string} command - The memcache command string to execute
484
951
  * @param {MemcacheNode[]} nodes - Array of MemcacheNode instances to execute on
485
- * @param {ExecuteOptions} options - Optional execution options
952
+ * @param {ExecuteOptions} options - Optional execution options including retry overrides
486
953
  * @returns {Promise<unknown[]>} Promise resolving to array of results from each node
487
954
  */
488
955
  execute(command: string, nodes: MemcacheNode[], options?: ExecuteOptions): Promise<unknown[]>;
@@ -500,6 +967,23 @@ declare class Memcache extends Hookified {
500
967
  * ```
501
968
  */
502
969
  validateKey(key: string): void;
970
+ /**
971
+ * Sleep utility for retry delays.
972
+ * @param {number} ms - Milliseconds to sleep
973
+ * @returns {Promise<void>}
974
+ */
975
+ private sleep;
976
+ /**
977
+ * Execute a command on a single node with retry logic.
978
+ * @param {MemcacheNode} node - The node to execute on
979
+ * @param {string} command - The command string
980
+ * @param {CommandOptions} commandOptions - Optional command options
981
+ * @param {number} maxRetries - Maximum number of retry attempts
982
+ * @param {number} retryDelay - Base delay between retries in milliseconds
983
+ * @param {RetryBackoffFunction} retryBackoff - Function to calculate backoff delay
984
+ * @returns {Promise<unknown>} Result or undefined on failure
985
+ */
986
+ private executeWithRetry;
503
987
  /**
504
988
  * Update all nodes with current keepAlive settings
505
989
  */
@@ -508,6 +992,8 @@ declare class Memcache extends Hookified {
508
992
  * Forward events from a MemcacheNode to the Memcache instance
509
993
  */
510
994
  private forwardNodeEvents;
995
+ private startAutoDiscovery;
996
+ private applyClusterConfig;
511
997
  }
512
998
 
513
- export { type ExecuteOptions, type HashProvider, Memcache, MemcacheEvents, type MemcacheOptions, type MemcacheStats, createNode, Memcache as default };
999
+ export { type AutoDiscoverOptions, AutoDiscovery, type ClusterConfig, type DiscoveredNode, type ExecuteOptions, type HashProvider, Memcache, MemcacheEvents, MemcacheNode, type MemcacheOptions, type MemcacheStats, ModulaHash, type RetryBackoffFunction, type SASLCredentials, createNode, Memcache as default, defaultRetryBackoff, exponentialRetryBackoff };