memcache 1.3.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,6 +20,7 @@ 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,
25
26
  MemcacheNode: () => MemcacheNode,
@@ -30,1322 +31,1576 @@ __export(index_exports, {
30
31
  exponentialRetryBackoff: () => exponentialRetryBackoff
31
32
  });
32
33
  module.exports = __toCommonJS(index_exports);
34
+ var import_hookified3 = require("hookified");
35
+
36
+ // src/auto-discovery.ts
33
37
  var import_hookified2 = require("hookified");
34
38
 
35
- // src/ketama.ts
36
- var import_node_crypto = require("crypto");
37
- var hashFunctionForBuiltin = (algorithm) => (value) => (0, import_node_crypto.createHash)(algorithm).update(value).digest().readInt32BE();
38
- var keyFor = (node) => typeof node === "string" ? node : node.key;
39
- var HashRing = class _HashRing {
40
- /**
41
- * Base weight of each node in the hash ring. Having a base weight of 1 is
42
- * not very desirable, since then, due to the ketama-style "clock", it's
43
- * possible to end up with a clock that's very skewed when dealing with a
44
- * small number of nodes. Setting to 50 nodes seems to give a better
45
- * distribution, so that load is spread roughly evenly to +/- 5%.
46
- */
47
- static baseWeight = 50;
48
- /** The hash function used to compute node positions on the ring */
49
- hashFn;
50
- /** The sorted array of [hash, node key] tuples representing virtual nodes on the ring */
51
- _clock = [];
52
- /** Map of node keys to actual node objects */
53
- _nodes = /* @__PURE__ */ new Map();
54
- /**
55
- * Gets the sorted array of [hash, node key] tuples representing virtual nodes on the ring.
56
- * @returns The hash clock array
57
- */
58
- get clock() {
59
- return this._clock;
60
- }
61
- /**
62
- * Gets the map of node keys to actual node objects.
63
- * @returns The nodes map
64
- */
65
- get nodes() {
66
- return this._nodes;
67
- }
68
- /**
69
- * Creates a new HashRing instance.
70
- *
71
- * @param initialNodes - Array of nodes to add to the ring, optionally with weights
72
- * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
73
- *
74
- * @example
75
- * ```typescript
76
- * // Simple ring with default SHA-1 hashing
77
- * const ring = new HashRing(['node1', 'node2']);
78
- *
79
- * // Ring with custom hash function
80
- * const customRing = new HashRing(['node1', 'node2'], 'md5');
81
- *
82
- * // Ring with weighted nodes
83
- * const weightedRing = new HashRing([
84
- * { node: 'heavy-server', weight: 3 },
85
- * { node: 'light-server', weight: 1 }
86
- * ]);
87
- * ```
88
- */
89
- constructor(initialNodes = [], hashFn = "sha1") {
90
- this.hashFn = typeof hashFn === "string" ? hashFunctionForBuiltin(hashFn) : hashFn;
91
- for (const node of initialNodes) {
92
- if (typeof node === "object" && "weight" in node && "node" in node) {
93
- this.addNode(node.node, node.weight);
94
- } else {
95
- this.addNode(node);
96
- }
97
- }
98
- }
99
- /**
100
- * Add a new node to the ring. If the node already exists in the ring, it
101
- * will be updated. For example, you can use this to update the node's weight.
102
- *
103
- * @param node - The node to add to the ring
104
- * @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.
105
- * @throws {RangeError} If weight is negative
106
- *
107
- * @example
108
- * ```typescript
109
- * const ring = new HashRing();
110
- * ring.addNode('server1'); // Add with default weight of 1
111
- * ring.addNode('server2', 2); // Add with weight of 2 (will handle ~2x more keys)
112
- * ring.addNode('server1', 3); // Update server1's weight to 3
113
- * ring.addNode('server2', 0); // Remove server2
114
- * ```
115
- */
116
- addNode(node, weight = 1) {
117
- if (weight === 0) {
118
- this.removeNode(node);
119
- } else if (weight < 0) {
120
- throw new RangeError("Cannot add a node to the hashring with weight < 0");
121
- } else {
122
- this.removeNode(node);
123
- const key = keyFor(node);
124
- this._nodes.set(key, node);
125
- this.addNodeToClock(key, Math.round(weight * _HashRing.baseWeight));
126
- }
127
- }
128
- /**
129
- * Removes the node from the ring. No-op if the node does not exist.
130
- *
131
- * @param node - The node to remove from the ring
132
- *
133
- * @example
134
- * ```typescript
135
- * const ring = new HashRing(['server1', 'server2']);
136
- * ring.removeNode('server1'); // Removes server1 from the ring
137
- * ring.removeNode('nonexistent'); // Safe to call with non-existent node
138
- * ```
139
- */
140
- removeNode(node) {
141
- const key = keyFor(node);
142
- if (this._nodes.delete(key)) {
143
- this._clock = this._clock.filter(([, n]) => n !== key);
144
- }
145
- }
146
- /**
147
- * Gets the node which should handle the given input key. Returns undefined if
148
- * the hashring has no nodes.
149
- *
150
- * Uses consistent hashing to ensure the same input always maps to the same node,
151
- * and minimizes redistribution when nodes are added or removed.
152
- *
153
- * @param input - The key to find the responsible node for (string or Buffer)
154
- * @returns The node responsible for this key, or undefined if ring is empty
155
- *
156
- * @example
157
- * ```typescript
158
- * const ring = new HashRing(['server1', 'server2', 'server3']);
159
- * const node = ring.getNode('user:123'); // Returns e.g., 'server2'
160
- * const sameNode = ring.getNode('user:123'); // Always returns 'server2'
161
- *
162
- * // Also accepts Buffer input
163
- * const bufferNode = ring.getNode(Buffer.from('user:123'));
164
- * ```
165
- */
166
- getNode(input) {
167
- if (this._clock.length === 0) {
168
- return void 0;
169
- }
170
- const index = this.getIndexForInput(input);
171
- const key = index === this._clock.length ? this._clock[0][1] : this._clock[index][1];
172
- return this._nodes.get(key);
173
- }
174
- /**
175
- * Finds the index in the clock for the given input by hashing it and performing binary search.
176
- *
177
- * @param input - The input to find the clock position for
178
- * @returns The index in the clock array
179
- */
180
- getIndexForInput(input) {
181
- const hash = this.hashFn(
182
- typeof input === "string" ? Buffer.from(input) : input
183
- );
184
- return binarySearchRing(this._clock, hash);
185
- }
186
- /**
187
- * Gets multiple replica nodes that should handle the given input. Useful for
188
- * implementing replication strategies where you want to store data on multiple nodes.
189
- *
190
- * The returned array will contain unique nodes in the order they appear on the ring
191
- * starting from the primary node. If there are fewer nodes than replicas requested,
192
- * all nodes are returned.
193
- *
194
- * @param input - The key to find replica nodes for (string or Buffer)
195
- * @param replicas - The number of replica nodes to return
196
- * @returns Array of nodes that should handle this key (length ≤ replicas)
197
- *
198
- * @example
199
- * ```typescript
200
- * const ring = new HashRing(['server1', 'server2', 'server3', 'server4']);
201
- *
202
- * // Get 3 replicas for a key
203
- * const replicas = ring.getNodes('user:123', 3);
204
- * // Returns e.g., ['server2', 'server4', 'server1']
205
- *
206
- * // If requesting more replicas than nodes, returns all nodes
207
- * const allNodes = ring.getNodes('user:123', 10);
208
- * // Returns ['server1', 'server2', 'server3', 'server4']
209
- * ```
210
- */
211
- getNodes(input, replicas) {
212
- if (this._clock.length === 0) {
213
- return [];
214
- }
215
- if (replicas >= this._nodes.size) {
216
- return [...this._nodes.values()];
217
- }
218
- const chosen = /* @__PURE__ */ new Set();
219
- for (let i = this.getIndexForInput(input); chosen.size < replicas; i++) {
220
- chosen.add(this._clock[i % this._clock.length][1]);
221
- }
222
- return [...chosen].map((c) => this._nodes.get(c));
223
- }
224
- /**
225
- * Adds virtual nodes to the clock for the given node key.
226
- * Creates multiple positions on the ring for better distribution.
227
- *
228
- * @param key - The node key to add to the clock
229
- * @param weight - The number of virtual nodes to create (weight * baseWeight)
230
- */
231
- addNodeToClock(key, weight) {
232
- for (let i = weight; i > 0; i--) {
233
- const hash = this.hashFn(Buffer.from(`${key}\0${i}`));
234
- this._clock.push([hash, key]);
235
- }
236
- this._clock.sort((a, b) => a[0] - b[0]);
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);
237
76
  }
