memcache 0.3.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 -74
- 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,2734 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: test file
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import Memcache, { createNode, MemcacheEvents } from "../src/index";
|
|
4
|
+
import { KetamaHash } from "../src/ketama";
|
|
5
|
+
|
|
6
|
+
describe("Memcache", () => {
|
|
7
|
+
let client: Memcache;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
client = new Memcache({
|
|
11
|
+
timeout: 5000,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (client.isConnected()) {
|
|
17
|
+
client.disconnect();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("Constructor", () => {
|
|
22
|
+
it("should create instance with default options", () => {
|
|
23
|
+
const defaultClient = new Memcache();
|
|
24
|
+
expect(defaultClient).toBeInstanceOf(Memcache);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should create instance with custom options", () => {
|
|
28
|
+
const customClient = new Memcache({
|
|
29
|
+
timeout: 10000,
|
|
30
|
+
keepAlive: false,
|
|
31
|
+
});
|
|
32
|
+
expect(customClient).toBeInstanceOf(Memcache);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should initialize with default node localhost:11211", () => {
|
|
36
|
+
const testClient = new Memcache();
|
|
37
|
+
expect(testClient.nodeIds).toHaveLength(1);
|
|
38
|
+
expect(testClient.nodeIds).toContain("localhost:11211");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should initialize with nodes from options", () => {
|
|
42
|
+
const testClient = new Memcache({
|
|
43
|
+
nodes: ["localhost:11211", "localhost:11212", "127.0.0.1:11213"],
|
|
44
|
+
});
|
|
45
|
+
expect(testClient.nodeIds).toHaveLength(3);
|
|
46
|
+
expect(testClient.nodeIds).toContain("localhost:11211");
|
|
47
|
+
expect(testClient.nodeIds).toContain("localhost:11212");
|
|
48
|
+
expect(testClient.nodeIds).toContain("127.0.0.1:11213");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should parse node URIs with protocols", () => {
|
|
52
|
+
const testClient = new Memcache({
|
|
53
|
+
nodes: [
|
|
54
|
+
"memcache://localhost:11211",
|
|
55
|
+
"tcp://192.168.1.100:11212",
|
|
56
|
+
"server3",
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
expect(testClient.nodeIds).toHaveLength(3);
|
|
60
|
+
expect(testClient.nodeIds).toContain("localhost:11211");
|
|
61
|
+
expect(testClient.nodeIds).toContain("192.168.1.100:11212");
|
|
62
|
+
expect(testClient.nodeIds).toContain("server3:11211");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should handle Unix socket URIs in nodes", () => {
|
|
66
|
+
const testClient = new Memcache({
|
|
67
|
+
nodes: ["unix:///var/run/memcached.sock", "/tmp/memcached.sock"],
|
|
68
|
+
});
|
|
69
|
+
expect(testClient.nodeIds).toHaveLength(2);
|
|
70
|
+
expect(testClient.nodeIds).toContain("/var/run/memcached.sock");
|
|
71
|
+
expect(testClient.nodeIds).toContain("/tmp/memcached.sock");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should handle IPv6 addresses in nodes", () => {
|
|
75
|
+
const testClient = new Memcache({
|
|
76
|
+
nodes: ["[::1]:11211", "memcache://[2001:db8::1]:11212"],
|
|
77
|
+
});
|
|
78
|
+
expect(testClient.nodeIds).toHaveLength(2);
|
|
79
|
+
expect(testClient.nodeIds).toContain("::1:11211");
|
|
80
|
+
expect(testClient.nodeIds).toContain("2001:db8::1:11212");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should allow setting timeout via setter", () => {
|
|
84
|
+
const testClient = new Memcache();
|
|
85
|
+
expect(testClient.timeout).toBe(5000); // Default timeout
|
|
86
|
+
|
|
87
|
+
testClient.timeout = 10000;
|
|
88
|
+
expect(testClient.timeout).toBe(10000);
|
|
89
|
+
|
|
90
|
+
testClient.timeout = 2000;
|
|
91
|
+
expect(testClient.timeout).toBe(2000);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should allow getting and setting keepAlive property", () => {
|
|
95
|
+
const testClient = new Memcache();
|
|
96
|
+
expect(testClient.keepAlive).toBe(true); // Default keepAlive
|
|
97
|
+
|
|
98
|
+
testClient.keepAlive = false;
|
|
99
|
+
expect(testClient.keepAlive).toBe(false);
|
|
100
|
+
|
|
101
|
+
testClient.keepAlive = true;
|
|
102
|
+
expect(testClient.keepAlive).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should initialize with custom keepAlive", () => {
|
|
106
|
+
const testClient = new Memcache({ keepAlive: false });
|
|
107
|
+
expect(testClient.keepAlive).toBe(false);
|
|
108
|
+
|
|
109
|
+
const testClient2 = new Memcache({ keepAlive: true });
|
|
110
|
+
expect(testClient2.keepAlive).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should allow getting and setting keepAliveDelay property", () => {
|
|
114
|
+
const testClient = new Memcache();
|
|
115
|
+
expect(testClient.keepAliveDelay).toBe(1000); // Default keepAliveDelay
|
|
116
|
+
|
|
117
|
+
testClient.keepAliveDelay = 3000;
|
|
118
|
+
expect(testClient.keepAliveDelay).toBe(3000);
|
|
119
|
+
|
|
120
|
+
testClient.keepAliveDelay = 100;
|
|
121
|
+
expect(testClient.keepAliveDelay).toBe(100);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should initialize with custom keepAliveDelay", () => {
|
|
125
|
+
const testClient = new Memcache({ keepAliveDelay: 2000 });
|
|
126
|
+
expect(testClient.keepAliveDelay).toBe(2000);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Removed: socket and commandQueue are now internal to MemcacheNode
|
|
130
|
+
// These were testing implementation details, not behavior
|
|
131
|
+
|
|
132
|
+
it("should allow getting and setting hash provider", () => {
|
|
133
|
+
const testClient = new Memcache();
|
|
134
|
+
|
|
135
|
+
// Test getter - should return KetamaHash instance
|
|
136
|
+
const hashProvider = testClient.hash;
|
|
137
|
+
expect(hashProvider).toBeDefined();
|
|
138
|
+
expect(hashProvider.name).toBe("ketama");
|
|
139
|
+
|
|
140
|
+
// Test setter - set a new hash provider
|
|
141
|
+
const customHashProvider = new KetamaHash();
|
|
142
|
+
testClient.hash = customHashProvider;
|
|
143
|
+
expect(testClient.hash).toBe(customHashProvider);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("parseUri", () => {
|
|
148
|
+
it("should parse host and port from simple format", () => {
|
|
149
|
+
const result = client.parseUri("localhost:11211");
|
|
150
|
+
expect(result).toEqual({ host: "localhost", port: 11211 });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should parse host and port from memcache:// format", () => {
|
|
154
|
+
const result = client.parseUri("memcache://localhost:11211");
|
|
155
|
+
expect(result).toEqual({ host: "localhost", port: 11211 });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should parse host and port from memcached:// format", () => {
|
|
159
|
+
const result = client.parseUri("memcached://localhost:11211");
|
|
160
|
+
expect(result).toEqual({ host: "localhost", port: 11211 });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should parse host and port from tcp:// format", () => {
|
|
164
|
+
const result = client.parseUri("tcp://localhost:11211");
|
|
165
|
+
expect(result).toEqual({ host: "localhost", port: 11211 });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should parse host with default port", () => {
|
|
169
|
+
const result = client.parseUri("localhost");
|
|
170
|
+
expect(result).toEqual({ host: "localhost", port: 11211 });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should parse host with default port from memcache:// format", () => {
|
|
174
|
+
const result = client.parseUri("memcache://localhost");
|
|
175
|
+
expect(result).toEqual({ host: "localhost", port: 11211 });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should parse host with default port from memcached:// format", () => {
|
|
179
|
+
const result = client.parseUri("memcached://localhost");
|
|
180
|
+
expect(result).toEqual({ host: "localhost", port: 11211 });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should parse host with default port from tcp:// format", () => {
|
|
184
|
+
const result = client.parseUri("tcp://localhost");
|
|
185
|
+
expect(result).toEqual({ host: "localhost", port: 11211 });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should parse IP address with port", () => {
|
|
189
|
+
const result = client.parseUri("127.0.0.1:11212");
|
|
190
|
+
expect(result).toEqual({ host: "127.0.0.1", port: 11212 });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should parse IPv6 address with brackets and port", () => {
|
|
194
|
+
const result = client.parseUri("[::1]:11211");
|
|
195
|
+
expect(result).toEqual({ host: "::1", port: 11211 });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should parse IPv6 address with brackets from memcache:// format", () => {
|
|
199
|
+
const result = client.parseUri("memcache://[2001:db8::1]:11212");
|
|
200
|
+
expect(result).toEqual({ host: "2001:db8::1", port: 11212 });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should parse IPv6 address with brackets and default port", () => {
|
|
204
|
+
const result = client.parseUri("[::1]");
|
|
205
|
+
expect(result).toEqual({ host: "::1", port: 11211 });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should parse domain with port", () => {
|
|
209
|
+
const result = client.parseUri("memcache.example.com:11213");
|
|
210
|
+
expect(result).toEqual({ host: "memcache.example.com", port: 11213 });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should parse Unix domain socket path", () => {
|
|
214
|
+
const result = client.parseUri("/var/run/memcached.sock");
|
|
215
|
+
expect(result).toEqual({ host: "/var/run/memcached.sock", port: 0 });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should parse Unix domain socket path from unix:// format", () => {
|
|
219
|
+
const result = client.parseUri("unix:///var/run/memcached.sock");
|
|
220
|
+
expect(result).toEqual({ host: "/var/run/memcached.sock", port: 0 });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should throw error for invalid protocol", () => {
|
|
224
|
+
expect(() => client.parseUri("http://localhost:11211")).toThrow(
|
|
225
|
+
"Invalid protocol",
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("should throw error for empty host", () => {
|
|
230
|
+
expect(() => client.parseUri(":11211")).toThrow(
|
|
231
|
+
"Invalid URI format: host is required",
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should throw error for invalid port", () => {
|
|
236
|
+
expect(() => client.parseUri("localhost:abc")).toThrow(
|
|
237
|
+
"Invalid port number",
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should throw error for negative port", () => {
|
|
242
|
+
expect(() => client.parseUri("localhost:-1")).toThrow(
|
|
243
|
+
"Invalid port number",
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should throw error for port zero with network host", () => {
|
|
248
|
+
expect(() => client.parseUri("localhost:0")).toThrow(
|
|
249
|
+
"Invalid port number",
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should throw error for port too large", () => {
|
|
254
|
+
expect(() => client.parseUri("localhost:65536")).toThrow(
|
|
255
|
+
"Invalid port number",
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should throw error for IPv6 missing closing bracket", () => {
|
|
260
|
+
expect(() => client.parseUri("[::1:11211")).toThrow(
|
|
261
|
+
"Invalid IPv6 format: missing closing bracket",
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("should throw error for IPv6 with empty host", () => {
|
|
266
|
+
expect(() => client.parseUri("[]:11211")).toThrow(
|
|
267
|
+
"Invalid URI format: host is required",
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should throw error for IPv6 with invalid format after bracket", () => {
|
|
272
|
+
expect(() => client.parseUri("[::1]abc")).toThrow(
|
|
273
|
+
"Invalid IPv6 format: expected ':' after bracket",
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should throw error for IPv6 with invalid port", () => {
|
|
278
|
+
expect(() => client.parseUri("[::1]:99999")).toThrow(
|
|
279
|
+
"Invalid port number",
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should throw error for too many colons in regular format", () => {
|
|
284
|
+
expect(() => client.parseUri("host:port:extra")).toThrow(
|
|
285
|
+
"Invalid URI format",
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("Key Validation", () => {
|
|
291
|
+
it("should throw error for empty key", async () => {
|
|
292
|
+
await expect(async () => {
|
|
293
|
+
await client.get("");
|
|
294
|
+
}).rejects.toThrow("Key cannot be empty");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should throw error for key longer than 250 characters", async () => {
|
|
298
|
+
const longKey = "a".repeat(251);
|
|
299
|
+
await expect(async () => {
|
|
300
|
+
await client.get(longKey);
|
|
301
|
+
}).rejects.toThrow("Key length cannot exceed 250 characters");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("should throw error for key with spaces", async () => {
|
|
305
|
+
await expect(async () => {
|
|
306
|
+
await client.get("key with spaces");
|
|
307
|
+
}).rejects.toThrow(
|
|
308
|
+
"Key cannot contain spaces, newlines, or null characters",
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should throw error for key with newlines", async () => {
|
|
313
|
+
await expect(async () => {
|
|
314
|
+
await client.get("key\nwith\nnewlines");
|
|
315
|
+
}).rejects.toThrow(
|
|
316
|
+
"Key cannot contain spaces, newlines, or null characters",
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("Connection Management", () => {
|
|
322
|
+
it("should handle connection state", () => {
|
|
323
|
+
expect(client.isConnected()).toBe(false);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("should lazy connect when not connected", async () => {
|
|
327
|
+
// With new architecture, connections are lazy - node connects on first use
|
|
328
|
+
const testClient = new Memcache();
|
|
329
|
+
expect(testClient.isConnected()).toBe(false);
|
|
330
|
+
|
|
331
|
+
// This should auto-connect
|
|
332
|
+
await testClient.set("lazy-test", "value");
|
|
333
|
+
expect(testClient.isConnected()).toBe(true);
|
|
334
|
+
|
|
335
|
+
await testClient.disconnect();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("should handle connecting when already connected", async () => {
|
|
339
|
+
const client12 = new Memcache();
|
|
340
|
+
await client12.connect();
|
|
341
|
+
expect(client12.isConnected()).toBe(true);
|
|
342
|
+
|
|
343
|
+
// Try to connect again - should resolve immediately
|
|
344
|
+
await client12.connect();
|
|
345
|
+
expect(client12.isConnected()).toBe(true);
|
|
346
|
+
|
|
347
|
+
client12.disconnect();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("should handle connection errors", async () => {
|
|
351
|
+
const client13 = new Memcache({
|
|
352
|
+
timeout: 100,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await expect(client13.connect("0.0.0.0")).rejects.toThrow();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("should handle connection timeout", async () => {
|
|
359
|
+
const client14 = new Memcache({
|
|
360
|
+
nodes: ["192.0.2.0:11211"], // TEST-NET-1, will timeout
|
|
361
|
+
timeout: 100, // Very short timeout
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Try to connect to all nodes (which will fail)
|
|
365
|
+
await expect(client14.connect()).rejects.toThrow("Connection timeout");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("should handle error event before connection is established", async () => {
|
|
369
|
+
const client16 = new Memcache({
|
|
370
|
+
timeout: 100,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// This should trigger an error immediately due to invalid port
|
|
374
|
+
await expect(client16.connect("localhost")).rejects.toThrow();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Removed: socket error tests - these test internal implementation
|
|
378
|
+
// Error events are now tested at the MemcacheNode level
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe("Reconnect", () => {
|
|
382
|
+
it("should reconnect all nodes successfully", async () => {
|
|
383
|
+
const testClient = new Memcache();
|
|
384
|
+
|
|
385
|
+
// First connection
|
|
386
|
+
await testClient.connect();
|
|
387
|
+
expect(testClient.isConnected()).toBe(true);
|
|
388
|
+
|
|
389
|
+
// Set a value to ensure connection is working
|
|
390
|
+
await testClient.set("reconnect-test", "initial-value");
|
|
391
|
+
const value1 = await testClient.get("reconnect-test");
|
|
392
|
+
expect(value1).toBe("initial-value");
|
|
393
|
+
|
|
394
|
+
// Reconnect
|
|
395
|
+
await testClient.reconnect();
|
|
396
|
+
expect(testClient.isConnected()).toBe(true);
|
|
397
|
+
|
|
398
|
+
// Verify we can still execute commands after reconnect
|
|
399
|
+
await testClient.set("reconnect-test", "new-value");
|
|
400
|
+
const value2 = await testClient.get("reconnect-test");
|
|
401
|
+
expect(value2).toBe("new-value");
|
|
402
|
+
|
|
403
|
+
await testClient.disconnect();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("should reconnect multiple nodes in parallel", async () => {
|
|
407
|
+
const testClient = new Memcache({
|
|
408
|
+
nodes: ["localhost:11211"],
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Connect all nodes
|
|
412
|
+
await testClient.connect();
|
|
413
|
+
expect(testClient.isConnected()).toBe(true);
|
|
414
|
+
|
|
415
|
+
// Set values on different nodes
|
|
416
|
+
await testClient.set("key1", "value1");
|
|
417
|
+
await testClient.set("key2", "value2");
|
|
418
|
+
|
|
419
|
+
// Reconnect all nodes
|
|
420
|
+
await testClient.reconnect();
|
|
421
|
+
expect(testClient.isConnected()).toBe(true);
|
|
422
|
+
|
|
423
|
+
// Verify all nodes work after reconnect
|
|
424
|
+
const value1 = await testClient.get("key1");
|
|
425
|
+
const value2 = await testClient.get("key2");
|
|
426
|
+
expect(value1).toBe("value1");
|
|
427
|
+
expect(value2).toBe("value2");
|
|
428
|
+
|
|
429
|
+
await testClient.disconnect();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("should handle reconnect when not initially connected", async () => {
|
|
433
|
+
const testClient = new Memcache();
|
|
434
|
+
|
|
435
|
+
// Don't connect first
|
|
436
|
+
expect(testClient.isConnected()).toBe(false);
|
|
437
|
+
|
|
438
|
+
// Reconnect should establish connections
|
|
439
|
+
await testClient.reconnect();
|
|
440
|
+
expect(testClient.isConnected()).toBe(true);
|
|
441
|
+
|
|
442
|
+
// Verify connections work
|
|
443
|
+
await testClient.set("reconnect-initial", "test-value");
|
|
444
|
+
const value = await testClient.get("reconnect-initial");
|
|
445
|
+
expect(value).toBe("test-value");
|
|
446
|
+
|
|
447
|
+
await testClient.disconnect();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("should emit connect events for all nodes on reconnect", async () => {
|
|
451
|
+
const testClient = new Memcache();
|
|
452
|
+
await testClient.connect();
|
|
453
|
+
|
|
454
|
+
let connectCount = 0;
|
|
455
|
+
testClient.on(MemcacheEvents.CONNECT, () => {
|
|
456
|
+
connectCount++;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Reconnect
|
|
460
|
+
await testClient.reconnect();
|
|
461
|
+
|
|
462
|
+
// Should emit connect event for each node
|
|
463
|
+
expect(connectCount).toBeGreaterThan(0);
|
|
464
|
+
expect(testClient.isConnected()).toBe(true);
|
|
465
|
+
|
|
466
|
+
await testClient.disconnect();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("should clear state on reconnect", async () => {
|
|
470
|
+
const testClient = new Memcache();
|
|
471
|
+
await testClient.connect();
|
|
472
|
+
|
|
473
|
+
// Set some data
|
|
474
|
+
await testClient.set("clear-test", "value");
|
|
475
|
+
|
|
476
|
+
// Get the nodes to check internal state
|
|
477
|
+
const nodes = testClient.getNodes();
|
|
478
|
+
const firstNode = Array.from(nodes.values())[0];
|
|
479
|
+
|
|
480
|
+
// Reconnect
|
|
481
|
+
await testClient.reconnect();
|
|
482
|
+
|
|
483
|
+
// Command queue should be empty after reconnect
|
|
484
|
+
expect(firstNode.commandQueue.length).toBe(0);
|
|
485
|
+
expect(testClient.isConnected()).toBe(true);
|
|
486
|
+
|
|
487
|
+
await testClient.disconnect();
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe("Command Queue", () => {
|
|
492
|
+
it("should handle multiple commands with lazy connection", async () => {
|
|
493
|
+
const testClient = new Memcache();
|
|
494
|
+
|
|
495
|
+
// All commands should auto-connect and succeed
|
|
496
|
+
await testClient.connect();
|
|
497
|
+
await testClient.set("key1", "value1");
|
|
498
|
+
await testClient.set("key2", "value2");
|
|
499
|
+
await testClient.set("key3", "value3");
|
|
500
|
+
|
|
501
|
+
const results = await Promise.all([
|
|
502
|
+
testClient.get("key1"),
|
|
503
|
+
testClient.get("key2"),
|
|
504
|
+
testClient.get("key3"),
|
|
505
|
+
]);
|
|
506
|
+
|
|
507
|
+
expect(results).toEqual(["value1", "value2", "value3"]);
|
|
508
|
+
await testClient.disconnect();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe("Error Handling", () => {
|
|
513
|
+
it("should handle connection being closed during pending commands", async () => {
|
|
514
|
+
const client2 = new Memcache();
|
|
515
|
+
await client2.connect();
|
|
516
|
+
|
|
517
|
+
// Use a non-existent key with unique name
|
|
518
|
+
const uniqueKey = `test-close-${Date.now()}`;
|
|
519
|
+
|
|
520
|
+
// Start a command but don't await it
|
|
521
|
+
const pendingCommand = await client2
|
|
522
|
+
.get(uniqueKey)
|
|
523
|
+
.catch((e) => e.message);
|
|
524
|
+
|
|
525
|
+
// Immediately disconnect to catch command in flight
|
|
526
|
+
setImmediate(() => {
|
|
527
|
+
client2.disconnect();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const result = await pendingCommand;
|
|
531
|
+
// With async hook, command might complete as undefined (not found) or get "Connection closed"
|
|
532
|
+
expect([undefined, "Connection closed"]).toContain(result);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Removed: currentCommand is internal to MemcacheNode
|
|
536
|
+
// This tests internal implementation details
|
|
537
|
+
|
|
538
|
+
it("should handle multiple pending commands when connection closes", async () => {
|
|
539
|
+
const client3 = new Memcache();
|
|
540
|
+
await client3.connect();
|
|
541
|
+
|
|
542
|
+
// Use unique keys that don't exist
|
|
543
|
+
const timestamp = Date.now();
|
|
544
|
+
|
|
545
|
+
// Start multiple commands without awaiting
|
|
546
|
+
const commands = [
|
|
547
|
+
client3.get(`key1-${timestamp}`).catch((e) => e.message),
|
|
548
|
+
client3.get(`key2-${timestamp}`).catch((e) => e.message),
|
|
549
|
+
client3.get(`key3-${timestamp}`).catch((e) => e.message),
|
|
550
|
+
];
|
|
551
|
+
|
|
552
|
+
// Immediately disconnect to catch commands in flight
|
|
553
|
+
setImmediate(() => {
|
|
554
|
+
client3.disconnect();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const results = await Promise.all(commands);
|
|
558
|
+
// With async hook, commands might complete as undefined or get "Connection closed"
|
|
559
|
+
results.forEach((result) => {
|
|
560
|
+
expect([undefined, "Connection closed"]).toContain(result);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("should handle protocol errors in responses", async () => {
|
|
565
|
+
const client4 = new Memcache();
|
|
566
|
+
await client4.connect();
|
|
567
|
+
|
|
568
|
+
// Test with an invalid key to trigger an error
|
|
569
|
+
const veryLongKey = "k".repeat(251);
|
|
570
|
+
await expect(client4.get(veryLongKey)).rejects.toThrow(
|
|
571
|
+
"Key length cannot exceed 250 characters",
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
client4.disconnect();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// Removed: Socket mocking tests for error responses and protocol parsing
|
|
578
|
+
// These test internal implementation details at the socket level
|
|
579
|
+
// Protocol handling is tested in MemcacheNode (node.test.ts)
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe("Memcached Integration Tests", () => {
|
|
583
|
+
it("should connect to memcached server", async () => {
|
|
584
|
+
await client.connect();
|
|
585
|
+
expect(client.isConnected()).toBe(true);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("should set and get a value", async () => {
|
|
589
|
+
await client.connect();
|
|
590
|
+
const key = "test-key";
|
|
591
|
+
const value = "test-value";
|
|
592
|
+
|
|
593
|
+
const setResult = await client.set(key, value);
|
|
594
|
+
expect(setResult).toBe(true);
|
|
595
|
+
|
|
596
|
+
const getValue = await client.get(key);
|
|
597
|
+
expect(getValue).toBe(value);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("should handle multiple gets", async () => {
|
|
601
|
+
await client.connect();
|
|
602
|
+
|
|
603
|
+
// Clean up any leftover key3 from previous tests
|
|
604
|
+
await client.delete("key3");
|
|
605
|
+
|
|
606
|
+
await client.set("key1", "value1");
|
|
607
|
+
await client.set("key2", "value2");
|
|
608
|
+
|
|
609
|
+
const results = await client.gets(["key1", "key2", "key3"]);
|
|
610
|
+
expect(results.get("key1")).toBe("value1");
|
|
611
|
+
expect(results.get("key2")).toBe("value2");
|
|
612
|
+
expect(results.has("key3")).toBe(false);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("should delete a key", async () => {
|
|
616
|
+
await client.connect();
|
|
617
|
+
const key = "delete-test";
|
|
618
|
+
|
|
619
|
+
await client.set(key, "value");
|
|
620
|
+
const deleteResult = await client.delete(key);
|
|
621
|
+
expect(deleteResult).toBe(true);
|
|
622
|
+
|
|
623
|
+
const getValue = await client.get(key);
|
|
624
|
+
expect(getValue).toBe(undefined);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("should increment and decrement values", async () => {
|
|
628
|
+
await client.connect();
|
|
629
|
+
const key = "counter";
|
|
630
|
+
|
|
631
|
+
await client.set(key, "10");
|
|
632
|
+
|
|
633
|
+
const incrResult = await client.incr(key, 5);
|
|
634
|
+
expect(incrResult).toBe(15);
|
|
635
|
+
|
|
636
|
+
const decrResult = await client.decr(key, 3);
|
|
637
|
+
expect(decrResult).toBe(12);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("should handle add command", async () => {
|
|
641
|
+
await client.connect();
|
|
642
|
+
const key = "add-test";
|
|
643
|
+
|
|
644
|
+
const firstAdd = await client.add(key, "value1");
|
|
645
|
+
expect(firstAdd).toBe(true);
|
|
646
|
+
|
|
647
|
+
const secondAdd = await client.add(key, "value2");
|
|
648
|
+
expect(secondAdd).toBe(false);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it("should handle replace command", async () => {
|
|
652
|
+
await client.connect();
|
|
653
|
+
const key = "replace-test";
|
|
654
|
+
|
|
655
|
+
const replaceNonExistent = await client.replace(key, "value1");
|
|
656
|
+
expect(replaceNonExistent).toBe(false);
|
|
657
|
+
|
|
658
|
+
await client.set(key, "initial");
|
|
659
|
+
const replaceExisting = await client.replace(key, "replaced");
|
|
660
|
+
expect(replaceExisting).toBe(true);
|
|
661
|
+
|
|
662
|
+
const getValue = await client.get(key);
|
|
663
|
+
expect(getValue).toBe("replaced");
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Removed: CAS tests with socket mocking
|
|
667
|
+
// CAS requires gets command with CAS tokens, which needs special protocol support
|
|
668
|
+
// This functionality can be added in the future with proper implementation
|
|
669
|
+
|
|
670
|
+
it("should handle append and prepend", async () => {
|
|
671
|
+
await client.connect();
|
|
672
|
+
const key = "concat-test";
|
|
673
|
+
|
|
674
|
+
await client.set(key, "middle");
|
|
675
|
+
|
|
676
|
+
await client.prepend(key, "start-");
|
|
677
|
+
await client.append(key, "-end");
|
|
678
|
+
|
|
679
|
+
const getValue = await client.get(key);
|
|
680
|
+
expect(getValue).toBe("start-middle-end");
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("should handle touch command", async () => {
|
|
684
|
+
await client.connect();
|
|
685
|
+
const key = "touch-test";
|
|
686
|
+
|
|
687
|
+
await client.set(key, "value", 3600);
|
|
688
|
+
const touchResult = await client.touch(key, 7200);
|
|
689
|
+
expect(touchResult).toBe(true);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("should handle flush commands", async () => {
|
|
693
|
+
await client.connect();
|
|
694
|
+
|
|
695
|
+
await client.set("flush1", "value1");
|
|
696
|
+
await client.set("flush2", "value2");
|
|
697
|
+
|
|
698
|
+
const flushResult = await client.flush();
|
|
699
|
+
expect(flushResult).toBe(true);
|
|
700
|
+
|
|
701
|
+
const getValue1 = await client.get("flush1");
|
|
702
|
+
const getValue2 = await client.get("flush2");
|
|
703
|
+
expect(getValue1).toBe(undefined);
|
|
704
|
+
expect(getValue2).toBe(undefined);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("should handle flushAll with delay", async () => {
|
|
708
|
+
await client.connect();
|
|
709
|
+
|
|
710
|
+
// Test flushAll with delay parameter (just test the command works)
|
|
711
|
+
const flushResult = await client.flush(1);
|
|
712
|
+
expect(flushResult).toBe(true);
|
|
713
|
+
|
|
714
|
+
// Also test without delay
|
|
715
|
+
const flushResultNoDelay = await client.flush();
|
|
716
|
+
expect(flushResultNoDelay).toBe(true);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("should get stats", async () => {
|
|
720
|
+
await client.connect();
|
|
721
|
+
|
|
722
|
+
const stats = await client.stats();
|
|
723
|
+
expect(stats).toBeDefined();
|
|
724
|
+
expect(typeof stats).toBe("object");
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("should get stats with type", async () => {
|
|
728
|
+
await client.connect();
|
|
729
|
+
|
|
730
|
+
const stats = await client.stats("items");
|
|
731
|
+
expect(stats).toBeDefined();
|
|
732
|
+
expect(typeof stats).toBe("object");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it("should get version from all nodes", async () => {
|
|
736
|
+
await client.connect();
|
|
737
|
+
|
|
738
|
+
const versions = await client.version();
|
|
739
|
+
expect(versions).toBeDefined();
|
|
740
|
+
expect(versions).toBeInstanceOf(Map);
|
|
741
|
+
expect(versions.size).toBeGreaterThan(0);
|
|
742
|
+
|
|
743
|
+
// Each node should have a version string
|
|
744
|
+
for (const [nodeId, version] of versions) {
|
|
745
|
+
expect(typeof nodeId).toBe("string");
|
|
746
|
+
expect(typeof version).toBe("string");
|
|
747
|
+
expect(version.length).toBeGreaterThan(0);
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("should handle quit command", async () => {
|
|
752
|
+
await client.connect();
|
|
753
|
+
expect(client.isConnected()).toBe(true);
|
|
754
|
+
|
|
755
|
+
await client.quit();
|
|
756
|
+
expect(client.isConnected()).toBe(false);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it("should handle disconnect", async () => {
|
|
760
|
+
await client.connect();
|
|
761
|
+
expect(client.isConnected()).toBe(true);
|
|
762
|
+
|
|
763
|
+
client.disconnect();
|
|
764
|
+
expect(client.isConnected()).toBe(false);
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
describe("Hooks", () => {
|
|
769
|
+
it("should call beforeHook and afterHook for get operation", async () => {
|
|
770
|
+
const beforeHookMock = vi.fn();
|
|
771
|
+
const afterHookMock = vi.fn();
|
|
772
|
+
|
|
773
|
+
client.onHook("before:get", beforeHookMock);
|
|
774
|
+
client.onHook("after:get", afterHookMock);
|
|
775
|
+
|
|
776
|
+
await client.connect();
|
|
777
|
+
await client.set("hook-test", "hook-value");
|
|
778
|
+
|
|
779
|
+
const result = await client.get("hook-test");
|
|
780
|
+
|
|
781
|
+
expect(beforeHookMock).toHaveBeenCalledWith({ key: "hook-test" });
|
|
782
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
783
|
+
key: "hook-test",
|
|
784
|
+
value: "hook-value",
|
|
785
|
+
});
|
|
786
|
+
expect(result).toBe("hook-value");
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it("should call hooks even when key doesn't exist", async () => {
|
|
790
|
+
const beforeHookMock = vi.fn();
|
|
791
|
+
const afterHookMock = vi.fn();
|
|
792
|
+
|
|
793
|
+
client.onHook("before:get", beforeHookMock);
|
|
794
|
+
client.onHook("after:get", afterHookMock);
|
|
795
|
+
|
|
796
|
+
await client.connect();
|
|
797
|
+
|
|
798
|
+
const result = await client.get("non-existent-hook-key");
|
|
799
|
+
|
|
800
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
801
|
+
key: "non-existent-hook-key",
|
|
802
|
+
});
|
|
803
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
804
|
+
key: "non-existent-hook-key",
|
|
805
|
+
value: undefined,
|
|
806
|
+
});
|
|
807
|
+
expect(result).toBe(undefined);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it("should support multiple hook listeners", async () => {
|
|
811
|
+
const beforeHook1 = vi.fn();
|
|
812
|
+
const beforeHook2 = vi.fn();
|
|
813
|
+
const afterHook1 = vi.fn();
|
|
814
|
+
const afterHook2 = vi.fn();
|
|
815
|
+
|
|
816
|
+
client.onHook("before:get", beforeHook1);
|
|
817
|
+
client.onHook("before:get", beforeHook2);
|
|
818
|
+
client.onHook("after:get", afterHook1);
|
|
819
|
+
client.onHook("after:get", afterHook2);
|
|
820
|
+
|
|
821
|
+
await client.connect();
|
|
822
|
+
await client.set("multi-hook-test", "value");
|
|
823
|
+
await client.get("multi-hook-test");
|
|
824
|
+
|
|
825
|
+
expect(beforeHook1).toHaveBeenCalled();
|
|
826
|
+
expect(beforeHook2).toHaveBeenCalled();
|
|
827
|
+
expect(afterHook1).toHaveBeenCalled();
|
|
828
|
+
expect(afterHook2).toHaveBeenCalled();
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it("should allow removing hook listeners", async () => {
|
|
832
|
+
const hookMock = vi.fn();
|
|
833
|
+
|
|
834
|
+
client.onHook("before:get", hookMock);
|
|
835
|
+
|
|
836
|
+
await client.connect();
|
|
837
|
+
await client.set("remove-hook-test", "value");
|
|
838
|
+
|
|
839
|
+
// First call should trigger the hook
|
|
840
|
+
await client.get("remove-hook-test");
|
|
841
|
+
expect(hookMock).toHaveBeenCalledTimes(1);
|
|
842
|
+
|
|
843
|
+
// Remove the hook
|
|
844
|
+
client.removeHook("before:get", hookMock);
|
|
845
|
+
|
|
846
|
+
// Second call should not trigger the hook
|
|
847
|
+
await client.get("remove-hook-test");
|
|
848
|
+
expect(hookMock).toHaveBeenCalledTimes(1);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it("should handle async hooks", async () => {
|
|
852
|
+
const asyncBeforeHook = vi.fn().mockImplementation(async () => {
|
|
853
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
const asyncAfterHook = vi.fn().mockImplementation(async () => {
|
|
857
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
client.onHook("before:get", asyncBeforeHook);
|
|
861
|
+
client.onHook("after:get", asyncAfterHook);
|
|
862
|
+
|
|
863
|
+
await client.connect();
|
|
864
|
+
await client.set("async-hook-test", "async-value");
|
|
865
|
+
|
|
866
|
+
const start = Date.now();
|
|
867
|
+
const result = await client.get("async-hook-test");
|
|
868
|
+
const duration = Date.now() - start;
|
|
869
|
+
|
|
870
|
+
expect(asyncBeforeHook).toHaveBeenCalled();
|
|
871
|
+
expect(asyncAfterHook).toHaveBeenCalled();
|
|
872
|
+
expect(result).toBe("async-value");
|
|
873
|
+
expect(duration).toBeGreaterThanOrEqual(18); // At least 18ms to account for timing imprecision
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it("should handle hook errors based on throwHookErrors setting", async () => {
|
|
877
|
+
// Test with throwHookErrors = true (should throw)
|
|
878
|
+
const errorClient = new Memcache();
|
|
879
|
+
errorClient.throwHookErrors = true;
|
|
880
|
+
|
|
881
|
+
const errorHook = vi.fn().mockImplementation(() => {
|
|
882
|
+
throw new Error("Hook error");
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
errorClient.onHook("before:get", errorHook);
|
|
886
|
+
|
|
887
|
+
await errorClient.connect();
|
|
888
|
+
await errorClient.set("error-hook-test", "value");
|
|
889
|
+
|
|
890
|
+
// Hook error should propagate and reject the promise
|
|
891
|
+
await expect(errorClient.get("error-hook-test")).rejects.toThrow(
|
|
892
|
+
"Hook error",
|
|
893
|
+
);
|
|
894
|
+
expect(errorHook).toHaveBeenCalled();
|
|
895
|
+
|
|
896
|
+
await errorClient.disconnect();
|
|
897
|
+
|
|
898
|
+
// Test with throwHookErrors = false (should not throw)
|
|
899
|
+
const noErrorClient = new Memcache();
|
|
900
|
+
noErrorClient.throwHookErrors = false;
|
|
901
|
+
|
|
902
|
+
const errorHook2 = vi.fn().mockImplementation(() => {
|
|
903
|
+
throw new Error("Hook error 2");
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
noErrorClient.onHook("before:get", errorHook2);
|
|
907
|
+
|
|
908
|
+
await noErrorClient.connect();
|
|
909
|
+
await noErrorClient.set("error-hook-test2", "value2");
|
|
910
|
+
|
|
911
|
+
// Operation should succeed despite hook error
|
|
912
|
+
const result = await noErrorClient.get("error-hook-test2");
|
|
913
|
+
expect(result).toBe("value2");
|
|
914
|
+
expect(errorHook2).toHaveBeenCalled();
|
|
915
|
+
|
|
916
|
+
await noErrorClient.disconnect();
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it("should provide hook context with correct data", async () => {
|
|
920
|
+
const beforeHookMock = vi.fn();
|
|
921
|
+
const afterHookMock = vi.fn();
|
|
922
|
+
|
|
923
|
+
client.onHook("before:get", beforeHookMock);
|
|
924
|
+
client.onHook("after:get", afterHookMock);
|
|
925
|
+
|
|
926
|
+
await client.connect();
|
|
927
|
+
|
|
928
|
+
// Test with multiple keys
|
|
929
|
+
await client.set("context-key1", "value1");
|
|
930
|
+
await client.set("context-key2", "value2");
|
|
931
|
+
|
|
932
|
+
await client.get("context-key1");
|
|
933
|
+
|
|
934
|
+
expect(beforeHookMock).toHaveBeenLastCalledWith({ key: "context-key1" });
|
|
935
|
+
expect(afterHookMock).toHaveBeenLastCalledWith({
|
|
936
|
+
key: "context-key1",
|
|
937
|
+
value: "value1",
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
await client.get("context-key2");
|
|
941
|
+
|
|
942
|
+
expect(beforeHookMock).toHaveBeenLastCalledWith({ key: "context-key2" });
|
|
943
|
+
expect(afterHookMock).toHaveBeenLastCalledWith({
|
|
944
|
+
key: "context-key2",
|
|
945
|
+
value: "value2",
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
it("should allow using onceHook for single execution", async () => {
|
|
950
|
+
const onceHookMock = vi.fn();
|
|
951
|
+
|
|
952
|
+
client.onceHook("before:get", onceHookMock);
|
|
953
|
+
|
|
954
|
+
await client.connect();
|
|
955
|
+
await client.set("once-hook-test", "value");
|
|
956
|
+
|
|
957
|
+
// First call should trigger the hook
|
|
958
|
+
await client.get("once-hook-test");
|
|
959
|
+
expect(onceHookMock).toHaveBeenCalledTimes(1);
|
|
960
|
+
|
|
961
|
+
// Second call should not trigger the hook (it was removed after first execution)
|
|
962
|
+
await client.get("once-hook-test");
|
|
963
|
+
expect(onceHookMock).toHaveBeenCalledTimes(1);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
it("should allow clearing all hooks", async () => {
|
|
967
|
+
const hook1 = vi.fn();
|
|
968
|
+
const hook2 = vi.fn();
|
|
969
|
+
|
|
970
|
+
client.onHook("before:get", hook1);
|
|
971
|
+
client.onHook("after:get", hook2);
|
|
972
|
+
|
|
973
|
+
await client.connect();
|
|
974
|
+
await client.set("clear-hooks-test", "value");
|
|
975
|
+
|
|
976
|
+
// First call should trigger hooks
|
|
977
|
+
await client.get("clear-hooks-test");
|
|
978
|
+
expect(hook1).toHaveBeenCalledTimes(1);
|
|
979
|
+
expect(hook2).toHaveBeenCalledTimes(1);
|
|
980
|
+
|
|
981
|
+
// Clear all hooks
|
|
982
|
+
client.clearHooks();
|
|
983
|
+
|
|
984
|
+
// Second call should not trigger any hooks
|
|
985
|
+
await client.get("clear-hooks-test");
|
|
986
|
+
expect(hook1).toHaveBeenCalledTimes(1);
|
|
987
|
+
expect(hook2).toHaveBeenCalledTimes(1);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it("should maintain hook execution order", async () => {
|
|
991
|
+
const executionOrder: string[] = [];
|
|
992
|
+
|
|
993
|
+
client.onHook("before:get", () => {
|
|
994
|
+
executionOrder.push("before1");
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
client.onHook("before:get", () => {
|
|
998
|
+
executionOrder.push("before2");
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
client.onHook("after:get", () => {
|
|
1002
|
+
executionOrder.push("after1");
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
client.onHook("after:get", () => {
|
|
1006
|
+
executionOrder.push("after2");
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
await client.connect();
|
|
1010
|
+
await client.set("order-test", "value");
|
|
1011
|
+
await client.get("order-test");
|
|
1012
|
+
|
|
1013
|
+
expect(executionOrder).toEqual([
|
|
1014
|
+
"before1",
|
|
1015
|
+
"before2",
|
|
1016
|
+
"after1",
|
|
1017
|
+
"after2",
|
|
1018
|
+
]);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
it("should call beforeHook and afterHook for set operation", async () => {
|
|
1022
|
+
const beforeHookMock = vi.fn();
|
|
1023
|
+
const afterHookMock = vi.fn();
|
|
1024
|
+
|
|
1025
|
+
client.onHook("before:set", beforeHookMock);
|
|
1026
|
+
client.onHook("after:set", afterHookMock);
|
|
1027
|
+
|
|
1028
|
+
await client.connect();
|
|
1029
|
+
|
|
1030
|
+
const result = await client.set(
|
|
1031
|
+
"set-hook-test",
|
|
1032
|
+
"set-hook-value",
|
|
1033
|
+
3600,
|
|
1034
|
+
42,
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1038
|
+
key: "set-hook-test",
|
|
1039
|
+
value: "set-hook-value",
|
|
1040
|
+
exptime: 3600,
|
|
1041
|
+
flags: 42,
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1045
|
+
key: "set-hook-test",
|
|
1046
|
+
value: "set-hook-value",
|
|
1047
|
+
exptime: 3600,
|
|
1048
|
+
flags: 42,
|
|
1049
|
+
success: true,
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
expect(result).toBe(true);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it("should call set hooks with default parameters", async () => {
|
|
1056
|
+
const beforeHookMock = vi.fn();
|
|
1057
|
+
const afterHookMock = vi.fn();
|
|
1058
|
+
|
|
1059
|
+
client.onHook("before:set", beforeHookMock);
|
|
1060
|
+
client.onHook("after:set", afterHookMock);
|
|
1061
|
+
|
|
1062
|
+
await client.connect();
|
|
1063
|
+
|
|
1064
|
+
const result = await client.set("set-hook-default", "default-value");
|
|
1065
|
+
|
|
1066
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1067
|
+
key: "set-hook-default",
|
|
1068
|
+
value: "default-value",
|
|
1069
|
+
exptime: 0,
|
|
1070
|
+
flags: 0,
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1074
|
+
key: "set-hook-default",
|
|
1075
|
+
value: "default-value",
|
|
1076
|
+
exptime: 0,
|
|
1077
|
+
flags: 0,
|
|
1078
|
+
success: true,
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
expect(result).toBe(true);
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
it("should handle async set hooks", async () => {
|
|
1085
|
+
const asyncBeforeHook = vi.fn().mockImplementation(async () => {
|
|
1086
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
const asyncAfterHook = vi.fn().mockImplementation(async () => {
|
|
1090
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
client.onHook("before:set", asyncBeforeHook);
|
|
1094
|
+
client.onHook("after:set", asyncAfterHook);
|
|
1095
|
+
|
|
1096
|
+
await client.connect();
|
|
1097
|
+
|
|
1098
|
+
const result = await client.set("async-set-test", "async-value");
|
|
1099
|
+
|
|
1100
|
+
expect(asyncBeforeHook).toHaveBeenCalled();
|
|
1101
|
+
expect(asyncAfterHook).toHaveBeenCalled();
|
|
1102
|
+
expect(result).toBe(true);
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it("should call beforeHook and afterHook for gets operation", async () => {
|
|
1106
|
+
const beforeHookMock = vi.fn();
|
|
1107
|
+
const afterHookMock = vi.fn();
|
|
1108
|
+
|
|
1109
|
+
client.onHook("before:gets", beforeHookMock);
|
|
1110
|
+
client.onHook("after:gets", afterHookMock);
|
|
1111
|
+
|
|
1112
|
+
await client.connect();
|
|
1113
|
+
|
|
1114
|
+
// Set some values first
|
|
1115
|
+
await client.set("gets-hook-1", "value1");
|
|
1116
|
+
await client.set("gets-hook-2", "value2");
|
|
1117
|
+
await client.set("gets-hook-3", "value3");
|
|
1118
|
+
|
|
1119
|
+
const keys = [
|
|
1120
|
+
"gets-hook-1",
|
|
1121
|
+
"gets-hook-2",
|
|
1122
|
+
"gets-hook-3",
|
|
1123
|
+
"non-existent",
|
|
1124
|
+
];
|
|
1125
|
+
const result = await client.gets(keys);
|
|
1126
|
+
|
|
1127
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1128
|
+
keys: ["gets-hook-1", "gets-hook-2", "gets-hook-3", "non-existent"],
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
const expectedMap = new Map([
|
|
1132
|
+
["gets-hook-1", "value1"],
|
|
1133
|
+
["gets-hook-2", "value2"],
|
|
1134
|
+
["gets-hook-3", "value3"],
|
|
1135
|
+
]);
|
|
1136
|
+
|
|
1137
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1138
|
+
keys: ["gets-hook-1", "gets-hook-2", "gets-hook-3", "non-existent"],
|
|
1139
|
+
values: expectedMap,
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
expect(result.get("gets-hook-1")).toBe("value1");
|
|
1143
|
+
expect(result.get("gets-hook-2")).toBe("value2");
|
|
1144
|
+
expect(result.get("gets-hook-3")).toBe("value3");
|
|
1145
|
+
expect(result.has("non-existent")).toBe(false);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it("should call gets hooks when all keys are missing", async () => {
|
|
1149
|
+
const beforeHookMock = vi.fn();
|
|
1150
|
+
const afterHookMock = vi.fn();
|
|
1151
|
+
|
|
1152
|
+
client.onHook("before:gets", beforeHookMock);
|
|
1153
|
+
client.onHook("after:gets", afterHookMock);
|
|
1154
|
+
|
|
1155
|
+
await client.connect();
|
|
1156
|
+
|
|
1157
|
+
const keys = ["missing1", "missing2", "missing3"];
|
|
1158
|
+
const result = await client.gets(keys);
|
|
1159
|
+
|
|
1160
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1161
|
+
keys: ["missing1", "missing2", "missing3"],
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1165
|
+
keys: ["missing1", "missing2", "missing3"],
|
|
1166
|
+
values: new Map(), // Empty map when no keys found
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
expect(result.size).toBe(0);
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
it("should handle async gets hooks", async () => {
|
|
1173
|
+
const asyncBeforeHook = vi.fn().mockImplementation(async () => {
|
|
1174
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
const asyncAfterHook = vi.fn().mockImplementation(async () => {
|
|
1178
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
client.onHook("before:gets", asyncBeforeHook);
|
|
1182
|
+
client.onHook("after:gets", asyncAfterHook);
|
|
1183
|
+
|
|
1184
|
+
await client.connect();
|
|
1185
|
+
await client.set("async-gets-1", "value1");
|
|
1186
|
+
await client.set("async-gets-2", "value2");
|
|
1187
|
+
|
|
1188
|
+
const start = Date.now();
|
|
1189
|
+
const result = await client.gets(["async-gets-1", "async-gets-2"]);
|
|
1190
|
+
const duration = Date.now() - start;
|
|
1191
|
+
|
|
1192
|
+
expect(asyncBeforeHook).toHaveBeenCalled();
|
|
1193
|
+
expect(asyncAfterHook).toHaveBeenCalled();
|
|
1194
|
+
expect(result.get("async-gets-1")).toBe("value1");
|
|
1195
|
+
expect(result.get("async-gets-2")).toBe("value2");
|
|
1196
|
+
expect(duration).toBeGreaterThanOrEqual(18); // At least 18ms to account for timing imprecision
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
it("should call beforeHook and afterHook for add operation", async () => {
|
|
1200
|
+
const beforeHookMock = vi.fn();
|
|
1201
|
+
const afterHookMock = vi.fn();
|
|
1202
|
+
|
|
1203
|
+
client.onHook("before:add", beforeHookMock);
|
|
1204
|
+
client.onHook("after:add", afterHookMock);
|
|
1205
|
+
|
|
1206
|
+
await client.connect();
|
|
1207
|
+
|
|
1208
|
+
// First add should succeed
|
|
1209
|
+
const result = await client.add("add-hook-test", "add-value", 3600, 10);
|
|
1210
|
+
|
|
1211
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1212
|
+
key: "add-hook-test",
|
|
1213
|
+
value: "add-value",
|
|
1214
|
+
exptime: 3600,
|
|
1215
|
+
flags: 10,
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1219
|
+
key: "add-hook-test",
|
|
1220
|
+
value: "add-value",
|
|
1221
|
+
exptime: 3600,
|
|
1222
|
+
flags: 10,
|
|
1223
|
+
success: true,
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
expect(result).toBe(true);
|
|
1227
|
+
|
|
1228
|
+
// Second add should fail (key already exists)
|
|
1229
|
+
beforeHookMock.mockClear();
|
|
1230
|
+
afterHookMock.mockClear();
|
|
1231
|
+
|
|
1232
|
+
const result2 = await client.add(
|
|
1233
|
+
"add-hook-test",
|
|
1234
|
+
"another-value",
|
|
1235
|
+
1800,
|
|
1236
|
+
5,
|
|
1237
|
+
);
|
|
1238
|
+
|
|
1239
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1240
|
+
key: "add-hook-test",
|
|
1241
|
+
value: "another-value",
|
|
1242
|
+
exptime: 1800,
|
|
1243
|
+
flags: 5,
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1247
|
+
key: "add-hook-test",
|
|
1248
|
+
value: "another-value",
|
|
1249
|
+
exptime: 1800,
|
|
1250
|
+
flags: 5,
|
|
1251
|
+
success: false,
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
expect(result2).toBe(false);
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
it("should call add hooks with default parameters", async () => {
|
|
1258
|
+
const beforeHookMock = vi.fn();
|
|
1259
|
+
const afterHookMock = vi.fn();
|
|
1260
|
+
|
|
1261
|
+
client.onHook("before:add", beforeHookMock);
|
|
1262
|
+
client.onHook("after:add", afterHookMock);
|
|
1263
|
+
|
|
1264
|
+
await client.connect();
|
|
1265
|
+
|
|
1266
|
+
// Ensure key doesn't exist
|
|
1267
|
+
await client.delete("add-hook-default");
|
|
1268
|
+
|
|
1269
|
+
const result = await client.add("add-hook-default", "default-value");
|
|
1270
|
+
|
|
1271
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1272
|
+
key: "add-hook-default",
|
|
1273
|
+
value: "default-value",
|
|
1274
|
+
exptime: 0,
|
|
1275
|
+
flags: 0,
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1279
|
+
key: "add-hook-default",
|
|
1280
|
+
value: "default-value",
|
|
1281
|
+
exptime: 0,
|
|
1282
|
+
flags: 0,
|
|
1283
|
+
success: true,
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
expect(result).toBe(true);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
it("should handle async add hooks", async () => {
|
|
1290
|
+
const asyncBeforeHook = vi.fn().mockImplementation(async () => {
|
|
1291
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
const asyncAfterHook = vi.fn().mockImplementation(async () => {
|
|
1295
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
client.onHook("before:add", asyncBeforeHook);
|
|
1299
|
+
client.onHook("after:add", asyncAfterHook);
|
|
1300
|
+
|
|
1301
|
+
await client.connect();
|
|
1302
|
+
|
|
1303
|
+
// Ensure key doesn't exist
|
|
1304
|
+
await client.delete("async-add-test");
|
|
1305
|
+
|
|
1306
|
+
const start = Date.now();
|
|
1307
|
+
const result = await client.add("async-add-test", "async-value");
|
|
1308
|
+
const duration = Date.now() - start;
|
|
1309
|
+
|
|
1310
|
+
expect(asyncBeforeHook).toHaveBeenCalled();
|
|
1311
|
+
expect(asyncAfterHook).toHaveBeenCalled();
|
|
1312
|
+
expect(result).toBe(true);
|
|
1313
|
+
expect(duration).toBeGreaterThanOrEqual(18); // At least 18ms to account for timing imprecision
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
it("should call beforeHook and afterHook for replace operation", async () => {
|
|
1317
|
+
const beforeHookMock = vi.fn();
|
|
1318
|
+
const afterHookMock = vi.fn();
|
|
1319
|
+
|
|
1320
|
+
client.onHook("before:replace", beforeHookMock);
|
|
1321
|
+
client.onHook("after:replace", afterHookMock);
|
|
1322
|
+
|
|
1323
|
+
await client.connect();
|
|
1324
|
+
|
|
1325
|
+
// First replace should fail (key doesn't exist)
|
|
1326
|
+
const result1 = await client.replace(
|
|
1327
|
+
"replace-hook-test",
|
|
1328
|
+
"replace-value",
|
|
1329
|
+
3600,
|
|
1330
|
+
15,
|
|
1331
|
+
);
|
|
1332
|
+
|
|
1333
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1334
|
+
key: "replace-hook-test",
|
|
1335
|
+
value: "replace-value",
|
|
1336
|
+
exptime: 3600,
|
|
1337
|
+
flags: 15,
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1341
|
+
key: "replace-hook-test",
|
|
1342
|
+
value: "replace-value",
|
|
1343
|
+
exptime: 3600,
|
|
1344
|
+
flags: 15,
|
|
1345
|
+
success: false,
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
expect(result1).toBe(false);
|
|
1349
|
+
|
|
1350
|
+
// Set the key first
|
|
1351
|
+
await client.set("replace-hook-test", "initial-value");
|
|
1352
|
+
|
|
1353
|
+
// Clear mocks
|
|
1354
|
+
beforeHookMock.mockClear();
|
|
1355
|
+
afterHookMock.mockClear();
|
|
1356
|
+
|
|
1357
|
+
// Now replace should succeed
|
|
1358
|
+
const result2 = await client.replace(
|
|
1359
|
+
"replace-hook-test",
|
|
1360
|
+
"new-value",
|
|
1361
|
+
1800,
|
|
1362
|
+
20,
|
|
1363
|
+
);
|
|
1364
|
+
|
|
1365
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1366
|
+
key: "replace-hook-test",
|
|
1367
|
+
value: "new-value",
|
|
1368
|
+
exptime: 1800,
|
|
1369
|
+
flags: 20,
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1373
|
+
key: "replace-hook-test",
|
|
1374
|
+
value: "new-value",
|
|
1375
|
+
exptime: 1800,
|
|
1376
|
+
flags: 20,
|
|
1377
|
+
success: true,
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
expect(result2).toBe(true);
|
|
1381
|
+
|
|
1382
|
+
// Verify the value was replaced
|
|
1383
|
+
const getValue = await client.get("replace-hook-test");
|
|
1384
|
+
expect(getValue).toBe("new-value");
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
it("should call replace hooks with default parameters", async () => {
|
|
1388
|
+
const beforeHookMock = vi.fn();
|
|
1389
|
+
const afterHookMock = vi.fn();
|
|
1390
|
+
|
|
1391
|
+
client.onHook("before:replace", beforeHookMock);
|
|
1392
|
+
client.onHook("after:replace", afterHookMock);
|
|
1393
|
+
|
|
1394
|
+
await client.connect();
|
|
1395
|
+
|
|
1396
|
+
// Set a key first
|
|
1397
|
+
await client.set("replace-hook-default", "initial");
|
|
1398
|
+
|
|
1399
|
+
const result = await client.replace(
|
|
1400
|
+
"replace-hook-default",
|
|
1401
|
+
"replaced-value",
|
|
1402
|
+
);
|
|
1403
|
+
|
|
1404
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1405
|
+
key: "replace-hook-default",
|
|
1406
|
+
value: "replaced-value",
|
|
1407
|
+
exptime: 0,
|
|
1408
|
+
flags: 0,
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1412
|
+
key: "replace-hook-default",
|
|
1413
|
+
value: "replaced-value",
|
|
1414
|
+
exptime: 0,
|
|
1415
|
+
flags: 0,
|
|
1416
|
+
success: true,
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
expect(result).toBe(true);
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
it("should handle async replace hooks", async () => {
|
|
1423
|
+
const asyncBeforeHook = vi.fn().mockImplementation(async () => {
|
|
1424
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
const asyncAfterHook = vi.fn().mockImplementation(async () => {
|
|
1428
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
client.onHook("before:replace", asyncBeforeHook);
|
|
1432
|
+
client.onHook("after:replace", asyncAfterHook);
|
|
1433
|
+
|
|
1434
|
+
await client.connect();
|
|
1435
|
+
|
|
1436
|
+
// Set a key first
|
|
1437
|
+
await client.set("async-replace-test", "initial");
|
|
1438
|
+
|
|
1439
|
+
const start = Date.now();
|
|
1440
|
+
const result = await client.replace(
|
|
1441
|
+
"async-replace-test",
|
|
1442
|
+
"async-replaced",
|
|
1443
|
+
);
|
|
1444
|
+
const duration = Date.now() - start;
|
|
1445
|
+
|
|
1446
|
+
expect(asyncBeforeHook).toHaveBeenCalled();
|
|
1447
|
+
expect(asyncAfterHook).toHaveBeenCalled();
|
|
1448
|
+
expect(result).toBe(true);
|
|
1449
|
+
expect(duration).toBeGreaterThanOrEqual(18); // At least 18ms to account for timing imprecision
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
it("should call beforeHook and afterHook for append operation", async () => {
|
|
1453
|
+
const beforeHookMock = vi.fn();
|
|
1454
|
+
const afterHookMock = vi.fn();
|
|
1455
|
+
|
|
1456
|
+
client.onHook("before:append", beforeHookMock);
|
|
1457
|
+
client.onHook("after:append", afterHookMock);
|
|
1458
|
+
|
|
1459
|
+
await client.connect();
|
|
1460
|
+
|
|
1461
|
+
// First append should fail (key doesn't exist)
|
|
1462
|
+
const result1 = await client.append("append-hook-test", "-appended");
|
|
1463
|
+
|
|
1464
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1465
|
+
key: "append-hook-test",
|
|
1466
|
+
value: "-appended",
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1470
|
+
key: "append-hook-test",
|
|
1471
|
+
value: "-appended",
|
|
1472
|
+
success: false,
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
expect(result1).toBe(false);
|
|
1476
|
+
|
|
1477
|
+
// Set the key first
|
|
1478
|
+
await client.set("append-hook-test", "initial");
|
|
1479
|
+
|
|
1480
|
+
// Clear mocks
|
|
1481
|
+
beforeHookMock.mockClear();
|
|
1482
|
+
afterHookMock.mockClear();
|
|
1483
|
+
|
|
1484
|
+
// Now append should succeed
|
|
1485
|
+
const result2 = await client.append("append-hook-test", "-appended");
|
|
1486
|
+
|
|
1487
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1488
|
+
key: "append-hook-test",
|
|
1489
|
+
value: "-appended",
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1493
|
+
key: "append-hook-test",
|
|
1494
|
+
value: "-appended",
|
|
1495
|
+
success: true,
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
expect(result2).toBe(true);
|
|
1499
|
+
|
|
1500
|
+
// Verify the value was appended
|
|
1501
|
+
const getValue = await client.get("append-hook-test");
|
|
1502
|
+
expect(getValue).toBe("initial-appended");
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
it("should handle multiple append hooks", async () => {
|
|
1506
|
+
const beforeHook1 = vi.fn();
|
|
1507
|
+
const beforeHook2 = vi.fn();
|
|
1508
|
+
const afterHook1 = vi.fn();
|
|
1509
|
+
const afterHook2 = vi.fn();
|
|
1510
|
+
|
|
1511
|
+
client.onHook("before:append", beforeHook1);
|
|
1512
|
+
client.onHook("before:append", beforeHook2);
|
|
1513
|
+
client.onHook("after:append", afterHook1);
|
|
1514
|
+
client.onHook("after:append", afterHook2);
|
|
1515
|
+
|
|
1516
|
+
await client.connect();
|
|
1517
|
+
|
|
1518
|
+
// Set a key first
|
|
1519
|
+
await client.set("multi-append-hook", "start");
|
|
1520
|
+
await client.append("multi-append-hook", "-end");
|
|
1521
|
+
|
|
1522
|
+
expect(beforeHook1).toHaveBeenCalled();
|
|
1523
|
+
expect(beforeHook2).toHaveBeenCalled();
|
|
1524
|
+
expect(afterHook1).toHaveBeenCalled();
|
|
1525
|
+
expect(afterHook2).toHaveBeenCalled();
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
it("should handle async append hooks", async () => {
|
|
1529
|
+
const asyncBeforeHook = vi.fn().mockImplementation(async () => {
|
|
1530
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
const asyncAfterHook = vi.fn().mockImplementation(async () => {
|
|
1534
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
client.onHook("before:append", asyncBeforeHook);
|
|
1538
|
+
client.onHook("after:append", asyncAfterHook);
|
|
1539
|
+
|
|
1540
|
+
await client.connect();
|
|
1541
|
+
|
|
1542
|
+
// Set a key first
|
|
1543
|
+
await client.set("async-append-test", "initial");
|
|
1544
|
+
|
|
1545
|
+
const start = Date.now();
|
|
1546
|
+
const result = await client.append("async-append-test", "-async");
|
|
1547
|
+
const duration = Date.now() - start;
|
|
1548
|
+
|
|
1549
|
+
expect(asyncBeforeHook).toHaveBeenCalled();
|
|
1550
|
+
expect(asyncAfterHook).toHaveBeenCalled();
|
|
1551
|
+
expect(result).toBe(true);
|
|
1552
|
+
expect(duration).toBeGreaterThanOrEqual(18); // At least 18ms to account for timing imprecision
|
|
1553
|
+
|
|
1554
|
+
// Verify the append worked
|
|
1555
|
+
const getValue = await client.get("async-append-test");
|
|
1556
|
+
expect(getValue).toBe("initial-async");
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
it("should call beforeHook and afterHook for prepend operation", async () => {
|
|
1560
|
+
const beforeHookMock = vi.fn();
|
|
1561
|
+
const afterHookMock = vi.fn();
|
|
1562
|
+
|
|
1563
|
+
client.onHook("before:prepend", beforeHookMock);
|
|
1564
|
+
client.onHook("after:prepend", afterHookMock);
|
|
1565
|
+
|
|
1566
|
+
await client.connect();
|
|
1567
|
+
|
|
1568
|
+
// First prepend should fail (key doesn't exist)
|
|
1569
|
+
const result1 = await client.prepend("prepend-hook-test", "prefix-");
|
|
1570
|
+
|
|
1571
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1572
|
+
key: "prepend-hook-test",
|
|
1573
|
+
value: "prefix-",
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1577
|
+
key: "prepend-hook-test",
|
|
1578
|
+
value: "prefix-",
|
|
1579
|
+
success: false,
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
expect(result1).toBe(false);
|
|
1583
|
+
|
|
1584
|
+
// Set the key first
|
|
1585
|
+
await client.set("prepend-hook-test", "initial");
|
|
1586
|
+
|
|
1587
|
+
// Clear mocks
|
|
1588
|
+
beforeHookMock.mockClear();
|
|
1589
|
+
afterHookMock.mockClear();
|
|
1590
|
+
|
|
1591
|
+
// Now prepend should succeed
|
|
1592
|
+
const result2 = await client.prepend("prepend-hook-test", "prefix-");
|
|
1593
|
+
|
|
1594
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1595
|
+
key: "prepend-hook-test",
|
|
1596
|
+
value: "prefix-",
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1600
|
+
key: "prepend-hook-test",
|
|
1601
|
+
value: "prefix-",
|
|
1602
|
+
success: true,
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
expect(result2).toBe(true);
|
|
1606
|
+
|
|
1607
|
+
// Verify the value was prepended
|
|
1608
|
+
const getValue = await client.get("prepend-hook-test");
|
|
1609
|
+
expect(getValue).toBe("prefix-initial");
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
it("should handle async prepend hooks", async () => {
|
|
1613
|
+
const asyncBeforeHook = vi.fn().mockImplementation(async () => {
|
|
1614
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
const asyncAfterHook = vi.fn().mockImplementation(async () => {
|
|
1618
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
client.onHook("before:prepend", asyncBeforeHook);
|
|
1622
|
+
client.onHook("after:prepend", asyncAfterHook);
|
|
1623
|
+
|
|
1624
|
+
await client.connect();
|
|
1625
|
+
|
|
1626
|
+
// Set a key first
|
|
1627
|
+
await client.set("async-prepend-test", "end");
|
|
1628
|
+
|
|
1629
|
+
const start = Date.now();
|
|
1630
|
+
const result = await client.prepend("async-prepend-test", "start-");
|
|
1631
|
+
const duration = Date.now() - start;
|
|
1632
|
+
|
|
1633
|
+
expect(asyncBeforeHook).toHaveBeenCalled();
|
|
1634
|
+
expect(asyncAfterHook).toHaveBeenCalled();
|
|
1635
|
+
expect(result).toBe(true);
|
|
1636
|
+
expect(duration).toBeGreaterThanOrEqual(18); // At least 18ms to account for timing imprecision
|
|
1637
|
+
|
|
1638
|
+
// Verify the prepend worked
|
|
1639
|
+
const getValue = await client.get("async-prepend-test");
|
|
1640
|
+
expect(getValue).toBe("start-end");
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
it("should support removing prepend hooks", async () => {
|
|
1644
|
+
const hookMock = vi.fn();
|
|
1645
|
+
|
|
1646
|
+
client.onHook("before:prepend", hookMock);
|
|
1647
|
+
|
|
1648
|
+
await client.connect();
|
|
1649
|
+
await client.set("remove-prepend-hook", "value");
|
|
1650
|
+
|
|
1651
|
+
// First call should trigger the hook
|
|
1652
|
+
await client.prepend("remove-prepend-hook", "pre-");
|
|
1653
|
+
expect(hookMock).toHaveBeenCalledTimes(1);
|
|
1654
|
+
|
|
1655
|
+
// Remove the hook
|
|
1656
|
+
client.removeHook("before:prepend", hookMock);
|
|
1657
|
+
|
|
1658
|
+
// Second call should not trigger the hook
|
|
1659
|
+
await client.prepend("remove-prepend-hook", "another-");
|
|
1660
|
+
expect(hookMock).toHaveBeenCalledTimes(1);
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
it("should call beforeHook and afterHook for delete operation", async () => {
|
|
1664
|
+
const beforeHookMock = vi.fn();
|
|
1665
|
+
const afterHookMock = vi.fn();
|
|
1666
|
+
|
|
1667
|
+
client.onHook("before:delete", beforeHookMock);
|
|
1668
|
+
client.onHook("after:delete", afterHookMock);
|
|
1669
|
+
|
|
1670
|
+
await client.connect();
|
|
1671
|
+
|
|
1672
|
+
// First delete should fail (key doesn't exist)
|
|
1673
|
+
const result1 = await client.delete("delete-hook-test");
|
|
1674
|
+
|
|
1675
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1676
|
+
key: "delete-hook-test",
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1680
|
+
key: "delete-hook-test",
|
|
1681
|
+
success: false,
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
expect(result1).toBe(false);
|
|
1685
|
+
|
|
1686
|
+
// Set the key first
|
|
1687
|
+
await client.set("delete-hook-test", "value-to-delete");
|
|
1688
|
+
|
|
1689
|
+
// Clear mocks
|
|
1690
|
+
beforeHookMock.mockClear();
|
|
1691
|
+
afterHookMock.mockClear();
|
|
1692
|
+
|
|
1693
|
+
// Now delete should succeed
|
|
1694
|
+
const result2 = await client.delete("delete-hook-test");
|
|
1695
|
+
|
|
1696
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1697
|
+
key: "delete-hook-test",
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1701
|
+
key: "delete-hook-test",
|
|
1702
|
+
success: true,
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
expect(result2).toBe(true);
|
|
1706
|
+
|
|
1707
|
+
// Verify the key was deleted
|
|
1708
|
+
const getValue = await client.get("delete-hook-test");
|
|
1709
|
+
expect(getValue).toBe(undefined);
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
it("should handle async delete hooks", async () => {
|
|
1713
|
+
const asyncBeforeHook = vi.fn().mockImplementation(async () => {
|
|
1714
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
const asyncAfterHook = vi.fn().mockImplementation(async () => {
|
|
1718
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
client.onHook("before:delete", asyncBeforeHook);
|
|
1722
|
+
client.onHook("after:delete", asyncAfterHook);
|
|
1723
|
+
|
|
1724
|
+
await client.connect();
|
|
1725
|
+
|
|
1726
|
+
// Set a key first
|
|
1727
|
+
await client.set("async-delete-test", "to-be-deleted");
|
|
1728
|
+
|
|
1729
|
+
const start = Date.now();
|
|
1730
|
+
const result = await client.delete("async-delete-test");
|
|
1731
|
+
const duration = Date.now() - start;
|
|
1732
|
+
|
|
1733
|
+
expect(asyncBeforeHook).toHaveBeenCalled();
|
|
1734
|
+
expect(asyncAfterHook).toHaveBeenCalled();
|
|
1735
|
+
expect(result).toBe(true);
|
|
1736
|
+
expect(duration).toBeGreaterThanOrEqual(18); // At least 18ms to account for timing imprecision
|
|
1737
|
+
|
|
1738
|
+
// Verify the delete worked
|
|
1739
|
+
const getValue = await client.get("async-delete-test");
|
|
1740
|
+
expect(getValue).toBe(undefined);
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
it("should handle delete hook errors with throwHookErrors", async () => {
|
|
1744
|
+
const errorClient = new Memcache();
|
|
1745
|
+
errorClient.throwHookErrors = true;
|
|
1746
|
+
|
|
1747
|
+
const errorHook = vi.fn().mockImplementation(() => {
|
|
1748
|
+
throw new Error("Delete hook error");
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
errorClient.onHook("before:delete", errorHook);
|
|
1752
|
+
|
|
1753
|
+
await errorClient.connect();
|
|
1754
|
+
await errorClient.set("error-delete-test", "value");
|
|
1755
|
+
|
|
1756
|
+
// Hook error should propagate and reject the promise
|
|
1757
|
+
await expect(errorClient.delete("error-delete-test")).rejects.toThrow(
|
|
1758
|
+
"Delete hook error",
|
|
1759
|
+
);
|
|
1760
|
+
expect(errorHook).toHaveBeenCalled();
|
|
1761
|
+
|
|
1762
|
+
await errorClient.disconnect();
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
it("should call beforeHook and afterHook for incr operation", async () => {
|
|
1766
|
+
const beforeHookMock = vi.fn();
|
|
1767
|
+
const afterHookMock = vi.fn();
|
|
1768
|
+
|
|
1769
|
+
client.onHook("before:incr", beforeHookMock);
|
|
1770
|
+
client.onHook("after:incr", afterHookMock);
|
|
1771
|
+
|
|
1772
|
+
await client.connect();
|
|
1773
|
+
|
|
1774
|
+
// Set an initial numeric value
|
|
1775
|
+
await client.set("incr-hook-test", "10");
|
|
1776
|
+
|
|
1777
|
+
// Increment by 5
|
|
1778
|
+
const result = await client.incr("incr-hook-test", 5);
|
|
1779
|
+
|
|
1780
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1781
|
+
key: "incr-hook-test",
|
|
1782
|
+
value: 5,
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1786
|
+
key: "incr-hook-test",
|
|
1787
|
+
value: 5,
|
|
1788
|
+
newValue: 15,
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
expect(result).toBe(15);
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
it("should call incr hooks with default increment value", async () => {
|
|
1795
|
+
const beforeHookMock = vi.fn();
|
|
1796
|
+
const afterHookMock = vi.fn();
|
|
1797
|
+
|
|
1798
|
+
client.onHook("before:incr", beforeHookMock);
|
|
1799
|
+
client.onHook("after:incr", afterHookMock);
|
|
1800
|
+
|
|
1801
|
+
await client.connect();
|
|
1802
|
+
|
|
1803
|
+
// Set an initial numeric value
|
|
1804
|
+
await client.set("incr-hook-default", "20");
|
|
1805
|
+
|
|
1806
|
+
// Increment with default value (1)
|
|
1807
|
+
const result = await client.incr("incr-hook-default");
|
|
1808
|
+
|
|
1809
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1810
|
+
key: "incr-hook-default",
|
|
1811
|
+
value: 1,
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1815
|
+
key: "incr-hook-default",
|
|
1816
|
+
value: 1,
|
|
1817
|
+
newValue: 21,
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
expect(result).toBe(21);
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
it("should handle incr hooks when key doesn't exist", async () => {
|
|
1824
|
+
const beforeHookMock = vi.fn();
|
|
1825
|
+
const afterHookMock = vi.fn();
|
|
1826
|
+
|
|
1827
|
+
client.onHook("before:incr", beforeHookMock);
|
|
1828
|
+
client.onHook("after:incr", afterHookMock);
|
|
1829
|
+
|
|
1830
|
+
await client.connect();
|
|
1831
|
+
|
|
1832
|
+
// Try to increment a non-existent key
|
|
1833
|
+
const result = await client.incr("incr-hook-nonexistent", 3);
|
|
1834
|
+
|
|
1835
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1836
|
+
key: "incr-hook-nonexistent",
|
|
1837
|
+
value: 3,
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1841
|
+
key: "incr-hook-nonexistent",
|
|
1842
|
+
value: 3,
|
|
1843
|
+
newValue: undefined,
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
expect(result).toBe(undefined);
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
it("should handle async incr hooks", async () => {
|
|
1850
|
+
const asyncBeforeHook = vi.fn().mockImplementation(async () => {
|
|
1851
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
const asyncAfterHook = vi.fn().mockImplementation(async () => {
|
|
1855
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
client.onHook("before:incr", asyncBeforeHook);
|
|
1859
|
+
client.onHook("after:incr", asyncAfterHook);
|
|
1860
|
+
|
|
1861
|
+
await client.connect();
|
|
1862
|
+
|
|
1863
|
+
// Set an initial numeric value
|
|
1864
|
+
await client.set("async-incr-test", "100");
|
|
1865
|
+
|
|
1866
|
+
const start = Date.now();
|
|
1867
|
+
const result = await client.incr("async-incr-test", 10);
|
|
1868
|
+
const duration = Date.now() - start;
|
|
1869
|
+
|
|
1870
|
+
expect(asyncBeforeHook).toHaveBeenCalled();
|
|
1871
|
+
expect(asyncAfterHook).toHaveBeenCalled();
|
|
1872
|
+
expect(result).toBe(110);
|
|
1873
|
+
expect(duration).toBeGreaterThanOrEqual(18); // At least 18ms to account for timing imprecision
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
it("should call beforeHook and afterHook for decr operation", async () => {
|
|
1877
|
+
const beforeHookMock = vi.fn();
|
|
1878
|
+
const afterHookMock = vi.fn();
|
|
1879
|
+
|
|
1880
|
+
client.onHook("before:decr", beforeHookMock);
|
|
1881
|
+
client.onHook("after:decr", afterHookMock);
|
|
1882
|
+
|
|
1883
|
+
await client.connect();
|
|
1884
|
+
|
|
1885
|
+
// Set an initial numeric value
|
|
1886
|
+
await client.set("decr-hook-test", "50");
|
|
1887
|
+
|
|
1888
|
+
// Decrement by 7
|
|
1889
|
+
const result = await client.decr("decr-hook-test", 7);
|
|
1890
|
+
|
|
1891
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1892
|
+
key: "decr-hook-test",
|
|
1893
|
+
value: 7,
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1897
|
+
key: "decr-hook-test",
|
|
1898
|
+
value: 7,
|
|
1899
|
+
newValue: 43,
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
expect(result).toBe(43);
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
it("should call decr hooks with default decrement value", async () => {
|
|
1906
|
+
const beforeHookMock = vi.fn();
|
|
1907
|
+
const afterHookMock = vi.fn();
|
|
1908
|
+
|
|
1909
|
+
client.onHook("before:decr", beforeHookMock);
|
|
1910
|
+
client.onHook("after:decr", afterHookMock);
|
|
1911
|
+
|
|
1912
|
+
await client.connect();
|
|
1913
|
+
|
|
1914
|
+
// Set an initial numeric value
|
|
1915
|
+
await client.set("decr-hook-default", "30");
|
|
1916
|
+
|
|
1917
|
+
// Decrement with default value (1)
|
|
1918
|
+
const result = await client.decr("decr-hook-default");
|
|
1919
|
+
|
|
1920
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1921
|
+
key: "decr-hook-default",
|
|
1922
|
+
value: 1,
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1926
|
+
key: "decr-hook-default",
|
|
1927
|
+
value: 1,
|
|
1928
|
+
newValue: 29,
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
expect(result).toBe(29);
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
it("should handle decr hooks when key doesn't exist", async () => {
|
|
1935
|
+
const beforeHookMock = vi.fn();
|
|
1936
|
+
const afterHookMock = vi.fn();
|
|
1937
|
+
|
|
1938
|
+
client.onHook("before:decr", beforeHookMock);
|
|
1939
|
+
client.onHook("after:decr", afterHookMock);
|
|
1940
|
+
|
|
1941
|
+
await client.connect();
|
|
1942
|
+
|
|
1943
|
+
// Try to decrement a non-existent key
|
|
1944
|
+
const result = await client.decr("decr-hook-nonexistent", 5);
|
|
1945
|
+
|
|
1946
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
1947
|
+
key: "decr-hook-nonexistent",
|
|
1948
|
+
value: 5,
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
1952
|
+
key: "decr-hook-nonexistent",
|
|
1953
|
+
value: 5,
|
|
1954
|
+
newValue: undefined,
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
expect(result).toBe(undefined);
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
it("should handle async decr hooks", async () => {
|
|
1961
|
+
const asyncBeforeHook = vi.fn().mockImplementation(async () => {
|
|
1962
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
const asyncAfterHook = vi.fn().mockImplementation(async () => {
|
|
1966
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
client.onHook("before:decr", asyncBeforeHook);
|
|
1970
|
+
client.onHook("after:decr", asyncAfterHook);
|
|
1971
|
+
|
|
1972
|
+
await client.connect();
|
|
1973
|
+
|
|
1974
|
+
// Set an initial numeric value
|
|
1975
|
+
await client.set("async-decr-test", "200");
|
|
1976
|
+
|
|
1977
|
+
const start = Date.now();
|
|
1978
|
+
const result = await client.decr("async-decr-test", 25);
|
|
1979
|
+
const duration = Date.now() - start;
|
|
1980
|
+
|
|
1981
|
+
expect(asyncBeforeHook).toHaveBeenCalled();
|
|
1982
|
+
expect(asyncAfterHook).toHaveBeenCalled();
|
|
1983
|
+
expect(result).toBe(175);
|
|
1984
|
+
expect(duration).toBeGreaterThanOrEqual(15); // At least 15ms for both hooks (with tolerance for timing variations)
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
it("should handle decr not going below zero", async () => {
|
|
1988
|
+
const beforeHookMock = vi.fn();
|
|
1989
|
+
const afterHookMock = vi.fn();
|
|
1990
|
+
|
|
1991
|
+
client.onHook("before:decr", beforeHookMock);
|
|
1992
|
+
client.onHook("after:decr", afterHookMock);
|
|
1993
|
+
|
|
1994
|
+
await client.connect();
|
|
1995
|
+
|
|
1996
|
+
// Set a small value
|
|
1997
|
+
await client.set("decr-zero-test", "5");
|
|
1998
|
+
|
|
1999
|
+
// Try to decrement by more than the value (memcached won't go below 0)
|
|
2000
|
+
const result = await client.decr("decr-zero-test", 10);
|
|
2001
|
+
|
|
2002
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
2003
|
+
key: "decr-zero-test",
|
|
2004
|
+
value: 10,
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
2008
|
+
key: "decr-zero-test",
|
|
2009
|
+
value: 10,
|
|
2010
|
+
newValue: 0, // Memcached stops at 0
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
expect(result).toBe(0);
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
it("should call beforeHook and afterHook for touch operation", async () => {
|
|
2017
|
+
const beforeHookMock = vi.fn();
|
|
2018
|
+
const afterHookMock = vi.fn();
|
|
2019
|
+
|
|
2020
|
+
client.onHook("before:touch", beforeHookMock);
|
|
2021
|
+
client.onHook("after:touch", afterHookMock);
|
|
2022
|
+
|
|
2023
|
+
await client.connect();
|
|
2024
|
+
|
|
2025
|
+
// Set a key first
|
|
2026
|
+
await client.set("touch-hook-test", "value");
|
|
2027
|
+
|
|
2028
|
+
// Touch with new exptime
|
|
2029
|
+
const result = await client.touch("touch-hook-test", 7200);
|
|
2030
|
+
|
|
2031
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
2032
|
+
key: "touch-hook-test",
|
|
2033
|
+
exptime: 7200,
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
2037
|
+
key: "touch-hook-test",
|
|
2038
|
+
exptime: 7200,
|
|
2039
|
+
success: true,
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
expect(result).toBe(true);
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
it("should handle touch hooks when key doesn't exist", async () => {
|
|
2046
|
+
const beforeHookMock = vi.fn();
|
|
2047
|
+
const afterHookMock = vi.fn();
|
|
2048
|
+
|
|
2049
|
+
client.onHook("before:touch", beforeHookMock);
|
|
2050
|
+
client.onHook("after:touch", afterHookMock);
|
|
2051
|
+
|
|
2052
|
+
await client.connect();
|
|
2053
|
+
|
|
2054
|
+
// Try to touch a non-existent key
|
|
2055
|
+
const result = await client.touch("touch-hook-nonexistent", 3600);
|
|
2056
|
+
|
|
2057
|
+
expect(beforeHookMock).toHaveBeenCalledWith({
|
|
2058
|
+
key: "touch-hook-nonexistent",
|
|
2059
|
+
exptime: 3600,
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
expect(afterHookMock).toHaveBeenCalledWith({
|
|
2063
|
+
key: "touch-hook-nonexistent",
|
|
2064
|
+
exptime: 3600,
|
|
2065
|
+
success: false,
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
expect(result).toBe(false);
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
it("should handle async touch hooks", async () => {
|
|
2072
|
+
const asyncBeforeHook = vi.fn().mockImplementation(async () => {
|
|
2073
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
const asyncAfterHook = vi.fn().mockImplementation(async () => {
|
|
2077
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
client.onHook("before:touch", asyncBeforeHook);
|
|
2081
|
+
client.onHook("after:touch", asyncAfterHook);
|
|
2082
|
+
|
|
2083
|
+
await client.connect();
|
|
2084
|
+
|
|
2085
|
+
// Set a key first
|
|
2086
|
+
await client.set("async-touch-test", "value");
|
|
2087
|
+
|
|
2088
|
+
const start = Date.now();
|
|
2089
|
+
const result = await client.touch("async-touch-test", 1800);
|
|
2090
|
+
const duration = Date.now() - start;
|
|
2091
|
+
|
|
2092
|
+
expect(asyncBeforeHook).toHaveBeenCalled();
|
|
2093
|
+
expect(asyncAfterHook).toHaveBeenCalled();
|
|
2094
|
+
expect(result).toBe(true);
|
|
2095
|
+
expect(duration).toBeGreaterThanOrEqual(18); // At least 18ms to account for timing imprecision
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
it("should support multiple touch hook listeners", async () => {
|
|
2099
|
+
const beforeHook1 = vi.fn();
|
|
2100
|
+
const beforeHook2 = vi.fn();
|
|
2101
|
+
const afterHook1 = vi.fn();
|
|
2102
|
+
const afterHook2 = vi.fn();
|
|
2103
|
+
|
|
2104
|
+
client.onHook("before:touch", beforeHook1);
|
|
2105
|
+
client.onHook("before:touch", beforeHook2);
|
|
2106
|
+
client.onHook("after:touch", afterHook1);
|
|
2107
|
+
client.onHook("after:touch", afterHook2);
|
|
2108
|
+
|
|
2109
|
+
await client.connect();
|
|
2110
|
+
|
|
2111
|
+
// Set a key first
|
|
2112
|
+
await client.set("multi-touch-hook", "value");
|
|
2113
|
+
await client.touch("multi-touch-hook", 900);
|
|
2114
|
+
|
|
2115
|
+
expect(beforeHook1).toHaveBeenCalled();
|
|
2116
|
+
expect(beforeHook2).toHaveBeenCalled();
|
|
2117
|
+
expect(afterHook1).toHaveBeenCalled();
|
|
2118
|
+
expect(afterHook2).toHaveBeenCalled();
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
// Removed: CAS hook tests with socket mocking
|
|
2122
|
+
// CAS functionality requires gets command with CAS tokens
|
|
2123
|
+
// These can be re-added once CAS is properly implemented
|
|
2124
|
+
|
|
2125
|
+
it("should handle cas hook errors with throwHookErrors", async () => {
|
|
2126
|
+
const errorClient = new Memcache();
|
|
2127
|
+
errorClient.throwHookErrors = true;
|
|
2128
|
+
|
|
2129
|
+
const errorHook = vi.fn().mockImplementation(() => {
|
|
2130
|
+
throw new Error("CAS hook error");
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
errorClient.onHook("before:cas", errorHook);
|
|
2134
|
+
|
|
2135
|
+
await errorClient.connect();
|
|
2136
|
+
await errorClient.set("error-cas-test", "value");
|
|
2137
|
+
|
|
2138
|
+
// Hook error should propagate and reject the promise
|
|
2139
|
+
await expect(
|
|
2140
|
+
errorClient.cas("error-cas-test", "new-value", "12345"),
|
|
2141
|
+
).rejects.toThrow("CAS hook error");
|
|
2142
|
+
expect(errorHook).toHaveBeenCalled();
|
|
2143
|
+
|
|
2144
|
+
await errorClient.disconnect();
|
|
2145
|
+
});
|
|
2146
|
+
|
|
2147
|
+
it("should support multiple cas hook listeners", async () => {
|
|
2148
|
+
const beforeHook1 = vi.fn();
|
|
2149
|
+
const beforeHook2 = vi.fn();
|
|
2150
|
+
const afterHook1 = vi.fn();
|
|
2151
|
+
const afterHook2 = vi.fn();
|
|
2152
|
+
|
|
2153
|
+
client.onHook("before:cas", beforeHook1);
|
|
2154
|
+
client.onHook("before:cas", beforeHook2);
|
|
2155
|
+
client.onHook("after:cas", afterHook1);
|
|
2156
|
+
client.onHook("after:cas", afterHook2);
|
|
2157
|
+
|
|
2158
|
+
await client.connect();
|
|
2159
|
+
|
|
2160
|
+
// Set a key first
|
|
2161
|
+
await client.set("multi-cas-hook", "start");
|
|
2162
|
+
await client.cas("multi-cas-hook", "end", "77777");
|
|
2163
|
+
|
|
2164
|
+
expect(beforeHook1).toHaveBeenCalled();
|
|
2165
|
+
expect(beforeHook2).toHaveBeenCalled();
|
|
2166
|
+
expect(afterHook1).toHaveBeenCalled();
|
|
2167
|
+
expect(afterHook2).toHaveBeenCalled();
|
|
2168
|
+
});
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
describe("Consistent Hashing Integration", () => {
|
|
2172
|
+
it("should return default node initially", () => {
|
|
2173
|
+
expect(client.nodeIds).toEqual(["localhost:11211"]);
|
|
2174
|
+
});
|
|
2175
|
+
|
|
2176
|
+
it("should return nodes as string array", async () => {
|
|
2177
|
+
await client.addNode("localhost:11212");
|
|
2178
|
+
await client.addNode("127.0.0.1:11213");
|
|
2179
|
+
|
|
2180
|
+
const nodes = client.nodeIds;
|
|
2181
|
+
expect(nodes).toHaveLength(3);
|
|
2182
|
+
expect(nodes).toContain("localhost:11211");
|
|
2183
|
+
expect(nodes).toContain("localhost:11212");
|
|
2184
|
+
expect(nodes).toContain("127.0.0.1:11213");
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
it("should allow adding nodes", async () => {
|
|
2188
|
+
// Client starts with default localhost:11211 node
|
|
2189
|
+
await client.addNode("server1");
|
|
2190
|
+
await client.addNode("server2");
|
|
2191
|
+
await client.addNode("server3");
|
|
2192
|
+
|
|
2193
|
+
const nodes = client.nodeIds;
|
|
2194
|
+
expect(nodes).toHaveLength(4); // 3 + default
|
|
2195
|
+
expect(nodes).toContain("server1:11211");
|
|
2196
|
+
expect(nodes).toContain("server2:11211");
|
|
2197
|
+
expect(nodes).toContain("server3:11211");
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
it("should throw error when adding duplicate node", async () => {
|
|
2201
|
+
// Try to add the default node again
|
|
2202
|
+
await expect(client.addNode("localhost:11211")).rejects.toThrow(
|
|
2203
|
+
"Node localhost:11211 already exists",
|
|
2204
|
+
);
|
|
2205
|
+
|
|
2206
|
+
// Add a new node
|
|
2207
|
+
await client.addNode("server1:11212");
|
|
2208
|
+
|
|
2209
|
+
// Try to add the same node again
|
|
2210
|
+
await expect(client.addNode("server1:11212")).rejects.toThrow(
|
|
2211
|
+
"Node server1:11212 already exists",
|
|
2212
|
+
);
|
|
2213
|
+
});
|
|
2214
|
+
|
|
2215
|
+
it("should allow getting nodes for a key", async () => {
|
|
2216
|
+
await client.addNode("localhost:11212");
|
|
2217
|
+
await client.addNode("localhost:11213");
|
|
2218
|
+
await client.addNode("localhost:11214");
|
|
2219
|
+
|
|
2220
|
+
const nodes = client.hash.getNodesByKey("test-key");
|
|
2221
|
+
expect(nodes).toBeDefined();
|
|
2222
|
+
expect(nodes.length).toBeGreaterThan(0);
|
|
2223
|
+
expect(nodes[0].id).toBeDefined();
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
it("should return consistent nodes for same key", async () => {
|
|
2227
|
+
await client.addNode("localhost:11212");
|
|
2228
|
+
await client.addNode("localhost:11213");
|
|
2229
|
+
await client.addNode("localhost:11214");
|
|
2230
|
+
|
|
2231
|
+
const nodes1 = client.hash.getNodesByKey("test-key");
|
|
2232
|
+
const nodes2 = client.hash.getNodesByKey("test-key");
|
|
2233
|
+
expect(nodes1[0]).toBe(nodes2[0]);
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
it("should allow removing nodes", async () => {
|
|
2237
|
+
// Client starts with default localhost:11211 node
|
|
2238
|
+
await client.addNode("server1");
|
|
2239
|
+
await client.addNode("server2");
|
|
2240
|
+
await client.addNode("server3");
|
|
2241
|
+
|
|
2242
|
+
expect(client.nodes).toHaveLength(4); // 3 + default
|
|
2243
|
+
|
|
2244
|
+
await client.removeNode("server2");
|
|
2245
|
+
expect(client.nodes).toHaveLength(3);
|
|
2246
|
+
expect(client.nodes).not.toContain("server2");
|
|
2247
|
+
});
|
|
2248
|
+
|
|
2249
|
+
it("should allow adding weighted nodes", async () => {
|
|
2250
|
+
// Client starts with default localhost:11211 node
|
|
2251
|
+
await client.addNode("localhost:11212", 3);
|
|
2252
|
+
await client.addNode("localhost:11213", 1);
|
|
2253
|
+
|
|
2254
|
+
expect(client.nodes).toHaveLength(3); // 2 + default
|
|
2255
|
+
|
|
2256
|
+
// Heavy server should get more keys
|
|
2257
|
+
const distribution = new Map<string, number>();
|
|
2258
|
+
for (let i = 0; i < 100; i++) {
|
|
2259
|
+
const nodes = client.hash.getNodesByKey(`key-${i}`);
|
|
2260
|
+
if (nodes.length > 0) {
|
|
2261
|
+
distribution.set(
|
|
2262
|
+
nodes[0].id,
|
|
2263
|
+
(distribution.get(nodes[0].id) || 0) + 1,
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
const heavy = distribution.get("localhost:11212") || 0;
|
|
2269
|
+
const light = distribution.get("localhost:11213") || 0;
|
|
2270
|
+
expect(heavy).toBeGreaterThan(light);
|
|
2271
|
+
});
|
|
2272
|
+
|
|
2273
|
+
it("should handle single default node", () => {
|
|
2274
|
+
// Client starts with default localhost:11211 node
|
|
2275
|
+
const nodes = client.hash.getNodesByKey("test-key");
|
|
2276
|
+
expect(nodes).toBeDefined();
|
|
2277
|
+
expect(nodes.length).toBeGreaterThan(0);
|
|
2278
|
+
expect(nodes[0].id).toBe("localhost:11211");
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
it("should get node by ID using getNode", async () => {
|
|
2282
|
+
await client.addNode("server1");
|
|
2283
|
+
await client.addNode("server2");
|
|
2284
|
+
|
|
2285
|
+
const node = client.getNode("server1:11211");
|
|
2286
|
+
expect(node).toBeDefined();
|
|
2287
|
+
expect(node?.id).toBe("server1:11211");
|
|
2288
|
+
|
|
2289
|
+
const node2 = client.getNode("server2:11211");
|
|
2290
|
+
expect(node2).toBeDefined();
|
|
2291
|
+
expect(node2?.id).toBe("server2:11211");
|
|
2292
|
+
|
|
2293
|
+
const nonExistent = client.getNode("nonexistent");
|
|
2294
|
+
expect(nonExistent).toBeUndefined();
|
|
2295
|
+
});
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
describe("Multi-node Replication", () => {
|
|
2299
|
+
it("should execute set on all replica nodes", async () => {
|
|
2300
|
+
// Create a client with a mock hash provider that returns multiple nodes
|
|
2301
|
+
const multiNodeClient = new Memcache();
|
|
2302
|
+
await multiNodeClient.connect();
|
|
2303
|
+
|
|
2304
|
+
// Mock getNodesByKey to return multiple nodes (simulating replication)
|
|
2305
|
+
const originalGetNodesByKey = multiNodeClient.hash.getNodesByKey.bind(
|
|
2306
|
+
multiNodeClient.hash,
|
|
2307
|
+
);
|
|
2308
|
+
vi.spyOn(multiNodeClient.hash, "getNodesByKey").mockImplementation(
|
|
2309
|
+
(key: string) => {
|
|
2310
|
+
const nodes = originalGetNodesByKey(key);
|
|
2311
|
+
// Return the same node twice to simulate replication
|
|
2312
|
+
return nodes.length > 0 ? [nodes[0], nodes[0]] : nodes;
|
|
2313
|
+
},
|
|
2314
|
+
);
|
|
2315
|
+
|
|
2316
|
+
const success = await multiNodeClient.set("test-key", "test-value");
|
|
2317
|
+
expect(success).toBe(true);
|
|
2318
|
+
|
|
2319
|
+
const value = await multiNodeClient.get("test-key");
|
|
2320
|
+
expect(value).toBe("test-value");
|
|
2321
|
+
|
|
2322
|
+
await multiNodeClient.disconnect();
|
|
2323
|
+
});
|
|
2324
|
+
|
|
2325
|
+
it("should execute delete on all replica nodes", async () => {
|
|
2326
|
+
const multiNodeClient = new Memcache();
|
|
2327
|
+
await multiNodeClient.connect();
|
|
2328
|
+
|
|
2329
|
+
await multiNodeClient.set("delete-test", "value");
|
|
2330
|
+
|
|
2331
|
+
// Mock getNodesByKey to return multiple nodes
|
|
2332
|
+
const originalGetNodesByKey = multiNodeClient.hash.getNodesByKey.bind(
|
|
2333
|
+
multiNodeClient.hash,
|
|
2334
|
+
);
|
|
2335
|
+
vi.spyOn(multiNodeClient.hash, "getNodesByKey").mockImplementation(
|
|
2336
|
+
(key: string) => {
|
|
2337
|
+
const nodes = originalGetNodesByKey(key);
|
|
2338
|
+
return nodes.length > 0 ? [nodes[0], nodes[0]] : nodes;
|
|
2339
|
+
},
|
|
2340
|
+
);
|
|
2341
|
+
|
|
2342
|
+
const deleted = await multiNodeClient.delete("delete-test");
|
|
2343
|
+
// When the same node is returned twice, the first delete succeeds
|
|
2344
|
+
// and the second fails (key already deleted), so overall result is false
|
|
2345
|
+
expect(typeof deleted).toBe("boolean");
|
|
2346
|
+
|
|
2347
|
+
await multiNodeClient.disconnect();
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
it("should execute incr on all replica nodes", async () => {
|
|
2351
|
+
const multiNodeClient = new Memcache();
|
|
2352
|
+
await multiNodeClient.connect();
|
|
2353
|
+
|
|
2354
|
+
await multiNodeClient.set("counter", "10");
|
|
2355
|
+
|
|
2356
|
+
// Mock getNodesByKey to return multiple nodes
|
|
2357
|
+
const originalGetNodesByKey = multiNodeClient.hash.getNodesByKey.bind(
|
|
2358
|
+
multiNodeClient.hash,
|
|
2359
|
+
);
|
|
2360
|
+
vi.spyOn(multiNodeClient.hash, "getNodesByKey").mockImplementation(
|
|
2361
|
+
(key: string) => {
|
|
2362
|
+
const nodes = originalGetNodesByKey(key);
|
|
2363
|
+
return nodes.length > 0 ? [nodes[0], nodes[0]] : nodes;
|
|
2364
|
+
},
|
|
2365
|
+
);
|
|
2366
|
+
|
|
2367
|
+
const newValue = await multiNodeClient.incr("counter", 5);
|
|
2368
|
+
expect(newValue).toBe(15);
|
|
2369
|
+
|
|
2370
|
+
await multiNodeClient.disconnect();
|
|
2371
|
+
});
|
|
2372
|
+
|
|
2373
|
+
it("should execute decr on all replica nodes", async () => {
|
|
2374
|
+
const multiNodeClient = new Memcache();
|
|
2375
|
+
await multiNodeClient.connect();
|
|
2376
|
+
|
|
2377
|
+
await multiNodeClient.set("counter", "20");
|
|
2378
|
+
|
|
2379
|
+
// Mock getNodesByKey to return multiple nodes
|
|
2380
|
+
const originalGetNodesByKey = multiNodeClient.hash.getNodesByKey.bind(
|
|
2381
|
+
multiNodeClient.hash,
|
|
2382
|
+
);
|
|
2383
|
+
vi.spyOn(multiNodeClient.hash, "getNodesByKey").mockImplementation(
|
|
2384
|
+
(key: string) => {
|
|
2385
|
+
const nodes = originalGetNodesByKey(key);
|
|
2386
|
+
return nodes.length > 0 ? [nodes[0], nodes[0]] : nodes;
|
|
2387
|
+
},
|
|
2388
|
+
);
|
|
2389
|
+
|
|
2390
|
+
const newValue = await multiNodeClient.decr("counter", 3);
|
|
2391
|
+
expect(newValue).toBe(17);
|
|
2392
|
+
|
|
2393
|
+
await multiNodeClient.disconnect();
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
it("should execute cas on all replica nodes", async () => {
|
|
2397
|
+
const multiNodeClient = new Memcache();
|
|
2398
|
+
await multiNodeClient.connect();
|
|
2399
|
+
|
|
2400
|
+
await multiNodeClient.set("cas-test", "initial");
|
|
2401
|
+
|
|
2402
|
+
// Mock getNodesByKey to return multiple nodes
|
|
2403
|
+
const originalGetNodesByKey = multiNodeClient.hash.getNodesByKey.bind(
|
|
2404
|
+
multiNodeClient.hash,
|
|
2405
|
+
);
|
|
2406
|
+
vi.spyOn(multiNodeClient.hash, "getNodesByKey").mockImplementation(
|
|
2407
|
+
(key: string) => {
|
|
2408
|
+
const nodes = originalGetNodesByKey(key);
|
|
2409
|
+
return nodes.length > 0 ? [nodes[0], nodes[0]] : nodes;
|
|
2410
|
+
},
|
|
2411
|
+
);
|
|
2412
|
+
|
|
2413
|
+
// CAS with a dummy token (real token retrieval would require gets command)
|
|
2414
|
+
const success = await multiNodeClient.cas(
|
|
2415
|
+
"cas-test",
|
|
2416
|
+
"updated",
|
|
2417
|
+
"123456",
|
|
2418
|
+
);
|
|
2419
|
+
// Note: This might fail since we don't have a real CAS token,
|
|
2420
|
+
// but it tests that the command executes on all nodes
|
|
2421
|
+
expect(typeof success).toBe("boolean");
|
|
2422
|
+
|
|
2423
|
+
await multiNodeClient.disconnect();
|
|
2424
|
+
});
|
|
2425
|
+
|
|
2426
|
+
it("should execute add on all replica nodes", async () => {
|
|
2427
|
+
const multiNodeClient = new Memcache();
|
|
2428
|
+
await multiNodeClient.connect();
|
|
2429
|
+
|
|
2430
|
+
// Mock getNodesByKey to return multiple nodes
|
|
2431
|
+
const originalGetNodesByKey = multiNodeClient.hash.getNodesByKey.bind(
|
|
2432
|
+
multiNodeClient.hash,
|
|
2433
|
+
);
|
|
2434
|
+
vi.spyOn(multiNodeClient.hash, "getNodesByKey").mockImplementation(
|
|
2435
|
+
(key: string) => {
|
|
2436
|
+
const nodes = originalGetNodesByKey(key);
|
|
2437
|
+
return nodes.length > 0 ? [nodes[0], nodes[0]] : nodes;
|
|
2438
|
+
},
|
|
2439
|
+
);
|
|
2440
|
+
|
|
2441
|
+
const success = await multiNodeClient.add("add-test", "value");
|
|
2442
|
+
// When the same node is returned twice, the first add succeeds
|
|
2443
|
+
// and the second fails (key already exists), so overall result is false
|
|
2444
|
+
expect(typeof success).toBe("boolean");
|
|
2445
|
+
|
|
2446
|
+
await multiNodeClient.disconnect();
|
|
2447
|
+
});
|
|
2448
|
+
|
|
2449
|
+
it("should execute replace on all replica nodes", async () => {
|
|
2450
|
+
const multiNodeClient = new Memcache();
|
|
2451
|
+
await multiNodeClient.connect();
|
|
2452
|
+
|
|
2453
|
+
await multiNodeClient.set("replace-test", "old-value");
|
|
2454
|
+
|
|
2455
|
+
// Mock getNodesByKey to return multiple nodes
|
|
2456
|
+
const originalGetNodesByKey = multiNodeClient.hash.getNodesByKey.bind(
|
|
2457
|
+
multiNodeClient.hash,
|
|
2458
|
+
);
|
|
2459
|
+
vi.spyOn(multiNodeClient.hash, "getNodesByKey").mockImplementation(
|
|
2460
|
+
(key: string) => {
|
|
2461
|
+
const nodes = originalGetNodesByKey(key);
|
|
2462
|
+
return nodes.length > 0 ? [nodes[0], nodes[0]] : nodes;
|
|
2463
|
+
},
|
|
2464
|
+
);
|
|
2465
|
+
|
|
2466
|
+
const success = await multiNodeClient.replace(
|
|
2467
|
+
"replace-test",
|
|
2468
|
+
"new-value",
|
|
2469
|
+
);
|
|
2470
|
+
expect(success).toBe(true);
|
|
2471
|
+
|
|
2472
|
+
const value = await multiNodeClient.get("replace-test");
|
|
2473
|
+
expect(value).toBe("new-value");
|
|
2474
|
+
|
|
2475
|
+
await multiNodeClient.disconnect();
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
it("should execute append on all replica nodes", async () => {
|
|
2479
|
+
const multiNodeClient = new Memcache();
|
|
2480
|
+
await multiNodeClient.connect();
|
|
2481
|
+
|
|
2482
|
+
await multiNodeClient.set("append-test", "hello");
|
|
2483
|
+
|
|
2484
|
+
// Mock getNodesByKey to return multiple nodes
|
|
2485
|
+
const originalGetNodesByKey = multiNodeClient.hash.getNodesByKey.bind(
|
|
2486
|
+
multiNodeClient.hash,
|
|
2487
|
+
);
|
|
2488
|
+
vi.spyOn(multiNodeClient.hash, "getNodesByKey").mockImplementation(
|
|
2489
|
+
(key: string) => {
|
|
2490
|
+
const nodes = originalGetNodesByKey(key);
|
|
2491
|
+
return nodes.length > 0 ? [nodes[0], nodes[0]] : nodes;
|
|
2492
|
+
},
|
|
2493
|
+
);
|
|
2494
|
+
|
|
2495
|
+
const success = await multiNodeClient.append("append-test", " world");
|
|
2496
|
+
expect(success).toBe(true);
|
|
2497
|
+
|
|
2498
|
+
const value = await multiNodeClient.get("append-test");
|
|
2499
|
+
// When the same node is returned twice, append happens twice
|
|
2500
|
+
expect(value).toBe("hello world world");
|
|
2501
|
+
|
|
2502
|
+
await multiNodeClient.disconnect();
|
|
2503
|
+
});
|
|
2504
|
+
|
|
2505
|
+
it("should execute prepend on all replica nodes", async () => {
|
|
2506
|
+
const multiNodeClient = new Memcache();
|
|
2507
|
+
await multiNodeClient.connect();
|
|
2508
|
+
|
|
2509
|
+
await multiNodeClient.set("prepend-test", "world");
|
|
2510
|
+
|
|
2511
|
+
// Mock getNodesByKey to return multiple nodes
|
|
2512
|
+
const originalGetNodesByKey = multiNodeClient.hash.getNodesByKey.bind(
|
|
2513
|
+
multiNodeClient.hash,
|
|
2514
|
+
);
|
|
2515
|
+
vi.spyOn(multiNodeClient.hash, "getNodesByKey").mockImplementation(
|
|
2516
|
+
(key: string) => {
|
|
2517
|
+
const nodes = originalGetNodesByKey(key);
|
|
2518
|
+
return nodes.length > 0 ? [nodes[0], nodes[0]] : nodes;
|
|
2519
|
+
},
|
|
2520
|
+
);
|
|
2521
|
+
|
|
2522
|
+
const success = await multiNodeClient.prepend("prepend-test", "hello ");
|
|
2523
|
+
expect(success).toBe(true);
|
|
2524
|
+
|
|
2525
|
+
const value = await multiNodeClient.get("prepend-test");
|
|
2526
|
+
// When the same node is returned twice, prepend happens twice
|
|
2527
|
+
expect(value).toBe("hello hello world");
|
|
2528
|
+
|
|
2529
|
+
await multiNodeClient.disconnect();
|
|
2530
|
+
});
|
|
2531
|
+
|
|
2532
|
+
it("should execute touch on all replica nodes", async () => {
|
|
2533
|
+
const multiNodeClient = new Memcache();
|
|
2534
|
+
await multiNodeClient.connect();
|
|
2535
|
+
|
|
2536
|
+
await multiNodeClient.set("touch-test", "value");
|
|
2537
|
+
|
|
2538
|
+
// Mock getNodesByKey to return multiple nodes
|
|
2539
|
+
const originalGetNodesByKey = multiNodeClient.hash.getNodesByKey.bind(
|
|
2540
|
+
multiNodeClient.hash,
|
|
2541
|
+
);
|
|
2542
|
+
vi.spyOn(multiNodeClient.hash, "getNodesByKey").mockImplementation(
|
|
2543
|
+
(key: string) => {
|
|
2544
|
+
const nodes = originalGetNodesByKey(key);
|
|
2545
|
+
return nodes.length > 0 ? [nodes[0], nodes[0]] : nodes;
|
|
2546
|
+
},
|
|
2547
|
+
);
|
|
2548
|
+
|
|
2549
|
+
const success = await multiNodeClient.touch("touch-test", 3600);
|
|
2550
|
+
expect(success).toBe(true);
|
|
2551
|
+
|
|
2552
|
+
await multiNodeClient.disconnect();
|
|
2553
|
+
});
|
|
2554
|
+
});
|
|
2555
|
+
|
|
2556
|
+
describe("Exports", () => {
|
|
2557
|
+
it("should export createNode function from index", () => {
|
|
2558
|
+
expect(createNode).toBeDefined();
|
|
2559
|
+
expect(typeof createNode).toBe("function");
|
|
2560
|
+
|
|
2561
|
+
const node = createNode("localhost", 11211);
|
|
2562
|
+
expect(node).toBeDefined();
|
|
2563
|
+
expect(node.host).toBe("localhost");
|
|
2564
|
+
expect(node.port).toBe(11211);
|
|
2565
|
+
});
|
|
2566
|
+
});
|
|
2567
|
+
|
|
2568
|
+
describe("MemcacheEvents", () => {
|
|
2569
|
+
it("should export MemcacheEvents enum with correct values", () => {
|
|
2570
|
+
expect(MemcacheEvents.CONNECT).toBe("connect");
|
|
2571
|
+
expect(MemcacheEvents.QUIT).toBe("quit");
|
|
2572
|
+
expect(MemcacheEvents.HIT).toBe("hit");
|
|
2573
|
+
expect(MemcacheEvents.MISS).toBe("miss");
|
|
2574
|
+
expect(MemcacheEvents.ERROR).toBe("error");
|
|
2575
|
+
expect(MemcacheEvents.WARN).toBe("warn");
|
|
2576
|
+
expect(MemcacheEvents.INFO).toBe("info");
|
|
2577
|
+
expect(MemcacheEvents.TIMEOUT).toBe("timeout");
|
|
2578
|
+
expect(MemcacheEvents.CLOSE).toBe("close");
|
|
2579
|
+
});
|
|
2580
|
+
|
|
2581
|
+
it("should emit connect event on connection", async () => {
|
|
2582
|
+
let connectEmitted = false;
|
|
2583
|
+
client.on(MemcacheEvents.CONNECT, () => {
|
|
2584
|
+
connectEmitted = true;
|
|
2585
|
+
});
|
|
2586
|
+
|
|
2587
|
+
await client.connect();
|
|
2588
|
+
expect(connectEmitted).toBe(true);
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2591
|
+
// Removed: Socket event mocking tests (error, timeout, close)
|
|
2592
|
+
// These test internal socket behavior
|
|
2593
|
+
// Events are now tested at MemcacheNode level or with real servers
|
|
2594
|
+
|
|
2595
|
+
it("should emit hit event with key and value on successful get", async () => {
|
|
2596
|
+
let hitEmitted = false;
|
|
2597
|
+
let hitKey = "";
|
|
2598
|
+
let hitValue = "";
|
|
2599
|
+
|
|
2600
|
+
client.on(MemcacheEvents.HIT, (key: string, value: string) => {
|
|
2601
|
+
hitEmitted = true;
|
|
2602
|
+
hitKey = key;
|
|
2603
|
+
hitValue = value;
|
|
2604
|
+
});
|
|
2605
|
+
|
|
2606
|
+
await client.connect();
|
|
2607
|
+
await client.set("test-hit", "test-value");
|
|
2608
|
+
await client.get("test-hit");
|
|
2609
|
+
|
|
2610
|
+
expect(hitEmitted).toBe(true);
|
|
2611
|
+
expect(hitKey).toBe("test-hit");
|
|
2612
|
+
expect(hitValue).toBe("test-value");
|
|
2613
|
+
});
|
|
2614
|
+
|
|
2615
|
+
it("should emit miss event with key on failed get", async () => {
|
|
2616
|
+
let missEmitted = false;
|
|
2617
|
+
let missKey = "";
|
|
2618
|
+
|
|
2619
|
+
client.on(MemcacheEvents.MISS, (key: string) => {
|
|
2620
|
+
missEmitted = true;
|
|
2621
|
+
missKey = key;
|
|
2622
|
+
});
|
|
2623
|
+
|
|
2624
|
+
await client.connect();
|
|
2625
|
+
await client.get("non-existent-key");
|
|
2626
|
+
|
|
2627
|
+
expect(missEmitted).toBe(true);
|
|
2628
|
+
expect(missKey).toBe("non-existent-key");
|
|
2629
|
+
});
|
|
2630
|
+
|
|
2631
|
+
it("should emit hit events for multiple gets with mixed results", async () => {
|
|
2632
|
+
const hits: Array<{ key: string; value: string }> = [];
|
|
2633
|
+
const misses: string[] = [];
|
|
2634
|
+
|
|
2635
|
+
client.on(MemcacheEvents.HIT, (key: string, value: string) => {
|
|
2636
|
+
hits.push({ key, value });
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
client.on(MemcacheEvents.MISS, (key: string) => {
|
|
2640
|
+
misses.push(key);
|
|
2641
|
+
});
|
|
2642
|
+
|
|
2643
|
+
await client.connect();
|
|
2644
|
+
await client.set("exists1", "value1");
|
|
2645
|
+
await client.set("exists2", "value2");
|
|
2646
|
+
|
|
2647
|
+
await client.gets(["exists1", "non-existent", "exists2", "another-miss"]);
|
|
2648
|
+
|
|
2649
|
+
expect(hits).toHaveLength(2);
|
|
2650
|
+
expect(hits[0]).toEqual({ key: "exists1", value: "value1" });
|
|
2651
|
+
expect(hits[1]).toEqual({ key: "exists2", value: "value2" });
|
|
2652
|
+
expect(misses).toEqual(["non-existent", "another-miss"]);
|
|
2653
|
+
});
|
|
2654
|
+
|
|
2655
|
+
it("should emit miss events for all keys when gets returns no results", async () => {
|
|
2656
|
+
const misses: string[] = [];
|
|
2657
|
+
|
|
2658
|
+
client.on(MemcacheEvents.MISS, (key: string) => {
|
|
2659
|
+
misses.push(key);
|
|
2660
|
+
});
|
|
2661
|
+
|
|
2662
|
+
await client.connect();
|
|
2663
|
+
|
|
2664
|
+
// Get non-existent keys
|
|
2665
|
+
await client.gets(["nonexist1", "nonexist2", "nonexist3"]);
|
|
2666
|
+
|
|
2667
|
+
expect(misses).toEqual(["nonexist1", "nonexist2", "nonexist3"]);
|
|
2668
|
+
});
|
|
2669
|
+
|
|
2670
|
+
it("should emit error event when node emits error", async () => {
|
|
2671
|
+
let errorEmitted = false;
|
|
2672
|
+
let errorNodeId = "";
|
|
2673
|
+
let errorInstance: Error | undefined;
|
|
2674
|
+
|
|
2675
|
+
client.on(MemcacheEvents.ERROR, (nodeId: string, err: Error) => {
|
|
2676
|
+
errorEmitted = true;
|
|
2677
|
+
errorNodeId = nodeId;
|
|
2678
|
+
errorInstance = err;
|
|
2679
|
+
});
|
|
2680
|
+
|
|
2681
|
+
await client.connect();
|
|
2682
|
+
|
|
2683
|
+
// Get the node and trigger an error event
|
|
2684
|
+
const nodes = client.getNodes();
|
|
2685
|
+
const node = Array.from(nodes.values())[0];
|
|
2686
|
+
const testError = new Error("Test error");
|
|
2687
|
+
node.emit("error", testError);
|
|
2688
|
+
|
|
2689
|
+
expect(errorEmitted).toBe(true);
|
|
2690
|
+
expect(errorNodeId).toBe("localhost:11211");
|
|
2691
|
+
expect(errorInstance).toBe(testError);
|
|
2692
|
+
});
|
|
2693
|
+
|
|
2694
|
+
it("should emit timeout event when node emits timeout", async () => {
|
|
2695
|
+
let timeoutEmitted = false;
|
|
2696
|
+
let timeoutNodeId = "";
|
|
2697
|
+
|
|
2698
|
+
client.on(MemcacheEvents.TIMEOUT, (nodeId: string) => {
|
|
2699
|
+
timeoutEmitted = true;
|
|
2700
|
+
timeoutNodeId = nodeId;
|
|
2701
|
+
});
|
|
2702
|
+
|
|
2703
|
+
await client.connect();
|
|
2704
|
+
|
|
2705
|
+
// Get the node and trigger a timeout event
|
|
2706
|
+
const nodes = client.getNodes();
|
|
2707
|
+
const node = Array.from(nodes.values())[0];
|
|
2708
|
+
node.emit("timeout");
|
|
2709
|
+
|
|
2710
|
+
expect(timeoutEmitted).toBe(true);
|
|
2711
|
+
expect(timeoutNodeId).toBe("localhost:11211");
|
|
2712
|
+
});
|
|
2713
|
+
|
|
2714
|
+
it("should emit close event when node emits close", async () => {
|
|
2715
|
+
let closeEmitted = false;
|
|
2716
|
+
let closeNodeId = "";
|
|
2717
|
+
|
|
2718
|
+
client.on(MemcacheEvents.CLOSE, (nodeId: string) => {
|
|
2719
|
+
closeEmitted = true;
|
|
2720
|
+
closeNodeId = nodeId;
|
|
2721
|
+
});
|
|
2722
|
+
|
|
2723
|
+
await client.connect();
|
|
2724
|
+
|
|
2725
|
+
// Get the node and trigger a close event
|
|
2726
|
+
const nodes = client.getNodes();
|
|
2727
|
+
const node = Array.from(nodes.values())[0];
|
|
2728
|
+
node.emit("close");
|
|
2729
|
+
|
|
2730
|
+
expect(closeEmitted).toBe(true);
|
|
2731
|
+
expect(closeNodeId).toBe("localhost:11211");
|
|
2732
|
+
});
|
|
2733
|
+
});
|
|
2734
|
+
});
|