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