memcache 0.2.0 → 1.0.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/.github/ISSUE_TEMPLATE/bug_report.md +14 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +6 -0
- package/.github/workflows/code-coverage.yaml +41 -0
- package/.github/workflows/codeql.yaml +75 -0
- package/.github/workflows/release.yaml +41 -0
- package/.github/workflows/tests.yaml +40 -0
- package/.nvmrc +1 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +27 -0
- package/LICENSE +21 -0
- package/README.md +369 -71
- package/SECURITY.md +3 -0
- package/biome.json +35 -0
- package/dist/index.cjs +1502 -0
- package/dist/index.d.cts +501 -0
- package/dist/index.d.ts +501 -0
- package/dist/index.js +1475 -0
- package/docker-compose.yml +24 -0
- package/package.json +38 -17
- package/pnpm-workspace.yaml +2 -0
- package/site/favicon.ico +0 -0
- package/site/logo.ai +7222 -37
- package/site/logo.png +0 -0
- package/site/logo.svg +7 -0
- package/site/logo.webp +0 -0
- package/site/logo_medium.png +0 -0
- package/site/logo_small.png +0 -0
- package/src/index.ts +1130 -0
- package/src/ketama.ts +449 -0
- package/src/node.ts +488 -0
- package/test/index.test.ts +2734 -0
- package/test/ketama.test.ts +526 -0
- package/test/memcache-node-instances.test.ts +102 -0
- package/test/node.test.ts +809 -0
- package/tsconfig.json +29 -0
- package/vitest.config.ts +16 -0
- package/.gitignore +0 -2
- package/Makefile +0 -13
- package/example.js +0 -68
- package/index.js +0 -1
- package/lib/memcache.js +0 -344
- package/test/test-memcache.js +0 -238
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: test file
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { createNode, MemcacheNode } from "../src/node";
|
|
4
|
+
|
|
5
|
+
describe("MemcacheNode", () => {
|
|
6
|
+
let node: MemcacheNode;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
node = new MemcacheNode("localhost", 11211, {
|
|
10
|
+
timeout: 5000,
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
if (node.isConnected()) {
|
|
16
|
+
await node.disconnect();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("Constructor and Properties", () => {
|
|
21
|
+
it("should create instance with host and port", () => {
|
|
22
|
+
expect(node).toBeInstanceOf(MemcacheNode);
|
|
23
|
+
expect(node.host).toBe("localhost");
|
|
24
|
+
expect(node.port).toBe(11211);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should generate correct id for standard port", () => {
|
|
28
|
+
const testNode = new MemcacheNode("localhost", 11211);
|
|
29
|
+
expect(testNode.id).toBe("localhost:11211");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should generate correct id for Unix socket (port 0)", () => {
|
|
33
|
+
const testNode = new MemcacheNode("/var/run/memcached.sock", 0);
|
|
34
|
+
expect(testNode.id).toBe("/var/run/memcached.sock");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should generate correct uri for standard port", () => {
|
|
38
|
+
const testNode = new MemcacheNode("localhost", 11211);
|
|
39
|
+
expect(testNode.uri).toBe("memcache://localhost:11211");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should generate correct uri for Unix socket (port 0)", () => {
|
|
43
|
+
const testNode = new MemcacheNode("/var/run/memcached.sock", 0);
|
|
44
|
+
expect(testNode.uri).toBe("memcache:///var/run/memcached.sock");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should use default options if not provided", () => {
|
|
48
|
+
const testNode = new MemcacheNode("localhost", 11211);
|
|
49
|
+
expect(testNode).toBeInstanceOf(MemcacheNode);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should have default weight of 1", () => {
|
|
53
|
+
const testNode = new MemcacheNode("localhost", 11211);
|
|
54
|
+
expect(testNode.weight).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should accept weight in options", () => {
|
|
58
|
+
const testNode = new MemcacheNode("localhost", 11211, { weight: 5 });
|
|
59
|
+
expect(testNode.weight).toBe(5);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should allow getting and setting weight", () => {
|
|
63
|
+
const testNode = new MemcacheNode("localhost", 11211, { weight: 2 });
|
|
64
|
+
expect(testNode.weight).toBe(2);
|
|
65
|
+
|
|
66
|
+
testNode.weight = 10;
|
|
67
|
+
expect(testNode.weight).toBe(10);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("createNode factory function", () => {
|
|
72
|
+
it("should create a new MemcacheNode instance", () => {
|
|
73
|
+
const node = createNode("localhost", 11211);
|
|
74
|
+
expect(node).toBeInstanceOf(MemcacheNode);
|
|
75
|
+
expect(node.host).toBe("localhost");
|
|
76
|
+
expect(node.port).toBe(11211);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should create node with options", () => {
|
|
80
|
+
const node = createNode("localhost", 11211, {
|
|
81
|
+
timeout: 5000,
|
|
82
|
+
keepAlive: true,
|
|
83
|
+
keepAliveDelay: 1000,
|
|
84
|
+
weight: 3,
|
|
85
|
+
});
|
|
86
|
+
expect(node).toBeInstanceOf(MemcacheNode);
|
|
87
|
+
expect(node.weight).toBe(3);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should create node without options", () => {
|
|
91
|
+
const node = createNode("192.168.1.1", 11212);
|
|
92
|
+
expect(node).toBeInstanceOf(MemcacheNode);
|
|
93
|
+
expect(node.host).toBe("192.168.1.1");
|
|
94
|
+
expect(node.port).toBe(11212);
|
|
95
|
+
expect(node.weight).toBe(1); // default weight
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("Constructor and Properties", () => {
|
|
100
|
+
it("should have default keepAlive of true", () => {
|
|
101
|
+
const testNode = new MemcacheNode("localhost", 11211);
|
|
102
|
+
expect(testNode.keepAlive).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should accept keepAlive in options", () => {
|
|
106
|
+
const testNode = new MemcacheNode("localhost", 11211, {
|
|
107
|
+
keepAlive: false,
|
|
108
|
+
});
|
|
109
|
+
expect(testNode.keepAlive).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should allow getting and setting keepAlive", () => {
|
|
113
|
+
const testNode = new MemcacheNode("localhost", 11211, {
|
|
114
|
+
keepAlive: true,
|
|
115
|
+
});
|
|
116
|
+
expect(testNode.keepAlive).toBe(true);
|
|
117
|
+
|
|
118
|
+
testNode.keepAlive = false;
|
|
119
|
+
expect(testNode.keepAlive).toBe(false);
|
|
120
|
+
|
|
121
|
+
testNode.keepAlive = true;
|
|
122
|
+
expect(testNode.keepAlive).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should have default keepAliveDelay of 1000", () => {
|
|
126
|
+
const testNode = new MemcacheNode("localhost", 11211);
|
|
127
|
+
expect(testNode.keepAliveDelay).toBe(1000);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should accept keepAliveDelay in options", () => {
|
|
131
|
+
const testNode = new MemcacheNode("localhost", 11211, {
|
|
132
|
+
keepAliveDelay: 5000,
|
|
133
|
+
});
|
|
134
|
+
expect(testNode.keepAliveDelay).toBe(5000);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should allow getting and setting keepAliveDelay", () => {
|
|
138
|
+
const testNode = new MemcacheNode("localhost", 11211, {
|
|
139
|
+
keepAliveDelay: 2000,
|
|
140
|
+
});
|
|
141
|
+
expect(testNode.keepAliveDelay).toBe(2000);
|
|
142
|
+
|
|
143
|
+
testNode.keepAliveDelay = 3000;
|
|
144
|
+
expect(testNode.keepAliveDelay).toBe(3000);
|
|
145
|
+
|
|
146
|
+
testNode.keepAliveDelay = 500;
|
|
147
|
+
expect(testNode.keepAliveDelay).toBe(500);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("Connection Lifecycle", () => {
|
|
152
|
+
it("should connect to memcached server", async () => {
|
|
153
|
+
await node.connect();
|
|
154
|
+
expect(node.isConnected()).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should handle connecting when already connected", async () => {
|
|
158
|
+
await node.connect();
|
|
159
|
+
expect(node.isConnected()).toBe(true);
|
|
160
|
+
|
|
161
|
+
// Try to connect again - should resolve immediately
|
|
162
|
+
await node.connect();
|
|
163
|
+
expect(node.isConnected()).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should disconnect from memcached server", async () => {
|
|
167
|
+
await node.connect();
|
|
168
|
+
expect(node.isConnected()).toBe(true);
|
|
169
|
+
|
|
170
|
+
await node.disconnect();
|
|
171
|
+
expect(node.isConnected()).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should emit connect event", async () => {
|
|
175
|
+
let connected = false;
|
|
176
|
+
node.on("connect", () => {
|
|
177
|
+
connected = true;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await node.connect();
|
|
181
|
+
expect(connected).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should emit close event on disconnect", async () => {
|
|
185
|
+
await node.connect();
|
|
186
|
+
|
|
187
|
+
const closePromise = new Promise<void>((resolve) => {
|
|
188
|
+
node.on("close", () => {
|
|
189
|
+
resolve();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await node.disconnect();
|
|
194
|
+
|
|
195
|
+
// Wait for close event with timeout
|
|
196
|
+
await Promise.race([
|
|
197
|
+
closePromise,
|
|
198
|
+
new Promise((_, reject) =>
|
|
199
|
+
setTimeout(() => reject(new Error("Close event not emitted")), 100),
|
|
200
|
+
),
|
|
201
|
+
]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should handle quit command", async () => {
|
|
205
|
+
await node.connect();
|
|
206
|
+
await node.quit();
|
|
207
|
+
expect(node.isConnected()).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should reconnect successfully", async () => {
|
|
211
|
+
// First connection
|
|
212
|
+
await node.connect();
|
|
213
|
+
expect(node.isConnected()).toBe(true);
|
|
214
|
+
|
|
215
|
+
// Set a value to ensure connection is working
|
|
216
|
+
const key = "node-test-reconnect";
|
|
217
|
+
const value = "initial-value";
|
|
218
|
+
const bytes = Buffer.byteLength(value);
|
|
219
|
+
await node.command(`set ${key} 0 0 ${bytes}\r\n${value}`);
|
|
220
|
+
|
|
221
|
+
// Reconnect
|
|
222
|
+
await node.reconnect();
|
|
223
|
+
expect(node.isConnected()).toBe(true);
|
|
224
|
+
|
|
225
|
+
// Verify we can still execute commands after reconnect
|
|
226
|
+
const result = await node.command("version");
|
|
227
|
+
expect(result).toBeDefined();
|
|
228
|
+
expect(typeof result).toBe("string");
|
|
229
|
+
expect(result).toContain("VERSION");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should clear pending commands on reconnect", async () => {
|
|
233
|
+
await node.connect();
|
|
234
|
+
|
|
235
|
+
// Queue a command that won't complete
|
|
236
|
+
const promise = node.command("get slow-key", { isMultiline: true });
|
|
237
|
+
|
|
238
|
+
// Reconnect immediately (this will disconnect and clear pending commands)
|
|
239
|
+
setImmediate(async () => {
|
|
240
|
+
await node.reconnect();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// The pending command should be rejected
|
|
244
|
+
await expect(promise).rejects.toThrow(
|
|
245
|
+
"Connection reset for reconnection",
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should reconnect when not initially connected", async () => {
|
|
250
|
+
// Don't connect first
|
|
251
|
+
expect(node.isConnected()).toBe(false);
|
|
252
|
+
|
|
253
|
+
// Reconnect should establish a connection
|
|
254
|
+
await node.reconnect();
|
|
255
|
+
expect(node.isConnected()).toBe(true);
|
|
256
|
+
|
|
257
|
+
// Verify connection works
|
|
258
|
+
const result = await node.command("version");
|
|
259
|
+
expect(result).toContain("VERSION");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("should emit connect event on reconnect", async () => {
|
|
263
|
+
await node.connect();
|
|
264
|
+
|
|
265
|
+
let connectCount = 0;
|
|
266
|
+
node.on("connect", () => {
|
|
267
|
+
connectCount++;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Reconnect
|
|
271
|
+
await node.reconnect();
|
|
272
|
+
|
|
273
|
+
// Should emit connect event for the new connection
|
|
274
|
+
expect(connectCount).toBe(1);
|
|
275
|
+
expect(node.isConnected()).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should reject connection to invalid host", async () => {
|
|
279
|
+
const badNode = new MemcacheNode("0.0.0.0", 99999, { timeout: 1000 });
|
|
280
|
+
await expect(badNode.connect()).rejects.toThrow();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should handle connection timeout", async () => {
|
|
284
|
+
// Use a valid IP that won't respond (TEST-NET-1)
|
|
285
|
+
const timeoutNode = new MemcacheNode("192.0.2.0", 11211, {
|
|
286
|
+
timeout: 1000,
|
|
287
|
+
});
|
|
288
|
+
await expect(timeoutNode.connect()).rejects.toThrow("Connection timeout");
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("Generic Command Execution", () => {
|
|
293
|
+
beforeEach(async () => {
|
|
294
|
+
await node.connect();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should execute version command", async () => {
|
|
298
|
+
const result = await node.command("version");
|
|
299
|
+
expect(result).toBeDefined();
|
|
300
|
+
expect(typeof result).toBe("string");
|
|
301
|
+
expect(result).toContain("VERSION");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("should execute set command", async () => {
|
|
305
|
+
const key = "node-test-set";
|
|
306
|
+
const value = "test-value";
|
|
307
|
+
const bytes = Buffer.byteLength(value);
|
|
308
|
+
const cmd = `set ${key} 0 0 ${bytes}\r\n${value}`;
|
|
309
|
+
const result = await node.command(cmd);
|
|
310
|
+
expect(result).toBe("STORED");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("should execute get command with multiline option", async () => {
|
|
314
|
+
const key = "node-test-get";
|
|
315
|
+
const value = "test-value";
|
|
316
|
+
|
|
317
|
+
// First set the value
|
|
318
|
+
const bytes = Buffer.byteLength(value);
|
|
319
|
+
await node.command(`set ${key} 0 0 ${bytes}\r\n${value}`);
|
|
320
|
+
|
|
321
|
+
// Then get it
|
|
322
|
+
const result = await node.command(`get ${key}`, { isMultiline: true });
|
|
323
|
+
expect(result).toBeInstanceOf(Array);
|
|
324
|
+
expect(result[0]).toBe(value);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should execute get command for non-existent key", async () => {
|
|
328
|
+
const key = "node-test-nonexistent";
|
|
329
|
+
const result = await node.command(`get ${key}`, { isMultiline: true });
|
|
330
|
+
expect(result).toBeUndefined();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("should execute delete command", async () => {
|
|
334
|
+
const key = "node-test-delete";
|
|
335
|
+
const value = "test-value";
|
|
336
|
+
const bytes = Buffer.byteLength(value);
|
|
337
|
+
|
|
338
|
+
// Set first
|
|
339
|
+
await node.command(`set ${key} 0 0 ${bytes}\r\n${value}`);
|
|
340
|
+
|
|
341
|
+
// Delete
|
|
342
|
+
const result = await node.command(`delete ${key}`);
|
|
343
|
+
expect(result).toBe("DELETED");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("should execute incr command", async () => {
|
|
347
|
+
const key = "node-test-incr";
|
|
348
|
+
|
|
349
|
+
// Set initial value
|
|
350
|
+
await node.command(`set ${key} 0 0 1\r\n0`);
|
|
351
|
+
|
|
352
|
+
// Increment
|
|
353
|
+
const result = await node.command(`incr ${key} 1`);
|
|
354
|
+
expect(result).toBe(1);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should execute decr command", async () => {
|
|
358
|
+
const key = "node-test-decr";
|
|
359
|
+
|
|
360
|
+
// Set initial value
|
|
361
|
+
await node.command(`set ${key} 0 0 2\r\n10`);
|
|
362
|
+
|
|
363
|
+
// Decrement
|
|
364
|
+
const result = await node.command(`decr ${key} 1`);
|
|
365
|
+
expect(result).toBe(9);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("should execute stats command", async () => {
|
|
369
|
+
const result = await node.command("stats", { isStats: true });
|
|
370
|
+
expect(result).toBeDefined();
|
|
371
|
+
expect(typeof result).toBe("object");
|
|
372
|
+
expect(result.version).toBeDefined();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should execute touch command", async () => {
|
|
376
|
+
const key = "node-test-touch";
|
|
377
|
+
const value = "test-value";
|
|
378
|
+
const bytes = Buffer.byteLength(value);
|
|
379
|
+
|
|
380
|
+
// Set first
|
|
381
|
+
await node.command(`set ${key} 0 0 ${bytes}\r\n${value}`);
|
|
382
|
+
|
|
383
|
+
// Touch
|
|
384
|
+
const result = await node.command(`touch ${key} 100`);
|
|
385
|
+
expect(result).toBe("TOUCHED");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("should execute flush_all command", async () => {
|
|
389
|
+
const result = await node.command("flush_all");
|
|
390
|
+
expect(result).toBe("OK");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("should handle NOT_STORED response for add command", async () => {
|
|
394
|
+
const key = "node-test-add-duplicate";
|
|
395
|
+
const value = "test-value";
|
|
396
|
+
const bytes = Buffer.byteLength(value);
|
|
397
|
+
|
|
398
|
+
// First set the key
|
|
399
|
+
await node.command(`set ${key} 0 0 ${bytes}\r\n${value}`);
|
|
400
|
+
|
|
401
|
+
// Try to add the same key (should fail since it exists)
|
|
402
|
+
const result = await node.command(`add ${key} 0 0 ${bytes}\r\n${value}`);
|
|
403
|
+
expect(result).toBe(false);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("should handle EXISTS response for cas command", async () => {
|
|
407
|
+
const key = "node-test-cas-exists";
|
|
408
|
+
const value = "test-value";
|
|
409
|
+
const bytes = Buffer.byteLength(value);
|
|
410
|
+
|
|
411
|
+
// Set initial value
|
|
412
|
+
await node.command(`set ${key} 0 0 ${bytes}\r\n${value}`);
|
|
413
|
+
|
|
414
|
+
// Get with cas to get the cas token
|
|
415
|
+
const getResult = await node.command(`gets ${key}`, {
|
|
416
|
+
isMultiline: true,
|
|
417
|
+
});
|
|
418
|
+
expect(getResult).toBeDefined();
|
|
419
|
+
|
|
420
|
+
// Modify the value to change cas
|
|
421
|
+
await node.command(`set ${key} 0 0 ${bytes}\r\n${value}`);
|
|
422
|
+
|
|
423
|
+
// Try cas with old token (should get EXISTS)
|
|
424
|
+
const mockSocket = (node as any)._socket;
|
|
425
|
+
const commandPromise = node.command(
|
|
426
|
+
`cas ${key} 0 0 ${bytes} 12345\r\n${value}`,
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// Simulate server EXISTS response
|
|
430
|
+
mockSocket.emit("data", "EXISTS\r\n");
|
|
431
|
+
|
|
432
|
+
const result = await commandPromise;
|
|
433
|
+
expect(result).toBe("EXISTS");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("should handle NOT_FOUND response for delete command", async () => {
|
|
437
|
+
const key = "node-test-delete-nonexistent";
|
|
438
|
+
|
|
439
|
+
// Try to delete a key that doesn't exist
|
|
440
|
+
const mockSocket = (node as any)._socket;
|
|
441
|
+
const commandPromise = node.command(`delete ${key}`);
|
|
442
|
+
|
|
443
|
+
// Simulate server NOT_FOUND response
|
|
444
|
+
mockSocket.emit("data", "NOT_FOUND\r\n");
|
|
445
|
+
|
|
446
|
+
const result = await commandPromise;
|
|
447
|
+
expect(result).toBe("NOT_FOUND");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("should handle multiple sequential commands", async () => {
|
|
451
|
+
const key1 = "node-test-seq1";
|
|
452
|
+
const key2 = "node-test-seq2";
|
|
453
|
+
const value = "test";
|
|
454
|
+
const bytes = Buffer.byteLength(value);
|
|
455
|
+
|
|
456
|
+
const result1 = await node.command(
|
|
457
|
+
`set ${key1} 0 0 ${bytes}\r\n${value}`,
|
|
458
|
+
);
|
|
459
|
+
expect(result1).toBe("STORED");
|
|
460
|
+
|
|
461
|
+
const result2 = await node.command(
|
|
462
|
+
`set ${key2} 0 0 ${bytes}\r\n${value}`,
|
|
463
|
+
);
|
|
464
|
+
expect(result2).toBe("STORED");
|
|
465
|
+
|
|
466
|
+
const result3 = await node.command(`get ${key1}`, { isMultiline: true });
|
|
467
|
+
expect(result3[0]).toBe(value);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("should emit hit event for successful get", async () => {
|
|
471
|
+
const key = "node-test-hit";
|
|
472
|
+
const value = "test-value";
|
|
473
|
+
const bytes = Buffer.byteLength(value);
|
|
474
|
+
|
|
475
|
+
// Set first
|
|
476
|
+
await node.command(`set ${key} 0 0 ${bytes}\r\n${value}`);
|
|
477
|
+
|
|
478
|
+
let hitEmitted = false;
|
|
479
|
+
let hitKey = "";
|
|
480
|
+
let hitValue = "";
|
|
481
|
+
|
|
482
|
+
node.on("hit", (k: string, v: string) => {
|
|
483
|
+
hitEmitted = true;
|
|
484
|
+
hitKey = k;
|
|
485
|
+
hitValue = v;
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Get with requestedKeys to trigger event
|
|
489
|
+
await node.command(`get ${key}`, {
|
|
490
|
+
isMultiline: true,
|
|
491
|
+
requestedKeys: [key],
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(hitEmitted).toBe(true);
|
|
495
|
+
expect(hitKey).toBe(key);
|
|
496
|
+
expect(hitValue).toBe(value);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("should emit miss event for non-existent key", async () => {
|
|
500
|
+
const key = "node-test-miss";
|
|
501
|
+
|
|
502
|
+
let missEmitted = false;
|
|
503
|
+
let missKey = "";
|
|
504
|
+
|
|
505
|
+
node.on("miss", (k: string) => {
|
|
506
|
+
missEmitted = true;
|
|
507
|
+
missKey = k;
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Get non-existent key with requestedKeys to trigger event
|
|
511
|
+
await node.command(`get ${key}`, {
|
|
512
|
+
isMultiline: true,
|
|
513
|
+
requestedKeys: [key],
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(missEmitted).toBe(true);
|
|
517
|
+
expect(missKey).toBe(key);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
describe("Error Handling", () => {
|
|
522
|
+
it("should throw error when not connected", async () => {
|
|
523
|
+
const disconnectedNode = new MemcacheNode("localhost", 11211);
|
|
524
|
+
await expect(disconnectedNode.command("version")).rejects.toThrow(
|
|
525
|
+
"Not connected to memcache server",
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("should handle protocol errors", async () => {
|
|
530
|
+
await node.connect();
|
|
531
|
+
|
|
532
|
+
// Try to set with invalid key (contains space)
|
|
533
|
+
await expect(
|
|
534
|
+
node.command("set invalid key 0 0 5\r\nvalue"),
|
|
535
|
+
).rejects.toThrow();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("should reject pending commands on disconnect", async () => {
|
|
539
|
+
await node.connect();
|
|
540
|
+
|
|
541
|
+
// Queue a command
|
|
542
|
+
const promise = node.command("get slow-key", { isMultiline: true });
|
|
543
|
+
|
|
544
|
+
// Immediately disconnect
|
|
545
|
+
setImmediate(() => {
|
|
546
|
+
node.disconnect();
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
await expect(promise).rejects.toThrow("Connection closed");
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("should emit error event", async () => {
|
|
553
|
+
await node.connect();
|
|
554
|
+
|
|
555
|
+
let errorEmitted = false;
|
|
556
|
+
node.on("error", () => {
|
|
557
|
+
errorEmitted = true;
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Force an error by destroying the socket
|
|
561
|
+
node.socket?.emit("error", new Error("Test error"));
|
|
562
|
+
|
|
563
|
+
expect(errorEmitted).toBe(true);
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe("Command Queue", () => {
|
|
568
|
+
beforeEach(async () => {
|
|
569
|
+
await node.connect();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("should maintain FIFO order for commands", async () => {
|
|
573
|
+
const key1 = "node-fifo-1";
|
|
574
|
+
const key2 = "node-fifo-2";
|
|
575
|
+
const key3 = "node-fifo-3";
|
|
576
|
+
const value = "test";
|
|
577
|
+
const bytes = Buffer.byteLength(value);
|
|
578
|
+
|
|
579
|
+
// Queue multiple commands
|
|
580
|
+
const promises = [
|
|
581
|
+
node.command(`set ${key1} 0 0 ${bytes}\r\n${value}`),
|
|
582
|
+
node.command(`set ${key2} 0 0 ${bytes}\r\n${value}`),
|
|
583
|
+
node.command(`set ${key3} 0 0 ${bytes}\r\n${value}`),
|
|
584
|
+
];
|
|
585
|
+
|
|
586
|
+
const results = await Promise.all(promises);
|
|
587
|
+
|
|
588
|
+
expect(results[0]).toBe("STORED");
|
|
589
|
+
expect(results[1]).toBe("STORED");
|
|
590
|
+
expect(results[2]).toBe("STORED");
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("should expose command queue", () => {
|
|
594
|
+
expect(node.commandQueue).toBeDefined();
|
|
595
|
+
expect(Array.isArray(node.commandQueue)).toBe(true);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
describe("Multiline Response Handling", () => {
|
|
600
|
+
beforeEach(async () => {
|
|
601
|
+
await node.connect();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("should handle multiple keys in get command", async () => {
|
|
605
|
+
const key1 = "node-multi-1";
|
|
606
|
+
const key2 = "node-multi-2";
|
|
607
|
+
const value1 = "value1";
|
|
608
|
+
const value2 = "value2";
|
|
609
|
+
|
|
610
|
+
// Set both keys
|
|
611
|
+
await node.command(
|
|
612
|
+
`set ${key1} 0 0 ${Buffer.byteLength(value1)}\r\n${value1}`,
|
|
613
|
+
);
|
|
614
|
+
await node.command(
|
|
615
|
+
`set ${key2} 0 0 ${Buffer.byteLength(value2)}\r\n${value2}`,
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
// Get both
|
|
619
|
+
const result = await node.command(`get ${key1} ${key2}`, {
|
|
620
|
+
isMultiline: true,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
expect(result).toBeInstanceOf(Array);
|
|
624
|
+
expect(result.length).toBe(2);
|
|
625
|
+
expect(result[0]).toBe(value1);
|
|
626
|
+
expect(result[1]).toBe(value2);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it("should handle large values", async () => {
|
|
630
|
+
const key = "node-large";
|
|
631
|
+
const value = "x".repeat(10000); // 10KB value
|
|
632
|
+
const bytes = Buffer.byteLength(value);
|
|
633
|
+
|
|
634
|
+
const setResult = await node.command(
|
|
635
|
+
`set ${key} 0 0 ${bytes}\r\n${value}`,
|
|
636
|
+
);
|
|
637
|
+
expect(setResult).toBe("STORED");
|
|
638
|
+
|
|
639
|
+
const result = await node.command(`get ${key}`, { isMultiline: true });
|
|
640
|
+
expect(result).toBeDefined();
|
|
641
|
+
expect(result[0]).toBe(value);
|
|
642
|
+
expect(result[0].length).toBe(10000);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("should handle partial data delivery for value bytes", async () => {
|
|
646
|
+
const key = "node-partial";
|
|
647
|
+
const value = "test-value-12345";
|
|
648
|
+
const bytes = Buffer.byteLength(value);
|
|
649
|
+
|
|
650
|
+
// Set the value first
|
|
651
|
+
await node.command(`set ${key} 0 0 ${bytes}\r\n${value}`);
|
|
652
|
+
|
|
653
|
+
const mockSocket = (node as any)._socket;
|
|
654
|
+
const commandPromise = node.command(`get ${key}`, {
|
|
655
|
+
isMultiline: true,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Simulate partial data delivery - send VALUE line first
|
|
659
|
+
mockSocket.emit("data", `VALUE ${key} 0 ${bytes}\r\n`);
|
|
660
|
+
|
|
661
|
+
// Send only part of the value bytes (not enough)
|
|
662
|
+
mockSocket.emit("data", value.substring(0, 5));
|
|
663
|
+
|
|
664
|
+
// Send rest of value and END
|
|
665
|
+
mockSocket.emit("data", `${value.substring(5)}\r\nEND\r\n`);
|
|
666
|
+
|
|
667
|
+
const result = await commandPromise;
|
|
668
|
+
expect(result).toBeDefined();
|
|
669
|
+
expect(result[0]).toBe(value);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
describe("Error Handling", () => {
|
|
674
|
+
it("should handle ERROR response for stats command", async () => {
|
|
675
|
+
await node.connect();
|
|
676
|
+
|
|
677
|
+
const mockSocket = (node as any)._socket;
|
|
678
|
+
const commandPromise = node.command("stats invalid_type", {
|
|
679
|
+
isStats: true,
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Simulate server ERROR response
|
|
683
|
+
mockSocket.emit("data", "ERROR\r\n");
|
|
684
|
+
|
|
685
|
+
await expect(commandPromise).rejects.toThrow("ERROR");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("should handle CLIENT_ERROR response for stats command", async () => {
|
|
689
|
+
await node.connect();
|
|
690
|
+
|
|
691
|
+
const mockSocket = (node as any)._socket;
|
|
692
|
+
const commandPromise = node.command("stats", { isStats: true });
|
|
693
|
+
|
|
694
|
+
// Simulate server CLIENT_ERROR response
|
|
695
|
+
mockSocket.emit("data", "CLIENT_ERROR bad command\r\n");
|
|
696
|
+
|
|
697
|
+
await expect(commandPromise).rejects.toThrow("CLIENT_ERROR bad command");
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("should handle SERVER_ERROR response for stats command", async () => {
|
|
701
|
+
await node.connect();
|
|
702
|
+
|
|
703
|
+
const mockSocket = (node as any)._socket;
|
|
704
|
+
const commandPromise = node.command("stats", { isStats: true });
|
|
705
|
+
|
|
706
|
+
// Simulate server SERVER_ERROR response
|
|
707
|
+
mockSocket.emit("data", "SERVER_ERROR out of memory\r\n");
|
|
708
|
+
|
|
709
|
+
await expect(commandPromise).rejects.toThrow(
|
|
710
|
+
"SERVER_ERROR out of memory",
|
|
711
|
+
);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("should handle unexpected line in stats command response", async () => {
|
|
715
|
+
await node.connect();
|
|
716
|
+
|
|
717
|
+
const mockSocket = (node as any)._socket;
|
|
718
|
+
const commandPromise = node.command("stats", { isStats: true });
|
|
719
|
+
|
|
720
|
+
// Simulate unexpected response line (not STAT, not END, not ERROR)
|
|
721
|
+
mockSocket.emit("data", "UNEXPECTED_LINE\r\n");
|
|
722
|
+
// Then send END to complete the command
|
|
723
|
+
mockSocket.emit("data", "END\r\n");
|
|
724
|
+
|
|
725
|
+
// Should still resolve successfully, ignoring the unexpected line
|
|
726
|
+
const result = await commandPromise;
|
|
727
|
+
expect(result).toBeDefined();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it("should handle ERROR response for multiline get command", async () => {
|
|
731
|
+
await node.connect();
|
|
732
|
+
|
|
733
|
+
const mockSocket = (node as any)._socket;
|
|
734
|
+
const commandPromise = node.command("get test_key", {
|
|
735
|
+
isMultiline: true,
|
|
736
|
+
requestedKeys: ["test_key"],
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// Simulate server ERROR response
|
|
740
|
+
mockSocket.emit("data", "ERROR\r\n");
|
|
741
|
+
|
|
742
|
+
await expect(commandPromise).rejects.toThrow("ERROR");
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("should handle CLIENT_ERROR response for multiline get command", async () => {
|
|
746
|
+
await node.connect();
|
|
747
|
+
|
|
748
|
+
const mockSocket = (node as any)._socket;
|
|
749
|
+
const commandPromise = node.command("get test_key", {
|
|
750
|
+
isMultiline: true,
|
|
751
|
+
requestedKeys: ["test_key"],
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// Simulate server CLIENT_ERROR response
|
|
755
|
+
mockSocket.emit("data", "CLIENT_ERROR invalid key\r\n");
|
|
756
|
+
|
|
757
|
+
await expect(commandPromise).rejects.toThrow("CLIENT_ERROR invalid key");
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it("should handle SERVER_ERROR response for multiline get command", async () => {
|
|
761
|
+
await node.connect();
|
|
762
|
+
|
|
763
|
+
const mockSocket = (node as any)._socket;
|
|
764
|
+
const commandPromise = node.command("get test_key", {
|
|
765
|
+
isMultiline: true,
|
|
766
|
+
requestedKeys: ["test_key"],
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Simulate server SERVER_ERROR response
|
|
770
|
+
mockSocket.emit("data", "SERVER_ERROR temporary failure\r\n");
|
|
771
|
+
|
|
772
|
+
await expect(commandPromise).rejects.toThrow(
|
|
773
|
+
"SERVER_ERROR temporary failure",
|
|
774
|
+
);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it("should reject current command on disconnect", async () => {
|
|
778
|
+
await node.connect();
|
|
779
|
+
|
|
780
|
+
// Start a command but don't let it complete
|
|
781
|
+
const commandPromise = node.command("get pending_key", {
|
|
782
|
+
isMultiline: true,
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// Disconnect immediately
|
|
786
|
+
await node.disconnect();
|
|
787
|
+
|
|
788
|
+
// The command should be rejected
|
|
789
|
+
await expect(commandPromise).rejects.toThrow();
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it("should reject queued commands on disconnect", async () => {
|
|
793
|
+
await node.connect();
|
|
794
|
+
|
|
795
|
+
// Queue multiple commands without responses
|
|
796
|
+
const promise1 = node.command("get key1", { isMultiline: true });
|
|
797
|
+
const promise2 = node.command("get key2", { isMultiline: true });
|
|
798
|
+
const promise3 = node.command("get key3", { isMultiline: true });
|
|
799
|
+
|
|
800
|
+
// Disconnect immediately
|
|
801
|
+
await node.disconnect();
|
|
802
|
+
|
|
803
|
+
// All commands should be rejected
|
|
804
|
+
await expect(promise1).rejects.toThrow();
|
|
805
|
+
await expect(promise2).rejects.toThrow();
|
|
806
|
+
await expect(promise3).rejects.toThrow();
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
});
|