flamecache 0.0.1
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 +556 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +174 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +171 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Anish Roy
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
# Flamecache
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/flamecache)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://github.com/iamanishroy/flamecache/actions)
|
|
7
|
+
|
|
8
|
+
> A Redis-like caching layer for Firebase Realtime Database with automatic expiration, counters, and batch operations.
|
|
9
|
+
|
|
10
|
+
Flamecache is a lightweight, robust caching solution that brings Redis-style operations to Firebase Realtime Database. Perfect for serverless applications, real-time apps, and projects already using Firebase.
|
|
11
|
+
|
|
12
|
+
## ✨ Features
|
|
13
|
+
|
|
14
|
+
- 🚀 **Redis-like API** - Familiar `get`, `set`, `incr`, `decr`, `expire` operations
|
|
15
|
+
- ⚡ **Fast & Lightweight** - Only 5KB (bundled), no heavy dependencies
|
|
16
|
+
- 🔄 **Auto-expiration** - Built-in TTL support with automatic cleanup
|
|
17
|
+
- 📊 **Counters** - Atomic increment/decrement operations
|
|
18
|
+
- 📦 **Batch Operations** - Multi-get, multi-set, multi-delete
|
|
19
|
+
- 🎯 **TypeScript** - Full type safety with generics
|
|
20
|
+
- 🔌 **Zero Config** - Works out of the box
|
|
21
|
+
- 🌐 **Serverless Ready** - Perfect for Firebase Functions, Vercel, Netlify
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install flamecache firebase
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Flamecache requires Firebase as a peer dependency. If you don't have Firebase installed:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install firebase flamecache
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 🚀 Quick Start
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { createCache } from 'flamecache';
|
|
39
|
+
|
|
40
|
+
// Initialize cache
|
|
41
|
+
const cache = createCache({
|
|
42
|
+
firebase: {
|
|
43
|
+
apiKey: 'your-api-key',
|
|
44
|
+
databaseURL: 'https://your-project.firebaseio.com',
|
|
45
|
+
projectId: 'your-project-id',
|
|
46
|
+
},
|
|
47
|
+
ttl: 3600, // Default TTL: 1 hour
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Basic usage
|
|
51
|
+
await cache.set('user:123', { name: 'Alice', email: 'alice@example.com' });
|
|
52
|
+
const user = await cache.get('user:123');
|
|
53
|
+
|
|
54
|
+
// With custom TTL (in seconds)
|
|
55
|
+
await cache.set('temp:token', 'abc123', 300); // Expires in 5 minutes
|
|
56
|
+
|
|
57
|
+
// Counters
|
|
58
|
+
await cache.incr('views:post:456'); // Increment view count
|
|
59
|
+
const views = await cache.get('views:post:456'); // 1
|
|
60
|
+
|
|
61
|
+
// Read-through caching
|
|
62
|
+
const posts = await cache.wrap(
|
|
63
|
+
'posts:latest',
|
|
64
|
+
async () => {
|
|
65
|
+
// This function only runs on cache miss
|
|
66
|
+
const response = await fetch('https://api.example.com/posts');
|
|
67
|
+
return response.json();
|
|
68
|
+
},
|
|
69
|
+
600
|
|
70
|
+
); // Cache for 10 minutes
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 📖 API Reference
|
|
74
|
+
|
|
75
|
+
### Core Operations
|
|
76
|
+
|
|
77
|
+
#### `get<T>(key: string): Promise<T | null>`
|
|
78
|
+
|
|
79
|
+
Retrieve a value from cache.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const user = await cache.get<User>('user:123');
|
|
83
|
+
if (user) {
|
|
84
|
+
console.log(user.name);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### `set<T>(key: string, data: T, ttl?: number): Promise<void>`
|
|
89
|
+
|
|
90
|
+
Store a value in cache with optional TTL (in seconds).
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// With default TTL
|
|
94
|
+
await cache.set('user:123', userData);
|
|
95
|
+
|
|
96
|
+
// With custom TTL (10 minutes)
|
|
97
|
+
await cache.set('session:abc', sessionData, 600);
|
|
98
|
+
|
|
99
|
+
// Never expires
|
|
100
|
+
await cache.set('config', configData, 0);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### `del(key: string): Promise<void>`
|
|
104
|
+
|
|
105
|
+
Delete a key from cache.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
await cache.del('user:123');
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### `has(key: string): Promise<boolean>`
|
|
112
|
+
|
|
113
|
+
Check if a key exists and is not expired.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
if (await cache.has('session:abc')) {
|
|
117
|
+
console.log('Session is valid');
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### `clear(): Promise<void>`
|
|
122
|
+
|
|
123
|
+
Clear all cache entries.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
await cache.clear();
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Counter Operations
|
|
130
|
+
|
|
131
|
+
#### `incr(key: string, by?: number): Promise<number>`
|
|
132
|
+
|
|
133
|
+
Increment a numeric value. Returns the new value.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const views = await cache.incr('views:post:123'); // Increment by 1
|
|
137
|
+
const score = await cache.incr('score:player', 10); // Increment by 10
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### `decr(key: string, by?: number): Promise<number>`
|
|
141
|
+
|
|
142
|
+
Decrement a numeric value. Returns the new value.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const stock = await cache.decr('stock:item:456'); // Decrement by 1
|
|
146
|
+
const credits = await cache.decr('credits:user', 5); // Decrement by 5
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### TTL Operations
|
|
150
|
+
|
|
151
|
+
#### `expire(key: string, ttl: number): Promise<boolean>`
|
|
152
|
+
|
|
153
|
+
Set or update expiration time for an existing key (in seconds).
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
await cache.expire('session:abc', 300); // Expire in 5 minutes
|
|
157
|
+
await cache.expire('permanent:key', 0); // Remove expiration
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### `getTtl(key: string): Promise<number>`
|
|
161
|
+
|
|
162
|
+
Get remaining time-to-live in seconds.
|
|
163
|
+
|
|
164
|
+
- Returns `0` if key doesn't exist or is expired
|
|
165
|
+
- Returns `-1` if key has no expiration
|
|
166
|
+
- Returns remaining seconds otherwise
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const remaining = await cache.ttl('session:abc');
|
|
170
|
+
if (remaining > 0 && remaining < 300) {
|
|
171
|
+
console.log('Session expiring soon!');
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### `touch(key: string, ttl?: number): Promise<boolean>`
|
|
176
|
+
|
|
177
|
+
Refresh TTL without changing the value.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Use default TTL
|
|
181
|
+
await cache.touch('session:abc');
|
|
182
|
+
|
|
183
|
+
// Use custom TTL
|
|
184
|
+
await cache.touch('session:abc', 3600);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Batch Operations
|
|
188
|
+
|
|
189
|
+
#### `mget<T>(keys: string[]): Promise<(T | null)[]>`
|
|
190
|
+
|
|
191
|
+
Get multiple keys at once.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const [user1, user2, user3] = await cache.mget(['user:1', 'user:2', 'user:3']);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### `mset(entries: Record<string, any>, ttl?: number): Promise<void>`
|
|
198
|
+
|
|
199
|
+
Set multiple keys at once.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
await cache.mset(
|
|
203
|
+
{
|
|
204
|
+
'user:1': userData1,
|
|
205
|
+
'user:2': userData2,
|
|
206
|
+
'user:3': userData3,
|
|
207
|
+
},
|
|
208
|
+
600
|
|
209
|
+
); // All expire in 10 minutes
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### `mdel(keys: string[]): Promise<void>`
|
|
213
|
+
|
|
214
|
+
Delete multiple keys at once.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
await cache.mdel(['temp:1', 'temp:2', 'temp:3']);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Advanced Operations
|
|
221
|
+
|
|
222
|
+
#### `wrap<T>(key: string, fetchFn: () => Promise<T>, ttl?: number): Promise<T>`
|
|
223
|
+
|
|
224
|
+
Read-through caching pattern. Gets from cache or fetches and stores automatically.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const userData = await cache.wrap(
|
|
228
|
+
'user:123',
|
|
229
|
+
async () => {
|
|
230
|
+
// This only runs on cache miss
|
|
231
|
+
const response = await fetch(`/api/users/123`);
|
|
232
|
+
return response.json();
|
|
233
|
+
},
|
|
234
|
+
300
|
|
235
|
+
);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### `pull<T>(key: string): Promise<T | null>`
|
|
239
|
+
|
|
240
|
+
Get and delete in one operation (atomic pop).
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
const token = await cache.pull('temp:verification-token');
|
|
244
|
+
// Token is retrieved and deleted
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## 🎯 Use Cases
|
|
248
|
+
|
|
249
|
+
### 1. Rate Limiting
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
async function checkRateLimit(userId: string): Promise<boolean> {
|
|
253
|
+
const key = `ratelimit:${userId}`;
|
|
254
|
+
const count = await cache.incr(key);
|
|
255
|
+
|
|
256
|
+
// Set expiration on first request
|
|
257
|
+
if (count === 1) {
|
|
258
|
+
await cache.expire(key, 60); // Reset every minute
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return count <= 100; // Max 100 requests per minute
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Usage
|
|
265
|
+
if (!(await checkRateLimit('user123'))) {
|
|
266
|
+
throw new Error('Rate limit exceeded');
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### 2. Session Management
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
class SessionManager {
|
|
274
|
+
async create(userId: string, data: any): Promise<string> {
|
|
275
|
+
const sessionId = generateId();
|
|
276
|
+
await cache.set(
|
|
277
|
+
`session:${sessionId}`,
|
|
278
|
+
{
|
|
279
|
+
userId,
|
|
280
|
+
...data,
|
|
281
|
+
createdAt: Date.now(),
|
|
282
|
+
},
|
|
283
|
+
3600
|
|
284
|
+
); // 1 hour session
|
|
285
|
+
return sessionId;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async get(sessionId: string) {
|
|
289
|
+
return await cache.get(`session:${sessionId}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async extend(sessionId: string): Promise<boolean> {
|
|
293
|
+
return await cache.touch(`session:${sessionId}`, 3600);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async destroy(sessionId: string): Promise<void> {
|
|
297
|
+
await cache.del(`session:${sessionId}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### 3. API Response Caching
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
async function getUser(userId: string) {
|
|
306
|
+
return cache.wrap(
|
|
307
|
+
`api:user:${userId}`,
|
|
308
|
+
async () => {
|
|
309
|
+
console.log('Fetching from API...');
|
|
310
|
+
const response = await fetch(`https://api.example.com/users/${userId}`);
|
|
311
|
+
return response.json();
|
|
312
|
+
},
|
|
313
|
+
600
|
|
314
|
+
); // Cache for 10 minutes
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// First call - fetches from API
|
|
318
|
+
const user1 = await getUser('123');
|
|
319
|
+
|
|
320
|
+
// Second call - uses cache (within 10 minutes)
|
|
321
|
+
const user2 = await getUser('123');
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### 4. Leaderboard / Scoring
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
async function addScore(playerId: string, points: number) {
|
|
328
|
+
await cache.incr(`score:${playerId}`, points);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function getTopScores(playerIds: string[]) {
|
|
332
|
+
const keys = playerIds.map((id) => `score:${id}`);
|
|
333
|
+
const scores = await cache.mget<number>(keys);
|
|
334
|
+
|
|
335
|
+
return playerIds
|
|
336
|
+
.map((id, i) => ({
|
|
337
|
+
playerId: id,
|
|
338
|
+
score: scores[i] || 0,
|
|
339
|
+
}))
|
|
340
|
+
.sort((a, b) => b.score - a.score);
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 5. Temporary Tokens
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
async function createVerificationToken(email: string): Promise<string> {
|
|
348
|
+
const token = generateToken();
|
|
349
|
+
await cache.set(`verify:${token}`, { email }, 900); // 15 minutes
|
|
350
|
+
return token;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function verifyToken(token: string) {
|
|
354
|
+
// Pull removes the token after reading (one-time use)
|
|
355
|
+
const data = await cache.pull(`verify:${token}`);
|
|
356
|
+
if (!data) {
|
|
357
|
+
throw new Error('Invalid or expired token');
|
|
358
|
+
}
|
|
359
|
+
return data;
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### 6. Real-time Analytics
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
// Track page views
|
|
367
|
+
async function trackPageView(pageId: string) {
|
|
368
|
+
await cache.incr(`views:${pageId}`);
|
|
369
|
+
await cache.incr(`views:today:${pageId}`);
|
|
370
|
+
|
|
371
|
+
// Expire daily stats at midnight
|
|
372
|
+
const secondsUntilMidnight = getSecondsUntilMidnight();
|
|
373
|
+
await cache.expire(`views:today:${pageId}`, secondsUntilMidnight);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Get analytics
|
|
377
|
+
async function getAnalytics(pageIds: string[]) {
|
|
378
|
+
const totalKeys = pageIds.map((id) => `views:${id}`);
|
|
379
|
+
const todayKeys = pageIds.map((id) => `views:today:${id}`);
|
|
380
|
+
|
|
381
|
+
const [totalViews, todayViews] = await Promise.all([
|
|
382
|
+
cache.mget<number>(totalKeys),
|
|
383
|
+
cache.mget<number>(todayKeys),
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
return pageIds.map((id, i) => ({
|
|
387
|
+
pageId: id,
|
|
388
|
+
total: totalViews[i] || 0,
|
|
389
|
+
today: todayViews[i] || 0,
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## 🔧 Configuration
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
interface CacheConfig {
|
|
398
|
+
// Firebase configuration (required)
|
|
399
|
+
firebase: {
|
|
400
|
+
apiKey: string;
|
|
401
|
+
authDomain?: string;
|
|
402
|
+
databaseURL: string;
|
|
403
|
+
projectId: string;
|
|
404
|
+
storageBucket?: string;
|
|
405
|
+
messagingSenderId?: string;
|
|
406
|
+
appId?: string;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// Root path in Firebase database (default: 'flamecache')
|
|
410
|
+
rootPath?: string;
|
|
411
|
+
|
|
412
|
+
// Default TTL in seconds (default: 3600 = 1 hour, 0 = never expires)
|
|
413
|
+
ttl?: number;
|
|
414
|
+
|
|
415
|
+
// Enable debug logs (default: false)
|
|
416
|
+
debug?: boolean;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Example with all options
|
|
420
|
+
const cache = createCache({
|
|
421
|
+
firebase: {
|
|
422
|
+
/* ... */
|
|
423
|
+
},
|
|
424
|
+
rootPath: 'my-cache',
|
|
425
|
+
ttl: 7200, // 2 hours
|
|
426
|
+
debug: true, // Log all operations
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## 🔑 Key Naming Conventions
|
|
431
|
+
|
|
432
|
+
Use colons (`:`) to create hierarchical keys for better organization:
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// Good ✅
|
|
436
|
+
'user:123';
|
|
437
|
+
'user:123:profile';
|
|
438
|
+
'session:abc123';
|
|
439
|
+
'api:github:users:456';
|
|
440
|
+
'ratelimit:user:789';
|
|
441
|
+
'cache:posts:latest';
|
|
442
|
+
|
|
443
|
+
// Avoid ❌
|
|
444
|
+
'user_123';
|
|
445
|
+
'user-profile-123';
|
|
446
|
+
'session.abc123';
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
## 🎨 TypeScript Support
|
|
450
|
+
|
|
451
|
+
Full TypeScript support with generics:
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
interface User {
|
|
455
|
+
id: string;
|
|
456
|
+
name: string;
|
|
457
|
+
email: string;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Type-safe operations
|
|
461
|
+
const user = await cache.get<User>('user:123');
|
|
462
|
+
// user is typed as User | null
|
|
463
|
+
|
|
464
|
+
await cache.set<User>('user:123', {
|
|
465
|
+
id: '123',
|
|
466
|
+
name: 'Alice',
|
|
467
|
+
email: 'alice@example.com',
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Works with complex types
|
|
471
|
+
type ApiResponse = {
|
|
472
|
+
data: User[];
|
|
473
|
+
meta: { total: number };
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const response = await cache.wrap<ApiResponse>('api:users', fetchUsers);
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## 🔒 Firebase Security Rules
|
|
480
|
+
|
|
481
|
+
### Development
|
|
482
|
+
|
|
483
|
+
```json
|
|
484
|
+
{
|
|
485
|
+
"rules": {
|
|
486
|
+
"cache": {
|
|
487
|
+
".read": true,
|
|
488
|
+
".write": true
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Production
|
|
495
|
+
|
|
496
|
+
```json
|
|
497
|
+
{
|
|
498
|
+
"rules": {
|
|
499
|
+
"cache": {
|
|
500
|
+
".read": "auth != null",
|
|
501
|
+
".write": "auth != null"
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Advanced (per-user caching)
|
|
508
|
+
|
|
509
|
+
```json
|
|
510
|
+
{
|
|
511
|
+
"rules": {
|
|
512
|
+
"cache": {
|
|
513
|
+
"users": {
|
|
514
|
+
"$uid": {
|
|
515
|
+
".read": "$uid === auth.uid",
|
|
516
|
+
".write": "$uid === auth.uid"
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
"public": {
|
|
520
|
+
".read": true,
|
|
521
|
+
".write": "auth != null"
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
## 🤝 Contributing
|
|
529
|
+
|
|
530
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
# Clone the repo
|
|
534
|
+
git clone https://github.com/iamanishroy/flamecache.git
|
|
535
|
+
cd flamecache
|
|
536
|
+
|
|
537
|
+
# Install dependencies
|
|
538
|
+
npm install
|
|
539
|
+
|
|
540
|
+
# Run tests
|
|
541
|
+
npm test
|
|
542
|
+
|
|
543
|
+
# Build
|
|
544
|
+
npm run build
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## 📝 License
|
|
548
|
+
|
|
549
|
+
MIT © [Anish Roy](https://anishroy.com)
|
|
550
|
+
|
|
551
|
+
## 🔗 Links
|
|
552
|
+
|
|
553
|
+
- [GitHub Repository](https://github.com/iamanishroy/flamecache)
|
|
554
|
+
- [npm Package](https://www.npmjs.com/package/flamecache)
|
|
555
|
+
- [Report Issues](https://github.com/iamanishroy/flamecache/issues)
|
|
556
|
+
- [Firebase Documentation](https://firebase.google.com/docs/database)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FirebaseOptions } from 'firebase/app';
|
|
2
|
+
export { FirebaseOptions } from 'firebase/app';
|
|
3
|
+
|
|
4
|
+
interface CacheConfig {
|
|
5
|
+
firebase: FirebaseOptions;
|
|
6
|
+
rootPath?: string;
|
|
7
|
+
ttl?: number;
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface CacheEntry<T> {
|
|
11
|
+
data: T;
|
|
12
|
+
exp: number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare class FirebaseCache {
|
|
16
|
+
private db;
|
|
17
|
+
private root;
|
|
18
|
+
private ttl;
|
|
19
|
+
private debug;
|
|
20
|
+
constructor(config: CacheConfig);
|
|
21
|
+
private path;
|
|
22
|
+
private log;
|
|
23
|
+
get<T = any>(key: string): Promise<T | null>;
|
|
24
|
+
set<T = any>(key: string, data: T, ttl?: number): Promise<void>;
|
|
25
|
+
del(key: string): Promise<void>;
|
|
26
|
+
has(key: string): Promise<boolean>;
|
|
27
|
+
wrap<T = any>(key: string, fetchFn: () => Promise<T>, ttl?: number): Promise<T>;
|
|
28
|
+
mget<T = any>(keys: string[]): Promise<(T | null)[]>;
|
|
29
|
+
mset(entries: Record<string, any>, ttl?: number): Promise<void>;
|
|
30
|
+
mdel(keys: string[]): Promise<void>;
|
|
31
|
+
clear(): Promise<void>;
|
|
32
|
+
disconnect(): Promise<void>;
|
|
33
|
+
touch(key: string, ttl?: number): Promise<boolean>;
|
|
34
|
+
pull<T = any>(key: string): Promise<T | null>;
|
|
35
|
+
incr(key: string, by?: number): Promise<number>;
|
|
36
|
+
decr(key: string, by?: number): Promise<number>;
|
|
37
|
+
expire(key: string, ttl: number): Promise<boolean>;
|
|
38
|
+
getTtl(key: string): Promise<number>;
|
|
39
|
+
}
|
|
40
|
+
declare function createCache(config: CacheConfig): FirebaseCache;
|
|
41
|
+
|
|
42
|
+
export { type CacheConfig, type CacheEntry, FirebaseCache, createCache };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FirebaseOptions } from 'firebase/app';
|
|
2
|
+
export { FirebaseOptions } from 'firebase/app';
|
|
3
|
+
|
|
4
|
+
interface CacheConfig {
|
|
5
|
+
firebase: FirebaseOptions;
|
|
6
|
+
rootPath?: string;
|
|
7
|
+
ttl?: number;
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface CacheEntry<T> {
|
|
11
|
+
data: T;
|
|
12
|
+
exp: number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare class FirebaseCache {
|
|
16
|
+
private db;
|
|
17
|
+
private root;
|
|
18
|
+
private ttl;
|
|
19
|
+
private debug;
|
|
20
|
+
constructor(config: CacheConfig);
|
|
21
|
+
private path;
|
|
22
|
+
private log;
|
|
23
|
+
get<T = any>(key: string): Promise<T | null>;
|
|
24
|
+
set<T = any>(key: string, data: T, ttl?: number): Promise<void>;
|
|
25
|
+
del(key: string): Promise<void>;
|
|
26
|
+
has(key: string): Promise<boolean>;
|
|
27
|
+
wrap<T = any>(key: string, fetchFn: () => Promise<T>, ttl?: number): Promise<T>;
|
|
28
|
+
mget<T = any>(keys: string[]): Promise<(T | null)[]>;
|
|
29
|
+
mset(entries: Record<string, any>, ttl?: number): Promise<void>;
|
|
30
|
+
mdel(keys: string[]): Promise<void>;
|
|
31
|
+
clear(): Promise<void>;
|
|
32
|
+
disconnect(): Promise<void>;
|
|
33
|
+
touch(key: string, ttl?: number): Promise<boolean>;
|
|
34
|
+
pull<T = any>(key: string): Promise<T | null>;
|
|
35
|
+
incr(key: string, by?: number): Promise<number>;
|
|
36
|
+
decr(key: string, by?: number): Promise<number>;
|
|
37
|
+
expire(key: string, ttl: number): Promise<boolean>;
|
|
38
|
+
getTtl(key: string): Promise<number>;
|
|
39
|
+
}
|
|
40
|
+
declare function createCache(config: CacheConfig): FirebaseCache;
|
|
41
|
+
|
|
42
|
+
export { type CacheConfig, type CacheEntry, FirebaseCache, createCache };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var app = require('firebase/app');
|
|
4
|
+
var database = require('firebase/database');
|
|
5
|
+
|
|
6
|
+
// src/cache.ts
|
|
7
|
+
var FirebaseCache = class {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
let app$1;
|
|
10
|
+
const existingApp = app.getApps().find((a) => a.name === (config.rootPath || "[DEFAULT]"));
|
|
11
|
+
if (existingApp) {
|
|
12
|
+
app$1 = existingApp;
|
|
13
|
+
} else {
|
|
14
|
+
app$1 = app.initializeApp(config.firebase, config.rootPath || "[DEFAULT]");
|
|
15
|
+
}
|
|
16
|
+
this.db = database.getDatabase(app$1);
|
|
17
|
+
this.root = config.rootPath || "flamecache";
|
|
18
|
+
this.ttl = config.ttl || 3600;
|
|
19
|
+
this.debug = config.debug || false;
|
|
20
|
+
}
|
|
21
|
+
path(key) {
|
|
22
|
+
return `${this.root}/${key}`;
|
|
23
|
+
}
|
|
24
|
+
log(...args) {
|
|
25
|
+
if (this.debug) console.log("[Flamecache]", ...args);
|
|
26
|
+
}
|
|
27
|
+
async get(key) {
|
|
28
|
+
try {
|
|
29
|
+
const snapshot = await database.get(database.ref(this.db, this.path(key)));
|
|
30
|
+
if (!snapshot.exists()) {
|
|
31
|
+
this.log("miss:", key);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const entry = snapshot.val();
|
|
35
|
+
if (entry.exp && Date.now() > entry.exp) {
|
|
36
|
+
this.log("expired:", key);
|
|
37
|
+
await this.del(key);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
this.log("hit:", key);
|
|
41
|
+
return entry.data;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
this.log("error get:", key, err);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async set(key, data, ttl) {
|
|
48
|
+
try {
|
|
49
|
+
const seconds = ttl !== void 0 ? ttl : this.ttl;
|
|
50
|
+
const entry = {
|
|
51
|
+
data,
|
|
52
|
+
exp: seconds > 0 ? Date.now() + seconds * 1e3 : null
|
|
53
|
+
};
|
|
54
|
+
await database.set(database.ref(this.db, this.path(key)), entry);
|
|
55
|
+
this.log("set:", key, `(${seconds}s)`);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
this.log("error set:", key, err);
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async del(key) {
|
|
62
|
+
try {
|
|
63
|
+
await database.remove(database.ref(this.db, this.path(key)));
|
|
64
|
+
this.log("del:", key);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
this.log("error del:", key, err);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async has(key) {
|
|
70
|
+
return await this.get(key) !== null;
|
|
71
|
+
}
|
|
72
|
+
async wrap(key, fetchFn, ttl) {
|
|
73
|
+
const cached = await this.get(key);
|
|
74
|
+
if (cached !== null) return cached;
|
|
75
|
+
this.log("fetch:", key);
|
|
76
|
+
const data = await fetchFn();
|
|
77
|
+
await this.set(key, data, ttl);
|
|
78
|
+
return data;
|
|
79
|
+
}
|
|
80
|
+
async mget(keys) {
|
|
81
|
+
return Promise.all(keys.map((k) => this.get(k)));
|
|
82
|
+
}
|
|
83
|
+
async mset(entries, ttl) {
|
|
84
|
+
await Promise.all(
|
|
85
|
+
Object.entries(entries).map(([k, v]) => this.set(k, v, ttl))
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
async mdel(keys) {
|
|
89
|
+
await Promise.all(keys.map((k) => this.del(k)));
|
|
90
|
+
}
|
|
91
|
+
async clear() {
|
|
92
|
+
try {
|
|
93
|
+
await database.remove(database.ref(this.db, this.root));
|
|
94
|
+
this.log("cleared all");
|
|
95
|
+
} catch (err) {
|
|
96
|
+
this.log("error clear:", err);
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async disconnect() {
|
|
101
|
+
await database.goOffline(this.db);
|
|
102
|
+
this.log("disconnected");
|
|
103
|
+
}
|
|
104
|
+
async touch(key, ttl) {
|
|
105
|
+
const data = await this.get(key);
|
|
106
|
+
if (data === null) return false;
|
|
107
|
+
await this.set(key, data, ttl);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
async pull(key) {
|
|
111
|
+
const data = await this.get(key);
|
|
112
|
+
if (data !== null) await this.del(key);
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
async incr(key, by = 1) {
|
|
116
|
+
const op = by >= 0 ? "incr" : "decr";
|
|
117
|
+
try {
|
|
118
|
+
const path = this.path(key);
|
|
119
|
+
const dbRef = database.ref(this.db, path);
|
|
120
|
+
await database.update(dbRef, {
|
|
121
|
+
"data": database.increment(by),
|
|
122
|
+
"exp": this.ttl > 0 ? Date.now() + this.ttl * 1e3 : null
|
|
123
|
+
});
|
|
124
|
+
const newValue = await this.get(key);
|
|
125
|
+
const sign = by >= 0 ? "+" : "";
|
|
126
|
+
this.log(`${op}:`, key, `${sign}${by} (atomic)`);
|
|
127
|
+
return newValue ?? 0;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
this.log(`error ${op}:`, key, err);
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async decr(key, by = 1) {
|
|
134
|
+
return this.incr(key, -by);
|
|
135
|
+
}
|
|
136
|
+
async expire(key, ttl) {
|
|
137
|
+
try {
|
|
138
|
+
const snapshot = await database.get(database.ref(this.db, this.path(key)));
|
|
139
|
+
if (!snapshot.exists()) {
|
|
140
|
+
this.log("expire failed - key not found:", key);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const entry = snapshot.val();
|
|
144
|
+
entry.exp = ttl > 0 ? Date.now() + ttl * 1e3 : null;
|
|
145
|
+
await database.set(database.ref(this.db, this.path(key)), entry);
|
|
146
|
+
this.log("expire:", key, `(${ttl}s)`);
|
|
147
|
+
return true;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
this.log("error expire:", key, err);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async getTtl(key) {
|
|
154
|
+
try {
|
|
155
|
+
const snapshot = await database.get(database.ref(this.db, this.path(key)));
|
|
156
|
+
if (!snapshot.exists()) return 0;
|
|
157
|
+
const entry = snapshot.val();
|
|
158
|
+
if (!entry.exp) return -1;
|
|
159
|
+
const remaining = Math.max(0, Math.floor((entry.exp - Date.now()) / 1e3));
|
|
160
|
+
return remaining;
|
|
161
|
+
} catch (err) {
|
|
162
|
+
this.log("error ttl:", key, err);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
function createCache(config) {
|
|
168
|
+
return new FirebaseCache(config);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
exports.FirebaseCache = FirebaseCache;
|
|
172
|
+
exports.createCache = createCache;
|
|
173
|
+
//# sourceMappingURL=index.js.map
|
|
174
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts"],"names":["app","getApps","initializeApp","getDatabase","get","ref","set","remove","goOffline","update","increment"],"mappings":";;;;;;AAcO,IAAM,gBAAN,MAAoB;AAAA,EAMzB,YAAY,MAAA,EAAqB;AAC/B,IAAA,IAAIA,KAAA;AACJ,IAAA,MAAM,WAAA,GAAcC,aAAQ,CAAE,IAAA,CAAK,OAAK,CAAA,CAAE,IAAA,MAAU,MAAA,CAAO,QAAA,IAAY,WAAA,CAAY,CAAA;AACnF,IAAA,IAAI,WAAA,EAAa;AACf,MAAAD,KAAA,GAAM,WAAA;AAAA,IACR,CAAA,MAAO;AACL,MAAAA,KAAA,GAAME,iBAAA,CAAc,MAAA,CAAO,QAAA,EAAU,MAAA,CAAO,YAAY,WAAW,CAAA;AAAA,IACrE;AAEA,IAAA,IAAA,CAAK,EAAA,GAAKC,qBAAYH,KAAG,CAAA;AACzB,IAAA,IAAA,CAAK,IAAA,GAAO,OAAO,QAAA,IAAY,YAAA;AAC/B,IAAA,IAAA,CAAK,GAAA,GAAM,OAAO,GAAA,IAAO,IAAA;AACzB,IAAA,IAAA,CAAK,KAAA,GAAQ,OAAO,KAAA,IAAS,KAAA;AAAA,EAC/B;AAAA,EAEQ,KAAK,GAAA,EAAqB;AAChC,IAAA,OAAO,CAAA,EAAG,IAAA,CAAK,IAAI,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA;AAAA,EAC5B;AAAA,EAEQ,OAAO,IAAA,EAAmB;AAChC,IAAA,IAAI,KAAK,KAAA,EAAO,OAAA,CAAQ,GAAA,CAAI,cAAA,EAAgB,GAAG,IAAI,CAAA;AAAA,EACrD;AAAA,EAEA,MAAM,IAAa,GAAA,EAAgC;AACjD,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAMI,YAAA,CAAIC,YAAA,CAAI,IAAA,CAAK,IAAI,IAAA,CAAK,IAAA,CAAK,GAAG,CAAC,CAAC,CAAA;AACvD,MAAA,IAAI,CAAC,QAAA,CAAS,MAAA,EAAO,EAAG;AACtB,QAAA,IAAA,CAAK,GAAA,CAAI,SAAS,GAAG,CAAA;AACrB,QAAA,OAAO,IAAA;AAAA,MACT;AAEA,MAAA,MAAM,KAAA,GAAuB,SAAS,GAAA,EAAI;AAE1C,MAAA,IAAI,MAAM,GAAA,IAAO,IAAA,CAAK,GAAA,EAAI,GAAI,MAAM,GAAA,EAAK;AACvC,QAAA,IAAA,CAAK,GAAA,CAAI,YAAY,GAAG,CAAA;AACxB,QAAA,MAAM,IAAA,CAAK,IAAI,GAAG,CAAA;AAClB,QAAA,OAAO,IAAA;AAAA,MACT;AAEA,MAAA,IAAA,CAAK,GAAA,CAAI,QAAQ,GAAG,CAAA;AACpB,MAAA,OAAO,KAAA,CAAM,IAAA;AAAA,IACf,SAAS,GAAA,EAAK;AACZ,MAAA,IAAA,CAAK,GAAA,CAAI,YAAA,EAAc,GAAA,EAAK,GAAG,CAAA;AAC/B,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,GAAA,CAAa,GAAA,EAAa,IAAA,EAAS,GAAA,EAA6B;AACpE,IAAA,IAAI;AACF,MAAA,MAAM,OAAA,GAAU,GAAA,KAAQ,KAAA,CAAA,GAAY,GAAA,GAAM,IAAA,CAAK,GAAA;AAC/C,MAAA,MAAM,KAAA,GAAuB;AAAA,QAC3B,IAAA;AAAA,QACA,KAAK,OAAA,GAAU,CAAA,GAAI,KAAK,GAAA,EAAI,GAAK,UAAU,GAAA,GAAQ;AAAA,OACrD;AAEA,MAAA,MAAMC,YAAA,CAAID,aAAI,IAAA,CAAK,EAAA,EAAI,KAAK,IAAA,CAAK,GAAG,CAAC,CAAA,EAAG,KAAK,CAAA;AAC7C,MAAA,IAAA,CAAK,GAAA,CAAI,MAAA,EAAQ,GAAA,EAAK,CAAA,CAAA,EAAI,OAAO,CAAA,EAAA,CAAI,CAAA;AAAA,IACvC,SAAS,GAAA,EAAK;AACZ,MAAA,IAAA,CAAK,GAAA,CAAI,YAAA,EAAc,GAAA,EAAK,GAAG,CAAA;AAC/B,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,GAAA,EAA4B;AACpC,IAAA,IAAI;AACF,MAAA,MAAME,eAAA,CAAOF,aAAI,IAAA,CAAK,EAAA,EAAI,KAAK,IAAA,CAAK,GAAG,CAAC,CAAC,CAAA;AACzC,MAAA,IAAA,CAAK,GAAA,CAAI,QAAQ,GAAG,CAAA;AAAA,IACtB,SAAS,GAAA,EAAK;AACZ,MAAA,IAAA,CAAK,GAAA,CAAI,YAAA,EAAc,GAAA,EAAK,GAAG,CAAA;AAAA,IACjC;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,GAAA,EAA+B;AACvC,IAAA,OAAQ,MAAM,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA,KAAO,IAAA;AAAA,EACnC;AAAA,EAEA,MAAM,IAAA,CACJ,GAAA,EACA,OAAA,EACA,GAAA,EACY;AACZ,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,GAAA,CAAO,GAAG,CAAA;AACpC,IAAA,IAAI,MAAA,KAAW,MAAM,OAAO,MAAA;AAE5B,IAAA,IAAA,CAAK,GAAA,CAAI,UAAU,GAAG,CAAA;AACtB,IAAA,MAAM,IAAA,GAAO,MAAM,OAAA,EAAQ;AAC3B,IAAA,MAAM,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,IAAA,EAAM,GAAG,CAAA;AAC7B,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,MAAM,KAAc,IAAA,EAAuC;AACzD,IAAA,OAAO,OAAA,CAAQ,IAAI,IAAA,CAAK,GAAA,CAAI,OAAK,IAAA,CAAK,GAAA,CAAO,CAAC,CAAC,CAAC,CAAA;AAAA,EAClD;AAAA,EAEA,MAAM,IAAA,CAAK,OAAA,EAA8B,GAAA,EAA6B;AACpE,IAAA,MAAM,OAAA,CAAQ,GAAA;AAAA,MACZ,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,CAAE,IAAI,CAAC,CAAC,CAAA,EAAG,CAAC,MAAM,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAA,EAAG,GAAG,CAAC;AAAA,KAC7D;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,IAAA,EAA+B;AACxC,IAAA,MAAM,OAAA,CAAQ,IAAI,IAAA,CAAK,GAAA,CAAI,OAAK,IAAA,CAAK,GAAA,CAAI,CAAC,CAAC,CAAC,CAAA;AAAA,EAC9C;AAAA,EAEA,MAAM,KAAA,GAAuB;AAC3B,IAAA,IAAI;AACF,MAAA,MAAME,gBAAOF,YAAA,CAAI,IAAA,CAAK,EAAA,EAAI,IAAA,CAAK,IAAI,CAAC,CAAA;AACpC,MAAA,IAAA,CAAK,IAAI,aAAa,CAAA;AAAA,IACxB,SAAS,GAAA,EAAK;AACZ,MAAA,IAAA,CAAK,GAAA,CAAI,gBAAgB,GAAG,CAAA;AAC5B,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,UAAA,GAA4B;AAChC,IAAA,MAAMG,kBAAA,CAAU,KAAK,EAAE,CAAA;AACvB,IAAA,IAAA,CAAK,IAAI,cAAc,CAAA;AAAA,EACzB;AAAA,EAEA,MAAM,KAAA,CAAM,GAAA,EAAa,GAAA,EAAgC;AACvD,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AAC/B,IAAA,IAAI,IAAA,KAAS,MAAM,OAAO,KAAA;AAC1B,IAAA,MAAM,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,IAAA,EAAM,GAAG,CAAA;AAC7B,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,MAAM,KAAc,GAAA,EAAgC;AAClD,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,GAAA,CAAO,GAAG,CAAA;AAClC,IAAA,IAAI,IAAA,KAAS,IAAA,EAAM,MAAM,IAAA,CAAK,IAAI,GAAG,CAAA;AACrC,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,MAAM,IAAA,CAAK,GAAA,EAAa,EAAA,GAAa,CAAA,EAAoB;AACvD,IAAA,MAAM,EAAA,GAAK,EAAA,IAAM,CAAA,GAAI,MAAA,GAAS,MAAA;AAC9B,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AAC1B,MAAA,MAAM,KAAA,GAAQH,YAAA,CAAI,IAAA,CAAK,EAAA,EAAI,IAAI,CAAA;AAE/B,MAAA,MAAMI,gBAAO,KAAA,EAAO;AAAA,QAClB,MAAA,EAAQC,mBAAU,EAAE,CAAA;AAAA,QACpB,KAAA,EAAO,KAAK,GAAA,GAAM,CAAA,GAAI,KAAK,GAAA,EAAI,GAAK,IAAA,CAAK,GAAA,GAAM,GAAA,GAAQ;AAAA,OACxD,CAAA;AAED,MAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,GAAA,CAAY,GAAG,CAAA;AAC3C,MAAA,MAAM,IAAA,GAAO,EAAA,IAAM,CAAA,GAAI,GAAA,GAAM,EAAA;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,GAAG,EAAE,CAAA,CAAA,CAAA,EAAK,KAAK,CAAA,EAAG,IAAI,CAAA,EAAG,EAAE,CAAA,SAAA,CAAW,CAAA;AAC/C,MAAA,OAAO,QAAA,IAAY,CAAA;AAAA,IACrB,SAAS,GAAA,EAAK;AACZ,MAAA,IAAA,CAAK,GAAA,CAAI,CAAA,MAAA,EAAS,EAAE,CAAA,CAAA,CAAA,EAAK,KAAK,GAAG,CAAA;AACjC,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,IAAA,CAAK,GAAA,EAAa,EAAA,GAAa,CAAA,EAAoB;AACvD,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,GAAA,EAAK,CAAC,EAAE,CAAA;AAAA,EAC3B;AAAA,EAEA,MAAM,MAAA,CAAO,GAAA,EAAa,GAAA,EAA+B;AACvD,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAMN,YAAA,CAAIC,YAAA,CAAI,IAAA,CAAK,IAAI,IAAA,CAAK,IAAA,CAAK,GAAG,CAAC,CAAC,CAAA;AACvD,MAAA,IAAI,CAAC,QAAA,CAAS,MAAA,EAAO,EAAG;AACtB,QAAA,IAAA,CAAK,GAAA,CAAI,kCAAkC,GAAG,CAAA;AAC9C,QAAA,OAAO,KAAA;AAAA,MACT;AAEA,MAAA,MAAM,KAAA,GAAyB,SAAS,GAAA,EAAI;AAC5C,MAAA,KAAA,CAAM,MAAM,GAAA,GAAM,CAAA,GAAI,KAAK,GAAA,EAAI,GAAK,MAAM,GAAA,GAAQ,IAAA;AAElD,MAAA,MAAMC,YAAA,CAAID,aAAI,IAAA,CAAK,EAAA,EAAI,KAAK,IAAA,CAAK,GAAG,CAAC,CAAA,EAAG,KAAK,CAAA;AAC7C,MAAA,IAAA,CAAK,GAAA,CAAI,SAAA,EAAW,GAAA,EAAK,CAAA,CAAA,EAAI,GAAG,CAAA,EAAA,CAAI,CAAA;AACpC,MAAA,OAAO,IAAA;AAAA,IACT,SAAS,GAAA,EAAK;AACZ,MAAA,IAAA,CAAK,GAAA,CAAI,eAAA,EAAiB,GAAA,EAAK,GAAG,CAAA;AAClC,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,GAAA,EAA8B;AACzC,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAMD,YAAA,CAAIC,YAAA,CAAI,IAAA,CAAK,IAAI,IAAA,CAAK,IAAA,CAAK,GAAG,CAAC,CAAC,CAAA;AACvD,MAAA,IAAI,CAAC,QAAA,CAAS,MAAA,EAAO,EAAG,OAAO,CAAA;AAE/B,MAAA,MAAM,KAAA,GAAyB,SAAS,GAAA,EAAI;AAE5C,MAAA,IAAI,CAAC,KAAA,CAAM,GAAA,EAAK,OAAO,CAAA,CAAA;AAEvB,MAAA,MAAM,SAAA,GAAY,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,KAAA,CAAA,CAAO,KAAA,CAAM,GAAA,GAAM,IAAA,CAAK,GAAA,EAAI,IAAK,GAAI,CAAC,CAAA;AACzE,MAAA,OAAO,SAAA;AAAA,IACT,SAAS,GAAA,EAAK;AACZ,MAAA,IAAA,CAAK,GAAA,CAAI,YAAA,EAAc,GAAA,EAAK,GAAG,CAAA;AAC/B,MAAA,OAAO,CAAA;AAAA,IACT;AAAA,EACF;AACF;AAEO,SAAS,YAAY,MAAA,EAAoC;AAC9D,EAAA,OAAO,IAAI,cAAc,MAAM,CAAA;AACjC","file":"index.js","sourcesContent":["import { initializeApp, getApps } from 'firebase/app';\nimport { \n getDatabase, \n ref, \n get, \n set, \n remove, \n goOffline,\n update,\n increment,\n Database \n} from 'firebase/database';\nimport { CacheConfig, CacheEntry } from './types';\n\nexport class FirebaseCache {\n private db: Database;\n private root: string;\n private ttl: number;\n private debug: boolean;\n\n constructor(config: CacheConfig) {\n let app;\n const existingApp = getApps().find(a => a.name === (config.rootPath || '[DEFAULT]'));\n if (existingApp) {\n app = existingApp;\n } else {\n app = initializeApp(config.firebase, config.rootPath || '[DEFAULT]');\n }\n \n this.db = getDatabase(app);\n this.root = config.rootPath || 'flamecache';\n this.ttl = config.ttl || 3600;\n this.debug = config.debug || false;\n }\n\n private path(key: string): string {\n return `${this.root}/${key}`;\n }\n\n private log(...args: any[]): void {\n if (this.debug) console.log('[Flamecache]', ...args);\n }\n\n async get<T = any>(key: string): Promise<T | null> {\n try {\n const snapshot = await get(ref(this.db, this.path(key)));\n if (!snapshot.exists()) {\n this.log('miss:', key);\n return null;\n }\n\n const entry: CacheEntry<T> = snapshot.val();\n \n if (entry.exp && Date.now() > entry.exp) {\n this.log('expired:', key);\n await this.del(key);\n return null;\n }\n\n this.log('hit:', key);\n return entry.data;\n } catch (err) {\n this.log('error get:', key, err);\n return null;\n }\n }\n\n async set<T = any>(key: string, data: T, ttl?: number): Promise<void> {\n try {\n const seconds = ttl !== undefined ? ttl : this.ttl;\n const entry: CacheEntry<T> = {\n data,\n exp: seconds > 0 ? Date.now() + (seconds * 1000) : null\n };\n \n await set(ref(this.db, this.path(key)), entry);\n this.log('set:', key, `(${seconds}s)`);\n } catch (err) {\n this.log('error set:', key, err);\n throw err;\n }\n }\n\n async del(key: string): Promise<void> {\n try {\n await remove(ref(this.db, this.path(key)));\n this.log('del:', key);\n } catch (err) {\n this.log('error del:', key, err);\n }\n }\n\n async has(key: string): Promise<boolean> {\n return (await this.get(key)) !== null;\n }\n\n async wrap<T = any>(\n key: string,\n fetchFn: () => Promise<T>,\n ttl?: number\n ): Promise<T> {\n const cached = await this.get<T>(key);\n if (cached !== null) return cached;\n\n this.log('fetch:', key);\n const data = await fetchFn();\n await this.set(key, data, ttl);\n return data;\n }\n\n async mget<T = any>(keys: string[]): Promise<(T | null)[]> {\n return Promise.all(keys.map(k => this.get<T>(k)));\n }\n\n async mset(entries: Record<string, any>, ttl?: number): Promise<void> {\n await Promise.all(\n Object.entries(entries).map(([k, v]) => this.set(k, v, ttl))\n );\n }\n\n async mdel(keys: string[]): Promise<void> {\n await Promise.all(keys.map(k => this.del(k)));\n }\n\n async clear(): Promise<void> {\n try {\n await remove(ref(this.db, this.root));\n this.log('cleared all');\n } catch (err) {\n this.log('error clear:', err);\n throw err;\n }\n }\n\n async disconnect(): Promise<void> {\n await goOffline(this.db);\n this.log('disconnected');\n }\n\n async touch(key: string, ttl?: number): Promise<boolean> {\n const data = await this.get(key);\n if (data === null) return false;\n await this.set(key, data, ttl);\n return true;\n }\n\n async pull<T = any>(key: string): Promise<T | null> {\n const data = await this.get<T>(key);\n if (data !== null) await this.del(key);\n return data;\n }\n\n async incr(key: string, by: number = 1): Promise<number> {\n const op = by >= 0 ? 'incr' : 'decr';\n try {\n const path = this.path(key);\n const dbRef = ref(this.db, path);\n \n await update(dbRef, {\n 'data': increment(by),\n 'exp': this.ttl > 0 ? Date.now() + (this.ttl * 1000) : null\n });\n\n const newValue = await this.get<number>(key);\n const sign = by >= 0 ? '+' : '';\n this.log(`${op}:`, key, `${sign}${by} (atomic)`);\n return newValue ?? 0;\n } catch (err) {\n this.log(`error ${op}:`, key, err);\n throw err;\n }\n }\n\n async decr(key: string, by: number = 1): Promise<number> {\n return this.incr(key, -by);\n }\n\n async expire(key: string, ttl: number): Promise<boolean> {\n try {\n const snapshot = await get(ref(this.db, this.path(key)));\n if (!snapshot.exists()) {\n this.log('expire failed - key not found:', key);\n return false;\n }\n\n const entry: CacheEntry<any> = snapshot.val();\n entry.exp = ttl > 0 ? Date.now() + (ttl * 1000) : null;\n \n await set(ref(this.db, this.path(key)), entry);\n this.log('expire:', key, `(${ttl}s)`);\n return true;\n } catch (err) {\n this.log('error expire:', key, err);\n return false;\n }\n }\n\n async getTtl(key: string): Promise<number> {\n try {\n const snapshot = await get(ref(this.db, this.path(key)));\n if (!snapshot.exists()) return 0;\n\n const entry: CacheEntry<any> = snapshot.val();\n \n if (!entry.exp) return -1;\n \n const remaining = Math.max(0, Math.floor((entry.exp - Date.now()) / 1000));\n return remaining;\n } catch (err) {\n this.log('error ttl:', key, err);\n return 0;\n }\n }\n}\n\nexport function createCache(config: CacheConfig): FirebaseCache {\n return new FirebaseCache(config);\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { getApps, initializeApp } from 'firebase/app';
|
|
2
|
+
import { getDatabase, get, ref, set, remove, goOffline, update, increment } from 'firebase/database';
|
|
3
|
+
|
|
4
|
+
// src/cache.ts
|
|
5
|
+
var FirebaseCache = class {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
let app;
|
|
8
|
+
const existingApp = getApps().find((a) => a.name === (config.rootPath || "[DEFAULT]"));
|
|
9
|
+
if (existingApp) {
|
|
10
|
+
app = existingApp;
|
|
11
|
+
} else {
|
|
12
|
+
app = initializeApp(config.firebase, config.rootPath || "[DEFAULT]");
|
|
13
|
+
}
|
|
14
|
+
this.db = getDatabase(app);
|
|
15
|
+
this.root = config.rootPath || "flamecache";
|
|
16
|
+
this.ttl = config.ttl || 3600;
|
|
17
|
+
this.debug = config.debug || false;
|
|
18
|
+
}
|
|
19
|
+
path(key) {
|
|
20
|
+
return `${this.root}/${key}`;
|
|
21
|
+
}
|
|
22
|
+
log(...args) {
|
|
23
|
+
if (this.debug) console.log("[Flamecache]", ...args);
|
|
24
|
+
}
|
|
25
|
+
async get(key) {
|
|
26
|
+
try {
|
|
27
|
+
const snapshot = await get(ref(this.db, this.path(key)));
|
|
28
|
+
if (!snapshot.exists()) {
|
|
29
|
+
this.log("miss:", key);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const entry = snapshot.val();
|
|
33
|
+
if (entry.exp && Date.now() > entry.exp) {
|
|
34
|
+
this.log("expired:", key);
|
|
35
|
+
await this.del(key);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
this.log("hit:", key);
|
|
39
|
+
return entry.data;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
this.log("error get:", key, err);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async set(key, data, ttl) {
|
|
46
|
+
try {
|
|
47
|
+
const seconds = ttl !== void 0 ? ttl : this.ttl;
|
|
48
|
+
const entry = {
|
|
49
|
+
data,
|
|
50
|
+
exp: seconds > 0 ? Date.now() + seconds * 1e3 : null
|
|
51
|
+
};
|
|
52
|
+
await set(ref(this.db, this.path(key)), entry);
|
|
53
|
+
this.log("set:", key, `(${seconds}s)`);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
this.log("error set:", key, err);
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async del(key) {
|
|
60
|
+
try {
|
|
61
|
+
await remove(ref(this.db, this.path(key)));
|
|
62
|
+
this.log("del:", key);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
this.log("error del:", key, err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async has(key) {
|
|
68
|
+
return await this.get(key) !== null;
|
|
69
|
+
}
|
|
70
|
+
async wrap(key, fetchFn, ttl) {
|
|
71
|
+
const cached = await this.get(key);
|
|
72
|
+
if (cached !== null) return cached;
|
|
73
|
+
this.log("fetch:", key);
|
|
74
|
+
const data = await fetchFn();
|
|
75
|
+
await this.set(key, data, ttl);
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
async mget(keys) {
|
|
79
|
+
return Promise.all(keys.map((k) => this.get(k)));
|
|
80
|
+
}
|
|
81
|
+
async mset(entries, ttl) {
|
|
82
|
+
await Promise.all(
|
|
83
|
+
Object.entries(entries).map(([k, v]) => this.set(k, v, ttl))
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
async mdel(keys) {
|
|
87
|
+
await Promise.all(keys.map((k) => this.del(k)));
|
|
88
|
+
}
|
|
89
|
+
async clear() {
|
|
90
|
+
try {
|
|
91
|
+
await remove(ref(this.db, this.root));
|
|
92
|
+
this.log("cleared all");
|
|
93
|
+
} catch (err) {
|
|
94
|
+
this.log("error clear:", err);
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async disconnect() {
|
|
99
|
+
await goOffline(this.db);
|
|
100
|
+
this.log("disconnected");
|
|
101
|
+
}
|
|
102
|
+
async touch(key, ttl) {
|
|
103
|
+
const data = await this.get(key);
|
|
104
|
+
if (data === null) return false;
|
|
105
|
+
await this.set(key, data, ttl);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
async pull(key) {
|
|
109
|
+
const data = await this.get(key);
|
|
110
|
+
if (data !== null) await this.del(key);
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
async incr(key, by = 1) {
|
|
114
|
+
const op = by >= 0 ? "incr" : "decr";
|
|
115
|
+
try {
|
|
116
|
+
const path = this.path(key);
|
|
117
|
+
const dbRef = ref(this.db, path);
|
|
118
|
+
await update(dbRef, {
|
|
119
|
+
"data": increment(by),
|
|
120
|
+
"exp": this.ttl > 0 ? Date.now() + this.ttl * 1e3 : null
|
|
121
|
+
});
|
|
122
|
+
const newValue = await this.get(key);
|
|
123
|
+
const sign = by >= 0 ? "+" : "";
|
|
124
|
+
this.log(`${op}:`, key, `${sign}${by} (atomic)`);
|
|
125
|
+
return newValue ?? 0;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
this.log(`error ${op}:`, key, err);
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async decr(key, by = 1) {
|
|
132
|
+
return this.incr(key, -by);
|
|
133
|
+
}
|
|
134
|
+
async expire(key, ttl) {
|
|
135
|
+
try {
|
|
136
|
+
const snapshot = await get(ref(this.db, this.path(key)));
|
|
137
|
+
if (!snapshot.exists()) {
|
|
138
|
+
this.log("expire failed - key not found:", key);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const entry = snapshot.val();
|
|
142
|
+
entry.exp = ttl > 0 ? Date.now() + ttl * 1e3 : null;
|
|
143
|
+
await set(ref(this.db, this.path(key)), entry);
|
|
144
|
+
this.log("expire:", key, `(${ttl}s)`);
|
|
145
|
+
return true;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
this.log("error expire:", key, err);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async getTtl(key) {
|
|
152
|
+
try {
|
|
153
|
+
const snapshot = await get(ref(this.db, this.path(key)));
|
|
154
|
+
if (!snapshot.exists()) return 0;
|
|
155
|
+
const entry = snapshot.val();
|
|
156
|
+
if (!entry.exp) return -1;
|
|
157
|
+
const remaining = Math.max(0, Math.floor((entry.exp - Date.now()) / 1e3));
|
|
158
|
+
return remaining;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
this.log("error ttl:", key, err);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
function createCache(config) {
|
|
166
|
+
return new FirebaseCache(config);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { FirebaseCache, createCache };
|
|
170
|
+
//# sourceMappingURL=index.mjs.map
|
|
171
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import { initializeApp, getApps } from 'firebase/app';\nimport { \n getDatabase, \n ref, \n get, \n set, \n remove, \n goOffline,\n update,\n increment,\n Database \n} from 'firebase/database';\nimport { CacheConfig, CacheEntry } from './types';\n\nexport class FirebaseCache {\n private db: Database;\n private root: string;\n private ttl: number;\n private debug: boolean;\n\n constructor(config: CacheConfig) {\n let app;\n const existingApp = getApps().find(a => a.name === (config.rootPath || '[DEFAULT]'));\n if (existingApp) {\n app = existingApp;\n } else {\n app = initializeApp(config.firebase, config.rootPath || '[DEFAULT]');\n }\n \n this.db = getDatabase(app);\n this.root = config.rootPath || 'flamecache';\n this.ttl = config.ttl || 3600;\n this.debug = config.debug || false;\n }\n\n private path(key: string): string {\n return `${this.root}/${key}`;\n }\n\n private log(...args: any[]): void {\n if (this.debug) console.log('[Flamecache]', ...args);\n }\n\n async get<T = any>(key: string): Promise<T | null> {\n try {\n const snapshot = await get(ref(this.db, this.path(key)));\n if (!snapshot.exists()) {\n this.log('miss:', key);\n return null;\n }\n\n const entry: CacheEntry<T> = snapshot.val();\n \n if (entry.exp && Date.now() > entry.exp) {\n this.log('expired:', key);\n await this.del(key);\n return null;\n }\n\n this.log('hit:', key);\n return entry.data;\n } catch (err) {\n this.log('error get:', key, err);\n return null;\n }\n }\n\n async set<T = any>(key: string, data: T, ttl?: number): Promise<void> {\n try {\n const seconds = ttl !== undefined ? ttl : this.ttl;\n const entry: CacheEntry<T> = {\n data,\n exp: seconds > 0 ? Date.now() + (seconds * 1000) : null\n };\n \n await set(ref(this.db, this.path(key)), entry);\n this.log('set:', key, `(${seconds}s)`);\n } catch (err) {\n this.log('error set:', key, err);\n throw err;\n }\n }\n\n async del(key: string): Promise<void> {\n try {\n await remove(ref(this.db, this.path(key)));\n this.log('del:', key);\n } catch (err) {\n this.log('error del:', key, err);\n }\n }\n\n async has(key: string): Promise<boolean> {\n return (await this.get(key)) !== null;\n }\n\n async wrap<T = any>(\n key: string,\n fetchFn: () => Promise<T>,\n ttl?: number\n ): Promise<T> {\n const cached = await this.get<T>(key);\n if (cached !== null) return cached;\n\n this.log('fetch:', key);\n const data = await fetchFn();\n await this.set(key, data, ttl);\n return data;\n }\n\n async mget<T = any>(keys: string[]): Promise<(T | null)[]> {\n return Promise.all(keys.map(k => this.get<T>(k)));\n }\n\n async mset(entries: Record<string, any>, ttl?: number): Promise<void> {\n await Promise.all(\n Object.entries(entries).map(([k, v]) => this.set(k, v, ttl))\n );\n }\n\n async mdel(keys: string[]): Promise<void> {\n await Promise.all(keys.map(k => this.del(k)));\n }\n\n async clear(): Promise<void> {\n try {\n await remove(ref(this.db, this.root));\n this.log('cleared all');\n } catch (err) {\n this.log('error clear:', err);\n throw err;\n }\n }\n\n async disconnect(): Promise<void> {\n await goOffline(this.db);\n this.log('disconnected');\n }\n\n async touch(key: string, ttl?: number): Promise<boolean> {\n const data = await this.get(key);\n if (data === null) return false;\n await this.set(key, data, ttl);\n return true;\n }\n\n async pull<T = any>(key: string): Promise<T | null> {\n const data = await this.get<T>(key);\n if (data !== null) await this.del(key);\n return data;\n }\n\n async incr(key: string, by: number = 1): Promise<number> {\n const op = by >= 0 ? 'incr' : 'decr';\n try {\n const path = this.path(key);\n const dbRef = ref(this.db, path);\n \n await update(dbRef, {\n 'data': increment(by),\n 'exp': this.ttl > 0 ? Date.now() + (this.ttl * 1000) : null\n });\n\n const newValue = await this.get<number>(key);\n const sign = by >= 0 ? '+' : '';\n this.log(`${op}:`, key, `${sign}${by} (atomic)`);\n return newValue ?? 0;\n } catch (err) {\n this.log(`error ${op}:`, key, err);\n throw err;\n }\n }\n\n async decr(key: string, by: number = 1): Promise<number> {\n return this.incr(key, -by);\n }\n\n async expire(key: string, ttl: number): Promise<boolean> {\n try {\n const snapshot = await get(ref(this.db, this.path(key)));\n if (!snapshot.exists()) {\n this.log('expire failed - key not found:', key);\n return false;\n }\n\n const entry: CacheEntry<any> = snapshot.val();\n entry.exp = ttl > 0 ? Date.now() + (ttl * 1000) : null;\n \n await set(ref(this.db, this.path(key)), entry);\n this.log('expire:', key, `(${ttl}s)`);\n return true;\n } catch (err) {\n this.log('error expire:', key, err);\n return false;\n }\n }\n\n async getTtl(key: string): Promise<number> {\n try {\n const snapshot = await get(ref(this.db, this.path(key)));\n if (!snapshot.exists()) return 0;\n\n const entry: CacheEntry<any> = snapshot.val();\n \n if (!entry.exp) return -1;\n \n const remaining = Math.max(0, Math.floor((entry.exp - Date.now()) / 1000));\n return remaining;\n } catch (err) {\n this.log('error ttl:', key, err);\n return 0;\n }\n }\n}\n\nexport function createCache(config: CacheConfig): FirebaseCache {\n return new FirebaseCache(config);\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flamecache",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Simple and robust caching layer using Firebase Realtime Database",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"import": "./dist/index.mjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "tsup --watch",
|
|
23
|
+
"test": "jest",
|
|
24
|
+
"test:watch": "jest --watch",
|
|
25
|
+
"prepublishOnly": "npm run build && npm test",
|
|
26
|
+
"lint": "eslint src",
|
|
27
|
+
"example": "tsx -r dotenv/config examples/runner.ts",
|
|
28
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
29
|
+
"clean": "rm -rf dist",
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"firebase",
|
|
34
|
+
"cache",
|
|
35
|
+
"caching",
|
|
36
|
+
"redis-like",
|
|
37
|
+
"realtime-database"
|
|
38
|
+
],
|
|
39
|
+
"author": "Anish Roy",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/iamanishroy/flamecache.git"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"firebase": "^10.0.0 || ^11.0.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@eslint/js": "^9.39.2",
|
|
50
|
+
"@types/jest": "^29.5.0",
|
|
51
|
+
"@types/node": "^20.0.0",
|
|
52
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
53
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
54
|
+
"dotenv": "^17.2.3",
|
|
55
|
+
"eslint": "^8.0.0",
|
|
56
|
+
"firebase": "^10.0.0",
|
|
57
|
+
"jest": "^29.5.0",
|
|
58
|
+
"prettier": "^3.0.0",
|
|
59
|
+
"ts-jest": "^29.1.0",
|
|
60
|
+
"tsup": "^8.0.0",
|
|
61
|
+
"tsx": "^4.21.0",
|
|
62
|
+
"typescript": "^5.0.0"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=16.0.0"
|
|
66
|
+
}
|
|
67
|
+
}
|