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.
@@ -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
+ });