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