+33
-10
@@ -1,2 +0,1 @@ | ||
| //#region src/types.d.ts | ||
| /** | ||
@@ -34,2 +33,4 @@ * Extended `Request` interface with optional `waitUntil` for background tasks. | ||
| integrity?: string; | ||
| /** When `true`, the entry is treated as expired on next access (set by `expireCache`). Cleared after a successful revalidation. */ | ||
| stale?: boolean; | ||
| } | ||
@@ -115,8 +116,7 @@ /** | ||
| } | ||
| //#endregion | ||
| //#region src/cache.d.ts | ||
| type CachedFunction<T, ArgsT extends unknown[]> = { | ||
| (...args: ArgsT): Promise<T>; /** Resolves all storage keys (one per base prefix) for the given arguments. */ | ||
| resolveKeys: (...args: ArgsT) => Promise<string[]>; /** Invalidates (removes) cached entries for the given arguments across all base prefixes. */ | ||
| invalidate: (...args: ArgsT) => Promise<void>; | ||
| invalidate: (...args: ArgsT) => Promise<void>; /** Marks cached entries as stale across all base prefixes. With SWR, stale values are still served (within `staleMaxAge`) while the next access triggers a background refresh. */ | ||
| expire: (...args: ArgsT) => Promise<void>; | ||
| }; | ||
@@ -181,5 +181,31 @@ /** | ||
| }): Promise<void>; | ||
| //#endregion | ||
| //#region src/http.d.ts | ||
| /** | ||
| * Expires cached entries for given arguments and cache options across all base prefixes, | ||
| * without removing them. | ||
| * | ||
| * Unlike {@link invalidateCache} (which removes entries entirely), expired entries keep | ||
| * serving the stale value with SWR — still bounded by the originally configured | ||
| * `staleMaxAge` window — while the next access triggers a background refresh. | ||
| * Without SWR, the next call re-resolves before returning. | ||
| * | ||
| * Uses the same key derivation as `defineCachedFunction` / `resolveCacheKeys`. | ||
| * Pass the same `maxAge` / `swr` / `staleMaxAge` options you cache with so the | ||
| * remaining storage TTL is preserved. | ||
| * | ||
| * @param input - Object with `options` (cache options) and optional `args` (function arguments). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Mark a cached entry for background refresh on next access | ||
| * await expireCache({ | ||
| * options: { name: "fetchUser", getKey: (id: string) => id, maxAge: 60, staleMaxAge: 300 }, | ||
| * args: ["user-123"], | ||
| * }); | ||
| * ``` | ||
| */ | ||
| declare function expireCache<ArgsT extends unknown[] = any[]>(input?: { | ||
| options?: Pick<CacheOptions<any, ArgsT>, "base" | "group" | "name" | "getKey" | "maxAge" | "swr" | "staleMaxAge">; | ||
| args?: ArgsT; | ||
| }): Promise<void>; | ||
| /** | ||
| * Wraps an HTTP event handler with response caching. | ||
@@ -196,4 +222,2 @@ * | ||
| declare function defineCachedHandler<E extends HTTPEvent = HTTPEvent>(handler: EventHandler<E>, opts?: CachedEventHandlerOptions<E>): EventHandler<E>; | ||
| //#endregion | ||
| //#region src/storage.d.ts | ||
| interface StorageInterface { | ||
@@ -211,3 +235,2 @@ get<T = unknown>(key: string): T | null | Promise<T | null>; | ||
| declare function setStorage(storage: StorageInterface): void; | ||
| //#endregion | ||
| export { type CacheConditions, type CacheEntry, type CacheOptions, type CachedEventHandlerOptions, type CachedFunction, type EventHandler, type HTTPEvent, type ResponseCacheEntry, type ServerRequest, type StorageInterface, cachedFunction, createMemoryStorage, defineCachedFunction, defineCachedHandler, invalidateCache, resolveCacheKeys, setStorage, useStorage }; | ||
| export { type CacheConditions, type CacheEntry, type CacheOptions, type CachedEventHandlerOptions, type CachedFunction, type EventHandler, type HTTPEvent, type ResponseCacheEntry, type ServerRequest, type StorageInterface, cachedFunction, createMemoryStorage, defineCachedFunction, defineCachedHandler, expireCache, invalidateCache, resolveCacheKeys, setStorage, useStorage }; |
+41
-79
| import { hash } from "ohash"; | ||
| //#region src/storage.ts | ||
| /** Creates an in-memory storage backed by a `Map` with optional TTL support (in seconds). */ | ||
| function createMemoryStorage() { | ||
@@ -48,3 +46,2 @@ const map = /* @__PURE__ */ new Map(); | ||
| let _storage; | ||
| /** Returns the current storage instance. If none has been set via `setStorage`, lazily initializes an in-memory storage. */ | ||
| function useStorage() { | ||
@@ -54,8 +51,5 @@ if (!_storage) _storage = createMemoryStorage(); | ||
| } | ||
| /** Sets a custom storage implementation to be used by all cached functions. */ | ||
| function setStorage(storage) { | ||
| _storage = storage; | ||
| } | ||
| //#endregion | ||
| //#region src/cache.ts | ||
| function defaultCacheOptions$1() { | ||
@@ -69,9 +63,2 @@ return { | ||
| } | ||
| /** | ||
| * Wraps a function with caching support including TTL, SWR, integrity checks, and request deduplication. | ||
| * | ||
| * @param fn - The function to cache. | ||
| * @param opts - Cache configuration options. | ||
| * @returns A cached function with a `.resolveKey(...args)` method for cache key resolution. | ||
| */ | ||
| function defineCachedFunction(fn, opts = {}) { | ||
@@ -118,3 +105,3 @@ opts = { | ||
| const isFullyExpired = staleTtl !== void 0 && ttl > 0 && Date.now() - (entry.mtime || 0) > ttl + staleTtl; | ||
| const expired = shouldInvalidateCache || entry.integrity !== integrity || opts.maxAge === 0 || ttl > 0 && Date.now() - (entry.mtime || 0) > ttl || validate(entry) === false; | ||
| const expired = shouldInvalidateCache || entry.stale === true || entry.integrity !== integrity || opts.maxAge === 0 || ttl > 0 && Date.now() - (entry.mtime || 0) > ttl || validate(entry) === false; | ||
| if (isFullyExpired) { | ||
@@ -142,3 +129,6 @@ entry.value = void 0; | ||
| delete pending[key]; | ||
| _evictFromStorage(key, bases, group, name); | ||
| const evictPromise = _evictFromStorage(key, bases, group, name).catch((error) => { | ||
| _onError("[cache] Cache eviction error.", error); | ||
| }); | ||
| event?.req.waitUntil?.(evictPromise); | ||
| } | ||
@@ -150,2 +140,3 @@ throw error; | ||
| entry.integrity = integrity; | ||
| entry.stale = void 0; | ||
| delete pending[key]; | ||
@@ -168,4 +159,9 @@ if (validate(entry) !== false) { | ||
| })(); | ||
| if ((event?.req)?.waitUntil) event.req.waitUntil(promise); | ||
| } else _evictFromStorage(key, bases, group, name); | ||
| event?.req.waitUntil?.(promise); | ||
| } else { | ||
| const evictPromise = _evictFromStorage(key, bases, group, name).catch((error) => { | ||
| _onError("[cache] Cache eviction error.", error); | ||
| }); | ||
| event?.req.waitUntil?.(evictPromise); | ||
| } | ||
| } | ||
@@ -175,3 +171,3 @@ }; | ||
| if (entry.value === void 0) await _resolvePromise; | ||
| else if (expired && (event?.req)?.waitUntil) event.req.waitUntil(_resolvePromise); | ||
| else if (expired) event?.req.waitUntil?.(_resolvePromise); | ||
| if (opts.swr && validate(entry) !== false) { | ||
@@ -200,30 +196,9 @@ _resolvePromise.catch((error) => { | ||
| }); | ||
| cachedFn.expire = (...args) => expireCache({ | ||
| options: opts, | ||
| args | ||
| }); | ||
| return cachedFn; | ||
| } | ||
| /** Alias for {@link defineCachedFunction}. */ | ||
| const cachedFunction = defineCachedFunction; | ||
| /** | ||
| * Resolves all cache storage keys (one per base prefix) for given arguments and cache options. | ||
| * | ||
| * Uses the same key derivation as `defineCachedFunction` internally: | ||
| * - When `opts.getKey` is provided, it is called with `args` to produce the key segment. | ||
| * - Otherwise, `args` are hashed with `ohash` (same default as `defineCachedFunction`). | ||
| * | ||
| * Pass the same `getKey`, `name`, `group`, and `base` options you use in | ||
| * `defineCachedFunction` / `defineCachedHandler` to get the exact storage keys. | ||
| * | ||
| * @param input - Object with `options` (cache options) and optional `args` (function arguments). | ||
| * @returns An array of storage key strings (one per base prefix). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const keys = await resolveCacheKeys({ | ||
| * options: { name: "fetchUser", getKey: (id: string) => id }, | ||
| * args: ["user-123"], | ||
| * }); | ||
| * for (const key of keys) { | ||
| * await useStorage().set(key, null); // invalidate all tiers | ||
| * } | ||
| * ``` | ||
| */ | ||
| async function resolveCacheKeys(input = {}) { | ||
@@ -235,18 +210,2 @@ const opts = input.options ?? {}; | ||
| } | ||
| /** | ||
| * Invalidates (removes) cached entries for given arguments and cache options across all base prefixes. | ||
| * | ||
| * Uses the same key derivation as `defineCachedFunction` / `resolveCacheKeys`. | ||
| * | ||
| * @param input - Object with `options` (cache options) and optional `args` (function arguments). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Invalidate a specific cached entry | ||
| * await invalidateCache({ | ||
| * options: { name: "fetchUser", getKey: (id: string) => id }, | ||
| * args: ["user-123"], | ||
| * }); | ||
| * ``` | ||
| */ | ||
| async function invalidateCache(input = {}) { | ||
@@ -257,2 +216,15 @@ const keys = await resolveCacheKeys(input); | ||
| } | ||
| async function expireCache(input = {}) { | ||
| const opts = input.options ?? {}; | ||
| const keys = await resolveCacheKeys(input); | ||
| const storage = useStorage(); | ||
| await Promise.all(keys.map(async (key) => { | ||
| const entry = await storage.get(key); | ||
| if (!entry || typeof entry !== "object" || entry.value === void 0) return; | ||
| await storage.set(key, { | ||
| ...entry, | ||
| stale: true | ||
| }, _remainingTtl(entry, opts)); | ||
| })); | ||
| } | ||
| function isHTTPEvent(input) { | ||
@@ -276,9 +248,14 @@ return input?.req instanceof Request; | ||
| } | ||
| function _evictFromStorage(key, bases, group, name) { | ||
| for (const b of bases) useStorage().set(_buildCacheKey(key, { | ||
| async function _evictFromStorage(key, bases, group, name) { | ||
| await Promise.all(bases.map((b) => useStorage().set(_buildCacheKey(key, { | ||
| group, | ||
| name | ||
| }, b), null); | ||
| }, b), null))); | ||
| } | ||
| /** Strips storage-location fields from opts so integrity only reflects the cached computation. */ | ||
| function _remainingTtl(entry, opts) { | ||
| if (!entry.mtime || opts.maxAge == null || opts.maxAge <= 0) return; | ||
| const ttlWindow = opts.swr === false ? opts.maxAge : opts.staleMaxAge != null && opts.staleMaxAge >= 0 ? opts.maxAge + opts.staleMaxAge : void 0; | ||
| if (ttlWindow === void 0) return; | ||
| return { ttl: Math.max(Math.ceil((entry.mtime + ttlWindow * 1e3 - Date.now()) / 1e3), 1) }; | ||
| } | ||
| function _integrityOpts$1(opts) { | ||
@@ -288,4 +265,2 @@ const { base: _, group: _g, name: _n, ...rest } = opts; | ||
| } | ||
| //#endregion | ||
| //#region src/http.ts | ||
| function defaultCacheOptions() { | ||
@@ -299,13 +274,2 @@ return { | ||
| } | ||
| /** | ||
| * Wraps an HTTP event handler with response caching. | ||
| * | ||
| * Automatically generates cache keys from the URL path and variable headers, | ||
| * sets `cache-control`, `etag`, and `last-modified` headers, and handles | ||
| * `304 Not Modified` responses via conditional request headers. | ||
| * | ||
| * @param handler - The event handler to cache. | ||
| * @param opts - Cache and HTTP-specific configuration options. | ||
| * @returns A new event handler that serves cached responses when available. | ||
| */ | ||
| function defineCachedHandler(handler, opts = {}) { | ||
@@ -398,3 +362,2 @@ opts = { | ||
| } | ||
| /** Strips storage-location fields from opts so integrity only reflects the cached computation. */ | ||
| function _integrityOpts(opts) { | ||
@@ -413,3 +376,2 @@ const { base: _, group: _g, name: _n, ...rest } = opts; | ||
| } | ||
| //#endregion | ||
| export { cachedFunction, createMemoryStorage, defineCachedFunction, defineCachedHandler, invalidateCache, resolveCacheKeys, setStorage, useStorage }; | ||
| export { cachedFunction, createMemoryStorage, defineCachedFunction, defineCachedHandler, expireCache, invalidateCache, resolveCacheKeys, setStorage, useStorage }; |
+10
-10
| { | ||
| "name": "ocache", | ||
| "version": "0.1.4", | ||
| "version": "0.1.5", | ||
| "description": "Standalone caching utilities with TTL, SWR, and HTTP response caching", | ||
@@ -30,5 +30,5 @@ "license": "MIT", | ||
| "devDependencies": { | ||
| "@types/node": "^25.5.0", | ||
| "@typescript/native-preview": "^7.0.0-dev.20260319.1", | ||
| "@vitest/coverage-v8": "^4.1.0", | ||
| "@types/node": "^25.9.2", | ||
| "@typescript/native-preview": "7.0.0-dev.20260609.1", | ||
| "@vitest/coverage-v8": "^4.1.8", | ||
| "automd": "^0.4.3", | ||
@@ -38,9 +38,9 @@ "changelogen": "^0.6.2", | ||
| "mitata": "^1.0.34", | ||
| "obuild": "^0.4.32", | ||
| "oxfmt": "^0.41.0", | ||
| "oxlint": "^1.56.0", | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^4.1.0" | ||
| "obuild": "^0.4.36", | ||
| "oxfmt": "^0.54.0", | ||
| "oxlint": "^1.69.0", | ||
| "typescript": "^6.0.3", | ||
| "vitest": "^4.1.8" | ||
| }, | ||
| "packageManager": "pnpm@10.32.1" | ||
| "packageManager": "pnpm@11.5.2" | ||
| } |
+55
-0
@@ -129,2 +129,22 @@ # ocache | ||
| ### Cache Expiration (SWR refresh) | ||
| While `.invalidate()` removes an entry entirely (the next call must wait for a fresh value), `.expire()` only marks it as stale. With SWR enabled, stale values keep being served — still bounded by the originally configured `staleMaxAge` window — and the next access triggers a background refresh: | ||
| ```ts | ||
| // Mark the entry stale: next call serves the stale value and refetches in the background | ||
| await getUser.expire("user-123"); | ||
| ``` | ||
| The standalone `expireCache()` works like `invalidateCache()` — pass the same `maxAge` / `swr` / `staleMaxAge` options you cache with so the remaining storage TTL is preserved: | ||
| ```ts | ||
| import { expireCache } from "ocache"; | ||
| await expireCache({ | ||
| options: { name: "getUser", getKey: (id: string) => id, maxAge: 60, staleMaxAge: 300 }, | ||
| args: ["user-123"], | ||
| }); | ||
| ``` | ||
| ### Multi-tier Caching | ||
@@ -250,2 +270,37 @@ | ||
| ### `expireCache` | ||
| ```ts | ||
| async function expireCache<ArgsT extends unknown[] = any[]>( | ||
| input: | ||
| ``` | ||
| Expires cached entries for given arguments and cache options across all base prefixes, | ||
| without removing them. | ||
| Unlike [`invalidateCache`](#invalidatecache) (which removes entries entirely), expired entries keep | ||
| serving the stale value with SWR — still bounded by the originally configured | ||
| `staleMaxAge` window — while the next access triggers a background refresh. | ||
| Without SWR, the next call re-resolves before returning. | ||
| Uses the same key derivation as `defineCachedFunction` / `resolveCacheKeys`. | ||
| Pass the same `maxAge` / `swr` / `staleMaxAge` options you cache with so the | ||
| remaining storage TTL is preserved. | ||
| **Parameters:** | ||
| - **`input`** — Object with `options` (cache options) and optional `args` (function arguments). | ||
| **Example:** | ||
| ```ts | ||
| // Mark a cached entry for background refresh on next access | ||
| await expireCache({ | ||
| options: { name: "fetchUser", getKey: (id: string) => id, maxAge: 60, staleMaxAge: 300 }, | ||
| args: ["user-123"], | ||
| }); | ||
| ``` | ||
| --- | ||
| ### `invalidateCache` | ||
@@ -252,0 +307,0 @@ |
36242
5.08%405
15.71%361
-9.52%