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