238
- };
239
- var KetamaHash = class {
240
- /** The name of this distribution strategy */
241
- name = "ketama";
242
- /** Internal hash ring for consistent hashing */
243
- hashRing;
244
- /** Map of node IDs to MemcacheNode instances */
245
- nodeMap;
246
- /**
247
- * Creates a new KetamaDistributionHash instance.
248
- *
249
- * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
250
- *
251
- * @example
252
- * ```typescript
253
- * // Use default SHA-1 hashing
254
- * const distribution = new KetamaDistributionHash();
255
- *
256
- * // Use MD5 hashing
257
- * const distribution = new KetamaDistributionHash('md5');
258
- * ```
259
- */
260
- constructor(hashFn) {
261
- this.hashRing = new HashRing([], hashFn);
262
- this.nodeMap = /* @__PURE__ */ new Map();
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]);
263
266
  }
264
- /**
265
- * Gets all nodes in the distribution.
266
- * @returns Array of all MemcacheNode instances
267
- */
268
- get nodes() {
269
- return Array.from(this.nodeMap.values());
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 };
270
282
  }
271
- /**
272
- * Adds a node to the distribution with its weight for consistent hashing.
273
- *
274
- * @param node - The MemcacheNode to add
275
- *
276
- * @example
277
- * ```typescript
278
- * const node = new MemcacheNode('localhost', 11211, { weight: 2 });
279
- * distribution.addNode(node);
280
- * ```
281
- */
282
- addNode(node) {
283
- this.nodeMap.set(node.id, node);
284
- this.hashRing.addNode(node.id, node.weight);
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 };
285
294
  }
286
- /**
287
- * Removes a node from the distribution by its ID.
288
- *
289
- * @param id - The node ID (e.g., "localhost:11211")
290
- *
291
- * @example
292
- * ```typescript
293
- * distribution.removeNode('localhost:11211');
294
- * ```
295
- */
296
- removeNode(id) {
297
- this.nodeMap.delete(id);
298
- this.hashRing.removeNode(id);
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;
299
328
  }
300
329
  /**
301
- * Gets a specific node by its ID.
302
- *
303
- * @param id - The node ID (e.g., "localhost:11211")
304
- * @returns The MemcacheNode if found, undefined otherwise
305
- *
306
- * @example
307
- * ```typescript
308
- * const node = distribution.getNode('localhost:11211');
309
- * if (node) {
310
- * console.log(`Found node: ${node.uri}`);
311
- * }
312
- * ```
330
+ * Get the host of this node
313
331
  */
