Skip to content

Commit bbf9786

Browse files
committed
memory - creating @cacheable/memory
1 parent 9dffc17 commit bbf9786

12 files changed

Lines changed: 2935 additions & 0 deletions

packages/memory/LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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.

packages/memory/README.md

Lines changed: 708 additions & 0 deletions
Large diffs are not rendered by default.

packages/memory/package.json

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"name": "cacheable",
3+
"version": "1.10.3",
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 <me@jaredwray.com>",
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 --coverage",
28+
"clean": "rimraf ./dist ./coverage ./node_modules"
29+
},
30+
"devDependencies": {
31+
"@faker-js/faker": "^9.9.0",
32+
"@keyv/redis": "^5.0.0",
33+
"@keyv/valkey": "^1.0.7",
34+
"@types/eslint": "^9.6.1",
35+
"@types/node": "^24.1.0",
36+
"@vitest/coverage-v8": "^3.2.4",
37+
"lru-cache": "^11.1.0",
38+
"rimraf": "^6.0.1",
39+
"tsup": "^8.5.0",
40+
"typescript": "^5.8.3",
41+
"vitest": "^3.2.4",
42+
"xo": "^1.2.1"
43+
},
44+
"dependencies": {
45+
"hookified": "^1.10.0",
46+
"keyv": "^5.4.0"
47+
},
48+
"keywords": [
49+
"cacheable",
50+
"high performance",
51+
"layer 1 caching",
52+
"layer 2 caching",
53+
"distributed caching",
54+
"Keyv storage engine",
55+
"memory caching",
56+
"LRU cache",
57+
"expiration",
58+
"CacheableMemory",
59+
"offline support",
60+
"distributed sync",
61+
"secondary store",
62+
"primary store",
63+
"non-blocking operations",
64+
"cache statistics",
65+
"layered caching",
66+
"fault tolerant",
67+
"scalable cache",
68+
"in-memory cache",
69+
"distributed cache",
70+
"lruSize",
71+
"lru",
72+
"multi-tier cache"
73+
],
74+
"files": [
75+
"dist",
76+
"LICENSE"
77+
]
78+
}

packages/memory/src/keyv-memory.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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<string, CacheableMemory>();
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<Value>(key: string): Promise<StoredData<Value> | undefined> {
47+
const result = this.getStore(this._namespace).get<Value>(key);
48+
if (result) {
49+
return result;
50+
}
51+
52+
return undefined;
53+
}
54+
55+
async getMany<Value>(keys: string[]): Promise<Array<StoredData<Value | undefined>>> {
56+
const result = this.getStore(this._namespace).getMany<Value>(keys);
57+
58+
return result;
59+
}
60+
61+
async set(key: string, value: any, ttl?: number): Promise<void> {
62+
this.getStore(this._namespace).set(key, value, ttl);
63+
}
64+
65+
async setMany(values: Array<{key: string; value: any; ttl?: number}>): Promise<void> {
66+
this.getStore(this._namespace).setMany(values);
67+
}
68+
69+
async delete(key: string): Promise<boolean> {
70+
this.getStore(this._namespace).delete(key);
71+
return true;
72+
}
73+
74+
async deleteMany?(key: string[]): Promise<boolean> {
75+
this.getStore(this._namespace).deleteMany(key);
76+
return true;
77+
}
78+
79+
async clear(): Promise<void> {
80+
this.getStore(this._namespace).clear();
81+
}
82+
83+
async has?(key: string): Promise<boolean> {
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+
}

packages/memory/src/memory-lru.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
export class ListNode<T> {
2+
// eslint-disable-next-line @typescript-eslint/parameter-properties
3+
value: T;
4+
prev: ListNode<T> | undefined = undefined;
5+
next: ListNode<T> | undefined = undefined;
6+
7+
constructor(value: T) {
8+
this.value = value;
9+
}
10+
}
11+
12+
export class DoublyLinkedList<T> {
13+
private head: ListNode<T> | undefined = undefined;
14+
private tail: ListNode<T> | undefined = undefined;
15+
private readonly nodesMap = new Map<T, ListNode<T>>();
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+
}

0 commit comments

Comments
 (0)