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