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