🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

ocache

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ocache - npm Package Compare versions

Comparing version
0.0.0
to
0.1.1
+131
dist/index.d.mts
//#region src/types.d.ts
/**
* Extended `Request` interface with optional `waitUntil` for background tasks.
*
* Compatible with srvx `ServerRequest`.
*/
interface ServerRequest extends Request {
waitUntil?: (promise: Promise<any>) => void;
}
/**
* Minimal HTTP event object containing a request and an optional pre-parsed URL.
*/
interface HTTPEvent {
req: ServerRequest;
/** Pre-parsed URL. Falls back to `new URL(req.url)` when not provided. */
url?: URL;
}
/**
* Handler function that receives an {@link HTTPEvent} and returns a response value.
*/
type EventHandler = (event: HTTPEvent) => unknown | Promise<unknown>;
/**
* Stored cache entry wrapping a cached value with metadata.
*/
interface CacheEntry<T = any> {
/** The cached value. */
value?: T;
/** Absolute timestamp (ms) when this entry expires. */
expires?: number;
/** Absolute timestamp (ms) when this entry was last resolved. */
mtime?: number;
/** Hash used to detect when the cached function or options have changed. */
integrity?: string;
}
/**
* Options for configuring cached functions created by `defineCachedFunction`.
*/
interface CacheOptions<T = any, ArgsT extends unknown[] = any[]> {
/** Name used as part of the cache key. Defaults to the function name or `"_"`. */
name?: string;
/** Custom cache key generator. Receives the same arguments as the cached function. */
getKey?: (...args: ArgsT) => string | Promise<string>;
/** Transform the cached entry before returning. Return value replaces the cached value. */
transform?: (entry: CacheEntry<T>, ...args: ArgsT) => any;
/** Validate a cache entry. Return `false` to treat the entry as invalid and re-resolve. */
validate?: (entry: CacheEntry<T>, ...args: ArgsT) => boolean;
/** When returns `true`, the cache is invalidated and the function is re-invoked. */
shouldInvalidateCache?: (...args: ArgsT) => boolean | Promise<boolean>;
/** When returns `true`, the cache is bypassed entirely and the function is called directly. */
shouldBypassCache?: (...args: ArgsT) => boolean | Promise<boolean>;
/** Cache key group prefix. Defaults to `"ocache/functions"`. */
group?: string;
/** Custom integrity value. Auto-generated from the function and options by default. */
integrity?: any;
/** Number of seconds to cache the response. Defaults to `1`. */
maxAge?: number;
/** Enable stale-while-revalidate behavior. When `true`, returns stale cache while refreshing in the background. Defaults to `true`. */
swr?: boolean;
/** Maximum number of seconds a stale entry can be served while revalidating. */
staleMaxAge?: number;
/** Base path prefix for cache keys. Defaults to `"/cache"`. */
base?: string;
/** Optional error handler called for all cache-related errors (read, write, SWR, malformed data). */
onError?: (error: unknown) => void;
}
/**
* Serialized HTTP response stored in the cache by `defineCachedHandler`.
*/
interface ResponseCacheEntry {
/** HTTP status code. */
status: number;
/** HTTP status text. */
statusText: string | undefined;
/** Response headers as a flat key-value record. */
headers: Record<string, string>;
/** Response body as a string. */
body: string | undefined;
}
/**
* Options for configuring cached HTTP handlers created by `defineCachedHandler`.
*
* Extends {@link CacheOptions} (without `transform` and `validate`, which are set internally).
*/
interface CachedEventHandlerOptions extends Omit<CacheOptions<ResponseCacheEntry, [HTTPEvent]>, "transform" | "validate"> {
/** When `true`, only handles conditional headers (304 responses) without full response caching. */
headersOnly?: boolean;
/** Request header names that should vary the cache key (e.g., `["accept-language"]`). */
varies?: string[] | readonly string[];
}
//#endregion
//#region src/cache.d.ts
/**
* 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 new async function that returns cached results when available.
*/
declare function defineCachedFunction<T, ArgsT extends unknown[] = any[]>(fn: (...args: ArgsT) => T | Promise<T>, opts?: CacheOptions<T, ArgsT>): (...args: ArgsT) => Promise<T>;
/** Alias for {@link defineCachedFunction}. */
declare const cachedFunction: typeof defineCachedFunction;
//#endregion
//#region src/http.d.ts
/**
* 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.
*/
declare function defineCachedHandler(handler: EventHandler, opts?: CachedEventHandlerOptions): EventHandler;
//#endregion
//#region src/storage.d.ts
interface StorageInterface {
get<T = unknown>(key: string): T | null | Promise<T | null>;
set<T = unknown>(key: string, value: T, opts?: {
ttl?: number;
}): void | Promise<void>;
}
/** Creates an in-memory storage backed by a `Map` with optional TTL support (in seconds). */
declare function createMemoryStorage(): StorageInterface;
/** Returns the current storage instance. If none has been set via `setStorage`, lazily initializes an in-memory storage. */
declare function useStorage(): StorageInterface;
/** Sets a custom storage implementation to be used by all cached functions. */
declare function setStorage(storage: StorageInterface): void;
//#endregion
export { type CacheEntry, type CacheOptions, type CachedEventHandlerOptions, type EventHandler, type HTTPEvent, type ResponseCacheEntry, type ServerRequest, type StorageInterface, cachedFunction, createMemoryStorage, defineCachedFunction, defineCachedHandler, setStorage, useStorage };
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() {
const map = /* @__PURE__ */ new Map();
return {
get(key) {
const entry = map.get(key);
if (!entry) return null;
if (entry.expires && Date.now() > entry.expires) {
map.delete(key);
return null;
}
return entry.value;
},
set(key, value, opts) {
map.set(key, {
value,
expires: opts?.ttl ? Date.now() + opts.ttl * 1e3 : void 0
});
}
};
}
let _storage;
/** Returns the current storage instance. If none has been set via `setStorage`, lazily initializes an in-memory storage. */
function useStorage() {
if (!_storage) _storage = createMemoryStorage();
return _storage;
}
/** 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() {
return {
name: "_",
base: "/cache",
swr: true,
maxAge: 1
};
}
/**
* 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 new async function that returns cached results when available.
*/
function defineCachedFunction(fn, opts = {}) {
opts = {
...defaultCacheOptions$1(),
...opts
};
const pending = {};
const group = opts.group || "ocache/functions";
const name = opts.name || fn.name || "_";
const integrity = opts.integrity || hash([fn, opts]);
const validate = opts.validate || ((entry) => entry.value !== void 0);
const _onError = (context, error) => {
if (opts.onError) opts.onError(error);
else console.error(context, error);
};
async function get(key, resolver, shouldInvalidateCache, event) {
const cacheKey = [
opts.base,
group,
name,
key + ".json"
].filter(Boolean).join(":").replace(/:\/$/, ":index");
let entry = await Promise.resolve(useStorage().get(cacheKey)).catch((error) => {
_onError("[cache] Cache read error.", error);
}) || {};
if (typeof entry !== "object") {
entry = {};
_onError("[cache]", /* @__PURE__ */ new Error("Malformed data read from cache."));
}
const ttl = (opts.maxAge ?? 0) * 1e3;
if (ttl) entry.expires = Date.now() + ttl;
const expired = shouldInvalidateCache || entry.integrity !== integrity || ttl && Date.now() - (entry.mtime || 0) > ttl || validate(entry) === false;
const _resolve = async () => {
const isPending = pending[key];
if (!isPending) {
if (entry.value !== void 0 && (opts.staleMaxAge || 0) >= 0 && opts.swr === false) {
entry.value = void 0;
entry.integrity = void 0;
entry.mtime = void 0;
entry.expires = void 0;
}
pending[key] = Promise.resolve(resolver());
}
try {
entry.value = await pending[key];
} catch (error) {
if (!isPending) delete pending[key];
throw error;
}
if (!isPending) {
entry.mtime = Date.now();
entry.integrity = integrity;
delete pending[key];
if (validate(entry) !== false) {
let setOpts;
if (opts.maxAge && !opts.swr) setOpts = { ttl: opts.maxAge };
const promise = Promise.resolve(useStorage().set(cacheKey, entry, setOpts)).catch((error) => {
_onError("[cache] Cache write error.", error);
});
if ((event?.req)?.waitUntil) event.req.waitUntil(promise);
}
}
};
const _resolvePromise = expired ? _resolve() : Promise.resolve();
if (entry.value === void 0) await _resolvePromise;
else if (expired && (event?.req)?.waitUntil) event.req.waitUntil(_resolvePromise);
if (opts.swr && validate(entry) !== false) {
_resolvePromise.catch((error) => {
_onError("[cache] SWR handler error.", error);
});
return entry;
}
return _resolvePromise.then(() => entry);
}
return async (...args) => {
if (await opts.shouldBypassCache?.(...args)) return fn(...args);
const entry = await get(await (opts.getKey || getKey)(...args), () => fn(...args), await opts.shouldInvalidateCache?.(...args), isHTTPEvent(args[0]) ? args[0] : void 0);
let value = entry.value;
if (opts.transform) value = await opts.transform(entry, ...args) || value;
return value;
};
}
/** Alias for {@link defineCachedFunction}. */
const cachedFunction = defineCachedFunction;
function isHTTPEvent(input) {
return input?.req instanceof Request;
}
function getKey(...args) {
return args.length > 0 ? hash(args) : "";
}
//#endregion
//#region src/http.ts
function defaultCacheOptions() {
return {
name: "_",
base: "/cache",
swr: true,
maxAge: 1
};
}
/**
* 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 = defaultCacheOptions()) {
const variableHeaderNames = (opts.varies || []).filter(Boolean).map((h) => h.toLowerCase()).sort();
const _cachedHandler = cachedFunction(async (event) => {
const filteredHeaders = [...event.req.headers.entries()].filter(([key]) => !variableHeaderNames.includes(key.toLowerCase()));
try {
const originalReq = event.req;
event.req = new Request(event.req.url, {
method: event.req.method,
headers: filteredHeaders
});
if (originalReq.runtime) event.req.runtime = originalReq.runtime;
} catch (error) {
console.error("[cache] Failed to filter headers:", error);
}
const rawValue = await handler(event);
const res = rawValue instanceof Response ? rawValue : new Response(String(rawValue));
const body = await res.text();
if (!res.headers.has("etag")) res.headers.set("etag", `W/"${hash(body)}"`);
if (!res.headers.has("last-modified")) res.headers.set("last-modified", (/* @__PURE__ */ new Date()).toUTCString());
const cacheControl = [];
if (opts.swr) {
if (opts.maxAge) cacheControl.push(`s-maxage=${opts.maxAge}`);
if (opts.staleMaxAge) cacheControl.push(`stale-while-revalidate=${opts.staleMaxAge}`);
else cacheControl.push("stale-while-revalidate");
} else if (opts.maxAge) cacheControl.push(`max-age=${opts.maxAge}`);
if (cacheControl.length > 0) res.headers.set("cache-control", cacheControl.join(", "));
return {
status: res.status,
statusText: res.statusText,
headers: Object.fromEntries(res.headers.entries()),
body
};
}, {
...opts,
shouldBypassCache: (event) => {
return event.req.method !== "GET" && event.req.method !== "HEAD";
},
getKey: async (event) => {
const customKey = await opts.getKey?.(event);
if (customKey) return escapeKey(customKey);
const _url = event.url ?? new URL(event.req.url);
const _path = _url.pathname + _url.search;
let _pathname;
try {
_pathname = escapeKey(decodeURI(new URL(_path, "http://localhost").pathname)).slice(0, 16) || "index";
} catch {
_pathname = "-";
}
return [`${_pathname}.${hash(_path)}`, ...variableHeaderNames.map((header) => [header, event.req.headers.get(header)]).map(([name, value]) => `${escapeKey(name)}.${hash(value)}`)].join(":");
},
validate: (entry) => {
if (!entry.value) return false;
if (entry.value.status >= 400) return false;
if (entry.value.body === void 0) return false;
if (entry.value.headers.etag === "undefined" || entry.value.headers["last-modified"] === "undefined") return false;
return true;
},
group: opts.group || "cache/handlers",
integrity: opts.integrity || hash([handler, opts])
});
return async (event) => {
if (opts.headersOnly) {
if (handleCacheHeaders(event, { maxAge: opts.maxAge })) return new Response(null, { status: 304 });
return handler(event);
}
const response = await _cachedHandler(event);
if (handleCacheHeaders(event, {
modifiedTime: new Date(response.headers["last-modified"]),
etag: response.headers.etag,
maxAge: opts.maxAge
})) return new Response(null, { status: 304 });
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
};
}
function escapeKey(key) {
return String(key).replace(/\W/g, "");
}
function handleCacheHeaders(event, opts) {
const ifNoneMatch = event.req.headers.get("if-none-match");
if (ifNoneMatch && opts.etag && ifNoneMatch === opts.etag) return true;
const ifModifiedSince = event.req.headers.get("if-modified-since");
if (ifModifiedSince && opts.modifiedTime) {
if (new Date(ifModifiedSince) >= opts.modifiedTime) return true;
}
return false;
}
//#endregion
export { cachedFunction, createMemoryStorage, defineCachedFunction, defineCachedHandler, setStorage, useStorage };
MIT License
Copyright (c) Pooya Parsa <pooya@pi0.io>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# ocache
<!-- automd:badges color=yellow -->
[![npm version](https://img.shields.io/npm/v/ocache?color=yellow)](https://npmjs.com/package/ocache)
[![npm downloads](https://img.shields.io/npm/dm/ocache?color=yellow)](https://npm.chart.dev/ocache)
<!-- /automd -->
## Usage
### Caching Functions
Wrap any function with `defineCachedFunction` to add caching with TTL, stale-while-revalidate, and request deduplication:
```ts
import { defineCachedFunction } from "ocache";
const cachedFetch = defineCachedFunction(
async (url: string) => {
const res = await fetch(url);
return res.json();
},
{
maxAge: 60, // Cache for 60 seconds
name: "api-fetch",
},
);
// First call hits the function, subsequent calls return cached result
const data = await cachedFetch("https://api.example.com/data");
```
#### Options
```ts
const cached = defineCachedFunction(fn, {
name: "my-fn", // Cache key name (defaults to function name)
maxAge: 10, // TTL in seconds (default: 1)
swr: true, // Stale-while-revalidate (default: true)
staleMaxAge: 60, // Max seconds to serve stale content
group: "my-group", // Cache key group (default: "ocache/functions")
getKey: (...args) => "custom-key", // Custom cache key generator
shouldBypassCache: (...args) => false, // Skip cache entirely when true
shouldInvalidateCache: (...args) => false, // Force refresh when true
validate: (entry) => entry.value !== undefined, // Custom validation
transform: (entry) => entry.value, // Transform before returning
onError: (error) => console.error(error), // Error handler
});
```
### Caching HTTP Handlers
Wrap HTTP handlers with `defineCachedHandler` for automatic response caching with `etag`, `last-modified`, and `304 Not Modified` support:
```ts
import { defineCachedHandler } from "ocache";
const handler = defineCachedHandler(
async (event) => {
// event.req is a standard Request object
const url = event.url ?? new URL(event.req.url);
const data = await getExpensiveData(url.pathname);
return new Response(JSON.stringify(data), {
headers: { "content-type": "application/json" },
});
},
{
maxAge: 300, // Cache for 5 minutes
swr: true,
staleMaxAge: 600,
varies: ["accept-language"], // Vary cache by these headers
},
);
// Use with any server that provides Request/Response
// e.g., Bun, Deno, Cloudflare Workers, srvx, etc.
```
#### Headers-only Mode
Use `headersOnly` to handle conditional requests without caching the full response:
```ts
const handler = defineCachedHandler(myHandler, {
headersOnly: true,
maxAge: 60,
});
```
### Custom Storage
By default, ocache uses an in-memory `Map`-based storage. You can provide a custom storage implementation:
```ts
import { setStorage } from "ocache";
import type { StorageInterface } from "ocache";
const redisStorage: StorageInterface = {
get: async (key) => {
return JSON.parse(await redis.get(key));
},
set: async (key, value, opts) => {
await redis.set(key, JSON.stringify(value), opts?.ttl ? { EX: opts.ttl } : undefined);
},
};
setStorage(redisStorage);
```
## API
<!-- automd:docs4ts -->
### `defineCachedFunction`
```ts
function defineCachedFunction<T, ArgsT extends unknown[] = any[]>(
fn: (...args: ArgsT) => T | Promise<T>,
opts: CacheOptions<T, ArgsT> =
```
Wraps a function with caching support including TTL, SWR, integrity checks, and request deduplication.
**Parameters:**
- **`fn`** — The function to cache.
- **`opts`** — Cache configuration options.
**Returns:** — A new async function that returns cached results when available.
---
### `cachedFunction`
```ts
const cachedFunction = defineCachedFunction;
```
Alias for [`defineCachedFunction`](#definecachedfunction).
---
### `defineCachedHandler`
```ts
function defineCachedHandler(
handler: EventHandler,
opts: CachedEventHandlerOptions = defaultCacheOptions(),
): EventHandler;
```
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.
**Parameters:**
- **`handler`** — The event handler to cache.
- **`opts`** — Cache and HTTP-specific configuration options.
**Returns:** — A new event handler that serves cached responses when available.
---
### `createMemoryStorage`
```ts
function createMemoryStorage(): StorageInterface;
```
Creates an in-memory storage backed by a `Map` with optional TTL support (in seconds).
---
### `useStorage`
```ts
function useStorage(): StorageInterface;
```
Returns the current storage instance. If none has been set via `setStorage`, lazily initializes an in-memory storage.
---
### `setStorage`
```ts
function setStorage(storage: StorageInterface): void;
```
Sets a custom storage implementation to be used by all cached functions.
---
### `ServerRequest`
```ts
interface ServerRequest extends Request
```
Extended `Request` interface with optional `waitUntil` for background tasks.
Compatible with srvx `ServerRequest`.
---
### `HTTPEvent`
```ts
interface HTTPEvent
```
Minimal HTTP event object containing a request and an optional pre-parsed URL.
---
### `EventHandler`
```ts
type EventHandler = (event: HTTPEvent) => unknown | Promise<unknown>;
```
Handler function that receives an [`HTTPEvent`](#httpevent) and returns a response value.
---
### `CacheEntry`
```ts
interface CacheEntry<T = any>
```
Stored cache entry wrapping a cached value with metadata.
---
### `CacheOptions`
```ts
interface CacheOptions<T = any, ArgsT extends unknown[] = any[]>
```
Options for configuring cached functions created by `defineCachedFunction`.
---
### `ResponseCacheEntry`
```ts
interface ResponseCacheEntry
```
Serialized HTTP response stored in the cache by `defineCachedHandler`.
---
### `CachedEventHandlerOptions`
```ts
interface CachedEventHandlerOptions extends Omit<
CacheOptions<ResponseCacheEntry, [HTTPEvent]>,
"transform" | "validate"
>
```
Options for configuring cached HTTP handlers created by `defineCachedHandler`.
Extends [`CacheOptions`](#cacheoptions) (without `transform` and `validate`, which are set internally).
<!-- /automd-->
## Development
<details>
<summary>local development</summary>
- Clone this repository
- Install latest LTS version of [Node.js](https://nodejs.org/en/)
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
- Install dependencies using `pnpm install`
- Run interactive tests using `pnpm dev`
</details>
## License
Published under the [MIT](https://github.com/unjs/ocache/blob/main/LICENSE) license 💛.
+42
-4
{
"name": "ocache",
"version": "0.0.0",
"license": "MIT"
}
"name": "ocache",
"version": "0.1.1",
"description": "Standalone caching utilities with TTL, SWR, and HTTP response caching",
"license": "MIT",
"repository": "unjs/ocache",
"files": [
"dist"
],
"type": "module",
"sideEffects": false,
"types": "./dist/index.d.mts",
"exports": {
".": "./dist/index.mjs"
},
"scripts": {
"build": "obuild",
"dev": "vitest dev",
"fmt": "automd && oxlint . --fix && oxfmt .",
"lint": "oxlint . && oxfmt --check .",
"prepack": "pnpm build",
"release": "pnpm test && pnpm build && changelogen --release && npm publish && git push --follow-tags",
"test": "pnpm lint && pnpm typecheck && vitest run --coverage",
"typecheck": "tsgo --noEmit --skipLibCheck"
},
"dependencies": {
"ohash": "^2.0.11"
},
"devDependencies": {
"@types/node": "latest",
"@typescript/native-preview": "latest",
"@vitest/coverage-v8": "latest",
"automd": "latest",
"changelogen": "latest",
"docs4ts": "^0.0.3",
"obuild": "latest",
"oxfmt": "latest",
"oxlint": "latest",
"typescript": "latest",
"vitest": "latest"
},
"packageManager": "pnpm@10.29.3"
}