@blacklizard/inertia-cache-redis 0.1.0

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) 2026 blacklizard
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,59 @@
1
+ # @blacklizard/inertia-cache-redis
2
+
3
+ [![CI](https://github.com/blacklizard/inertia-express/actions/workflows/ci.yml/badge.svg)](https://github.com/blacklizard/inertia-express/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@blacklizard/inertia-cache-redis.svg)](https://www.npmjs.com/package/@blacklizard/inertia-cache-redis)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![Node.js >= 24](https://img.shields.io/badge/node-%3E%3D24-brightgreen)](https://nodejs.org)
7
+
8
+ Redis-backed SSR view cache store for [@blacklizard/inertia-express](../inertia-express). Compatible with [`node-redis` v4+](https://github.com/redis/node-redis) and `ioredis`.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pnpm add @blacklizard/inertia-cache-redis redis
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import express from "express";
20
+ import { createClient } from "redis";
21
+ import { inertia, viteManifestVersion } from "@blacklizard/inertia-express";
22
+ import { createRedisCacheStore } from "@blacklizard/inertia-cache-redis";
23
+
24
+ const redis = createClient({ url: process.env.REDIS_URL });
25
+ await redis.connect();
26
+
27
+ const app = express();
28
+ app.use(
29
+ inertia({
30
+ version: viteManifestVersion({ manifestPath: "./dist/client/manifest.json" }),
31
+ ssr: yourSsrFetcher,
32
+ cache: {
33
+ store: createRedisCacheStore({
34
+ client: redis,
35
+ keyPrefix: "myapp:prod",
36
+ onError: (op, err) => console.warn(`redis cache ${op} failed`, err),
37
+ }),
38
+ ttlSeconds: 600,
39
+ },
40
+ }),
41
+ );
42
+ ```
43
+
44
+ ## ioredis
45
+
46
+ Pass `setMode: "ioredis"`:
47
+
48
+ ```ts
49
+ import Redis from "ioredis";
50
+ import { createRedisCacheStore } from "@blacklizard/inertia-cache-redis";
51
+
52
+ const client = new Redis(process.env.REDIS_URL);
53
+
54
+ createRedisCacheStore({ client, setMode: "ioredis" });
55
+ ```
56
+
57
+ ## Cache invalidation on deploy
58
+
59
+ Cache keys include the asset version. On deploy, a new version automatically retires every existing entry with no manual flush. Old keys expire via TTL.
@@ -0,0 +1,58 @@
1
+ import { SsrCacheStore } from "@blacklizard/inertia-express/core";
2
+
3
+ //#region src/index.d.ts
4
+ /**
5
+ * Structural type representing the subset of the `redis` (node-redis v4+)
6
+ * client API that we use. Defined here so this package does not need a
7
+ * runtime dependency on a specific node-redis version — pass any client
8
+ * with these methods, including `ioredis` (which has a compatible signature
9
+ * for `get`/`set`/`del`).
10
+ */
11
+ interface RedisClientLike {
12
+ get: (key: string) => Promise<string | null>;
13
+ /**
14
+ * node-redis v4+ accepts an options object: `set(key, value, { EX: n })`
15
+ * for TTL in seconds. We pass that shape; the call is type-erased here so
16
+ * either `redis@4+` or `ioredis` (which uses `set(key, value, "EX", n)`)
17
+ * can be adapted at construction time via `setMode`.
18
+ */
19
+ set: (key: string, value: string, options?: unknown) => Promise<unknown>;
20
+ del: (keys: string | string[]) => Promise<number>;
21
+ }
22
+ /**
23
+ * Construction options for {@link createRedisCacheStore}.
24
+ */
25
+ interface RedisCacheStoreOptions {
26
+ /** Redis client. node-redis v4+ recommended; ioredis works with `setMode: "ioredis"`. */
27
+ client: RedisClientLike;
28
+ /**
29
+ * Calling convention for `SET` with TTL.
30
+ * - `"node-redis"` (default): `client.set(key, value, { EX: ttl })`
31
+ * - `"ioredis"`: `client.set(key, value, "EX", ttl)`
32
+ */
33
+ setMode?: 'node-redis' | 'ioredis';
34
+ /**
35
+ * Optional key-prefix wrapper applied on top of whatever prefix the
36
+ * adapter computes. Use this for additional namespacing (per-environment,
37
+ * per-tenant, etc.).
38
+ */
39
+ keyPrefix?: string;
40
+ /**
41
+ * Called when a Redis op throws. Cache failures are non-fatal — the
42
+ * adapter's middleware swallows them — but logging here is recommended.
43
+ */
44
+ onError?: (op: 'get' | 'set' | 'delete', err: unknown) => void;
45
+ }
46
+ /**
47
+ * Build a {@link SsrCacheStore} backed by Redis.
48
+ *
49
+ * Entries are stored as JSON strings. A `JSON.parse` failure on read is
50
+ * treated as a cache miss (the poisoned key is reported via `onError` and
51
+ * best-effort deleted) so a corrupted entry can never break a page render.
52
+ *
53
+ * @param options Store configuration — see {@link RedisCacheStoreOptions}.
54
+ */
55
+ declare function createRedisCacheStore(options: RedisCacheStoreOptions): SsrCacheStore;
56
+ //#endregion
57
+ export { RedisCacheStoreOptions, RedisClientLike, createRedisCacheStore };
58
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;AASA;;;;;UAAiB,eAAA;EACf,GAAA,GAAM,GAAA,aAAgB,OAAA;EAQmB;;;;;;EADzC,GAAA,GAAM,GAAA,UAAa,KAAA,UAAe,OAAA,eAAsB,OAAA;EACxD,GAAA,GAAM,IAAA,wBAA4B,OAAA;AAAA;;;;UAMnB,sBAAA;EAN0B;EAQzC,MAAA,EAAQ,eAAe;EAFc;;;;;EAQrC,OAAA;EAMA;;;;;EAAA,SAAA;EAgDc;;;;EA3Cd,OAAA,IAAW,EAAA,4BAA8B,GAAA;AAAA;;;AA2C0C;;;;;;;iBAArE,qBAAA,CAAsB,OAAA,EAAS,sBAAA,GAAyB,aAAa"}
package/dist/index.js ADDED
@@ -0,0 +1,105 @@
1
+ //#region src/index.ts
2
+ /**
3
+ * Join an optional namespace prefix onto a cache key with a `:` separator.
4
+ *
5
+ * @param prefix Optional namespace; when undefined the key is returned unchanged.
6
+ * @param key Cache key to prefix.
7
+ */
8
+ function withPrefix(prefix, key) {
9
+ return prefix ? `${prefix}:${key}` : key;
10
+ }
11
+ /**
12
+ * Delete a key, swallowing any failure after reporting it via `onError`.
13
+ * Used to evict a poisoned (unparseable) cache entry without letting the
14
+ * cleanup error mask the original read.
15
+ *
16
+ * @param client Redis client.
17
+ * @param key Fully-prefixed key to remove.
18
+ * @param onError Optional error reporter.
19
+ */
20
+ async function bestEffortDelete(client, key, onError) {
21
+ try {
22
+ await client.del(key);
23
+ } catch (delErr) {
24
+ onError?.("delete", delErr);
25
+ }
26
+ }
27
+ /**
28
+ * Build a {@link SsrCacheStore} backed by Redis.
29
+ *
30
+ * Entries are stored as JSON strings. A `JSON.parse` failure on read is
31
+ * treated as a cache miss (the poisoned key is reported via `onError` and
32
+ * best-effort deleted) so a corrupted entry can never break a page render.
33
+ *
34
+ * @param options Store configuration — see {@link RedisCacheStoreOptions}.
35
+ */
36
+ function createRedisCacheStore(options) {
37
+ const setMode = options.setMode ?? "node-redis";
38
+ const prefix = options.keyPrefix;
39
+ return {
40
+ /**
41
+ * Read a cache entry. Returns `null` on miss or on poisoned (unparseable)
42
+ * data; rethrows when the Redis client itself errors after reporting via
43
+ * `onError`.
44
+ *
45
+ * @param key Cache key, before prefixing.
46
+ */
47
+ async get(key) {
48
+ const fullKey = withPrefix(prefix, key);
49
+ try {
50
+ const raw = await options.client.get(fullKey);
51
+ if (raw === null) return null;
52
+ try {
53
+ return JSON.parse(raw);
54
+ } catch (parseErr) {
55
+ options.onError?.("get", parseErr);
56
+ await bestEffortDelete(options.client, fullKey, options.onError);
57
+ return null;
58
+ }
59
+ } catch (err) {
60
+ options.onError?.("get", err);
61
+ throw err;
62
+ }
63
+ },
64
+ /**
65
+ * Write a cache entry with a TTL. Reports via `onError` and rethrows on
66
+ * Redis failure (the calling middleware swallows the throw — writes are
67
+ * fire-and-forget).
68
+ *
69
+ * @param key Cache key, before prefixing.
70
+ * @param value Entry to JSON-serialize and store.
71
+ * @param ttlSeconds Time-to-live in seconds.
72
+ */
73
+ async set(key, value, ttlSeconds) {
74
+ const fullKey = withPrefix(prefix, key);
75
+ const payload = JSON.stringify(value);
76
+ try {
77
+ if (setMode === "ioredis") {
78
+ const setFn = options.client.set;
79
+ await setFn(fullKey, payload, "EX", ttlSeconds);
80
+ } else await options.client.set(fullKey, payload, { EX: ttlSeconds });
81
+ } catch (err) {
82
+ options.onError?.("set", err);
83
+ throw err;
84
+ }
85
+ },
86
+ /**
87
+ * Remove a cache entry. Reports via `onError` and rethrows on failure.
88
+ *
89
+ * @param key Cache key, before prefixing.
90
+ */
91
+ async delete(key) {
92
+ const fullKey = withPrefix(prefix, key);
93
+ try {
94
+ await options.client.del(fullKey);
95
+ } catch (err) {
96
+ options.onError?.("delete", err);
97
+ throw err;
98
+ }
99
+ }
100
+ };
101
+ }
102
+ //#endregion
103
+ export { createRedisCacheStore };
104
+
105
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { SsrCacheEntry, SsrCacheStore } from '@blacklizard/inertia-express/core';\n\n/**\n * Structural type representing the subset of the `redis` (node-redis v4+)\n * client API that we use. Defined here so this package does not need a\n * runtime dependency on a specific node-redis version — pass any client\n * with these methods, including `ioredis` (which has a compatible signature\n * for `get`/`set`/`del`).\n */\nexport interface RedisClientLike {\n get: (key: string) => Promise<string | null>;\n /**\n * node-redis v4+ accepts an options object: `set(key, value, { EX: n })`\n * for TTL in seconds. We pass that shape; the call is type-erased here so\n * either `redis@4+` or `ioredis` (which uses `set(key, value, \"EX\", n)`)\n * can be adapted at construction time via `setMode`.\n */\n set: (key: string, value: string, options?: unknown) => Promise<unknown>;\n del: (keys: string | string[]) => Promise<number>;\n}\n\n/**\n * Construction options for {@link createRedisCacheStore}.\n */\nexport interface RedisCacheStoreOptions {\n /** Redis client. node-redis v4+ recommended; ioredis works with `setMode: \"ioredis\"`. */\n client: RedisClientLike;\n /**\n * Calling convention for `SET` with TTL.\n * - `\"node-redis\"` (default): `client.set(key, value, { EX: ttl })`\n * - `\"ioredis\"`: `client.set(key, value, \"EX\", ttl)`\n */\n setMode?: 'node-redis' | 'ioredis';\n /**\n * Optional key-prefix wrapper applied on top of whatever prefix the\n * adapter computes. Use this for additional namespacing (per-environment,\n * per-tenant, etc.).\n */\n keyPrefix?: string;\n /**\n * Called when a Redis op throws. Cache failures are non-fatal — the\n * adapter's middleware swallows them — but logging here is recommended.\n */\n onError?: (op: 'get' | 'set' | 'delete', err: unknown) => void;\n}\n\n/**\n * Join an optional namespace prefix onto a cache key with a `:` separator.\n *\n * @param prefix Optional namespace; when undefined the key is returned unchanged.\n * @param key Cache key to prefix.\n */\nfunction withPrefix(prefix: string | undefined, key: string): string {\n return prefix ? `${prefix}:${key}` : key;\n}\n\n/**\n * Delete a key, swallowing any failure after reporting it via `onError`.\n * Used to evict a poisoned (unparseable) cache entry without letting the\n * cleanup error mask the original read.\n *\n * @param client Redis client.\n * @param key Fully-prefixed key to remove.\n * @param onError Optional error reporter.\n */\nasync function bestEffortDelete(\n client: RedisClientLike,\n key: string,\n onError: RedisCacheStoreOptions['onError'],\n): Promise<void> {\n try {\n await client.del(key);\n } catch (delErr) {\n onError?.('delete', delErr);\n }\n}\n\n/**\n * Build a {@link SsrCacheStore} backed by Redis.\n *\n * Entries are stored as JSON strings. A `JSON.parse` failure on read is\n * treated as a cache miss (the poisoned key is reported via `onError` and\n * best-effort deleted) so a corrupted entry can never break a page render.\n *\n * @param options Store configuration — see {@link RedisCacheStoreOptions}.\n */\nexport function createRedisCacheStore(options: RedisCacheStoreOptions): SsrCacheStore {\n const setMode = options.setMode ?? 'node-redis';\n const prefix = options.keyPrefix;\n\n return {\n /**\n * Read a cache entry. Returns `null` on miss or on poisoned (unparseable)\n * data; rethrows when the Redis client itself errors after reporting via\n * `onError`.\n *\n * @param key Cache key, before prefixing.\n */\n async get(key: string): Promise<SsrCacheEntry | null> {\n const fullKey = withPrefix(prefix, key);\n\n try {\n const raw = await options.client.get(fullKey);\n\n if (raw === null) {\n return null;\n }\n\n try {\n return JSON.parse(raw) as SsrCacheEntry;\n } catch (parseErr) {\n // Poisoned key — report it, best-effort delete, then treat as a miss.\n options.onError?.('get', parseErr);\n await bestEffortDelete(options.client, fullKey, options.onError);\n\n return null;\n }\n } catch (err) {\n options.onError?.('get', err);\n throw err;\n }\n },\n /**\n * Write a cache entry with a TTL. Reports via `onError` and rethrows on\n * Redis failure (the calling middleware swallows the throw — writes are\n * fire-and-forget).\n *\n * @param key Cache key, before prefixing.\n * @param value Entry to JSON-serialize and store.\n * @param ttlSeconds Time-to-live in seconds.\n */\n async set(key: string, value: SsrCacheEntry, ttlSeconds: number): Promise<void> {\n const fullKey = withPrefix(prefix, key);\n const payload = JSON.stringify(value);\n\n try {\n if (setMode === 'ioredis') {\n // ioredis: client.set(key, value, \"EX\", ttl)\n const setFn = options.client.set as unknown as (\n k: string,\n v: string,\n mode: string,\n ttl: number,\n ) => Promise<unknown>;\n await setFn(fullKey, payload, 'EX', ttlSeconds);\n } else {\n // node-redis v4+: client.set(key, value, { EX: ttl })\n await options.client.set(fullKey, payload, { EX: ttlSeconds });\n }\n } catch (err) {\n options.onError?.('set', err);\n throw err;\n }\n },\n /**\n * Remove a cache entry. Reports via `onError` and rethrows on failure.\n *\n * @param key Cache key, before prefixing.\n */\n async delete(key: string): Promise<void> {\n const fullKey = withPrefix(prefix, key);\n\n try {\n await options.client.del(fullKey);\n } catch (err) {\n options.onError?.('delete', err);\n throw err;\n }\n },\n };\n}\n"],"mappings":";;;;;;;AAoDA,SAAS,WAAW,QAA4B,KAAqB;CACnE,OAAO,SAAS,GAAG,OAAO,GAAG,QAAQ;AACvC;;;;;;;;;;AAWA,eAAe,iBACb,QACA,KACA,SACe;CACf,IAAI;EACF,MAAM,OAAO,IAAI,GAAG;CACtB,SAAS,QAAQ;EACf,UAAU,UAAU,MAAM;CAC5B;AACF;;;;;;;;;;AAWA,SAAgB,sBAAsB,SAAgD;CACpF,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,SAAS,QAAQ;CAEvB,OAAO;;;;;;;;EAQL,MAAM,IAAI,KAA4C;GACpD,MAAM,UAAU,WAAW,QAAQ,GAAG;GAEtC,IAAI;IACF,MAAM,MAAM,MAAM,QAAQ,OAAO,IAAI,OAAO;IAE5C,IAAI,QAAQ,MACV,OAAO;IAGT,IAAI;KACF,OAAO,KAAK,MAAM,GAAG;IACvB,SAAS,UAAU;KAEjB,QAAQ,UAAU,OAAO,QAAQ;KACjC,MAAM,iBAAiB,QAAQ,QAAQ,SAAS,QAAQ,OAAO;KAE/D,OAAO;IACT;GACF,SAAS,KAAK;IACZ,QAAQ,UAAU,OAAO,GAAG;IAC5B,MAAM;GACR;EACF;;;;;;;;;;EAUA,MAAM,IAAI,KAAa,OAAsB,YAAmC;GAC9E,MAAM,UAAU,WAAW,QAAQ,GAAG;GACtC,MAAM,UAAU,KAAK,UAAU,KAAK;GAEpC,IAAI;IACF,IAAI,YAAY,WAAW;KAEzB,MAAM,QAAQ,QAAQ,OAAO;KAM7B,MAAM,MAAM,SAAS,SAAS,MAAM,UAAU;IAChD,OAEE,MAAM,QAAQ,OAAO,IAAI,SAAS,SAAS,EAAE,IAAI,WAAW,CAAC;GAEjE,SAAS,KAAK;IACZ,QAAQ,UAAU,OAAO,GAAG;IAC5B,MAAM;GACR;EACF;;;;;;EAMA,MAAM,OAAO,KAA4B;GACvC,MAAM,UAAU,WAAW,QAAQ,GAAG;GAEtC,IAAI;IACF,MAAM,QAAQ,OAAO,IAAI,OAAO;GAClC,SAAS,KAAK;IACZ,QAAQ,UAAU,UAAU,GAAG;IAC/B,MAAM;GACR;EACF;CACF;AACF"}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@blacklizard/inertia-cache-redis",
3
+ "version": "0.1.0",
4
+ "description": "Redis-backed SSR view cache store for @blacklizard/inertia-express. Compatible with node-redis v4+.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "blacklizard",
8
+ "homepage": "https://github.com/blacklizard/inertia-express/tree/main/packages/inertia-cache-redis#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/blacklizard/inertia-express.git",
12
+ "directory": "packages/inertia-cache-redis"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/blacklizard/inertia-express/issues"
16
+ },
17
+ "engines": {
18
+ "node": ">=24"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ }
35
+ },
36
+ "peerDependencies": {
37
+ "redis": "^4.6.0 || ^5.0.0",
38
+ "@blacklizard/inertia-express": "0.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^25.9.1",
42
+ "tsdown": "^0.22.0",
43
+ "typescript": "^6.0.3",
44
+ "vitest": "^4.1.7",
45
+ "@blacklizard/inertia-express": "0.1.0"
46
+ },
47
+ "keywords": [
48
+ "inertia",
49
+ "inertia-js",
50
+ "inertiajs",
51
+ "express",
52
+ "adapter",
53
+ "ssr",
54
+ "spa",
55
+ "redis",
56
+ "cache",
57
+ "worker",
58
+ "load-balancer",
59
+ "self-healing"
60
+ ],
61
+ "scripts": {
62
+ "build": "tsdown",
63
+ "dev": "tsdown --watch",
64
+ "test": "vitest run",
65
+ "test:watch": "vitest",
66
+ "typecheck": "tsc --noEmit",
67
+ "clean": "rm -rf dist"
68
+ }
69
+ }