hono-universal-cache 0.1.2

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/README.md ADDED
@@ -0,0 +1,400 @@
1
+ # hono-universal-cache
2
+
3
+ Universal cache middleware for [Hono](https://hono.dev) powered by [unstorage](https://unstorage.unjs.io).
4
+
5
+ Cache **API responses** across **any runtime** - Cloudflare Workers, Vercel Edge, Node.js, Bun, Deno, and more.
6
+
7
+ > **Note:** Optimized for API responses (JSON, text, HTML). For static assets (images, videos, files), use CDN/edge caching instead.
8
+
9
+ ## Features
10
+
11
+ ✨ **Universal Runtime Support** - Works everywhere Hono works
12
+ đŸ—„ī¸ **Multiple Storage Drivers** - Memory, Redis, Cloudflare KV, Vercel KV, filesystem, and [more](https://unstorage.unjs.io/drivers)
13
+ ⚡ **TTL Support** - Automatic expiration with configurable time-to-live
14
+ đŸŽ¯ **Selective Caching** - Control what gets cached by status code
15
+ 🔑 **Custom Key Generation** - Flexible cache key strategies
16
+ đŸĒļ **Lightweight** - Minimal overhead, focused on storage operations
17
+ 🎨 **HTTP Header Agnostic** - You control your own Cache-Control headers
18
+ đŸ“Ļ **Efficient Storage** - Optimized for text-based API responses
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install hono-universal-cache
24
+ # or
25
+ pnpm add hono-universal-cache
26
+ # or
27
+ yarn add hono-universal-cache
28
+ ```
29
+
30
+ > **Note:** `unstorage` is included as a dependency - no need to install it separately!
31
+
32
+ ## Quick Start
33
+
34
+ ### Basic Usage (In-Memory)
35
+
36
+ ```typescript
37
+ import { Hono } from 'hono'
38
+ import { universalCache } from 'hono-universal-cache'
39
+
40
+ const app = new Hono()
41
+
42
+ app.use('*', universalCache({
43
+ cacheName: 'my-app-cache',
44
+ ttl: 3600 // 1 hour
45
+ }))
46
+
47
+ app.get('/api/data', (c) => {
48
+ // Set your own Cache-Control headers
49
+ c.header('Cache-Control', 'public, max-age=3600')
50
+ return c.json({ timestamp: Date.now() })
51
+ })
52
+
53
+ export default app
54
+ ```
55
+
56
+ ### With Custom Storage Driver
57
+
58
+ ```typescript
59
+ import { createStorage } from 'unstorage'
60
+ import redisDriver from 'unstorage/drivers/redis'
61
+
62
+ const storage = createStorage({
63
+ driver: redisDriver({
64
+ host: 'localhost',
65
+ port: 6379
66
+ })
67
+ })
68
+
69
+ app.use('*', universalCache({
70
+ cacheName: 'api-cache',
71
+ storage,
72
+ ttl: 3600
73
+ }))
74
+ ```
75
+
76
+ ## Runtime-Specific Examples
77
+
78
+ ### Cloudflare Workers
79
+
80
+ ```typescript
81
+ import { Hono } from 'hono'
82
+ import { universalCache } from 'hono-universal-cache'
83
+ import { createStorage } from 'unstorage'
84
+ import cloudflareKVBindingDriver from 'unstorage/drivers/cloudflare-kv-binding'
85
+
86
+ type Env = {
87
+ MY_KV: KVNamespace
88
+ }
89
+
90
+ const app = new Hono<{ Bindings: Env }>()
91
+
92
+ app.use('*', async (c, next) => {
93
+ const storage = createStorage({
94
+ driver: cloudflareKVBindingDriver({
95
+ binding: c.env.MY_KV
96
+ })
97
+ })
98
+
99
+ return universalCache({
100
+ cacheName: 'worker-cache',
101
+ storage,
102
+ ttl: 3600
103
+ })(c, next)
104
+ })
105
+
106
+ export default app
107
+ ```
108
+
109
+ ### Vercel Edge
110
+
111
+ ```typescript
112
+ import { createStorage } from 'unstorage'
113
+ import vercelKVDriver from 'unstorage/drivers/vercel-kv'
114
+
115
+ const storage = createStorage({
116
+ driver: vercelKVDriver({
117
+ // Auto-detects from environment:
118
+ // KV_REST_API_URL and KV_REST_API_TOKEN
119
+ })
120
+ })
121
+
122
+ app.use('*', universalCache({
123
+ cacheName: 'edge-cache',
124
+ storage,
125
+ ttl: 3600
126
+ }))
127
+ ```
128
+
129
+ ### Node.js / Bun (Filesystem)
130
+
131
+ ```typescript
132
+ import { createStorage } from 'unstorage'
133
+ import fsDriver from 'unstorage/drivers/fs'
134
+
135
+ const storage = createStorage({
136
+ driver: fsDriver({
137
+ base: './cache'
138
+ })
139
+ })
140
+
141
+ app.use('*', universalCache({
142
+ cacheName: 'fs-cache',
143
+ storage,
144
+ ttl: 3600
145
+ }))
146
+ ```
147
+
148
+ ### Redis
149
+
150
+ ```typescript
151
+ import { createStorage } from 'unstorage'
152
+ import redisDriver from 'unstorage/drivers/redis'
153
+
154
+ const storage = createStorage({
155
+ driver: redisDriver({
156
+ host: 'localhost',
157
+ port: 6379,
158
+ // password: 'your-password'
159
+ })
160
+ })
161
+
162
+ app.use('*', universalCache({
163
+ cacheName: 'redis-cache',
164
+ storage,
165
+ ttl: 3600
166
+ }))
167
+ ```
168
+
169
+ ## API Reference
170
+
171
+ ### `universalCache(options)`
172
+
173
+ Creates a Hono middleware for response caching.
174
+
175
+ #### Options
176
+
177
+ ```typescript
178
+ type CacheOptions = {
179
+ // Required: Cache namespace
180
+ cacheName: string | ((c: Context) => Promise<string> | string)
181
+
182
+ // Optional: Unstorage instance (defaults to in-memory)
183
+ storage?: Storage
184
+
185
+ // Optional: Time-to-live in seconds
186
+ ttl?: number
187
+
188
+ // Optional: Status codes to cache (default: [200])
189
+ cacheableStatusCodes?: number[]
190
+
191
+ // Optional: Custom cache key generator
192
+ keyGenerator?: (c: Context) => Promise<string> | string
193
+ }
194
+ ```
195
+
196
+ ## Common Use Cases
197
+
198
+ ### Dynamic Cache Names
199
+
200
+ Cache different tenants or users separately:
201
+
202
+ ```typescript
203
+ app.use('*', universalCache({
204
+ cacheName: (c) => {
205
+ const tenant = c.req.header('X-Tenant-ID') || 'default'
206
+ return `cache:${tenant}`
207
+ },
208
+ storage,
209
+ ttl: 3600
210
+ }))
211
+ ```
212
+
213
+ ### Custom Key Generation
214
+
215
+ Cache based on custom logic (e.g., ignore specific query params):
216
+
217
+ ```typescript
218
+ app.use('*', universalCache({
219
+ cacheName: 'api-cache',
220
+ keyGenerator: (c) => {
221
+ const url = new URL(c.req.url)
222
+ // Ignore tracking parameters
223
+ url.searchParams.delete('utm_source')
224
+ url.searchParams.delete('utm_campaign')
225
+ return url.toString()
226
+ },
227
+ storage
228
+ }))
229
+ ```
230
+
231
+ ### Selective Caching by Status Code
232
+
233
+ Cache successful and redirect responses:
234
+
235
+ ```typescript
236
+ app.use('*', universalCache({
237
+ cacheName: 'selective-cache',
238
+ cacheableStatusCodes: [200, 201, 301, 302],
239
+ storage
240
+ }))
241
+ ```
242
+
243
+ ### Managing HTTP Headers
244
+
245
+ The middleware focuses on storage-level caching. You control HTTP headers in your route handlers:
246
+
247
+ ```typescript
248
+ app.get('/api/public', (c) => {
249
+ // Set headers for browser/CDN caching
250
+ c.header('Cache-Control', 'public, max-age=3600')
251
+ c.header('Vary', 'Accept-Encoding')
252
+
253
+ return c.json({ data: 'public data' })
254
+ })
255
+
256
+ app.get('/api/private', (c) => {
257
+ // Private data - browser should not cache
258
+ c.header('Cache-Control', 'private, no-cache')
259
+
260
+ // But server-side cache can still store it
261
+ return c.json({ data: 'user-specific data' })
262
+ })
263
+ ```
264
+
265
+ ## Storage Drivers
266
+
267
+ Use any [unstorage driver](https://unstorage.unjs.io/drivers):
268
+
269
+ ### Popular Drivers
270
+
271
+ - **Memory** - `unstorage/drivers/memory` (default, ephemeral)
272
+ - **Filesystem** - `unstorage/drivers/fs` (Node.js/Bun)
273
+ - **Redis** - `unstorage/drivers/redis` (persistent, distributed)
274
+ - **Cloudflare KV** - `unstorage/drivers/cloudflare-kv-binding`
275
+ - **Vercel KV** - `unstorage/drivers/vercel-kv`
276
+ - **MongoDB** - `unstorage/drivers/mongodb`
277
+ - **Upstash Redis** - `unstorage/drivers/upstash`
278
+ - **LRU Cache** - `unstorage/drivers/lru-cache` (in-memory with eviction)
279
+
280
+ ### Cloud Storage
281
+
282
+ - **AWS S3** - `unstorage/drivers/s3`
283
+ - **Azure Blob** - `unstorage/drivers/azure-storage-blob`
284
+ - **Cloudflare R2** - `unstorage/drivers/cloudflare-r2-binding`
285
+ - **Vercel Blob** - `unstorage/drivers/vercel-blob`
286
+
287
+ See [all 50+ drivers](https://unstorage.unjs.io/drivers) in the unstorage documentation.
288
+
289
+ ## How It Works
290
+
291
+ 1. **Request arrives** → Middleware generates cache key
292
+ 2. **Check cache** → Retrieve from storage if exists and not expired
293
+ 3. **Cache hit** → Return cached response immediately
294
+ 4. **Cache miss** → Execute route handler
295
+ 5. **Check cacheability** → Verify status code and Vary header
296
+ 6. **Store response** → Save text body to storage with TTL metadata (non-blocking)
297
+ 7. **Return response** → Send to client
298
+
299
+ ## Important Notes
300
+
301
+ ### Optimized for API Responses
302
+
303
+ This middleware is designed for **text-based API responses**:
304
+
305
+ - ✅ **JSON APIs** - Perfect use case
306
+ - ✅ **Text responses** - Works great
307
+ - ✅ **HTML pages** - Fully supported
308
+ - ✅ **XML/RSS feeds** - No problem
309
+ - ❌ **Binary assets** (images, PDFs, videos) - Use CDN/edge caching instead
310
+
311
+ **Why not binary?** The middleware uses `response.text()` for optimal storage efficiency. For static assets, use:
312
+ - CDN caching (Cloudflare, CloudFront)
313
+ - Object storage (S3, R2, Blob Storage)
314
+ - Hono's built-in static file serving with CDN
315
+
316
+ ### Vary: * Behavior
317
+
318
+ Responses with `Vary: *` header are **never cached**, per RFC 9111:
319
+
320
+ ```typescript
321
+ app.get('/api/uncacheable', (c) => {
322
+ c.header('Vary', '*') // This response will not be cached
323
+ return c.json({ random: Math.random() })
324
+ })
325
+ ```
326
+
327
+ ### HTTP Headers vs Storage Caching
328
+
329
+ This middleware handles **server-side storage caching only**:
330
+
331
+ - ✅ Stores responses in Redis, KV, filesystem, etc.
332
+ - ❌ Does NOT modify Cache-Control or other HTTP headers
333
+ - 💡 You control browser/CDN caching via headers in your routes
334
+
335
+ ### Non-blocking Cache Writes
336
+
337
+ Cache writes happen asynchronously and don't block responses:
338
+
339
+ - **Cloudflare Workers/Vercel Edge**: Uses `waitUntil()` for background writes
340
+ - **Other runtimes**: Uses promises with error handling
341
+
342
+ ## Advanced Usage
343
+
344
+ ### CacheManager API
345
+
346
+ Access the low-level cache manager for manual operations:
347
+
348
+ ```typescript
349
+ import { CacheManager } from 'hono-universal-cache'
350
+ import { createStorage } from 'unstorage'
351
+
352
+ const storage = createStorage()
353
+ const cache = new CacheManager(storage, 3600) // 1 hour TTL
354
+
355
+ // Manual cache operations
356
+ await cache.set('key', response)
357
+ const cached = await cache.get('key')
358
+ const exists = await cache.has('key')
359
+ await cache.delete('key')
360
+ await cache.clear()
361
+ const keys = await cache.keys()
362
+ ```
363
+
364
+ ### Per-Route Caching
365
+
366
+ Apply caching to specific routes only:
367
+
368
+ ```typescript
369
+ const app = new Hono()
370
+
371
+ // Global middleware without cache
372
+ app.use('*', logger())
373
+
374
+ // Cache only API routes
375
+ app.use('/api/*', universalCache({
376
+ cacheName: 'api-cache',
377
+ storage,
378
+ ttl: 300 // 5 minutes
379
+ }))
380
+
381
+ // Cache product pages longer
382
+ app.use('/products/*', universalCache({
383
+ cacheName: 'products-cache',
384
+ storage,
385
+ ttl: 3600 // 1 hour
386
+ }))
387
+ ```
388
+
389
+ ## License
390
+
391
+ MIT
392
+
393
+ ## Contributing
394
+
395
+ Contributions welcome! Please open an issue or PR.
396
+
397
+ ## Credits
398
+
399
+ - [Hono](https://hono.dev) - Ultrafast web framework
400
+ - [unstorage](https://unstorage.unjs.io) - Universal storage layer
@@ -0,0 +1,181 @@
1
+ import { Storage } from "unstorage";
2
+ import { Context, MiddlewareHandler } from "hono";
3
+
4
+ //#region src/types.d.ts
5
+
6
+ /**
7
+ * Options for configuring the universal cache middleware.
8
+ */
9
+ type CacheOptions = {
10
+ /**
11
+ * Name or function to generate cache namespace.
12
+ * Used to organize cached entries.
13
+ */
14
+ cacheName: string | ((c: Context) => Promise<string> | string);
15
+ /**
16
+ * HTTP status codes that should be cached.
17
+ * @default [200]
18
+ */
19
+ cacheableStatusCodes?: number[];
20
+ /**
21
+ * Custom function to generate cache keys.
22
+ * @default Uses request URL
23
+ */
24
+ keyGenerator?: (c: Context) => Promise<string> | string;
25
+ /**
26
+ * Unstorage instance for cache persistence.
27
+ * If not provided, defaults to in-memory storage.
28
+ */
29
+ storage?: Storage;
30
+ /**
31
+ * Time-to-live for cached entries in seconds.
32
+ * After this duration, cached entries are considered stale.
33
+ */
34
+ ttl?: number;
35
+ };
36
+ /**
37
+ * Serialized representation of a cached Response.
38
+ * Stored in unstorage with metadata.
39
+ * Optimized for text-based API responses (JSON, text, HTML).
40
+ */
41
+ type CachedResponse = {
42
+ /**
43
+ * Response body as text string.
44
+ * Works for JSON, text, HTML, and other text-based responses.
45
+ */
46
+ body: string;
47
+ /**
48
+ * Response headers as key-value pairs.
49
+ */
50
+ headers: Record<string, string>;
51
+ /**
52
+ * HTTP status code.
53
+ */
54
+ status: number;
55
+ /**
56
+ * HTTP status text.
57
+ */
58
+ statusText: string;
59
+ /**
60
+ * Timestamp when the response was cached (Unix epoch ms).
61
+ */
62
+ cachedAt: number;
63
+ };
64
+ /**
65
+ * Metadata stored alongside cached entries.
66
+ */
67
+ type CacheMetadata = {
68
+ /**
69
+ * Timestamp when entry was created (Date object for unstorage compatibility).
70
+ */
71
+ mtime: Date;
72
+ /**
73
+ * Expiration timestamp (Unix epoch ms).
74
+ * Calculated as cachedAt + ttl.
75
+ */
76
+ expires?: number;
77
+ };
78
+ //#endregion
79
+ //#region src/cache.d.ts
80
+ /**
81
+ * CacheManager handles Response serialization, storage, and retrieval.
82
+ *
83
+ * Wraps unstorage to provide Response-specific caching with TTL support.
84
+ * Designed for API responses (JSON, text, HTML) - not binary assets.
85
+ */
86
+ declare class CacheManager {
87
+ private storage;
88
+ private ttl?;
89
+ /**
90
+ * Creates a new CacheManager instance.
91
+ *
92
+ * @param storage - Unstorage instance
93
+ * @param ttl - Optional time-to-live in seconds
94
+ */
95
+ constructor(storage: Storage, ttl?: number);
96
+ /**
97
+ * Serializes a Response object for storage.
98
+ *
99
+ * Converts Response to a plain object with text body.
100
+ * Optimized for API responses (JSON, text, HTML).
101
+ *
102
+ * @param response - Response to serialize
103
+ * @returns Serialized response object
104
+ */
105
+ private serializeResponse;
106
+ /**
107
+ * Deserializes a cached response back to Response object.
108
+ *
109
+ * @param cached - Serialized response
110
+ * @returns Reconstructed Response object
111
+ */
112
+ private deserializeResponse;
113
+ /**
114
+ * Retrieves a cached response by key.
115
+ *
116
+ * Returns null if:
117
+ * - Entry doesn't exist
118
+ * - Entry has expired (based on TTL)
119
+ *
120
+ * @param key - Cache key
121
+ * @returns Cached Response or null
122
+ */
123
+ get(key: string): Promise<Response | null>;
124
+ /**
125
+ * Stores a Response in cache.
126
+ *
127
+ * @param key - Cache key
128
+ * @param response - Response to cache
129
+ */
130
+ set(key: string, response: Response): Promise<void>;
131
+ /**
132
+ * Checks if a key exists in cache and is not expired.
133
+ *
134
+ * @param key - Cache key
135
+ * @returns True if key exists and is valid
136
+ */
137
+ has(key: string): Promise<boolean>;
138
+ /**
139
+ * Deletes a cached entry.
140
+ *
141
+ * @param key - Cache key
142
+ */
143
+ delete(key: string): Promise<void>;
144
+ /**
145
+ * Clears all cached entries.
146
+ */
147
+ clear(): Promise<void>;
148
+ /**
149
+ * Gets all cache keys.
150
+ *
151
+ * @returns Array of cache keys
152
+ */
153
+ keys(): Promise<string[]>;
154
+ }
155
+ //#endregion
156
+ //#region src/index.d.ts
157
+ /**
158
+ * Universal cache middleware for Hono.
159
+ *
160
+ * Caches responses using unstorage for cross-runtime compatibility.
161
+ * Works with Cloudflare Workers, Vercel Edge, Node.js, Bun, Deno, and more.
162
+ *
163
+ * @param options - Cache configuration options
164
+ * @returns Hono middleware handler
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * import { Hono } from 'hono'
169
+ * import { universalCache } from 'hono-universal-cache'
170
+ *
171
+ * const app = new Hono()
172
+ *
173
+ * app.use('*', universalCache({
174
+ * cacheName: 'my-app-cache',
175
+ * ttl: 3600
176
+ * }))
177
+ * ```
178
+ */
179
+ declare const universalCache: (options: CacheOptions) => MiddlewareHandler;
180
+ //#endregion
181
+ export { CacheManager, type CacheMetadata, type CacheOptions, type CachedResponse, universalCache };
package/dist/index.mjs ADDED
@@ -0,0 +1,248 @@
1
+ import { createStorage } from "unstorage";
2
+ import { getRuntimeKey } from "hono/adapter";
3
+
4
+ //#region src/utils.ts
5
+ /**
6
+ * Generates a cache key for the given context.
7
+ *
8
+ * @param c - Hono context
9
+ * @param keyGenerator - Optional custom key generator function
10
+ * @returns Cache key string
11
+ */
12
+ async function generateCacheKey(c, keyGenerator) {
13
+ if (keyGenerator) return await keyGenerator(c);
14
+ return c.req.url;
15
+ }
16
+ /**
17
+ * Checks if a status code is cacheable.
18
+ *
19
+ * @param status - HTTP status code
20
+ * @param cacheableStatusCodes - Set of allowed status codes
21
+ * @returns True if status code is cacheable
22
+ */
23
+ function isCacheableStatus(status, cacheableStatusCodes) {
24
+ return cacheableStatusCodes.has(status);
25
+ }
26
+ /**
27
+ * Checks if a cached entry has expired based on TTL.
28
+ *
29
+ * @param cachedAt - Timestamp when entry was cached (Unix epoch ms)
30
+ * @param ttl - Time-to-live in seconds
31
+ * @returns True if entry has expired
32
+ */
33
+ function isExpired(cachedAt, ttl) {
34
+ if (!ttl) return false;
35
+ return Date.now() > cachedAt + ttl * 1e3;
36
+ }
37
+
38
+ //#endregion
39
+ //#region src/cache.ts
40
+ /**
41
+ * CacheManager handles Response serialization, storage, and retrieval.
42
+ *
43
+ * Wraps unstorage to provide Response-specific caching with TTL support.
44
+ * Designed for API responses (JSON, text, HTML) - not binary assets.
45
+ */
46
+ var CacheManager = class {
47
+ storage;
48
+ ttl;
49
+ /**
50
+ * Creates a new CacheManager instance.
51
+ *
52
+ * @param storage - Unstorage instance
53
+ * @param ttl - Optional time-to-live in seconds
54
+ */
55
+ constructor(storage, ttl) {
56
+ this.storage = storage;
57
+ this.ttl = ttl;
58
+ }
59
+ /**
60
+ * Serializes a Response object for storage.
61
+ *
62
+ * Converts Response to a plain object with text body.
63
+ * Optimized for API responses (JSON, text, HTML).
64
+ *
65
+ * @param response - Response to serialize
66
+ * @returns Serialized response object
67
+ */
68
+ async serializeResponse(response) {
69
+ const body = await response.text();
70
+ const headers = {};
71
+ response.headers.forEach((value, key) => {
72
+ headers[key] = value;
73
+ });
74
+ return {
75
+ body,
76
+ headers,
77
+ status: response.status,
78
+ statusText: response.statusText,
79
+ cachedAt: Date.now()
80
+ };
81
+ }
82
+ /**
83
+ * Deserializes a cached response back to Response object.
84
+ *
85
+ * @param cached - Serialized response
86
+ * @returns Reconstructed Response object
87
+ */
88
+ deserializeResponse(cached) {
89
+ const headers = new Headers(cached.headers);
90
+ const body = cached.status === 204 || cached.status === 205 ? null : cached.body;
91
+ return new Response(body, {
92
+ status: cached.status,
93
+ statusText: cached.statusText,
94
+ headers
95
+ });
96
+ }
97
+ /**
98
+ * Retrieves a cached response by key.
99
+ *
100
+ * Returns null if:
101
+ * - Entry doesn't exist
102
+ * - Entry has expired (based on TTL)
103
+ *
104
+ * @param key - Cache key
105
+ * @returns Cached Response or null
106
+ */
107
+ async get(key) {
108
+ try {
109
+ const cached = await this.storage.getItem(key);
110
+ if (!cached) return null;
111
+ if (isExpired(cached.cachedAt, this.ttl)) {
112
+ await this.delete(key);
113
+ return null;
114
+ }
115
+ return this.deserializeResponse(cached);
116
+ } catch (error) {
117
+ console.error("Cache retrieval error:", error);
118
+ return null;
119
+ }
120
+ }
121
+ /**
122
+ * Stores a Response in cache.
123
+ *
124
+ * @param key - Cache key
125
+ * @param response - Response to cache
126
+ */
127
+ async set(key, response) {
128
+ try {
129
+ const serialized = await this.serializeResponse(response);
130
+ await this.storage.setItem(key, serialized);
131
+ if (this.ttl) {
132
+ const metadata = {
133
+ mtime: new Date(serialized.cachedAt),
134
+ expires: serialized.cachedAt + this.ttl * 1e3
135
+ };
136
+ await this.storage.setMeta(key, metadata);
137
+ }
138
+ } catch (error) {
139
+ console.error("Cache storage error:", error);
140
+ }
141
+ }
142
+ /**
143
+ * Checks if a key exists in cache and is not expired.
144
+ *
145
+ * @param key - Cache key
146
+ * @returns True if key exists and is valid
147
+ */
148
+ async has(key) {
149
+ try {
150
+ const cached = await this.storage.getItem(key);
151
+ if (!cached) return false;
152
+ return !isExpired(cached.cachedAt, this.ttl);
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+ /**
158
+ * Deletes a cached entry.
159
+ *
160
+ * @param key - Cache key
161
+ */
162
+ async delete(key) {
163
+ try {
164
+ await this.storage.removeItem(key);
165
+ } catch (error) {
166
+ console.error("Cache deletion error:", error);
167
+ }
168
+ }
169
+ /**
170
+ * Clears all cached entries.
171
+ */
172
+ async clear() {
173
+ try {
174
+ await this.storage.clear();
175
+ } catch (error) {
176
+ console.error("Cache clear error:", error);
177
+ }
178
+ }
179
+ /**
180
+ * Gets all cache keys.
181
+ *
182
+ * @returns Array of cache keys
183
+ */
184
+ async keys() {
185
+ try {
186
+ return await this.storage.getKeys();
187
+ } catch (error) {
188
+ console.error("Cache keys retrieval error:", error);
189
+ return [];
190
+ }
191
+ }
192
+ };
193
+
194
+ //#endregion
195
+ //#region src/types.ts
196
+ /**
197
+ * Status codes that can be cached by default.
198
+ */
199
+ const DEFAULT_CACHEABLE_STATUS_CODES = [200];
200
+
201
+ //#endregion
202
+ //#region src/index.ts
203
+ /**
204
+ * Universal cache middleware for Hono.
205
+ *
206
+ * Caches responses using unstorage for cross-runtime compatibility.
207
+ * Works with Cloudflare Workers, Vercel Edge, Node.js, Bun, Deno, and more.
208
+ *
209
+ * @param options - Cache configuration options
210
+ * @returns Hono middleware handler
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * import { Hono } from 'hono'
215
+ * import { universalCache } from 'hono-universal-cache'
216
+ *
217
+ * const app = new Hono()
218
+ *
219
+ * app.use('*', universalCache({
220
+ * cacheName: 'my-app-cache',
221
+ * ttl: 3600
222
+ * }))
223
+ * ```
224
+ */
225
+ const universalCache = (options) => {
226
+ let storage = options.storage;
227
+ if (!storage) {
228
+ console.warn("No storage provided, using default in-memory storage");
229
+ storage = createStorage();
230
+ }
231
+ const cacheManager = new CacheManager(storage, options.ttl);
232
+ const cacheableStatusCodes = new Set(options.cacheableStatusCodes ?? DEFAULT_CACHEABLE_STATUS_CODES);
233
+ return async (c, next) => {
234
+ const baseKey = await generateCacheKey(c, options.keyGenerator);
235
+ const key = `${typeof options.cacheName === "function" ? await options.cacheName(c) : options.cacheName}:${baseKey}`;
236
+ const cachedResponse = await cacheManager.get(key);
237
+ if (cachedResponse) return new Response(cachedResponse.body, cachedResponse);
238
+ await next();
239
+ if (!isCacheableStatus(c.res.status, cacheableStatusCodes)) return;
240
+ const res = c.res.clone();
241
+ const cachePromise = cacheManager.set(key, res);
242
+ if (getRuntimeKey() === "workerd") c?.executionCtx?.waitUntil?.(cachePromise);
243
+ else await cachePromise;
244
+ };
245
+ };
246
+
247
+ //#endregion
248
+ export { CacheManager, universalCache };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "hono-universal-cache",
3
+ "type": "module",
4
+ "version": "0.1.2",
5
+ "description": "Universal cache middleware for Hono powered by unstorage - works across Cloudflare Workers, Vercel Edge, Node.js, Bun, Deno, and more.",
6
+ "author": "Anas Mohammed",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/anasmohammed361/hono-universal-cache#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/anasmohammed361/hono-universal-cache.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/anasmohammed361/hono-universal-cache/issues"
15
+ },
16
+ "keywords": [
17
+ "hono",
18
+ "cache",
19
+ "middleware",
20
+ "universal",
21
+ "unstorage",
22
+ "cloudflare",
23
+ "vercel",
24
+ "edge",
25
+ "workers",
26
+ "redis",
27
+ "kv"
28
+ ],
29
+ "exports": {
30
+ ".": "./dist/index.mjs",
31
+ "./package.json": "./package.json"
32
+ },
33
+ "main": "./dist/index.mjs",
34
+ "module": "./dist/index.mjs",
35
+ "types": "./dist/index.d.mts",
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "devDependencies": {
40
+ "@types/node": "^25.0.3",
41
+ "bumpp": "^10.3.2",
42
+ "hono": "^4.11.3",
43
+ "tsdown": "^0.18.1",
44
+ "typescript": "^5.9.3",
45
+ "vitest": "^4.0.16"
46
+ },
47
+ "dependencies": {
48
+ "unstorage": "^1.17.3"
49
+ },
50
+ "peerDependencies": {
51
+ "hono": ">=4.0.0"
52
+ },
53
+ "scripts": {
54
+ "build": "tsdown",
55
+ "dev": "tsdown --watch",
56
+ "test": "vitest",
57
+ "typecheck": "tsc --noEmit",
58
+ "release": "pnpm build && pnpm typecheck && bumpp && pnpm run release:publish",
59
+ "release:publish": "gh release create v$(node -p \"require('./package.json').version\") --generate-notes"
60
+ }
61
+ }