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