cache-craft-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/cache-engine.d.ts +44 -0
- package/dist/cache-engine.js +206 -0
- package/dist/index.cjs +211 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +209 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MJ.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# CacheCraft 🛠️
|
|
2
|
+
|
|
3
|
+
**A lightweight, fully client-side IndexedDB cache engine**
|
|
4
|
+
With support for compression (gzip), encoding (base64), LRU eviction, TTL, stale-while-revalidate, and namespaces.
|
|
5
|
+
|
|
6
|
+
- **Framework-agnostic** → React, Vue, Svelte, Vanilla JS, etc.
|
|
7
|
+
- **Persistence** → Data survives page reloads and tab closures
|
|
8
|
+
- **Production-ready** → Smart eviction + size management + automatic compression
|
|
9
|
+
|
|
10
|
+
[](https://www.npmjs.com/package/cache-craft)
|
|
11
|
+
[](https://github.com/MJavadSF/CacheCraft)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Key Features
|
|
16
|
+
|
|
17
|
+
- Persistent storage with **IndexedDB**
|
|
18
|
+
- **LRU eviction** (removes least recently used items when exceeding max size)
|
|
19
|
+
- **gzip compression** automatic for large items (>10KB by default)
|
|
20
|
+
- **Base64 encoding** optional
|
|
21
|
+
- **TTL** (time to live)
|
|
22
|
+
- **Stale-While-Revalidate** → Return stale data quickly + update in the background
|
|
23
|
+
- **Namespaces** for logical separation of caches (e.g., user / media / offline-forms)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install cache-craft
|
|
31
|
+
# Or directly from GitHub (before official release):
|
|
32
|
+
# npm install github:MJavadSF/CacheCraft
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Quick Usage
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { CacheEngine } from 'cache-craft';
|
|
41
|
+
|
|
42
|
+
const cache = new CacheEngine({
|
|
43
|
+
dbName: 'my-app-cache',
|
|
44
|
+
maxSize: 150 * 1024 * 1024, // 150 MB
|
|
45
|
+
compressionThreshold: 8 * 1024, // Compress from 8 KB upwards
|
|
46
|
+
namespace: 'app-v1', // Optional
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Set
|
|
50
|
+
await cache.set('user-profile', { id: 42, name: 'Ali', avatar: '...' }, {
|
|
51
|
+
ttl: 10 * 60 * 1000, // 10 minutes
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Simple get
|
|
55
|
+
const user = await cache.get<{ id: number; name: string }>('user-profile');
|
|
56
|
+
|
|
57
|
+
// Stale-while-revalidate (great for APIs)
|
|
58
|
+
const posts = await cache.get('posts-list', {
|
|
59
|
+
staleWhileRevalidate: true,
|
|
60
|
+
revalidate: async () => {
|
|
61
|
+
const res = await fetch('/api/posts');
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
await cache.set('posts-list', data, { ttl: 300_000 });
|
|
64
|
+
return data;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Separate namespace
|
|
69
|
+
const imageCache = cache.namespace('images');
|
|
70
|
+
await imageCache.set('cover-001', { url: '/assets/cover.jpg', alt: '...' });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Full API
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// Create instance
|
|
79
|
+
const cache = new CacheEngine(config?: CacheConfig);
|
|
80
|
+
|
|
81
|
+
// Set
|
|
82
|
+
await cache.set(key: string, value: any, options?: CacheSetOptions);
|
|
83
|
+
|
|
84
|
+
// Get
|
|
85
|
+
await cache.get<T>(key: string, options?: CacheGetOptions<T>): Promise<T | null>;
|
|
86
|
+
|
|
87
|
+
// Remove single item
|
|
88
|
+
await cache.remove(key: string);
|
|
89
|
+
|
|
90
|
+
// Clear all cache
|
|
91
|
+
await cache.clear();
|
|
92
|
+
|
|
93
|
+
// Create sub-cache with namespace
|
|
94
|
+
const subCache = cache.namespace('prefix');
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### CacheSetOptions
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
{
|
|
101
|
+
ttl?: number; // milliseconds
|
|
102
|
+
encode?: boolean; // Base64 encode?
|
|
103
|
+
forceCompress?: boolean;// Compress even if small
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### CacheGetOptions
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
{
|
|
111
|
+
staleWhileRevalidate?: boolean;
|
|
112
|
+
revalidate?: () => Promise<T>;
|
|
113
|
+
ttlOnRevalidate?: number;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Practical Examples
|
|
120
|
+
|
|
121
|
+
### Cache API Response with TTL
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
async function getUser(id: number) {
|
|
125
|
+
const key = `user:${id}`;
|
|
126
|
+
let data = await cache.get(key);
|
|
127
|
+
|
|
128
|
+
if (!data) {
|
|
129
|
+
const res = await fetch(`/api/users/${id}`);
|
|
130
|
+
data = await res.json();
|
|
131
|
+
await cache.set(key, data, { ttl: 60_000 }); // 1 minute
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Offline Form Draft
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// Save on type
|
|
142
|
+
await cache.set('checkout:draft', formValues, { ttl: 24 * 60 * 60_000 });
|
|
143
|
+
|
|
144
|
+
// On page load
|
|
145
|
+
const saved = await cache.get('checkout:draft');
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Large Data with Forced Compression
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
await cache.set('analytics:2025', hugeJsonData, {
|
|
152
|
+
forceCompress: true,
|
|
153
|
+
ttl: 7 * 24 * 60 * 60_000, // One week
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Best Practices
|
|
160
|
+
|
|
161
|
+
- Always use **namespaces** for different data types
|
|
162
|
+
- Never cache **sensitive data** (tokens, passwords)
|
|
163
|
+
- Choose **appropriate TTL** (long for static, short for APIs)
|
|
164
|
+
- Use **staleWhileRevalidate** for high-traffic pages
|
|
165
|
+
- Set **maxSize** based on user devices (lower for mobile)
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Links
|
|
170
|
+
|
|
171
|
+
- GitHub: [CacheCraft](https://github.com/MJavadSF/CacheCraft/)
|
|
172
|
+
- npm: [cache-craft](https://www.npmjs.com/package/cache-craft)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type CacheEntry<T = any> = {
|
|
2
|
+
value: T | string | Uint8Array;
|
|
3
|
+
isEncoded: boolean;
|
|
4
|
+
isCompressed: boolean;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
lastAccessed: number;
|
|
7
|
+
expiresAt?: number;
|
|
8
|
+
size: number;
|
|
9
|
+
};
|
|
10
|
+
export type CacheSetOptions = {
|
|
11
|
+
ttl?: number;
|
|
12
|
+
encode?: boolean;
|
|
13
|
+
forceCompress?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type CacheGetOptions<T> = {
|
|
16
|
+
staleWhileRevalidate?: boolean;
|
|
17
|
+
revalidate?: () => Promise<T>;
|
|
18
|
+
ttlOnRevalidate?: number;
|
|
19
|
+
};
|
|
20
|
+
export type CacheConfig = {
|
|
21
|
+
dbName?: string;
|
|
22
|
+
version?: number;
|
|
23
|
+
storeName?: string;
|
|
24
|
+
maxSize?: number;
|
|
25
|
+
compressionThreshold?: number;
|
|
26
|
+
namespace?: string;
|
|
27
|
+
};
|
|
28
|
+
export declare class CacheEngine {
|
|
29
|
+
private dbPromise;
|
|
30
|
+
private readonly config;
|
|
31
|
+
constructor(cfg?: CacheConfig);
|
|
32
|
+
private getDB;
|
|
33
|
+
private tx;
|
|
34
|
+
private k;
|
|
35
|
+
private getRaw;
|
|
36
|
+
private putRaw;
|
|
37
|
+
private deleteRaw;
|
|
38
|
+
private evict;
|
|
39
|
+
set<T>(key: string, value: T, opt?: CacheSetOptions): Promise<void>;
|
|
40
|
+
get<T>(key: string, opt?: CacheGetOptions<T>): Promise<T | null>;
|
|
41
|
+
remove(key: string): Promise<void>;
|
|
42
|
+
clear(): Promise<void>;
|
|
43
|
+
namespace(ns: string): CacheEngine;
|
|
44
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// cache-engine.ts
|
|
2
|
+
// ==============================
|
|
3
|
+
// Utils
|
|
4
|
+
// ==============================
|
|
5
|
+
function isClient() {
|
|
6
|
+
return (typeof window !== "undefined" &&
|
|
7
|
+
"indexedDB" in window &&
|
|
8
|
+
"CompressionStream" in window);
|
|
9
|
+
}
|
|
10
|
+
async function compress(data) {
|
|
11
|
+
const cs = new CompressionStream("gzip");
|
|
12
|
+
const writer = cs.writable.getWriter();
|
|
13
|
+
await writer.write(new TextEncoder().encode(data));
|
|
14
|
+
await writer.close();
|
|
15
|
+
const out = await new Response(cs.readable).arrayBuffer();
|
|
16
|
+
return new Uint8Array(out);
|
|
17
|
+
}
|
|
18
|
+
async function decompress(data) {
|
|
19
|
+
const ds = new DecompressionStream("gzip");
|
|
20
|
+
const writer = ds.writable.getWriter();
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
await writer.write(data);
|
|
23
|
+
await writer.close();
|
|
24
|
+
const out = await new Response(ds.readable).arrayBuffer();
|
|
25
|
+
return new TextDecoder().decode(out);
|
|
26
|
+
}
|
|
27
|
+
function encode(v) {
|
|
28
|
+
return btoa(encodeURIComponent(JSON.stringify(v)));
|
|
29
|
+
}
|
|
30
|
+
function decode(v) {
|
|
31
|
+
return JSON.parse(decodeURIComponent(atob(v)));
|
|
32
|
+
}
|
|
33
|
+
// ==============================
|
|
34
|
+
// Cache Engine
|
|
35
|
+
// ==============================
|
|
36
|
+
export class CacheEngine {
|
|
37
|
+
constructor(cfg) {
|
|
38
|
+
this.dbPromise = null;
|
|
39
|
+
this.config = {
|
|
40
|
+
dbName: cfg?.dbName ?? "cache-db",
|
|
41
|
+
version: cfg?.version ?? 1,
|
|
42
|
+
storeName: cfg?.storeName ?? "cache",
|
|
43
|
+
maxSize: cfg?.maxSize ?? 100 * 1024 * 1024,
|
|
44
|
+
compressionThreshold: cfg?.compressionThreshold ?? 10 * 1024,
|
|
45
|
+
namespace: cfg?.namespace ?? "",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// ==============================
|
|
49
|
+
// DB
|
|
50
|
+
// ==============================
|
|
51
|
+
async getDB() {
|
|
52
|
+
if (!isClient())
|
|
53
|
+
throw new Error("IndexedDB unsupported");
|
|
54
|
+
if (!this.dbPromise) {
|
|
55
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
56
|
+
const req = indexedDB.open(this.config.dbName, this.config.version);
|
|
57
|
+
req.onupgradeneeded = () => {
|
|
58
|
+
const db = req.result;
|
|
59
|
+
if (!db.objectStoreNames.contains(this.config.storeName)) {
|
|
60
|
+
db.createObjectStore(this.config.storeName);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
req.onsuccess = () => resolve(req.result);
|
|
64
|
+
req.onerror = () => reject(req.error);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return this.dbPromise;
|
|
68
|
+
}
|
|
69
|
+
async tx(mode, fn) {
|
|
70
|
+
const db = await this.getDB();
|
|
71
|
+
const tx = db.transaction(this.config.storeName, mode);
|
|
72
|
+
const store = tx.objectStore(this.config.storeName);
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
let result;
|
|
75
|
+
try {
|
|
76
|
+
const req = fn(store);
|
|
77
|
+
if (req)
|
|
78
|
+
req.onsuccess = () => (result = req.result);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
reject(e);
|
|
82
|
+
}
|
|
83
|
+
tx.oncomplete = () => resolve(result);
|
|
84
|
+
tx.onerror = () => reject(tx.error);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// ==============================
|
|
88
|
+
// Helpers
|
|
89
|
+
// ==============================
|
|
90
|
+
k(key) {
|
|
91
|
+
return this.config.namespace
|
|
92
|
+
? `${this.config.namespace}:${key}`
|
|
93
|
+
: key;
|
|
94
|
+
}
|
|
95
|
+
async getRaw(key) {
|
|
96
|
+
return this.tx("readonly", s => s.get(this.k(key)));
|
|
97
|
+
}
|
|
98
|
+
async putRaw(key, val) {
|
|
99
|
+
return this.tx("readwrite", s => s.put(val, this.k(key)));
|
|
100
|
+
}
|
|
101
|
+
async deleteRaw(key) {
|
|
102
|
+
return this.tx("readwrite", s => s.delete(this.k(key)));
|
|
103
|
+
}
|
|
104
|
+
// ==============================
|
|
105
|
+
// Eviction (LRU)
|
|
106
|
+
// ==============================
|
|
107
|
+
async evict() {
|
|
108
|
+
const db = await this.getDB();
|
|
109
|
+
const tx = db.transaction(this.config.storeName, "readonly");
|
|
110
|
+
const store = tx.objectStore(this.config.storeName);
|
|
111
|
+
const entries = [];
|
|
112
|
+
await new Promise(res => {
|
|
113
|
+
store.openCursor().onsuccess = (e) => {
|
|
114
|
+
const c = e.target.result;
|
|
115
|
+
if (!c)
|
|
116
|
+
return res();
|
|
117
|
+
entries.push({
|
|
118
|
+
key: c.key,
|
|
119
|
+
size: c.value.size,
|
|
120
|
+
last: c.value.lastAccessed,
|
|
121
|
+
});
|
|
122
|
+
c.continue();
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
let total = entries.reduce((s, e) => s + e.size, 0);
|
|
126
|
+
if (total <= this.config.maxSize)
|
|
127
|
+
return;
|
|
128
|
+
entries.sort((a, b) => a.last - b.last);
|
|
129
|
+
for (const e of entries) {
|
|
130
|
+
if (total <= this.config.maxSize)
|
|
131
|
+
break;
|
|
132
|
+
await this.deleteRaw(e.key);
|
|
133
|
+
total -= e.size;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ==============================
|
|
137
|
+
// Public API
|
|
138
|
+
// ==============================
|
|
139
|
+
async set(key, value, opt) {
|
|
140
|
+
const json = JSON.stringify(value);
|
|
141
|
+
let final = json;
|
|
142
|
+
let size = new Blob([json]).size;
|
|
143
|
+
let compressed = false;
|
|
144
|
+
if (opt?.forceCompress ||
|
|
145
|
+
size > this.config.compressionThreshold) {
|
|
146
|
+
final = await compress(json);
|
|
147
|
+
size = final.byteLength;
|
|
148
|
+
compressed = true;
|
|
149
|
+
}
|
|
150
|
+
else if (opt?.encode) {
|
|
151
|
+
final = encode(value);
|
|
152
|
+
size = new Blob([final]).size;
|
|
153
|
+
}
|
|
154
|
+
const entry = {
|
|
155
|
+
value: final,
|
|
156
|
+
isEncoded: opt?.encode ?? false,
|
|
157
|
+
isCompressed: compressed,
|
|
158
|
+
createdAt: Date.now(),
|
|
159
|
+
lastAccessed: Date.now(),
|
|
160
|
+
expiresAt: opt?.ttl
|
|
161
|
+
? Date.now() + opt.ttl
|
|
162
|
+
: undefined,
|
|
163
|
+
size,
|
|
164
|
+
};
|
|
165
|
+
await this.putRaw(key, entry);
|
|
166
|
+
await this.evict();
|
|
167
|
+
}
|
|
168
|
+
async get(key, opt) {
|
|
169
|
+
const entry = await this.getRaw(key);
|
|
170
|
+
if (!entry)
|
|
171
|
+
return null;
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const expired = entry.expiresAt && now > entry.expiresAt;
|
|
174
|
+
if (!expired)
|
|
175
|
+
entry.lastAccessed = now;
|
|
176
|
+
if (expired && opt?.staleWhileRevalidate && opt.revalidate) {
|
|
177
|
+
opt.revalidate().then(v => this.set(key, v, {
|
|
178
|
+
ttl: opt.ttlOnRevalidate,
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
if (expired && !opt?.staleWhileRevalidate) {
|
|
182
|
+
await this.deleteRaw(key);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
let v = entry.value;
|
|
186
|
+
if (entry.isCompressed)
|
|
187
|
+
v = await decompress(v);
|
|
188
|
+
if (entry.isEncoded)
|
|
189
|
+
v = decode(v);
|
|
190
|
+
return typeof v === "string"
|
|
191
|
+
? JSON.parse(v)
|
|
192
|
+
: v;
|
|
193
|
+
}
|
|
194
|
+
async remove(key) {
|
|
195
|
+
await this.deleteRaw(key);
|
|
196
|
+
}
|
|
197
|
+
async clear() {
|
|
198
|
+
await this.tx("readwrite", s => s.clear());
|
|
199
|
+
}
|
|
200
|
+
namespace(ns) {
|
|
201
|
+
return new CacheEngine({
|
|
202
|
+
...this.config,
|
|
203
|
+
namespace: ns,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// cache-engine.ts
|
|
4
|
+
// ==============================
|
|
5
|
+
// Utils
|
|
6
|
+
// ==============================
|
|
7
|
+
function isClient() {
|
|
8
|
+
return (typeof window !== "undefined" &&
|
|
9
|
+
"indexedDB" in window &&
|
|
10
|
+
"CompressionStream" in window);
|
|
11
|
+
}
|
|
12
|
+
async function compress(data) {
|
|
13
|
+
const cs = new CompressionStream("gzip");
|
|
14
|
+
const writer = cs.writable.getWriter();
|
|
15
|
+
await writer.write(new TextEncoder().encode(data));
|
|
16
|
+
await writer.close();
|
|
17
|
+
const out = await new Response(cs.readable).arrayBuffer();
|
|
18
|
+
return new Uint8Array(out);
|
|
19
|
+
}
|
|
20
|
+
async function decompress(data) {
|
|
21
|
+
const ds = new DecompressionStream("gzip");
|
|
22
|
+
const writer = ds.writable.getWriter();
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
await writer.write(data);
|
|
25
|
+
await writer.close();
|
|
26
|
+
const out = await new Response(ds.readable).arrayBuffer();
|
|
27
|
+
return new TextDecoder().decode(out);
|
|
28
|
+
}
|
|
29
|
+
function encode(v) {
|
|
30
|
+
return btoa(encodeURIComponent(JSON.stringify(v)));
|
|
31
|
+
}
|
|
32
|
+
function decode(v) {
|
|
33
|
+
return JSON.parse(decodeURIComponent(atob(v)));
|
|
34
|
+
}
|
|
35
|
+
// ==============================
|
|
36
|
+
// Cache Engine
|
|
37
|
+
// ==============================
|
|
38
|
+
class CacheEngine {
|
|
39
|
+
constructor(cfg) {
|
|
40
|
+
this.dbPromise = null;
|
|
41
|
+
this.config = {
|
|
42
|
+
dbName: cfg?.dbName ?? "cache-db",
|
|
43
|
+
version: cfg?.version ?? 1,
|
|
44
|
+
storeName: cfg?.storeName ?? "cache",
|
|
45
|
+
maxSize: cfg?.maxSize ?? 100 * 1024 * 1024,
|
|
46
|
+
compressionThreshold: cfg?.compressionThreshold ?? 10 * 1024,
|
|
47
|
+
namespace: cfg?.namespace ?? "",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// ==============================
|
|
51
|
+
// DB
|
|
52
|
+
// ==============================
|
|
53
|
+
async getDB() {
|
|
54
|
+
if (!isClient())
|
|
55
|
+
throw new Error("IndexedDB unsupported");
|
|
56
|
+
if (!this.dbPromise) {
|
|
57
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
58
|
+
const req = indexedDB.open(this.config.dbName, this.config.version);
|
|
59
|
+
req.onupgradeneeded = () => {
|
|
60
|
+
const db = req.result;
|
|
61
|
+
if (!db.objectStoreNames.contains(this.config.storeName)) {
|
|
62
|
+
db.createObjectStore(this.config.storeName);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
req.onsuccess = () => resolve(req.result);
|
|
66
|
+
req.onerror = () => reject(req.error);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return this.dbPromise;
|
|
70
|
+
}
|
|
71
|
+
async tx(mode, fn) {
|
|
72
|
+
const db = await this.getDB();
|
|
73
|
+
const tx = db.transaction(this.config.storeName, mode);
|
|
74
|
+
const store = tx.objectStore(this.config.storeName);
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let result;
|
|
77
|
+
try {
|
|
78
|
+
const req = fn(store);
|
|
79
|
+
if (req)
|
|
80
|
+
req.onsuccess = () => (result = req.result);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
reject(e);
|
|
84
|
+
}
|
|
85
|
+
tx.oncomplete = () => resolve(result);
|
|
86
|
+
tx.onerror = () => reject(tx.error);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// ==============================
|
|
90
|
+
// Helpers
|
|
91
|
+
// ==============================
|
|
92
|
+
k(key) {
|
|
93
|
+
return this.config.namespace
|
|
94
|
+
? `${this.config.namespace}:${key}`
|
|
95
|
+
: key;
|
|
96
|
+
}
|
|
97
|
+
async getRaw(key) {
|
|
98
|
+
return this.tx("readonly", s => s.get(this.k(key)));
|
|
99
|
+
}
|
|
100
|
+
async putRaw(key, val) {
|
|
101
|
+
return this.tx("readwrite", s => s.put(val, this.k(key)));
|
|
102
|
+
}
|
|
103
|
+
async deleteRaw(key) {
|
|
104
|
+
return this.tx("readwrite", s => s.delete(this.k(key)));
|
|
105
|
+
}
|
|
106
|
+
// ==============================
|
|
107
|
+
// Eviction (LRU)
|
|
108
|
+
// ==============================
|
|
109
|
+
async evict() {
|
|
110
|
+
const db = await this.getDB();
|
|
111
|
+
const tx = db.transaction(this.config.storeName, "readonly");
|
|
112
|
+
const store = tx.objectStore(this.config.storeName);
|
|
113
|
+
const entries = [];
|
|
114
|
+
await new Promise(res => {
|
|
115
|
+
store.openCursor().onsuccess = (e) => {
|
|
116
|
+
const c = e.target.result;
|
|
117
|
+
if (!c)
|
|
118
|
+
return res();
|
|
119
|
+
entries.push({
|
|
120
|
+
key: c.key,
|
|
121
|
+
size: c.value.size,
|
|
122
|
+
last: c.value.lastAccessed,
|
|
123
|
+
});
|
|
124
|
+
c.continue();
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
let total = entries.reduce((s, e) => s + e.size, 0);
|
|
128
|
+
if (total <= this.config.maxSize)
|
|
129
|
+
return;
|
|
130
|
+
entries.sort((a, b) => a.last - b.last);
|
|
131
|
+
for (const e of entries) {
|
|
132
|
+
if (total <= this.config.maxSize)
|
|
133
|
+
break;
|
|
134
|
+
await this.deleteRaw(e.key);
|
|
135
|
+
total -= e.size;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// ==============================
|
|
139
|
+
// Public API
|
|
140
|
+
// ==============================
|
|
141
|
+
async set(key, value, opt) {
|
|
142
|
+
const json = JSON.stringify(value);
|
|
143
|
+
let final = json;
|
|
144
|
+
let size = new Blob([json]).size;
|
|
145
|
+
let compressed = false;
|
|
146
|
+
if (opt?.forceCompress ||
|
|
147
|
+
size > this.config.compressionThreshold) {
|
|
148
|
+
final = await compress(json);
|
|
149
|
+
size = final.byteLength;
|
|
150
|
+
compressed = true;
|
|
151
|
+
}
|
|
152
|
+
else if (opt?.encode) {
|
|
153
|
+
final = encode(value);
|
|
154
|
+
size = new Blob([final]).size;
|
|
155
|
+
}
|
|
156
|
+
const entry = {
|
|
157
|
+
value: final,
|
|
158
|
+
isEncoded: opt?.encode ?? false,
|
|
159
|
+
isCompressed: compressed,
|
|
160
|
+
createdAt: Date.now(),
|
|
161
|
+
lastAccessed: Date.now(),
|
|
162
|
+
expiresAt: opt?.ttl
|
|
163
|
+
? Date.now() + opt.ttl
|
|
164
|
+
: undefined,
|
|
165
|
+
size,
|
|
166
|
+
};
|
|
167
|
+
await this.putRaw(key, entry);
|
|
168
|
+
await this.evict();
|
|
169
|
+
}
|
|
170
|
+
async get(key, opt) {
|
|
171
|
+
const entry = await this.getRaw(key);
|
|
172
|
+
if (!entry)
|
|
173
|
+
return null;
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
const expired = entry.expiresAt && now > entry.expiresAt;
|
|
176
|
+
if (!expired)
|
|
177
|
+
entry.lastAccessed = now;
|
|
178
|
+
if (expired && opt?.staleWhileRevalidate && opt.revalidate) {
|
|
179
|
+
opt.revalidate().then(v => this.set(key, v, {
|
|
180
|
+
ttl: opt.ttlOnRevalidate,
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
if (expired && !opt?.staleWhileRevalidate) {
|
|
184
|
+
await this.deleteRaw(key);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
let v = entry.value;
|
|
188
|
+
if (entry.isCompressed)
|
|
189
|
+
v = await decompress(v);
|
|
190
|
+
if (entry.isEncoded)
|
|
191
|
+
v = decode(v);
|
|
192
|
+
return typeof v === "string"
|
|
193
|
+
? JSON.parse(v)
|
|
194
|
+
: v;
|
|
195
|
+
}
|
|
196
|
+
async remove(key) {
|
|
197
|
+
await this.deleteRaw(key);
|
|
198
|
+
}
|
|
199
|
+
async clear() {
|
|
200
|
+
await this.tx("readwrite", s => s.clear());
|
|
201
|
+
}
|
|
202
|
+
namespace(ns) {
|
|
203
|
+
return new CacheEngine({
|
|
204
|
+
...this.config,
|
|
205
|
+
namespace: ns,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
exports.CacheEngine = CacheEngine;
|
|
211
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/cache-engine.ts"],"sourcesContent":["// cache-engine.ts\r\n\r\n// ==============================\r\n// Types\r\n// ==============================\r\nexport type CacheEntry<T = any> = {\r\n value: T | string | Uint8Array;\r\n isEncoded: boolean;\r\n isCompressed: boolean;\r\n createdAt: number;\r\n lastAccessed: number;\r\n expiresAt?: number;\r\n size: number;\r\n};\r\n\r\nexport type CacheSetOptions = {\r\n ttl?: number;\r\n encode?: boolean;\r\n forceCompress?: boolean;\r\n};\r\n\r\nexport type CacheGetOptions<T> = {\r\n staleWhileRevalidate?: boolean;\r\n revalidate?: () => Promise<T>;\r\n ttlOnRevalidate?: number;\r\n};\r\n\r\nexport type CacheConfig = {\r\n dbName?: string;\r\n version?: number;\r\n storeName?: string;\r\n maxSize?: number;\r\n compressionThreshold?: number;\r\n namespace?: string;\r\n};\r\n\r\n// ==============================\r\n// Utils\r\n// ==============================\r\nfunction isClient() {\r\n return (\r\n typeof window !== \"undefined\" &&\r\n \"indexedDB\" in window &&\r\n \"CompressionStream\" in window\r\n );\r\n}\r\n\r\nasync function compress(data: string): Promise<Uint8Array> {\r\n const cs = new CompressionStream(\"gzip\");\r\n const writer = cs.writable.getWriter();\r\n await writer.write(new TextEncoder().encode(data));\r\n await writer.close();\r\n const out = await new Response(cs.readable).arrayBuffer();\r\n return new Uint8Array(out);\r\n}\r\n\r\nasync function decompress(data: Uint8Array): Promise<string> {\r\n const ds = new DecompressionStream(\"gzip\");\r\n const writer = ds.writable.getWriter();\r\n // @ts-ignore\r\n await writer.write(data);\r\n await writer.close();\r\n const out = await new Response(ds.readable).arrayBuffer();\r\n return new TextDecoder().decode(out);\r\n}\r\n\r\nfunction encode(v: any) {\r\n return btoa(encodeURIComponent(JSON.stringify(v)));\r\n}\r\n\r\nfunction decode(v: string) {\r\n return JSON.parse(decodeURIComponent(atob(v)));\r\n}\r\n\r\n// ==============================\r\n// Cache Engine\r\n// ==============================\r\nexport class CacheEngine {\r\n private dbPromise: Promise<IDBDatabase> | null = null;\r\n private readonly config: Required<CacheConfig>;\r\n\r\n constructor(cfg?: CacheConfig) {\r\n this.config = {\r\n dbName: cfg?.dbName ?? \"cache-db\",\r\n version: cfg?.version ?? 1,\r\n storeName: cfg?.storeName ?? \"cache\",\r\n maxSize: cfg?.maxSize ?? 100 * 1024 * 1024,\r\n compressionThreshold: cfg?.compressionThreshold ?? 10 * 1024,\r\n namespace: cfg?.namespace ?? \"\",\r\n };\r\n }\r\n\r\n // ==============================\r\n // DB\r\n // ==============================\r\n private async getDB(): Promise<IDBDatabase> {\r\n if (!isClient()) throw new Error(\"IndexedDB unsupported\");\r\n\r\n if (!this.dbPromise) {\r\n this.dbPromise = new Promise((resolve, reject) => {\r\n const req = indexedDB.open(\r\n this.config.dbName,\r\n this.config.version\r\n );\r\n\r\n req.onupgradeneeded = () => {\r\n const db = req.result;\r\n if (!db.objectStoreNames.contains(this.config.storeName)) {\r\n db.createObjectStore(this.config.storeName);\r\n }\r\n };\r\n\r\n req.onsuccess = () => resolve(req.result);\r\n req.onerror = () => reject(req.error);\r\n });\r\n }\r\n\r\n return this.dbPromise;\r\n }\r\n\r\n private async tx<T>(\r\n mode: IDBTransactionMode,\r\n fn: (store: IDBObjectStore) => IDBRequest | void\r\n ): Promise<T> {\r\n const db = await this.getDB();\r\n const tx = db.transaction(this.config.storeName, mode);\r\n const store = tx.objectStore(this.config.storeName);\r\n\r\n return new Promise((resolve, reject) => {\r\n let result: any;\r\n\r\n try {\r\n const req = fn(store);\r\n if (req) req.onsuccess = () => (result = req.result);\r\n } catch (e) {\r\n reject(e);\r\n }\r\n\r\n tx.oncomplete = () => resolve(result);\r\n tx.onerror = () => reject(tx.error);\r\n });\r\n }\r\n\r\n // ==============================\r\n // Helpers\r\n // ==============================\r\n private k(key: string) {\r\n return this.config.namespace\r\n ? `${this.config.namespace}:${key}`\r\n : key;\r\n }\r\n\r\n private async getRaw(key: string) {\r\n return this.tx<CacheEntry>(\"readonly\", s => s.get(this.k(key)));\r\n }\r\n\r\n private async putRaw(key: string, val: CacheEntry) {\r\n return this.tx(\"readwrite\", s => s.put(val, this.k(key)));\r\n }\r\n\r\n private async deleteRaw(key: string) {\r\n return this.tx(\"readwrite\", s => s.delete(this.k(key)));\r\n }\r\n\r\n // ==============================\r\n // Eviction (LRU)\r\n // ==============================\r\n private async evict() {\r\n const db = await this.getDB();\r\n const tx = db.transaction(this.config.storeName, \"readonly\");\r\n const store = tx.objectStore(this.config.storeName);\r\n\r\n const entries: any[] = [];\r\n\r\n await new Promise<void>(res => {\r\n store.openCursor().onsuccess = (e: any) => {\r\n const c = e.target.result;\r\n if (!c) return res();\r\n\r\n entries.push({\r\n key: c.key,\r\n size: c.value.size,\r\n last: c.value.lastAccessed,\r\n });\r\n\r\n c.continue();\r\n };\r\n });\r\n\r\n let total = entries.reduce((s, e) => s + e.size, 0);\r\n\r\n if (total <= this.config.maxSize) return;\r\n\r\n entries.sort((a, b) => a.last - b.last);\r\n\r\n for (const e of entries) {\r\n if (total <= this.config.maxSize) break;\r\n await this.deleteRaw(e.key);\r\n total -= e.size;\r\n }\r\n }\r\n\r\n // ==============================\r\n // Public API\r\n // ==============================\r\n async set<T>(\r\n key: string,\r\n value: T,\r\n opt?: CacheSetOptions\r\n ) {\r\n const json = JSON.stringify(value);\r\n\r\n let final: string | Uint8Array = json;\r\n let size = new Blob([json]).size;\r\n let compressed = false;\r\n\r\n if (\r\n opt?.forceCompress ||\r\n size > this.config.compressionThreshold\r\n ) {\r\n final = await compress(json);\r\n size = final.byteLength;\r\n compressed = true;\r\n } else if (opt?.encode) {\r\n final = encode(value);\r\n size = new Blob([final]).size;\r\n }\r\n\r\n const entry: CacheEntry = {\r\n value: final,\r\n isEncoded: opt?.encode ?? false,\r\n isCompressed: compressed,\r\n createdAt: Date.now(),\r\n lastAccessed: Date.now(),\r\n expiresAt: opt?.ttl\r\n ? Date.now() + opt.ttl\r\n : undefined,\r\n size,\r\n };\r\n\r\n await this.putRaw(key, entry);\r\n await this.evict();\r\n }\r\n\r\n async get<T>(\r\n key: string,\r\n opt?: CacheGetOptions<T>\r\n ): Promise<T | null> {\r\n const entry = await this.getRaw(key);\r\n if (!entry) return null;\r\n\r\n const now = Date.now();\r\n const expired =\r\n entry.expiresAt && now > entry.expiresAt;\r\n\r\n if (!expired)\r\n entry.lastAccessed = now;\r\n\r\n if (expired && opt?.staleWhileRevalidate && opt.revalidate) {\r\n opt.revalidate().then(v =>\r\n this.set(key, v, {\r\n ttl: opt.ttlOnRevalidate,\r\n })\r\n );\r\n }\r\n\r\n if (expired && !opt?.staleWhileRevalidate) {\r\n await this.deleteRaw(key);\r\n return null;\r\n }\r\n\r\n let v: any = entry.value;\r\n\r\n if (entry.isCompressed)\r\n v = await decompress(v as Uint8Array);\r\n\r\n if (entry.isEncoded)\r\n v = decode(v as string);\r\n\r\n return typeof v === \"string\"\r\n ? JSON.parse(v)\r\n : v;\r\n }\r\n\r\n async remove(key: string) {\r\n await this.deleteRaw(key);\r\n }\r\n\r\n async clear() {\r\n await this.tx(\"readwrite\", s => s.clear());\r\n }\r\n\r\n namespace(ns: string) {\r\n return new CacheEngine({\r\n ...this.config,\r\n namespace: ns,\r\n });\r\n }\r\n}\r\n"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
type CacheEntry<T = any> = {
|
|
2
|
+
value: T | string | Uint8Array;
|
|
3
|
+
isEncoded: boolean;
|
|
4
|
+
isCompressed: boolean;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
lastAccessed: number;
|
|
7
|
+
expiresAt?: number;
|
|
8
|
+
size: number;
|
|
9
|
+
};
|
|
10
|
+
type CacheSetOptions = {
|
|
11
|
+
ttl?: number;
|
|
12
|
+
encode?: boolean;
|
|
13
|
+
forceCompress?: boolean;
|
|
14
|
+
};
|
|
15
|
+
type CacheGetOptions<T> = {
|
|
16
|
+
staleWhileRevalidate?: boolean;
|
|
17
|
+
revalidate?: () => Promise<T>;
|
|
18
|
+
ttlOnRevalidate?: number;
|
|
19
|
+
};
|
|
20
|
+
type CacheConfig = {
|
|
21
|
+
dbName?: string;
|
|
22
|
+
version?: number;
|
|
23
|
+
storeName?: string;
|
|
24
|
+
maxSize?: number;
|
|
25
|
+
compressionThreshold?: number;
|
|
26
|
+
namespace?: string;
|
|
27
|
+
};
|
|
28
|
+
declare class CacheEngine {
|
|
29
|
+
private dbPromise;
|
|
30
|
+
private readonly config;
|
|
31
|
+
constructor(cfg?: CacheConfig);
|
|
32
|
+
private getDB;
|
|
33
|
+
private tx;
|
|
34
|
+
private k;
|
|
35
|
+
private getRaw;
|
|
36
|
+
private putRaw;
|
|
37
|
+
private deleteRaw;
|
|
38
|
+
private evict;
|
|
39
|
+
set<T>(key: string, value: T, opt?: CacheSetOptions): Promise<void>;
|
|
40
|
+
get<T>(key: string, opt?: CacheGetOptions<T>): Promise<T | null>;
|
|
41
|
+
remove(key: string): Promise<void>;
|
|
42
|
+
clear(): Promise<void>;
|
|
43
|
+
namespace(ns: string): CacheEngine;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { CacheEngine };
|
|
47
|
+
export type { CacheConfig, CacheEntry, CacheGetOptions, CacheSetOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// cache-engine.ts
|
|
2
|
+
// ==============================
|
|
3
|
+
// Utils
|
|
4
|
+
// ==============================
|
|
5
|
+
function isClient() {
|
|
6
|
+
return (typeof window !== "undefined" &&
|
|
7
|
+
"indexedDB" in window &&
|
|
8
|
+
"CompressionStream" in window);
|
|
9
|
+
}
|
|
10
|
+
async function compress(data) {
|
|
11
|
+
const cs = new CompressionStream("gzip");
|
|
12
|
+
const writer = cs.writable.getWriter();
|
|
13
|
+
await writer.write(new TextEncoder().encode(data));
|
|
14
|
+
await writer.close();
|
|
15
|
+
const out = await new Response(cs.readable).arrayBuffer();
|
|
16
|
+
return new Uint8Array(out);
|
|
17
|
+
}
|
|
18
|
+
async function decompress(data) {
|
|
19
|
+
const ds = new DecompressionStream("gzip");
|
|
20
|
+
const writer = ds.writable.getWriter();
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
await writer.write(data);
|
|
23
|
+
await writer.close();
|
|
24
|
+
const out = await new Response(ds.readable).arrayBuffer();
|
|
25
|
+
return new TextDecoder().decode(out);
|
|
26
|
+
}
|
|
27
|
+
function encode(v) {
|
|
28
|
+
return btoa(encodeURIComponent(JSON.stringify(v)));
|
|
29
|
+
}
|
|
30
|
+
function decode(v) {
|
|
31
|
+
return JSON.parse(decodeURIComponent(atob(v)));
|
|
32
|
+
}
|
|
33
|
+
// ==============================
|
|
34
|
+
// Cache Engine
|
|
35
|
+
// ==============================
|
|
36
|
+
class CacheEngine {
|
|
37
|
+
constructor(cfg) {
|
|
38
|
+
this.dbPromise = null;
|
|
39
|
+
this.config = {
|
|
40
|
+
dbName: cfg?.dbName ?? "cache-db",
|
|
41
|
+
version: cfg?.version ?? 1,
|
|
42
|
+
storeName: cfg?.storeName ?? "cache",
|
|
43
|
+
maxSize: cfg?.maxSize ?? 100 * 1024 * 1024,
|
|
44
|
+
compressionThreshold: cfg?.compressionThreshold ?? 10 * 1024,
|
|
45
|
+
namespace: cfg?.namespace ?? "",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// ==============================
|
|
49
|
+
// DB
|
|
50
|
+
// ==============================
|
|
51
|
+
async getDB() {
|
|
52
|
+
if (!isClient())
|
|
53
|
+
throw new Error("IndexedDB unsupported");
|
|
54
|
+
if (!this.dbPromise) {
|
|
55
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
56
|
+
const req = indexedDB.open(this.config.dbName, this.config.version);
|
|
57
|
+
req.onupgradeneeded = () => {
|
|
58
|
+
const db = req.result;
|
|
59
|
+
if (!db.objectStoreNames.contains(this.config.storeName)) {
|
|
60
|
+
db.createObjectStore(this.config.storeName);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
req.onsuccess = () => resolve(req.result);
|
|
64
|
+
req.onerror = () => reject(req.error);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return this.dbPromise;
|
|
68
|
+
}
|
|
69
|
+
async tx(mode, fn) {
|
|
70
|
+
const db = await this.getDB();
|
|
71
|
+
const tx = db.transaction(this.config.storeName, mode);
|
|
72
|
+
const store = tx.objectStore(this.config.storeName);
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
let result;
|
|
75
|
+
try {
|
|
76
|
+
const req = fn(store);
|
|
77
|
+
if (req)
|
|
78
|
+
req.onsuccess = () => (result = req.result);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
reject(e);
|
|
82
|
+
}
|
|
83
|
+
tx.oncomplete = () => resolve(result);
|
|
84
|
+
tx.onerror = () => reject(tx.error);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// ==============================
|
|
88
|
+
// Helpers
|
|
89
|
+
// ==============================
|
|
90
|
+
k(key) {
|
|
91
|
+
return this.config.namespace
|
|
92
|
+
? `${this.config.namespace}:${key}`
|
|
93
|
+
: key;
|
|
94
|
+
}
|
|
95
|
+
async getRaw(key) {
|
|
96
|
+
return this.tx("readonly", s => s.get(this.k(key)));
|
|
97
|
+
}
|
|
98
|
+
async putRaw(key, val) {
|
|
99
|
+
return this.tx("readwrite", s => s.put(val, this.k(key)));
|
|
100
|
+
}
|
|
101
|
+
async deleteRaw(key) {
|
|
102
|
+
return this.tx("readwrite", s => s.delete(this.k(key)));
|
|
103
|
+
}
|
|
104
|
+
// ==============================
|
|
105
|
+
// Eviction (LRU)
|
|
106
|
+
// ==============================
|
|
107
|
+
async evict() {
|
|
108
|
+
const db = await this.getDB();
|
|
109
|
+
const tx = db.transaction(this.config.storeName, "readonly");
|
|
110
|
+
const store = tx.objectStore(this.config.storeName);
|
|
111
|
+
const entries = [];
|
|
112
|
+
await new Promise(res => {
|
|
113
|
+
store.openCursor().onsuccess = (e) => {
|
|
114
|
+
const c = e.target.result;
|
|
115
|
+
if (!c)
|
|
116
|
+
return res();
|
|
117
|
+
entries.push({
|
|
118
|
+
key: c.key,
|
|
119
|
+
size: c.value.size,
|
|
120
|
+
last: c.value.lastAccessed,
|
|
121
|
+
});
|
|
122
|
+
c.continue();
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
let total = entries.reduce((s, e) => s + e.size, 0);
|
|
126
|
+
if (total <= this.config.maxSize)
|
|
127
|
+
return;
|
|
128
|
+
entries.sort((a, b) => a.last - b.last);
|
|
129
|
+
for (const e of entries) {
|
|
130
|
+
if (total <= this.config.maxSize)
|
|
131
|
+
break;
|
|
132
|
+
await this.deleteRaw(e.key);
|
|
133
|
+
total -= e.size;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ==============================
|
|
137
|
+
// Public API
|
|
138
|
+
// ==============================
|
|
139
|
+
async set(key, value, opt) {
|
|
140
|
+
const json = JSON.stringify(value);
|
|
141
|
+
let final = json;
|
|
142
|
+
let size = new Blob([json]).size;
|
|
143
|
+
let compressed = false;
|
|
144
|
+
if (opt?.forceCompress ||
|
|
145
|
+
size > this.config.compressionThreshold) {
|
|
146
|
+
final = await compress(json);
|
|
147
|
+
size = final.byteLength;
|
|
148
|
+
compressed = true;
|
|
149
|
+
}
|
|
150
|
+
else if (opt?.encode) {
|
|
151
|
+
final = encode(value);
|
|
152
|
+
size = new Blob([final]).size;
|
|
153
|
+
}
|
|
154
|
+
const entry = {
|
|
155
|
+
value: final,
|
|
156
|
+
isEncoded: opt?.encode ?? false,
|
|
157
|
+
isCompressed: compressed,
|
|
158
|
+
createdAt: Date.now(),
|
|
159
|
+
lastAccessed: Date.now(),
|
|
160
|
+
expiresAt: opt?.ttl
|
|
161
|
+
? Date.now() + opt.ttl
|
|
162
|
+
: undefined,
|
|
163
|
+
size,
|
|
164
|
+
};
|
|
165
|
+
await this.putRaw(key, entry);
|
|
166
|
+
await this.evict();
|
|
167
|
+
}
|
|
168
|
+
async get(key, opt) {
|
|
169
|
+
const entry = await this.getRaw(key);
|
|
170
|
+
if (!entry)
|
|
171
|
+
return null;
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const expired = entry.expiresAt && now > entry.expiresAt;
|
|
174
|
+
if (!expired)
|
|
175
|
+
entry.lastAccessed = now;
|
|
176
|
+
if (expired && opt?.staleWhileRevalidate && opt.revalidate) {
|
|
177
|
+
opt.revalidate().then(v => this.set(key, v, {
|
|
178
|
+
ttl: opt.ttlOnRevalidate,
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
if (expired && !opt?.staleWhileRevalidate) {
|
|
182
|
+
await this.deleteRaw(key);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
let v = entry.value;
|
|
186
|
+
if (entry.isCompressed)
|
|
187
|
+
v = await decompress(v);
|
|
188
|
+
if (entry.isEncoded)
|
|
189
|
+
v = decode(v);
|
|
190
|
+
return typeof v === "string"
|
|
191
|
+
? JSON.parse(v)
|
|
192
|
+
: v;
|
|
193
|
+
}
|
|
194
|
+
async remove(key) {
|
|
195
|
+
await this.deleteRaw(key);
|
|
196
|
+
}
|
|
197
|
+
async clear() {
|
|
198
|
+
await this.tx("readwrite", s => s.clear());
|
|
199
|
+
}
|
|
200
|
+
namespace(ns) {
|
|
201
|
+
return new CacheEngine({
|
|
202
|
+
...this.config,
|
|
203
|
+
namespace: ns,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export { CacheEngine };
|
|
209
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/cache-engine.ts"],"sourcesContent":["// cache-engine.ts\r\n\r\n// ==============================\r\n// Types\r\n// ==============================\r\nexport type CacheEntry<T = any> = {\r\n value: T | string | Uint8Array;\r\n isEncoded: boolean;\r\n isCompressed: boolean;\r\n createdAt: number;\r\n lastAccessed: number;\r\n expiresAt?: number;\r\n size: number;\r\n};\r\n\r\nexport type CacheSetOptions = {\r\n ttl?: number;\r\n encode?: boolean;\r\n forceCompress?: boolean;\r\n};\r\n\r\nexport type CacheGetOptions<T> = {\r\n staleWhileRevalidate?: boolean;\r\n revalidate?: () => Promise<T>;\r\n ttlOnRevalidate?: number;\r\n};\r\n\r\nexport type CacheConfig = {\r\n dbName?: string;\r\n version?: number;\r\n storeName?: string;\r\n maxSize?: number;\r\n compressionThreshold?: number;\r\n namespace?: string;\r\n};\r\n\r\n// ==============================\r\n// Utils\r\n// ==============================\r\nfunction isClient() {\r\n return (\r\n typeof window !== \"undefined\" &&\r\n \"indexedDB\" in window &&\r\n \"CompressionStream\" in window\r\n );\r\n}\r\n\r\nasync function compress(data: string): Promise<Uint8Array> {\r\n const cs = new CompressionStream(\"gzip\");\r\n const writer = cs.writable.getWriter();\r\n await writer.write(new TextEncoder().encode(data));\r\n await writer.close();\r\n const out = await new Response(cs.readable).arrayBuffer();\r\n return new Uint8Array(out);\r\n}\r\n\r\nasync function decompress(data: Uint8Array): Promise<string> {\r\n const ds = new DecompressionStream(\"gzip\");\r\n const writer = ds.writable.getWriter();\r\n // @ts-ignore\r\n await writer.write(data);\r\n await writer.close();\r\n const out = await new Response(ds.readable).arrayBuffer();\r\n return new TextDecoder().decode(out);\r\n}\r\n\r\nfunction encode(v: any) {\r\n return btoa(encodeURIComponent(JSON.stringify(v)));\r\n}\r\n\r\nfunction decode(v: string) {\r\n return JSON.parse(decodeURIComponent(atob(v)));\r\n}\r\n\r\n// ==============================\r\n// Cache Engine\r\n// ==============================\r\nexport class CacheEngine {\r\n private dbPromise: Promise<IDBDatabase> | null = null;\r\n private readonly config: Required<CacheConfig>;\r\n\r\n constructor(cfg?: CacheConfig) {\r\n this.config = {\r\n dbName: cfg?.dbName ?? \"cache-db\",\r\n version: cfg?.version ?? 1,\r\n storeName: cfg?.storeName ?? \"cache\",\r\n maxSize: cfg?.maxSize ?? 100 * 1024 * 1024,\r\n compressionThreshold: cfg?.compressionThreshold ?? 10 * 1024,\r\n namespace: cfg?.namespace ?? \"\",\r\n };\r\n }\r\n\r\n // ==============================\r\n // DB\r\n // ==============================\r\n private async getDB(): Promise<IDBDatabase> {\r\n if (!isClient()) throw new Error(\"IndexedDB unsupported\");\r\n\r\n if (!this.dbPromise) {\r\n this.dbPromise = new Promise((resolve, reject) => {\r\n const req = indexedDB.open(\r\n this.config.dbName,\r\n this.config.version\r\n );\r\n\r\n req.onupgradeneeded = () => {\r\n const db = req.result;\r\n if (!db.objectStoreNames.contains(this.config.storeName)) {\r\n db.createObjectStore(this.config.storeName);\r\n }\r\n };\r\n\r\n req.onsuccess = () => resolve(req.result);\r\n req.onerror = () => reject(req.error);\r\n });\r\n }\r\n\r\n return this.dbPromise;\r\n }\r\n\r\n private async tx<T>(\r\n mode: IDBTransactionMode,\r\n fn: (store: IDBObjectStore) => IDBRequest | void\r\n ): Promise<T> {\r\n const db = await this.getDB();\r\n const tx = db.transaction(this.config.storeName, mode);\r\n const store = tx.objectStore(this.config.storeName);\r\n\r\n return new Promise((resolve, reject) => {\r\n let result: any;\r\n\r\n try {\r\n const req = fn(store);\r\n if (req) req.onsuccess = () => (result = req.result);\r\n } catch (e) {\r\n reject(e);\r\n }\r\n\r\n tx.oncomplete = () => resolve(result);\r\n tx.onerror = () => reject(tx.error);\r\n });\r\n }\r\n\r\n // ==============================\r\n // Helpers\r\n // ==============================\r\n private k(key: string) {\r\n return this.config.namespace\r\n ? `${this.config.namespace}:${key}`\r\n : key;\r\n }\r\n\r\n private async getRaw(key: string) {\r\n return this.tx<CacheEntry>(\"readonly\", s => s.get(this.k(key)));\r\n }\r\n\r\n private async putRaw(key: string, val: CacheEntry) {\r\n return this.tx(\"readwrite\", s => s.put(val, this.k(key)));\r\n }\r\n\r\n private async deleteRaw(key: string) {\r\n return this.tx(\"readwrite\", s => s.delete(this.k(key)));\r\n }\r\n\r\n // ==============================\r\n // Eviction (LRU)\r\n // ==============================\r\n private async evict() {\r\n const db = await this.getDB();\r\n const tx = db.transaction(this.config.storeName, \"readonly\");\r\n const store = tx.objectStore(this.config.storeName);\r\n\r\n const entries: any[] = [];\r\n\r\n await new Promise<void>(res => {\r\n store.openCursor().onsuccess = (e: any) => {\r\n const c = e.target.result;\r\n if (!c) return res();\r\n\r\n entries.push({\r\n key: c.key,\r\n size: c.value.size,\r\n last: c.value.lastAccessed,\r\n });\r\n\r\n c.continue();\r\n };\r\n });\r\n\r\n let total = entries.reduce((s, e) => s + e.size, 0);\r\n\r\n if (total <= this.config.maxSize) return;\r\n\r\n entries.sort((a, b) => a.last - b.last);\r\n\r\n for (const e of entries) {\r\n if (total <= this.config.maxSize) break;\r\n await this.deleteRaw(e.key);\r\n total -= e.size;\r\n }\r\n }\r\n\r\n // ==============================\r\n // Public API\r\n // ==============================\r\n async set<T>(\r\n key: string,\r\n value: T,\r\n opt?: CacheSetOptions\r\n ) {\r\n const json = JSON.stringify(value);\r\n\r\n let final: string | Uint8Array = json;\r\n let size = new Blob([json]).size;\r\n let compressed = false;\r\n\r\n if (\r\n opt?.forceCompress ||\r\n size > this.config.compressionThreshold\r\n ) {\r\n final = await compress(json);\r\n size = final.byteLength;\r\n compressed = true;\r\n } else if (opt?.encode) {\r\n final = encode(value);\r\n size = new Blob([final]).size;\r\n }\r\n\r\n const entry: CacheEntry = {\r\n value: final,\r\n isEncoded: opt?.encode ?? false,\r\n isCompressed: compressed,\r\n createdAt: Date.now(),\r\n lastAccessed: Date.now(),\r\n expiresAt: opt?.ttl\r\n ? Date.now() + opt.ttl\r\n : undefined,\r\n size,\r\n };\r\n\r\n await this.putRaw(key, entry);\r\n await this.evict();\r\n }\r\n\r\n async get<T>(\r\n key: string,\r\n opt?: CacheGetOptions<T>\r\n ): Promise<T | null> {\r\n const entry = await this.getRaw(key);\r\n if (!entry) return null;\r\n\r\n const now = Date.now();\r\n const expired =\r\n entry.expiresAt && now > entry.expiresAt;\r\n\r\n if (!expired)\r\n entry.lastAccessed = now;\r\n\r\n if (expired && opt?.staleWhileRevalidate && opt.revalidate) {\r\n opt.revalidate().then(v =>\r\n this.set(key, v, {\r\n ttl: opt.ttlOnRevalidate,\r\n })\r\n );\r\n }\r\n\r\n if (expired && !opt?.staleWhileRevalidate) {\r\n await this.deleteRaw(key);\r\n return null;\r\n }\r\n\r\n let v: any = entry.value;\r\n\r\n if (entry.isCompressed)\r\n v = await decompress(v as Uint8Array);\r\n\r\n if (entry.isEncoded)\r\n v = decode(v as string);\r\n\r\n return typeof v === \"string\"\r\n ? JSON.parse(v)\r\n : v;\r\n }\r\n\r\n async remove(key: string) {\r\n await this.deleteRaw(key);\r\n }\r\n\r\n async clear() {\r\n await this.tx(\"readwrite\", s => s.clear());\r\n }\r\n\r\n namespace(ns: string) {\r\n return new CacheEngine({\r\n ...this.config,\r\n namespace: ns,\r\n });\r\n }\r\n}\r\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cache-craft-engine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight, fully client-side IndexedDB cache engine with compression, encoding, LRU eviction, TTL and stale-while-revalidate support",
|
|
5
|
+
"author": "Mohammad Javad <your-email@example.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc && rollup -c",
|
|
23
|
+
"prepublishOnly": "npm run build",
|
|
24
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"cache",
|
|
28
|
+
"indexeddb",
|
|
29
|
+
"client-side",
|
|
30
|
+
"offline",
|
|
31
|
+
"compression",
|
|
32
|
+
"lru",
|
|
33
|
+
"stale-while-revalidate"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/MJavadSF/CacheCraft.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/MJavadSF/CacheCraft/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/MJavadSF/CacheCraft#readme",
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
45
|
+
"rollup": "^4.57.1",
|
|
46
|
+
"rollup-plugin-dts": "^6.3.0",
|
|
47
|
+
"tslib": "^2.8.1",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
}
|
|
50
|
+
}
|