(valueOrFunction: T | Function_, ...arguments_: P[]): T {
6 | return typeof valueOrFunction === 'function' ? (valueOrFunction as Function_
)(...arguments_) : valueOrFunction;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/cache-id.test.ts:
--------------------------------------------------------------------------------
1 | import {Keyv} from 'keyv';
2 | import {
3 | beforeEach, describe, expect, it,
4 | } from 'vitest';
5 | import {createCache} from '../src/index.js';
6 |
7 | describe('cacheId', () => {
8 | let keyv: Keyv;
9 |
10 | beforeEach(async () => {
11 | keyv = new Keyv();
12 | });
13 |
14 | it('user set', () => {
15 | const cache = createCache({stores: [keyv], cacheId: 'my-cache-id'});
16 | expect(cache.cacheId()).toEqual('my-cache-id');
17 | });
18 | it('auto generated', () => {
19 | const cache = createCache({stores: [keyv]});
20 | expect(cache.cacheId()).toBeTypeOf('string');
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/clear.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-await-in-loop */
2 | import {Keyv} from 'keyv';
3 | import {
4 | beforeEach, describe, expect, it,
5 | } from 'vitest';
6 | import {faker} from '@faker-js/faker';
7 | import {createCache} from '../src/index.js';
8 | import {sleep} from './sleep.js';
9 |
10 | describe('clear', () => {
11 | let keyv: Keyv;
12 | let cache: ReturnType;
13 | let ttl = 500;
14 | const data = {key: '', value: ''};
15 |
16 | beforeEach(async () => {
17 | data.key = faker.string.alpha(20);
18 | data.value = faker.string.sample();
19 | ttl = faker.number.int({min: 500, max: 1000});
20 | keyv = new Keyv();
21 | cache = createCache({stores: [keyv]});
22 | });
23 |
24 | it('basic', async () => {
25 | const array = [1, 2, 3];
26 | for (const index of array) {
27 | await cache.set(data.key + index, data.value + index, ttl);
28 | await expect(cache.get(data.key + index)).resolves.toEqual(data.value + index);
29 | }
30 |
31 | await expect(cache.clear()).resolves.toEqual(true);
32 | for (const index of array) {
33 | await expect(cache.get(data.key + index)).resolves.toBeUndefined();
34 | }
35 | });
36 |
37 | it('clear should be non-blocking', async () => {
38 | const secondKeyv = new Keyv();
39 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true});
40 | await cache.set(data.key, data.value);
41 | expect(await secondKeyv.get(data.key)).toBe(data.value);
42 | await cache.clear();
43 | await sleep(200);
44 | await expect(cache.get(data.key)).resolves.toBeUndefined();
45 | await expect(secondKeyv.get(data.key)).resolves.toBeUndefined();
46 | });
47 |
48 | it('error', async () => {
49 | await cache.set(data.key, data.value);
50 | const error = new Error('clear error');
51 | keyv.clear = () => {
52 | throw error;
53 | };
54 |
55 | await expect(cache.clear()).rejects.toThrowError(error);
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/del.test.ts:
--------------------------------------------------------------------------------
1 | import {Keyv} from 'keyv';
2 | import {
3 | beforeEach, describe, expect, it,
4 | } from 'vitest';
5 | import {faker} from '@faker-js/faker';
6 | import {createCache} from '../src/index.js';
7 | import {sleep} from './sleep.js';
8 |
9 | describe('del', () => {
10 | let keyv: Keyv;
11 | let cache: ReturnType;
12 | let ttl = 500;
13 | const data = {key: '', value: ''};
14 |
15 | beforeEach(async () => {
16 | data.key = faker.string.alpha(20);
17 | data.value = faker.string.sample();
18 | ttl = faker.number.int({min: 500, max: 1000});
19 | keyv = new Keyv();
20 | cache = createCache({stores: [keyv]});
21 | });
22 |
23 | it('basic', async () => {
24 | await cache.set(data.key, data.value, ttl);
25 | await expect(cache.get(data.key)).resolves.toEqual(data.value);
26 | await expect(cache.del(data.key)).resolves.toEqual(true);
27 | await expect(cache.get(data.key)).resolves.toBeUndefined();
28 | });
29 |
30 | it('error', async () => {
31 | await cache.set(data.key, data.value);
32 | const error = new Error('delete error');
33 | keyv.delete = () => {
34 | throw error;
35 | };
36 |
37 | await expect(cache.del(data.key)).rejects.toThrowError(error);
38 | });
39 | it('del should be non-blocking', async () => {
40 | const secondKeyv = new Keyv();
41 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true});
42 | await cache.set(data.key, data.value);
43 | await cache.del(data.key);
44 | await sleep(200);
45 | await expect(cache.get(data.key)).resolves.toBeUndefined();
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/disconnect.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | describe, expect, test, vi,
3 | } from 'vitest';
4 | import {Keyv} from 'keyv';
5 | import KeyvRedis from '@keyv/redis';
6 | import {CacheableMemory} from 'cacheable';
7 | import {createCache} from '../src/index.js';
8 |
9 | describe('disconnect', () => {
10 | test('disconnect from multiple stores', async () => {
11 | const cacheableKeyvStore = new Keyv({
12 | store: new CacheableMemory({ttl: 60_000, lruSize: 5000}),
13 | });
14 | const redisKeyvStore = new Keyv({
15 | store: new KeyvRedis('redis://localhost:6379'),
16 | });
17 | // Multiple stores
18 | const cache = createCache({
19 | stores: [
20 | cacheableKeyvStore,
21 | redisKeyvStore,
22 | ],
23 | });
24 |
25 | const cacheableDisconnectSpy = vi.spyOn(cacheableKeyvStore, 'disconnect');
26 | const redisDisconnectSpy = vi.spyOn(redisKeyvStore, 'disconnect');
27 | await cache.disconnect();
28 |
29 | expect(cacheableDisconnectSpy).toBeCalled();
30 | expect(redisDisconnectSpy).toBeCalled();
31 | });
32 | test('error', async () => {
33 | const keyv = new Keyv();
34 | const cache = createCache({
35 | stores: [
36 | keyv,
37 | ],
38 | });
39 | const error = new Error('disconnect error');
40 | keyv.disconnect = () => {
41 | throw error;
42 | };
43 |
44 | await expect(cache.disconnect()).rejects.toThrowError(error);
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/events.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-function */
2 | import {Keyv} from 'keyv';
3 | import {
4 | beforeEach, describe, expect, it, vi,
5 | } from 'vitest';
6 | import {faker} from '@faker-js/faker';
7 | import {createCache} from '../src/index.js';
8 | import {sleep} from './sleep.js';
9 |
10 | describe('events', () => {
11 | let keyv: Keyv;
12 | let cache: ReturnType;
13 | let ttl = 500;
14 | const data = {key: '', value: ''};
15 |
16 | beforeEach(async () => {
17 | data.key = faker.string.alpha(20);
18 | data.value = faker.string.sample();
19 | ttl = faker.number.int({min: 500, max: 1000});
20 | keyv = new Keyv();
21 | cache = createCache({stores: [keyv]});
22 | });
23 |
24 | it('event: set', async () => {
25 | const listener = vi.fn(() => {});
26 | cache.on('set', listener);
27 |
28 | await cache.set(data.key, data.value);
29 | expect(listener).toBeCalledWith(data);
30 |
31 | const error = new Error('set failed');
32 | keyv.set = () => {
33 | throw error;
34 | };
35 |
36 | await expect(cache.set(data.key, data.value)).rejects.toThrowError(error);
37 | expect(listener).toBeCalledWith({key: data.key, value: data.value, error});
38 | });
39 |
40 | it('event: del', async () => {
41 | const listener = vi.fn(() => {});
42 | cache.on('del', listener);
43 |
44 | await cache.set(data.key, data.value);
45 | await cache.del(data.key);
46 | expect(listener).toBeCalledWith({key: data.key});
47 |
48 | const error = new Error('del failed');
49 | keyv.delete = () => {
50 | throw error;
51 | };
52 |
53 | await expect(cache.del(data.key)).rejects.toThrowError(error);
54 | expect(listener).toBeCalledWith({key: data.key, error});
55 | });
56 |
57 | it('event: clear', async () => {
58 | const listener = vi.fn(() => {});
59 | cache.on('clear', listener);
60 |
61 | await cache.set(data.key, data.value);
62 | await cache.clear();
63 | expect(listener).toBeCalled();
64 |
65 | const error = new Error('clear failed');
66 | keyv.clear = () => {
67 | throw error;
68 | };
69 |
70 | await expect(cache.clear()).rejects.toThrowError(error);
71 | expect(listener).toBeCalledWith(error);
72 | });
73 |
74 | it('event: refresh', async () => {
75 | const getValue = () => data.value;
76 | const listener = vi.fn(() => {});
77 | cache.on('refresh', listener);
78 |
79 | const refreshThreshold = ttl / 2;
80 | await cache.wrap(data.key, getValue, ttl, refreshThreshold);
81 | await sleep(ttl - refreshThreshold + 100);
82 | await cache.wrap(data.key, getValue, ttl, refreshThreshold);
83 | await vi.waitUntil(() => listener.mock.calls.length > 0);
84 | expect(listener).toBeCalledWith({key: data.key, value: data.value});
85 | });
86 |
87 | it('event: refresh get error', async () => {
88 | const listener = vi.fn(() => {});
89 | cache.on('refresh', listener);
90 |
91 | const refreshThreshold = ttl / 2;
92 | await cache.wrap(data.key, () => data.value, ttl, refreshThreshold);
93 |
94 | const error = new Error('get failed');
95 | await sleep(ttl - refreshThreshold + 100);
96 | await cache.wrap(
97 | data.key,
98 | () => {
99 | throw error;
100 | },
101 | ttl,
102 | refreshThreshold,
103 | );
104 | await vi.waitUntil(() => listener.mock.calls.length > 0);
105 | expect(listener).toBeCalledWith({key: data.key, value: data.value, error});
106 | });
107 |
108 | it('event: refresh set error', async () => {
109 | const getValue = () => data.value;
110 | const listener = vi.fn(() => {});
111 | cache.on('refresh', listener);
112 |
113 | const refreshThreshold = ttl / 2;
114 | await cache.wrap(data.key, getValue, ttl, refreshThreshold);
115 |
116 | const error = new Error('set failed');
117 | keyv.set = () => {
118 | throw error;
119 | };
120 |
121 | await sleep(ttl - refreshThreshold + 100);
122 | await cache.wrap(data.key, getValue, ttl, refreshThreshold);
123 | await vi.waitUntil(() => listener.mock.calls.length > 0);
124 | expect(listener).toBeCalledWith({key: data.key, value: data.value, error});
125 | });
126 | });
127 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/example.test.ts:
--------------------------------------------------------------------------------
1 | import {describe, expect, test} from 'vitest';
2 | import {Keyv} from 'keyv';
3 | import KeyvRedis from '@keyv/redis';
4 | import {CacheableMemory, KeyvCacheableMemory} from 'cacheable';
5 | import {createCache} from '../src/index.js';
6 |
7 | describe('examples of cache-manager', async () => {
8 | test('set and get with multiple stores', async () => {
9 | // Multiple stores
10 | const cache = createCache({
11 | stores: [
12 | // High performance in-memory cache with LRU and TTL
13 | new Keyv({
14 | store: new CacheableMemory({ttl: 60_000, lruSize: 5000}),
15 | }),
16 |
17 | // Redis Store
18 | new Keyv({
19 | store: new KeyvRedis('redis://localhost:6379'),
20 | }),
21 | ],
22 | });
23 | await cache.set('foo', 'bar');
24 | const value = await cache.get('foo');
25 | expect(value).toBe('bar');
26 | });
27 | test('set and get with KeyvCacheableMemory', async () => {
28 | const cache = createCache({
29 | stores: [
30 | // High performance in-memory cache with LRU and TTL
31 | new Keyv({
32 | store: new KeyvCacheableMemory({ttl: 60_000, lruSize: 5000}),
33 | }),
34 |
35 | // Redis Store
36 | new Keyv({
37 | store: new KeyvRedis('redis://localhost:6379'),
38 | }),
39 | ],
40 | });
41 | await cache.set('foo', 'bar');
42 | const value = await cache.get('foo');
43 | expect(value).toBe('bar');
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/get.test.ts:
--------------------------------------------------------------------------------
1 | import {Keyv} from 'keyv';
2 | import {
3 | beforeEach, describe, expect, it,
4 | } from 'vitest';
5 | import {faker} from '@faker-js/faker';
6 | import {createCache} from '../src/index.js';
7 | import {sleep} from './sleep.js';
8 |
9 | describe('get', () => {
10 | let keyv: Keyv;
11 | let cache: ReturnType;
12 | let ttl = 500;
13 | const data = {key: '', value: ''};
14 |
15 | beforeEach(async () => {
16 | data.key = faker.string.alpha(20);
17 | data.value = faker.string.sample();
18 | ttl = faker.number.int({min: 500, max: 1000});
19 | keyv = new Keyv();
20 | cache = createCache({stores: [keyv]});
21 | });
22 |
23 | it('basic', async () => {
24 | await cache.set(data.key, data.value);
25 | await expect(cache.get(data.key)).resolves.toEqual(data.value);
26 | });
27 |
28 | it('expired', async () => {
29 | await cache.set(data.key, data.value, ttl);
30 | await sleep(ttl + 100);
31 | await expect(cache.get(data.key)).resolves.toBeUndefined();
32 | });
33 |
34 | it('error', async () => {
35 | await cache.set(data.key, data.value);
36 | keyv.get = () => {
37 | throw new Error('get error');
38 | };
39 |
40 | await expect(cache.get(data.key)).resolves.toBeUndefined();
41 | });
42 | it('error on non-blocking enabled', async () => {
43 | const secondKeyv = new Keyv();
44 | keyv.get = () => {
45 | throw new Error('get error');
46 | };
47 |
48 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true});
49 | await cache.set(data.key, data.value);
50 | await expect(cache.get(data.key)).resolves.toBeUndefined();
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/init.test.ts:
--------------------------------------------------------------------------------
1 | import {Keyv} from 'keyv';
2 | import {
3 | beforeEach, describe, expect, it,
4 | } from 'vitest';
5 | import {faker} from '@faker-js/faker';
6 | import {createCache} from '../src/index.js';
7 | import {sleep} from './sleep.js';
8 |
9 | describe('init', () => {
10 | let ttl = 1000;
11 | const data = {key: '', value: ''};
12 |
13 | beforeEach(async () => {
14 | data.key = faker.string.alpha(20);
15 | data.value = faker.string.sample();
16 | ttl = faker.number.int({min: 500, max: 1000});
17 | });
18 |
19 | it('basic', async () => {
20 | const cache = createCache();
21 | expect(cache).toBeDefined();
22 | });
23 |
24 | it('default ttl', async () => {
25 | const cache = createCache({ttl});
26 | await cache.set(data.key, data.value);
27 | await sleep(ttl + 100);
28 | await expect(cache.get(data.key)).resolves.toBeUndefined();
29 | });
30 |
31 | it('single store', async () => {
32 | const cache = createCache({
33 | stores: [new Keyv()],
34 | });
35 | expect(cache).toBeDefined();
36 | });
37 |
38 | it('multiple stores', async () => {
39 | const store1 = new Keyv();
40 | const store2 = new Keyv();
41 | const cache = createCache({
42 | stores: [store1, store2],
43 | });
44 | expect(cache).toBeDefined();
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/keyv-adapter.test.ts:
--------------------------------------------------------------------------------
1 | import {Keyv} from 'keyv';
2 | import {
3 | describe, expect, it, vi,
4 | } from 'vitest';
5 | import {faker} from '@faker-js/faker';
6 | import {redisStore as redisYetStore} from 'cache-manager-redis-yet';
7 | import {createCache, KeyvAdapter, type CacheManagerStore} from '../src/index.js';
8 |
9 | const mockCacheManagerStore: CacheManagerStore = {
10 | name: 'MockCacheManagerStore',
11 | isCacheable: vi.fn((value: unknown) => value !== undefined),
12 | get: vi.fn(async (key: string) => `Value for ${key}`),
13 | mget: vi.fn(async (...keys: string[]) => keys.map(key => `Value for ${key}`)),
14 | set: vi.fn(async (key: string, value: any, ttl?: number) => `Set ${key} to ${value} with TTL ${ttl}`),
15 | mset: vi.fn(async () => undefined),
16 | del: vi.fn(async () => undefined),
17 | mdel: vi.fn(async () => undefined),
18 | ttl: vi.fn(async () => 0),
19 | keys: vi.fn(async () => ['key1', 'key2', 'key3']),
20 | reset: vi.fn(async () => undefined),
21 | on: vi.fn((event: string) => {
22 | console.log(`Event ${event} registered.`);
23 | }),
24 | disconnect: vi.fn(async () => undefined),
25 | };
26 |
27 | describe('keyv-adapter', async () => {
28 | it('able to handle redis yet third party conversion', async () => {
29 | const store = await redisYetStore();
30 | const adapter = new KeyvAdapter(store);
31 | const keyv = new Keyv({store: adapter});
32 | const cache = createCache({stores: [keyv]});
33 | const key = faker.string.alpha(20);
34 | const value = faker.string.sample();
35 | await cache.set(key, value);
36 | const result = await cache.get(key);
37 | expect(result).toEqual(value);
38 | });
39 |
40 | it('returns undefined on get', async () => {
41 | const store = await redisYetStore();
42 | const adapter = new KeyvAdapter(store);
43 | const keyv = new Keyv({store: adapter});
44 | const cache = createCache({stores: [keyv]});
45 | const result = await cache.get('key');
46 | expect(result).toBeUndefined();
47 | });
48 |
49 | it('deletes a key', async () => {
50 | const store = await redisYetStore();
51 | const adapter = new KeyvAdapter(store);
52 | const keyv = new Keyv({store: adapter});
53 | const cache = createCache({stores: [keyv]});
54 | const key = faker.string.alpha(20);
55 | const value = faker.string.sample();
56 | await cache.set(key, value);
57 | const result = await cache.get(key);
58 | expect(result).toEqual(value);
59 | await cache.del(key);
60 | const result2 = await cache.get(key);
61 | expect(result2).toBeUndefined();
62 | });
63 |
64 | it('clears the cache', async () => {
65 | const store = await redisYetStore();
66 | const adapter = new KeyvAdapter(store);
67 | const keyv = new Keyv({store: adapter});
68 | const cache = createCache({stores: [keyv]});
69 | const key = faker.string.alpha(20);
70 | const value = faker.string.sample();
71 | await cache.set(key, value);
72 | const result = await cache.get(key);
73 | expect(result).toEqual(value);
74 | await cache.clear();
75 | const result2 = await cache.get(key);
76 | expect(result2).toBeUndefined();
77 | });
78 |
79 | it('returns false on has', async () => {
80 | const store = await redisYetStore();
81 | const adapter = new KeyvAdapter(store);
82 | const keyv = new Keyv({store: adapter});
83 | const result = await keyv.has('key');
84 | expect(result).toEqual(false);
85 | });
86 |
87 | it('returns true on has', async () => {
88 | const store = await redisYetStore();
89 | const adapter = new KeyvAdapter(store);
90 | const keyv = new Keyv({store: adapter});
91 | const key = faker.string.alpha(20);
92 | const value = faker.string.sample();
93 | await keyv.set(key, value);
94 | const result = await keyv.has(key);
95 | expect(result).toEqual(true);
96 | });
97 |
98 | it('gets many keys', async () => {
99 | const store = await redisYetStore();
100 | const adapter = new KeyvAdapter(store);
101 | const keyv = new Keyv({store: adapter});
102 | const cache = createCache({stores: [keyv]});
103 | const list = [
104 | {key: faker.string.alpha(20), value: faker.string.sample()},
105 | {key: faker.string.alpha(20), value: faker.string.sample()},
106 | ];
107 |
108 | await cache.mset(list);
109 | const keyvResult = await keyv.get(list.map(({key}) => key));
110 | expect(keyvResult).toEqual(list.map(({value}) => value));
111 | const result = await cache.mget(list.map(({key}) => key));
112 | expect(result).toEqual([list[0].value, list[1].value]);
113 | });
114 |
115 | it('should delete many keys', async () => {
116 | const store = await redisYetStore();
117 | const adapter = new KeyvAdapter(store);
118 | const keyv = new Keyv({store: adapter});
119 | const list = [
120 | {key: faker.string.alpha(20), value: faker.string.sample()},
121 | {key: faker.string.alpha(20), value: faker.string.sample()},
122 | ];
123 |
124 | await keyv.set(list[0].key, list[0].value);
125 | await keyv.set(list[1].key, list[1].value);
126 | await keyv.delete(list.map(({key}) => key));
127 | const result = await keyv.get(list.map(({key}) => key));
128 | expect(result).toEqual([undefined, undefined]);
129 | });
130 |
131 | it('should disconnect', async () => {
132 | // Store without disconnect
133 | const storeNoDisconnect = await redisYetStore();
134 | const adapterNoDisconnect = new KeyvAdapter(storeNoDisconnect);
135 | const keyvNoDisconnect = new Keyv({store: adapterNoDisconnect});
136 |
137 | await keyvNoDisconnect.disconnect();
138 |
139 | // Store with disconnect
140 | const adapterWithDisconnect = new KeyvAdapter(mockCacheManagerStore);
141 | const keyvWithDisconnect = new Keyv({store: adapterWithDisconnect});
142 |
143 | await keyvWithDisconnect.disconnect();
144 | expect(mockCacheManagerStore.disconnect).toBeCalled();
145 | });
146 | });
147 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/mdel.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-floating-promises, promise/prefer-await-to-then */
2 | import {Keyv} from 'keyv';
3 | import {
4 | beforeEach, describe, expect, it, vi,
5 | } from 'vitest';
6 | import {faker} from '@faker-js/faker';
7 | import {createCache} from '../src/index.js';
8 | import {sleep} from './sleep.js';
9 |
10 | describe('mdel', () => {
11 | let keyv: Keyv;
12 | let cache: ReturnType;
13 | let ttl = 500;
14 | let list = [] as Array<{key: string; value: string}>;
15 |
16 | beforeEach(async () => {
17 | ttl = faker.number.int({min: 500, max: 1000});
18 | keyv = new Keyv();
19 | cache = createCache({stores: [keyv]});
20 | list = [
21 | {key: faker.string.alpha(20), value: faker.string.sample()},
22 | {key: faker.string.alpha(20), value: faker.string.sample()},
23 | {key: faker.string.alpha(20), value: faker.string.sample()},
24 | ];
25 | });
26 |
27 | it('basic', async () => {
28 | await cache.mset(list);
29 | await expect(cache.get(list[0].key)).resolves.toEqual(list[0].value);
30 | await expect(cache.get(list[1].key)).resolves.toEqual(list[1].value);
31 | await expect(cache.get(list[2].key)).resolves.toEqual(list[2].value);
32 | await cache.mdel([list[0].key, list[1].key]);
33 | await expect(cache.get(list[0].key)).resolves.toBeUndefined();
34 | await expect(cache.get(list[1].key)).resolves.toBeUndefined();
35 | await expect(cache.get(list[2].key)).resolves.toEqual(list[2].value);
36 | });
37 |
38 | it('should work blocking', async () => {
39 | let resolveDeleted: (value: boolean) => void = () => undefined;
40 | const deletePromise = new Promise(_resolve => {
41 | resolveDeleted = _resolve;
42 | });
43 | const cache = createCache({stores: [keyv], nonBlocking: false});
44 | await cache.mset(list);
45 |
46 | const delHandler = vi.spyOn(keyv, 'delete').mockReturnValue(deletePromise);
47 | const deleteResolved = vi.fn();
48 | const deleteRejected = vi.fn();
49 | cache.mdel(list.map(({key}) => key)).catch(deleteRejected).then(deleteResolved);
50 |
51 | expect(delHandler).toBeCalledTimes(list.length);
52 |
53 | await sleep(200);
54 |
55 | expect(deleteResolved).not.toBeCalled();
56 | expect(deleteRejected).not.toBeCalled();
57 |
58 | resolveDeleted(true);
59 | await sleep(1);
60 |
61 | expect(deleteResolved).toBeCalled();
62 | expect(deleteRejected).not.toBeCalled();
63 | });
64 |
65 | it('should work non-blocking', async () => {
66 | const deletePromise = new Promise(_resolve => {
67 | // Do nothing, this will be a never resolved promise
68 | });
69 | const cache = createCache({stores: [keyv], nonBlocking: true});
70 | await cache.mset(list);
71 |
72 | const delHandler = vi.spyOn(keyv, 'delete').mockReturnValue(deletePromise);
73 | const deleteResolved = vi.fn();
74 | const deleteRejected = vi.fn();
75 | cache.mdel(list.map(({key}) => key)).catch(deleteRejected).then(deleteResolved);
76 |
77 | expect(delHandler).toBeCalledTimes(list.length);
78 |
79 | await sleep(1);
80 |
81 | expect(deleteResolved).toBeCalled();
82 | expect(deleteRejected).not.toBeCalled();
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/mget.test.ts:
--------------------------------------------------------------------------------
1 | import {Keyv} from 'keyv';
2 | import {
3 | beforeEach, describe, expect, it,
4 | } from 'vitest';
5 | import {faker} from '@faker-js/faker';
6 | import {createCache} from '../src/index.js';
7 |
8 | describe('mget', () => {
9 | let keyv: Keyv;
10 | let cache: ReturnType;
11 | let ttl = 500;
12 | let list = [] as Array<{key: string; value: string}>;
13 |
14 | beforeEach(async () => {
15 | ttl = faker.number.int({min: 500, max: 1000});
16 | keyv = new Keyv();
17 | cache = createCache({stores: [keyv]});
18 | list = [
19 | {key: faker.string.alpha(20), value: faker.string.sample()},
20 | {key: faker.string.alpha(20), value: faker.string.sample()},
21 | {key: faker.string.alpha(20), value: faker.string.sample()},
22 | ];
23 | });
24 |
25 | it('basic', async () => {
26 | await cache.mset(list);
27 | const keys = list.map(item => item.key);
28 | const values = list.map(item => item.value);
29 | await expect(cache.mget(keys)).resolves.toEqual(values);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/mset.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-floating-promises, promise/prefer-await-to-then */
2 | import {Keyv} from 'keyv';
3 | import {
4 | beforeEach, describe, expect, it, vi,
5 | } from 'vitest';
6 | import {faker} from '@faker-js/faker';
7 | import {createCache} from '../src/index.js';
8 | import {sleep} from './sleep.js';
9 |
10 | describe('mset', () => {
11 | let keyv: Keyv;
12 | let cache: ReturnType;
13 | let ttl = 500;
14 |
15 | beforeEach(async () => {
16 | ttl = faker.number.int({min: 500, max: 1000});
17 | keyv = new Keyv();
18 | cache = createCache({stores: [keyv]});
19 | });
20 |
21 | it('basic', async () => {
22 | const list = [
23 | {key: faker.string.alpha(20), value: faker.string.sample()},
24 | {key: faker.string.alpha(20), value: faker.string.sample()},
25 | {key: faker.string.alpha(20), value: faker.string.sample()},
26 | ];
27 |
28 | await expect(cache.mset(list)).resolves.toEqual(list);
29 | await expect(cache.get(list[0].key)).resolves.toEqual(list[0].value);
30 | });
31 |
32 | it('should work blocking', async () => {
33 | const list = [
34 | {key: faker.string.alpha(20), value: faker.string.sample()},
35 | {key: faker.string.alpha(20), value: faker.string.sample()},
36 | {key: faker.string.alpha(20), value: faker.string.sample()},
37 | ];
38 |
39 | let resolveSet: (value: boolean) => void = () => undefined;
40 | const setPromise = new Promise(_resolve => {
41 | resolveSet = _resolve;
42 | });
43 |
44 | const cache = createCache({stores: [keyv], nonBlocking: false});
45 | const setHandler = vi.spyOn(keyv, 'set').mockReturnValue(setPromise);
46 | const setResolved = vi.fn();
47 | const setRejected = vi.fn();
48 | cache.mset(list).catch(setRejected).then(setResolved);
49 |
50 | expect(setHandler).toBeCalledTimes(list.length);
51 |
52 | await sleep(200);
53 |
54 | expect(setResolved).not.toBeCalled();
55 | expect(setRejected).not.toBeCalled();
56 |
57 | resolveSet(true);
58 | await sleep(1);
59 |
60 | expect(setResolved).toBeCalled();
61 | expect(setRejected).not.toBeCalled();
62 | });
63 |
64 | it('should work non-blocking', async () => {
65 | const list = [
66 | {key: faker.string.alpha(20), value: faker.string.sample()},
67 | {key: faker.string.alpha(20), value: faker.string.sample()},
68 | {key: faker.string.alpha(20), value: faker.string.sample()},
69 | ];
70 |
71 | const setPromise = new Promise(_resolve => {
72 | // Do nothing, this will be a never resolved promise
73 | });
74 |
75 | const cache = createCache({stores: [keyv], nonBlocking: true});
76 | const setHandler = vi.spyOn(keyv, 'set').mockReturnValue(setPromise);
77 | const setResolved = vi.fn();
78 | const setRejected = vi.fn();
79 | cache.mset(list).catch(setRejected).then(setResolved);
80 |
81 | expect(setHandler).toBeCalledTimes(list.length);
82 |
83 | await sleep(1);
84 |
85 | expect(setResolved).toBeCalled();
86 | expect(setRejected).not.toBeCalled();
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/multiple-stores.test.ts:
--------------------------------------------------------------------------------
1 | import {Keyv} from 'keyv';
2 | import {
3 | beforeEach, describe, expect, it, vi,
4 | } from 'vitest';
5 | import {faker} from '@faker-js/faker';
6 | import {createCache} from '../src/index.js';
7 | import {sleep} from './sleep.js';
8 |
9 | describe('multiple stores', () => {
10 | let keyv1: Keyv;
11 | let keyv2: Keyv;
12 | let cache: ReturnType;
13 | let ttl = 500;
14 | const data = {key: '', value: ''};
15 |
16 | beforeEach(async () => {
17 | data.key = faker.string.alpha(20);
18 | data.value = faker.string.sample();
19 | ttl = faker.number.int({min: 500, max: 1000});
20 | keyv1 = new Keyv();
21 | keyv2 = new Keyv();
22 | cache = createCache({stores: [keyv1, keyv2]});
23 | });
24 |
25 | it('set', async () => {
26 | await cache.set(data.key, data.value, ttl);
27 | await expect(keyv1.get(data.key)).resolves.toEqual(data.value);
28 | await expect(keyv2.get(data.key)).resolves.toEqual(data.value);
29 | await expect(cache.get(data.key)).resolves.toEqual(data.value);
30 | });
31 |
32 | it('get - 1 store error', async () => {
33 | await cache.set(data.key, data.value, ttl);
34 |
35 | keyv1.get = () => {
36 | throw new Error('store 1 get error');
37 | };
38 |
39 | await expect(cache.get(data.key)).resolves.toEqual(data.value);
40 | });
41 |
42 | it('get - 2 stores error', async () => {
43 | await cache.set(data.key, data.value, ttl);
44 |
45 | const getError = () => {
46 | throw new Error('store 1 get error');
47 | };
48 |
49 | keyv1.get = getError;
50 | keyv2.get = getError;
51 |
52 | await expect(cache.get(data.key)).resolves.toBeUndefined();
53 | });
54 |
55 | it('del', async () => {
56 | await cache.set(data.key, data.value, ttl);
57 |
58 | await expect(keyv1.get(data.key)).resolves.toEqual(data.value);
59 | await expect(keyv2.get(data.key)).resolves.toEqual(data.value);
60 |
61 | await cache.del(data.key);
62 |
63 | await expect(keyv1.get(data.key)).resolves.toBeUndefined();
64 | await expect(keyv2.get(data.key)).resolves.toBeUndefined();
65 | });
66 |
67 | it('wrap', async () => {
68 | await cache.wrap(data.key, () => data.value, ttl);
69 |
70 | await expect(keyv1.get(data.key)).resolves.toEqual(data.value);
71 | await expect(keyv2.get(data.key)).resolves.toEqual(data.value);
72 |
73 | // Store 1 get error
74 | keyv1.get = () => {
75 | throw new Error('store 1 get error');
76 | };
77 |
78 | // eslint-disable-next-line @typescript-eslint/no-empty-function
79 | const listener = vi.fn(() => {});
80 | cache.on('set', listener);
81 |
82 | await expect(cache.wrap(data.key, () => data.value, ttl)).resolves.toEqual(data.value);
83 | await vi.waitUntil(() => listener.mock.calls.length > 0);
84 | expect(listener).toBeCalledWith({key: data.key, value: data.value});
85 | });
86 |
87 | it('wrap - refresh', async () => {
88 | const refreshThreshold = ttl / 2;
89 | await cache.wrap(data.key, () => data.value, ttl, refreshThreshold);
90 |
91 | await expect(keyv1.get(data.key)).resolves.toEqual(data.value);
92 | await expect(keyv2.get(data.key)).resolves.toEqual(data.value);
93 |
94 | // Store 1 get error
95 | const getOk = keyv1.get;
96 | const getError = () => {
97 | throw new Error('store 1 get error');
98 | };
99 |
100 | keyv1.get = getError;
101 |
102 | await sleep(ttl - refreshThreshold + 100);
103 |
104 | await expect(cache.wrap(data.key, () => 'new', ttl, refreshThreshold)).resolves.toEqual(data.value);
105 |
106 | keyv1.get = getOk;
107 | // Store 1 has been updated the latest value
108 | await expect(keyv1.get(data.key)).resolves.toEqual('new');
109 | await expect(cache.wrap(data.key, () => 'latest', ttl)).resolves.toEqual('new');
110 | });
111 | });
112 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/set.test.ts:
--------------------------------------------------------------------------------
1 | import {Keyv} from 'keyv';
2 | import {
3 | beforeEach, describe, expect, it,
4 | } from 'vitest';
5 | import {faker} from '@faker-js/faker';
6 | import {createCache} from '../src/index.js';
7 | import {sleep} from './sleep.js';
8 |
9 | describe('set', () => {
10 | let keyv: Keyv;
11 | let cache: ReturnType;
12 | let ttl = 500;
13 | const data = {key: '', value: ''};
14 |
15 | beforeEach(async () => {
16 | data.key = faker.string.alpha(20);
17 | data.value = faker.string.sample();
18 | ttl = faker.number.int({min: 500, max: 1000});
19 | keyv = new Keyv();
20 | cache = createCache({stores: [keyv]});
21 | });
22 |
23 | it('basic', async () => {
24 | await expect(cache.set(data.key, data.value)).resolves.toEqual(data.value);
25 | await expect(cache.set(data.key, data.value, ttl)).resolves.toEqual(data.value);
26 | await expect(cache.get(data.key)).resolves.toEqual(data.value);
27 | });
28 |
29 | it('error', async () => {
30 | const error = new Error('set error');
31 | keyv.set = () => {
32 | throw error;
33 | };
34 |
35 | await expect(cache.set(data.key, data.value)).rejects.toThrowError(error);
36 | await expect(cache.get(data.key)).resolves.toBeUndefined();
37 | });
38 | it('set should be non-blocking', async () => {
39 | const secondKeyv = new Keyv();
40 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true});
41 | await cache.set(data.key, data.value);
42 | await sleep(300);
43 | await expect(cache.get(data.key)).resolves.toEqual(data.value);
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/sleep.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable promise/param-names, no-promise-executor-return */
2 | export const sleep = async (ms: number) => new Promise(r => setTimeout(r, ms));
3 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/stores.test.ts:
--------------------------------------------------------------------------------
1 | import {Keyv} from 'keyv';
2 | import {createKeyv} from '@keyv/redis';
3 | import {
4 | describe, expect, it,
5 | } from 'vitest';
6 | import {simpleFaker} from '@faker-js/faker';
7 | import {createCache} from '../src/index.js';
8 |
9 | describe('stores', () => {
10 | it('can get the keyv store', () => {
11 | const cache = createCache();
12 | expect(cache.stores.length).toEqual(1);
13 | });
14 |
15 | it('can see multiple stores', () => {
16 | const keyv = new Keyv();
17 | const redis = createKeyv();
18 | const cache = createCache({stores: [keyv, redis]});
19 | expect(cache.stores.length).toEqual(2);
20 | expect(cache.stores[0]).toEqual(keyv);
21 | expect(cache.stores[1]).toEqual(redis);
22 | });
23 |
24 | it('can get the keyv store and do iterator', async () => {
25 | const cache = createCache();
26 | expect(cache.stores.length).toEqual(1);
27 | const keyName = simpleFaker.string.uuid();
28 | const keyValue = simpleFaker.string.uuid();
29 | await cache.set(keyName, keyValue);
30 | const keyv = cache.stores[0];
31 | expect(keyv).toBeInstanceOf(Keyv);
32 |
33 | let returnValue;
34 |
35 | if (keyv?.iterator) {
36 | for await (const [key, value] of keyv.iterator({})) {
37 | if (key === keyName) {
38 | returnValue = value;
39 | }
40 | }
41 | }
42 |
43 | expect(returnValue).toEqual(keyValue);
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/packages/cache-manager/test/ttl.test.ts:
--------------------------------------------------------------------------------
1 | import {Keyv} from 'keyv';
2 | import {
3 | beforeEach, describe, expect, it,
4 | } from 'vitest';
5 | import {faker} from '@faker-js/faker';
6 | import {createCache} from '../src/index.js';
7 | import {sleep} from './sleep.js';
8 |
9 | describe('get', () => {
10 | let keyv: Keyv;
11 | let cache: ReturnType;
12 | let ttl = 500;
13 | const data = {key: '', value: ''};
14 |
15 | beforeEach(async () => {
16 | data.key = faker.string.alpha(20);
17 | data.value = faker.string.sample();
18 | ttl = faker.number.int({min: 500, max: 1000});
19 | keyv = new Keyv();
20 | cache = createCache({stores: [keyv]});
21 | });
22 |
23 | it('basic', async () => {
24 | await cache.set(data.key, data.value);
25 | await expect(cache.ttl(data.key)).resolves.toBeUndefined();
26 | });
27 |
28 | it('expired', async () => {
29 | await cache.set(data.key, data.value, ttl);
30 | await sleep(ttl + 100);
31 | await expect(cache.ttl(data.key)).resolves.toBeUndefined();
32 | });
33 |
34 | it('error', async () => {
35 | await cache.set(data.key, data.value);
36 | keyv.get = () => {
37 | throw new Error('get error');
38 | };
39 |
40 | await expect(cache.ttl(data.key)).resolves.toBeUndefined();
41 | });
42 | it('error on non-blocking enabled', async () => {
43 | const secondKeyv = new Keyv();
44 | keyv.get = () => {
45 | throw new Error('get error');
46 | };
47 |
48 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true});
49 | await cache.set(data.key, data.value);
50 | await expect(cache.ttl(data.key)).resolves.toBeUndefined();
51 | });
52 | it('gets the expiration of a key', async () => {
53 | await cache.set(data.key, data.value, ttl);
54 | const expiration = Date.now() + ttl;
55 | await expect(cache.ttl(data.key)).resolves.toBeGreaterThanOrEqual(expiration - 100);
56 | });
57 |
58 | it('gets the expiration of a key with nonBlocking', async () => {
59 | const secondKeyv = new Keyv();
60 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true});
61 | await cache.set(data.key, data.value, ttl);
62 | const expiration = Date.now() + ttl;
63 | await expect(cache.ttl(data.key)).resolves.toBeGreaterThanOrEqual(expiration - 100);
64 | });
65 |
66 | it('gets null of a key with nonBlocking', async () => {
67 | const secondKeyv = new Keyv();
68 | const cache = createCache({stores: [keyv, secondKeyv], nonBlocking: true});
69 | await expect(cache.ttl('non-block-bad-key1')).resolves.toBeUndefined();
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/packages/cache-manager/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
7 |
8 | /* Emit */
9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */
11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */
12 |
13 | /* Interop Constraints */
14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
16 |
17 | /* Type Checking */
18 | "strict": true, /* Enable all strict type-checking options. */
19 |
20 | /* Completeness */
21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */
22 | "lib": [
23 | "ESNext", "DOM"
24 | ]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/cache-manager/vite.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | coverage: {
6 | reporter: ['json', 'text'],
7 | exclude: ['test', 'vite.config.ts', 'dist', 'node_modules'],
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/packages/cacheable-request/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License & © Jared Wray
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to
5 | deal in the Software without restriction, including without limitation the
6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | sell copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/packages/cacheable-request/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cacheable-request",
3 | "version": "13.0.7",
4 | "description": "Wrap native HTTP requests with RFC compliant cache support",
5 | "license": "MIT",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/jaredwray/cacheable.git",
9 | "directory": "packages/cacheable-request"
10 | },
11 | "author": "Jared Wray (http://jaredwray.com)",
12 | "type": "module",
13 | "exports": "./dist/index.js",
14 | "types": "./dist/index.d.ts",
15 | "engines": {
16 | "node": ">=18"
17 | },
18 | "scripts": {
19 | "test": "xo --fix && vitest run --coverage",
20 | "test:ci": "xo && vitest run",
21 | "prepublish": "pnpm run build",
22 | "build": "rimraf ./dist && tsc --project tsconfig.build.json",
23 | "clean": "rimraf node_modules ./coverage ./test/testdb.sqlite ./dist"
24 | },
25 | "files": [
26 | "dist",
27 | "LICENSE"
28 | ],
29 | "keywords": [
30 | "HTTP",
31 | "HTTPS",
32 | "cache",
33 | "caching",
34 | "layer",
35 | "cacheable",
36 | "RFC 7234",
37 | "RFC",
38 | "7234",
39 | "compliant"
40 | ],
41 | "dependenciesComments": {
42 | "@types/http-cache-semantics": "It needs to be in the dependencies list and not devDependencies because otherwise projects that use this one will be getting `Could not find a declaration file for module 'http-cache-semantics'` error when running `tsc`, see https://github.com/jaredwray/cacheable-request/issues/194 for details"
43 | },
44 | "dependencies": {
45 | "@types/http-cache-semantics": "^4.0.4",
46 | "get-stream": "^9.0.1",
47 | "http-cache-semantics": "^4.2.0",
48 | "keyv": "^5.3.3",
49 | "mimic-response": "^4.0.0",
50 | "normalize-url": "^8.0.1",
51 | "responselike": "^3.0.0"
52 | },
53 | "devDependencies": {
54 | "@keyv/sqlite": "^4.0.4",
55 | "@types/node": "^22.15.30",
56 | "@types/responselike": "^1.0.3",
57 | "@vitest/coverage-v8": "^3.2.2",
58 | "body-parser": "^2.2.0",
59 | "delay": "^6.0.0",
60 | "express": "^4.21.2",
61 | "pify": "^6.1.0",
62 | "rimraf": "^6.0.1",
63 | "sqlite3": "^5.1.7",
64 | "tsup": "^8.5.0",
65 | "typescript": "^5.8.3",
66 | "vitest": "^3.2.2",
67 | "xo": "^1.1.0"
68 | },
69 | "xo": {
70 | "rules": {
71 | "@typescript-eslint/triple-slash-reference": 0,
72 | "@typescript-eslint/no-namespace": 0,
73 | "@typescript-eslint/no-unsafe-assignment": 0,
74 | "@typescript-eslint/no-unsafe-call": 0,
75 | "@typescript-eslint/ban-types": 0,
76 | "@typescript-eslint/restrict-template-expressions": 0,
77 | "@typescript-eslint/no-unsafe-return": 0,
78 | "@typescript-eslint/no-unsafe-argument": 0,
79 | "new-cap": 0,
80 | "unicorn/no-abusive-eslint-disable": 0,
81 | "@typescript-eslint/restrict-plus-operands": 0,
82 | "@typescript-eslint/no-implicit-any-catch": 0,
83 | "@typescript-eslint/consistent-type-imports": 0,
84 | "@typescript-eslint/consistent-type-definitions": 0,
85 | "@typescript-eslint/prefer-nullish-coalescing": 0,
86 | "n/prefer-global/url": 0,
87 | "n/no-deprecated-api": 0,
88 | "unicorn/prefer-event-target": 0,
89 | "@typescript-eslint/no-unnecessary-type-assertion": 0,
90 | "promise/prefer-await-to-then": 0,
91 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": 0
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/packages/cacheable-request/src/types.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for cacheable-request 6.0
2 | // Project: https://github.com/lukechilds/cacheable-request#readme
3 | // Definitions by: BendingBender
4 | // Paul Melnikow
5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
6 | // TypeScript Version: 2.3
7 |
8 | ///
9 |
10 | import {
11 | request, RequestOptions, ClientRequest, ServerResponse,
12 | } from 'node:http';
13 | import {URL} from 'node:url';
14 | import {EventEmitter} from 'node:events';
15 | import {Buffer} from 'node:buffer';
16 | import ResponseLike from 'responselike';
17 | import {CachePolicyObject} from 'http-cache-semantics';
18 |
19 | export type RequestFn = typeof request;
20 | export type RequestFunction = typeof request;
21 | export type CacheResponse = ServerResponse | typeof ResponseLike;
22 |
23 | export type CacheableRequestFunction = (
24 | options: CacheableOptions,
25 | callback?: (response: CacheResponse) => void
26 | ) => Emitter;
27 |
28 | export type CacheableOptions = Options & RequestOptions | string | URL;
29 |
30 | export interface Options {
31 | /**
32 | * If the cache should be used. Setting this to `false` will completely bypass the cache for the current request.
33 | * @default true
34 | */
35 | cache?: boolean | undefined;
36 |
37 | /**
38 | * If set to `true` once a cached resource has expired it is deleted and will have to be re-requested.
39 | *
40 | * If set to `false`, after a cached resource's TTL expires it is kept in the cache and will be revalidated
41 | * on the next request with `If-None-Match`/`If-Modified-Since` headers.
42 | * @default false
43 | */
44 | strictTtl?: boolean | undefined;
45 |
46 | /**
47 | * Limits TTL. The `number` represents milliseconds.
48 | * @default undefined
49 | */
50 | maxTtl?: number | undefined;
51 |
52 | /**
53 | * When set to `true`, if the DB connection fails we will automatically fallback to a network request.
54 | * DB errors will still be emitted to notify you of the problem even though the request callback may succeed.
55 | * @default false
56 | */
57 | automaticFailover?: boolean | undefined;
58 |
59 | /**
60 | * Forces refreshing the cache. If the response could be retrieved from the cache, it will perform a
61 | * new request and override the cache instead.
62 | * @default false
63 | */
64 | forceRefresh?: boolean | undefined;
65 | remoteAddress?: boolean | undefined;
66 |
67 | url?: string | undefined;
68 |
69 | headers?: Record;
70 |
71 | // eslint-disable-next-line @typescript-eslint/no-restricted-types
72 | body?: Buffer;
73 | }
74 |
75 | export interface CacheValue extends Record {
76 | url: string;
77 | statusCode: number;
78 | // eslint-disable-next-line @typescript-eslint/no-restricted-types
79 | body: Buffer | string;
80 | cachePolicy: CachePolicyObject;
81 | }
82 |
83 | export interface Emitter extends EventEmitter {
84 | addListener(event: 'request', listener: (request: ClientRequest) => void): this;
85 | addListener(
86 | event: 'response',
87 | listener: (response: CacheResponse) => void
88 | ): this;
89 | addListener(event: 'error', listener: (error: RequestError | CacheError) => void): this;
90 | on(event: 'request', listener: (request: ClientRequest) => void): this;
91 | on(event: 'response', listener: (response: CacheResponse) => void): this;
92 | on(event: 'error', listener: (error: RequestError | CacheError) => void): this;
93 | once(event: 'request', listener: (request: ClientRequest) => void): this;
94 | once(event: 'response', listener: (response: CacheResponse) => void): this;
95 | once(event: 'error', listener: (error: RequestError | CacheError) => void): this;
96 | prependListener(event: 'request', listener: (request: ClientRequest) => void): this;
97 | prependListener(
98 | event: 'response',
99 | listener: (response: CacheResponse) => void
100 | ): this;
101 | prependListener(event: 'error', listener: (error: RequestError | CacheError) => void): this;
102 | prependOnceListener(event: 'request', listener: (request: ClientRequest) => void): this;
103 | prependOnceListener(
104 | event: 'response',
105 | listener: (response: CacheResponse) => void
106 | ): this;
107 | prependOnceListener(
108 | event: 'error',
109 | listener: (error: RequestError | CacheError) => void
110 | ): this;
111 | removeListener(event: 'request', listener: (request: ClientRequest) => void): this;
112 | removeListener(
113 | event: 'response',
114 | listener: (response: CacheResponse) => void
115 | ): this;
116 | removeListener(event: 'error', listener: (error: RequestError | CacheError) => void): this;
117 | off(event: 'request', listener: (request: ClientRequest) => void): this;
118 | off(event: 'response', listener: (response: CacheResponse) => void): this;
119 | off(event: 'error', listener: (error: RequestError | CacheError) => void): this;
120 | removeAllListeners(event?: 'request' | 'response' | 'error'): this;
121 | listeners(event: 'request'): Array<(request: ClientRequest) => void>;
122 | listeners(event: 'response'): Array<(response: CacheResponse) => void>;
123 | listeners(event: 'error'): Array<(error: RequestError | CacheError) => void>;
124 | rawListeners(event: 'request'): Array<(request: ClientRequest) => void>;
125 | rawListeners(event: 'response'): Array<(response: CacheResponse) => void>;
126 | rawListeners(event: 'error'): Array<(error: RequestError | CacheError) => void>;
127 | emit(event: 'request', request: ClientRequest): boolean;
128 | emit(event: 'response', response: CacheResponse): boolean;
129 | emit(event: 'error', error: RequestError | CacheError): boolean;
130 | eventNames(): Array<'request' | 'response' | 'error'>;
131 | listenerCount(type: 'request' | 'response' | 'error'): number;
132 | }
133 |
134 | export class RequestError extends Error {
135 | constructor(error: Error) {
136 | super(error.message);
137 | Object.defineProperties(this, Object.getOwnPropertyDescriptors(error));
138 | }
139 | }
140 |
141 | export class CacheError extends Error {
142 | constructor(error: Error) {
143 | super(error.message);
144 | Object.defineProperties(this, Object.getOwnPropertyDescriptors(error));
145 | }
146 | }
147 |
148 | export interface UrlOption {
149 | path: string;
150 | pathname?: string;
151 | search?: string;
152 | }
153 |
--------------------------------------------------------------------------------
/packages/cacheable-request/test/cacheable-request-class.test.ts:
--------------------------------------------------------------------------------
1 | import {request} from 'node:http';
2 | import {test, expect} from 'vitest';
3 | import {Keyv} from 'keyv';
4 | import CacheableRequest from '../src/index.js';
5 |
6 | test('CacheableRequest is a function', () => {
7 | expect(typeof CacheableRequest).toBe('function');
8 | });
9 | test('CacheableRequest accepts Keyv instance', () => {
10 | expect(() => new CacheableRequest(request, new Keyv())).not.toThrow();
11 | });
12 |
13 | test('CacheableRequest should accept hook', () => {
14 | const cacheableRequest = new CacheableRequest(request);
15 | cacheableRequest.addHook('response', (response: any) => response);
16 | expect(cacheableRequest.getHook('response')).not.toBeUndefined();
17 | expect(cacheableRequest.getHook('not')).toBeUndefined();
18 | });
19 |
20 | test('CacheableRequest should remove hook', () => {
21 | const cacheableRequest = new CacheableRequest(request);
22 | cacheableRequest.addHook('response', (response: any) => response);
23 | expect(cacheableRequest.getHook('response')).not.toBeUndefined();
24 | cacheableRequest.removeHook('response');
25 | expect(cacheableRequest.getHook('response')).toBeUndefined();
26 | });
27 |
28 | test('CacheableRequest should run hook', async () => {
29 | const cacheableRequest = new CacheableRequest(request);
30 | cacheableRequest.addHook('response', (response: any) => response);
31 | expect(cacheableRequest.getHook('response')).not.toBeUndefined();
32 | const value = await cacheableRequest.runHook('response', 10);
33 | expect(value).toBe(10);
34 | });
35 |
--------------------------------------------------------------------------------
/packages/cacheable-request/test/create-test-server/index.mjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | 'use strict';
3 |
4 | import http from 'node:http';
5 | import express from 'express';
6 | import pify from 'pify';
7 | import bodyParser from 'body-parser';
8 |
9 | const createTestServer = (opts = {}) => {
10 | const server = express();
11 | server.http = http.createServer(server);
12 |
13 | server.set('etag', false);
14 |
15 | if (opts.bodyParser !== false) {
16 | server.use(bodyParser.json(Object.assign({ limit: '1mb', type: 'application/json' }, opts.bodyParser)));
17 | server.use(bodyParser.text(Object.assign({ limit: '1mb', type: 'text/plain' }, opts.bodyParser)));
18 | server.use(bodyParser.urlencoded(Object.assign({ limit: '1mb', type: 'application/x-www-form-urlencoded', extended: true }, opts.bodyParser)));
19 | server.use(bodyParser.raw(Object.assign({ limit: '1mb', type: 'application/octet-stream' }, opts.bodyParser)));
20 | }
21 |
22 | const send = fn => (req, res, next) => {
23 | const cb = typeof fn === 'function' ? fn(req, res, next) : fn;
24 |
25 | Promise.resolve(cb).then(val => {
26 | if (val) {
27 | /* c8 ignore next 3 */
28 | res.send(val);
29 | }
30 | });
31 | };
32 |
33 | const get = server.get.bind(server);
34 | server.get = function () {
35 | const [path, ...handlers] = [...arguments];
36 |
37 | for (const handler of handlers) {
38 | get(path, send(handler));
39 | }
40 | };
41 |
42 | server.listen = () => Promise.all([
43 | pify(server.http.listen.bind(server.http))().then(() => {
44 | server.port = server.http.address().port;
45 | server.url = `http://localhost:${server.port}`;
46 | })
47 | ]);
48 |
49 | server.close = () => Promise.all([
50 | pify(server.http.close.bind(server.http))().then(() => {
51 | server.port = undefined;
52 | server.url = undefined;
53 | })
54 | ]);
55 |
56 | return server.listen().then(() => server);
57 | };
58 |
59 | export default createTestServer;
--------------------------------------------------------------------------------
/packages/cacheable-request/test/dependencies.test.ts:
--------------------------------------------------------------------------------
1 | import {readFileSync} from 'node:fs';
2 | import {test, expect} from 'vitest';
3 |
4 | test('@types/http-cache-semantics is a regular (not dev) dependency', () => {
5 | // Required to avoid `Could not find a declaration file for module 'http-cache-semantics'` error from `tsc` when using this package in other projects
6 |
7 | // Arrange
8 | const packageJsonContents = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
9 |
10 | // Assert
11 | expect(packageJsonContents).toHaveProperty('dependencies.@types/http-cache-semantics');
12 | });
13 |
--------------------------------------------------------------------------------
/packages/cacheable-request/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src/**/*"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/cacheable-request/vitest.config.mjs:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | coverage: {
6 | exclude: [
7 | 'site/docula.config.cjs',
8 | 'site-output/**',
9 | '.pnp.*',
10 | '.yarn/**',
11 | 'test/**',
12 | 'vitest.config.mjs',
13 | 'dist/**',
14 | ],
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/packages/cacheable/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License & © Jared Wray
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to
5 | deal in the Software without restriction, including without limitation the
6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | sell copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/packages/cacheable/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cacheable",
3 | "version": "1.10.0",
4 | "description": "High Performance Layer 1 / Layer 2 Caching with Keyv Storage",
5 | "type": "module",
6 | "main": "./dist/index.cjs",
7 | "module": "./dist/index.js",
8 | "types": "./dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "require": "./dist/index.cjs",
12 | "import": "./dist/index.js"
13 | }
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/jaredwray/cacheable.git",
18 | "directory": "packages/cacheable"
19 | },
20 | "author": "Jared Wray ",
21 | "license": "MIT",
22 | "private": false,
23 | "scripts": {
24 | "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
25 | "prepublish": "pnpm build",
26 | "test": "xo --fix && vitest run --coverage",
27 | "test:ci": "xo && vitest run",
28 | "clean": "rimraf ./dist ./coverage ./node_modules"
29 | },
30 | "devDependencies": {
31 | "@faker-js/faker": "^9.7.0",
32 | "@keyv/redis": "^4.4.0",
33 | "@types/node": "^22.15.3",
34 | "@vitest/coverage-v8": "^3.1.3",
35 | "lru-cache": "^11.1.0",
36 | "rimraf": "^6.0.1",
37 | "tsup": "^8.4.0",
38 | "typescript": "^5.8.3",
39 | "vitest": "^3.1.3",
40 | "xo": "^0.60.0"
41 | },
42 | "dependencies": {
43 | "hookified": "^1.8.2",
44 | "keyv": "^5.3.3"
45 | },
46 | "keywords": [
47 | "cacheable",
48 | "high performance",
49 | "layer 1 caching",
50 | "layer 2 caching",
51 | "distributed caching",
52 | "Keyv storage engine",
53 | "memory caching",
54 | "LRU cache",
55 | "expiration",
56 | "CacheableMemory",
57 | "offline support",
58 | "distributed sync",
59 | "secondary store",
60 | "primary store",
61 | "non-blocking operations",
62 | "cache statistics",
63 | "layered caching",
64 | "fault tolerant",
65 | "scalable cache",
66 | "in-memory cache",
67 | "distributed cache",
68 | "lruSize",
69 | "lru",
70 | "multi-tier cache"
71 | ],
72 | "files": [
73 | "dist",
74 | "LICENSE"
75 | ]
76 | }
77 |
--------------------------------------------------------------------------------
/packages/cacheable/src/cacheable-item-types.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * CacheableItem
4 | * @typedef {Object} CacheableItem
5 | * @property {string} key - The key of the cacheable item
6 | * @property {any} value - The value of the cacheable item
7 | * @property {number|string} [ttl] - Time to Live - If you set a number it is miliseconds, if you set a string it is a human-readable
8 | * format such as `1s` for 1 second or `1h` for 1 hour. Setting undefined means that it will use the default time-to-live. If both are
9 | * undefined then it will not have a time-to-live.
10 | */
11 | export type CacheableItem = {
12 | key: string;
13 | value: any;
14 | ttl?: number | string;
15 | };
16 |
17 | export type CacheableStoreItem = {
18 | key: string;
19 | value: any;
20 | expires?: number;
21 | };
22 |
--------------------------------------------------------------------------------
/packages/cacheable/src/coalesce-async.ts:
--------------------------------------------------------------------------------
1 | type PromiseCallback = {
2 | resolve: (value: T | PromiseLike) => void;
3 | reject: (reason: E) => void;
4 | };
5 |
6 | const callbacks = new Map();
7 |
8 | function hasKey(key: string): boolean {
9 | return callbacks.has(key);
10 | }
11 |
12 | function addKey(key: string): void {
13 | callbacks.set(key, []);
14 | }
15 |
16 | function removeKey(key: string): void {
17 | callbacks.delete(key);
18 | }
19 |
20 | function addCallbackToKey(key: string, callback: PromiseCallback): void {
21 | const stash = getCallbacksByKey(key);
22 | stash.push(callback);
23 | callbacks.set(key, stash);
24 | }
25 |
26 | function getCallbacksByKey(key: string): Array> {
27 | /* c8 ignore next 1 */
28 | return callbacks.get(key) ?? [];
29 | }
30 |
31 | async function enqueue(key: string): Promise {
32 | return new Promise((resolve, reject) => {
33 | const callback: PromiseCallback = {resolve, reject};
34 | addCallbackToKey(key, callback);
35 | });
36 | }
37 |
38 | function dequeue(key: string): Array> {
39 | const stash = getCallbacksByKey(key);
40 | removeKey(key);
41 | return stash;
42 | }
43 |
44 | function coalesce(options: {key: string; error?: Error; result?: T}): void {
45 | const {key, error, result} = options;
46 |
47 | for (const callback of dequeue(key)) {
48 | /* c8 ignore next 1 */
49 | if (error) {
50 | /* c8 ignore next 3 */
51 | callback.reject(error);
52 | } else {
53 | callback.resolve(result);
54 | }
55 | }
56 | }
57 |
58 | /**
59 | * Enqueue a promise for the group identified by `key`.
60 | *
61 | * All requests received for the same key while a request for that key
62 | * is already being executed will wait. Once the running request settles
63 | * then all the waiting requests in the group will settle, too.
64 | * This minimizes how many times the function itself runs at the same time.
65 | * This function resolves or rejects according to the given function argument.
66 | *
67 | * @url https://github.com/douglascayers/promise-coalesce
68 | */
69 | export async function coalesceAsync(
70 | /**
71 | * Any identifier to group requests together.
72 | */
73 | key: string,
74 | /**
75 | * The function to run.
76 | */
77 | fnc: () => T | PromiseLike,
78 | ): Promise {
79 | if (!hasKey(key)) {
80 | addKey(key);
81 | try {
82 | const result = await Promise.resolve(fnc());
83 | coalesce({key, result});
84 | return result;
85 | /* c8 ignore next 1 */
86 | } catch (error: any) {
87 | /* c8 ignore next 5 */
88 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
89 | coalesce({key, error});
90 | // eslint-disable-next-line @typescript-eslint/only-throw-error
91 | throw error;
92 | }
93 | }
94 |
95 | return enqueue(key);
96 | }
97 |
--------------------------------------------------------------------------------
/packages/cacheable/src/hash.ts:
--------------------------------------------------------------------------------
1 | import * as crypto from 'node:crypto';
2 |
3 | /**
4 | * Hashes an object using the specified algorithm. The default algorithm is 'sha256'.
5 | * @param object The object to hash
6 | * @param algorithm The hash algorithm to use
7 | * @returns {string} The hash of the object
8 | */
9 | export function hash(object: any, algorithm = 'sha256'): string {
10 | // Convert the object to a string
11 | const objectString = JSON.stringify(object);
12 |
13 | // Check if the algorithm is supported
14 | if (!crypto.getHashes().includes(algorithm)) {
15 | throw new Error(`Unsupported hash algorithm: '${algorithm}'`);
16 | }
17 |
18 | const hasher = crypto.createHash(algorithm);
19 | hasher.update(objectString);
20 | return hasher.digest('hex');
21 | }
22 |
23 | export function hashToNumber(object: any, min = 0, max = 10, algorithm = 'sha256'): number {
24 | // Convert the object to a string
25 | const objectString = JSON.stringify(object);
26 |
27 | // Check if the algorithm is supported
28 | if (!crypto.getHashes().includes(algorithm)) {
29 | throw new Error(`Unsupported hash algorithm: '${algorithm}'`);
30 | }
31 |
32 | // Create a hasher and update it with the object string
33 | const hasher = crypto.createHash(algorithm);
34 | hasher.update(objectString);
35 |
36 | // Get the hash as a hexadecimal string
37 | const hashHex = hasher.digest('hex');
38 |
39 | // Convert the hex string to a number (base 16)
40 | const hashNumber = Number.parseInt(hashHex, 16);
41 |
42 | // Calculate the range size
43 | const range = max - min + 1;
44 |
45 | // Return a number within the specified range
46 | return min + (hashNumber % range);
47 | }
48 |
49 | export function djb2Hash(string_: string, min = 0, max = 10): number {
50 | // DJB2 hash algorithm
51 | let hash = 5381;
52 | for (let i = 0; i < string_.length; i++) {
53 | // eslint-disable-next-line no-bitwise, unicorn/prefer-code-point
54 | hash = (hash * 33) ^ string_.charCodeAt(i); // 33 is a prime multiplier
55 | }
56 |
57 | // Calculate the range size
58 | const range = max - min + 1;
59 |
60 | // Return a value within the specified range
61 | return min + (Math.abs(hash) % range);
62 | }
63 |
--------------------------------------------------------------------------------
/packages/cacheable/src/keyv-memory.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Keyv, type KeyvOptions, type KeyvStoreAdapter, type StoredData,
3 | } from 'keyv';
4 | import {CacheableMemory, type CacheableMemoryOptions} from './memory.js';
5 |
6 | export type KeyvCacheableMemoryOptions = CacheableMemoryOptions & {
7 | namespace?: string;
8 | };
9 |
10 | export class KeyvCacheableMemory implements KeyvStoreAdapter {
11 | opts: CacheableMemoryOptions = {
12 | ttl: 0,
13 | useClone: true,
14 | lruSize: 0,
15 | checkInterval: 0,
16 | };
17 |
18 | private readonly _defaultCache = new CacheableMemory();
19 | private readonly _nCache = new Map();
20 | private _namespace?: string;
21 |
22 | constructor(options?: KeyvCacheableMemoryOptions) {
23 | if (options) {
24 | this.opts = options;
25 | this._defaultCache = new CacheableMemory(options);
26 |
27 | if (options.namespace) {
28 | this._namespace = options.namespace;
29 | this._nCache.set(this._namespace, new CacheableMemory(options));
30 | }
31 | }
32 | }
33 |
34 | get namespace(): string | undefined {
35 | return this._namespace;
36 | }
37 |
38 | set namespace(value: string | undefined) {
39 | this._namespace = value;
40 | }
41 |
42 | public get store(): CacheableMemory {
43 | return this.getStore(this._namespace);
44 | }
45 |
46 | async get(key: string): Promise | undefined> {
47 | const result = this.getStore(this._namespace).get(key);
48 | if (result) {
49 | return result;
50 | }
51 |
52 | return undefined;
53 | }
54 |
55 | async getMany(keys: string[]): Promise>> {
56 | const result = this.getStore(this._namespace).getMany(keys);
57 |
58 | return result;
59 | }
60 |
61 | async set(key: string, value: any, ttl?: number): Promise {
62 | this.getStore(this._namespace).set(key, value, ttl);
63 | }
64 |
65 | async setMany(values: Array<{key: string; value: any; ttl?: number}>): Promise {
66 | this.getStore(this._namespace).setMany(values);
67 | }
68 |
69 | async delete(key: string): Promise {
70 | this.getStore(this._namespace).delete(key);
71 | return true;
72 | }
73 |
74 | async deleteMany?(key: string[]): Promise {
75 | this.getStore(this._namespace).deleteMany(key);
76 | return true;
77 | }
78 |
79 | async clear(): Promise {
80 | this.getStore(this._namespace).clear();
81 | }
82 |
83 | async has?(key: string): Promise {
84 | return this.getStore(this._namespace).has(key);
85 | }
86 |
87 | on(event: string, listener: (...arguments_: any[]) => void): this {
88 | this.getStore(this._namespace).on(event, listener);
89 | return this;
90 | }
91 |
92 | public getStore(namespace?: string): CacheableMemory {
93 | if (!namespace) {
94 | return this._defaultCache;
95 | }
96 |
97 | if (!this._nCache.has(namespace)) {
98 | this._nCache.set(namespace, new CacheableMemory(this.opts));
99 | }
100 |
101 | return this._nCache.get(namespace)!;
102 | }
103 | }
104 |
105 | /**
106 | * Creates a new Keyv instance with a new KeyvCacheableMemory store. This also removes the serialize/deserialize methods from the Keyv instance for optimization.
107 | * @param options
108 | * @returns
109 | */
110 | export function createKeyv(options?: KeyvCacheableMemoryOptions): Keyv {
111 | const store = new KeyvCacheableMemory(options);
112 | const namespace = options?.namespace;
113 |
114 | let ttl;
115 | if (options?.ttl && Number.isInteger(options.ttl)) {
116 | ttl = options?.ttl as number;
117 | }
118 |
119 | const keyv = new Keyv({store, namespace, ttl});
120 | // Remove seriazlize/deserialize
121 | keyv.serialize = undefined;
122 | keyv.deserialize = undefined;
123 | return keyv;
124 | }
125 |
--------------------------------------------------------------------------------
/packages/cacheable/src/memory-lru.ts:
--------------------------------------------------------------------------------
1 | export class ListNode {
2 | // eslint-disable-next-line @typescript-eslint/parameter-properties
3 | value: T;
4 | prev: ListNode | undefined = undefined;
5 | next: ListNode | undefined = undefined;
6 |
7 | constructor(value: T) {
8 | this.value = value;
9 | }
10 | }
11 |
12 | export class DoublyLinkedList {
13 | private head: ListNode | undefined = undefined;
14 | private tail: ListNode | undefined = undefined;
15 | private readonly nodesMap = new Map>();
16 |
17 | // Add a new node to the front (most recently used)
18 | addToFront(value: T): void {
19 | const newNode = new ListNode(value);
20 |
21 | if (this.head) {
22 | newNode.next = this.head;
23 | this.head.prev = newNode;
24 | this.head = newNode;
25 | } else {
26 | // eslint-disable-next-line no-multi-assign
27 | this.head = this.tail = newNode;
28 | }
29 |
30 | // Store the node reference in the map
31 | this.nodesMap.set(value, newNode);
32 | }
33 |
34 | // Move an existing node to the front (most recently used)
35 | moveToFront(value: T): void {
36 | const node = this.nodesMap.get(value);
37 | if (!node || this.head === node) {
38 | return;
39 | } // Node doesn't exist or is already at the front
40 |
41 | // Remove the node from its current position
42 | if (node.prev) {
43 | node.prev.next = node.next;
44 | }
45 |
46 | /* c8 ignore next 3 */
47 | if (node.next) {
48 | node.next.prev = node.prev;
49 | }
50 |
51 | // Update tail if necessary
52 | if (node === this.tail) {
53 | this.tail = node.prev;
54 | }
55 |
56 | // Move node to the front
57 | node.prev = undefined;
58 | node.next = this.head;
59 | if (this.head) {
60 | this.head.prev = node;
61 | }
62 |
63 | this.head = node;
64 |
65 | // If list was empty, update tail
66 | this.tail ||= node;
67 | }
68 |
69 | // Get the oldest node (tail)
70 | getOldest(): T | undefined {
71 | return this.tail ? this.tail.value : undefined;
72 | }
73 |
74 | // Remove the oldest node (tail)
75 | removeOldest(): T | undefined {
76 | /* c8 ignore next 3 */
77 | if (!this.tail) {
78 | return undefined;
79 | }
80 |
81 | const oldValue = this.tail.value;
82 |
83 | if (this.tail.prev) {
84 | this.tail = this.tail.prev;
85 | this.tail.next = undefined;
86 | /* c8 ignore next 4 */
87 | } else {
88 | // eslint-disable-next-line no-multi-assign
89 | this.head = this.tail = undefined;
90 | }
91 |
92 | // Remove the node from the map
93 | this.nodesMap.delete(oldValue);
94 | return oldValue;
95 | }
96 |
97 | get size(): number {
98 | return this.nodesMap.size;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/packages/cacheable/src/shorthand-time.ts:
--------------------------------------------------------------------------------
1 |
2 | export const shorthandToMilliseconds = (shorthand?: string | number): number | undefined => {
3 | let milliseconds: number;
4 |
5 | if (shorthand === undefined) {
6 | return undefined;
7 | }
8 |
9 | if (typeof shorthand === 'number') {
10 | milliseconds = shorthand;
11 | } else if (typeof shorthand === 'string') {
12 | shorthand = shorthand.trim();
13 |
14 | // Check if the string is purely numeric
15 | if (Number.isNaN(Number(shorthand))) {
16 | // Use a case-insensitive regex that supports decimals and 'ms' unit
17 | const match = /^([\d.]+)\s*(ms|s|m|h|hr|d)$/i.exec(shorthand);
18 |
19 | if (!match) {
20 | throw new Error(
21 | `Unsupported time format: "${shorthand}". Use 'ms', 's', 'm', 'h', 'hr', or 'd'.`,
22 | );
23 | }
24 |
25 | const [, value, unit] = match;
26 | const numericValue = Number.parseFloat(value);
27 | const unitLower = unit.toLowerCase();
28 |
29 | switch (unitLower) {
30 | case 'ms': {
31 | milliseconds = numericValue;
32 | break;
33 | }
34 |
35 | case 's': {
36 | milliseconds = numericValue * 1000;
37 | break;
38 | }
39 |
40 | case 'm': {
41 | milliseconds = numericValue * 1000 * 60;
42 | break;
43 | }
44 |
45 | case 'h': {
46 | milliseconds = numericValue * 1000 * 60 * 60;
47 | break;
48 | }
49 |
50 | case 'hr': {
51 | milliseconds = numericValue * 1000 * 60 * 60;
52 | break;
53 | }
54 |
55 | case 'd': {
56 | milliseconds = numericValue * 1000 * 60 * 60 * 24;
57 | break;
58 | }
59 |
60 | /* c8 ignore next 3 */
61 | default: {
62 | milliseconds = Number(shorthand);
63 | }
64 | }
65 | /* c8 ignore next 6 */
66 | } else {
67 | milliseconds = Number(shorthand);
68 | }
69 | } else {
70 | throw new TypeError('Time must be a string or a number.');
71 | }
72 |
73 | return milliseconds;
74 | };
75 |
76 | export const shorthandToTime = (shorthand?: string | number, fromDate?: Date): number => {
77 | fromDate ||= new Date();
78 |
79 | const milliseconds = shorthandToMilliseconds(shorthand);
80 | if (milliseconds === undefined) {
81 | return fromDate.getTime();
82 | }
83 |
84 | return fromDate.getTime() + milliseconds;
85 | };
86 |
87 |
--------------------------------------------------------------------------------
/packages/cacheable/src/stats.ts:
--------------------------------------------------------------------------------
1 |
2 | export type CacheableOptions = {
3 | enabled?: boolean;
4 | };
5 |
6 | export class CacheableStats {
7 | private _hits = 0;
8 | private _misses = 0;
9 | private _gets = 0;
10 | private _sets = 0;
11 | private _deletes = 0;
12 | private _clears = 0;
13 | private _vsize = 0;
14 | private _ksize = 0;
15 | private _count = 0;
16 | private _enabled = false;
17 |
18 | constructor(options?: CacheableOptions) {
19 | if (options?.enabled) {
20 | this._enabled = options.enabled;
21 | }
22 | }
23 |
24 | /**
25 | * @returns {boolean} - Whether the stats are enabled
26 | */
27 | public get enabled(): boolean {
28 | return this._enabled;
29 | }
30 |
31 | /**
32 | * @param {boolean} enabled - Whether to enable the stats
33 | */
34 | public set enabled(enabled: boolean) {
35 | this._enabled = enabled;
36 | }
37 |
38 | /**
39 | * @returns {number} - The number of hits
40 | * @readonly
41 | */
42 | public get hits(): number {
43 | return this._hits;
44 | }
45 |
46 | /**
47 | * @returns {number} - The number of misses
48 | * @readonly
49 | */
50 | public get misses(): number {
51 | return this._misses;
52 | }
53 |
54 | /**
55 | * @returns {number} - The number of gets
56 | * @readonly
57 | */
58 | public get gets(): number {
59 | return this._gets;
60 | }
61 |
62 | /**
63 | * @returns {number} - The number of sets
64 | * @readonly
65 | */
66 | public get sets(): number {
67 | return this._sets;
68 | }
69 |
70 | /**
71 | * @returns {number} - The number of deletes
72 | * @readonly
73 | */
74 | public get deletes(): number {
75 | return this._deletes;
76 | }
77 |
78 | /**
79 | * @returns {number} - The number of clears
80 | * @readonly
81 | */
82 | public get clears(): number {
83 | return this._clears;
84 | }
85 |
86 | /**
87 | * @returns {number} - The vsize (value size) of the cache instance
88 | * @readonly
89 | */
90 | public get vsize(): number {
91 | return this._vsize;
92 | }
93 |
94 | /**
95 | * @returns {number} - The ksize (key size) of the cache instance
96 | * @readonly
97 | */
98 | public get ksize(): number {
99 | return this._ksize;
100 | }
101 |
102 | /**
103 | * @returns {number} - The count of the cache instance
104 | * @readonly
105 | */
106 | public get count(): number {
107 | return this._count;
108 | }
109 |
110 | public incrementHits(): void {
111 | if (!this._enabled) {
112 | return;
113 | }
114 |
115 | this._hits++;
116 | }
117 |
118 | public incrementMisses(): void {
119 | if (!this._enabled) {
120 | return;
121 | }
122 |
123 | this._misses++;
124 | }
125 |
126 | public incrementGets(): void {
127 | if (!this._enabled) {
128 | return;
129 | }
130 |
131 | this._gets++;
132 | }
133 |
134 | public incrementSets(): void {
135 | if (!this._enabled) {
136 | return;
137 | }
138 |
139 | this._sets++;
140 | }
141 |
142 | public incrementDeletes(): void {
143 | if (!this._enabled) {
144 | return;
145 | }
146 |
147 | this._deletes++;
148 | }
149 |
150 | public incrementClears(): void {
151 | if (!this._enabled) {
152 | return;
153 | }
154 |
155 | this._clears++;
156 | }
157 |
158 | // eslint-disable-next-line @typescript-eslint/naming-convention
159 | public incrementVSize(value: any): void {
160 | if (!this._enabled) {
161 | return;
162 | }
163 |
164 | this._vsize += this.roughSizeOfObject(value);
165 | }
166 |
167 | // eslint-disable-next-line @typescript-eslint/naming-convention
168 | public decreaseVSize(value: any): void {
169 | if (!this._enabled) {
170 | return;
171 | }
172 |
173 | this._vsize -= this.roughSizeOfObject(value);
174 | }
175 |
176 | // eslint-disable-next-line @typescript-eslint/naming-convention
177 | public incrementKSize(key: string): void {
178 | if (!this._enabled) {
179 | return;
180 | }
181 |
182 | this._ksize += this.roughSizeOfString(key);
183 | }
184 |
185 | // eslint-disable-next-line @typescript-eslint/naming-convention
186 | public decreaseKSize(key: string): void {
187 | if (!this._enabled) {
188 | return;
189 | }
190 |
191 | this._ksize -= this.roughSizeOfString(key);
192 | }
193 |
194 | public incrementCount(): void {
195 | if (!this._enabled) {
196 | return;
197 | }
198 |
199 | this._count++;
200 | }
201 |
202 | public decreaseCount(): void {
203 | if (!this._enabled) {
204 | return;
205 | }
206 |
207 | this._count--;
208 | }
209 |
210 | public setCount(count: number): void {
211 | if (!this._enabled) {
212 | return;
213 | }
214 |
215 | this._count = count;
216 | }
217 |
218 | public roughSizeOfString(value: string): number {
219 | // Keys are strings (UTF-16)
220 | return value.length * 2;
221 | }
222 |
223 | public roughSizeOfObject(object: any): number {
224 | const objectList: any[] = [];
225 | const stack: any[] = [object];
226 | let bytes = 0;
227 |
228 | while (stack.length > 0) {
229 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
230 | const value = stack.pop();
231 |
232 | if (typeof value === 'boolean') {
233 | bytes += 4; // Booleans are 4 bytes
234 | } else if (typeof value === 'string') {
235 | bytes += value.length * 2; // Each character is 2 bytes (UTF-16 encoding)
236 | } else if (typeof value === 'number') {
237 | bytes += 8; // Numbers are 8 bytes (IEEE 754 format)
238 | } else if (typeof value === 'object' && value !== null && !objectList.includes(value)) {
239 | objectList.push(value);
240 |
241 | // Estimate object overhead, and then recursively estimate the size of properties
242 | // eslint-disable-next-line guard-for-in
243 | for (const key in value) {
244 | bytes += key.length * 2; // Keys are strings (UTF-16)
245 | stack.push(value[key]); // Add values to the stack to compute their size
246 | }
247 | }
248 | }
249 |
250 | return bytes;
251 | }
252 |
253 | public reset(): void {
254 | this._hits = 0;
255 | this._misses = 0;
256 | this._gets = 0;
257 | this._sets = 0;
258 | this._deletes = 0;
259 | this._clears = 0;
260 | this._vsize = 0;
261 | this._ksize = 0;
262 | this._count = 0;
263 | }
264 |
265 | public resetStoreValues(): void {
266 | this._vsize = 0;
267 | this._ksize = 0;
268 | this._count = 0;
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/packages/cacheable/src/ttl.ts:
--------------------------------------------------------------------------------
1 | import {shorthandToMilliseconds} from '../src/shorthand-time.js';
2 |
3 | /**
4 | * Converts a exspires value to a TTL value.
5 | * @param expires - The expires value to convert.
6 | * @returns {number | undefined} The TTL value in milliseconds, or undefined if the expires value is not valid.
7 | */
8 | export function getTtlFromExpires(expires: number | undefined): number | undefined {
9 | if (expires === undefined || expires === null) {
10 | return undefined;
11 | }
12 |
13 | const now = Date.now();
14 | if (expires < now) {
15 | return undefined;
16 | }
17 |
18 | return expires - now;
19 | }
20 |
21 | /**
22 | * Get the TTL value from the cacheableTtl, primaryTtl, and secondaryTtl values.
23 | * @param cacheableTtl - The cacheableTtl value to use.
24 | * @param primaryTtl - The primaryTtl value to use.
25 | * @param secondaryTtl - The secondaryTtl value to use.
26 | * @returns {number | undefined} The TTL value in milliseconds, or undefined if all values are undefined.
27 | */
28 | export function getCascadingTtl(cacheableTtl?: number | string, primaryTtl?: number, secondaryTtl?: number): number | undefined {
29 | return secondaryTtl ?? primaryTtl ?? shorthandToMilliseconds(cacheableTtl);
30 | }
31 |
32 | /**
33 | * Calculate the TTL value from the expires value. If the ttl is undefined, it will be set to the expires value. If the
34 | * expires value is undefined, it will be set to the ttl value. If both values are defined, the smaller of the two will be used.
35 | * @param ttl
36 | * @param expires
37 | * @returns
38 | */
39 | export function calculateTtlFromExpiration(ttl: number | undefined, expires: number | undefined): number | undefined {
40 | const ttlFromExpires = getTtlFromExpires(expires);
41 | const expiresFromTtl = ttl ? Date.now() + ttl : undefined;
42 | if (ttlFromExpires === undefined) {
43 | return ttl;
44 | }
45 |
46 | if (expiresFromTtl === undefined) {
47 | return ttlFromExpires;
48 | }
49 |
50 | if (expires! > expiresFromTtl) {
51 | return ttl;
52 | }
53 |
54 | return ttlFromExpires;
55 | }
56 |
--------------------------------------------------------------------------------
/packages/cacheable/src/wrap.ts:
--------------------------------------------------------------------------------
1 | import {hash} from './hash.js';
2 | import {coalesceAsync} from './coalesce-async.js';
3 | import {type Cacheable, type CacheableMemory} from './index.js';
4 |
5 | export type GetOrSetFunctionOptions = {
6 | ttl?: number | string;
7 | cacheErrors?: boolean;
8 | };
9 |
10 | export type GetOrSetOptions = GetOrSetFunctionOptions & {
11 | cacheId?: string;
12 | cache: Cacheable;
13 | };
14 |
15 | export type WrapFunctionOptions = {
16 | ttl?: number | string;
17 | keyPrefix?: string;
18 | cacheErrors?: boolean;
19 | cacheId?: string;
20 | };
21 |
22 | export type WrapOptions = WrapFunctionOptions & {
23 | cache: Cacheable;
24 | };
25 |
26 | export type WrapSyncOptions = WrapFunctionOptions & {
27 | cache: CacheableMemory;
28 | };
29 |
30 | export type AnyFunction = (...arguments_: any[]) => any;
31 |
32 | export function wrapSync(function_: AnyFunction, options: WrapSyncOptions): AnyFunction {
33 | const {ttl, keyPrefix, cache} = options;
34 |
35 | return function (...arguments_: any[]) {
36 | const cacheKey = createWrapKey(function_, arguments_, keyPrefix);
37 | let value = cache.get(cacheKey);
38 |
39 | if (value === undefined) {
40 | try {
41 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
42 | value = function_(...arguments_);
43 | cache.set(cacheKey, value, ttl);
44 | } catch (error) {
45 | cache.emit('error', error);
46 | if (options.cacheErrors) {
47 | cache.set(cacheKey, error, ttl);
48 | }
49 | }
50 | }
51 |
52 | return value as T;
53 | };
54 | }
55 |
56 | export async function getOrSet(key: string, function_: () => Promise, options: GetOrSetOptions): Promise {
57 | let value = await options.cache.get(key) as T | undefined;
58 | if (value === undefined) {
59 | const cacheId = options.cacheId ?? 'default';
60 | const coalesceKey = `${cacheId}::${key}`;
61 | value = await coalesceAsync(coalesceKey, async () => {
62 | try {
63 | const result = await function_() as T;
64 | await options.cache.set(key, result, options.ttl);
65 | return result;
66 | } catch (error) {
67 | options.cache.emit('error', error);
68 | if (options.cacheErrors) {
69 | await options.cache.set(key, error, options.ttl);
70 | }
71 | }
72 | });
73 | }
74 |
75 | return value;
76 | }
77 |
78 | export function wrap(function_: AnyFunction, options: WrapOptions): AnyFunction {
79 | const {keyPrefix, cache} = options;
80 |
81 | return async function (...arguments_: any[]) {
82 | const cacheKey = createWrapKey(function_, arguments_, keyPrefix);
83 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return
84 | return cache.getOrSet(cacheKey, async (): Promise => function_(...arguments_), options);
85 | };
86 | }
87 |
88 | export function createWrapKey(function_: AnyFunction, arguments_: any[], keyPrefix?: string): string {
89 | if (!keyPrefix) {
90 | return `${function_.name}::${hash(arguments_)}`;
91 | }
92 |
93 | return `${keyPrefix}::${function_.name}::${hash(arguments_)}`;
94 | }
95 |
--------------------------------------------------------------------------------
/packages/cacheable/test/hash.test.ts:
--------------------------------------------------------------------------------
1 | import {describe, test, expect} from 'vitest';
2 | import {hash, hashToNumber} from '../src/hash.js';
3 |
4 | describe('hash', () => {
5 | test('hashes an object using the specified algorithm', () => {
6 | // Arrange
7 | const object = {foo: 'bar'};
8 | const algorithm = 'sha256';
9 |
10 | // Act
11 | const result = hash(object, algorithm);
12 |
13 | // Assert
14 | expect(result).toBe('7a38bf81f383f69433ad6e900d35b3e2385593f76a7b7ab5d4355b8ba41ee24b');
15 | });
16 | test('hashes a string using the default algorithm', () => {
17 | // Arrange
18 | const object = 'foo';
19 |
20 | // Act
21 | const result = hash(object);
22 |
23 | // Assert
24 | expect(result).toBe('b2213295d564916f89a6a42455567c87c3f480fcd7a1c15e220f17d7169a790b');
25 | });
26 |
27 | test('hashes a number using the default algorithm', () => {
28 | // Arrange
29 | const object = '123';
30 |
31 | // Act
32 | const result = hashToNumber(object);
33 |
34 | // Assert
35 | expect(result).toBeDefined();
36 | });
37 |
38 | test('throws an error when the algorithm is not supported', () => {
39 | // Arrange
40 | const object = {foo: 'bar'};
41 | const algorithm = 'md5foo';
42 |
43 | // Act & Assert
44 | expect(() => hashToNumber(object, 0, 100, algorithm)).toThrowError('Unsupported hash algorithm: \'md5foo\'');
45 | });
46 |
47 | test('throws an error when the algorithm is not supported', () => {
48 | expect(() => hash('foo', 'md5foo')).toThrowError('Unsupported hash algorithm: \'md5foo\'');
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/packages/cacheable/test/keyv-memory.test.ts:
--------------------------------------------------------------------------------
1 | import {describe, test, expect} from 'vitest';
2 | import {Keyv} from 'keyv';
3 | import {KeyvCacheableMemory, createKeyv} from '../src/keyv-memory.js';
4 |
5 | describe('Keyv Cacheable Memory', () => {
6 | test('should initialize keyv cacheable memory', async () => {
7 | const keyvCacheableMemory = new KeyvCacheableMemory();
8 | expect(keyvCacheableMemory).toBeDefined();
9 | const keyv = new Keyv({store: keyvCacheableMemory});
10 | expect(keyv).toBeDefined();
11 | });
12 | test('should set namespace for keyv cacheable memory', async () => {
13 | const namespace = 'ns1';
14 | const keyvCacheableMemory = new KeyvCacheableMemory({namespace});
15 | expect(keyvCacheableMemory.namespace).toBe(namespace);
16 | keyvCacheableMemory.namespace = 'ns2';
17 | expect(keyvCacheableMemory.namespace).toBe('ns2');
18 | });
19 | test('should set options for keyv cacheable memory', async () => {
20 | const keyvCacheableMemory = new KeyvCacheableMemory({ttl: 1000, lruSize: 1000});
21 | expect(keyvCacheableMemory).toBeDefined();
22 | const keyv = new Keyv({store: keyvCacheableMemory});
23 | expect(keyv).toBeDefined();
24 | });
25 | test('should get undefined from keyv cacheable memory', async () => {
26 | const keyvCacheableMemory = new KeyvCacheableMemory();
27 | const keyv = new Keyv({store: keyvCacheableMemory});
28 | const value = await keyv.get('key') as string | undefined;
29 | expect(value).toBe(undefined);
30 | });
31 | test('should set and get value from keyv cacheable memory', async () => {
32 | const keyvCacheableMemory = new KeyvCacheableMemory();
33 | const keyv = new Keyv({store: keyvCacheableMemory});
34 | await keyv.set('key', 'value');
35 | const value = await keyv.get('key');
36 | expect(value).toBe('value');
37 | });
38 | test('should delete value from keyv cacheable memory', async () => {
39 | const keyvCacheableMemory = new KeyvCacheableMemory();
40 | const keyv = new Keyv({store: keyvCacheableMemory});
41 | await keyv.set('key', 'value');
42 | await keyv.delete('key');
43 | const value = await keyv.get('key');
44 | expect(value).toBe(undefined);
45 | });
46 | test('should clear keyv cacheable memory', async () => {
47 | const keyvCacheableMemory = new KeyvCacheableMemory();
48 | const keyv = new Keyv({store: keyvCacheableMemory});
49 | await keyv.set('key', 'value');
50 | await keyv.clear();
51 | const value = await keyv.get('key');
52 | expect(value).toBe(undefined);
53 | });
54 | test('should check if key exists in keyv cacheable memory', async () => {
55 | const keyvCacheableMemory = new KeyvCacheableMemory();
56 | const keyv = new Keyv({store: keyvCacheableMemory});
57 | await keyv.set('key', 'value');
58 | const exists = await keyv.has('key');
59 | expect(exists).toBe(true);
60 | });
61 | test('should get many values from keyv cacheable memory', async () => {
62 | const keyvCacheableMemory = new KeyvCacheableMemory();
63 | const keyv = new Keyv({store: keyvCacheableMemory});
64 | await keyv.set('key', 'value');
65 | const values = await keyv.get(['key']);
66 | expect(values).toEqual(['value']);
67 | });
68 | test('should delete many values from keyv cacheable memory', async () => {
69 | const keyvCacheableMemory = new KeyvCacheableMemory();
70 | const keyv = new Keyv({store: keyvCacheableMemory});
71 | await keyv.set('key', 'value');
72 | await keyv.delete(['key']);
73 | const value = await keyv.get('key');
74 | expect(value).toBe(undefined);
75 | });
76 | test('should set many values in keyv cacheable memory', async () => {
77 | const keyvCacheableMemory = new KeyvCacheableMemory();
78 | await keyvCacheableMemory.setMany([{key: 'key', value: 'value'}, {key: 'key1', value: 'value1'}]);
79 | const value = await keyvCacheableMemory.get('key1');
80 | expect(value).toBe('value1');
81 | });
82 | test('should be able to get the store based on namespace', async () => {
83 | const cache = new KeyvCacheableMemory();
84 | await cache.set('key1', 'default');
85 | expect(await cache.get('key1')).toBe('default');
86 | cache.namespace = 'ns1';
87 | expect(await cache.get('key1')).toBe(undefined);
88 | expect(cache.store.get('key1')).toBe(undefined);
89 | await cache.set('key1', 'ns1');
90 | expect(await cache.get('key1')).toBe('ns1');
91 | expect(cache.store.get('key1')).toBe('ns1');
92 | cache.namespace = undefined;
93 | expect(await cache.get('key1')).toBe('default');
94 | expect(cache.store.get('key1')).toBe('default');
95 | });
96 |
97 | test('should be able to createKeyv with cacheable memory store', async () => {
98 | const keyv = createKeyv({ttl: 1000, lruSize: 1000});
99 | expect(keyv).toBeDefined();
100 | expect(keyv.store).toBeInstanceOf(KeyvCacheableMemory);
101 | expect(keyv.store.opts.ttl).toBe(1000);
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/packages/cacheable/test/secondary-primary.test.ts:
--------------------------------------------------------------------------------
1 | import {expect, test} from 'vitest';
2 | import {Keyv} from 'keyv';
3 | import {faker} from '@faker-js/faker';
4 | import {Cacheable, CacheableHooks} from '../src/index.js';
5 | import {getTtlFromExpires} from '../src/ttl.js';
6 | import {sleep} from './sleep.js';
7 |
8 | test('should set a new ttl when secondary is setting primary', async () => {
9 | const secondary = new Keyv({ttl: 100});
10 | const cacheable = new Cacheable({secondary});
11 | const data = {
12 | key: faker.string.uuid(),
13 | value: faker.string.uuid(),
14 | };
15 |
16 | cacheable.onHook(CacheableHooks.BEFORE_SECONDARY_SETS_PRIMARY, async item => {
17 | item.ttl = 10;
18 | });
19 |
20 | await cacheable.set(data.key, data.value);
21 | const result = await cacheable.get(data.key);
22 | expect(result).toEqual(data.value);
23 |
24 | // Remove the item from primary
25 | await cacheable.primary.delete(data.key);
26 | const primaryResult1 = await cacheable.primary.get(data.key, {raw: true});
27 | expect(primaryResult1).toEqual(undefined);
28 |
29 | // Update the item from secondary
30 | await cacheable.get(data.key);
31 | const primaryResult2 = await cacheable.primary.get(data.key, {raw: true});
32 | expect(primaryResult2?.value).toEqual(data.value);
33 | const ttlFromExpires = getTtlFromExpires(primaryResult2?.expires as number | undefined);
34 | expect(ttlFromExpires).toBeLessThan(12);
35 |
36 | // Now make sure that it expires after 10 seconds
37 | await sleep(20);
38 | const primaryResult3 = await cacheable.primary.get(data.key, {raw: true});
39 | expect(primaryResult3).toEqual(undefined);
40 |
41 | // Verify that the secondary is still there
42 | const secondaryResult = await cacheable.secondary?.get(data.key, {raw: true});
43 | expect(secondaryResult?.value).toEqual(data.value);
44 | });
45 |
46 | test('should use the cacheable default ttl on secondary -> primary', async () => {
47 | const data = {
48 | key: faker.string.uuid(),
49 | value: faker.string.uuid(),
50 | };
51 |
52 | const secondary = new Keyv();
53 | const cacheable = new Cacheable({secondary, ttl: 100});
54 |
55 | // Set the value on secondary with no ttl
56 | await cacheable.secondary?.set(data.key, data.value);
57 |
58 | const result = await cacheable.get(data.key);
59 | expect(result).toEqual(data.value);
60 |
61 | // Get the value from primary raw to validate it has expires
62 | const primaryResult = await cacheable.primary.get(data.key, {raw: true});
63 | expect(primaryResult?.value).toEqual(data.value);
64 | const ttlFromExpires = getTtlFromExpires(primaryResult?.expires as number | undefined);
65 | expect(ttlFromExpires).toBeGreaterThan(95);
66 | expect(ttlFromExpires).toBeLessThan(105);
67 | });
68 |
69 | test('should use the primary ttl on secondary -> primary', async () => {
70 | const data = {
71 | key: faker.string.uuid(),
72 | value: faker.string.uuid(),
73 | };
74 |
75 | const secondary = new Keyv();
76 | const primary = new Keyv({ttl: 50});
77 | const cacheable = new Cacheable({secondary, primary, ttl: 100});
78 |
79 | // Set the value on secondary with no ttl
80 | await cacheable.secondary?.set(data.key, data.value);
81 |
82 | const result = await cacheable.get(data.key);
83 | expect(result).toEqual(data.value);
84 |
85 | // Get the value from primary raw to validate it has expires
86 | const primaryResult = await cacheable.primary.get(data.key, {raw: true});
87 | expect(primaryResult?.value).toEqual(data.value);
88 | const ttlFromExpires = getTtlFromExpires(primaryResult?.expires as number | undefined);
89 | expect(ttlFromExpires).toBeGreaterThan(45);
90 | expect(ttlFromExpires).toBeLessThan(55);
91 | });
92 |
93 | test('should use the secondary ttl on secondary -> primary', async () => {
94 | const data = {
95 | key: faker.string.uuid(),
96 | value: faker.string.uuid(),
97 | };
98 |
99 | const secondary = new Keyv({ttl: 50});
100 | const primary = new Keyv();
101 | const cacheable = new Cacheable({secondary, primary, ttl: 100});
102 |
103 | // Set the value on secondary with no ttl
104 | await cacheable.secondary?.set(data.key, data.value);
105 |
106 | const result = await cacheable.get(data.key);
107 | expect(result).toEqual(data.value);
108 |
109 | // Get the value from primary raw to validate it has expires
110 | const primaryResult = await cacheable.primary.get(data.key, {raw: true});
111 | expect(primaryResult?.value).toEqual(data.value);
112 | const ttlFromExpires = getTtlFromExpires(primaryResult?.expires as number | undefined);
113 | expect(ttlFromExpires).toBeGreaterThan(45);
114 | expect(ttlFromExpires).toBeLessThan(55);
115 | });
116 |
--------------------------------------------------------------------------------
/packages/cacheable/test/shared-secondary.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | test, expect,
3 | } from 'vitest';
4 | import {Keyv} from 'keyv';
5 | import {Cacheable} from '../src/index.js';
6 | import {sleep} from './sleep.js';
7 |
8 | /*
9 | Should get a value from the secondary store and respect its ttl when setting the value in the primary store (item specific ttl)
10 | */
11 | test('should get a value from the secondary store and respect its ttl', async () => {
12 | const instance1Primary = new Keyv();
13 | const instance2Primary = new Keyv();
14 |
15 | const sharedSecondary = new Keyv();
16 |
17 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary});
18 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary});
19 |
20 | // Set the value in the first instance
21 | await instance1.set('key', 'value', 50);
22 |
23 | await sleep(25);
24 |
25 | // Get the value in the second instance
26 | const result = await instance2.get('key');
27 | expect(result).toEqual('value');
28 |
29 | // Wait for the value to expire
30 | await sleep(100);
31 |
32 | // Get the value in the second instance (it should be expired)
33 | const result2 = await instance2.get('key');
34 | expect(result2, 'result should have expired').toBeUndefined();
35 | });
36 |
37 | /*
38 | Should get a value from the secondary store and respect its zero-ttl when setting the value in the primary store (item specific zero-ttl)
39 | */
40 | test('secondar store as a zero ttl set', async () => {
41 | const instance1Primary = new Keyv();
42 | const instance2Primary = new Keyv();
43 |
44 | const sharedSecondary = new Keyv();
45 |
46 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary, ttl: 50});
47 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary, ttl: 50});
48 |
49 | // Set the value in the first instance
50 | await instance1.set('key', 'value', 0);
51 |
52 | await sleep(25);
53 |
54 | // Get the value in the second instance
55 | const result = await instance2.get('key');
56 | expect(result).toEqual('value');
57 |
58 | // Wait past the time of the default TTL of 500ms
59 | await sleep(75);
60 |
61 | // Get the value in the second instance (it should be valid)
62 | const result2 = await instance2.get('key');
63 | expect(result2).toEqual('value');
64 | });
65 |
66 | /*
67 | Should get a value from the secondary store and respect its ttl when setting the value in the primary store (default ttl when setting)
68 | */
69 | test('default ttl when setting', async () => {
70 | const instance1Primary = new Keyv();
71 | const instance2Primary = new Keyv();
72 |
73 | const sharedSecondary = new Keyv();
74 |
75 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary, ttl: 50});
76 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary});
77 |
78 | // Set the value in the first instance
79 | await instance1.set('key', 'value');
80 |
81 | await sleep(25);
82 |
83 | // Get the value in the second instance
84 | const result = await instance2.get('key');
85 | expect(result).toEqual('value');
86 |
87 | // Wait for the value to expire
88 | await sleep(75);
89 |
90 | // Get the value in the second instance (it should be expired)
91 | const result2 = await instance2.get('key');
92 | expect(result2, 'result should have expired').toBeUndefined();
93 | });
94 |
95 | /*
96 | Should get a value from the secondary store and respect its zero-ttl when setting the value in the primary store (default zero-ttl when setting)
97 | */
98 | test('should get a value from the secondary store and respect its zero-ttl', async () => {
99 | const instance1Primary = new Keyv();
100 | const instance2Primary = new Keyv();
101 |
102 | const sharedSecondary = new Keyv();
103 |
104 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary, ttl: 0});
105 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary, ttl: 100});
106 |
107 | // Set the value in the first instance
108 | await instance1.set('key', 'value');
109 |
110 | await sleep(50);
111 |
112 | // Get the value in the second instance
113 | const result = await instance2.get('key');
114 | expect(result).toEqual('value');
115 |
116 | // Wait past instance2's default TTL of 500ms
117 | await sleep(125);
118 |
119 | // Get the value in the second instance (it should be valid)
120 | const result2 = await instance2.get('key');
121 | expect(result2).toEqual('value');
122 | });
123 |
124 | /*
125 | Should get a value from the secondary store and respect its ttl when setting the value in the primary store (default ttl when setting in the first instance, despite alternative ttl when getting in the second instance)
126 | */
127 | test('default ttl when setting in the first instance, despite alternative ttl', async () => {
128 | const instance1Primary = new Keyv();
129 | const instance2Primary = new Keyv();
130 |
131 | const sharedSecondary = new Keyv();
132 |
133 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary, ttl: 50});
134 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary, ttl: 100});
135 |
136 | // Set the value in the first instance
137 | await instance1.set('key', 'value');
138 |
139 | await sleep(25);
140 |
141 | // Get the value in the second instance
142 | const result = await instance2.get('key');
143 | expect(result).toEqual('value');
144 |
145 | // Wait for the value to expire
146 | await sleep(100);
147 |
148 | // Get the value in the second instance (it should be expired)
149 | const result2 = await instance2.get('key');
150 | expect(result2, 'result should have expired').toBeUndefined();
151 | });
152 |
153 | /*
154 | Should get a value from the secondary store and respect its zero-ttl when setting the value in the primary store
155 | (default zero-ttl when setting in the first instance, despite alternative ttl when getting in the second instance)
156 | */
157 | test('default zero-ttl when setting in the first instance, despite alternative ttl', async () => {
158 | const instance1Primary = new Keyv();
159 | const instance2Primary = new Keyv();
160 |
161 | const sharedSecondary = new Keyv();
162 |
163 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary, ttl: 0});
164 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary, ttl: 100});
165 |
166 | // Set the value in the first instance
167 | await instance1.set('key', 'value');
168 |
169 | await sleep(50);
170 |
171 | // Get the value in the second instance
172 | const result = await instance2.get('key');
173 | expect(result).toEqual('value');
174 |
175 | // Wait past instance2's default TTL of 500ms
176 | await sleep(125);
177 |
178 | // Get the value in the second instance (it should be valid)
179 | const result2 = await instance2.get('key');
180 | expect(result2).toEqual('value');
181 | });
182 |
183 | /*
184 | Should not return a value from the secondary store or set it in the primary store when the value is expired in the secondary store
185 | */
186 | test('should not set in primary store if expired', async () => {
187 | const instance1Primary = new Keyv();
188 | const instance2Primary = new Keyv();
189 |
190 | // A custom Keyv class designed return an expired value
191 | class CustomKeyv extends Keyv {
192 | async get(key: string | string[], options?: {raw?: boolean}): Promise {
193 | const value = await super.get(key as unknown as string, options?.raw ? {raw: true} : undefined);
194 |
195 | await sleep(100);
196 |
197 | return value;
198 | }
199 | }
200 | const sharedSecondary = new CustomKeyv();
201 |
202 | const instance1 = new Cacheable({primary: instance1Primary, secondary: sharedSecondary});
203 | const instance2 = new Cacheable({primary: instance2Primary, secondary: sharedSecondary});
204 |
205 | // Set the value in the secondary store
206 | await instance1.set('key', 'value', 25);
207 |
208 | await sleep(50);
209 |
210 | // Get the value in the second instance
211 | const result = await instance2.get('key');
212 | expect(result, 'result should have expired').toBeUndefined();
213 |
214 | // Get the value in the primary store
215 | const result2 = await instance2Primary.get('key') as unknown;
216 | expect(result2, 'result should not be placed in the primary store').toBeUndefined();
217 | });
218 |
--------------------------------------------------------------------------------
/packages/cacheable/test/shorthand-time.test.ts:
--------------------------------------------------------------------------------
1 | import {describe, test, expect} from 'vitest';
2 | import {shorthandToMilliseconds, shorthandToTime} from '../src/shorthand-time.js';
3 |
4 | describe('time parser', () => {
5 | test('send in number', () => {
6 | expect(shorthandToMilliseconds(1000)).toBe(1000);
7 | });
8 | test('send in string with milliseconds', () => {
9 | expect(shorthandToMilliseconds('1ms')).toBe(1);
10 | });
11 | test('send in string', () => {
12 | expect(shorthandToMilliseconds('1s')).toBe(1000);
13 | });
14 | test('send in string with spaces', () => {
15 | expect(shorthandToMilliseconds('1 s')).toBe(1000);
16 | });
17 | test('send in string with decimal', () => {
18 | expect(shorthandToMilliseconds('1.5s')).toBe(1500);
19 | });
20 | test('send in string with minutes', () => {
21 | expect(shorthandToMilliseconds('1m')).toBe(60_000);
22 | });
23 | test('send in string with hours', () => {
24 | expect(shorthandToMilliseconds('1h')).toBe(3_600_000);
25 | });
26 | test('send in string with days', () => {
27 | expect(shorthandToMilliseconds('1d')).toBe(86_400_000);
28 | });
29 | test('send in string with unsupported unit', () => {
30 | expect(() => shorthandToMilliseconds('1z')).toThrowError('Unsupported time format: "1z". Use \'ms\', \'s\', \'m\', \'h\', \'hr\', or \'d\'.');
31 | });
32 | test('send in string with number', () => {
33 | expect(shorthandToMilliseconds('1000')).toBe(1000);
34 | });
35 | test('send in string with number and decimal', () => {
36 | expect(shorthandToMilliseconds('1.5h')).toBe(5_400_000);
37 | });
38 | test('send in string with number and decimal', () => {
39 | expect(shorthandToMilliseconds('1.5hr')).toBe(5_400_000);
40 | });
41 | });
42 |
43 | describe('parse to time', () => {
44 | test('send in number', () => {
45 | expect(shorthandToTime(1000)).toBeGreaterThan(Date.now());
46 | });
47 |
48 | test('send in string', () => {
49 | expect(shorthandToTime('10s')).toBeGreaterThan(Date.now());
50 | });
51 |
52 | test('send in nothing', () => {
53 | expect(shorthandToTime()).toBeDefined();
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/packages/cacheable/test/sleep.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-promise-executor-return
2 | export const sleep = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
3 |
--------------------------------------------------------------------------------
/packages/cacheable/test/stats.test.ts:
--------------------------------------------------------------------------------
1 | import {describe, test, expect} from 'vitest';
2 | import {CacheableStats} from '../src/stats.js';
3 |
4 | describe('cacheable stats', () => {
5 | test('should be able to instantiate', () => {
6 | const stats = new CacheableStats();
7 | expect(stats).toBeDefined();
8 | });
9 |
10 | test('properties should be initialized', () => {
11 | const stats = new CacheableStats();
12 | expect(stats.hits).toBe(0);
13 | expect(stats.misses).toBe(0);
14 | expect(stats.gets).toBe(0);
15 | expect(stats.sets).toBe(0);
16 | expect(stats.deletes).toBe(0);
17 | expect(stats.clears).toBe(0);
18 | expect(stats.vsize).toBe(0);
19 | expect(stats.ksize).toBe(0);
20 | expect(stats.count).toBe(0);
21 | });
22 |
23 | test('should be able to enable stats', () => {
24 | const stats = new CacheableStats({enabled: true});
25 | expect(stats.enabled).toBe(true);
26 | stats.enabled = false;
27 | expect(stats.enabled).toBe(false);
28 | });
29 |
30 | test('should be able to increment stats', () => {
31 | const stats = new CacheableStats({enabled: true});
32 | stats.incrementHits();
33 | stats.incrementMisses();
34 | stats.incrementGets();
35 | stats.incrementSets();
36 | stats.incrementDeletes();
37 | stats.incrementClears();
38 | stats.incrementVSize('foo');
39 | stats.incrementKSize('foo');
40 | stats.incrementCount();
41 | expect(stats.hits).toBe(1);
42 | expect(stats.misses).toBe(1);
43 | expect(stats.gets).toBe(1);
44 | expect(stats.sets).toBe(1);
45 | expect(stats.deletes).toBe(1);
46 | expect(stats.clears).toBe(1);
47 | expect(stats.vsize).toBe(6);
48 | expect(stats.ksize).toBe(6);
49 | expect(stats.count).toBe(1);
50 | });
51 |
52 | test('should be able to reset stats', () => {
53 | const stats = new CacheableStats({enabled: true});
54 | stats.incrementHits();
55 | stats.incrementMisses();
56 | stats.incrementGets();
57 | stats.incrementSets();
58 | stats.incrementDeletes();
59 | stats.incrementClears();
60 | stats.incrementVSize('foo');
61 | stats.incrementKSize('foo');
62 | stats.incrementCount();
63 | stats.reset();
64 | expect(stats.hits).toBe(0);
65 | expect(stats.misses).toBe(0);
66 | expect(stats.gets).toBe(0);
67 | expect(stats.sets).toBe(0);
68 | expect(stats.deletes).toBe(0);
69 | expect(stats.clears).toBe(0);
70 | expect(stats.vsize).toBe(0);
71 | expect(stats.ksize).toBe(0);
72 | expect(stats.count).toBe(0);
73 | });
74 |
75 | test('should be able to decrease certain stats', () => {
76 | const stats = new CacheableStats({enabled: true});
77 | stats.incrementVSize('foo');
78 | stats.incrementKSize('foo');
79 | stats.incrementCount();
80 | expect(stats.vsize).toBe(6);
81 | expect(stats.ksize).toBe(6);
82 | expect(stats.count).toBe(1);
83 | stats.decreaseVSize('foo');
84 | stats.decreaseKSize('foo');
85 | stats.decreaseCount();
86 | expect(stats.vsize).toBe(0);
87 | expect(stats.ksize).toBe(0);
88 | expect(stats.count).toBe(0);
89 | });
90 |
91 | test('should not keep going if stats are disabled', () => {
92 | const stats = new CacheableStats({enabled: false});
93 | stats.incrementHits();
94 | stats.incrementMisses();
95 | stats.incrementGets();
96 | stats.incrementSets();
97 | stats.incrementDeletes();
98 | stats.incrementClears();
99 | stats.incrementVSize('foo');
100 | stats.incrementKSize('foo');
101 | stats.incrementCount();
102 | expect(stats.hits).toBe(0);
103 | expect(stats.misses).toBe(0);
104 | expect(stats.gets).toBe(0);
105 | expect(stats.sets).toBe(0);
106 | expect(stats.deletes).toBe(0);
107 | expect(stats.clears).toBe(0);
108 | expect(stats.vsize).toBe(0);
109 | expect(stats.ksize).toBe(0);
110 | expect(stats.count).toBe(0);
111 | stats.enabled = true;
112 | stats.incrementHits();
113 | stats.incrementMisses();
114 | stats.incrementGets();
115 | stats.incrementSets();
116 | stats.incrementDeletes();
117 | stats.incrementClears();
118 | stats.incrementVSize('foo');
119 | stats.incrementKSize('foo');
120 | stats.incrementCount();
121 | expect(stats.hits).toBe(1);
122 | expect(stats.misses).toBe(1);
123 | expect(stats.gets).toBe(1);
124 | expect(stats.sets).toBe(1);
125 | expect(stats.deletes).toBe(1);
126 | expect(stats.clears).toBe(1);
127 | expect(stats.vsize).toBe(6);
128 | expect(stats.ksize).toBe(6);
129 | expect(stats.count).toBe(1);
130 | stats.enabled = false;
131 | stats.decreaseKSize('foo');
132 | stats.decreaseVSize('foo');
133 | stats.decreaseCount();
134 | expect(stats.vsize).toBe(6);
135 | expect(stats.ksize).toBe(6);
136 | expect(stats.count).toBe(1);
137 | });
138 | test('should get the rough size of the stats object', () => {
139 | const stats = new CacheableStats();
140 | expect(stats.roughSizeOfObject(true)).toBeGreaterThan(0);
141 | expect(stats.roughSizeOfObject('wow')).toBeGreaterThan(0);
142 | expect(stats.roughSizeOfObject(123)).toBeGreaterThan(0);
143 | expect(stats.roughSizeOfObject({foo: 'bar'})).toBeGreaterThan(0);
144 | expect(stats.roughSizeOfObject([1, 2, 3])).toBeGreaterThan(0);
145 | });
146 | test('set the count property', () => {
147 | const stats = new CacheableStats();
148 | stats.setCount(10);
149 | expect(stats.count).toBe(0);
150 | stats.enabled = true;
151 | stats.setCount(10);
152 | expect(stats.count).toBe(10);
153 | });
154 | });
155 |
--------------------------------------------------------------------------------
/packages/cacheable/test/ttl.test.ts:
--------------------------------------------------------------------------------
1 | import {test, expect} from 'vitest';
2 | import {faker} from '@faker-js/faker';
3 | import {Cacheable} from '../src/index.js';
4 | import {getTtlFromExpires, getCascadingTtl, calculateTtlFromExpiration} from '../src/ttl.js';
5 | import {sleep} from './sleep.js';
6 |
7 | test('should set a value with ttl', async () => {
8 | const data = {
9 | key: faker.string.uuid(),
10 | value: faker.string.uuid(),
11 | };
12 | const cacheable = new Cacheable({ttl: 100});
13 | await cacheable.set(data.key, data.value);
14 | await sleep(150);
15 | const result = await cacheable.get(data.key);
16 | expect(result).toBeUndefined();
17 | });
18 |
19 | test('should set a ttl on parameter', {timeout: 2000}, async () => {
20 | const cacheable = new Cacheable({ttl: 50});
21 | await cacheable.set('key', 'value', 1000);
22 | await sleep(100);
23 | const result = await cacheable.get('key');
24 | expect(result).toEqual('value');
25 | });
26 |
27 | test('should get the ttl from expires', () => {
28 | const now = Date.now();
29 | const expires = now + 2000;
30 | const result = getTtlFromExpires(expires);
31 | expect(result).toBeGreaterThan(1995);
32 | expect(result).toBeLessThan(2005);
33 | });
34 |
35 | test('should get undefined when expires is undefined', () => {
36 | const result = getTtlFromExpires(undefined);
37 | expect(result).toBeUndefined();
38 | });
39 |
40 | test('should get undefined when expires is in the past', () => {
41 | const now = Date.now();
42 | const expires = now - 1000;
43 | const result = getTtlFromExpires(expires);
44 | expect(result).toBeUndefined();
45 | });
46 |
47 | test('should cascade ttl from secondary', () => {
48 | const result = getCascadingTtl(1000, undefined, 3000);
49 | expect(result).toBe(3000);
50 | });
51 |
52 | test('should cascade ttl from primary', () => {
53 | const result = getCascadingTtl(1000, 2000);
54 | expect(result).toBe(2000);
55 | });
56 |
57 | test('should cascade ttl from cacheable', () => {
58 | const result = getCascadingTtl(1000, undefined, undefined);
59 | expect(result).toBe(1000);
60 | });
61 |
62 | test('should cascade ttl with shorthand on cacheable', () => {
63 | const result = getCascadingTtl('1s', undefined, undefined);
64 | expect(result).toBe(1000);
65 | });
66 |
67 | test('should calculate and choose the ttl as it is lower', () => {
68 | const now = Date.now();
69 | const expires = now + 3000;
70 | const ttl = 2000;
71 | const result = calculateTtlFromExpiration(ttl, expires);
72 | expect(result).toBeLessThan(2002);
73 | expect(result).toBeGreaterThan(1998);
74 | });
75 |
76 | test('should calculate and choose the expires ttl as it is lower', () => {
77 | const now = Date.now();
78 | const expires = now + 1000;
79 | const ttl = 2000;
80 | const result = calculateTtlFromExpiration(ttl, expires);
81 | expect(result).toBeLessThan(1002);
82 | expect(result).toBeGreaterThan(998);
83 | });
84 |
85 | test('should calculate and choose ttl as expires is undefined', () => {
86 | const ttl = 2000;
87 | const result = calculateTtlFromExpiration(ttl, undefined);
88 | expect(result).toBeLessThan(2002);
89 | expect(result).toBeGreaterThan(1998);
90 | });
91 |
92 | test('should calculate and choose expires as ttl is undefined', () => {
93 | const now = Date.now();
94 | const expires = now + 1000;
95 | const result = calculateTtlFromExpiration(undefined, expires);
96 | expect(result).toBe(1000);
97 | expect(result).toBeLessThan(1002);
98 | expect(result).toBeGreaterThan(998);
99 | });
100 |
101 | test('should calculate and choose undefined as both are undefined', () => {
102 | const result = calculateTtlFromExpiration(undefined, undefined);
103 | expect(result).toBeUndefined();
104 | });
105 |
--------------------------------------------------------------------------------
/packages/cacheable/test/wrap.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2 | import {
3 | describe, it, expect, vi,
4 | } from 'vitest';
5 | import {Cacheable, CacheableMemory} from '../src/index.js';
6 | import {
7 | wrap, createWrapKey, wrapSync, type WrapOptions, type WrapSyncOptions,
8 | } from '../src/wrap.js';
9 | import {sleep} from './sleep.js';
10 |
11 | describe('wrap function', () => {
12 | it('should cache asynchronous function results', async () => {
13 | const asyncFunction = async (a: number, b: number) => a + b;
14 | const cache = new Cacheable();
15 |
16 | const options: WrapOptions = {
17 | keyPrefix: 'cacheKey',
18 | cache,
19 | };
20 |
21 | // Wrap the async function
22 | const wrapped = wrap(asyncFunction, options);
23 |
24 | // Call the wrapped function
25 | const result = await wrapped(1, 2);
26 |
27 | // Expectations
28 | expect(result).toBe(3);
29 | const cacheKey = createWrapKey(asyncFunction, [1, 2], options.keyPrefix);
30 | const cacheResult = await cache.get(cacheKey);
31 | expect(cacheResult).toBe(3);
32 | });
33 |
34 | it('should return cached async value with hash', async () => {
35 | // Mock cache and async function
36 | const asyncFunction = async (value: number) => Math.random() * value;
37 | const cache = new Cacheable();
38 |
39 | const options: WrapOptions = {
40 | cache,
41 | };
42 |
43 | // Wrap the async function
44 | const wrapped = wrap(asyncFunction, options);
45 |
46 | // Call the wrapped function
47 | const result = await wrapped(12);
48 | const result2 = await wrapped(12);
49 | // Expectations
50 | expect(result).toBe(result2);
51 | });
52 |
53 | it('should cache synchronous function results', () => {
54 | // Mock cache and sync function
55 | const syncFunction = (value: number) => Math.random() * value;
56 | const cache = new CacheableMemory();
57 | const options: WrapSyncOptions = {
58 | cache,
59 | };
60 |
61 | // Wrap the sync function
62 | const wrapped = wrapSync(syncFunction, options);
63 |
64 | // Call the wrapped function
65 | const result = wrapped(1, 2);
66 | const result2 = wrapped(1, 2);
67 |
68 | // Expectations
69 | expect(result).toBe(result2);
70 | const cacheKey = createWrapKey(syncFunction, [1, 2], options.keyPrefix);
71 | const cacheResult = cache.get(cacheKey);
72 | expect(cacheResult).toBe(result);
73 | });
74 |
75 | it('should cache synchronous function results with hash', () => {
76 | // Mock cache and sync function
77 | const syncFunction = (value: number) => Math.random() * value;
78 | const cache = new CacheableMemory();
79 | const options: WrapSyncOptions = {
80 | keyPrefix: 'testPrefix',
81 | cache,
82 | };
83 |
84 | // Wrap the sync function
85 | const wrapped = wrapSync(syncFunction, options);
86 |
87 | // Call the wrapped function
88 | const result = wrapped(1, 2);
89 | const result2 = wrapped(1, 2);
90 |
91 | // Expectations
92 | expect(result).toBe(result2);
93 | });
94 |
95 | it('should cache synchronous function results with key and ttl', async () => {
96 | // Mock cache and sync function
97 | const syncFunction = (value: number) => Math.random() * value;
98 | const cache = new CacheableMemory();
99 | const options: WrapSyncOptions = {
100 | cache,
101 | ttl: 10,
102 | keyPrefix: 'cacheKey',
103 | };
104 |
105 | // Wrap the sync function
106 | const wrapped = wrapSync(syncFunction, options);
107 |
108 | // Call the wrapped function
109 | const result = wrapped(1, 2);
110 | const result2 = wrapped(1, 2);
111 |
112 | // Expectations
113 | expect(result).toBe(result2);
114 | await sleep(30);
115 | const cacheKey = createWrapKey(syncFunction, [1, 2], options.keyPrefix);
116 | const cacheResult = cache.get(cacheKey);
117 | expect(cacheResult).toBe(undefined);
118 | });
119 |
120 | it('should cache synchronous function results with complex args', async () => {
121 | // Mock cache and sync function
122 | const syncFunction = (value: number, person: {first: string; last: string; meta: any}) => Math.random() * value;
123 | const cache = new CacheableMemory();
124 | const options: WrapSyncOptions = {
125 | keyPrefix: 'cacheKey',
126 | cache,
127 | };
128 |
129 | // Wrap the sync function
130 | const wrapped = wrapSync(syncFunction, options);
131 |
132 | // Call the wrapped function
133 | const result = wrapped(1, {first: 'John', last: 'Doe', meta: {age: 30}});
134 | const result2 = wrapped(1, {first: 'John', last: 'Doe', meta: {age: 30}});
135 |
136 | // Expectations
137 | expect(result).toBe(result2);
138 | });
139 |
140 | it('should cache synchronous function results with complex args and shorthand ttl', async () => {
141 | // Mock cache and sync function
142 | const syncFunction = (value: number, person: {first: string; last: string; meta: any}) => Math.random() * value;
143 | const cache = new CacheableMemory();
144 | const options: WrapSyncOptions = {
145 | cache,
146 | ttl: '100ms',
147 | keyPrefix: 'cacheKey',
148 | };
149 |
150 | // Wrap the sync function
151 | const wrapped = wrapSync(syncFunction, options);
152 |
153 | // Call the wrapped function
154 | const result = wrapped(1, {first: 'John', last: 'Doe', meta: {age: 30}});
155 | const result2 = wrapped(1, {first: 'John', last: 'Doe', meta: {age: 30}});
156 |
157 | // Expectations
158 | expect(result).toBe(result2);
159 | await sleep(200);
160 | const cacheKey = createWrapKey(wrapSync, [1, {first: 'John', last: 'Doe', meta: {age: 30}}], options.keyPrefix);
161 | const cacheResult = cache.get(cacheKey);
162 | expect(cacheResult).toBe(undefined);
163 | });
164 | });
165 |
166 | describe('wrap function with stampede protection', () => {
167 | it('should only execute the wrapped function once when called concurrently with the same key', async () => {
168 | const cache = new Cacheable();
169 | const mockFunction = vi.fn().mockResolvedValue('result');
170 | const mockedKey = createWrapKey(mockFunction, ['arg1'], 'test');
171 | const wrappedFunction = wrap(mockFunction, {cache, keyPrefix: 'test'});
172 |
173 | // Call the wrapped function concurrently
174 | const [result1, result2, result3, result4] = await Promise.all([wrappedFunction('arg1'), wrappedFunction('arg1'), wrappedFunction('arg2'), wrappedFunction('arg2')]);
175 |
176 | // Verify that the wrapped function was only called two times do to arg1 and arg2
177 | expect(mockFunction).toHaveBeenCalledTimes(2);
178 |
179 | // Verify that both calls returned the same result
180 | expect(result1).toBe('result');
181 | expect(result2).toBe('result');
182 | expect(result3).toBe('result');
183 |
184 | // Verify that the result was cached
185 | expect(await cache.has(mockedKey)).toBe(true);
186 | });
187 |
188 | it('should handle error if the function fails', async () => {
189 | const cache = new Cacheable();
190 | const mockFunction = vi.fn().mockRejectedValue(new Error('Function failed'));
191 | const mockedKey = createWrapKey(mockFunction, ['arg1'], 'test');
192 | const wrappedFunction = wrap(mockFunction, {cache, keyPrefix: 'test'});
193 |
194 | await wrappedFunction('arg1');
195 |
196 | // Verify that the wrapped function was only called once
197 | expect(mockFunction).toHaveBeenCalledTimes(1);
198 | });
199 | });
200 |
201 | describe('wrap functions handling thrown errors', () => {
202 | it('wrapSync should emit an error by default and return undefined but not cache errors', () => {
203 | const cache = new CacheableMemory();
204 | const options: WrapSyncOptions = {
205 | cache,
206 | ttl: '1s',
207 | keyPrefix: 'cacheKey',
208 | };
209 |
210 | const wrapped = wrapSync(() => {
211 | throw new Error('Test error');
212 | }, options);
213 |
214 | let errorCallCount = 0;
215 |
216 | cache.on('error', error => {
217 | expect(error.message).toBe('Test error');
218 | errorCallCount++;
219 | });
220 |
221 | const result = wrapped();
222 |
223 | expect(result).toBe(undefined);
224 | expect(errorCallCount).toBe(1);
225 | const values = [...cache.items];
226 | expect(values.length).toBe(0);
227 | });
228 |
229 | it('wrapSync should cache the error when the property is set', () => {
230 | const cache = new CacheableMemory();
231 | const options: WrapSyncOptions = {
232 | cache,
233 | ttl: '1s',
234 | keyPrefix: 'cacheKey',
235 | cacheErrors: true,
236 | };
237 |
238 | const wrapped = wrapSync(() => {
239 | throw new Error('Test error');
240 | }, options);
241 |
242 | let errorCallCount = 0;
243 |
244 | cache.on('error', error => {
245 | expect(error.message).toBe('Test error');
246 | errorCallCount++;
247 | });
248 |
249 | wrapped();
250 | wrapped(); // Should be cached
251 |
252 | expect(errorCallCount).toBe(1);
253 | });
254 |
255 | it('wrap should throw an error if the wrapped function throws an error', async () => {
256 | const cache = new Cacheable();
257 | const error = new Error('Test error');
258 | const options: WrapOptions = {
259 | cache,
260 | ttl: '1s',
261 | keyPrefix: 'cacheKey',
262 | };
263 | const wrapped = wrap(() => {
264 | throw error;
265 | }, options);
266 |
267 | let errorCallCount = 0;
268 |
269 | cache.on('error', error_ => {
270 | expect(error_).toBe(error);
271 | errorCallCount++;
272 | });
273 |
274 | expect(await wrapped()).toBe(undefined);
275 | const cacheKey = createWrapKey(() => {
276 | throw error;
277 | }, [], options.keyPrefix);
278 | const result = await cache.get(cacheKey);
279 | expect(result).toBe(undefined);
280 | expect(errorCallCount).toBe(1);
281 | });
282 |
283 | it('wrap should cache the error when the property is set', async () => {
284 | const cache = new Cacheable();
285 | const error = new Error('Test error');
286 | const options: WrapOptions = {
287 | cache,
288 | ttl: '1s',
289 | keyPrefix: 'cacheKey',
290 | cacheErrors: true,
291 | };
292 | const wrapped = wrap(() => {
293 | throw error;
294 | }, options);
295 |
296 | let errorCallCount = 0;
297 |
298 | cache.on('error', error_ => {
299 | expect(error_).toBe(error);
300 | errorCallCount++;
301 | });
302 |
303 | await wrapped();
304 | await wrapped(); // Should be cached
305 |
306 | expect(errorCallCount).toBe(1);
307 | });
308 | });
309 |
--------------------------------------------------------------------------------
/packages/cacheable/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src/**/*"]
4 | }
--------------------------------------------------------------------------------
/packages/cacheable/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
7 |
8 | /* Emit */
9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */
11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */
12 |
13 | /* Interop Constraints */
14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
16 |
17 | /* Type Checking */
18 | "strict": true, /* Enable all strict type-checking options. */
19 |
20 | /* Completeness */
21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */
22 | "lib": [
23 | "ESNext", "DOM"
24 | ]
25 | }
26 | }
--------------------------------------------------------------------------------
/packages/cacheable/vite.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | slowTestThreshold: 750,
6 | coverage: {
7 | reporter: ['json', 'text'],
8 | exclude: ['test', 'src/cacheable-item-types.ts', 'vite.config.ts', 'dist', 'node_modules'],
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/packages/file-entry-cache/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License & © Jared Wray
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to
5 | deal in the Software without restriction, including without limitation the
6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | sell copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/packages/file-entry-cache/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "file-entry-cache",
3 | "version": "10.1.1",
4 | "description": "A lightweight cache for file metadata, ideal for processes that work on a specific set of files and only need to reprocess files that have changed since the last run",
5 | "type": "module",
6 | "main": "./dist/index.cjs",
7 | "module": "./dist/index.js",
8 | "types": "./dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "require": "./dist/index.cjs",
12 | "import": "./dist/index.js"
13 | }
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/jaredwray/cacheable.git",
18 | "directory": "packages/file-entry-cache"
19 | },
20 | "author": "Jared Wray ",
21 | "license": "MIT",
22 | "private": false,
23 | "keywords": [
24 | "file cache",
25 | "task cache files",
26 | "file cache",
27 | "key par",
28 | "key value",
29 | "cache"
30 | ],
31 | "scripts": {
32 | "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
33 | "prepublish": "pnpm build",
34 | "test": "xo --fix && vitest run --coverage",
35 | "test:ci": "xo && vitest run",
36 | "clean": "rimraf ./dist ./coverage ./node_modules"
37 | },
38 | "devDependencies": {
39 | "@types/node": "^22.15.30",
40 | "@vitest/coverage-v8": "^3.2.2",
41 | "rimraf": "^6.0.1",
42 | "tsup": "^8.5.0",
43 | "typescript": "^5.8.3",
44 | "vitest": "^3.2.2",
45 | "xo": "^1.1.0"
46 | },
47 | "dependencies": {
48 | "flat-cache": "workspace:^"
49 | },
50 | "files": [
51 | "dist",
52 | "license"
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/packages/file-entry-cache/test/eslint.test.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs';
2 | import path from 'node:path';
3 | import {
4 | describe, test, expect, beforeEach, afterEach,
5 | } from 'vitest';
6 | import fileEntryCache from '../src/index.js';
7 |
8 | describe('eslint tests scenarios', () => {
9 | const fileCacheName = 'eslint-files';
10 | const eslintCacheName = '.eslintcache';
11 | const eslintDirectory = 'cache';
12 | const useCheckSum = true;
13 | beforeEach(() => {
14 | // Generate files for testing
15 | fs.mkdirSync(path.resolve(`./${fileCacheName}`));
16 | fs.writeFileSync(path.resolve(`./${fileCacheName}/test1.txt`), 'test');
17 | fs.writeFileSync(path.resolve(`./${fileCacheName}/test2.txt`), 'test sdfljsdlfjsdflsj');
18 | fs.writeFileSync(path.resolve(`./${fileCacheName}/test3.txt`), 'test3');
19 | // Src files
20 | fs.mkdirSync(path.resolve(`./${fileCacheName}/src`));
21 | fs.writeFileSync(path.resolve(`./${fileCacheName}/src/my-file.js`), 'var foo = \'bar\';\r\n');
22 | });
23 |
24 | afterEach(() => {
25 | fs.rmSync(path.resolve(`./${fileCacheName}`), {recursive: true, force: true});
26 | fs.rmSync(path.resolve(`./${eslintDirectory}`), {recursive: true, force: true});
27 | });
28 | test('about to do absolute paths', () => {
29 | // Make sure the cache doesnt exist before we start
30 | fs.rmSync(path.resolve(`./${eslintDirectory}`), {recursive: true, force: true});
31 | // This is setting .eslintcache with cache directory
32 | const cache = fileEntryCache.create(eslintCacheName, eslintDirectory, useCheckSum);
33 | const myFileJavascriptPath = path.resolve(`./${fileCacheName}/src/my-file.js`); // Absolute path
34 | const myFileJavascriptDescriptor = cache.getFileDescriptor(myFileJavascriptPath);
35 | expect(myFileJavascriptDescriptor.key).toBe(myFileJavascriptPath);
36 | expect(myFileJavascriptDescriptor.meta).toBeDefined();
37 | expect(myFileJavascriptDescriptor.changed).toBe(true); // First run
38 | expect(myFileJavascriptDescriptor.meta?.hash).toBeDefined();
39 |
40 | // Now lets set the data and reconcile
41 | if (myFileJavascriptDescriptor.meta) {
42 | myFileJavascriptDescriptor.meta.data = {foo: 'bar'};
43 | }
44 |
45 | // Reconcile
46 | cache.reconcile();
47 |
48 | // Verify that the data is set
49 | const myFileJavascriptData = cache.getFileDescriptor(myFileJavascriptPath);
50 | expect(myFileJavascriptData.meta.data).toStrictEqual({foo: 'bar'});
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/packages/file-entry-cache/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
7 |
8 | /* Emit */
9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */
11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */
12 |
13 | /* Interop Constraints */
14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
16 |
17 | /* Type Checking */
18 | "strict": true, /* Enable all strict type-checking options. */
19 |
20 | /* Completeness */
21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */
22 | "lib": [
23 | "ESNext", "DOM"
24 | ]
25 | }
26 | }
--------------------------------------------------------------------------------
/packages/file-entry-cache/vite.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | coverage: {
6 | reporter: ['json', 'text'],
7 | exclude: ['test', 'vite.config.ts', 'dist', 'node_modules'],
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/packages/flat-cache/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License & © Jared Wray
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to
5 | deal in the Software without restriction, including without limitation the
6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | sell copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/packages/flat-cache/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flat-cache",
3 | "version": "6.1.10",
4 | "description": "A simple key/value storage using files to persist the data",
5 | "type": "module",
6 | "main": "./dist/index.cjs",
7 | "module": "./dist/index.js",
8 | "types": "./dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "require": "./dist/index.cjs",
12 | "import": "./dist/index.js"
13 | }
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/jaredwray/cacheable.git",
18 | "directory": "packages/flat-cache"
19 | },
20 | "author": "Jared Wray ",
21 | "license": "MIT",
22 | "private": false,
23 | "keywords": [
24 | "cache",
25 | "caching",
26 | "cacheable",
27 | "flat-cache",
28 | "flat",
29 | "file",
30 | "file-cache",
31 | "file-caching",
32 | "file-based-cache",
33 | "file-persist",
34 | "file-persistence",
35 | "file-storage",
36 | "file-system",
37 | "file-management",
38 | "filesystem-cache",
39 | "disk-cache",
40 | "cache-persistence",
41 | "cache-persist",
42 | "persistent-cache",
43 | "persistent-storage",
44 | "cache-to-file",
45 | "cache-on-disk",
46 | "cache-file",
47 | "cache-expiration",
48 | "cache-lifetime",
49 | "data-persistence",
50 | "data-storage",
51 | "local-storage",
52 | "file-system-cache"
53 | ],
54 | "scripts": {
55 | "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
56 | "prepublish": "pnpm build",
57 | "test": "xo --fix && vitest run --coverage",
58 | "test:ci": "xo && vitest run",
59 | "clean": "rimraf ./dist ./coverage ./node_modules"
60 | },
61 | "devDependencies": {
62 | "@types/node": "^22.15.30",
63 | "@vitest/coverage-v8": "^3.2.2",
64 | "rimraf": "^6.0.1",
65 | "tsup": "^8.5.0",
66 | "typescript": "^5.8.3",
67 | "vitest": "^3.2.2",
68 | "xo": "^1.1.0"
69 | },
70 | "dependencies": {
71 | "cacheable": "workspace:^",
72 | "flatted": "^3.3.3",
73 | "hookified": "^1.9.1"
74 | },
75 | "files": [
76 | "dist",
77 | "license"
78 | ]
79 | }
80 |
--------------------------------------------------------------------------------
/packages/flat-cache/test/fixtures/.cache/cache1:
--------------------------------------------------------------------------------
1 | [{"key1":"1","key2":"2","key3":"3","key4":"4"},"value1","value2",{"bar":"5"},{"foo":"6"},"bar2","foo2"]
--------------------------------------------------------------------------------
/packages/flat-cache/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
7 |
8 | /* Emit */
9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */
11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */
12 |
13 | /* Interop Constraints */
14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
16 |
17 | /* Type Checking */
18 | "strict": true, /* Enable all strict type-checking options. */
19 |
20 | /* Completeness */
21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */
22 | "lib": [
23 | "ESNext", "DOM"
24 | ]
25 | }
26 | }
--------------------------------------------------------------------------------
/packages/flat-cache/vite.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | coverage: {
6 | reporter: ['json', 'text'],
7 | exclude: ['test', 'vite.config.ts', 'dist', 'node_modules'],
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/packages/node-cache/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredwray/cacheable/bc5387b7f0d99ac4c19a2dc69619db9163355fbe/packages/node-cache/.DS_Store
--------------------------------------------------------------------------------
/packages/node-cache/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License & © Jared Wray
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to
5 | deal in the Software without restriction, including without limitation the
6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | sell copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/packages/node-cache/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cacheable/node-cache",
3 | "version": "1.5.6",
4 | "description": "Simple and Maintained fast NodeJS internal caching",
5 | "type": "module",
6 | "main": "./dist/index.cjs",
7 | "module": "./dist/index.js",
8 | "types": "./dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "require": "./dist/index.cjs",
12 | "import": "./dist/index.js"
13 | }
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/jaredwray/cacheable.git",
18 | "directory": "packages/node-cache"
19 | },
20 | "author": "Jared Wray ",
21 | "license": "MIT",
22 | "private": false,
23 | "keywords": [
24 | "cache",
25 | "caching",
26 | "node",
27 | "nodejs",
28 | "cacheable",
29 | "cacheable-node-cache",
30 | "node-cache",
31 | "cacheable-node"
32 | ],
33 | "scripts": {
34 | "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
35 | "prepublish": "pnpm build",
36 | "test": "xo --fix && vitest run --coverage",
37 | "test:ci": "xo && vitest run",
38 | "clean": "rimraf ./dist ./coverage ./node_modules"
39 | },
40 | "devDependencies": {
41 | "@types/node": "^22.15.30",
42 | "@vitest/coverage-v8": "^3.2.2",
43 | "rimraf": "^6.0.1",
44 | "tsup": "^8.5.0",
45 | "typescript": "^5.8.3",
46 | "vitest": "^3.2.2",
47 | "xo": "^1.1.0"
48 | },
49 | "dependencies": {
50 | "cacheable": "workspace:^",
51 | "hookified": "^1.9.1",
52 | "keyv": "^5.3.3"
53 | },
54 | "files": [
55 | "dist",
56 | "license"
57 | ]
58 | }
59 |
--------------------------------------------------------------------------------
/packages/node-cache/src/store.ts:
--------------------------------------------------------------------------------
1 | import {Cacheable, CacheableMemory, type CacheableItem} from 'cacheable';
2 | import {Keyv} from 'keyv';
3 | import {type NodeCacheItem} from 'index.js';
4 | import {Hookified} from 'hookified';
5 |
6 | export type NodeCacheStoreOptions = {
7 | /**
8 | * Time to live in milliseconds. This is a breaking change from the original NodeCache.
9 | */
10 | ttl?: number | string;
11 | /**
12 | * Maximum number of keys to store in the cache. If this is set to a value greater than 0, the cache will keep track of the number of keys and will not store more than the specified number of keys.
13 | */
14 | maxKeys?: number;
15 | /**
16 | * Primary cache store.
17 | */
18 | primary?: Keyv;
19 | /**
20 | * Secondary cache store. Learn more about the secondary cache store in the cacheable documentation.
21 | * [storage-tiering-and-caching](https://github.com/jaredwray/cacheable/tree/main/packages/cacheable#storage-tiering-and-caching)
22 | */
23 | secondary?: Keyv;
24 |
25 | /**
26 | * Enable stats tracking. This is a breaking change from the original NodeCache.
27 | */
28 | stats?: boolean;
29 | };
30 |
31 | export class NodeCacheStore extends Hookified {
32 | private _maxKeys = 0;
33 | private readonly _cache = new Cacheable({primary: new Keyv({store: new CacheableMemory()})});
34 | constructor(options?: NodeCacheStoreOptions) {
35 | super();
36 | if (options) {
37 | const cacheOptions = {
38 | ttl: options.ttl,
39 | primary: options.primary,
40 | secondary: options.secondary,
41 | stats: options.stats ?? true,
42 | };
43 |
44 | this._cache = new Cacheable(cacheOptions);
45 |
46 | if (options.maxKeys) {
47 | this._maxKeys = options.maxKeys;
48 | }
49 | }
50 |
51 | // Hook up the cacheable events
52 | this._cache.on('error', (error: Error) => {
53 | /* c8 ignore next 1 */
54 | this.emit('error', error);
55 | });
56 | }
57 |
58 | /**
59 | * Cacheable instance.
60 | * @returns {Cacheable}
61 | * @readonly
62 | */
63 | public get cache(): Cacheable {
64 | return this._cache;
65 | }
66 |
67 | /**
68 | * Time to live in milliseconds.
69 | * @returns {number | string | undefined}
70 | * @readonly
71 | */
72 | public get ttl(): number | string | undefined {
73 | return this._cache.ttl;
74 | }
75 |
76 | /**
77 | * Time to live in milliseconds.
78 | * @param {number | string | undefined} ttl
79 | */
80 | public set ttl(ttl: number | string | undefined) {
81 | this._cache.ttl = ttl;
82 | }
83 |
84 | /**
85 | * Primary cache store.
86 | * @returns {Keyv}
87 | * @readonly
88 | */
89 | public get primary(): Keyv {
90 | return this._cache.primary;
91 | }
92 |
93 | /**
94 | * Primary cache store.
95 | * @param {Keyv} primary
96 | */
97 | public set primary(primary: Keyv) {
98 | this._cache.primary = primary;
99 | }
100 |
101 | /**
102 | * Secondary cache store. Learn more about the secondary cache store in the
103 | * [cacheable](https://github.com/jaredwray/cacheable/tree/main/packages/cacheable#storage-tiering-and-caching) documentation.
104 | * @returns {Keyv | undefined}
105 | */
106 | public get secondary(): Keyv | undefined {
107 | return this._cache.secondary;
108 | }
109 |
110 | /**
111 | * Secondary cache store. Learn more about the secondary cache store in the
112 | * [cacheable](https://github.com/jaredwray/cacheable/tree/main/packages/cacheable#storage-tiering-and-caching) documentation.
113 | * @param {Keyv | undefined} secondary
114 | */
115 | public set secondary(secondary: Keyv | undefined) {
116 | this._cache.secondary = secondary;
117 | }
118 |
119 | /**
120 | * Maximum number of keys to store in the cache. if this is set to a value greater than 0,
121 | * the cache will keep track of the number of keys and will not store more than the specified number of keys.
122 | * @returns {number}
123 | * @readonly
124 | */
125 | public get maxKeys(): number {
126 | return this._maxKeys;
127 | }
128 |
129 | /**
130 | * Maximum number of keys to store in the cache. if this is set to a value greater than 0,
131 | * the cache will keep track of the number of keys and will not store more than the specified number of keys.
132 | * @param {number} maxKeys
133 | */
134 | public set maxKeys(maxKeys: number) {
135 | this._maxKeys = maxKeys;
136 | if (this._maxKeys > 0) {
137 | this._cache.stats.enabled = true;
138 | }
139 | }
140 |
141 | /**
142 | * Set a key/value pair in the cache.
143 | * @param {string | number} key
144 | * @param {any} value
145 | * @param {number} [ttl]
146 | * @returns {boolean}
147 | */
148 | public async set(key: string | number, value: any, ttl?: number): Promise {
149 | if (this._maxKeys > 0) {
150 | // eslint-disable-next-line unicorn/no-lonely-if
151 | if (this._cache.stats.count >= this._maxKeys) {
152 | return false;
153 | }
154 | }
155 |
156 | await this._cache.set(key.toString(), value, ttl);
157 | return true;
158 | }
159 |
160 | /**
161 | * Set multiple key/value pairs in the cache.
162 | * @param {NodeCacheItem[]} list
163 | * @returns {void}
164 | */
165 | public async mset(list: NodeCacheItem[]): Promise {
166 | const items = new Array();
167 | for (const item of list) {
168 | items.push({key: item.key.toString(), value: item.value, ttl: item.ttl});
169 | }
170 |
171 | await this._cache.setMany(items);
172 | }
173 |
174 | /**
175 | * Get a value from the cache.
176 | * @param {string | number} key
177 | * @returns {any | undefined}
178 | */
179 | public async get(key: string | number): Promise {
180 | return this._cache.get(key.toString());
181 | }
182 |
183 | /**
184 | * Get multiple values from the cache.
185 | * @param {Array} keys
186 | * @returns {Record}
187 | */
188 | public async mget(keys: Array): Promise> {
189 | const result: Record = {};
190 | for (const key of keys) {
191 | // eslint-disable-next-line no-await-in-loop
192 | result[key.toString()] = await this._cache.get(key.toString());
193 | }
194 |
195 | return result;
196 | }
197 |
198 | /**
199 | * Delete a key from the cache.
200 | * @param {string | number} key
201 | * @returns {boolean}
202 | */
203 | public async del(key: string | number): Promise {
204 | return this._cache.delete(key.toString());
205 | }
206 |
207 | /**
208 | * Delete multiple keys from the cache.
209 | * @param {Array} keys
210 | * @returns {boolean}
211 | */
212 | public async mdel(keys: Array): Promise {
213 | return this._cache.deleteMany(keys.map(key => key.toString()));
214 | }
215 |
216 | /**
217 | * Clear the cache.
218 | * @returns {void}
219 | */
220 | public async clear(): Promise {
221 | return this._cache.clear();
222 | }
223 |
224 | /**
225 | * Check if a key exists in the cache.
226 | * @param {string | number} key
227 | * @returns {boolean}
228 | */
229 | public async setTtl(key: string | number, ttl?: number): Promise {
230 | const item = await this._cache.get(key.toString());
231 | if (item) {
232 | await this._cache.set(key.toString(), item, ttl);
233 | return true;
234 | }
235 |
236 | return false;
237 | }
238 |
239 | /**
240 | * Check if a key exists in the cache. If it does exist it will get the value and delete the item from the cache.
241 | * @param {string | number} key
242 | * @returns {any | undefined}
243 | */
244 | public async take(key: string | number): Promise {
245 | return this._cache.take(key.toString());
246 | }
247 |
248 | /**
249 | * Disconnect from the cache.
250 | * @returns {void}
251 | */
252 | public async disconnect(): Promise {
253 | await this._cache.disconnect();
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/packages/node-cache/test/export.test.ts:
--------------------------------------------------------------------------------
1 | import {describe, test, expect} from 'vitest';
2 | import {NodeCache} from '../src/index.js';
3 |
4 | const cache = new NodeCache({checkperiod: 0});
5 |
6 | describe('NodeCache', () => {
7 | test('should create a new instance of NodeCache', () => {
8 | const cache = new NodeCache({checkperiod: 0});
9 | expect(cache).toBeInstanceOf(NodeCache);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/node-cache/test/store.test.ts:
--------------------------------------------------------------------------------
1 | import {describe, test, expect} from 'vitest';
2 | import {Keyv} from 'keyv';
3 | import {NodeCacheStore} from '../src/store.js';
4 |
5 | // eslint-disable-next-line no-promise-executor-return
6 | const sleep = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
7 |
8 | describe('NodeCacheStore', () => {
9 | test('should create a new instance', () => {
10 | const store = new NodeCacheStore();
11 | expect(store).toBeDefined();
12 | });
13 | test('should create a new instance with options', () => {
14 | const store = new NodeCacheStore({maxKeys: 100});
15 | expect(store.maxKeys).toBe(100);
16 | store.maxKeys = 200;
17 | expect(store.maxKeys).toBe(200);
18 | });
19 | test('should set a ttl', () => {
20 | const store = new NodeCacheStore({ttl: 100});
21 | expect(store.ttl).toBe(100);
22 | store.ttl = 200;
23 | expect(store.ttl).toBe(200);
24 | });
25 | test('should set a primary keyv store', () => {
26 | const store = new NodeCacheStore();
27 | expect(store.primary).toBeDefined();
28 | const keyv = new Keyv();
29 | store.primary = keyv;
30 | expect(store.primary).toBe(keyv);
31 | });
32 | test('should set a secondary keyv store', () => {
33 | const store = new NodeCacheStore();
34 | expect(store.secondary).toBeUndefined();
35 | const keyv = new Keyv();
36 | store.secondary = keyv;
37 | expect(store.secondary).toBe(keyv);
38 | });
39 | test('should be able to get and set primary and secondary keyv stores', async () => {
40 | const store = new NodeCacheStore();
41 | expect(store.primary).toBeDefined();
42 | expect(store.secondary).toBeUndefined();
43 | const primary = new Keyv();
44 | const secondary = new Keyv();
45 | store.primary = primary;
46 | store.secondary = secondary;
47 | expect(store.primary).toBe(primary);
48 | expect(store.secondary).toBe(secondary);
49 | await store.set('test', 'value');
50 | const restult1 = await store.get('test');
51 | expect(restult1).toBe('value');
52 | await store.set('test', 'value', 100);
53 | const restult2 = await store.get('test');
54 | expect(restult2).toBe('value');
55 | await sleep(200);
56 | const restult3 = await store.get('test');
57 | expect(restult3).toBeUndefined();
58 | });
59 | test('should set a maxKeys limit', async () => {
60 | const store = new NodeCacheStore({maxKeys: 3});
61 | expect(store.maxKeys).toBe(3);
62 | expect(store.cache.stats.enabled).toBe(true);
63 | await store.set('test1', 'value1');
64 | await store.set('test2', 'value2');
65 | await store.set('test3', 'value3');
66 | await store.set('test4', 'value4');
67 | const result1 = await store.get('test4');
68 | expect(result1).toBeUndefined();
69 | });
70 | test('should clear the cache', async () => {
71 | const store = new NodeCacheStore();
72 | await store.set('test', 'value');
73 | await store.clear();
74 | const result1 = await store.get('test');
75 | expect(result1).toBeUndefined();
76 | });
77 | test('should delete a key', async () => {
78 | const store = new NodeCacheStore();
79 | await store.set('test', 'value');
80 | await store.del('test');
81 | const result1 = await store.get('test');
82 | expect(result1).toBeUndefined();
83 | });
84 | test('should be able to get and set an object', async () => {
85 | const store = new NodeCacheStore();
86 | await store.set('test', {foo: 'bar'});
87 | const result1 = await store.get('test');
88 | expect(result1).toEqual({foo: 'bar'});
89 | });
90 | test('should be able to get and set an array', async () => {
91 | const store = new NodeCacheStore();
92 | await store.set('test', ['foo', 'bar']);
93 | const result1 = await store.get('test');
94 | expect(result1).toEqual(['foo', 'bar']);
95 | });
96 | test('should be able to get and set a number', async () => {
97 | const store = new NodeCacheStore();
98 | await store.set('test', 123);
99 | const result1 = await store.get('test');
100 | expect(result1).toBe(123);
101 | });
102 | test('should be able to set multiple keys', async () => {
103 | const store = new NodeCacheStore();
104 | await store.mset([
105 | {key: 'test1', value: 'value1'},
106 | {key: 'test2', value: 'value2'},
107 | ]);
108 | const result1 = await store.get('test1');
109 | const result2 = await store.get('test2');
110 | expect(result1).toBe('value1');
111 | expect(result2).toBe('value2');
112 | });
113 | test('should be able to get multiple keys', async () => {
114 | const store = new NodeCacheStore();
115 | await store.set('test1', 'value1');
116 | await store.set('test2', 'value2');
117 | const result1 = await store.mget(['test1', 'test2']);
118 | expect(result1).toEqual({test1: 'value1', test2: 'value2'});
119 | });
120 | test('should be able to delete multiple keys', async () => {
121 | const store = new NodeCacheStore();
122 | await store.set('test1', 'value1');
123 | await store.set('test2', 'value2');
124 | await store.mdel(['test1', 'test2']);
125 | const result1 = await store.get('test1');
126 | const result2 = await store.get('test2');
127 | expect(result1).toBeUndefined();
128 | expect(result2).toBeUndefined();
129 | });
130 | test('should be able to set a key with ttl', async () => {
131 | const store = new NodeCacheStore();
132 | await store.set('test', 'value', 1000);
133 | const result1 = await store.get('test');
134 | expect(result1).toBe('value');
135 | const result2 = await store.setTtl('test', 1000);
136 | expect(result2).toBe(true);
137 | });
138 | test('should return false if no ttl is set', async () => {
139 | const store = new NodeCacheStore({ttl: 1000});
140 | const result1 = await store.setTtl('test');
141 | expect(result1).toBe(false);
142 | });
143 | test('should be able to disconnect', async () => {
144 | const store = new NodeCacheStore();
145 | await store.disconnect();
146 | });
147 | test('should be able to take a key', async () => {
148 | const store = new NodeCacheStore();
149 | await store.set('test', 'value');
150 | const result1 = await store.take('test');
151 | expect(result1).toBe('value');
152 | const result2 = await store.get('test');
153 | expect(result2).toBeUndefined();
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/packages/node-cache/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
6 | "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
7 |
8 | /* Emit */
9 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
10 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */
11 | "outDir": "./dist", /* Specify an output folder for all emitted files. */
12 |
13 | /* Interop Constraints */
14 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
15 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
16 |
17 | /* Type Checking */
18 | "strict": true, /* Enable all strict type-checking options. */
19 |
20 | /* Completeness */
21 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */
22 | "lib": [
23 | "ESNext", "DOM"
24 | ]
25 | }
26 | }
--------------------------------------------------------------------------------
/packages/node-cache/vite.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | coverage: {
6 | reporter: ['json', 'text'],
7 | exclude: ['test', 'vite.config.ts', 'dist', 'node_modules'],
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/packages/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cacheable/website",
3 | "version": "1.0.0",
4 | "description": "Cacheable Website",
5 | "repository": "https://github.com/jaredwray/cacheable.git",
6 | "author": "Jared Wray ",
7 | "license": "MIT",
8 | "private": true,
9 | "scripts": {
10 | "generate-docs": "npx tsx ./src/docs.cts",
11 | "website:build": "rimraf ./site/docs && pnpm generate-docs && docula build",
12 | "website:serve": "rimraf ./site/docs && pnpm generate-docs && docula serve",
13 | "clean": "rimraf ./dist"
14 | },
15 | "devDependencies": {
16 | "@types/node": "^22.15.30",
17 | "docula": "^0.12.2",
18 | "rimraf": "^6.0.1",
19 | "tsx": "^4.19.4"
20 | },
21 | "dependencies": {
22 | "@types/fs-extra": "^11.0.4",
23 | "fs-extra": "^11.3.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/website/site/docula.config.mjs:
--------------------------------------------------------------------------------
1 | export const options = {
2 | githubPath: 'jaredwray/cacheable',
3 | siteTitle: 'Cacheable',
4 | siteDescription: 'Caching for Node.js',
5 | siteUrl: 'https://cacheable.org',
6 | sections: [
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/packages/website/site/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredwray/cacheable/bc5387b7f0d99ac4c19a2dc69619db9163355fbe/packages/website/site/favicon.ico
--------------------------------------------------------------------------------
/packages/website/site/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/website/site/symbol.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/website/site/variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --font-family: 'Open Sans', sans-serif;
3 |
4 | --color-primary: #fa3c32;
5 | --color-secondary: #ff7800;
6 | --color-secondary-dark: #322d28;
7 | --color-text: #322d28;
8 |
9 | --background: #ffffff;
10 | --home-background: #ffffff;
11 | --header-background: #ffffff;
12 |
13 | --sidebar-background: #ffffff;
14 | --sidebar-text: #322d28;
15 | --sidebar-text-active: #ff7800;
16 |
17 | --border: rgba(238,238,245,1);
18 |
19 | --background-search-highlight: var(--color-secondary-dark);
20 | --color-search-highlight: #ffffff;
21 | --search-input-background: var(--header-background);
22 |
23 | --code: rgba(238,238,245,1);
24 |
25 | --pagefind-ui-text: var(--color-text) !important;
26 | --pagefind-ui-font: var(--font-family) !important;
27 | --pagefind-ui-background: var(--background) !important;
28 | --pagefind-ui-border: var(--border) !important;
29 | --pagefind-ui-scale: .9 !important;
30 | }
--------------------------------------------------------------------------------
/packages/website/src/docs.cts:
--------------------------------------------------------------------------------
1 | import fs from "fs-extra";
2 |
3 | async function main() {
4 |
5 | console.log("packages path:" + getRelativePackagePath());
6 |
7 | await copyPackages();
8 | await copyGettingStarted();
9 | await copyCacheableSymbol();
10 | };
11 |
12 | async function copyPackages() {
13 | const packagesPath = getRelativePackagePath();
14 | const packageList = await fs.promises.readdir(`${packagesPath}`);
15 | const filterList = ["website", ".DS_Store"];
16 |
17 | for (const packageName of packageList) {
18 | if((filterList.indexOf(packageName) > -1) !== true ) {
19 | console.log("Adding package: " + packageName);
20 | await createDoc(packageName, `${packagesPath}`, `${packagesPath}/website/site/docs`);
21 | }
22 | };
23 | }
24 |
25 | async function copyCacheableSymbol() {
26 | const rootPath = getRelativeRootPath();
27 | const packagesPath = getRelativePackagePath();
28 | const outputPath = `${packagesPath}/website/dist`;
29 | await fs.ensureDir(`${outputPath}`);
30 | await fs.copy(`${packagesPath}/website/site/symbol.svg`, `${outputPath}/symbol.svg`);
31 | }
32 |
33 | async function copyGettingStarted() {
34 | console.log("Adding Getting Started");
35 | const rootPath = getRelativeRootPath();
36 | const packagesPath = getRelativePackagePath();
37 | const outputPath = `${packagesPath}/website/site/docs`;
38 | const originalFileText = await fs.readFile(`${rootPath}/README.md`, "utf8");
39 | let newFileText = "---\n";
40 | newFileText += `title: 'Getting Started Guide'\n`;
41 | newFileText += `order: 1\n`;
42 | //newFileText += `parent: '${parent}'\n`;
43 | newFileText += "---\n";
44 | newFileText += "\n";
45 | newFileText += originalFileText;
46 |
47 | newFileText = cleanDocumentFromImage(newFileText);
48 |
49 | await fs.ensureDir(`${outputPath}`);
50 | await fs.writeFile(`${outputPath}/index.md`, newFileText);
51 | }
52 |
53 | function cleanDocumentFromImage(document: string) {
54 | document = document.replace(`[
](https://github.com/jaredwray/cacheable)`, "");
55 | document = document.replace(`[
](https://github.com/jaredwray/cacheable)`, "");
56 | return document;
57 | };
58 |
59 | function getRelativePackagePath() {
60 | if(fs.pathExistsSync("packages")) {
61 | //we are in the root
62 | return "packages";
63 | }
64 |
65 | //we are in the website folder
66 | return "../../packages"
67 | }
68 |
69 | function getRelativeRootPath() {
70 | if(fs.pathExistsSync("packages")) {
71 | //we are in the root
72 | return "./";
73 | }
74 |
75 | //we are in the website folder
76 | return "../../"
77 | }
78 |
79 | async function createDoc(packageName: string, path: string, outputPath: string) {
80 | const originalFileName = "README.md";
81 | const newFileName = `${packageName}.md`;
82 | const packageJSONPath = `${path}/${packageName}/package.json`;
83 | const packageJSON = await fs.readJSON(packageJSONPath);
84 | const originalFileText = await fs.readFile(`${path}/${packageName}/${originalFileName}`, "utf8");
85 | let newFileText = "---\n";
86 | newFileText += `title: '${packageJSON.name}'\n`;
87 | newFileText += `sidebarTitle: '${packageJSON.name}'\n`;
88 | //newFileText += `parent: '${parent}'\n`;
89 | newFileText += "---\n";
90 | newFileText += "\n";
91 | newFileText += originalFileText;
92 |
93 | newFileText = cleanDocumentFromImage(newFileText);
94 |
95 | await fs.ensureDir(`${outputPath}`);
96 | await fs.writeFile(`${outputPath}/${newFileName}`, newFileText);
97 | }
98 |
99 | main();
100 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
--------------------------------------------------------------------------------
/vitest.workspace.ts:
--------------------------------------------------------------------------------
1 | import { defineWorkspace } from 'vitest/config'
2 |
3 | export default defineWorkspace([
4 | "./packages/node-cache/vite.config.ts",
5 | "./packages/cache-manager/vite.config.ts",
6 | "./packages/file-entry-cache/vite.config.ts",
7 | "./packages/flat-cache/vite.config.ts",
8 | "./packages/cacheable-request/vitest.config.mjs",
9 | "./packages/cacheable/vite.config.ts"
10 | ])
11 |
--------------------------------------------------------------------------------