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.cjs CHANGED
@@ -20,420 +20,375 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AutoDiscovery: () => AutoDiscovery,
23
24
  Memcache: () => Memcache,
24
25
  MemcacheEvents: () => MemcacheEvents,
26
+ MemcacheNode: () => MemcacheNode,
27
+ ModulaHash: () => ModulaHash,
25
28
  createNode: () => createNode,
26
- default: () => index_default
29
+ default: () => index_default,
30
+ defaultRetryBackoff: () => defaultRetryBackoff,
31
+ exponentialRetryBackoff: () => exponentialRetryBackoff
27
32
  });
28
33
  module.exports = __toCommonJS(index_exports);
34
+ var import_hookified3 = require("hookified");
35
+
36
+ // src/auto-discovery.ts
29
37
  var import_hookified2 = require("hookified");
30
38
 
31
- // src/ketama.ts
32
- var import_node_crypto = require("crypto");
33
- var hashFunctionForBuiltin = (algorithm) => (value) => (0, import_node_crypto.createHash)(algorithm).update(value).digest().readInt32BE();
34
- var keyFor = (node) => typeof node === "string" ? node : node.key;
35
- var HashRing = class _HashRing {
39
+ // src/node.ts
40
+ var import_node_net = require("net");
41
+ var import_hookified = require("hookified");
42
+
43
+ // src/binary-protocol.ts
44
+ var REQUEST_MAGIC = 128;
45
+ var OPCODE_SASL_AUTH = 33;
46
+ var OPCODE_GET = 0;
47
+ var OPCODE_SET = 1;
48
+ var OPCODE_ADD = 2;
49
+ var OPCODE_REPLACE = 3;
50
+ var OPCODE_DELETE = 4;
51
+ var OPCODE_INCREMENT = 5;
52
+ var OPCODE_DECREMENT = 6;
53
+ var OPCODE_QUIT = 7;
54
+ var OPCODE_FLUSH = 8;
55
+ var OPCODE_VERSION = 11;
56
+ var OPCODE_APPEND = 14;
57
+ var OPCODE_PREPEND = 15;
58
+ var OPCODE_STAT = 16;
59
+ var OPCODE_TOUCH = 28;
60
+ var STATUS_SUCCESS = 0;
61
+ var STATUS_KEY_NOT_FOUND = 1;
62
+ var STATUS_AUTH_ERROR = 32;
63
+ var HEADER_SIZE = 24;
64
+ function serializeHeader(header) {
65
+ const buf = Buffer.alloc(HEADER_SIZE);
66
+ buf.writeUInt8(header.magic ?? REQUEST_MAGIC, 0);
67
+ buf.writeUInt8(header.opcode ?? 0, 1);
68
+ buf.writeUInt16BE(header.keyLength ?? 0, 2);
69
+ buf.writeUInt8(header.extrasLength ?? 0, 4);
70
+ buf.writeUInt8(header.dataType ?? 0, 5);
71
+ buf.writeUInt16BE(header.status ?? 0, 6);
72
+ buf.writeUInt32BE(header.totalBodyLength ?? 0, 8);
73
+ buf.writeUInt32BE(header.opaque ?? 0, 12);
74
+ if (header.cas) {
75
+ header.cas.copy(buf, 16);
76
+ }
77
+ return buf;
78
+ }
79
+ function deserializeHeader(buf) {
80
+ return {
81
+ magic: buf.readUInt8(0),
82
+ opcode: buf.readUInt8(1),
83
+ keyLength: buf.readUInt16BE(2),
84
+ extrasLength: buf.readUInt8(4),
85
+ dataType: buf.readUInt8(5),
86
+ status: buf.readUInt16BE(6),
87
+ totalBodyLength: buf.readUInt32BE(8),
88
+ opaque: buf.readUInt32BE(12),
89
+ cas: buf.subarray(16, 24)
90
+ };
91
+ }
92
+ function buildSaslPlainRequest(username, password) {
93
+ const mechanism = "PLAIN";
94
+ const authData = `\0${username}\0${password}`;
95
+ const keyBuf = Buffer.from(mechanism, "utf8");
96
+ const valueBuf = Buffer.from(authData, "utf8");
97
+ const header = serializeHeader({
98
+ magic: REQUEST_MAGIC,
99
+ opcode: OPCODE_SASL_AUTH,
100
+ keyLength: keyBuf.length,
101
+ totalBodyLength: keyBuf.length + valueBuf.length
102
+ });
103
+ return Buffer.concat([header, keyBuf, valueBuf]);
104
+ }
105
+ function buildGetRequest(key) {
106
+ const keyBuf = Buffer.from(key, "utf8");
107
+ const header = serializeHeader({
108
+ magic: REQUEST_MAGIC,
109
+ opcode: OPCODE_GET,
110
+ keyLength: keyBuf.length,
111
+ totalBodyLength: keyBuf.length
112
+ });
113
+ return Buffer.concat([header, keyBuf]);
114
+ }
115
+ function buildSetRequest(key, value, flags = 0, exptime = 0) {
116
+ const keyBuf = Buffer.from(key, "utf8");
117
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
118
+ const extras = Buffer.alloc(8);
119
+ extras.writeUInt32BE(flags, 0);
120
+ extras.writeUInt32BE(exptime, 4);
121
+ const header = serializeHeader({
122
+ magic: REQUEST_MAGIC,
123
+ opcode: OPCODE_SET,
124
+ keyLength: keyBuf.length,
125
+ extrasLength: 8,
126
+ totalBodyLength: 8 + keyBuf.length + valueBuf.length
127
+ });
128
+ return Buffer.concat([header, extras, keyBuf, valueBuf]);
129
+ }
130
+ function buildAddRequest(key, value, flags = 0, exptime = 0) {
131
+ const keyBuf = Buffer.from(key, "utf8");
132
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
133
+ const extras = Buffer.alloc(8);
134
+ extras.writeUInt32BE(flags, 0);
135
+ extras.writeUInt32BE(exptime, 4);
136
+ const header = serializeHeader({
137
+ magic: REQUEST_MAGIC,
138
+ opcode: OPCODE_ADD,
139
+ keyLength: keyBuf.length,
140
+ extrasLength: 8,
141
+ totalBodyLength: 8 + keyBuf.length + valueBuf.length
142
+ });
143
+ return Buffer.concat([header, extras, keyBuf, valueBuf]);
144
+ }
145
+ function buildReplaceRequest(key, value, flags = 0, exptime = 0) {
146
+ const keyBuf = Buffer.from(key, "utf8");
147
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
148
+ const extras = Buffer.alloc(8);
149
+ extras.writeUInt32BE(flags, 0);
150
+ extras.writeUInt32BE(exptime, 4);
151
+ const header = serializeHeader({
152
+ magic: REQUEST_MAGIC,
153
+ opcode: OPCODE_REPLACE,
154
+ keyLength: keyBuf.length,
155
+ extrasLength: 8,
156
+ totalBodyLength: 8 + keyBuf.length + valueBuf.length
157
+ });
158
+ return Buffer.concat([header, extras, keyBuf, valueBuf]);
159
+ }
160
+ function buildDeleteRequest(key) {
161
+ const keyBuf = Buffer.from(key, "utf8");
162
+ const header = serializeHeader({
163
+ magic: REQUEST_MAGIC,
164
+ opcode: OPCODE_DELETE,
165
+ keyLength: keyBuf.length,
166
+ totalBodyLength: keyBuf.length
167
+ });
168
+ return Buffer.concat([header, keyBuf]);
169
+ }
170
+ function buildIncrementRequest(key, delta = 1, initial = 0, exptime = 0) {
171
+ const keyBuf = Buffer.from(key, "utf8");
172
+ const extras = Buffer.alloc(20);
173
+ extras.writeUInt32BE(Math.floor(delta / 4294967296), 0);
174
+ extras.writeUInt32BE(delta >>> 0, 4);
175
+ extras.writeUInt32BE(Math.floor(initial / 4294967296), 8);
176
+ extras.writeUInt32BE(initial >>> 0, 12);
177
+ extras.writeUInt32BE(exptime, 16);
178
+ const header = serializeHeader({
179
+ magic: REQUEST_MAGIC,
180
+ opcode: OPCODE_INCREMENT,
181
+ keyLength: keyBuf.length,
182
+ extrasLength: 20,
183
+ totalBodyLength: 20 + keyBuf.length
184
+ });
185
+ return Buffer.concat([header, extras, keyBuf]);
186
+ }
187
+ function buildDecrementRequest(key, delta = 1, initial = 0, exptime = 0) {
188
+ const keyBuf = Buffer.from(key, "utf8");
189
+ const extras = Buffer.alloc(20);
190
+ extras.writeUInt32BE(Math.floor(delta / 4294967296), 0);
191
+ extras.writeUInt32BE(delta >>> 0, 4);
192
+ extras.writeUInt32BE(Math.floor(initial / 4294967296), 8);
193
+ extras.writeUInt32BE(initial >>> 0, 12);
194
+ extras.writeUInt32BE(exptime, 16);
195
+ const header = serializeHeader({
196
+ magic: REQUEST_MAGIC,
197
+ opcode: OPCODE_DECREMENT,
198
+ keyLength: keyBuf.length,
199
+ extrasLength: 20,
200
+ totalBodyLength: 20 + keyBuf.length
201
+ });
202
+ return Buffer.concat([header, extras, keyBuf]);
203
+ }
204
+ function buildAppendRequest(key, value) {
205
+ const keyBuf = Buffer.from(key, "utf8");
206
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
207
+ const header = serializeHeader({
208
+ magic: REQUEST_MAGIC,
209
+ opcode: OPCODE_APPEND,
210
+ keyLength: keyBuf.length,
211
+ totalBodyLength: keyBuf.length + valueBuf.length
212
+ });
213
+ return Buffer.concat([header, keyBuf, valueBuf]);
214
+ }
215
+ function buildPrependRequest(key, value) {
216
+ const keyBuf = Buffer.from(key, "utf8");
217
+ const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
218
+ const header = serializeHeader({
219
+ magic: REQUEST_MAGIC,
220
+ opcode: OPCODE_PREPEND,
221
+ keyLength: keyBuf.length,
222
+ totalBodyLength: keyBuf.length + valueBuf.length
223
+ });
224
+ return Buffer.concat([header, keyBuf, valueBuf]);
225
+ }
226
+ function buildTouchRequest(key, exptime) {
227
+ const keyBuf = Buffer.from(key, "utf8");
228
+ const extras = Buffer.alloc(4);
229
+ extras.writeUInt32BE(exptime, 0);
230
+ const header = serializeHeader({
231
+ magic: REQUEST_MAGIC,
232
+ opcode: OPCODE_TOUCH,
233
+ keyLength: keyBuf.length,
234
+ extrasLength: 4,
235
+ totalBodyLength: 4 + keyBuf.length
236
+ });
237
+ return Buffer.concat([header, extras, keyBuf]);
238
+ }
239
+ function buildFlushRequest(exptime = 0) {
240
+ const extras = Buffer.alloc(4);
241
+ extras.writeUInt32BE(exptime, 0);
242
+ const header = serializeHeader({
243
+ magic: REQUEST_MAGIC,
244
+ opcode: OPCODE_FLUSH,
245
+ extrasLength: 4,
246
+ totalBodyLength: 4
247
+ });
248
+ return Buffer.concat([header, extras]);
249
+ }
250
+ function buildVersionRequest() {
251
+ return serializeHeader({
252
+ magic: REQUEST_MAGIC,
253
+ opcode: OPCODE_VERSION
254
+ });
255
+ }
256
+ function buildStatRequest(key) {
257
+ if (key) {
258
+ const keyBuf = Buffer.from(key, "utf8");
259
+ const header = serializeHeader({
260
+ magic: REQUEST_MAGIC,
261
+ opcode: OPCODE_STAT,
262
+ keyLength: keyBuf.length,
263
+ totalBodyLength: keyBuf.length
264
+ });
265
+ return Buffer.concat([header, keyBuf]);
266
+ }
267
+ return serializeHeader({
268
+ magic: REQUEST_MAGIC,
269
+ opcode: OPCODE_STAT
270
+ });
271
+ }
272
+ function buildQuitRequest() {
273
+ return serializeHeader({
274
+ magic: REQUEST_MAGIC,
275
+ opcode: OPCODE_QUIT
276
+ });
277
+ }
278
+ function parseGetResponse(buf) {
279
+ const header = deserializeHeader(buf);
280
+ if (header.status !== STATUS_SUCCESS) {
281
+ return { header, value: void 0, key: void 0 };
282
+ }
283
+ const extrasEnd = HEADER_SIZE + header.extrasLength;
284
+ const keyEnd = extrasEnd + header.keyLength;
285
+ const valueEnd = HEADER_SIZE + header.totalBodyLength;
286
+ const key = header.keyLength > 0 ? buf.subarray(extrasEnd, keyEnd).toString("utf8") : void 0;
287
+ const value = valueEnd > keyEnd ? buf.subarray(keyEnd, valueEnd) : void 0;
288
+ return { header, value, key };
289
+ }
290
+ function parseIncrDecrResponse(buf) {
291
+ const header = deserializeHeader(buf);
292
+ if (header.status !== STATUS_SUCCESS || header.totalBodyLength < 8) {
293
+ return { header, value: void 0 };
294
+ }
295
+ const high = buf.readUInt32BE(HEADER_SIZE);
296
+ const low = buf.readUInt32BE(HEADER_SIZE + 4);
297
+ const value = high * 4294967296 + low;
298
+ return { header, value };
299
+ }
300
+
301
+ // src/node.ts
302
+ var MemcacheNode = class extends import_hookified.Hookified {
303
+ _host;
304
+ _port;
305
+ _socket = void 0;
306
+ _timeout;
307
+ _keepAlive;
308
+ _keepAliveDelay;
309
+ _weight;
310
+ _connected = false;
311
+ _commandQueue = [];
312
+ _buffer = "";
313
+ _currentCommand = void 0;
314
+ _multilineData = [];
315
+ _pendingValueBytes = 0;
316
+ _sasl;
317
+ _authenticated = false;
318
+ _binaryBuffer = Buffer.alloc(0);
319
+ constructor(host, port, options) {
320
+ super();
321
+ this._host = host;
322
+ this._port = port;
323
+ this._timeout = options?.timeout || 5e3;
324
+ this._keepAlive = options?.keepAlive !== false;
325
+ this._keepAliveDelay = options?.keepAliveDelay || 1e3;
326
+ this._weight = options?.weight || 1;
327
+ this._sasl = options?.sasl;
328
+ }
36
329
  /**
37
- * Base weight of each node in the hash ring. Having a base weight of 1 is
38
- * not very desirable, since then, due to the ketama-style "clock", it's
39
- * possible to end up with a clock that's very skewed when dealing with a
40
- * small number of nodes. Setting to 50 nodes seems to give a better
41
- * distribution, so that load is spread roughly evenly to +/- 5%.
330
+ * Get the host of this node
42
331
  */
43
- static baseWeight = 50;
44
- /** The hash function used to compute node positions on the ring */
45
- hashFn;
46
- /** The sorted array of [hash, node key] tuples representing virtual nodes on the ring */
47
- _clock = [];
48
- /** Map of node keys to actual node objects */
49
- _nodes = /* @__PURE__ */ new Map();
332
+ get host() {
333
+ return this._host;
334
+ }
50
335
  /**
51
- * Gets the sorted array of [hash, node key] tuples representing virtual nodes on the ring.
52
- * @returns The hash clock array
336
+ * Get the port of this node
53
337
  */
54
- get clock() {
55
- return this._clock;
338
+ get port() {
339
+ return this._port;
56
340
  }
57
341
  /**
58
- * Gets the map of node keys to actual node objects.
59
- * @returns The nodes map
342
+ * Get the unique identifier for this node (host:port format)
60
343
  */
61
- get nodes() {
62
- return this._nodes;
344
+ get id() {
345
+ if (this._port === 0) {
346
+ return this._host;
347
+ }
348
+ const host = this._host.includes(":") ? `[${this._host}]` : this._host;
349
+ return `${host}:${this._port}`;
63
350
  }
64
351
  /**
65
- * Creates a new HashRing instance.
66
- *
67
- * @param initialNodes - Array of nodes to add to the ring, optionally with weights
68
- * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
69
- *
70
- * @example
71
- * ```typescript
72
- * // Simple ring with default SHA-1 hashing
73
- * const ring = new HashRing(['node1', 'node2']);
74
- *
75
- * // Ring with custom hash function
76
- * const customRing = new HashRing(['node1', 'node2'], 'md5');
77
- *
78
- * // Ring with weighted nodes
79
- * const weightedRing = new HashRing([
80
- * { node: 'heavy-server', weight: 3 },
81
- * { node: 'light-server', weight: 1 }
82
- * ]);
83
- * ```
352
+ * Get the full uri like memcache://localhost:11211
84
353
  */
85
- constructor(initialNodes = [], hashFn = "sha1") {
86
- this.hashFn = typeof hashFn === "string" ? hashFunctionForBuiltin(hashFn) : hashFn;
87
- for (const node of initialNodes) {
88
- if (typeof node === "object" && "weight" in node && "node" in node) {
89
- this.addNode(node.node, node.weight);
90
- } else {
91
- this.addNode(node);
92
- }
93
- }
354
+ get uri() {
355
+ return `memcache://${this.id}`;
94
356
  }
95
357
  /**
96
- * Add a new node to the ring. If the node already exists in the ring, it
97
- * will be updated. For example, you can use this to update the node's weight.
98
- *
99
- * @param node - The node to add to the ring
100
- * @param weight - The relative weight of this node (default: 1). Higher weights mean more keys will be assigned to this node. A weight of 0 removes the node.
101
- * @throws {RangeError} If weight is negative
102
- *
103
- * @example
104
- * ```typescript
105
- * const ring = new HashRing();
106
- * ring.addNode('server1'); // Add with default weight of 1
107
- * ring.addNode('server2', 2); // Add with weight of 2 (will handle ~2x more keys)
108
- * ring.addNode('server1', 3); // Update server1's weight to 3
109
- * ring.addNode('server2', 0); // Remove server2
110
- * ```
358
+ * Get the socket connection
111
359
  */
112
- addNode(node, weight = 1) {
113
- if (weight === 0) {
114
- this.removeNode(node);
115
- } else if (weight < 0) {
116
- throw new RangeError("Cannot add a node to the hashring with weight < 0");
117
- } else {
118
- this.removeNode(node);
119
- const key = keyFor(node);
120
- this._nodes.set(key, node);
121
- this.addNodeToClock(key, Math.round(weight * _HashRing.baseWeight));
122
- }
360
+ get socket() {
361
+ return this._socket;
123
362
  }
124
363
  /**
125
- * Removes the node from the ring. No-op if the node does not exist.
126
- *
127
- * @param node - The node to remove from the ring
128
- *
129
- * @example
130
- * ```typescript
131
- * const ring = new HashRing(['server1', 'server2']);
132
- * ring.removeNode('server1'); // Removes server1 from the ring
133
- * ring.removeNode('nonexistent'); // Safe to call with non-existent node
134
- * ```
364
+ * Get the weight of this node (used for consistent hashing distribution)
135
365
  */
136
- removeNode(node) {
137
- const key = keyFor(node);
138
- if (this._nodes.delete(key)) {
139
- this._clock = this._clock.filter(([, n]) => n !== key);
140
- }
366
+ get weight() {
367
+ return this._weight;
141
368
  }
142
369
  /**
143
- * Gets the node which should handle the given input key. Returns undefined if
144
- * the hashring has no nodes.
145
- *
146
- * Uses consistent hashing to ensure the same input always maps to the same node,
147
- * and minimizes redistribution when nodes are added or removed.
148
- *
149
- * @param input - The key to find the responsible node for (string or Buffer)
150
- * @returns The node responsible for this key, or undefined if ring is empty
151
- *
152
- * @example
153
- * ```typescript
154
- * const ring = new HashRing(['server1', 'server2', 'server3']);
155
- * const node = ring.getNode('user:123'); // Returns e.g., 'server2'
156
- * const sameNode = ring.getNode('user:123'); // Always returns 'server2'
157
- *
158
- * // Also accepts Buffer input
159
- * const bufferNode = ring.getNode(Buffer.from('user:123'));
160
- * ```
370
+ * Set the weight of this node (used for consistent hashing distribution)
161
371
  */
162
- getNode(input) {
163
- if (this._clock.length === 0) {
164
- return void 0;
165
- }
166
- const index = this.getIndexForInput(input);
167
- const key = index === this._clock.length ? this._clock[0][1] : this._clock[index][1];
168
- return this._nodes.get(key);
372
+ set weight(value) {
373
+ this._weight = value;
169
374
  }
170
375
  /**
171
- * Finds the index in the clock for the given input by hashing it and performing binary search.
172
- *
173
- * @param input - The input to find the clock position for
174
- * @returns The index in the clock array
376
+ * Get the keepAlive setting for this node
175
377
  */
176
- getIndexForInput(input) {
177
- const hash = this.hashFn(
178
- typeof input === "string" ? Buffer.from(input) : input
179
- );
180
- return binarySearchRing(this._clock, hash);
378
+ get keepAlive() {
379
+ return this._keepAlive;
181
380
  }
182
381
  /**
183
- * Gets multiple replica nodes that should handle the given input. Useful for
184
- * implementing replication strategies where you want to store data on multiple nodes.
185
- *
186
- * The returned array will contain unique nodes in the order they appear on the ring
187
- * starting from the primary node. If there are fewer nodes than replicas requested,
188
- * all nodes are returned.
189
- *
190
- * @param input - The key to find replica nodes for (string or Buffer)
191
- * @param replicas - The number of replica nodes to return
192
- * @returns Array of nodes that should handle this key (length ≤ replicas)
193
- *
194
- * @example
195
- * ```typescript
196
- * const ring = new HashRing(['server1', 'server2', 'server3', 'server4']);
197
- *
198
- * // Get 3 replicas for a key
199
- * const replicas = ring.getNodes('user:123', 3);
200
- * // Returns e.g., ['server2', 'server4', 'server1']
201
- *
202
- * // If requesting more replicas than nodes, returns all nodes
203
- * const allNodes = ring.getNodes('user:123', 10);
204
- * // Returns ['server1', 'server2', 'server3', 'server4']
205
- * ```
382
+ * Set the keepAlive setting for this node
206
383
  */
207
- getNodes(input, replicas) {
208
- if (this._clock.length === 0) {
209
- return [];
210
- }
211
- if (replicas >= this._nodes.size) {
212
- return [...this._nodes.values()];
213
- }
214
- const chosen = /* @__PURE__ */ new Set();
215
- for (let i = this.getIndexForInput(input); chosen.size < replicas; i++) {
216
- chosen.add(this._clock[i % this._clock.length][1]);
217
- }
218
- return [...chosen].map((c) => this._nodes.get(c));
384
+ set keepAlive(value) {
385
+ this._keepAlive = value;
219
386
  }
220
387
  /**
221
- * Adds virtual nodes to the clock for the given node key.
222
- * Creates multiple positions on the ring for better distribution.
223
- *
224
- * @param key - The node key to add to the clock
225
- * @param weight - The number of virtual nodes to create (weight * baseWeight)
388
+ * Get the keepAliveDelay setting for this node
226
389
  */
227
- addNodeToClock(key, weight) {
228
- for (let i = weight; i > 0; i--) {
229
- const hash = this.hashFn(Buffer.from(`${key}\0${i}`));
230
- this._clock.push([hash, key]);
231
- }
232
- this._clock.sort((a, b) => a[0] - b[0]);
233
- }
234
- };
235
- var KetamaHash = class {
236
- /** The name of this distribution strategy */
237
- name = "ketama";
238
- /** Internal hash ring for consistent hashing */
239
- hashRing;
240
- /** Map of node IDs to MemcacheNode instances */
241
- nodeMap;
242
- /**
243
- * Creates a new KetamaDistributionHash instance.
244
- *
245
- * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
246
- *
247
- * @example
248
- * ```typescript
249
- * // Use default SHA-1 hashing
250
- * const distribution = new KetamaDistributionHash();
251
- *
252
- * // Use MD5 hashing
253
- * const distribution = new KetamaDistributionHash('md5');
254
- * ```
255
- */
256
- constructor(hashFn) {
257
- this.hashRing = new HashRing([], hashFn);
258
- this.nodeMap = /* @__PURE__ */ new Map();
259
- }
260
- /**
261
- * Gets all nodes in the distribution.
262
- * @returns Array of all MemcacheNode instances
263
- */
264
- get nodes() {
265
- return Array.from(this.nodeMap.values());
266
- }
267
- /**
268
- * Adds a node to the distribution with its weight for consistent hashing.
269
- *
270
- * @param node - The MemcacheNode to add
271
- *
272
- * @example
273
- * ```typescript
274
- * const node = new MemcacheNode('localhost', 11211, { weight: 2 });
275
- * distribution.addNode(node);
276
- * ```
277
- */
278
- addNode(node) {
279
- this.nodeMap.set(node.id, node);
280
- this.hashRing.addNode(node.id, node.weight);
281
- }
282
- /**
283
- * Removes a node from the distribution by its ID.
284
- *
285
- * @param id - The node ID (e.g., "localhost:11211")
286
- *
287
- * @example
288
- * ```typescript
289
- * distribution.removeNode('localhost:11211');
290
- * ```
291
- */
292
- removeNode(id) {
293
- this.nodeMap.delete(id);
294
- this.hashRing.removeNode(id);
295
- }
296
- /**
297
- * Gets a specific node by its ID.
298
- *
299
- * @param id - The node ID (e.g., "localhost:11211")
300
- * @returns The MemcacheNode if found, undefined otherwise
301
- *
302
- * @example
303
- * ```typescript
304
- * const node = distribution.getNode('localhost:11211');
305
- * if (node) {
306
- * console.log(`Found node: ${node.uri}`);
307
- * }
308
- * ```
309
- */
310
- getNode(id) {
311
- return this.nodeMap.get(id);
312
- }
313
- /**
314
- * Gets the nodes responsible for a given key using consistent hashing.
315
- * Currently returns a single node (the primary node for the key).
316
- *
317
- * @param key - The cache key to find the responsible node for
318
- * @returns Array containing the responsible node(s), empty if no nodes available
319
- *
320
- * @example
321
- * ```typescript
322
- * const nodes = distribution.getNodesByKey('user:123');
323
- * if (nodes.length > 0) {
324
- * console.log(`Key will be stored on: ${nodes[0].id}`);
325
- * }
326
- * ```
327
- */
328
- getNodesByKey(key) {
329
- const nodeId = this.hashRing.getNode(key);
330
- if (!nodeId) {
331
- return [];
332
- }
333
- const node = this.nodeMap.get(nodeId);
334
- return node ? [node] : [];
335
- }
336
- };
337
- function binarySearchRing(ring, hash) {
338
- let mid;
339
- let lo = 0;
340
- let hi = ring.length - 1;
341
- while (lo <= hi) {
342
- mid = Math.floor((lo + hi) / 2);
343
- if (ring[mid][0] >= hash) {
344
- hi = mid - 1;
345
- } else {
346
- lo = mid + 1;
347
- }
348
- }
349
- return lo;
350
- }
351
-
352
- // src/node.ts
353
- var import_node_net = require("net");
354
- var import_hookified = require("hookified");
355
- var MemcacheNode = class extends import_hookified.Hookified {
356
- _host;
357
- _port;
358
- _socket = void 0;
359
- _timeout;
360
- _keepAlive;
361
- _keepAliveDelay;
362
- _weight;
363
- _connected = false;
364
- _commandQueue = [];
365
- _buffer = "";
366
- _currentCommand = void 0;
367
- _multilineData = [];
368
- _pendingValueBytes = 0;
369
- constructor(host, port, options) {
370
- super();
371
- this._host = host;
372
- this._port = port;
373
- this._timeout = options?.timeout || 5e3;
374
- this._keepAlive = options?.keepAlive !== false;
375
- this._keepAliveDelay = options?.keepAliveDelay || 1e3;
376
- this._weight = options?.weight || 1;
377
- }
378
- /**
379
- * Get the host of this node
380
- */
381
- get host() {
382
- return this._host;
383
- }
384
- /**
385
- * Get the port of this node
386
- */
387
- get port() {
388
- return this._port;
389
- }
390
- /**
391
- * Get the unique identifier for this node (host:port format)
392
- */
393
- get id() {
394
- return this._port === 0 ? this._host : `${this._host}:${this._port}`;
395
- }
396
- /**
397
- * Get the full uri like memcache://localhost:11211
398
- */
399
- get uri() {
400
- return `memcache://${this.id}`;
401
- }
402
- /**
403
- * Get the socket connection
404
- */
405
- get socket() {
406
- return this._socket;
407
- }
408
- /**
409
- * Get the weight of this node (used for consistent hashing distribution)
410
- */
411
- get weight() {
412
- return this._weight;
413
- }
414
- /**
415
- * Set the weight of this node (used for consistent hashing distribution)
416
- */
417
- set weight(value) {
418
- this._weight = value;
419
- }
420
- /**
421
- * Get the keepAlive setting for this node
422
- */
423
- get keepAlive() {
424
- return this._keepAlive;
425
- }
426
- /**
427
- * Set the keepAlive setting for this node
428
- */
429
- set keepAlive(value) {
430
- this._keepAlive = value;
431
- }
432
- /**
433
- * Get the keepAliveDelay setting for this node
434
- */
435
- get keepAliveDelay() {
436
- return this._keepAliveDelay;
390
+ get keepAliveDelay() {
391
+ return this._keepAliveDelay;
437
392
  }
438
393
  /**
439
394
  * Set the keepAliveDelay setting for this node
@@ -447,6 +402,18 @@ var MemcacheNode = class extends import_hookified.Hookified {
447
402
  get commandQueue() {
448
403
  return this._commandQueue;
449
404
  }
405
+ /**
406
+ * Get whether SASL authentication is configured
407
+ */
408
+ get hasSaslCredentials() {
409
+ return !!this._sasl?.username && !!this._sasl?.password;
410
+ }
411
+ /**
412
+ * Get whether the node is authenticated (only relevant if SASL is configured)
413
+ */
414
+ get isAuthenticated() {
415
+ return this._authenticated;
416
+ }
450
417
  /**
451
418
  * Connect to the memcache server
452
419
  */
@@ -463,14 +430,31 @@ var MemcacheNode = class extends import_hookified.Hookified {
463
430
  keepAliveInitialDelay: this._keepAliveDelay
464
431
  });
465
432
  this._socket.setTimeout(this._timeout);
466
- this._socket.setEncoding("utf8");
467
- this._socket.on("connect", () => {
433
+ if (!this._sasl) {
434
+ this._socket.setEncoding("utf8");
435
+ }
436
+ this._socket.on("connect", async () => {
468
437
  this._connected = true;
469
- this.emit("connect");
470
- resolve();
438
+ if (this._sasl) {
439
+ try {
440
+ await this.performSaslAuth();
441
+ this.emit("connect");
442
+ resolve();
443
+ } catch (error) {
444
+ this._socket?.destroy();
445
+ this._connected = false;
446
+ this._authenticated = false;
447
+ reject(error);
448
+ }
449
+ } else {
450
+ this.emit("connect");
451
+ resolve();
452
+ }
471
453
  });
472
454
  this._socket.on("data", (data) => {
473
- this.handleData(data);
455
+ if (typeof data === "string") {
456
+ this.handleData(data);
457
+ }
474
458
  });
475
459
  this._socket.on("error", (error) => {
476
460
  this.emit("error", error);
@@ -480,6 +464,7 @@ var MemcacheNode = class extends import_hookified.Hookified {
480
464
  });
481
465
  this._socket.on("close", () => {
482
466
  this._connected = false;
467
+ this._authenticated = false;
483
468
  this.emit("close");
484
469
  this.rejectPendingCommands(new Error("Connection closed"));
485
470
  });
@@ -513,47 +498,296 @@ var MemcacheNode = class extends import_hookified.Hookified {
513
498
  this._currentCommand = void 0;
514
499
  this._multilineData = [];
515
500
  this._pendingValueBytes = 0;
501
+ this._authenticated = false;
502
+ this._binaryBuffer = Buffer.alloc(0);
516
503
  }
517
504
  await this.connect();
518
505
  }
519
506
  /**
520
- * Gracefully quit the connection (send quit command then disconnect)
507
+ * Perform SASL PLAIN authentication using the binary protocol
521
508
  */
522
- async quit() {
523
- if (this._connected && this._socket) {
524
- try {
525
- await this.command("quit");
526
- } catch (error) {
527
- }
528
- await this.disconnect();
509
+ async performSaslAuth() {
510
+ if (!this._sasl || !this._socket) {
511
+ throw new Error("SASL credentials not configured");
529
512
  }
513
+ const socket = this._socket;
514
+ const sasl = this._sasl;
515
+ return new Promise((resolve, reject) => {
516
+ this._binaryBuffer = Buffer.alloc(0);
517
+ const authPacket = buildSaslPlainRequest(sasl.username, sasl.password);
518
+ const binaryHandler = (data) => {
519
+ this._binaryBuffer = Buffer.concat([this._binaryBuffer, data]);
520
+ if (this._binaryBuffer.length < HEADER_SIZE) {
521
+ return;
522
+ }
523
+ const header = deserializeHeader(this._binaryBuffer);
524
+ const totalLength = HEADER_SIZE + header.totalBodyLength;
525
+ if (this._binaryBuffer.length < totalLength) {
526
+ return;
527
+ }
528
+ socket.removeListener("data", binaryHandler);
529
+ if (header.status === STATUS_SUCCESS) {
530
+ this._authenticated = true;
531
+ this.emit("authenticated");
532
+ resolve();
533
+ } else if (header.status === STATUS_AUTH_ERROR) {
534
+ const body = this._binaryBuffer.subarray(HEADER_SIZE, totalLength);
535
+ reject(
536
+ new Error(
537
+ `SASL authentication failed: ${body.toString() || "Invalid credentials"}`
538
+ )
539
+ );
540
+ } else {
541
+ reject(
542
+ new Error(
543
+ `SASL authentication failed with status: 0x${header.status.toString(16)}`
544
+ )
545
+ );
546
+ }
547
+ };
548
+ socket.on("data", binaryHandler);
549
+ socket.write(authPacket);
550
+ });
530
551
  }
531
552
  /**
532
- * Check if connected to the memcache server
553
+ * Send a binary protocol request and wait for response.
554
+ * Used internally for SASL-authenticated connections.
533
555
  */
534
- isConnected() {
535
- return this._connected;
556
+ async binaryRequest(packet) {
557
+ if (!this._socket) {
558
+ throw new Error("Not connected");
559
+ }
560
+ const socket = this._socket;
561
+ return new Promise((resolve) => {
562
+ let buffer = Buffer.alloc(0);
563
+ const dataHandler = (data) => {
564
+ buffer = Buffer.concat([buffer, data]);
565
+ if (buffer.length < HEADER_SIZE) {
566
+ return;
567
+ }
568
+ const header = deserializeHeader(buffer);
569
+ const totalLength = HEADER_SIZE + header.totalBodyLength;
570
+ if (buffer.length < totalLength) {
571
+ return;
572
+ }
573
+ socket.removeListener("data", dataHandler);
574
+ resolve(buffer.subarray(0, totalLength));
575
+ };
576
+ socket.on("data", dataHandler);
577
+ socket.write(packet);
578
+ });
536
579
  }
537
580
  /**
538
- * Send a generic command to the memcache server
539
- * @param cmd The command string to send (without trailing \r\n)
540
- * @param options Command options for response parsing
581
+ * Binary protocol GET operation
541
582
  */
542
- async command(cmd, options) {
543
- if (!this._connected || !this._socket) {
544
- throw new Error(`Not connected to memcache server ${this.id}`);
583
+ async binaryGet(key) {
584
+ const response = await this.binaryRequest(buildGetRequest(key));
585
+ const { header, value } = parseGetResponse(response);
586
+ if (header.status === STATUS_KEY_NOT_FOUND) {
587
+ this.emit("miss", key);
588
+ return void 0;
545
589
  }
546
- return new Promise((resolve, reject) => {
547
- this._commandQueue.push({
548
- command: cmd,
549
- resolve,
550
- reject,
551
- isMultiline: options?.isMultiline,
552
- isStats: options?.isStats,
553
- requestedKeys: options?.requestedKeys
554
- });
555
- this._socket.write(`${cmd}\r
556
- `);
590
+ if (header.status !== STATUS_SUCCESS || !value) {
591
+ return void 0;
592
+ }
593
+ const result = value.toString("utf8");
594
+ this.emit("hit", key, result);
595
+ return result;
596
+ }
597
+ /**
598
+ * Binary protocol SET operation
599
+ */
600
+ async binarySet(key, value, exptime = 0, flags = 0) {
601
+ const response = await this.binaryRequest(
602
+ buildSetRequest(key, value, flags, exptime)
603
+ );
604
+ const header = deserializeHeader(response);
605
+ return header.status === STATUS_SUCCESS;
606
+ }
607
+ /**
608
+ * Binary protocol ADD operation
609
+ */
610
+ async binaryAdd(key, value, exptime = 0, flags = 0) {
611
+ const response = await this.binaryRequest(
612
+ buildAddRequest(key, value, flags, exptime)
613
+ );
614
+ const header = deserializeHeader(response);
615
+ return header.status === STATUS_SUCCESS;
616
+ }
617
+ /**
618
+ * Binary protocol REPLACE operation
619
+ */
620
+ async binaryReplace(key, value, exptime = 0, flags = 0) {
621
+ const response = await this.binaryRequest(
622
+ buildReplaceRequest(key, value, flags, exptime)
623
+ );
624
+ const header = deserializeHeader(response);
625
+ return header.status === STATUS_SUCCESS;
626
+ }
627
+ /**
628
+ * Binary protocol DELETE operation
629
+ */
630
+ async binaryDelete(key) {
631
+ const response = await this.binaryRequest(buildDeleteRequest(key));
632
+ const header = deserializeHeader(response);
633
+ return header.status === STATUS_SUCCESS || header.status === STATUS_KEY_NOT_FOUND;
634
+ }
635
+ /**
636
+ * Binary protocol INCREMENT operation
637
+ */
638
+ async binaryIncr(key, delta = 1, initial = 0, exptime = 0) {
639
+ const response = await this.binaryRequest(
640
+ buildIncrementRequest(key, delta, initial, exptime)
641
+ );
642
+ const { header, value } = parseIncrDecrResponse(response);
643
+ if (header.status !== STATUS_SUCCESS) {
644
+ return void 0;
645
+ }
646
+ return value;
647
+ }
648
+ /**
649
+ * Binary protocol DECREMENT operation
650
+ */
651
+ async binaryDecr(key, delta = 1, initial = 0, exptime = 0) {
652
+ const response = await this.binaryRequest(
653
+ buildDecrementRequest(key, delta, initial, exptime)
654
+ );
655
+ const { header, value } = parseIncrDecrResponse(response);
656
+ if (header.status !== STATUS_SUCCESS) {
657
+ return void 0;
658
+ }
659
+ return value;
660
+ }
661
+ /**
662
+ * Binary protocol APPEND operation
663
+ */
664
+ async binaryAppend(key, value) {
665
+ const response = await this.binaryRequest(buildAppendRequest(key, value));
666
+ const header = deserializeHeader(response);
667
+ return header.status === STATUS_SUCCESS;
668
+ }
669
+ /**
670
+ * Binary protocol PREPEND operation
671
+ */
672
+ async binaryPrepend(key, value) {
673
+ const response = await this.binaryRequest(buildPrependRequest(key, value));
674
+ const header = deserializeHeader(response);
675
+ return header.status === STATUS_SUCCESS;
676
+ }
677
+ /**
678
+ * Binary protocol TOUCH operation
679
+ */
680
+ async binaryTouch(key, exptime) {
681
+ const response = await this.binaryRequest(buildTouchRequest(key, exptime));
682
+ const header = deserializeHeader(response);
683
+ return header.status === STATUS_SUCCESS;
684
+ }
685
+ /**
686
+ * Binary protocol FLUSH operation
687
+ */
688
+ /* v8 ignore next -- @preserve */
689
+ async binaryFlush(exptime = 0) {
690
+ const response = await this.binaryRequest(buildFlushRequest(exptime));
691
+ const header = deserializeHeader(response);
692
+ return header.status === STATUS_SUCCESS;
693
+ }
694
+ /**
695
+ * Binary protocol VERSION operation
696
+ */
697
+ async binaryVersion() {
698
+ const response = await this.binaryRequest(buildVersionRequest());
699
+ const header = deserializeHeader(response);
700
+ if (header.status !== STATUS_SUCCESS) {
701
+ return void 0;
702
+ }
703
+ return response.subarray(HEADER_SIZE, HEADER_SIZE + header.totalBodyLength).toString("utf8");
704
+ }
705
+ /**
706
+ * Binary protocol STATS operation
707
+ */
708
+ async binaryStats() {
709
+ if (!this._socket) {
710
+ throw new Error("Not connected");
711
+ }
712
+ const socket = this._socket;
713
+ const stats = {};
714
+ return new Promise((resolve) => {
715
+ let buffer = Buffer.alloc(0);
716
+ const dataHandler = (data) => {
717
+ buffer = Buffer.concat([buffer, data]);
718
+ while (buffer.length >= HEADER_SIZE) {
719
+ const header = deserializeHeader(buffer);
720
+ const totalLength = HEADER_SIZE + header.totalBodyLength;
721
+ if (buffer.length < totalLength) {
722
+ return;
723
+ }
724
+ if (header.keyLength === 0 && header.totalBodyLength === 0) {
725
+ socket.removeListener("data", dataHandler);
726
+ resolve(stats);
727
+ return;
728
+ }
729
+ if (header.opcode === OPCODE_STAT && header.status === STATUS_SUCCESS) {
730
+ const keyStart = HEADER_SIZE;
731
+ const keyEnd = keyStart + header.keyLength;
732
+ const valueEnd = HEADER_SIZE + header.totalBodyLength;
733
+ const key = buffer.subarray(keyStart, keyEnd).toString("utf8");
734
+ const value = buffer.subarray(keyEnd, valueEnd).toString("utf8");
735
+ stats[key] = value;
736
+ }
737
+ buffer = buffer.subarray(totalLength);
738
+ }
739
+ };
740
+ socket.on("data", dataHandler);
741
+ socket.write(buildStatRequest());
742
+ });
743
+ }
744
+ /**
745
+ * Binary protocol QUIT operation
746
+ */
747
+ async binaryQuit() {
748
+ if (this._socket) {
749
+ this._socket.write(buildQuitRequest());
750
+ }
751
+ }
752
+ /**
753
+ * Gracefully quit the connection (send quit command then disconnect)
754
+ */
755
+ async quit() {
756
+ if (this._connected && this._socket) {
757
+ try {
758
+ await this.command("quit");
759
+ } catch (error) {
760
+ }
761
+ await this.disconnect();
762
+ }
763
+ }
764
+ /**
765
+ * Check if connected to the memcache server
766
+ */
767
+ isConnected() {
768
+ return this._connected;
769
+ }
770
+ /**
771
+ * Send a generic command to the memcache server
772
+ * @param cmd The command string to send (without trailing \r\n)
773
+ * @param options Command options for response parsing
774
+ */
775
+ async command(cmd, options) {
776
+ if (!this._connected || !this._socket) {
777
+ throw new Error(`Not connected to memcache server ${this.id}`);
778
+ }
779
+ return new Promise((resolve, reject) => {
780
+ this._commandQueue.push({
781
+ command: cmd,
782
+ resolve,
783
+ reject,
784
+ isMultiline: options?.isMultiline,
785
+ isStats: options?.isStats,
786
+ isConfig: options?.isConfig,
787
+ requestedKeys: options?.requestedKeys
788
+ });
789
+ this._socket.write(`${cmd}\r
790
+ `);
557
791
  });
558
792
  }
559
793
  handleData(data) {
@@ -606,81 +840,769 @@ var MemcacheNode = class extends import_hookified.Hookified {
606
840
  }
607
841
  return;
608
842
  }
609
- if (this._currentCommand.isMultiline) {
610
- if (this._currentCommand.requestedKeys && !this._currentCommand.foundKeys) {
611
- this._currentCommand.foundKeys = [];
612
- }
613
- if (line.startsWith("VALUE ")) {
614
- const parts = line.split(" ");
615
- const key = parts[1];
616
- const bytes = parseInt(parts[3], 10);
617
- if (this._currentCommand.requestedKeys) {
618
- this._currentCommand.foundKeys?.push(key);
619
- }
620
- this._pendingValueBytes = bytes;
621
- } else if (line === "END") {
622
- let result;
623
- if (this._currentCommand.requestedKeys && this._currentCommand.foundKeys) {
624
- result = {
625
- values: this._multilineData.length > 0 ? this._multilineData : void 0,
626
- foundKeys: this._currentCommand.foundKeys
627
- };
628
- } else {
629
- result = this._multilineData.length > 0 ? this._multilineData : void 0;
630
- }
631
- if (this._currentCommand.requestedKeys && this._currentCommand.foundKeys) {
632
- const foundKeys = this._currentCommand.foundKeys;
633
- for (let i = 0; i < foundKeys.length; i++) {
634
- this.emit("hit", foundKeys[i], this._multilineData[i]);
635
- }
636
- const missedKeys = this._currentCommand.requestedKeys.filter(
637
- (key) => !foundKeys.includes(key)
638
- );
639
- for (const key of missedKeys) {
640
- this.emit("miss", key);
641
- }
642
- }
643
- this._currentCommand.resolve(result);
644
- this._multilineData = [];
645
- this._currentCommand = void 0;
646
- } else if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
647
- this._currentCommand.reject(new Error(line));
648
- this._multilineData = [];
649
- this._currentCommand = void 0;
650
- }
843
+ if (this._currentCommand.isConfig) {
844
+ if (line.startsWith("CONFIG ")) {
845
+ const parts = line.split(" ");
846
+ const bytes = Number.parseInt(parts[3], 10);
847
+ this._pendingValueBytes = bytes;
848
+ } else if (line === "END") {
849
+ const result = this._multilineData.length > 0 ? this._multilineData : void 0;
850
+ this._currentCommand.resolve(result);
851
+ this._multilineData = [];
852
+ this._currentCommand = void 0;
853
+ } else if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
854
+ this._currentCommand.reject(new Error(line));
855
+ this._multilineData = [];
856
+ this._currentCommand = void 0;
857
+ }
858
+ return;
859
+ }
860
+ if (this._currentCommand.isMultiline) {
861
+ if (this._currentCommand.requestedKeys && !this._currentCommand.foundKeys) {
862
+ this._currentCommand.foundKeys = [];
863
+ }
864
+ if (line.startsWith("VALUE ")) {
865
+ const parts = line.split(" ");
866
+ const key = parts[1];
867
+ const bytes = parseInt(parts[3], 10);
868
+ if (this._currentCommand.requestedKeys) {
869
+ this._currentCommand.foundKeys?.push(key);
870
+ }
871
+ this._pendingValueBytes = bytes;
872
+ } else if (line === "END") {
873
+ let result;
874
+ if (this._currentCommand.requestedKeys && this._currentCommand.foundKeys) {
875
+ result = {
876
+ values: this._multilineData.length > 0 ? this._multilineData : void 0,
877
+ foundKeys: this._currentCommand.foundKeys
878
+ };
879
+ } else {
880
+ result = this._multilineData.length > 0 ? this._multilineData : void 0;
881
+ }
882
+ if (this._currentCommand.requestedKeys && this._currentCommand.foundKeys) {
883
+ const foundKeys = this._currentCommand.foundKeys;
884
+ for (let i = 0; i < foundKeys.length; i++) {
885
+ this.emit("hit", foundKeys[i], this._multilineData[i]);
886
+ }
887
+ const missedKeys = this._currentCommand.requestedKeys.filter(
888
+ (key) => !foundKeys.includes(key)
889
+ );
890
+ for (const key of missedKeys) {
891
+ this.emit("miss", key);
892
+ }
893
+ }
894
+ this._currentCommand.resolve(result);
895
+ this._multilineData = [];
896
+ this._currentCommand = void 0;
897
+ } else if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
898
+ this._currentCommand.reject(new Error(line));
899
+ this._multilineData = [];
900
+ this._currentCommand = void 0;
901
+ }
902
+ } else {
903
+ if (line === "STORED" || line === "DELETED" || line === "OK" || line === "TOUCHED" || line === "EXISTS" || line === "NOT_FOUND") {
904
+ this._currentCommand.resolve(line);
905
+ } else if (line === "NOT_STORED") {
906
+ this._currentCommand.resolve(false);
907
+ } else if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
908
+ this._currentCommand.reject(new Error(line));
909
+ } else if (/^\d+$/.test(line)) {
910
+ this._currentCommand.resolve(parseInt(line, 10));
911
+ } else {
912
+ this._currentCommand.resolve(line);
913
+ }
914
+ this._currentCommand = void 0;
915
+ }
916
+ }
917
+ rejectPendingCommands(error) {
918
+ if (this._currentCommand) {
919
+ this._currentCommand.reject(error);
920
+ this._currentCommand = void 0;
921
+ }
922
+ while (this._commandQueue.length > 0) {
923
+ const cmd = this._commandQueue.shift();
924
+ if (cmd) {
925
+ cmd.reject(error);
926
+ }
927
+ }
928
+ }
929
+ };
930
+ function createNode(host, port, options) {
931
+ return new MemcacheNode(host, port, options);
932
+ }
933
+
934
+ // src/auto-discovery.ts
935
+ var AutoDiscovery = class _AutoDiscovery extends import_hookified2.Hookified {
936
+ _configEndpoint;
937
+ _pollingInterval;
938
+ _useLegacyCommand;
939
+ _configVersion = -1;
940
+ _pollingTimer;
941
+ _configNode;
942
+ _timeout;
943
+ _keepAlive;
944
+ _keepAliveDelay;
945
+ _sasl;
946
+ _isRunning = false;
947
+ _isPolling = false;
948
+ constructor(options) {
949
+ super();
950
+ this._configEndpoint = options.configEndpoint;
951
+ this._pollingInterval = options.pollingInterval;
952
+ this._useLegacyCommand = options.useLegacyCommand;
953
+ this._timeout = options.timeout;
954
+ this._keepAlive = options.keepAlive;
955
+ this._keepAliveDelay = options.keepAliveDelay;
956
+ this._sasl = options.sasl;
957
+ }
958
+ /** Current config version. -1 means no config has been fetched yet. */
959
+ get configVersion() {
960
+ return this._configVersion;
961
+ }
962
+ /** Whether auto discovery is currently running. */
963
+ get isRunning() {
964
+ return this._isRunning;
965
+ }
966
+ /** The configuration endpoint being used. */
967
+ get configEndpoint() {
968
+ return this._configEndpoint;
969
+ }
970
+ /**
971
+ * Start the auto discovery process.
972
+ * Performs an initial discovery, then starts the polling timer.
973
+ */
974
+ async start() {
975
+ if (this._isRunning) {
976
+ throw new Error("Auto discovery is already running");
977
+ }
978
+ this._isRunning = true;
979
+ let config;
980
+ try {
981
+ const configNode = await this.ensureConfigNode();
982
+ config = await this.fetchConfig(configNode);
983
+ } catch (error) {
984
+ this._isRunning = false;
985
+ throw error;
986
+ }
987
+ this._configVersion = config.version;
988
+ this.emit("autoDiscover", config);
989
+ this._pollingTimer = setInterval(() => {
990
+ void this.poll();
991
+ }, this._pollingInterval);
992
+ if (this._pollingTimer && typeof this._pollingTimer === "object" && "unref" in this._pollingTimer) {
993
+ this._pollingTimer.unref();
994
+ }
995
+ return config;
996
+ }
997
+ /**
998
+ * Stop the auto discovery process.
999
+ */
1000
+ async stop() {
1001
+ this._isRunning = false;
1002
+ if (this._pollingTimer) {
1003
+ clearInterval(this._pollingTimer);
1004
+ this._pollingTimer = void 0;
1005
+ }
1006
+ if (this._configNode) {
1007
+ await this._configNode.disconnect();
1008
+ this._configNode = void 0;
1009
+ }
1010
+ }
1011
+ /**
1012
+ * Perform a single discovery cycle.
1013
+ * Returns the ClusterConfig if the version has changed, or undefined if unchanged.
1014
+ */
1015
+ async discover() {
1016
+ const configNode = await this.ensureConfigNode();
1017
+ const config = await this.fetchConfig(configNode);
1018
+ if (config.version === this._configVersion) {
1019
+ return void 0;
1020
+ }
1021
+ this._configVersion = config.version;
1022
+ return config;
1023
+ }
1024
+ /**
1025
+ * Parse the raw response data from a config get cluster command.
1026
+ * The raw data is the value content between the CONFIG/VALUE header and END.
1027
+ * Format: "<version>\n<host1>|<ip1>|<port1> <host2>|<ip2>|<port2>\n"
1028
+ */
1029
+ static parseConfigResponse(rawData) {
1030
+ if (!rawData || rawData.length === 0) {
1031
+ throw new Error("Empty config response");
1032
+ }
1033
+ const data = rawData.join("");
1034
+ const lines = data.split("\n").filter((line) => line.trim().length > 0);
1035
+ if (lines.length < 2) {
1036
+ throw new Error(
1037
+ "Invalid config response: expected version and node list"
1038
+ );
1039
+ }
1040
+ const version = Number.parseInt(lines[0].trim(), 10);
1041
+ if (Number.isNaN(version)) {
1042
+ throw new Error(`Invalid config version: ${lines[0]}`);
1043
+ }
1044
+ const nodeEntries = lines[1].trim().split(" ").filter((e) => e.length > 0);
1045
+ const nodes = nodeEntries.map(
1046
+ (entry) => _AutoDiscovery.parseNodeEntry(entry)
1047
+ );
1048
+ return { version, nodes };
1049
+ }
1050
+ /**
1051
+ * Parse a single node entry in the format "hostname|ip|port".
1052
+ */
1053
+ static parseNodeEntry(entry) {
1054
+ const parts = entry.split("|");
1055
+ if (parts.length !== 3) {
1056
+ throw new Error(`Invalid node entry format: ${entry}`);
1057
+ }
1058
+ const hostname = parts[0];
1059
+ const ip = parts[1];
1060
+ const port = Number.parseInt(parts[2], 10);
1061
+ if (!hostname) {
1062
+ throw new Error(`Invalid node entry: missing hostname in ${entry}`);
1063
+ }
1064
+ if (Number.isNaN(port) || port <= 0 || port > 65535) {
1065
+ throw new Error(`Invalid port in node entry: ${entry}`);
1066
+ }
1067
+ return { hostname, ip, port };
1068
+ }
1069
+ /**
1070
+ * Build a node ID from a DiscoveredNode.
1071
+ * Prefers IP when available, falls back to hostname.
1072
+ */
1073
+ static nodeId(node) {
1074
+ const host = node.ip || node.hostname;
1075
+ const wrappedHost = host.includes(":") ? `[${host}]` : host;
1076
+ return `${wrappedHost}:${node.port}`;
1077
+ }
1078
+ async ensureConfigNode() {
1079
+ if (this._configNode?.isConnected()) {
1080
+ return this._configNode;
1081
+ }
1082
+ const { host, port } = this.parseEndpoint(this._configEndpoint);
1083
+ this._configNode = new MemcacheNode(host, port, {
1084
+ timeout: this._timeout,
1085
+ keepAlive: this._keepAlive,
1086
+ keepAliveDelay: this._keepAliveDelay,
1087
+ sasl: this._sasl
1088
+ });
1089
+ await this._configNode.connect();
1090
+ return this._configNode;
1091
+ }
1092
+ async fetchConfig(node) {
1093
+ if (!node.isConnected()) {
1094
+ await node.connect();
1095
+ }
1096
+ if (this._useLegacyCommand) {
1097
+ const result2 = await node.command("get AmazonElastiCache:cluster", {
1098
+ isMultiline: true,
1099
+ requestedKeys: ["AmazonElastiCache:cluster"]
1100
+ });
1101
+ if (!result2?.values || result2.values.length === 0) {
1102
+ throw new Error("No config data received from legacy command");
1103
+ }
1104
+ return _AutoDiscovery.parseConfigResponse(result2.values);
1105
+ }
1106
+ const result = await node.command("config get cluster", {
1107
+ isConfig: true
1108
+ });
1109
+ if (!result || result.length === 0) {
1110
+ throw new Error("No config data received");
1111
+ }
1112
+ return _AutoDiscovery.parseConfigResponse(result);
1113
+ }
1114
+ async poll() {
1115
+ if (this._isPolling) {
1116
+ return;
1117
+ }
1118
+ this._isPolling = true;
1119
+ try {
1120
+ const config = await this.discover();
1121
+ if (config) {
1122
+ this.emit("autoDiscoverUpdate", config);
1123
+ }
1124
+ } catch (error) {
1125
+ this.emit("autoDiscoverError", error);
1126
+ try {
1127
+ if (this._configNode && !this._configNode.isConnected()) {
1128
+ await this._configNode.reconnect();
1129
+ }
1130
+ } catch {
1131
+ }
1132
+ } finally {
1133
+ this._isPolling = false;
1134
+ }
1135
+ }
1136
+ parseEndpoint(endpoint) {
1137
+ if (endpoint.startsWith("[")) {
1138
+ const bracketEnd = endpoint.indexOf("]");
1139
+ if (bracketEnd === -1) {
1140
+ throw new Error("Invalid IPv6 endpoint: missing closing bracket");
1141
+ }
1142
+ const host2 = endpoint.slice(1, bracketEnd);
1143
+ const remainder = endpoint.slice(bracketEnd + 1);
1144
+ if (remainder === "" || remainder === ":") {
1145
+ return { host: host2, port: 11211 };
1146
+ }
1147
+ if (remainder.startsWith(":")) {
1148
+ const port2 = Number.parseInt(remainder.slice(1), 10);
1149
+ return { host: host2, port: Number.isNaN(port2) ? 11211 : port2 };
1150
+ }
1151
+ return { host: host2, port: 11211 };
1152
+ }
1153
+ const colonIndex = endpoint.lastIndexOf(":");
1154
+ if (colonIndex === -1) {
1155
+ return { host: endpoint, port: 11211 };
1156
+ }
1157
+ const host = endpoint.slice(0, colonIndex);
1158
+ const port = Number.parseInt(endpoint.slice(colonIndex + 1), 10);
1159
+ return { host, port: Number.isNaN(port) ? 11211 : port };
1160
+ }
1161
+ };
1162
+
1163
+ // src/ketama.ts
1164
+ var import_node_crypto = require("crypto");
1165
+ var hashFunctionForBuiltin = (algorithm) => (value) => (0, import_node_crypto.createHash)(algorithm).update(value).digest().readInt32BE();
1166
+ var keyFor = (node) => typeof node === "string" ? node : node.key;
1167
+ var HashRing = class _HashRing {
1168
+ /**
1169
+ * Base weight of each node in the hash ring. Having a base weight of 1 is
1170
+ * not very desirable, since then, due to the ketama-style "clock", it's
1171
+ * possible to end up with a clock that's very skewed when dealing with a
1172
+ * small number of nodes. Setting to 50 nodes seems to give a better
1173
+ * distribution, so that load is spread roughly evenly to +/- 5%.
1174
+ */
1175
+ static baseWeight = 50;
1176
+ /** The hash function used to compute node positions on the ring */
1177
+ hashFn;
1178
+ /** The sorted array of [hash, node key] tuples representing virtual nodes on the ring */
1179
+ _clock = [];
1180
+ /** Map of node keys to actual node objects */
1181
+ _nodes = /* @__PURE__ */ new Map();
1182
+ /**
1183
+ * Gets the sorted array of [hash, node key] tuples representing virtual nodes on the ring.
1184
+ * @returns The hash clock array
1185
+ */
1186
+ get clock() {
1187
+ return this._clock;
1188
+ }
1189
+ /**
1190
+ * Gets the map of node keys to actual node objects.
1191
+ * @returns The nodes map
1192
+ */
1193
+ get nodes() {
1194
+ return this._nodes;
1195
+ }
1196
+ /**
1197
+ * Creates a new HashRing instance.
1198
+ *
1199
+ * @param initialNodes - Array of nodes to add to the ring, optionally with weights
1200
+ * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
1201
+ *
1202
+ * @example
1203
+ * ```typescript
1204
+ * // Simple ring with default SHA-1 hashing
1205
+ * const ring = new HashRing(['node1', 'node2']);
1206
+ *
1207
+ * // Ring with custom hash function
1208
+ * const customRing = new HashRing(['node1', 'node2'], 'md5');
1209
+ *
1210
+ * // Ring with weighted nodes
1211
+ * const weightedRing = new HashRing([
1212
+ * { node: 'heavy-server', weight: 3 },
1213
+ * { node: 'light-server', weight: 1 }
1214
+ * ]);
1215
+ * ```
1216
+ */
1217
+ constructor(initialNodes = [], hashFn = "sha1") {
1218
+ this.hashFn = typeof hashFn === "string" ? hashFunctionForBuiltin(hashFn) : hashFn;
1219
+ for (const node of initialNodes) {
1220
+ if (typeof node === "object" && "weight" in node && "node" in node) {
1221
+ this.addNode(node.node, node.weight);
1222
+ } else {
1223
+ this.addNode(node);
1224
+ }
1225
+ }
1226
+ }
1227
+ /**
1228
+ * Add a new node to the ring. If the node already exists in the ring, it
1229
+ * will be updated. For example, you can use this to update the node's weight.
1230
+ *
1231
+ * @param node - The node to add to the ring
1232
+ * @param weight - The relative weight of this node (default: 1). Higher weights mean more keys will be assigned to this node. A weight of 0 removes the node.
1233
+ * @throws {RangeError} If weight is negative
1234
+ *
1235
+ * @example
1236
+ * ```typescript
1237
+ * const ring = new HashRing();
1238
+ * ring.addNode('server1'); // Add with default weight of 1
1239
+ * ring.addNode('server2', 2); // Add with weight of 2 (will handle ~2x more keys)
1240
+ * ring.addNode('server1', 3); // Update server1's weight to 3
1241
+ * ring.addNode('server2', 0); // Remove server2
1242
+ * ```
1243
+ */
1244
+ addNode(node, weight = 1) {
1245
+ if (weight === 0) {
1246
+ this.removeNode(node);
1247
+ } else if (weight < 0) {
1248
+ throw new RangeError("Cannot add a node to the hashring with weight < 0");
1249
+ } else {
1250
+ this.removeNode(node);
1251
+ const key = keyFor(node);
1252
+ this._nodes.set(key, node);
1253
+ this.addNodeToClock(key, Math.round(weight * _HashRing.baseWeight));
1254
+ }
1255
+ }
1256
+ /**
1257
+ * Removes the node from the ring. No-op if the node does not exist.
1258
+ *
1259
+ * @param node - The node to remove from the ring
1260
+ *
1261
+ * @example
1262
+ * ```typescript
1263
+ * const ring = new HashRing(['server1', 'server2']);
1264
+ * ring.removeNode('server1'); // Removes server1 from the ring
1265
+ * ring.removeNode('nonexistent'); // Safe to call with non-existent node
1266
+ * ```
1267
+ */
1268
+ removeNode(node) {
1269
+ const key = keyFor(node);
1270
+ if (this._nodes.delete(key)) {
1271
+ this._clock = this._clock.filter(([, n]) => n !== key);
1272
+ }
1273
+ }
1274
+ /**
1275
+ * Gets the node which should handle the given input key. Returns undefined if
1276
+ * the hashring has no nodes.
1277
+ *
1278
+ * Uses consistent hashing to ensure the same input always maps to the same node,
1279
+ * and minimizes redistribution when nodes are added or removed.
1280
+ *
1281
+ * @param input - The key to find the responsible node for (string or Buffer)
1282
+ * @returns The node responsible for this key, or undefined if ring is empty
1283
+ *
1284
+ * @example
1285
+ * ```typescript
1286
+ * const ring = new HashRing(['server1', 'server2', 'server3']);
1287
+ * const node = ring.getNode('user:123'); // Returns e.g., 'server2'
1288
+ * const sameNode = ring.getNode('user:123'); // Always returns 'server2'
1289
+ *
1290
+ * // Also accepts Buffer input
1291
+ * const bufferNode = ring.getNode(Buffer.from('user:123'));
1292
+ * ```
1293
+ */
1294
+ getNode(input) {
1295
+ if (this._clock.length === 0) {
1296
+ return void 0;
1297
+ }
1298
+ const index = this.getIndexForInput(input);
1299
+ const key = index === this._clock.length ? this._clock[0][1] : this._clock[index][1];
1300
+ return this._nodes.get(key);
1301
+ }
1302
+ /**
1303
+ * Finds the index in the clock for the given input by hashing it and performing binary search.
1304
+ *
1305
+ * @param input - The input to find the clock position for
1306
+ * @returns The index in the clock array
1307
+ */
1308
+ getIndexForInput(input) {
1309
+ const hash = this.hashFn(
1310
+ typeof input === "string" ? Buffer.from(input) : input
1311
+ );
1312
+ return binarySearchRing(this._clock, hash);
1313
+ }
1314
+ /**
1315
+ * Gets multiple replica nodes that should handle the given input. Useful for
1316
+ * implementing replication strategies where you want to store data on multiple nodes.
1317
+ *
1318
+ * The returned array will contain unique nodes in the order they appear on the ring
1319
+ * starting from the primary node. If there are fewer nodes than replicas requested,
1320
+ * all nodes are returned.
1321
+ *
1322
+ * @param input - The key to find replica nodes for (string or Buffer)
1323
+ * @param replicas - The number of replica nodes to return
1324
+ * @returns Array of nodes that should handle this key (length ≤ replicas)
1325
+ *
1326
+ * @example
1327
+ * ```typescript
1328
+ * const ring = new HashRing(['server1', 'server2', 'server3', 'server4']);
1329
+ *
1330
+ * // Get 3 replicas for a key
1331
+ * const replicas = ring.getNodes('user:123', 3);
1332
+ * // Returns e.g., ['server2', 'server4', 'server1']
1333
+ *
1334
+ * // If requesting more replicas than nodes, returns all nodes
1335
+ * const allNodes = ring.getNodes('user:123', 10);
1336
+ * // Returns ['server1', 'server2', 'server3', 'server4']
1337
+ * ```
1338
+ */
1339
+ getNodes(input, replicas) {
1340
+ if (this._clock.length === 0) {
1341
+ return [];
1342
+ }
1343
+ if (replicas >= this._nodes.size) {
1344
+ return [...this._nodes.values()];
1345
+ }
1346
+ const chosen = /* @__PURE__ */ new Set();
1347
+ for (let i = this.getIndexForInput(input); chosen.size < replicas; i++) {
1348
+ chosen.add(this._clock[i % this._clock.length][1]);
1349
+ }
1350
+ return [...chosen].map((c) => this._nodes.get(c));
1351
+ }
1352
+ /**
1353
+ * Adds virtual nodes to the clock for the given node key.
1354
+ * Creates multiple positions on the ring for better distribution.
1355
+ *
1356
+ * @param key - The node key to add to the clock
1357
+ * @param weight - The number of virtual nodes to create (weight * baseWeight)
1358
+ */
1359
+ addNodeToClock(key, weight) {
1360
+ for (let i = weight; i > 0; i--) {
1361
+ const hash = this.hashFn(Buffer.from(`${key}\0${i}`));
1362
+ this._clock.push([hash, key]);
1363
+ }
1364
+ this._clock.sort((a, b) => a[0] - b[0]);
1365
+ }
1366
+ };
1367
+ var KetamaHash = class {
1368
+ /** The name of this distribution strategy */
1369
+ name = "ketama";
1370
+ /** Internal hash ring for consistent hashing */
1371
+ hashRing;
1372
+ /** Map of node IDs to MemcacheNode instances */
1373
+ nodeMap;
1374
+ /**
1375
+ * Creates a new KetamaDistributionHash instance.
1376
+ *
1377
+ * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
1378
+ *
1379
+ * @example
1380
+ * ```typescript
1381
+ * // Use default SHA-1 hashing
1382
+ * const distribution = new KetamaDistributionHash();
1383
+ *
1384
+ * // Use MD5 hashing
1385
+ * const distribution = new KetamaDistributionHash('md5');
1386
+ * ```
1387
+ */
1388
+ constructor(hashFn) {
1389
+ this.hashRing = new HashRing([], hashFn);
1390
+ this.nodeMap = /* @__PURE__ */ new Map();
1391
+ }
1392
+ /**
1393
+ * Gets all nodes in the distribution.
1394
+ * @returns Array of all MemcacheNode instances
1395
+ */
1396
+ get nodes() {
1397
+ return Array.from(this.nodeMap.values());
1398
+ }
1399
+ /**
1400
+ * Adds a node to the distribution with its weight for consistent hashing.
1401
+ *
1402
+ * @param node - The MemcacheNode to add
1403
+ *
1404
+ * @example
1405
+ * ```typescript
1406
+ * const node = new MemcacheNode('localhost', 11211, { weight: 2 });
1407
+ * distribution.addNode(node);
1408
+ * ```
1409
+ */
1410
+ addNode(node) {
1411
+ this.nodeMap.set(node.id, node);
1412
+ this.hashRing.addNode(node.id, node.weight);
1413
+ }
1414
+ /**
1415
+ * Removes a node from the distribution by its ID.
1416
+ *
1417
+ * @param id - The node ID (e.g., "localhost:11211")
1418
+ *
1419
+ * @example
1420
+ * ```typescript
1421
+ * distribution.removeNode('localhost:11211');
1422
+ * ```
1423
+ */
1424
+ removeNode(id) {
1425
+ this.nodeMap.delete(id);
1426
+ this.hashRing.removeNode(id);
1427
+ }
1428
+ /**
1429
+ * Gets a specific node by its ID.
1430
+ *
1431
+ * @param id - The node ID (e.g., "localhost:11211")
1432
+ * @returns The MemcacheNode if found, undefined otherwise
1433
+ *
1434
+ * @example
1435
+ * ```typescript
1436
+ * const node = distribution.getNode('localhost:11211');
1437
+ * if (node) {
1438
+ * console.log(`Found node: ${node.uri}`);
1439
+ * }
1440
+ * ```
1441
+ */
1442
+ getNode(id) {
1443
+ return this.nodeMap.get(id);
1444
+ }
1445
+ /**
1446
+ * Gets the nodes responsible for a given key using consistent hashing.
1447
+ * Currently returns a single node (the primary node for the key).
1448
+ *
1449
+ * @param key - The cache key to find the responsible node for
1450
+ * @returns Array containing the responsible node(s), empty if no nodes available
1451
+ *
1452
+ * @example
1453
+ * ```typescript
1454
+ * const nodes = distribution.getNodesByKey('user:123');
1455
+ * if (nodes.length > 0) {
1456
+ * console.log(`Key will be stored on: ${nodes[0].id}`);
1457
+ * }
1458
+ * ```
1459
+ */
1460
+ getNodesByKey(key) {
1461
+ const nodeId = this.hashRing.getNode(key);
1462
+ if (!nodeId) {
1463
+ return [];
1464
+ }
1465
+ const node = this.nodeMap.get(nodeId);
1466
+ return node ? [node] : [];
1467
+ }
1468
+ };
1469
+ function binarySearchRing(ring, hash) {
1470
+ let mid;
1471
+ let lo = 0;
1472
+ let hi = ring.length - 1;
1473
+ while (lo <= hi) {
1474
+ mid = Math.floor((lo + hi) / 2);
1475
+ if (ring[mid][0] >= hash) {
1476
+ hi = mid - 1;
651
1477
  } else {
652
- if (line === "STORED" || line === "DELETED" || line === "OK" || line === "TOUCHED" || line === "EXISTS" || line === "NOT_FOUND") {
653
- this._currentCommand.resolve(line);
654
- } else if (line === "NOT_STORED") {
655
- this._currentCommand.resolve(false);
656
- } else if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
657
- this._currentCommand.reject(new Error(line));
658
- } else if (/^\d+$/.test(line)) {
659
- this._currentCommand.resolve(parseInt(line, 10));
660
- } else {
661
- this._currentCommand.resolve(line);
662
- }
663
- this._currentCommand = void 0;
1478
+ lo = mid + 1;
664
1479
  }
665
1480
  }
666
- rejectPendingCommands(error) {
667
- if (this._currentCommand) {
668
- this._currentCommand.reject(error);
669
- this._currentCommand = void 0;
1481
+ return lo;
1482
+ }
1483
+
1484
+ // src/modula.ts
1485
+ var import_node_crypto2 = require("crypto");
1486
+ var hashFunctionForBuiltin2 = (algorithm) => (value) => (0, import_node_crypto2.createHash)(algorithm).update(value).digest().readUInt32BE(0);
1487
+ var ModulaHash = class {
1488
+ /** The name of this distribution strategy */
1489
+ name = "modula";
1490
+ /** The hash function used to compute key hashes */
1491
+ hashFn;
1492
+ /** Map of node IDs to MemcacheNode instances */
1493
+ nodeMap;
1494
+ /**
1495
+ * Weighted list of node IDs for modulo distribution.
1496
+ * Nodes with higher weights appear multiple times.
1497
+ */
1498
+ nodeList;
1499
+ /**
1500
+ * Creates a new ModulaHash instance.
1501
+ *
1502
+ * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
1503
+ *
1504
+ * @example
1505
+ * ```typescript
1506
+ * // Use default SHA-1 hashing
1507
+ * const distribution = new ModulaHash();
1508
+ *
1509
+ * // Use MD5 hashing
1510
+ * const distribution = new ModulaHash('md5');
1511
+ *
1512
+ * // Use custom hash function
1513
+ * const distribution = new ModulaHash((buf) => buf.readUInt32BE(0));
1514
+ * ```
1515
+ */
1516
+ constructor(hashFn) {
1517
+ this.hashFn = typeof hashFn === "string" ? hashFunctionForBuiltin2(hashFn) : hashFn ?? hashFunctionForBuiltin2("sha1");
1518
+ this.nodeMap = /* @__PURE__ */ new Map();
1519
+ this.nodeList = [];
1520
+ }
1521
+ /**
1522
+ * Gets all nodes in the distribution.
1523
+ * @returns Array of all MemcacheNode instances
1524
+ */
1525
+ get nodes() {
1526
+ return Array.from(this.nodeMap.values());
1527
+ }
1528
+ /**
1529
+ * Adds a node to the distribution with its weight.
1530
+ * Weight determines how many times the node appears in the distribution list.
1531
+ *
1532
+ * @param node - The MemcacheNode to add
1533
+ *
1534
+ * @example
1535
+ * ```typescript
1536
+ * const node = new MemcacheNode('localhost', 11211, { weight: 2 });
1537
+ * distribution.addNode(node);
1538
+ * ```
1539
+ */
1540
+ addNode(node) {
1541
+ this.nodeMap.set(node.id, node);
1542
+ const weight = node.weight || 1;
1543
+ for (let i = 0; i < weight; i++) {
1544
+ this.nodeList.push(node.id);
670
1545
  }
671
- while (this._commandQueue.length > 0) {
672
- const cmd = this._commandQueue.shift();
673
- if (cmd) {
674
- cmd.reject(error);
675
- }
1546
+ }
1547
+ /**
1548
+ * Removes a node from the distribution by its ID.
1549
+ *
1550
+ * @param id - The node ID (e.g., "localhost:11211")
1551
+ *
1552
+ * @example
1553
+ * ```typescript
1554
+ * distribution.removeNode('localhost:11211');
1555
+ * ```
1556
+ */
1557
+ removeNode(id) {
1558
+ this.nodeMap.delete(id);
1559
+ this.nodeList = this.nodeList.filter((nodeId) => nodeId !== id);
1560
+ }
1561
+ /**
1562
+ * Gets a specific node by its ID.
1563
+ *
1564
+ * @param id - The node ID (e.g., "localhost:11211")
1565
+ * @returns The MemcacheNode if found, undefined otherwise
1566
+ *
1567
+ * @example
1568
+ * ```typescript
1569
+ * const node = distribution.getNode('localhost:11211');
1570
+ * if (node) {
1571
+ * console.log(`Found node: ${node.uri}`);
1572
+ * }
1573
+ * ```
1574
+ */
1575
+ getNode(id) {
1576
+ return this.nodeMap.get(id);
1577
+ }
1578
+ /**
1579
+ * Gets the nodes responsible for a given key using modulo hashing.
1580
+ * Uses `hash(key) % nodeCount` to determine the target node.
1581
+ *
1582
+ * @param key - The cache key to find the responsible node for
1583
+ * @returns Array containing the responsible node(s), empty if no nodes available
1584
+ *
1585
+ * @example
1586
+ * ```typescript
1587
+ * const nodes = distribution.getNodesByKey('user:123');
1588
+ * if (nodes.length > 0) {
1589
+ * console.log(`Key will be stored on: ${nodes[0].id}`);
1590
+ * }
1591
+ * ```
1592
+ */
1593
+ getNodesByKey(key) {
1594
+ if (this.nodeList.length === 0) {
1595
+ return [];
676
1596
  }
1597
+ const hash = this.hashFn(Buffer.from(key));
1598
+ const index = hash % this.nodeList.length;
1599
+ const nodeId = this.nodeList[index];
1600
+ const node = this.nodeMap.get(nodeId);
1601
+ return node ? [node] : [];
677
1602
  }
678
1603
  };
679
- function createNode(host, port, options) {
680
- return new MemcacheNode(host, port, options);
681
- }
682
1604
 
683
- // src/index.ts
1605
+ // src/types.ts
684
1606
  var MemcacheEvents = /* @__PURE__ */ ((MemcacheEvents2) => {
685
1607
  MemcacheEvents2["CONNECT"] = "connect";
686
1608
  MemcacheEvents2["QUIT"] = "quit";
@@ -691,26 +1613,52 @@ var MemcacheEvents = /* @__PURE__ */ ((MemcacheEvents2) => {
691
1613
  MemcacheEvents2["INFO"] = "info";
692
1614
  MemcacheEvents2["TIMEOUT"] = "timeout";
693
1615
  MemcacheEvents2["CLOSE"] = "close";
1616
+ MemcacheEvents2["AUTO_DISCOVER"] = "autoDiscover";
1617
+ MemcacheEvents2["AUTO_DISCOVER_ERROR"] = "autoDiscoverError";
1618
+ MemcacheEvents2["AUTO_DISCOVER_UPDATE"] = "autoDiscoverUpdate";
694
1619
  return MemcacheEvents2;
695
1620
  })(MemcacheEvents || {});
696
- var Memcache = class extends import_hookified2.Hookified {
1621
+
1622
+ // src/index.ts
1623
+ var defaultRetryBackoff = (_attempt, baseDelay) => baseDelay;
1624
+ var exponentialRetryBackoff = (attempt, baseDelay) => baseDelay * 2 ** attempt;
1625
+ var Memcache = class extends import_hookified3.Hookified {
697
1626
  _nodes = [];
698
1627
  _timeout;
699
1628
  _keepAlive;
700
1629
  _keepAliveDelay;
701
1630
  _hash;
1631
+ _retries;
1632
+ _retryDelay;
1633
+ _retryBackoff;
1634
+ _retryOnlyIdempotent;
1635
+ _sasl;
1636
+ _autoDiscovery;
1637
+ _autoDiscoverOptions;
702
1638
  constructor(options) {
703
1639
  super();
704
- this._hash = new KetamaHash();
705
1640
  if (typeof options === "string") {
1641
+ this._hash = new KetamaHash();
706
1642
  this._timeout = 5e3;
707
1643
  this._keepAlive = true;
708
1644
  this._keepAliveDelay = 1e3;
1645
+ this._retries = 0;
1646
+ this._retryDelay = 100;
1647
+ this._retryBackoff = defaultRetryBackoff;
1648
+ this._retryOnlyIdempotent = true;
1649
+ this._sasl = void 0;
709
1650
  this.addNode(options);
710
1651
  } else {
1652
+ this._hash = options?.hash ?? new KetamaHash();
711
1653
  this._timeout = options?.timeout || 5e3;
712
1654
  this._keepAlive = options?.keepAlive !== false;
713
1655
  this._keepAliveDelay = options?.keepAliveDelay || 1e3;
1656
+ this._retries = options?.retries ?? 0;
1657
+ this._retryDelay = options?.retryDelay ?? 100;
1658
+ this._retryBackoff = options?.retryBackoff ?? defaultRetryBackoff;
1659
+ this._retryOnlyIdempotent = options?.retryOnlyIdempotent ?? true;
1660
+ this._sasl = options?.sasl;
1661
+ this._autoDiscoverOptions = options?.autoDiscover;
714
1662
  const nodeUris = options?.nodes || ["localhost:11211"];
715
1663
  for (const nodeUri of nodeUris) {
716
1664
  this.addNode(nodeUri);
@@ -815,6 +1763,74 @@ var Memcache = class extends import_hookified2.Hookified {
815
1763
  this._keepAliveDelay = value;
816
1764
  this.updateNodes();
817
1765
  }
1766
+ /**
1767
+ * Get the number of retry attempts for failed commands.
1768
+ * @returns {number}
1769
+ * @default 0
1770
+ */
1771
+ get retries() {
1772
+ return this._retries;
1773
+ }
1774
+ /**
1775
+ * Set the number of retry attempts for failed commands.
1776
+ * Set to 0 to disable retries.
1777
+ * @param {number} value
1778
+ * @default 0
1779
+ */
1780
+ set retries(value) {
1781
+ this._retries = Math.max(0, Math.floor(value));
1782
+ }
1783
+ /**
1784
+ * Get the base delay in milliseconds between retry attempts.
1785
+ * @returns {number}
1786
+ * @default 100
1787
+ */
1788
+ get retryDelay() {
1789
+ return this._retryDelay;
1790
+ }
1791
+ /**
1792
+ * Set the base delay in milliseconds between retry attempts.
1793
+ * @param {number} value
1794
+ * @default 100
1795
+ */
1796
+ set retryDelay(value) {
1797
+ this._retryDelay = Math.max(0, value);
1798
+ }
1799
+ /**
1800
+ * Get the backoff function for retry delays.
1801
+ * @returns {RetryBackoffFunction}
1802
+ * @default defaultRetryBackoff
1803
+ */
1804
+ get retryBackoff() {
1805
+ return this._retryBackoff;
1806
+ }
1807
+ /**
1808
+ * Set the backoff function for retry delays.
1809
+ * @param {RetryBackoffFunction} value
1810
+ * @default defaultRetryBackoff
1811
+ */
1812
+ set retryBackoff(value) {
1813
+ this._retryBackoff = value;
1814
+ }
1815
+ /**
1816
+ * Get whether retries are restricted to idempotent commands only.
1817
+ * @returns {boolean}
1818
+ * @default true
1819
+ */
1820
+ get retryOnlyIdempotent() {
1821
+ return this._retryOnlyIdempotent;
1822
+ }
1823
+ /**
1824
+ * Set whether retries are restricted to idempotent commands only.
1825
+ * When true (default), retries only occur for commands explicitly marked
1826
+ * as idempotent via ExecuteOptions. This prevents accidental double-execution
1827
+ * of non-idempotent operations like incr, decr, append, etc.
1828
+ * @param {boolean} value
1829
+ * @default true
1830
+ */
1831
+ set retryOnlyIdempotent(value) {
1832
+ this._retryOnlyIdempotent = value;
1833
+ }
818
1834
  /**
819
1835
  * Get an array of all MemcacheNode instances
820
1836
  * @returns {MemcacheNode[]}
@@ -848,7 +1864,8 @@ var Memcache = class extends import_hookified2.Hookified {
848
1864
  timeout: this._timeout,
849
1865
  keepAlive: this._keepAlive,
850
1866
  keepAliveDelay: this._keepAliveDelay,
851
- weight
1867
+ weight,
1868
+ sasl: this._sasl
852
1869
  });
853
1870
  } else {
854
1871
  node = uri;
@@ -957,6 +1974,9 @@ var Memcache = class extends import_hookified2.Hookified {
957
1974
  return;
958
1975
  }
959
1976
  await Promise.all(this._nodes.map((node) => node.connect()));
1977
+ if (this._autoDiscoverOptions?.enabled && !this._autoDiscovery) {
1978
+ await this.startAutoDiscovery();
1979
+ }
960
1980
  }
961
1981
  /**
962
1982
  * Get a value from the Memcache server.
@@ -1316,6 +2336,10 @@ ${valueStr}`;
1316
2336
  * @returns {Promise<void>}
1317
2337
  */
1318
2338
  async quit() {
2339
+ if (this._autoDiscovery) {
2340
+ await this._autoDiscovery.stop();
2341
+ this._autoDiscovery = void 0;
2342
+ }
1319
2343
  await Promise.all(
1320
2344
  this._nodes.map(async (node) => {
1321
2345
  if (node.isConnected()) {
@@ -1329,6 +2353,10 @@ ${valueStr}`;
1329
2353
  * @returns {Promise<void>}
1330
2354
  */
1331
2355
  async disconnect() {
2356
+ if (this._autoDiscovery) {
2357
+ await this._autoDiscovery.stop();
2358
+ this._autoDiscovery = void 0;
2359
+ }
1332
2360
  await Promise.all(this._nodes.map((node) => node.disconnect()));
1333
2361
  }
1334
2362
  /**
@@ -1366,19 +2394,27 @@ ${valueStr}`;
1366
2394
  return nodes;
1367
2395
  }
1368
2396
  /**
1369
- * Execute a command on the specified nodes.
2397
+ * Execute a command on the specified nodes with retry support.
1370
2398
  * @param {string} command - The memcache command string to execute
1371
2399
  * @param {MemcacheNode[]} nodes - Array of MemcacheNode instances to execute on
1372
- * @param {ExecuteOptions} options - Optional execution options
2400
+ * @param {ExecuteOptions} options - Optional execution options including retry overrides
1373
2401
  * @returns {Promise<unknown[]>} Promise resolving to array of results from each node
1374
2402
  */
1375
2403
  async execute(command, nodes, options) {
2404
+ const configuredRetries = options?.retries ?? this._retries;
2405
+ const retryDelay = options?.retryDelay ?? this._retryDelay;
2406
+ const retryBackoff = options?.retryBackoff ?? this._retryBackoff;
2407
+ const isIdempotent = options?.idempotent === true;
2408
+ const maxRetries = this._retryOnlyIdempotent && !isIdempotent ? 0 : configuredRetries;
1376
2409
  const promises = nodes.map(async (node) => {
1377
- try {
1378
- return await node.command(command, options?.commandOptions);
1379
- } catch {
1380
- return void 0;
1381
- }
2410
+ return this.executeWithRetry(
2411
+ node,
2412
+ command,
2413
+ options?.commandOptions,
2414
+ maxRetries,
2415
+ retryDelay,
2416
+ retryBackoff
2417
+ );
1382
2418
  });
1383
2419
  return Promise.all(promises);
1384
2420
  }
@@ -1409,6 +2445,46 @@ ${valueStr}`;
1409
2445
  }
1410
2446
  }
1411
2447
  // Private methods
2448
+ /**
2449
+ * Sleep utility for retry delays.
2450
+ * @param {number} ms - Milliseconds to sleep
2451
+ * @returns {Promise<void>}
2452
+ */
2453
+ sleep(ms) {
2454
+ return new Promise((resolve) => setTimeout(resolve, ms));
2455
+ }
2456
+ /**
2457
+ * Execute a command on a single node with retry logic.
2458
+ * @param {MemcacheNode} node - The node to execute on
2459
+ * @param {string} command - The command string
2460
+ * @param {CommandOptions} commandOptions - Optional command options
2461
+ * @param {number} maxRetries - Maximum number of retry attempts
2462
+ * @param {number} retryDelay - Base delay between retries in milliseconds
2463
+ * @param {RetryBackoffFunction} retryBackoff - Function to calculate backoff delay
2464
+ * @returns {Promise<unknown>} Result or undefined on failure
2465
+ */
2466
+ async executeWithRetry(node, command, commandOptions, maxRetries, retryDelay, retryBackoff) {
2467
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
2468
+ try {
2469
+ return await node.command(command, commandOptions);
2470
+ } catch {
2471
+ if (attempt >= maxRetries) {
2472
+ break;
2473
+ }
2474
+ const delay = retryBackoff(attempt, retryDelay);
2475
+ if (delay > 0) {
2476
+ await this.sleep(delay);
2477
+ }
2478
+ if (!node.isConnected()) {
2479
+ try {
2480
+ await node.connect();
2481
+ } catch {
2482
+ }
2483
+ }
2484
+ }
2485
+ }
2486
+ return void 0;
2487
+ }
1412
2488
  /**
1413
2489
  * Update all nodes with current keepAlive settings
1414
2490
  */
@@ -1435,12 +2511,92 @@ ${valueStr}`;
1435
2511
  );
1436
2512
  node.on("miss", (key) => this.emit("miss" /* MISS */, key));
1437
2513
  }
2514
+ async startAutoDiscovery() {
2515
+ const options = this._autoDiscoverOptions;
2516
+ if (!options) {
2517
+ return;
2518
+ }
2519
+ const configEndpoint = options.configEndpoint || (this._nodes.length > 0 ? this._nodes[0].id : "localhost:11211");
2520
+ this._autoDiscovery = new AutoDiscovery({
2521
+ configEndpoint,
2522
+ pollingInterval: options.pollingInterval ?? 6e4,
2523
+ useLegacyCommand: options.useLegacyCommand ?? false,
2524
+ timeout: this._timeout,
2525
+ keepAlive: this._keepAlive,
2526
+ keepAliveDelay: this._keepAliveDelay,
2527
+ sasl: this._sasl
2528
+ });
2529
+ this._autoDiscovery.on("autoDiscover", (config) => {
2530
+ this.emit("autoDiscover" /* AUTO_DISCOVER */, config);
2531
+ });
2532
+ this._autoDiscovery.on("autoDiscoverError", (error) => {
2533
+ this.emit("autoDiscoverError" /* AUTO_DISCOVER_ERROR */, error);
2534
+ });
2535
+ this._autoDiscovery.on(
2536
+ "autoDiscoverUpdate",
2537
+ /* v8 ignore next -- @preserve */
2538
+ async (config) => {
2539
+ this.emit("autoDiscoverUpdate" /* AUTO_DISCOVER_UPDATE */, config);
2540
+ try {
2541
+ await this.applyClusterConfig(config);
2542
+ } catch (error) {
2543
+ this.emit("autoDiscoverError" /* AUTO_DISCOVER_ERROR */, error);
2544
+ }
2545
+ }
2546
+ );
2547
+ try {
2548
+ const initialConfig = await this._autoDiscovery.start();
2549
+ await this.applyClusterConfig(initialConfig);
2550
+ } catch (error) {
2551
+ this.emit("autoDiscoverError" /* AUTO_DISCOVER_ERROR */, error);
2552
+ }
2553
+ }
2554
+ async applyClusterConfig(config) {
2555
+ if (config.nodes.length === 0) {
2556
+ this.emit(
2557
+ "autoDiscoverError" /* AUTO_DISCOVER_ERROR */,
2558
+ new Error("Discovery returned zero nodes; keeping current topology")
2559
+ );
2560
+ return;
2561
+ }
2562
+ const discoveredNodeIds = new Set(
2563
+ config.nodes.map((n) => AutoDiscovery.nodeId(n))
2564
+ );
2565
+ const currentNodeIds = new Set(this.nodeIds);
2566
+ for (const node of config.nodes) {
2567
+ const id = AutoDiscovery.nodeId(node);
2568
+ if (!currentNodeIds.has(id)) {
2569
+ try {
2570
+ const host = node.ip || node.hostname;
2571
+ const wrappedHost = host.includes(":") ? `[${host}]` : host;
2572
+ await this.addNode(`${wrappedHost}:${node.port}`);
2573
+ } catch (error) {
2574
+ this.emit("error" /* ERROR */, id, error);
2575
+ }
2576
+ }
2577
+ }
2578
+ for (const nodeId of currentNodeIds) {
2579
+ if (!discoveredNodeIds.has(nodeId)) {
2580
+ try {
2581
+ await this.removeNode(nodeId);
2582
+ } catch (error) {
2583
+ this.emit("error" /* ERROR */, nodeId, error);
2584
+ }
2585
+ }
2586
+ }
2587
+ }
1438
2588
  };
1439
2589
  var index_default = Memcache;
1440
2590
  // Annotate the CommonJS export names for ESM import in node:
1441
2591
  0 && (module.exports = {
2592
+ AutoDiscovery,
1442
2593
  Memcache,
1443
2594
  MemcacheEvents,
1444
- createNode
2595
+ MemcacheNode,
2596
+ ModulaHash,
2597
+ createNode,
2598
+ defaultRetryBackoff,
2599
+ exponentialRetryBackoff
1445
2600
  });
1446
2601
  /* v8 ignore next -- @preserve */
2602
+ /* v8 ignore next 3 -- @preserve */