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