314
- getNode(id) {
315
- return this.nodeMap.get(id);
332
+ get host() {
333
+ return this._host;
316
334
  }
317
335
  /**
318
- * Gets the nodes responsible for a given key using consistent hashing.
319
- * Currently returns a single node (the primary node for the key).
320
- *
321
- * @param key - The cache key to find the responsible node for
322
- * @returns Array containing the responsible node(s), empty if no nodes available
323
- *
324
- * @example
325
- * ```typescript
326
- * const nodes = distribution.getNodesByKey('user:123');
327
- * if (nodes.length > 0) {
328
- * console.log(`Key will be stored on: ${nodes[0].id}`);
329
- * }
330
- * ```
336
+ * Get the port of this node
331
337
  */
332
- getNodesByKey(key) {
333
- const nodeId = this.hashRing.getNode(key);
334
- if (!nodeId) {
335
- return [];
336
- }
337
- const node = this.nodeMap.get(nodeId);
338
- return node ? [node] : [];
338
+ get port() {
339
+ return this._port;
339
340
  }
340
- };
341
- function binarySearchRing(ring, hash) {
342
- let mid;
343
- let lo = 0;
344
- let hi = ring.length - 1;
345
- while (lo <= hi) {
346
- mid = Math.floor((lo + hi) / 2);
347
- if (ring[mid][0] >= hash) {
348
- hi = mid - 1;
349
- } else {
350
- lo = mid + 1;
341
+ /**
342
+ * Get the unique identifier for this node (host:port format)
343
+ */
344
+ get id() {
345
+ if (this._port === 0) {
346
+ return this._host;
351
347
  }
348
+ const host = this._host.includes(":") ? `[${this._host}]` : this._host;
349
+ return `${host}:${this._port}`;
352
350
  }
353
- return lo;
354
- }
355
-
356
- // src/modula.ts
357
- var import_node_crypto2 = require("crypto");
358
- var hashFunctionForBuiltin2 = (algorithm) => (value) => (0, import_node_crypto2.createHash)(algorithm).update(value).digest().readUInt32BE(0);
359
- var ModulaHash = class {
360
- /** The name of this distribution strategy */
361
- name = "modula";
362
- /** The hash function used to compute key hashes */
363
- hashFn;
364
- /** Map of node IDs to MemcacheNode instances */
365
- nodeMap;
366
351
  /**
367
- * Weighted list of node IDs for modulo distribution.
368
- * Nodes with higher weights appear multiple times.
352
+ * Get the full uri like memcache://localhost:11211
369
353
  */
370
- nodeList;
354
+ get uri() {
355
+ return `memcache://${this.id}`;
356
+ }
371
357
  /**
372
- * Creates a new ModulaHash instance.
373
- *
374
- * @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
375
- *
376
- * @example
377
- * ```typescript
378
- * // Use default SHA-1 hashing
379
- * const distribution = new ModulaHash();
380
- *
381
- * // Use MD5 hashing
382
- * const distribution = new ModulaHash('md5');
383
- *
384
- * // Use custom hash function
385
- * const distribution = new ModulaHash((buf) => buf.readUInt32BE(0));
386
- * ```
358
+ * Get the socket connection
387
359
  */
388
- constructor(hashFn) {
389
- this.hashFn = typeof hashFn === "string" ? hashFunctionForBuiltin2(hashFn) : hashFn ?? hashFunctionForBuiltin2("sha1");
390
- this.nodeMap = /* @__PURE__ */ new Map();
391
- this.nodeList = [];
360
+ get socket() {
361
+ return this._socket;
392
362
  }
393
363
  /**
394
- * Gets all nodes in the distribution.
395
- * @returns Array of all MemcacheNode instances
364
+ * Get the weight of this node (used for consistent hashing distribution)
396
365
  */
397
- get nodes() {
398
- return Array.from(this.nodeMap.values());
366
+ get weight() {
367
+ return this._weight;
399
368
  }
400
369
  /**
401
- * Adds a node to the distribution with its weight.
402
- * Weight determines how many times the node appears in the distribution list.
403
- *
404
- * @param node - The MemcacheNode to add
405
- *
406
- * @example
407
- * ```typescript
408
- * const node = new MemcacheNode('localhost', 11211, { weight: 2 });
409
- * distribution.addNode(node);
410
- * ```
370
+ * Set the weight of this node (used for consistent hashing distribution)
411
371
  */
412
- addNode(node) {
413
- this.nodeMap.set(node.id, node);
414
- const weight = node.weight || 1;
415
- for (let i = 0; i < weight; i++) {
416
- this.nodeList.push(node.id);
417
- }
372
+ set weight(value) {
373
+ this._weight = value;
418
374
  }
419
375
  /**
420
- * Removes a node from the distribution by its ID.
421
- *
422
- * @param id - The node ID (e.g., "localhost:11211")
423
- *
424
- * @example
425
- * ```typescript
426
- * distribution.removeNode('localhost:11211');
427
- * ```
376
+ * Get the keepAlive setting for this node
428
377
  */
429
- removeNode(id) {
430
- this.nodeMap.delete(id);
431
- this.nodeList = this.nodeList.filter((nodeId) => nodeId !== id);
378
+ get keepAlive() {
379
+ return this._keepAlive;
432
380
  }
433
381
  /**
434
- * Gets a specific node by its ID.
435
- *
436
- * @param id - The node ID (e.g., "localhost:11211")
437
- * @returns The MemcacheNode if found, undefined otherwise
438
- *
439
- * @example
440
- * ```typescript
441
- * const node = distribution.getNode('localhost:11211');
442
- * if (node) {
443
- * console.log(`Found node: ${node.uri}`);
444
- * }
445
- * ```
382
+ * Set the keepAlive setting for this node
446
383
  */
447
- getNode(id) {
448
- return this.nodeMap.get(id);
384
+ set keepAlive(value) {
385
+ this._keepAlive = value;
449
386
  }
450
387
  /**
451
- * Gets the nodes responsible for a given key using modulo hashing.
452
- * Uses `hash(key) % nodeCount` to determine the target node.
453
- *
454
- * @param key - The cache key to find the responsible node for
455
- * @returns Array containing the responsible node(s), empty if no nodes available
456
- *
457
- * @example
458
- * ```typescript
459
- * const nodes = distribution.getNodesByKey('user:123');
460
- * if (nodes.length > 0) {
461
- * console.log(`Key will be stored on: ${nodes[0].id}`);
462
- * }
463
- * ```
388
+ * Get the keepAliveDelay setting for this node
464
389
  */
465
- getNodesByKey(key) {
466
- if (this.nodeList.length === 0) {
467
- return [];
468
- }
469
- const hash = this.hashFn(Buffer.from(key));
470
- const index = hash % this.nodeList.length;
471
- const nodeId = this.nodeList[index];
472
- const node = this.nodeMap.get(nodeId);
473
- return node ? [node] : [];
474
- }
475
- };
476
-
477
- // src/node.ts
478
- var import_node_net = require("net");
479
- var import_hookified = require("hookified");
480
-
481
- // src/binary-protocol.ts
482
- var REQUEST_MAGIC = 128;
483
- var OPCODE_SASL_AUTH = 33;
484
- var OPCODE_GET = 0;
485
- var OPCODE_SET = 1;
486
- var OPCODE_ADD = 2;
487
- var OPCODE_REPLACE = 3;
488
- var OPCODE_DELETE = 4;
489
- var OPCODE_INCREMENT = 5;
490
- var OPCODE_DECREMENT = 6;
491
- var OPCODE_QUIT = 7;
492
- var OPCODE_FLUSH = 8;
493
- var OPCODE_VERSION = 11;
494
- var OPCODE_APPEND = 14;
495
- var OPCODE_PREPEND = 15;
496
- var OPCODE_STAT = 16;
497
- var OPCODE_TOUCH = 28;
498
- var STATUS_SUCCESS = 0;
499
- var STATUS_KEY_NOT_FOUND = 1;
500
- var STATUS_AUTH_ERROR = 32;
501
- var HEADER_SIZE = 24;
502
- function serializeHeader(header) {
503
- const buf = Buffer.alloc(HEADER_SIZE);
504
- buf.writeUInt8(header.magic ?? REQUEST_MAGIC, 0);
505
- buf.writeUInt8(header.opcode ?? 0, 1);
506
- buf.writeUInt16BE(header.keyLength ?? 0, 2);
507
- buf.writeUInt8(header.extrasLength ?? 0, 4);
508
- buf.writeUInt8(header.dataType ?? 0, 5);
509
- buf.writeUInt16BE(header.status ?? 0, 6);
510
- buf.writeUInt32BE(header.totalBodyLength ?? 0, 8);
511
- buf.writeUInt32BE(header.opaque ?? 0, 12);
512
- if (header.cas) {
513
- header.cas.copy(buf, 16);
390
+ get keepAliveDelay() {
391
+ return this._keepAliveDelay;
514
392
  }
515
- return buf;
516
- }
517
- function deserializeHeader(buf) {
518
- return {
519
- magic: buf.readUInt8(0),
520
- opcode: buf.readUInt8(1),
521
- keyLength: buf.readUInt16BE(2),
522
- extrasLength: buf.readUInt8(4),
523
- dataType: buf.readUInt8(5),
524
- status: buf.readUInt16BE(6),
525
- totalBodyLength: buf.readUInt32BE(8),
526
- opaque: buf.readUInt32BE(12),
527
- cas: buf.subarray(16, 24)
528
- };
529
- }
530
- function buildSaslPlainRequest(username, password) {
531
- const mechanism = "PLAIN";
532
- const authData = `\0${username}\0${password}`;
533
- const keyBuf = Buffer.from(mechanism, "utf8");
534
- const valueBuf = Buffer.from(authData, "utf8");
535
- const header = serializeHeader({
536
- magic: REQUEST_MAGIC,
537
- opcode: OPCODE_SASL_AUTH,
538
- keyLength: keyBuf.length,
539
- totalBodyLength: keyBuf.length + valueBuf.length
540
- });
541
- return Buffer.concat([header, keyBuf, valueBuf]);
542
- }
543
- function buildGetRequest(key) {
544
- const keyBuf = Buffer.from(key, "utf8");
545
- const header = serializeHeader({
546
- magic: REQUEST_MAGIC,
547
- opcode: OPCODE_GET,
548
- keyLength: keyBuf.length,
549
- totalBodyLength: keyBuf.length
550
- });
551
- return Buffer.concat([header, keyBuf]);
552
- }
553
- function buildSetRequest(key, value, flags = 0, exptime = 0) {
554
- const keyBuf = Buffer.from(key, "utf8");
555
- const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
556
- const extras = Buffer.alloc(8);
557
- extras.writeUInt32BE(flags, 0);
558
- extras.writeUInt32BE(exptime, 4);
559
- const header = serializeHeader({
560
- magic: REQUEST_MAGIC,
561
- opcode: OPCODE_SET,
562
- keyLength: keyBuf.length,
563
- extrasLength: 8,
564
- totalBodyLength: 8 + keyBuf.length + valueBuf.length
565
- });
566
- return Buffer.concat([header, extras, keyBuf, valueBuf]);
567
- }
568
- function buildAddRequest(key, value, flags = 0, exptime = 0) {
569
- const keyBuf = Buffer.from(key, "utf8");
570
- const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
571
- const extras = Buffer.alloc(8);
572
- extras.writeUInt32BE(flags, 0);
573
- extras.writeUInt32BE(exptime, 4);
574
- const header = serializeHeader({
575
- magic: REQUEST_MAGIC,
576
- opcode: OPCODE_ADD,
577
- keyLength: keyBuf.length,
578
- extrasLength: 8,
579
- totalBodyLength: 8 + keyBuf.length + valueBuf.length
580
- });
581
- return Buffer.concat([header, extras, keyBuf, valueBuf]);
582
- }
583
- function buildReplaceRequest(key, value, flags = 0, exptime = 0) {
584
- const keyBuf = Buffer.from(key, "utf8");
585
- const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
586
- const extras = Buffer.alloc(8);
587
- extras.writeUInt32BE(flags, 0);
588
- extras.writeUInt32BE(exptime, 4);
589
- const header = serializeHeader({
590
- magic: REQUEST_MAGIC,
591
- opcode: OPCODE_REPLACE,
592
- keyLength: keyBuf.length,
593
- extrasLength: 8,
594
- totalBodyLength: 8 + keyBuf.length + valueBuf.length
595
- });
596
- return Buffer.concat([header, extras, keyBuf, valueBuf]);
597
- }
598
- function buildDeleteRequest(key) {
599
- const keyBuf = Buffer.from(key, "utf8");
600
- const header = serializeHeader({
601
- magic: REQUEST_MAGIC,
602
- opcode: OPCODE_DELETE,
603
- keyLength: keyBuf.length,
604
- totalBodyLength: keyBuf.length
605
- });
606
- return Buffer.concat([header, keyBuf]);
607
- }
608
- function buildIncrementRequest(key, delta = 1, initial = 0, exptime = 0) {
609
- const keyBuf = Buffer.from(key, "utf8");
610
- const extras = Buffer.alloc(20);
611
- extras.writeUInt32BE(Math.floor(delta / 4294967296), 0);
612
- extras.writeUInt32BE(delta >>> 0, 4);
613
- extras.writeUInt32BE(Math.floor(initial / 4294967296), 8);
614
- extras.writeUInt32BE(initial >>> 0, 12);
615
- extras.writeUInt32BE(exptime, 16);
616
- const header = serializeHeader({
617
- magic: REQUEST_MAGIC,
618
- opcode: OPCODE_INCREMENT,
619
- keyLength: keyBuf.length,
620
- extrasLength: 20,
621
- totalBodyLength: 20 + keyBuf.length
622
- });
623
- return Buffer.concat([header, extras, keyBuf]);
624
- }
625
- function buildDecrementRequest(key, delta = 1, initial = 0, exptime = 0) {
626
- const keyBuf = Buffer.from(key, "utf8");
627
- const extras = Buffer.alloc(20);
628
- extras.writeUInt32BE(Math.floor(delta / 4294967296), 0);
629
- extras.writeUInt32BE(delta >>> 0, 4);
630
- extras.writeUInt32BE(Math.floor(initial / 4294967296), 8);
631
- extras.writeUInt32BE(initial >>> 0, 12);
632
- extras.writeUInt32BE(exptime, 16);
633
- const header = serializeHeader({
634
- magic: REQUEST_MAGIC,
635
- opcode: OPCODE_DECREMENT,
636
- keyLength: keyBuf.length,
637
- extrasLength: 20,
638
- totalBodyLength: 20 + keyBuf.length
639
- });
640
- return Buffer.concat([header, extras, keyBuf]);
641
- }
642
- function buildAppendRequest(key, value) {
643
- const keyBuf = Buffer.from(key, "utf8");
644
- const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
645
- const header = serializeHeader({
646
- magic: REQUEST_MAGIC,
647
- opcode: OPCODE_APPEND,
648
- keyLength: keyBuf.length,
649
- totalBodyLength: keyBuf.length + valueBuf.length
650
- });
651
- return Buffer.concat([header, keyBuf, valueBuf]);
652
- }
653
- function buildPrependRequest(key, value) {
654
- const keyBuf = Buffer.from(key, "utf8");
655
- const valueBuf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
656
- const header = serializeHeader({
657
- magic: REQUEST_MAGIC,
658
- opcode: OPCODE_PREPEND,
659
- keyLength: keyBuf.length,
660
- totalBodyLength: keyBuf.length + valueBuf.length
661
- });
662
- return Buffer.concat([header, keyBuf, valueBuf]);
663
- }
664
- function buildTouchRequest(key, exptime) {
665
- const keyBuf = Buffer.from(key, "utf8");
666
- const extras = Buffer.alloc(4);
667
- extras.writeUInt32BE(exptime, 0);
668
- const header = serializeHeader({
669
- magic: REQUEST_MAGIC,
670
- opcode: OPCODE_TOUCH,
671
- keyLength: keyBuf.length,
672
- extrasLength: 4,
673
- totalBodyLength: 4 + keyBuf.length
674
- });
675
- return Buffer.concat([header, extras, keyBuf]);
676
- }
677
- function buildFlushRequest(exptime = 0) {
678
- const extras = Buffer.alloc(4);
679
- extras.writeUInt32BE(exptime, 0);
680
- const header = serializeHeader({
681
- magic: REQUEST_MAGIC,
682
- opcode: OPCODE_FLUSH,
683
- extrasLength: 4,
684
- totalBodyLength: 4
685
- });
686
- return Buffer.concat([header, extras]);
687
- }
688
- function buildVersionRequest() {
689
- return serializeHeader({
690
- magic: REQUEST_MAGIC,
691
- opcode: OPCODE_VERSION
692
- });
693
- }
694
- function buildStatRequest(key) {
695
- if (key) {
696
- const keyBuf = Buffer.from(key, "utf8");
697
- const header = serializeHeader({
698
- magic: REQUEST_MAGIC,
699
- opcode: OPCODE_STAT,
700
- keyLength: keyBuf.length,
701
- totalBodyLength: keyBuf.length
393
+ /**
394
+ * Set the keepAliveDelay setting for this node
395
+ */
396
+ set keepAliveDelay(value) {
397
+ this._keepAliveDelay = value;
398
+ }
399
+ /**
400
+ * Get the command queue
401
+ */
402
+ get commandQueue() {
403
+ return this._commandQueue;
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
+ }
417
+ /**
418
+ * Connect to the memcache server
419
+ */
420
+ async connect() {
421
+ return new Promise((resolve, reject) => {
422
+ if (this._connected) {
423
+ resolve();
424
+ return;
425
+ }
426
+ this._socket = (0, import_node_net.createConnection)({
427
+ host: this._host,
428
+ port: this._port,
429
+ keepAlive: this._keepAlive,
430
+ keepAliveInitialDelay: this._keepAliveDelay
431
+ });
432
+ this._socket.setTimeout(this._timeout);
433
+ if (!this._sasl) {
434
+ this._socket.setEncoding("utf8");
435
+ }
436
+ this._socket.on("connect", async () => {
437
+ this._connected = true;
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
+ }
453
+ });
454
+ this._socket.on("data", (data) => {
455
+ if (typeof data === "string") {
456
+ this.handleData(data);
457
+ }
458
+ });
459
+ this._socket.on("error", (error) => {
460
+ this.emit("error", error);
461
+ if (!this._connected) {
462
+ reject(error);
463
+ }
464
+ });
465
+ this._socket.on("close", () => {
466
+ this._connected = false;
467
+ this._authenticated = false;
468
+ this.emit("close");
469
+ this.rejectPendingCommands(new Error("Connection closed"));
470
+ });
471
+ this._socket.on("timeout", () => {
472
+ this.emit("timeout");
473
+ this._socket?.destroy();
474
+ reject(new Error("Connection timeout"));
475
+ });
476
+ });
477
+ }
478
+ /**
479
+ * Disconnect from the memcache server
480
+ */
481
+ async disconnect() {
482
+ if (this._socket) {
483
+ this._socket.destroy();
484
+ this._socket = void 0;
485
+ this._connected = false;
486
+ }
487
+ }
488
+ /**
489
+ * Reconnect to the memcache server by disconnecting and connecting again
490
+ */
491
+ async reconnect() {
492
+ if (this._connected || this._socket) {
493
+ await this.disconnect();
494
+ this.rejectPendingCommands(
495
+ new Error("Connection reset for reconnection")
496
+ );
497
+ this._buffer = "";
498
+ this._currentCommand = void 0;
499
+ this._multilineData = [];
500
+ this._pendingValueBytes = 0;
501
+ this._authenticated = false;
502
+ this._binaryBuffer = Buffer.alloc(0);
503
+ }
504
+ await this.connect();
505
+ }
506
+ /**
507
+ * Perform SASL PLAIN authentication using the binary protocol
508
+ */
509
+ async performSaslAuth() {
510
+ if (!this._sasl || !this._socket) {
511
+ throw new Error("SASL credentials not configured");
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
+ });
551
+ }
552
+ /**
553
+ * Send a binary protocol request and wait for response.
554
+ * Used internally for SASL-authenticated connections.
555
+ */
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
+ });
579
+ }
580
+ /**
581
+ * Binary protocol GET operation
582
+ */
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;
589
+ }
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());
702
742
  });
703
- return Buffer.concat([header, keyBuf]);
704
743
  }
705
- return serializeHeader({
706
- magic: REQUEST_MAGIC,
707
- opcode: OPCODE_STAT
708
- });
709
- }
710
- function buildQuitRequest() {
711
- return serializeHeader({
712
- magic: REQUEST_MAGIC,
713
- opcode: OPCODE_QUIT
714
- });
715
- }
716
- function parseGetResponse(buf) {
717
- const header = deserializeHeader(buf);
718
- if (header.status !== STATUS_SUCCESS) {
719
- return { header, value: void 0, key: void 0 };
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
+ `);
791
+ });
792
+ }
793
+ handleData(data) {
794
+ this._buffer += data;
795
+ while (true) {
796
+ if (this._pendingValueBytes > 0) {
797
+ if (this._buffer.length >= this._pendingValueBytes + 2) {
798
+ const value = this._buffer.substring(0, this._pendingValueBytes);
799
+ this._buffer = this._buffer.substring(this._pendingValueBytes + 2);
800
+ this._multilineData.push(value);
801
+ this._pendingValueBytes = 0;
802
+ } else {
803
+ break;
804
+ }
805
+ }
806
+ const lineEnd = this._buffer.indexOf("\r\n");
807
+ if (lineEnd === -1) break;
808
+ const line = this._buffer.substring(0, lineEnd);
809
+ this._buffer = this._buffer.substring(lineEnd + 2);
810
+ this.processLine(line);
811
+ }
812
+ }
813
+ processLine(line) {
814
+ if (!this._currentCommand) {
815
+ this._currentCommand = this._commandQueue.shift();
816
+ if (!this._currentCommand) return;
817
+ }
818
+ if (this._currentCommand.isStats) {
819
+ if (line === "END") {
820
+ const stats = {};
821
+ for (const statLine of this._multilineData) {
822
+ const [, key, value] = statLine.split(" ");
823
+ if (key && value) {
824
+ stats[key] = value;
825
+ }
826
+ }
827
+ this._currentCommand.resolve(stats);
828
+ this._multilineData = [];
829
+ this._currentCommand = void 0;
830
+ return;
831
+ }
832
+ if (line.startsWith("STAT ")) {
833
+ this._multilineData.push(line);
834
+ return;
835
+ }
836
+ if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
837
+ this._currentCommand.reject(new Error(line));
838
+ this._currentCommand = void 0;
839
+ return;
840
+ }
841
+ return;
842
+ }
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
+ }
720
916
  }
721
- const extrasEnd = HEADER_SIZE + header.extrasLength;
722
- const keyEnd = extrasEnd + header.keyLength;
723
- const valueEnd = HEADER_SIZE + header.totalBodyLength;
724
- const key = header.keyLength > 0 ? buf.subarray(extrasEnd, keyEnd).toString("utf8") : void 0;
725
- const value = valueEnd > keyEnd ? buf.subarray(keyEnd, valueEnd) : void 0;
726
- return { header, value, key };
727
- }
728
- function parseIncrDecrResponse(buf) {
729
- const header = deserializeHeader(buf);
730
- if (header.status !== STATUS_SUCCESS || header.totalBodyLength < 8) {
731
- return { header, value: void 0 };
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
+ }
732
928
  }
733
- const high = buf.readUInt32BE(HEADER_SIZE);
734
- const low = buf.readUInt32BE(HEADER_SIZE + 4);
735
- const value = high * 4294967296 + low;
736
- return { header, value };
929
+ };
930
+ function createNode(host, port, options) {
931
+ return new MemcacheNode(host, port, options);
737
932
  }
738
933
 
739
- // src/node.ts
740
- var MemcacheNode = class extends import_hookified.Hookified {
741
- _host;
742
- _port;
743
- _socket = void 0;
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;
744
942
  _timeout;
745
943
  _keepAlive;
746
944
  _keepAliveDelay;
747
- _weight;
748
- _connected = false;
749
- _commandQueue = [];
750
- _buffer = "";
751
- _currentCommand = void 0;
752
- _multilineData = [];
753
- _pendingValueBytes = 0;
754
945
  _sasl;
755
- _authenticated = false;
756
- _binaryBuffer = Buffer.alloc(0);
757
- constructor(host, port, options) {
946
+ _isRunning = false;
947
+ _isPolling = false;
948
+ constructor(options) {
758
949
  super();
759
- this._host = host;
760
- this._port = port;
761
- this._timeout = options?.timeout || 5e3;
762
- this._keepAlive = options?.keepAlive !== false;
763
- this._keepAliveDelay = options?.keepAliveDelay || 1e3;
764
- this._weight = options?.weight || 1;
765
- this._sasl = options?.sasl;
766
- }
767
- /**
768
- * Get the host of this node
769
- */
770
- get host() {
771
- return this._host;
772
- }
773
- /**
774
- * Get the port of this node
775
- */
776
- get port() {
777
- return this._port;
778
- }
779
- /**
780
- * Get the unique identifier for this node (host:port format)
781
- */
782
- get id() {
783
- return this._port === 0 ? this._host : `${this._host}:${this._port}`;
784
- }
785
- /**
786
- * Get the full uri like memcache://localhost:11211
787
- */
788
- get uri() {
789
- return `memcache://${this.id}`;
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;
790
957
  }
791
- /**
792
- * Get the socket connection
793
- */
794
- get socket() {
795
- return this._socket;
958
+ /** Current config version. -1 means no config has been fetched yet. */
959
+ get configVersion() {
960
+ return this._configVersion;
796
961
  }
797
- /**
798
- * Get the weight of this node (used for consistent hashing distribution)
799
- */
800
- get weight() {
801
- return this._weight;
962
+ /** Whether auto discovery is currently running. */
963
+ get isRunning() {
964
+ return this._isRunning;
802
965
  }
803
- /**
804
- * Set the weight of this node (used for consistent hashing distribution)
805
- */
806
- set weight(value) {
807
- this._weight = value;
966
+ /** The configuration endpoint being used. */
967
+ get configEndpoint() {
968
+ return this._configEndpoint;
808
969
  }
809
970
  /**
810
- * Get the keepAlive setting for this node
971
+ * Start the auto discovery process.
972
+ * Performs an initial discovery, then starts the polling timer.
811
973
  */
812
- get keepAlive() {
813
- return this._keepAlive;
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;
814
996
  }
815
997
  /**
816
- * Set the keepAlive setting for this node
998
+ * Stop the auto discovery process.
817
999
  */
818
- set keepAlive(value) {
819
- this._keepAlive = value;
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
+ }
820
1010
  }
821
1011
  /**
822
- * Get the keepAliveDelay setting for this node
1012
+ * Perform a single discovery cycle.
1013
+ * Returns the ClusterConfig if the version has changed, or undefined if unchanged.
823
1014
  */
824
- get keepAliveDelay() {
825
- return this._keepAliveDelay;
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;
826
1023
  }
827
1024
  /**
828
- * Set the keepAliveDelay setting for this node
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"
829
1028
  */
830
- set keepAliveDelay(value) {
831
- this._keepAliveDelay = value;
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 };
832
1049
  }
833
1050
  /**
834
- * Get the command queue
1051
+ * Parse a single node entry in the format "hostname|ip|port".
835
1052
  */
836
- get commandQueue() {
837
- return this._commandQueue;
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 };
838
1068
  }
839
1069
  /**
840
- * Get whether SASL authentication is configured
1070
+ * Build a node ID from a DiscoveredNode.
1071
+ * Prefers IP when available, falls back to hostname.
841
1072
  */
842
- get hasSaslCredentials() {
843
- return !!this._sasl?.username && !!this._sasl?.password;
1073
+ static nodeId(node) {
1074
+ const host = node.ip || node.hostname;
1075
+ const wrappedHost = host.includes(":") ? `[${host}]` : host;
1076
+ return `${wrappedHost}:${node.port}`;
844
1077
  }
845
- /**
846
- * Get whether the node is authenticated (only relevant if SASL is configured)
847
- */
848
- get isAuthenticated() {
849
- return this._authenticated;
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;
850
1091
  }
851
- /**
852
- * Connect to the memcache server
853
- */
854
- async connect() {
855
- return new Promise((resolve, reject) => {
856
- if (this._connected) {
857
- resolve();
858
- return;
859
- }
860
- this._socket = (0, import_node_net.createConnection)({
861
- host: this._host,
862
- port: this._port,
863
- keepAlive: this._keepAlive,
864
- keepAliveInitialDelay: this._keepAliveDelay
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"]
865
1100
  });
866
- this._socket.setTimeout(this._timeout);
867
- if (!this._sasl) {
868
- this._socket.setEncoding("utf8");
1101
+ if (!result2?.values || result2.values.length === 0) {
1102
+ throw new Error("No config data received from legacy command");
869
1103
  }
870
- this._socket.on("connect", async () => {
871
- this._connected = true;
872
- if (this._sasl) {
873
- try {
874
- await this.performSaslAuth();
875
- this.emit("connect");
876
- resolve();
877
- } catch (error) {
878
- this._socket?.destroy();
879
- this._connected = false;
880
- this._authenticated = false;
881
- reject(error);
882
- }
883
- } else {
884
- this.emit("connect");
885
- resolve();
886
- }
887
- });
888
- this._socket.on("data", (data) => {
889
- if (typeof data === "string") {
890
- this.handleData(data);
891
- }
892
- });
893
- this._socket.on("error", (error) => {
894
- this.emit("error", error);
895
- if (!this._connected) {
896
- reject(error);
897
- }
898
- });
899
- this._socket.on("close", () => {
900
- this._connected = false;
901
- this._authenticated = false;
902
- this.emit("close");
903
- this.rejectPendingCommands(new Error("Connection closed"));
904
- });
905
- this._socket.on("timeout", () => {
906
- this.emit("timeout");
907
- this._socket?.destroy();
908
- reject(new Error("Connection timeout"));
909
- });
1104
+ return _AutoDiscovery.parseConfigResponse(result2.values);
1105
+ }
1106
+ const result = await node.command("config get cluster", {
1107
+ isConfig: true
910
1108
  });
1109
+ if (!result || result.length === 0) {
1110
+ throw new Error("No config data received");
1111
+ }
1112
+ return _AutoDiscovery.parseConfigResponse(result);
911
1113
  }
912
- /**
913
- * Disconnect from the memcache server
914
- */
915
- async disconnect() {
916
- if (this._socket) {
917
- this._socket.destroy();
918
- this._socket = void 0;
919
- this._connected = false;
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 };
920
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 };
921
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 {
922
1168
  /**
923
- * Reconnect to the memcache server by disconnecting and connecting again
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%.
924
1174
  */
925
- async reconnect() {
926
- if (this._connected || this._socket) {
927
- await this.disconnect();
928
- this.rejectPendingCommands(
929
- new Error("Connection reset for reconnection")
930
- );
931
- this._buffer = "";
932
- this._currentCommand = void 0;
933
- this._multilineData = [];
934
- this._pendingValueBytes = 0;
935
- this._authenticated = false;
936
- this._binaryBuffer = Buffer.alloc(0);
937
- }
938
- await this.connect();
939
- }
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();
940
1182
  /**
941
- * Perform SASL PLAIN authentication using the binary protocol
1183
+ * Gets the sorted array of [hash, node key] tuples representing virtual nodes on the ring.
1184
+ * @returns The hash clock array
942
1185
  */
943
- async performSaslAuth() {
944
- if (!this._sasl || !this._socket) {
945
- throw new Error("SASL credentials not configured");
946
- }
947
- const socket = this._socket;
948
- const sasl = this._sasl;
949
- return new Promise((resolve, reject) => {
950
- this._binaryBuffer = Buffer.alloc(0);
951
- const authPacket = buildSaslPlainRequest(sasl.username, sasl.password);
952
- const binaryHandler = (data) => {
953
- this._binaryBuffer = Buffer.concat([this._binaryBuffer, data]);
954
- if (this._binaryBuffer.length < HEADER_SIZE) {
955
- return;
956
- }
957
- const header = deserializeHeader(this._binaryBuffer);
958
- const totalLength = HEADER_SIZE + header.totalBodyLength;
959
- if (this._binaryBuffer.length < totalLength) {
960
- return;
961
- }
962
- socket.removeListener("data", binaryHandler);
963
- if (header.status === STATUS_SUCCESS) {
964
- this._authenticated = true;
965
- this.emit("authenticated");
966
- resolve();
967
- } else if (header.status === STATUS_AUTH_ERROR) {
968
- const body = this._binaryBuffer.subarray(HEADER_SIZE, totalLength);
969
- reject(
970
- new Error(
971
- `SASL authentication failed: ${body.toString() || "Invalid credentials"}`
972
- )
973
- );
974
- } else {
975
- reject(
976
- new Error(
977
- `SASL authentication failed with status: 0x${header.status.toString(16)}`
978
- )
979
- );
980
- }
981
- };
982
- socket.on("data", binaryHandler);
983
- socket.write(authPacket);
984
- });
1186
+ get clock() {
1187
+ return this._clock;
985
1188
  }
986
1189
  /**
987
- * Send a binary protocol request and wait for response.
988
- * Used internally for SASL-authenticated connections.
1190
+ * Gets the map of node keys to actual node objects.
1191
+ * @returns The nodes map
989
1192
  */
990
- async binaryRequest(packet) {
991
- if (!this._socket) {
992
- throw new Error("Not connected");
993
- }
994
- const socket = this._socket;
995
- return new Promise((resolve) => {
996
- let buffer = Buffer.alloc(0);
997
- const dataHandler = (data) => {
998
- buffer = Buffer.concat([buffer, data]);
999
- if (buffer.length < HEADER_SIZE) {
1000
- return;
1001
- }
1002
- const header = deserializeHeader(buffer);
1003
- const totalLength = HEADER_SIZE + header.totalBodyLength;
1004
- if (buffer.length < totalLength) {
1005
- return;
1006
- }
1007
- socket.removeListener("data", dataHandler);
1008
- resolve(buffer.subarray(0, totalLength));
1009
- };
1010
- socket.on("data", dataHandler);
1011
- socket.write(packet);
1012
- });
1193
+ get nodes() {
1194
+ return this._nodes;
1013
1195
  }
1014
1196
  /**
1015
- * Binary protocol GET operation
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
+ * ```
1016
1216
  */
1017
- async binaryGet(key) {
1018
- const response = await this.binaryRequest(buildGetRequest(key));
1019
- const { header, value } = parseGetResponse(response);
1020
- if (header.status === STATUS_KEY_NOT_FOUND) {
1021
- this.emit("miss", key);
1022
- return void 0;
1023
- }
1024
- if (header.status !== STATUS_SUCCESS || !value) {
1025
- return void 0;
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
+ }
1026
1225
  }
1027
- const result = value.toString("utf8");
1028
- this.emit("hit", key, result);
1029
- return result;
1030
1226
  }
1031
1227
  /**
1032
- * Binary protocol SET operation
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
+ * ```
1033
1243
  */
1034
- async binarySet(key, value, exptime = 0, flags = 0) {
1035
- const response = await this.binaryRequest(
1036
- buildSetRequest(key, value, flags, exptime)
1037
- );
1038
- const header = deserializeHeader(response);
1039
- return header.status === STATUS_SUCCESS;
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
+ }
1040
1255
  }
1041
1256
  /**
1042
- * Binary protocol ADD operation
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
+ * ```
1043
1267
  */
1044
- async binaryAdd(key, value, exptime = 0, flags = 0) {
1045
- const response = await this.binaryRequest(
1046
- buildAddRequest(key, value, flags, exptime)
1047
- );
1048
- const header = deserializeHeader(response);
1049
- return header.status === STATUS_SUCCESS;
1268
+ removeNode(node) {
1269
+ const key = keyFor(node);
1270
+ if (this._nodes.delete(key)) {
1271
+ this._clock = this._clock.filter(([, n]) => n !== key);
1272
+ }
1050
1273
  }
1051
1274
  /**
1052
- * Binary protocol REPLACE operation
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
+ * ```
1053
1293
  */
1054
- async binaryReplace(key, value, exptime = 0, flags = 0) {
1055
- const response = await this.binaryRequest(
1056
- buildReplaceRequest(key, value, flags, exptime)
1057
- );
1058
- const header = deserializeHeader(response);
1059
- return header.status === STATUS_SUCCESS;
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);
1060
1301
  }
1061
1302
  /**
1062
- * Binary protocol DELETE operation
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
1063
1307
  */
1064
- async binaryDelete(key) {
1065
- const response = await this.binaryRequest(buildDeleteRequest(key));
1066
- const header = deserializeHeader(response);
1067
- return header.status === STATUS_SUCCESS || header.status === STATUS_KEY_NOT_FOUND;
1308
+ getIndexForInput(input) {
1309
+ const hash = this.hashFn(
1310
+ typeof input === "string" ? Buffer.from(input) : input
1311
+ );
1312
+ return binarySearchRing(this._clock, hash);
1068
1313
  }
1069
1314
  /**
1070
- * Binary protocol INCREMENT operation
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
+ * ```
1071
1338
  */
1072
- async binaryIncr(key, delta = 1, initial = 0, exptime = 0) {
1073
- const response = await this.binaryRequest(
1074
- buildIncrementRequest(key, delta, initial, exptime)
1075
- );
1076
- const { header, value } = parseIncrDecrResponse(response);
1077
- if (header.status !== STATUS_SUCCESS) {
1078
- return void 0;
1339
+ getNodes(input, replicas) {
1340
+ if (this._clock.length === 0) {
1341
+ return [];
1079
1342
  }
1080
- return value;
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));
1081
1351
  }
1082
1352
  /**
1083
- * Binary protocol DECREMENT operation
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)
1084
1358
  */
1085
- async binaryDecr(key, delta = 1, initial = 0, exptime = 0) {
1086
- const response = await this.binaryRequest(
1087
- buildDecrementRequest(key, delta, initial, exptime)
1088
- );
1089
- const { header, value } = parseIncrDecrResponse(response);
1090
- if (header.status !== STATUS_SUCCESS) {
1091
- return void 0;
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]);
1092
1363
  }
1093
- return value;
1364
+ this._clock.sort((a, b) => a[0] - b[0]);
1094
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;
1095
1374
  /**
1096
- * Binary protocol APPEND operation
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
+ * ```
1097
1387
  */
1098
- async binaryAppend(key, value) {
1099
- const response = await this.binaryRequest(buildAppendRequest(key, value));
1100
- const header = deserializeHeader(response);
1101
- return header.status === STATUS_SUCCESS;
1388
+ constructor(hashFn) {
1389
+ this.hashRing = new HashRing([], hashFn);
1390
+ this.nodeMap = /* @__PURE__ */ new Map();
1102
1391
  }
1103
1392
  /**
1104
- * Binary protocol PREPEND operation
1393
+ * Gets all nodes in the distribution.
1394
+ * @returns Array of all MemcacheNode instances
1105
1395
  */
1106
- async binaryPrepend(key, value) {
1107
- const response = await this.binaryRequest(buildPrependRequest(key, value));
1108
- const header = deserializeHeader(response);
1109
- return header.status === STATUS_SUCCESS;
1396
+ get nodes() {
1397
+ return Array.from(this.nodeMap.values());
1110
1398
  }
1111
1399
  /**
1112
- * Binary protocol TOUCH operation
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
+ * ```
1113
1409
  */
1114
- async binaryTouch(key, exptime) {
1115
- const response = await this.binaryRequest(buildTouchRequest(key, exptime));
1116
- const header = deserializeHeader(response);
1117
- return header.status === STATUS_SUCCESS;
1410
+ addNode(node) {
1411
+ this.nodeMap.set(node.id, node);
1412
+ this.hashRing.addNode(node.id, node.weight);
1118
1413
  }
1119
1414
  /**
1120
- * Binary protocol FLUSH operation
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
+ * ```
1121
1423
  */
1122
- /* v8 ignore next -- @preserve */
1123
- async binaryFlush(exptime = 0) {
1124
- const response = await this.binaryRequest(buildFlushRequest(exptime));
1125
- const header = deserializeHeader(response);
1126
- return header.status === STATUS_SUCCESS;
1424
+ removeNode(id) {
1425
+ this.nodeMap.delete(id);
1426
+ this.hashRing.removeNode(id);
1127
1427
  }
1128
1428
  /**
1129
- * Binary protocol VERSION operation
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
+ * ```
1130
1441
  */
1131
- async binaryVersion() {
1132
- const response = await this.binaryRequest(buildVersionRequest());
1133
- const header = deserializeHeader(response);
1134
- if (header.status !== STATUS_SUCCESS) {
1135
- return void 0;
1136
- }
1137
- return response.subarray(HEADER_SIZE, HEADER_SIZE + header.totalBodyLength).toString("utf8");
1442
+ getNode(id) {
1443
+ return this.nodeMap.get(id);
1138
1444
  }
1139
1445
  /**
1140
- * Binary protocol STATS operation
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
+ * ```
1141
1459
  */
1142
- async binaryStats() {
1143
- if (!this._socket) {
1144
- throw new Error("Not connected");
1460
+ getNodesByKey(key) {
1461
+ const nodeId = this.hashRing.getNode(key);
1462
+ if (!nodeId) {
1463
+ return [];
1145
1464
  }
1146
- const socket = this._socket;
1147
- const stats = {};
1148
- return new Promise((resolve) => {
1149
- let buffer = Buffer.alloc(0);
1150
- const dataHandler = (data) => {
1151
- buffer = Buffer.concat([buffer, data]);
1152
- while (buffer.length >= HEADER_SIZE) {
1153
- const header = deserializeHeader(buffer);
1154
- const totalLength = HEADER_SIZE + header.totalBodyLength;
1155
- if (buffer.length < totalLength) {
1156
- return;
1157
- }
1158
- if (header.keyLength === 0 && header.totalBodyLength === 0) {
1159
- socket.removeListener("data", dataHandler);
1160
- resolve(stats);
1161
- return;
1162
- }
1163
- if (header.opcode === OPCODE_STAT && header.status === STATUS_SUCCESS) {
1164
- const keyStart = HEADER_SIZE;
1165
- const keyEnd = keyStart + header.keyLength;
1166
- const valueEnd = HEADER_SIZE + header.totalBodyLength;
1167
- const key = buffer.subarray(keyStart, keyEnd).toString("utf8");
1168
- const value = buffer.subarray(keyEnd, valueEnd).toString("utf8");
1169
- stats[key] = value;
1170
- }
1171
- buffer = buffer.subarray(totalLength);
1172
- }
1173
- };
1174
- socket.on("data", dataHandler);
1175
- socket.write(buildStatRequest());
1176
- });
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;
1477
+ } else {
1478
+ lo = mid + 1;
1479
+ }
1480
+ }
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 = [];
1177
1520
  }
1178
1521
  /**
1179
- * Binary protocol QUIT operation
1522
+ * Gets all nodes in the distribution.
1523
+ * @returns Array of all MemcacheNode instances
1180
1524
  */
1181
- async binaryQuit() {
1182
- if (this._socket) {
1183
- this._socket.write(buildQuitRequest());
1184
- }
1525
+ get nodes() {
1526
+ return Array.from(this.nodeMap.values());
1185
1527
  }
1186
1528
  /**
1187
- * Gracefully quit the connection (send quit command then disconnect)
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
+ * ```
1188
1539
  */
1189
- async quit() {
1190
- if (this._connected && this._socket) {
1191
- try {
1192
- await this.command("quit");
1193
- } catch (error) {
1194
- }
1195
- await this.disconnect();
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);
1196
1545
  }
1197
1546
  }
1198
1547
  /**
1199
- * Check if connected to the memcache server
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
+ * ```
1200
1556
  */
1201
- isConnected() {
1202
- return this._connected;
1557
+ removeNode(id) {
1558
+ this.nodeMap.delete(id);
1559
+ this.nodeList = this.nodeList.filter((nodeId) => nodeId !== id);
1203
1560
  }
1204
1561
  /**
1205
- * Send a generic command to the memcache server
1206
- * @param cmd The command string to send (without trailing \r\n)
1207
- * @param options Command options for response parsing
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
+ * ```
1208
1574
  */
1209
- async command(cmd, options) {
1210
- if (!this._connected || !this._socket) {
1211
- throw new Error(`Not connected to memcache server ${this.id}`);
1212
- }
1213
- return new Promise((resolve, reject) => {
1214
- this._commandQueue.push({
1215
- command: cmd,
1216
- resolve,
1217
- reject,
1218
- isMultiline: options?.isMultiline,
1219
- isStats: options?.isStats,
1220
- requestedKeys: options?.requestedKeys
1221
- });
1222
- this._socket.write(`${cmd}\r
1223
- `);
1224
- });
1225
- }
1226
- handleData(data) {
1227
- this._buffer += data;
1228
- while (true) {
1229
- if (this._pendingValueBytes > 0) {
1230
- if (this._buffer.length >= this._pendingValueBytes + 2) {
1231
- const value = this._buffer.substring(0, this._pendingValueBytes);
1232
- this._buffer = this._buffer.substring(this._pendingValueBytes + 2);
1233
- this._multilineData.push(value);
1234
- this._pendingValueBytes = 0;
1235
- } else {
1236
- break;
1237
- }
1238
- }
1239
- const lineEnd = this._buffer.indexOf("\r\n");
1240
- if (lineEnd === -1) break;
1241
- const line = this._buffer.substring(0, lineEnd);
1242
- this._buffer = this._buffer.substring(lineEnd + 2);
1243
- this.processLine(line);
1244
- }
1245
- }
1246
- processLine(line) {
1247
- if (!this._currentCommand) {
1248
- this._currentCommand = this._commandQueue.shift();
1249
- if (!this._currentCommand) return;
1250
- }
1251
- if (this._currentCommand.isStats) {
1252
- if (line === "END") {
1253
- const stats = {};
1254
- for (const statLine of this._multilineData) {
1255
- const [, key, value] = statLine.split(" ");
1256
- if (key && value) {
1257
- stats[key] = value;
1258
- }
1259
- }
1260
- this._currentCommand.resolve(stats);
1261
- this._multilineData = [];
1262
- this._currentCommand = void 0;
1263
- return;
1264
- }
1265
- if (line.startsWith("STAT ")) {
1266
- this._multilineData.push(line);
1267
- return;
1268
- }
1269
- if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
1270
- this._currentCommand.reject(new Error(line));
1271
- this._currentCommand = void 0;
1272
- return;
1273
- }
1274
- return;
1275
- }
1276
- if (this._currentCommand.isMultiline) {
1277
- if (this._currentCommand.requestedKeys && !this._currentCommand.foundKeys) {
1278
- this._currentCommand.foundKeys = [];
1279
- }
1280
- if (line.startsWith("VALUE ")) {
1281
- const parts = line.split(" ");
1282
- const key = parts[1];
1283
- const bytes = parseInt(parts[3], 10);
1284
- if (this._currentCommand.requestedKeys) {
1285
- this._currentCommand.foundKeys?.push(key);
1286
- }
1287
- this._pendingValueBytes = bytes;
1288
- } else if (line === "END") {
1289
- let result;
1290
- if (this._currentCommand.requestedKeys && this._currentCommand.foundKeys) {
1291
- result = {
1292
- values: this._multilineData.length > 0 ? this._multilineData : void 0,
1293
- foundKeys: this._currentCommand.foundKeys
1294
- };
1295
- } else {
1296
- result = this._multilineData.length > 0 ? this._multilineData : void 0;
1297
- }
1298
- if (this._currentCommand.requestedKeys && this._currentCommand.foundKeys) {
1299
- const foundKeys = this._currentCommand.foundKeys;
1300
- for (let i = 0; i < foundKeys.length; i++) {
1301
- this.emit("hit", foundKeys[i], this._multilineData[i]);
1302
- }
1303
- const missedKeys = this._currentCommand.requestedKeys.filter(
1304
- (key) => !foundKeys.includes(key)
1305
- );
1306
- for (const key of missedKeys) {
1307
- this.emit("miss", key);
1308
- }
1309
- }
1310
- this._currentCommand.resolve(result);
1311
- this._multilineData = [];
1312
- this._currentCommand = void 0;
1313
- } else if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
1314
- this._currentCommand.reject(new Error(line));
1315
- this._multilineData = [];
1316
- this._currentCommand = void 0;
1317
- }
1318
- } else {
1319
- if (line === "STORED" || line === "DELETED" || line === "OK" || line === "TOUCHED" || line === "EXISTS" || line === "NOT_FOUND") {
1320
- this._currentCommand.resolve(line);
1321
- } else if (line === "NOT_STORED") {
1322
- this._currentCommand.resolve(false);
1323
- } else if (line.startsWith("ERROR") || line.startsWith("CLIENT_ERROR") || line.startsWith("SERVER_ERROR")) {
1324
- this._currentCommand.reject(new Error(line));
1325
- } else if (/^\d+$/.test(line)) {
1326
- this._currentCommand.resolve(parseInt(line, 10));
1327
- } else {
1328
- this._currentCommand.resolve(line);
1329
- }
1330
- this._currentCommand = void 0;
1331
- }
1575
+ getNode(id) {
1576
+ return this.nodeMap.get(id);
1332
1577
  }
1333
- rejectPendingCommands(error) {
1334
- if (this._currentCommand) {
1335
- this._currentCommand.reject(error);
1336
- this._currentCommand = void 0;
1337
- }
1338
- while (this._commandQueue.length > 0) {
1339
- const cmd = this._commandQueue.shift();
1340
- if (cmd) {
1341
- cmd.reject(error);
1342
- }
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 [];
1343
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] : [];
1344
1602
  }
1345
1603
  };
1346
- function createNode(host, port, options) {
1347
- return new MemcacheNode(host, port, options);
1348
- }
1349
1604
 
1350
1605
  // src/types.ts
1351
1606
  var MemcacheEvents = /* @__PURE__ */ ((MemcacheEvents2) => {
@@ -1358,13 +1613,16 @@ var MemcacheEvents = /* @__PURE__ */ ((MemcacheEvents2) => {
1358
1613
  MemcacheEvents2["INFO"] = "info";
1359
1614
  MemcacheEvents2["TIMEOUT"] = "timeout";
1360
1615
  MemcacheEvents2["CLOSE"] = "close";
1616
+ MemcacheEvents2["AUTO_DISCOVER"] = "autoDiscover";
1617
+ MemcacheEvents2["AUTO_DISCOVER_ERROR"] = "autoDiscoverError";
1618
+ MemcacheEvents2["AUTO_DISCOVER_UPDATE"] = "autoDiscoverUpdate";
1361
1619
  return MemcacheEvents2;
1362
1620
  })(MemcacheEvents || {});
1363
1621
 
1364
1622
  // src/index.ts
1365
1623
  var defaultRetryBackoff = (_attempt, baseDelay) => baseDelay;
1366
1624
  var exponentialRetryBackoff = (attempt, baseDelay) => baseDelay * 2 ** attempt;
1367
- var Memcache = class extends import_hookified2.Hookified {
1625
+ var Memcache = class extends import_hookified3.Hookified {
1368
1626
  _nodes = [];
1369
1627
  _timeout;
1370
1628
  _keepAlive;
@@ -1375,6 +1633,8 @@ var Memcache = class extends import_hookified2.Hookified {
1375
1633
  _retryBackoff;
1376
1634
  _retryOnlyIdempotent;
1377
1635
  _sasl;
1636
+ _autoDiscovery;
1637
+ _autoDiscoverOptions;
1378
1638
  constructor(options) {
1379
1639
  super();
1380
1640
  if (typeof options === "string") {
@@ -1398,6 +1658,7 @@ var Memcache = class extends import_hookified2.Hookified {
1398
1658
  this._retryBackoff = options?.retryBackoff ?? defaultRetryBackoff;
1399
1659
  this._retryOnlyIdempotent = options?.retryOnlyIdempotent ?? true;
1400
1660
  this._sasl = options?.sasl;
1661
+ this._autoDiscoverOptions = options?.autoDiscover;
1401
1662
  const nodeUris = options?.nodes || ["localhost:11211"];
1402
1663
  for (const nodeUri of nodeUris) {
1403
1664
  this.addNode(nodeUri);
@@ -1713,6 +1974,9 @@ var Memcache = class extends import_hookified2.Hookified {
1713
1974
  return;
1714
1975
  }
1715
1976
  await Promise.all(this._nodes.map((node) => node.connect()));
1977
+ if (this._autoDiscoverOptions?.enabled && !this._autoDiscovery) {
1978
+ await this.startAutoDiscovery();
1979
+ }
1716
1980
  }
1717
1981
  /**
1718
1982
  * Get a value from the Memcache server.
@@ -2072,6 +2336,10 @@ ${valueStr}`;
2072
2336
  * @returns {Promise<void>}
2073
2337
  */
2074
2338
  async quit() {
2339
+ if (this._autoDiscovery) {
2340
+ await this._autoDiscovery.stop();
2341
+ this._autoDiscovery = void 0;
2342
+ }
2075
2343
  await Promise.all(
2076
2344
  this._nodes.map(async (node) => {
2077
2345
  if (node.isConnected()) {
@@ -2085,6 +2353,10 @@ ${valueStr}`;
2085
2353
  * @returns {Promise<void>}
2086
2354
  */
2087
2355
  async disconnect() {
2356
+ if (this._autoDiscovery) {
2357
+ await this._autoDiscovery.stop();
2358
+ this._autoDiscovery = void 0;
2359
+ }
2088
2360
  await Promise.all(this._nodes.map((node) => node.disconnect()));
2089
2361
  }
2090
2362
  /**
@@ -2239,10 +2511,85 @@ ${valueStr}`;
2239
2511
  );
2240
2512
  node.on("miss", (key) => this.emit("miss" /* MISS */, key));
2241
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
+ }
2242
2588
  };
2243
2589
  var index_default = Memcache;
2244
2590
  // Annotate the CommonJS export names for ESM import in node:
2245
2591
  0 && (module.exports = {
2592
+ AutoDiscovery,
2246
2593
  Memcache,
2247
2594
  MemcacheEvents,
2248
2595
  MemcacheNode,