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 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
+ [![npm version](https://badge.fury.io/js/flamecache.svg)](https://www.npmjs.com/package/flamecache)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org/)
6
+ [![Tests](https://github.com/iamanishroy/flamecache/workflows/CI/badge.svg)](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)
@@ -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 };
@@ -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":"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","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
+ }