@geekmidas/cache 10.0.0-alpha.99 → 10.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/CHANGELOG.md +52 -0
- package/dist/{rolldown-runtime-VH7oDXx4.cjs → chunk-CUT6urMc.cjs} +10 -8
- package/dist/expo.cjs +10 -10
- package/dist/expo.cjs.map +1 -1
- package/dist/expo.d.cts +6 -2
- package/dist/expo.d.cts.map +1 -1
- package/dist/expo.d.mts +6 -2
- package/dist/expo.d.mts.map +1 -1
- package/dist/expo.mjs +6 -4
- package/dist/expo.mjs.map +1 -1
- package/dist/file.cjs +19 -16
- package/dist/file.cjs.map +1 -1
- package/dist/file.d.cts +6 -3
- package/dist/file.d.cts.map +1 -1
- package/dist/file.d.mts +6 -3
- package/dist/file.d.mts.map +1 -1
- package/dist/file.mjs +12 -7
- package/dist/file.mjs.map +1 -1
- package/dist/index-BSYYdwRm.d.cts +36 -0
- package/dist/index-BSYYdwRm.d.cts.map +1 -0
- package/dist/index-Dx96gr-9.d.mts +36 -0
- package/dist/index-Dx96gr-9.d.mts.map +1 -0
- package/dist/index.cjs +2 -17
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -12
- package/dist/memory.cjs +11 -8
- package/dist/memory.cjs.map +1 -1
- package/dist/memory.d.cts +6 -2
- package/dist/memory.d.cts.map +1 -1
- package/dist/memory.d.mts +6 -2
- package/dist/memory.d.mts.map +1 -1
- package/dist/memory.mjs +8 -5
- package/dist/memory.mjs.map +1 -1
- package/dist/src-CrE4Nl5T.mjs +14 -0
- package/dist/src-CrE4Nl5T.mjs.map +1 -0
- package/dist/src-zfYb7mdS.cjs +20 -0
- package/dist/src-zfYb7mdS.cjs.map +1 -0
- package/dist/upstash.cjs +6 -46
- package/dist/upstash.cjs.map +1 -1
- package/dist/upstash.d.cts +6 -27
- package/dist/upstash.d.cts.map +1 -1
- package/dist/upstash.d.mts +6 -27
- package/dist/upstash.d.mts.map +1 -1
- package/dist/upstash.mjs +4 -41
- package/dist/upstash.mjs.map +1 -1
- package/package.json +6 -53
- package/src/__benchmarks__/memory.bench.ts +51 -0
- package/src/__tests__/cache.spec.ts +144 -0
- package/src/__tests__/file.spec.ts +192 -0
- package/src/__tests__/integration.spec.ts +282 -0
- package/src/__tests__/memory.spec.ts +134 -0
- package/src/__tests__/upstash.spec.ts +276 -0
- package/src/expo.ts +50 -0
- package/src/file.ts +126 -0
- package/src/index.ts +48 -0
- package/src/memory.ts +38 -0
- package/src/upstash.ts +41 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +5 -0
- package/dist/index-BbdGhmWP.d.cts +0 -62
- package/dist/index-BbdGhmWP.d.cts.map +0 -1
- package/dist/index-BbdGhmWP.d.mts +0 -62
- package/dist/index-BbdGhmWP.d.mts.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/postgres.cjs +0 -185
- package/dist/postgres.cjs.map +0 -1
- package/dist/postgres.d.cts +0 -71
- package/dist/postgres.d.cts.map +0 -1
- package/dist/postgres.d.mts +0 -71
- package/dist/postgres.d.mts.map +0 -1
- package/dist/postgres.mjs +0 -180
- package/dist/postgres.mjs.map +0 -1
- package/dist/redis.cjs +0 -88
- package/dist/redis.cjs.map +0 -1
- package/dist/redis.d.cts +0 -27
- package/dist/redis.d.cts.map +0 -1
- package/dist/redis.d.mts +0 -27
- package/dist/redis.d.mts.map +0 -1
- package/dist/redis.mjs +0 -85
- package/dist/redis.mjs.map +0 -1
- package/dist/registry.cjs +0 -48
- package/dist/registry.cjs.map +0 -1
- package/dist/registry.d.cts +0 -2
- package/dist/registry.d.mts +0 -2
- package/dist/registry.mjs +0 -44
- package/dist/registry.mjs.map +0 -1
- package/dist/telemetry.cjs +0 -38
- package/dist/telemetry.cjs.map +0 -1
- package/dist/telemetry.d.cts +0 -6
- package/dist/telemetry.d.cts.map +0 -1
- package/dist/telemetry.d.mts +0 -6
- package/dist/telemetry.d.mts.map +0 -1
- package/dist/telemetry.mjs +0 -37
- package/dist/telemetry.mjs.map +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { Cache } from '../';
|
|
3
|
+
import { getExpirationInSeconds } from '../';
|
|
4
|
+
import { InMemoryCache } from '../memory';
|
|
5
|
+
import { UpstashCache } from '../upstash';
|
|
6
|
+
|
|
7
|
+
describe('Cache Interface', () => {
|
|
8
|
+
describe('interface compliance', () => {
|
|
9
|
+
it('InMemoryCache should implement Cache interface', () => {
|
|
10
|
+
const cache: Cache = new InMemoryCache();
|
|
11
|
+
|
|
12
|
+
expect(typeof cache.get).toBe('function');
|
|
13
|
+
expect(typeof cache.set).toBe('function');
|
|
14
|
+
expect(typeof cache.delete).toBe('function');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('UpstashCache should implement Cache interface', () => {
|
|
18
|
+
const cache: Cache = new UpstashCache(
|
|
19
|
+
'http://localhost:8079',
|
|
20
|
+
'example_token',
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(typeof cache.get).toBe('function');
|
|
24
|
+
expect(typeof cache.set).toBe('function');
|
|
25
|
+
expect(typeof cache.delete).toBe('function');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('method signatures', () => {
|
|
30
|
+
it('should have correct get method signature', () => {
|
|
31
|
+
const cache: Cache = new InMemoryCache();
|
|
32
|
+
|
|
33
|
+
// These should compile without errors
|
|
34
|
+
const getResult: Promise<unknown> = cache.get('test');
|
|
35
|
+
expect(getResult).toBeInstanceOf(Promise);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should have correct set method signature', () => {
|
|
39
|
+
const cache: Cache = new InMemoryCache();
|
|
40
|
+
|
|
41
|
+
// These should compile without errors
|
|
42
|
+
const setResult: Promise<void> = cache.set('test', 'value');
|
|
43
|
+
expect(setResult).toBeInstanceOf(Promise);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should have correct delete method signature', () => {
|
|
47
|
+
const cache: Cache = new InMemoryCache();
|
|
48
|
+
|
|
49
|
+
// These should compile without errors
|
|
50
|
+
const deleteResult: Promise<void> = cache.delete('test');
|
|
51
|
+
expect(deleteResult).toBeInstanceOf(Promise);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('function-level generic type support', () => {
|
|
56
|
+
it('should support string types with function-level generics', async () => {
|
|
57
|
+
const cache: Cache = new InMemoryCache();
|
|
58
|
+
|
|
59
|
+
await cache.set<string>('key', 'value');
|
|
60
|
+
const result = await cache.get<string>('key');
|
|
61
|
+
|
|
62
|
+
expect(typeof result === 'string' || result === undefined).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should support number types with function-level generics', async () => {
|
|
66
|
+
const cache: Cache = new InMemoryCache();
|
|
67
|
+
|
|
68
|
+
await cache.set<number>('key', 42);
|
|
69
|
+
const result = await cache.get<number>('key');
|
|
70
|
+
|
|
71
|
+
expect(typeof result === 'number' || result === undefined).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should support object types with function-level generics', async () => {
|
|
75
|
+
interface User {
|
|
76
|
+
id: number;
|
|
77
|
+
name: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const cache: Cache = new InMemoryCache();
|
|
81
|
+
const user: User = { id: 1, name: 'John' };
|
|
82
|
+
|
|
83
|
+
await cache.set<User>('user', user);
|
|
84
|
+
const result = await cache.get<User>('user');
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
result === undefined ||
|
|
88
|
+
(typeof result === 'object' && 'id' in result && 'name' in result),
|
|
89
|
+
).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should support mixed types in same cache instance', async () => {
|
|
93
|
+
const cache: Cache = new InMemoryCache();
|
|
94
|
+
|
|
95
|
+
await cache.set<string>('string', 'text');
|
|
96
|
+
await cache.set<number>('number', 42);
|
|
97
|
+
await cache.set<boolean>('boolean', true);
|
|
98
|
+
|
|
99
|
+
const stringResult = await cache.get<string>('string');
|
|
100
|
+
const numberResult = await cache.get<number>('number');
|
|
101
|
+
const booleanResult = await cache.get<boolean>('boolean');
|
|
102
|
+
|
|
103
|
+
expect(stringResult).toBe('text');
|
|
104
|
+
expect(numberResult).toBe(42);
|
|
105
|
+
expect(booleanResult).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('getExpirationInSeconds', () => {
|
|
111
|
+
it('should return 0 for null', () => {
|
|
112
|
+
expect(getExpirationInSeconds(null)).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should return 0 for undefined', () => {
|
|
116
|
+
expect(getExpirationInSeconds(undefined)).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should return positive seconds for future date', () => {
|
|
120
|
+
const futureDate = new Date(Date.now() + 60000); // 1 minute in the future
|
|
121
|
+
const seconds = getExpirationInSeconds(futureDate);
|
|
122
|
+
expect(seconds).toBeGreaterThan(0);
|
|
123
|
+
expect(seconds).toBeLessThanOrEqual(60);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should return 0 for past date', () => {
|
|
127
|
+
const pastDate = new Date(Date.now() - 60000); // 1 minute in the past
|
|
128
|
+
expect(getExpirationInSeconds(pastDate)).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle ISO string dates', () => {
|
|
132
|
+
const futureDate = new Date(Date.now() + 120000).toISOString(); // 2 minutes in the future
|
|
133
|
+
const seconds = getExpirationInSeconds(futureDate);
|
|
134
|
+
expect(seconds).toBeGreaterThan(0);
|
|
135
|
+
expect(seconds).toBeLessThanOrEqual(120);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle timestamp numbers', () => {
|
|
139
|
+
const futureTimestamp = Date.now() + 30000; // 30 seconds in the future
|
|
140
|
+
const seconds = getExpirationInSeconds(futureTimestamp);
|
|
141
|
+
expect(seconds).toBeGreaterThan(0);
|
|
142
|
+
expect(seconds).toBeLessThanOrEqual(30);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { FileCache } from '../file';
|
|
7
|
+
|
|
8
|
+
describe('FileCache', () => {
|
|
9
|
+
let dir: string;
|
|
10
|
+
let cachePath: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
dir = await mkdtemp(join(tmpdir(), 'file-cache-test-'));
|
|
14
|
+
cachePath = join(dir, 'cache.json');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
vi.useRealTimers();
|
|
19
|
+
await rm(dir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('get', () => {
|
|
23
|
+
it('should return undefined for non-existent key', async () => {
|
|
24
|
+
const cache = new FileCache({ path: cachePath });
|
|
25
|
+
const result = await cache.get('non-existent');
|
|
26
|
+
expect(result).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return undefined when file does not exist', async () => {
|
|
30
|
+
const cache = new FileCache({
|
|
31
|
+
path: join(dir, 'subdir', 'cache.json'),
|
|
32
|
+
});
|
|
33
|
+
const result = await cache.get('any-key');
|
|
34
|
+
expect(result).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return stored value', async () => {
|
|
38
|
+
const cache = new FileCache({ path: cachePath });
|
|
39
|
+
await cache.set('key', 'hello');
|
|
40
|
+
const result = await cache.get('key');
|
|
41
|
+
expect(result).toBe('hello');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should handle different data types', async () => {
|
|
45
|
+
const cache = new FileCache({ path: cachePath });
|
|
46
|
+
|
|
47
|
+
await cache.set('string', 'value');
|
|
48
|
+
await cache.set('number', 42);
|
|
49
|
+
await cache.set('boolean', true);
|
|
50
|
+
await cache.set('object', { id: 1, name: 'test' });
|
|
51
|
+
await cache.set('array', [1, 2, 3]);
|
|
52
|
+
await cache.set('null', null);
|
|
53
|
+
|
|
54
|
+
expect(await cache.get('string')).toBe('value');
|
|
55
|
+
expect(await cache.get('number')).toBe(42);
|
|
56
|
+
expect(await cache.get('boolean')).toBe(true);
|
|
57
|
+
expect(await cache.get('object')).toEqual({ id: 1, name: 'test' });
|
|
58
|
+
expect(await cache.get('array')).toEqual([1, 2, 3]);
|
|
59
|
+
expect(await cache.get('null')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should return undefined for expired entries', async () => {
|
|
63
|
+
const cache = new FileCache({ path: cachePath });
|
|
64
|
+
|
|
65
|
+
vi.useFakeTimers();
|
|
66
|
+
await cache.set('expiring', 'value', 1);
|
|
67
|
+
|
|
68
|
+
// Advance past expiration
|
|
69
|
+
vi.advanceTimersByTime(2000);
|
|
70
|
+
|
|
71
|
+
const result = await cache.get('expiring');
|
|
72
|
+
expect(result).toBeUndefined();
|
|
73
|
+
|
|
74
|
+
vi.useRealTimers();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('set', () => {
|
|
79
|
+
it('should create file and parent directory', async () => {
|
|
80
|
+
const nested = join(dir, 'nested', 'dir', 'cache.json');
|
|
81
|
+
const cache = new FileCache({ path: nested });
|
|
82
|
+
|
|
83
|
+
await cache.set('key', 'value');
|
|
84
|
+
|
|
85
|
+
expect(existsSync(nested)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should overwrite existing value', async () => {
|
|
89
|
+
const cache = new FileCache({ path: cachePath });
|
|
90
|
+
|
|
91
|
+
await cache.set('key', 'first');
|
|
92
|
+
await cache.set('key', 'second');
|
|
93
|
+
|
|
94
|
+
expect(await cache.get('key')).toBe('second');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should persist data in valid JSON format', async () => {
|
|
98
|
+
const cache = new FileCache({ path: cachePath });
|
|
99
|
+
|
|
100
|
+
await cache.set('test', { hello: 'world' });
|
|
101
|
+
|
|
102
|
+
const raw = await readFile(cachePath, 'utf-8');
|
|
103
|
+
const parsed = JSON.parse(raw);
|
|
104
|
+
|
|
105
|
+
expect(parsed.test).toBeDefined();
|
|
106
|
+
expect(parsed.test.value).toEqual({ hello: 'world' });
|
|
107
|
+
expect(parsed.test.expiresAt).toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should use custom default TTL', async () => {
|
|
111
|
+
const cache = new FileCache({ path: cachePath, ttl: 3600 });
|
|
112
|
+
|
|
113
|
+
await cache.set('key', 'value');
|
|
114
|
+
|
|
115
|
+
const remaining = await cache.ttl('key');
|
|
116
|
+
expect(remaining).toBeGreaterThan(3500);
|
|
117
|
+
expect(remaining).toBeLessThanOrEqual(3600);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('delete', () => {
|
|
122
|
+
it('should remove existing key', async () => {
|
|
123
|
+
const cache = new FileCache({ path: cachePath });
|
|
124
|
+
|
|
125
|
+
await cache.set('key', 'value');
|
|
126
|
+
await cache.delete('key');
|
|
127
|
+
|
|
128
|
+
expect(await cache.get('key')).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should not throw for non-existent key', async () => {
|
|
132
|
+
const cache = new FileCache({ path: cachePath });
|
|
133
|
+
await expect(cache.delete('non-existent')).resolves.not.toThrow();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should only delete specified key', async () => {
|
|
137
|
+
const cache = new FileCache({ path: cachePath });
|
|
138
|
+
|
|
139
|
+
await cache.set('key1', 'value1');
|
|
140
|
+
await cache.set('key2', 'value2');
|
|
141
|
+
await cache.delete('key1');
|
|
142
|
+
|
|
143
|
+
expect(await cache.get('key1')).toBeUndefined();
|
|
144
|
+
expect(await cache.get('key2')).toBe('value2');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('ttl', () => {
|
|
149
|
+
it('should return 0 for non-existent key', async () => {
|
|
150
|
+
const cache = new FileCache({ path: cachePath });
|
|
151
|
+
expect(await cache.ttl('non-existent')).toBe(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should return remaining seconds for existing key', async () => {
|
|
155
|
+
const cache = new FileCache({ path: cachePath });
|
|
156
|
+
await cache.set('key', 'value', 300);
|
|
157
|
+
|
|
158
|
+
const remaining = await cache.ttl('key');
|
|
159
|
+
expect(remaining).toBeGreaterThan(298);
|
|
160
|
+
expect(remaining).toBeLessThanOrEqual(300);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('concurrency', () => {
|
|
165
|
+
it('should handle concurrent writes without corruption', async () => {
|
|
166
|
+
const cache = new FileCache({ path: cachePath });
|
|
167
|
+
|
|
168
|
+
// Write 10 keys concurrently
|
|
169
|
+
await Promise.all(
|
|
170
|
+
Array.from({ length: 10 }, (_, i) =>
|
|
171
|
+
cache.set(`key-${i}`, `value-${i}`),
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Verify all keys were written
|
|
176
|
+
for (let i = 0; i < 10; i++) {
|
|
177
|
+
const value = await cache.get(`key-${i}`);
|
|
178
|
+
expect(value).toBe(`value-${i}`);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('persistence', () => {
|
|
184
|
+
it('should persist across cache instances', async () => {
|
|
185
|
+
const cache1 = new FileCache({ path: cachePath });
|
|
186
|
+
await cache1.set('persistent', 'data');
|
|
187
|
+
|
|
188
|
+
const cache2 = new FileCache({ path: cachePath });
|
|
189
|
+
expect(await cache2.get('persistent')).toBe('data');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import type { Cache } from '../';
|
|
3
|
+
import { InMemoryCache } from '../memory';
|
|
4
|
+
|
|
5
|
+
describe('Cache Integration Tests', () => {
|
|
6
|
+
let _memoryCache: Cache;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
_memoryCache = new InMemoryCache();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('cross-cache behavior consistency', () => {
|
|
13
|
+
const testCases = [
|
|
14
|
+
{ name: 'InMemoryCache', cache: () => new InMemoryCache() },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
testCases.forEach(({ name, cache }) => {
|
|
18
|
+
describe(name, () => {
|
|
19
|
+
let cacheInstance: Cache;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
cacheInstance = cache();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should handle basic CRUD operations', async () => {
|
|
26
|
+
// Create
|
|
27
|
+
await cacheInstance.set('user:1', { id: 1, name: 'John' });
|
|
28
|
+
|
|
29
|
+
// Read
|
|
30
|
+
const user = await cacheInstance.get('user:1');
|
|
31
|
+
expect(user).toEqual({ id: 1, name: 'John' });
|
|
32
|
+
|
|
33
|
+
// Update
|
|
34
|
+
await cacheInstance.set('user:1', { id: 1, name: 'Jane' });
|
|
35
|
+
const updatedUser = await cacheInstance.get('user:1');
|
|
36
|
+
expect(updatedUser).toEqual({ id: 1, name: 'Jane' });
|
|
37
|
+
|
|
38
|
+
// Delete
|
|
39
|
+
await cacheInstance.delete('user:1');
|
|
40
|
+
const deletedUser = await cacheInstance.get('user:1');
|
|
41
|
+
expect(deletedUser).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should handle concurrent operations', async () => {
|
|
45
|
+
const operations = [
|
|
46
|
+
cacheInstance.set('key1', 'value1'),
|
|
47
|
+
cacheInstance.set('key2', 'value2'),
|
|
48
|
+
cacheInstance.set('key3', 'value3'),
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
await Promise.all(operations);
|
|
52
|
+
|
|
53
|
+
const results = await Promise.all([
|
|
54
|
+
cacheInstance.get('key1'),
|
|
55
|
+
cacheInstance.get('key2'),
|
|
56
|
+
cacheInstance.get('key3'),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
expect(results).toEqual(['value1', 'value2', 'value3']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle data type preservation', async () => {
|
|
63
|
+
const testData = {
|
|
64
|
+
string: 'hello',
|
|
65
|
+
number: 42,
|
|
66
|
+
boolean: true,
|
|
67
|
+
object: { nested: true },
|
|
68
|
+
array: [1, 2, 3],
|
|
69
|
+
null: null,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Store all data types
|
|
73
|
+
await Promise.all(
|
|
74
|
+
Object.entries(testData).map(([key, value]) =>
|
|
75
|
+
cacheInstance.set(key, value),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Retrieve and verify
|
|
80
|
+
const results = await Promise.all(
|
|
81
|
+
Object.keys(testData).map((key) => cacheInstance.get(key)),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(results[0]).toBe('hello');
|
|
85
|
+
expect(results[1]).toBe(42);
|
|
86
|
+
expect(results[2]).toBe(true);
|
|
87
|
+
expect(results[3]).toEqual({ nested: true });
|
|
88
|
+
expect(results[4]).toEqual([1, 2, 3]);
|
|
89
|
+
expect(results[5]).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle key isolation', async () => {
|
|
93
|
+
await cacheInstance.set('user:1', 'User One');
|
|
94
|
+
await cacheInstance.set('user:2', 'User Two');
|
|
95
|
+
await cacheInstance.set('session:1', 'Session One');
|
|
96
|
+
|
|
97
|
+
await cacheInstance.delete('user:1');
|
|
98
|
+
|
|
99
|
+
expect(await cacheInstance.get('user:1')).toBeUndefined();
|
|
100
|
+
expect(await cacheInstance.get('user:2')).toBe('User Two');
|
|
101
|
+
expect(await cacheInstance.get('session:1')).toBe('Session One');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('cache migration scenarios', () => {
|
|
108
|
+
it('should support migrating data between cache implementations', async () => {
|
|
109
|
+
const sourceCache = new InMemoryCache();
|
|
110
|
+
const targetCache = new InMemoryCache();
|
|
111
|
+
|
|
112
|
+
// Populate source cache
|
|
113
|
+
const testData = {
|
|
114
|
+
'user:1': { id: 1, name: 'John' },
|
|
115
|
+
'user:2': { id: 2, name: 'Jane' },
|
|
116
|
+
'config:app': { theme: 'dark', notifications: true },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
for (const [key, value] of Object.entries(testData)) {
|
|
120
|
+
await sourceCache.set(key, value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Simulate migration by reading from source and writing to target
|
|
124
|
+
const migrationKeys = Object.keys(testData);
|
|
125
|
+
for (const key of migrationKeys) {
|
|
126
|
+
const value = await sourceCache.get(key);
|
|
127
|
+
if (value !== undefined) {
|
|
128
|
+
await targetCache.set(key, value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Verify migration
|
|
133
|
+
for (const [key, expectedValue] of Object.entries(testData)) {
|
|
134
|
+
const migratedValue = await targetCache.get(key);
|
|
135
|
+
expect(migratedValue).toEqual(expectedValue);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('cache abstraction patterns', () => {
|
|
141
|
+
it('should support cache factory pattern', async () => {
|
|
142
|
+
const createCache = (type: 'memory'): Cache => {
|
|
143
|
+
switch (type) {
|
|
144
|
+
case 'memory':
|
|
145
|
+
return new InMemoryCache();
|
|
146
|
+
default:
|
|
147
|
+
throw new Error(`Unknown cache type: ${type}`);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const userCache = createCache('memory');
|
|
152
|
+
const sessionCache = createCache('memory');
|
|
153
|
+
|
|
154
|
+
await userCache.set<{ id: number; name: string }>('user:1', {
|
|
155
|
+
id: 1,
|
|
156
|
+
name: 'John',
|
|
157
|
+
});
|
|
158
|
+
await sessionCache.set<string>('session:abc', 'active');
|
|
159
|
+
|
|
160
|
+
expect(
|
|
161
|
+
await userCache.get<{ id: number; name: string }>('user:1'),
|
|
162
|
+
).toEqual({ id: 1, name: 'John' });
|
|
163
|
+
expect(await sessionCache.get<string>('session:abc')).toBe('active');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should support cache wrapper pattern', async () => {
|
|
167
|
+
class LoggingCache implements Cache {
|
|
168
|
+
private logs: string[] = [];
|
|
169
|
+
|
|
170
|
+
constructor(private underlying: Cache) {}
|
|
171
|
+
|
|
172
|
+
async get<T>(key: string): Promise<T | undefined> {
|
|
173
|
+
this.logs.push(`GET ${key}`);
|
|
174
|
+
return this.underlying.get<T>(key);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
|
178
|
+
this.logs.push(`SET ${key}`);
|
|
179
|
+
return this.underlying.set<T>(key, value, ttl);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async delete(key: string): Promise<void> {
|
|
183
|
+
this.logs.push(`DELETE ${key}`);
|
|
184
|
+
return this.underlying.delete(key);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async ttl(key: string): Promise<number> {
|
|
188
|
+
return this.underlying.ttl(key);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getLogs(): string[] {
|
|
192
|
+
return [...this.logs];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const baseCache = new InMemoryCache();
|
|
197
|
+
const loggingCache = new LoggingCache(baseCache);
|
|
198
|
+
|
|
199
|
+
await loggingCache.set('test', 'value');
|
|
200
|
+
await loggingCache.get('test');
|
|
201
|
+
await loggingCache.delete('test');
|
|
202
|
+
|
|
203
|
+
const logs = loggingCache.getLogs();
|
|
204
|
+
expect(logs).toEqual(['SET test', 'GET test', 'DELETE test']);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('performance and stress tests', () => {
|
|
209
|
+
it('should handle large number of operations', async () => {
|
|
210
|
+
const cache = new InMemoryCache();
|
|
211
|
+
const operationCount = 1000;
|
|
212
|
+
|
|
213
|
+
// Bulk insert
|
|
214
|
+
const insertPromises = Array.from({ length: operationCount }, (_, i) =>
|
|
215
|
+
cache.set(`key:${i}`, i),
|
|
216
|
+
);
|
|
217
|
+
await Promise.all(insertPromises);
|
|
218
|
+
|
|
219
|
+
// Bulk read
|
|
220
|
+
const readPromises = Array.from({ length: operationCount }, (_, i) =>
|
|
221
|
+
cache.get(`key:${i}`),
|
|
222
|
+
);
|
|
223
|
+
const results = await Promise.all(readPromises);
|
|
224
|
+
|
|
225
|
+
// Verify all values
|
|
226
|
+
for (let i = 0; i < operationCount; i++) {
|
|
227
|
+
expect(results[i]).toBe(i);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Bulk delete
|
|
231
|
+
const deletePromises = Array.from({ length: operationCount }, (_, i) =>
|
|
232
|
+
cache.delete(`key:${i}`),
|
|
233
|
+
);
|
|
234
|
+
await Promise.all(deletePromises);
|
|
235
|
+
|
|
236
|
+
// Verify deletion
|
|
237
|
+
const postDeleteRead = await cache.get('key:0');
|
|
238
|
+
expect(postDeleteRead).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should handle large objects efficiently', async () => {
|
|
242
|
+
const cache = new InMemoryCache();
|
|
243
|
+
|
|
244
|
+
const largeObject = {
|
|
245
|
+
id: 1,
|
|
246
|
+
data: Array.from({ length: 1000 }, (_, i) => ({
|
|
247
|
+
index: i,
|
|
248
|
+
value: `value-${i}`,
|
|
249
|
+
metadata: {
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
random: Math.random(),
|
|
252
|
+
},
|
|
253
|
+
})),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const startTime = Date.now();
|
|
257
|
+
await cache.set('large-object', largeObject);
|
|
258
|
+
const result = await cache.get('large-object');
|
|
259
|
+
const endTime = Date.now();
|
|
260
|
+
|
|
261
|
+
expect(result).toEqual(largeObject);
|
|
262
|
+
expect(endTime - startTime).toBeLessThan(100); // Should complete within 100ms
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('error scenarios', () => {
|
|
267
|
+
it('should handle operations after errors gracefully', async () => {
|
|
268
|
+
const cache = new InMemoryCache();
|
|
269
|
+
|
|
270
|
+
// Successful operations
|
|
271
|
+
await cache.set('valid-key', 'valid-value');
|
|
272
|
+
expect(await cache.get('valid-key')).toBe('valid-value');
|
|
273
|
+
|
|
274
|
+
// Operations should continue working normally
|
|
275
|
+
await cache.set('another-key', 'another-value');
|
|
276
|
+
expect(await cache.get('another-key')).toBe('another-value');
|
|
277
|
+
|
|
278
|
+
await cache.delete('valid-key');
|
|
279
|
+
expect(await cache.get('valid-key')).toBeUndefined();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|