@revealui/cache 0.1.1 → 0.1.3
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/dist/adapters/index.d.ts +72 -0
- package/dist/adapters/index.js +195 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/index.d.ts +83 -1
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -1
- package/dist/types-CmU1eRbl.d.ts +34 -0
- package/package.json +13 -3
- package/LICENSE.commercial +0 -111
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { C as CacheStore } from '../types-CmU1eRbl.js';
|
|
2
|
+
export { a as CacheEntry } from '../types-CmU1eRbl.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* In-Memory Cache Store
|
|
6
|
+
*
|
|
7
|
+
* Map-backed cache store. Fast, zero-dependency, single-instance only.
|
|
8
|
+
* Use for development, testing, or when distributed state isn't needed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
declare class InMemoryCacheStore implements CacheStore {
|
|
12
|
+
private store;
|
|
13
|
+
private maxEntries;
|
|
14
|
+
constructor(options?: {
|
|
15
|
+
maxEntries?: number;
|
|
16
|
+
});
|
|
17
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
18
|
+
set<T = unknown>(key: string, value: T, ttlSeconds: number, tags?: string[]): Promise<void>;
|
|
19
|
+
delete(...keys: string[]): Promise<number>;
|
|
20
|
+
deleteByPrefix(prefix: string): Promise<number>;
|
|
21
|
+
deleteByTags(tags: string[]): Promise<number>;
|
|
22
|
+
clear(): Promise<void>;
|
|
23
|
+
size(): Promise<number>;
|
|
24
|
+
prune(): Promise<number>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* PGlite Cache Store
|
|
30
|
+
*
|
|
31
|
+
* PostgreSQL-compatible cache store backed by PGlite (in-memory or file-based).
|
|
32
|
+
* Provides the same CacheStore interface as InMemoryCacheStore but uses SQL
|
|
33
|
+
* for persistence and querying — enabling distributed invalidation via
|
|
34
|
+
* ElectricSQL shape subscriptions in Phase 5.10C.
|
|
35
|
+
*
|
|
36
|
+
* Table schema is auto-created on first use (no external migrations needed).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** Minimal PGlite interface — avoids importing the full @electric-sql/pglite package. */
|
|
40
|
+
interface PGliteInstance {
|
|
41
|
+
exec(query: string): Promise<unknown>;
|
|
42
|
+
query<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<{
|
|
43
|
+
rows: T[];
|
|
44
|
+
}>;
|
|
45
|
+
close(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
interface PGliteCacheStoreOptions {
|
|
48
|
+
/** PGlite instance (caller owns lifecycle unless closeOnDestroy is true). */
|
|
49
|
+
db: PGliteInstance;
|
|
50
|
+
/** Table name prefix to avoid collisions (default: none). */
|
|
51
|
+
tablePrefix?: string;
|
|
52
|
+
/** Close the PGlite instance when close() is called (default: false). */
|
|
53
|
+
closeOnDestroy?: boolean;
|
|
54
|
+
}
|
|
55
|
+
declare class PGliteCacheStore implements CacheStore {
|
|
56
|
+
private db;
|
|
57
|
+
private ready;
|
|
58
|
+
private closeOnDestroy;
|
|
59
|
+
constructor(options: PGliteCacheStoreOptions);
|
|
60
|
+
private init;
|
|
61
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
62
|
+
set<T = unknown>(key: string, value: T, ttlSeconds: number, tags?: string[]): Promise<void>;
|
|
63
|
+
delete(...keys: string[]): Promise<number>;
|
|
64
|
+
deleteByPrefix(prefix: string): Promise<number>;
|
|
65
|
+
deleteByTags(tags: string[]): Promise<number>;
|
|
66
|
+
clear(): Promise<void>;
|
|
67
|
+
size(): Promise<number>;
|
|
68
|
+
prune(): Promise<number>;
|
|
69
|
+
close(): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { CacheStore, InMemoryCacheStore, PGliteCacheStore };
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/adapters/memory.ts
|
|
2
|
+
var InMemoryCacheStore = class {
|
|
3
|
+
store = /* @__PURE__ */ new Map();
|
|
4
|
+
maxEntries;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.maxEntries = options?.maxEntries ?? 1e4;
|
|
7
|
+
}
|
|
8
|
+
async get(key) {
|
|
9
|
+
const entry = this.store.get(key);
|
|
10
|
+
if (!entry) return null;
|
|
11
|
+
if (Date.now() > entry.expiresAt) {
|
|
12
|
+
this.store.delete(key);
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return JSON.parse(entry.value);
|
|
16
|
+
}
|
|
17
|
+
async set(key, value, ttlSeconds, tags) {
|
|
18
|
+
if (this.store.size >= this.maxEntries && !this.store.has(key)) {
|
|
19
|
+
const firstKey = this.store.keys().next().value;
|
|
20
|
+
if (firstKey !== void 0) {
|
|
21
|
+
this.store.delete(firstKey);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
this.store.set(key, {
|
|
25
|
+
value: JSON.stringify(value),
|
|
26
|
+
expiresAt: Date.now() + ttlSeconds * 1e3,
|
|
27
|
+
tags: tags ?? []
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async delete(...keys) {
|
|
31
|
+
let count = 0;
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
if (this.store.delete(key)) count++;
|
|
34
|
+
}
|
|
35
|
+
return count;
|
|
36
|
+
}
|
|
37
|
+
async deleteByPrefix(prefix) {
|
|
38
|
+
let count = 0;
|
|
39
|
+
for (const key of this.store.keys()) {
|
|
40
|
+
if (key.startsWith(prefix)) {
|
|
41
|
+
this.store.delete(key);
|
|
42
|
+
count++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
47
|
+
async deleteByTags(tags) {
|
|
48
|
+
const tagSet = new Set(tags);
|
|
49
|
+
let count = 0;
|
|
50
|
+
for (const [key, entry] of this.store.entries()) {
|
|
51
|
+
if (entry.tags.some((t) => tagSet.has(t))) {
|
|
52
|
+
this.store.delete(key);
|
|
53
|
+
count++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return count;
|
|
57
|
+
}
|
|
58
|
+
async clear() {
|
|
59
|
+
this.store.clear();
|
|
60
|
+
}
|
|
61
|
+
async size() {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
let count = 0;
|
|
64
|
+
for (const entry of this.store.values()) {
|
|
65
|
+
if (entry.expiresAt > now) count++;
|
|
66
|
+
}
|
|
67
|
+
return count;
|
|
68
|
+
}
|
|
69
|
+
async prune() {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
let pruned = 0;
|
|
72
|
+
for (const [key, entry] of this.store.entries()) {
|
|
73
|
+
if (entry.expiresAt <= now) {
|
|
74
|
+
this.store.delete(key);
|
|
75
|
+
pruned++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return pruned;
|
|
79
|
+
}
|
|
80
|
+
async close() {
|
|
81
|
+
this.store.clear();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/adapters/pglite.ts
|
|
86
|
+
var CREATE_TABLE_SQL = `
|
|
87
|
+
CREATE TABLE IF NOT EXISTS _cache_entries (
|
|
88
|
+
key TEXT PRIMARY KEY,
|
|
89
|
+
value TEXT NOT NULL,
|
|
90
|
+
expires_at BIGINT NOT NULL,
|
|
91
|
+
tags TEXT[] NOT NULL DEFAULT '{}'
|
|
92
|
+
);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS _cache_entries_expires_idx ON _cache_entries (expires_at);
|
|
94
|
+
`;
|
|
95
|
+
var PGliteCacheStore = class {
|
|
96
|
+
db;
|
|
97
|
+
ready;
|
|
98
|
+
closeOnDestroy;
|
|
99
|
+
constructor(options) {
|
|
100
|
+
this.db = options.db;
|
|
101
|
+
this.closeOnDestroy = options.closeOnDestroy ?? false;
|
|
102
|
+
this.ready = this.init();
|
|
103
|
+
}
|
|
104
|
+
async init() {
|
|
105
|
+
await this.db.exec(CREATE_TABLE_SQL);
|
|
106
|
+
}
|
|
107
|
+
async get(key) {
|
|
108
|
+
await this.ready;
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
const result = await this.db.query(
|
|
111
|
+
"SELECT value FROM _cache_entries WHERE key = $1 AND expires_at > $2",
|
|
112
|
+
[key, now]
|
|
113
|
+
);
|
|
114
|
+
const row = result.rows[0];
|
|
115
|
+
if (!row) return null;
|
|
116
|
+
return JSON.parse(row.value);
|
|
117
|
+
}
|
|
118
|
+
async set(key, value, ttlSeconds, tags) {
|
|
119
|
+
await this.ready;
|
|
120
|
+
const expiresAt = Date.now() + ttlSeconds * 1e3;
|
|
121
|
+
const serialized = JSON.stringify(value);
|
|
122
|
+
const tagArray = tags ?? [];
|
|
123
|
+
await this.db.query(
|
|
124
|
+
`INSERT INTO _cache_entries (key, value, expires_at, tags)
|
|
125
|
+
VALUES ($1, $2, $3, $4)
|
|
126
|
+
ON CONFLICT (key) DO UPDATE
|
|
127
|
+
SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at, tags = EXCLUDED.tags`,
|
|
128
|
+
[key, serialized, expiresAt, tagArray]
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
async delete(...keys) {
|
|
132
|
+
await this.ready;
|
|
133
|
+
if (keys.length === 0) return 0;
|
|
134
|
+
const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
|
|
135
|
+
const result = await this.db.query(
|
|
136
|
+
`WITH deleted AS (DELETE FROM _cache_entries WHERE key IN (${placeholders}) RETURNING 1)
|
|
137
|
+
SELECT count(*)::text AS count FROM deleted`,
|
|
138
|
+
keys
|
|
139
|
+
);
|
|
140
|
+
return Number.parseInt(result.rows[0]?.count ?? "0", 10);
|
|
141
|
+
}
|
|
142
|
+
async deleteByPrefix(prefix) {
|
|
143
|
+
await this.ready;
|
|
144
|
+
const escaped = prefix.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
145
|
+
const result = await this.db.query(
|
|
146
|
+
`WITH deleted AS (DELETE FROM _cache_entries WHERE key LIKE $1 ESCAPE '\\' RETURNING 1)
|
|
147
|
+
SELECT count(*)::text AS count FROM deleted`,
|
|
148
|
+
[`${escaped}%`]
|
|
149
|
+
);
|
|
150
|
+
return Number.parseInt(result.rows[0]?.count ?? "0", 10);
|
|
151
|
+
}
|
|
152
|
+
async deleteByTags(tags) {
|
|
153
|
+
await this.ready;
|
|
154
|
+
if (tags.length === 0) return 0;
|
|
155
|
+
const result = await this.db.query(
|
|
156
|
+
`WITH deleted AS (DELETE FROM _cache_entries WHERE tags && $1 RETURNING 1)
|
|
157
|
+
SELECT count(*)::text AS count FROM deleted`,
|
|
158
|
+
[tags]
|
|
159
|
+
);
|
|
160
|
+
return Number.parseInt(result.rows[0]?.count ?? "0", 10);
|
|
161
|
+
}
|
|
162
|
+
async clear() {
|
|
163
|
+
await this.ready;
|
|
164
|
+
await this.db.exec("DELETE FROM _cache_entries");
|
|
165
|
+
}
|
|
166
|
+
async size() {
|
|
167
|
+
await this.ready;
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
const result = await this.db.query(
|
|
170
|
+
"SELECT count(*)::text AS count FROM _cache_entries WHERE expires_at > $1",
|
|
171
|
+
[now]
|
|
172
|
+
);
|
|
173
|
+
return Number.parseInt(result.rows[0]?.count ?? "0", 10);
|
|
174
|
+
}
|
|
175
|
+
async prune() {
|
|
176
|
+
await this.ready;
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
const result = await this.db.query(
|
|
179
|
+
`WITH deleted AS (DELETE FROM _cache_entries WHERE expires_at <= $1 RETURNING 1)
|
|
180
|
+
SELECT count(*)::text AS count FROM deleted`,
|
|
181
|
+
[now]
|
|
182
|
+
);
|
|
183
|
+
return Number.parseInt(result.rows[0]?.count ?? "0", 10);
|
|
184
|
+
}
|
|
185
|
+
async close() {
|
|
186
|
+
if (this.closeOnDestroy) {
|
|
187
|
+
await this.db.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
export {
|
|
192
|
+
InMemoryCacheStore,
|
|
193
|
+
PGliteCacheStore
|
|
194
|
+
};
|
|
195
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/memory.ts","../../src/adapters/pglite.ts"],"sourcesContent":["/**\n * In-Memory Cache Store\n *\n * Map-backed cache store. Fast, zero-dependency, single-instance only.\n * Use for development, testing, or when distributed state isn't needed.\n */\n\nimport type { CacheStore } from './types.js';\n\ninterface MemoryEntry {\n value: string; // JSON-serialized\n expiresAt: number;\n tags: string[];\n}\n\nexport class InMemoryCacheStore implements CacheStore {\n private store = new Map<string, MemoryEntry>();\n private maxEntries: number;\n\n constructor(options?: { maxEntries?: number }) {\n this.maxEntries = options?.maxEntries ?? 10_000;\n }\n\n async get<T = unknown>(key: string): Promise<T | null> {\n const entry = this.store.get(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiresAt) {\n this.store.delete(key);\n return null;\n }\n\n return JSON.parse(entry.value) as T;\n }\n\n async set<T = unknown>(\n key: string,\n value: T,\n ttlSeconds: number,\n tags?: string[],\n ): Promise<void> {\n // Evict oldest if at capacity\n if (this.store.size >= this.maxEntries && !this.store.has(key)) {\n const firstKey = this.store.keys().next().value;\n if (firstKey !== undefined) {\n this.store.delete(firstKey);\n }\n }\n\n this.store.set(key, {\n value: JSON.stringify(value),\n expiresAt: Date.now() + ttlSeconds * 1000,\n tags: tags ?? [],\n });\n }\n\n async delete(...keys: string[]): Promise<number> {\n let count = 0;\n for (const key of keys) {\n if (this.store.delete(key)) count++;\n }\n return count;\n }\n\n async deleteByPrefix(prefix: string): Promise<number> {\n let count = 0;\n for (const key of this.store.keys()) {\n if (key.startsWith(prefix)) {\n this.store.delete(key);\n count++;\n }\n }\n return count;\n }\n\n async deleteByTags(tags: string[]): Promise<number> {\n const tagSet = new Set(tags);\n let count = 0;\n for (const [key, entry] of this.store.entries()) {\n if (entry.tags.some((t) => tagSet.has(t))) {\n this.store.delete(key);\n count++;\n }\n }\n return count;\n }\n\n async clear(): Promise<void> {\n this.store.clear();\n }\n\n async size(): Promise<number> {\n // Count only non-expired entries\n const now = Date.now();\n let count = 0;\n for (const entry of this.store.values()) {\n if (entry.expiresAt > now) count++;\n }\n return count;\n }\n\n async prune(): Promise<number> {\n const now = Date.now();\n let pruned = 0;\n for (const [key, entry] of this.store.entries()) {\n if (entry.expiresAt <= now) {\n this.store.delete(key);\n pruned++;\n }\n }\n return pruned;\n }\n\n async close(): Promise<void> {\n this.store.clear();\n }\n}\n","/**\n * PGlite Cache Store\n *\n * PostgreSQL-compatible cache store backed by PGlite (in-memory or file-based).\n * Provides the same CacheStore interface as InMemoryCacheStore but uses SQL\n * for persistence and querying — enabling distributed invalidation via\n * ElectricSQL shape subscriptions in Phase 5.10C.\n *\n * Table schema is auto-created on first use (no external migrations needed).\n */\n\nimport type { CacheStore } from './types.js';\n\n/** Minimal PGlite interface — avoids importing the full @electric-sql/pglite package. */\ninterface PGliteInstance {\n exec(query: string): Promise<unknown>;\n query<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<{ rows: T[] }>;\n close(): Promise<void>;\n}\n\nconst CREATE_TABLE_SQL = `\n CREATE TABLE IF NOT EXISTS _cache_entries (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n expires_at BIGINT NOT NULL,\n tags TEXT[] NOT NULL DEFAULT '{}'\n );\n CREATE INDEX IF NOT EXISTS _cache_entries_expires_idx ON _cache_entries (expires_at);\n`;\n\ninterface PGliteCacheStoreOptions {\n /** PGlite instance (caller owns lifecycle unless closeOnDestroy is true). */\n db: PGliteInstance;\n /** Table name prefix to avoid collisions (default: none). */\n tablePrefix?: string;\n /** Close the PGlite instance when close() is called (default: false). */\n closeOnDestroy?: boolean;\n}\n\nexport class PGliteCacheStore implements CacheStore {\n private db: PGliteInstance;\n private ready: Promise<void>;\n private closeOnDestroy: boolean;\n\n constructor(options: PGliteCacheStoreOptions) {\n this.db = options.db;\n this.closeOnDestroy = options.closeOnDestroy ?? false;\n this.ready = this.init();\n }\n\n private async init(): Promise<void> {\n await this.db.exec(CREATE_TABLE_SQL);\n }\n\n async get<T = unknown>(key: string): Promise<T | null> {\n await this.ready;\n const now = Date.now();\n\n const result = await this.db.query<{ value: string }>(\n 'SELECT value FROM _cache_entries WHERE key = $1 AND expires_at > $2',\n [key, now],\n );\n\n const row = result.rows[0];\n if (!row) return null;\n\n return JSON.parse(row.value) as T;\n }\n\n async set<T = unknown>(\n key: string,\n value: T,\n ttlSeconds: number,\n tags?: string[],\n ): Promise<void> {\n await this.ready;\n const expiresAt = Date.now() + ttlSeconds * 1000;\n const serialized = JSON.stringify(value);\n const tagArray = tags ?? [];\n\n await this.db.query(\n `INSERT INTO _cache_entries (key, value, expires_at, tags)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (key) DO UPDATE\n SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at, tags = EXCLUDED.tags`,\n [key, serialized, expiresAt, tagArray],\n );\n }\n\n async delete(...keys: string[]): Promise<number> {\n await this.ready;\n if (keys.length === 0) return 0;\n\n // Build parameterized IN clause\n const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ');\n const result = await this.db.query<{ count: string }>(\n `WITH deleted AS (DELETE FROM _cache_entries WHERE key IN (${placeholders}) RETURNING 1)\n SELECT count(*)::text AS count FROM deleted`,\n keys,\n );\n\n return Number.parseInt(result.rows[0]?.count ?? '0', 10);\n }\n\n async deleteByPrefix(prefix: string): Promise<number> {\n await this.ready;\n // Escape LIKE metacharacters — backslash first, then % and _\n const escaped = prefix.replaceAll('\\\\', '\\\\\\\\').replaceAll('%', '\\\\%').replaceAll('_', '\\\\_');\n\n const result = await this.db.query<{ count: string }>(\n `WITH deleted AS (DELETE FROM _cache_entries WHERE key LIKE $1 ESCAPE '\\\\' RETURNING 1)\n SELECT count(*)::text AS count FROM deleted`,\n [`${escaped}%`],\n );\n\n return Number.parseInt(result.rows[0]?.count ?? '0', 10);\n }\n\n async deleteByTags(tags: string[]): Promise<number> {\n await this.ready;\n if (tags.length === 0) return 0;\n\n const result = await this.db.query<{ count: string }>(\n `WITH deleted AS (DELETE FROM _cache_entries WHERE tags && $1 RETURNING 1)\n SELECT count(*)::text AS count FROM deleted`,\n [tags],\n );\n\n return Number.parseInt(result.rows[0]?.count ?? '0', 10);\n }\n\n async clear(): Promise<void> {\n await this.ready;\n await this.db.exec('DELETE FROM _cache_entries');\n }\n\n async size(): Promise<number> {\n await this.ready;\n const now = Date.now();\n const result = await this.db.query<{ count: string }>(\n 'SELECT count(*)::text AS count FROM _cache_entries WHERE expires_at > $1',\n [now],\n );\n return Number.parseInt(result.rows[0]?.count ?? '0', 10);\n }\n\n async prune(): Promise<number> {\n await this.ready;\n const now = Date.now();\n const result = await this.db.query<{ count: string }>(\n `WITH deleted AS (DELETE FROM _cache_entries WHERE expires_at <= $1 RETURNING 1)\n SELECT count(*)::text AS count FROM deleted`,\n [now],\n );\n return Number.parseInt(result.rows[0]?.count ?? '0', 10);\n }\n\n async close(): Promise<void> {\n if (this.closeOnDestroy) {\n await this.db.close();\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { C as CacheStore } from './types-CmU1eRbl.js';
|
|
2
|
+
export { a as CacheEntry } from './types-CmU1eRbl.js';
|
|
1
3
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -342,6 +344,86 @@ declare function warmISRCache(paths: string[], baseURL?: string): Promise<{
|
|
|
342
344
|
}>;
|
|
343
345
|
}>;
|
|
344
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Cache Invalidation Channel
|
|
349
|
+
*
|
|
350
|
+
* Coordinates cache invalidation across instances using a shared database table.
|
|
351
|
+
* Events are written to `_cache_invalidation_events` and consumed by polling.
|
|
352
|
+
*
|
|
353
|
+
* Architecture:
|
|
354
|
+
* - Publisher: writes invalidation event to shared PGlite/PostgreSQL table
|
|
355
|
+
* - Subscriber: polls the table for new events and forwards to local CacheStore
|
|
356
|
+
* - Events auto-expire after TTL to prevent unbounded table growth
|
|
357
|
+
*
|
|
358
|
+
* Future: Replace polling with ElectricSQL shape subscriptions or LISTEN/NOTIFY
|
|
359
|
+
* for real-time push-based invalidation (Phase 5.10C/E).
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
type InvalidationEventType = 'delete' | 'delete-prefix' | 'delete-tags' | 'clear';
|
|
363
|
+
interface InvalidationEvent {
|
|
364
|
+
id: string;
|
|
365
|
+
type: InvalidationEventType;
|
|
366
|
+
/** Cache keys to delete (for 'delete' type). */
|
|
367
|
+
keys?: string[];
|
|
368
|
+
/** Prefix to match (for 'delete-prefix' type). */
|
|
369
|
+
prefix?: string;
|
|
370
|
+
/** Tags to match (for 'delete-tags' type). */
|
|
371
|
+
tags?: string[];
|
|
372
|
+
/** Instance ID that published the event (for deduplication). */
|
|
373
|
+
sourceInstance: string;
|
|
374
|
+
/** Timestamp when the event was created. */
|
|
375
|
+
createdAt: number;
|
|
376
|
+
}
|
|
377
|
+
interface InvalidationChannelOptions {
|
|
378
|
+
/** Unique instance identifier (used to skip self-published events). */
|
|
379
|
+
instanceId: string;
|
|
380
|
+
/** Poll interval in milliseconds (default: 5000). */
|
|
381
|
+
pollIntervalMs?: number;
|
|
382
|
+
/** Event TTL in seconds — events older than this are pruned (default: 60). */
|
|
383
|
+
eventTtlSeconds?: number;
|
|
384
|
+
}
|
|
385
|
+
interface PGliteInstance {
|
|
386
|
+
exec(query: string): Promise<unknown>;
|
|
387
|
+
query<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<{
|
|
388
|
+
rows: T[];
|
|
389
|
+
}>;
|
|
390
|
+
close(): Promise<void>;
|
|
391
|
+
}
|
|
392
|
+
declare class CacheInvalidationChannel {
|
|
393
|
+
private db;
|
|
394
|
+
private store;
|
|
395
|
+
private instanceId;
|
|
396
|
+
private pollIntervalMs;
|
|
397
|
+
private eventTtlSeconds;
|
|
398
|
+
private lastSeenTimestamp;
|
|
399
|
+
/** IDs processed at exactly lastSeenTimestamp (prevents re-processing on >= query). */
|
|
400
|
+
private processedAtBoundary;
|
|
401
|
+
private pollTimer;
|
|
402
|
+
private ready;
|
|
403
|
+
constructor(db: PGliteInstance, store: CacheStore, options: InvalidationChannelOptions);
|
|
404
|
+
private init;
|
|
405
|
+
/** Start polling for invalidation events. */
|
|
406
|
+
start(): Promise<void>;
|
|
407
|
+
/** Stop polling. */
|
|
408
|
+
stop(): void;
|
|
409
|
+
/** Publish a key deletion event. */
|
|
410
|
+
publishDelete(...keys: string[]): Promise<void>;
|
|
411
|
+
/** Publish a prefix deletion event. */
|
|
412
|
+
publishDeletePrefix(prefix: string): Promise<void>;
|
|
413
|
+
/** Publish a tag-based deletion event. */
|
|
414
|
+
publishDeleteTags(tags: string[]): Promise<void>;
|
|
415
|
+
/** Publish a clear-all event. */
|
|
416
|
+
publishClear(): Promise<void>;
|
|
417
|
+
private publish;
|
|
418
|
+
/** Poll for new events and apply them to the local cache store. */
|
|
419
|
+
poll(): Promise<number>;
|
|
420
|
+
private applyEvent;
|
|
421
|
+
/** Remove events older than the TTL. */
|
|
422
|
+
private prune;
|
|
423
|
+
/** Release resources. */
|
|
424
|
+
close(): Promise<void>;
|
|
425
|
+
}
|
|
426
|
+
|
|
345
427
|
/**
|
|
346
428
|
* Internal logger for @revealui/cache.
|
|
347
429
|
*
|
|
@@ -363,4 +445,4 @@ declare function configureCacheLogger(logger: CacheLogger): void;
|
|
|
363
445
|
*/
|
|
364
446
|
declare function getCacheLogger(): CacheLogger;
|
|
365
447
|
|
|
366
|
-
export { type CDNCacheConfig, type CDNPurgeConfig, CDN_CACHE_PRESETS, type CacheLogger, DEFAULT_CDN_CONFIG, type EdgeCacheConfig, type EdgeRateLimitConfig, EdgeRateLimiter, type GeoLocation, type ISRConfig, ISR_PRESETS, type PersonalizationConfig, addPreloadLinks, configureCacheLogger, createCachedFunction, createEdgeCachedFetch, generateCacheControl, generateCacheTags, generateCloudflareConfig, generateStaticParams, generateVercelCacheConfig, getABTestVariant, getCacheLogger, getCacheTTL, getGeoLocation, getPersonalizationConfig, purgeAllCache, purgeCDNCache, purgeCacheByTag, revalidatePath, revalidatePaths, revalidateTag, revalidateTags, setEdgeCacheHeaders, shouldCacheResponse, warmCDNCache, warmISRCache };
|
|
448
|
+
export { type CDNCacheConfig, type CDNPurgeConfig, CDN_CACHE_PRESETS, CacheInvalidationChannel, type CacheLogger, CacheStore, DEFAULT_CDN_CONFIG, type EdgeCacheConfig, type EdgeRateLimitConfig, EdgeRateLimiter, type GeoLocation, type ISRConfig, ISR_PRESETS, type InvalidationChannelOptions, type InvalidationEvent, type InvalidationEventType, type PersonalizationConfig, addPreloadLinks, configureCacheLogger, createCachedFunction, createEdgeCachedFetch, generateCacheControl, generateCacheTags, generateCloudflareConfig, generateStaticParams, generateVercelCacheConfig, getABTestVariant, getCacheLogger, getCacheTTL, getGeoLocation, getPersonalizationConfig, purgeAllCache, purgeCDNCache, purgeCacheByTag, revalidatePath, revalidatePaths, revalidateTag, revalidateTags, setEdgeCacheHeaders, shouldCacheResponse, warmCDNCache, warmISRCache };
|
package/dist/index.js
CHANGED
|
@@ -732,8 +732,168 @@ async function warmISRCache(paths, baseURL = process.env.NEXT_PUBLIC_URL || "htt
|
|
|
732
732
|
}
|
|
733
733
|
return { warmed, failed, errors };
|
|
734
734
|
}
|
|
735
|
+
|
|
736
|
+
// src/invalidation-channel.ts
|
|
737
|
+
var CREATE_EVENTS_TABLE_SQL = `
|
|
738
|
+
CREATE TABLE IF NOT EXISTS _cache_invalidation_events (
|
|
739
|
+
id TEXT PRIMARY KEY,
|
|
740
|
+
type TEXT NOT NULL,
|
|
741
|
+
keys TEXT[],
|
|
742
|
+
prefix TEXT,
|
|
743
|
+
tags TEXT[],
|
|
744
|
+
source_instance TEXT NOT NULL,
|
|
745
|
+
created_at BIGINT NOT NULL
|
|
746
|
+
);
|
|
747
|
+
CREATE INDEX IF NOT EXISTS _cache_inv_created_idx ON _cache_invalidation_events (created_at);
|
|
748
|
+
`;
|
|
749
|
+
var CacheInvalidationChannel = class {
|
|
750
|
+
db;
|
|
751
|
+
store;
|
|
752
|
+
instanceId;
|
|
753
|
+
pollIntervalMs;
|
|
754
|
+
eventTtlSeconds;
|
|
755
|
+
lastSeenTimestamp;
|
|
756
|
+
/** IDs processed at exactly lastSeenTimestamp (prevents re-processing on >= query). */
|
|
757
|
+
processedAtBoundary = /* @__PURE__ */ new Set();
|
|
758
|
+
pollTimer = null;
|
|
759
|
+
ready;
|
|
760
|
+
constructor(db, store, options) {
|
|
761
|
+
this.db = db;
|
|
762
|
+
this.store = store;
|
|
763
|
+
this.instanceId = options.instanceId;
|
|
764
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 5e3;
|
|
765
|
+
this.eventTtlSeconds = options.eventTtlSeconds ?? 60;
|
|
766
|
+
this.lastSeenTimestamp = Date.now() - 1;
|
|
767
|
+
this.ready = this.init();
|
|
768
|
+
}
|
|
769
|
+
async init() {
|
|
770
|
+
await this.db.exec(CREATE_EVENTS_TABLE_SQL);
|
|
771
|
+
}
|
|
772
|
+
/** Start polling for invalidation events. */
|
|
773
|
+
async start() {
|
|
774
|
+
await this.ready;
|
|
775
|
+
if (this.pollTimer) return;
|
|
776
|
+
this.pollTimer = setInterval(() => {
|
|
777
|
+
void this.poll();
|
|
778
|
+
}, this.pollIntervalMs);
|
|
779
|
+
if (this.pollTimer.unref) this.pollTimer.unref();
|
|
780
|
+
}
|
|
781
|
+
/** Stop polling. */
|
|
782
|
+
stop() {
|
|
783
|
+
if (this.pollTimer) {
|
|
784
|
+
clearInterval(this.pollTimer);
|
|
785
|
+
this.pollTimer = null;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
// ─── Publishing ─────────────────────────────────────────────────────
|
|
789
|
+
/** Publish a key deletion event. */
|
|
790
|
+
async publishDelete(...keys) {
|
|
791
|
+
await this.publish({ type: "delete", keys });
|
|
792
|
+
}
|
|
793
|
+
/** Publish a prefix deletion event. */
|
|
794
|
+
async publishDeletePrefix(prefix) {
|
|
795
|
+
await this.publish({ type: "delete-prefix", prefix });
|
|
796
|
+
}
|
|
797
|
+
/** Publish a tag-based deletion event. */
|
|
798
|
+
async publishDeleteTags(tags) {
|
|
799
|
+
await this.publish({ type: "delete-tags", tags });
|
|
800
|
+
}
|
|
801
|
+
/** Publish a clear-all event. */
|
|
802
|
+
async publishClear() {
|
|
803
|
+
await this.publish({ type: "clear" });
|
|
804
|
+
}
|
|
805
|
+
async publish(event) {
|
|
806
|
+
await this.ready;
|
|
807
|
+
const id = crypto.randomUUID();
|
|
808
|
+
const now = Date.now();
|
|
809
|
+
await this.db.query(
|
|
810
|
+
`INSERT INTO _cache_invalidation_events (id, type, keys, prefix, tags, source_instance, created_at)
|
|
811
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
812
|
+
[
|
|
813
|
+
id,
|
|
814
|
+
event.type,
|
|
815
|
+
event.keys ?? null,
|
|
816
|
+
event.prefix ?? null,
|
|
817
|
+
event.tags ?? null,
|
|
818
|
+
this.instanceId,
|
|
819
|
+
now
|
|
820
|
+
]
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
// ─── Polling ────────────────────────────────────────────────────────
|
|
824
|
+
/** Poll for new events and apply them to the local cache store. */
|
|
825
|
+
async poll() {
|
|
826
|
+
await this.ready;
|
|
827
|
+
const logger = getCacheLogger();
|
|
828
|
+
const result = await this.db.query(
|
|
829
|
+
`SELECT id, type, keys, prefix, tags, source_instance, created_at
|
|
830
|
+
FROM _cache_invalidation_events
|
|
831
|
+
WHERE created_at >= $1 AND source_instance != $2
|
|
832
|
+
ORDER BY created_at ASC`,
|
|
833
|
+
[this.lastSeenTimestamp, this.instanceId]
|
|
834
|
+
);
|
|
835
|
+
let applied = 0;
|
|
836
|
+
for (const row of result.rows) {
|
|
837
|
+
if (this.processedAtBoundary.has(row.id)) continue;
|
|
838
|
+
const createdAt = Number(row.created_at);
|
|
839
|
+
if (createdAt > this.lastSeenTimestamp) {
|
|
840
|
+
this.lastSeenTimestamp = createdAt;
|
|
841
|
+
this.processedAtBoundary.clear();
|
|
842
|
+
}
|
|
843
|
+
this.processedAtBoundary.add(row.id);
|
|
844
|
+
try {
|
|
845
|
+
await this.applyEvent(row.type, row);
|
|
846
|
+
applied++;
|
|
847
|
+
} catch (error) {
|
|
848
|
+
logger.error(
|
|
849
|
+
"Failed to apply invalidation event",
|
|
850
|
+
error instanceof Error ? error : new Error(String(error))
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
await this.prune();
|
|
855
|
+
return applied;
|
|
856
|
+
}
|
|
857
|
+
async applyEvent(type, row) {
|
|
858
|
+
switch (type) {
|
|
859
|
+
case "delete":
|
|
860
|
+
if (row.keys && row.keys.length > 0) {
|
|
861
|
+
await this.store.delete(...row.keys);
|
|
862
|
+
}
|
|
863
|
+
break;
|
|
864
|
+
case "delete-prefix":
|
|
865
|
+
if (row.prefix) {
|
|
866
|
+
await this.store.deleteByPrefix(row.prefix);
|
|
867
|
+
}
|
|
868
|
+
break;
|
|
869
|
+
case "delete-tags":
|
|
870
|
+
if (row.tags && row.tags.length > 0) {
|
|
871
|
+
await this.store.deleteByTags(row.tags);
|
|
872
|
+
}
|
|
873
|
+
break;
|
|
874
|
+
case "clear":
|
|
875
|
+
await this.store.clear();
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
/** Remove events older than the TTL. */
|
|
880
|
+
async prune() {
|
|
881
|
+
const cutoff = Date.now() - this.eventTtlSeconds * 1e3;
|
|
882
|
+
const result = await this.db.query(
|
|
883
|
+
`WITH deleted AS (DELETE FROM _cache_invalidation_events WHERE created_at < $1 RETURNING 1)
|
|
884
|
+
SELECT count(*)::text AS count FROM deleted`,
|
|
885
|
+
[cutoff]
|
|
886
|
+
);
|
|
887
|
+
return Number.parseInt(result.rows[0]?.count ?? "0", 10);
|
|
888
|
+
}
|
|
889
|
+
/** Release resources. */
|
|
890
|
+
async close() {
|
|
891
|
+
this.stop();
|
|
892
|
+
}
|
|
893
|
+
};
|
|
735
894
|
export {
|
|
736
895
|
CDN_CACHE_PRESETS,
|
|
896
|
+
CacheInvalidationChannel,
|
|
737
897
|
DEFAULT_CDN_CONFIG,
|
|
738
898
|
EdgeRateLimiter,
|
|
739
899
|
ISR_PRESETS,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cdn-config.ts","../src/logger.ts","../src/edge-cache.ts"],"sourcesContent":["/**\n * CDN Configuration and Cache Management\n *\n * Utilities for CDN caching, edge caching, and cache invalidation\n */\n\n/**\n * CDN Cache Configuration\n */\nexport interface CDNCacheConfig {\n provider?: 'cloudflare' | 'vercel' | 'fastly' | 'custom';\n zones?: string[];\n ttl?: number;\n staleWhileRevalidate?: number;\n staleIfError?: number;\n bypassCache?: boolean;\n cacheKey?: string[];\n varyHeaders?: string[];\n}\n\nexport const DEFAULT_CDN_CONFIG: CDNCacheConfig = {\n provider: 'vercel',\n ttl: 31536000, // 1 year for static assets\n staleWhileRevalidate: 86400, // 1 day\n staleIfError: 604800, // 1 week\n bypassCache: false,\n cacheKey: ['url', 'headers.accept', 'headers.accept-encoding'],\n varyHeaders: ['Accept', 'Accept-Encoding'],\n};\n\n/**\n * Generate Cache-Control header\n */\nexport function generateCacheControl(config: {\n maxAge?: number;\n sMaxAge?: number;\n staleWhileRevalidate?: number;\n staleIfError?: number;\n public?: boolean;\n private?: boolean;\n immutable?: boolean;\n noCache?: boolean;\n noStore?: boolean;\n}): string {\n const directives: string[] = [];\n\n // Visibility\n if (config.noStore) {\n directives.push('no-store');\n return directives.join(', ');\n }\n\n if (config.noCache) {\n directives.push('no-cache');\n return directives.join(', ');\n }\n\n if (config.public) {\n directives.push('public');\n } else if (config.private) {\n directives.push('private');\n }\n\n // Max age\n if (config.maxAge !== undefined) {\n directives.push(`max-age=${config.maxAge}`);\n }\n\n // Shared max age (CDN)\n if (config.sMaxAge !== undefined) {\n directives.push(`s-maxage=${config.sMaxAge}`);\n }\n\n // Stale-while-revalidate\n if (config.staleWhileRevalidate !== undefined) {\n directives.push(`stale-while-revalidate=${config.staleWhileRevalidate}`);\n }\n\n // Stale-if-error\n if (config.staleIfError !== undefined) {\n directives.push(`stale-if-error=${config.staleIfError}`);\n }\n\n // Immutable\n if (config.immutable) {\n directives.push('immutable');\n }\n\n return directives.join(', ');\n}\n\n/**\n * Cache presets for different asset types\n */\nexport const CDN_CACHE_PRESETS = {\n // Static assets with hashed filenames (immutable)\n immutable: {\n maxAge: 31536000, // 1 year\n sMaxAge: 31536000,\n public: true,\n immutable: true,\n },\n\n // Static assets (images, fonts)\n static: {\n maxAge: 2592000, // 30 days\n sMaxAge: 31536000, // 1 year on CDN\n staleWhileRevalidate: 86400, // 1 day\n public: true,\n },\n\n // API responses (short-lived)\n api: {\n maxAge: 0,\n sMaxAge: 60, // 1 minute on CDN\n staleWhileRevalidate: 30,\n public: true,\n },\n\n // HTML pages (dynamic)\n page: {\n maxAge: 0,\n sMaxAge: 300, // 5 minutes on CDN\n staleWhileRevalidate: 60,\n public: true,\n },\n\n // User-specific data\n private: {\n maxAge: 300, // 5 minutes\n private: true,\n staleWhileRevalidate: 60,\n },\n\n // No caching\n noCache: {\n noStore: true,\n },\n\n // Revalidate every request\n revalidate: {\n maxAge: 0,\n sMaxAge: 0,\n noCache: true,\n },\n} as const;\n\n/**\n * CDN Purge Configuration\n */\nexport interface CDNPurgeConfig {\n provider: 'cloudflare' | 'vercel' | 'fastly';\n apiKey?: string;\n apiSecret?: string;\n zoneId?: string;\n distributionId?: string;\n}\n\n/**\n * Purge CDN cache\n */\nexport async function purgeCDNCache(\n urls: string[],\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; purged: number; errors?: string[] }> {\n const { provider } = config;\n\n switch (provider) {\n case 'cloudflare':\n return purgeCloudflare(urls, config);\n case 'vercel':\n return purgeVercel(urls, config);\n case 'fastly':\n return purgeFastly(urls, config);\n default:\n throw new Error(`Unsupported CDN provider: ${provider}`);\n }\n}\n\n/**\n * Purge Cloudflare cache\n */\nasync function purgeCloudflare(\n urls: string[],\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; purged: number; errors?: string[] }> {\n const { apiKey, zoneId } = config;\n\n if (!(apiKey && zoneId)) {\n throw new Error('Cloudflare API key and zone ID required');\n }\n\n try {\n const response = await fetch(\n `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ files: urls }),\n },\n );\n\n const data = await response.json();\n\n return {\n success: data.success,\n purged: urls.length,\n errors: data.errors,\n };\n } catch (error) {\n return {\n success: false,\n purged: 0,\n errors: [error instanceof Error ? error.message : 'Unknown error'],\n };\n }\n}\n\n/**\n * Purge Vercel cache\n */\nasync function purgeVercel(\n urls: string[],\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; purged: number; errors?: string[] }> {\n const { apiKey } = config;\n\n if (!apiKey) {\n throw new Error('Vercel API token required');\n }\n\n try {\n const response = await fetch('https://api.vercel.com/v1/purge', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ urls }),\n });\n\n const data = await response.json();\n\n return {\n success: response.ok,\n purged: urls.length,\n errors: data.error ? [data.error.message] : undefined,\n };\n } catch (error) {\n return {\n success: false,\n purged: 0,\n errors: [error instanceof Error ? error.message : 'Unknown error'],\n };\n }\n}\n\n/**\n * Purge Fastly cache\n */\nasync function purgeFastly(\n urls: string[],\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; purged: number; errors?: string[] }> {\n const { apiKey } = config;\n\n if (!apiKey) {\n throw new Error('Fastly API key required');\n }\n\n try {\n const results = await Promise.all(\n urls.map(async (url) => {\n const response = await fetch(url, {\n method: 'PURGE',\n headers: {\n 'Fastly-Key': apiKey,\n },\n });\n\n return response.ok;\n }),\n );\n\n const purged = results.filter(Boolean).length;\n\n return {\n success: purged === urls.length,\n purged,\n };\n } catch (error) {\n return {\n success: false,\n purged: 0,\n errors: [error instanceof Error ? error.message : 'Unknown error'],\n };\n }\n}\n\n/**\n * Purge by cache tag\n */\nexport async function purgeCacheByTag(\n tags: string[],\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; purged: number; errors?: string[] }> {\n const { provider, apiKey, zoneId } = config;\n\n if (provider === 'cloudflare') {\n if (!(apiKey && zoneId)) {\n throw new Error('Cloudflare API key and zone ID required');\n }\n\n try {\n const response = await fetch(\n `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ tags }),\n },\n );\n\n const data = await response.json();\n\n return {\n success: data.success,\n purged: tags.length,\n errors: data.errors,\n };\n } catch (error) {\n return {\n success: false,\n purged: 0,\n errors: [error instanceof Error ? error.message : 'Unknown error'],\n };\n }\n }\n\n throw new Error(`Cache tag purging not supported for ${provider}`);\n}\n\n/**\n * Purge everything\n */\nexport async function purgeAllCache(\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; errors?: string[] }> {\n const { provider, apiKey, zoneId } = config;\n\n if (provider === 'cloudflare') {\n if (!(apiKey && zoneId)) {\n throw new Error('Cloudflare API key and zone ID required');\n }\n\n try {\n const response = await fetch(\n `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ purge_everything: true }),\n },\n );\n\n const data = await response.json();\n\n return {\n success: data.success,\n errors: data.errors,\n };\n } catch (error) {\n return {\n success: false,\n errors: [error instanceof Error ? error.message : 'Unknown error'],\n };\n }\n }\n\n throw new Error(`Purge all not supported for ${provider}`);\n}\n\n/**\n * CDN cache warming\n */\nexport async function warmCDNCache(\n urls: string[],\n options: {\n concurrency?: number;\n headers?: Record<string, string>;\n } = {},\n): Promise<{ warmed: number; failed: number; errors: string[] }> {\n const { concurrency = 5, headers = {} } = options;\n\n const results: { success: boolean; error?: string }[] = [];\n const chunks: string[][] = [];\n\n // Split into chunks\n for (let i = 0; i < urls.length; i += concurrency) {\n chunks.push(urls.slice(i, i + concurrency));\n }\n\n // Warm cache in chunks\n for (const chunk of chunks) {\n const chunkResults = await Promise.all(\n chunk.map(async (url) => {\n try {\n const response = await fetch(url, { headers });\n return {\n success: response.ok,\n error: response.ok ? undefined : `${response.status} ${response.statusText}`,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }),\n );\n\n results.push(...chunkResults);\n }\n\n const warmed = results.filter((r) => r.success).length;\n const failed = results.filter((r) => !r.success).length;\n const errors = results.flatMap((r) => (r.error ? [r.error] : []));\n\n return { warmed, failed, errors };\n}\n\n/**\n * Generate cache tags\n */\nexport function generateCacheTags(resource: {\n type: string;\n id?: string | number;\n related?: string[];\n}): string[] {\n const tags: string[] = [];\n\n // Type tag\n tags.push(resource.type);\n\n // ID tag\n if (resource.id) {\n tags.push(`${resource.type}:${resource.id}`);\n }\n\n // Related tags\n if (resource.related) {\n tags.push(...resource.related);\n }\n\n return tags;\n}\n\n/**\n * Edge cache configuration for Vercel\n */\nexport function generateVercelCacheConfig(preset: keyof typeof CDN_CACHE_PRESETS) {\n const config = CDN_CACHE_PRESETS[preset];\n const cacheControl = generateCacheControl(config);\n\n return {\n headers: {\n 'Cache-Control': cacheControl,\n 'CDN-Cache-Control': cacheControl,\n 'Vercel-CDN-Cache-Control': cacheControl,\n },\n };\n}\n\n/**\n * Edge cache configuration for Cloudflare\n */\nexport function generateCloudflareConfig(\n preset: keyof typeof CDN_CACHE_PRESETS,\n options: {\n cacheTags?: string[];\n bypassOnCookie?: string;\n } = {},\n) {\n const config = CDN_CACHE_PRESETS[preset];\n const cacheControl = generateCacheControl(config);\n\n const headers: Record<string, string> = {\n 'Cache-Control': cacheControl,\n };\n\n // Cache tags\n if (options.cacheTags && options.cacheTags.length > 0) {\n headers['Cache-Tag'] = options.cacheTags.join(',');\n }\n\n // Bypass on cookie\n if (options.bypassOnCookie) {\n headers['Cache-Control'] = `${cacheControl}, bypass=${options.bypassOnCookie}`;\n }\n\n return { headers };\n}\n\n/**\n * Check if response should be cached\n */\nexport function shouldCacheResponse(status: number, headers: Headers): boolean {\n // Don't cache errors\n if (status >= 400) {\n return false;\n }\n\n // Check Cache-Control header\n const cacheControl = headers.get('cache-control') || '';\n if (\n cacheControl.includes('no-store') ||\n cacheControl.includes('no-cache') ||\n cacheControl.includes('private')\n ) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Calculate cache TTL from headers\n */\nexport function getCacheTTL(headers: Headers): number {\n const cacheControl = headers.get('cache-control') || '';\n\n // Check s-maxage first (CDN), then max-age\n for (const directive of cacheControl.split(',')) {\n const trimmed = directive.trim();\n if (trimmed.startsWith('s-maxage=')) {\n const val = trimmed.slice('s-maxage='.length);\n const num = Number.parseInt(val, 10);\n if (!Number.isNaN(num)) return num;\n }\n }\n for (const directive of cacheControl.split(',')) {\n const trimmed = directive.trim();\n if (trimmed.startsWith('max-age=')) {\n const val = trimmed.slice('max-age='.length);\n const num = Number.parseInt(val, 10);\n if (!Number.isNaN(num)) return num;\n }\n }\n\n // Check Expires header\n const expires = headers.get('expires');\n if (expires) {\n const expiresDate = new Date(expires);\n const now = new Date();\n return Math.max(0, Math.floor((expiresDate.getTime() - now.getTime()) / 1000));\n }\n\n return 0;\n}\n","/**\n * Internal logger for @revealui/cache.\n *\n * Defaults to `console`. Consumers should call `configureCacheLogger()`\n * to supply a structured logger (e.g. from `@revealui/utils/logger`).\n */\n\nexport interface CacheLogger {\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n info(message: string, ...args: unknown[]): void;\n debug(message: string, ...args: unknown[]): void;\n}\n\nlet cacheLogger: CacheLogger = console;\n\n/**\n * Replace the default console logger with a structured logger.\n */\nexport function configureCacheLogger(logger: CacheLogger): void {\n cacheLogger = logger;\n}\n\n/**\n * Get the current cache logger instance.\n */\nexport function getCacheLogger(): CacheLogger {\n return cacheLogger;\n}\n","/**\n * Edge Caching and ISR (Incremental Static Regeneration)\n *\n * Utilities for Next.js edge caching, ISR, and on-demand revalidation\n */\n\nimport type { NextRequest, NextResponse } from 'next/server';\nimport { getCacheLogger } from './logger.js';\n\n/**\n * Next.js extends the standard RequestInit with a `next` property\n * for ISR revalidation and cache tags.\n */\ninterface NextFetchRequestInit extends RequestInit {\n next?: {\n revalidate?: number | false;\n tags?: string[];\n };\n}\n\n/**\n * ISR Configuration\n */\nexport interface ISRConfig {\n revalidate?: number | false;\n tags?: string[];\n dynamicParams?: boolean;\n}\n\nexport const ISR_PRESETS = {\n // Revalidate every request\n always: {\n revalidate: 0,\n },\n\n // Revalidate every minute\n minute: {\n revalidate: 60,\n },\n\n // Revalidate every 5 minutes\n fiveMinutes: {\n revalidate: 300,\n },\n\n // Revalidate every hour\n hourly: {\n revalidate: 3600,\n },\n\n // Revalidate daily\n daily: {\n revalidate: 86400,\n },\n\n // Never revalidate (static)\n never: {\n revalidate: false,\n },\n} as const;\n\n/**\n * Generate static params for ISR\n */\nexport async function generateStaticParams<T>(\n fetchFn: () => Promise<T[]>,\n mapFn: (item: T) => Record<string, string>,\n): Promise<Array<Record<string, string>>> {\n try {\n const items = await fetchFn();\n return items.map(mapFn);\n } catch (error) {\n getCacheLogger().error(\n 'Failed to generate static params',\n error instanceof Error ? error : new Error(String(error)),\n );\n return [];\n }\n}\n\n/**\n * Revalidate tag\n */\nexport async function revalidateTag(\n tag: string,\n secret?: string,\n): Promise<{ revalidated: boolean; error?: string }> {\n const baseUrl = process.env.NEXT_PUBLIC_URL;\n if (!baseUrl) {\n getCacheLogger().warn('revalidateTag skipped: NEXT_PUBLIC_URL is not configured', { tag });\n return { revalidated: false, error: 'NEXT_PUBLIC_URL is not configured' };\n }\n\n try {\n const url = new URL('/api/revalidate', baseUrl);\n\n const headers: HeadersInit = { 'Content-Type': 'application/json' };\n if (secret) {\n headers['x-revalidate-secret'] = secret;\n }\n\n const response = await fetch(url.toString(), {\n method: 'POST',\n headers,\n body: JSON.stringify({ tag }),\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n getCacheLogger().warn('revalidateTag failed', {\n tag,\n status: response.status,\n error: data.error,\n });\n }\n\n return {\n revalidated: response.ok,\n error: data.error,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n getCacheLogger().warn('revalidateTag error', { tag, error: message });\n return {\n revalidated: false,\n error: message,\n };\n }\n}\n\n/**\n * Revalidate path\n */\nexport async function revalidatePath(\n path: string,\n secret?: string,\n): Promise<{ revalidated: boolean; error?: string }> {\n const baseUrl = process.env.NEXT_PUBLIC_URL;\n if (!baseUrl) {\n getCacheLogger().warn('revalidatePath skipped: NEXT_PUBLIC_URL is not configured', { path });\n return { revalidated: false, error: 'NEXT_PUBLIC_URL is not configured' };\n }\n\n try {\n const url = new URL('/api/revalidate', baseUrl);\n\n const headers: HeadersInit = { 'Content-Type': 'application/json' };\n if (secret) {\n headers['x-revalidate-secret'] = secret;\n }\n\n const response = await fetch(url.toString(), {\n method: 'POST',\n headers,\n body: JSON.stringify({ path }),\n });\n\n const data = await response.json();\n\n return {\n revalidated: response.ok,\n error: data.error,\n };\n } catch (error) {\n return {\n revalidated: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n}\n\n/**\n * Revalidate multiple paths\n */\nexport async function revalidatePaths(\n paths: string[],\n secret?: string,\n): Promise<{\n revalidated: number;\n failed: number;\n errors: Array<{ path: string; error: string }>;\n}> {\n const results = await Promise.allSettled(paths.map((path) => revalidatePath(path, secret)));\n\n let revalidated = 0;\n let failed = 0;\n const errors: Array<{ path: string; error: string }> = [];\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n const path = paths[i];\n\n if (!(result && path)) {\n continue;\n }\n\n if (result.status === 'fulfilled' && result.value.revalidated) {\n revalidated++;\n } else {\n failed++;\n const error =\n result.status === 'fulfilled'\n ? result.value.error || 'Unknown error'\n : String(result.reason) || 'Unknown error';\n\n errors.push({ path, error });\n }\n }\n\n return { revalidated, failed, errors };\n}\n\n/**\n * Revalidate multiple tags\n */\nexport async function revalidateTags(\n tags: string[],\n secret?: string,\n): Promise<{\n revalidated: number;\n failed: number;\n errors: Array<{ tag: string; error: string }>;\n}> {\n const results = await Promise.allSettled(tags.map((tag) => revalidateTag(tag, secret)));\n\n let revalidated = 0;\n let failed = 0;\n const errors: Array<{ tag: string; error: string }> = [];\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n const tag = tags[i];\n\n if (!(result && tag)) {\n continue;\n }\n\n if (result.status === 'fulfilled' && result.value.revalidated) {\n revalidated++;\n } else {\n failed++;\n const error =\n result.status === 'fulfilled'\n ? result.value.error || 'Unknown error'\n : String(result.reason) || 'Unknown error';\n\n errors.push({ tag, error });\n }\n }\n\n return { revalidated, failed, errors };\n}\n\n/**\n * Edge middleware cache configuration\n */\nexport interface EdgeCacheConfig {\n cache?: 'force-cache' | 'no-cache' | 'no-store' | 'only-if-cached';\n next?: {\n revalidate?: number | false;\n tags?: string[];\n };\n}\n\n/**\n * Create edge cached fetch\n */\nexport function createEdgeCachedFetch(config: EdgeCacheConfig = {}) {\n return async <T>(url: string, options?: NextFetchRequestInit): Promise<T> => {\n const fetchOptions: NextFetchRequestInit = {\n ...options,\n ...config,\n next: {\n ...options?.next,\n ...config.next,\n },\n };\n\n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n throw new Error(`Fetch failed: ${response.statusText}`);\n }\n\n return response.json();\n };\n}\n\n/**\n * Unstable cache wrapper (Next.js 14+)\n */\nexport function createCachedFunction<TArgs extends unknown[], TReturn>(\n fn: (...args: TArgs) => Promise<TReturn>,\n options: {\n tags?: string[];\n revalidate?: number | false;\n } = {},\n): (...args: TArgs) => Promise<TReturn> {\n // If revalidation is disabled, bypass cache entirely\n if (options.revalidate === false) {\n return fn;\n }\n\n const ttlMs = (options.revalidate ?? 60) * 1000;\n const cache = new Map<string, { value: TReturn; expiresAt: number }>();\n\n return async (...args: TArgs): Promise<TReturn> => {\n const key = JSON.stringify(args);\n const now = Date.now();\n const cached = cache.get(key);\n\n if (cached && now < cached.expiresAt) {\n return cached.value;\n }\n\n const value = await fn(...args);\n cache.set(key, { value, expiresAt: now + ttlMs });\n return value;\n };\n}\n\n/**\n * Edge rate limiting with cache\n */\nexport interface EdgeRateLimitConfig {\n limit: number;\n window: number;\n key?: (request: NextRequest) => string;\n}\n\nexport class EdgeRateLimiter {\n private cache: Map<string, { count: number; resetTime: number }> = new Map();\n\n constructor(private config: EdgeRateLimitConfig) {}\n\n /**\n * Check rate limit\n */\n check(request: NextRequest): {\n allowed: boolean;\n limit: number;\n remaining: number;\n reset: number;\n } {\n const key = this.config.key\n ? this.config.key(request)\n : request.headers.get('x-forwarded-for') || 'unknown';\n\n const now = Date.now();\n let entry = this.cache.get(key);\n\n // Reset if window expired\n if (!entry || now > entry.resetTime) {\n entry = {\n count: 0,\n resetTime: now + this.config.window,\n };\n this.cache.set(key, entry);\n }\n\n // Increment count\n entry.count++;\n\n const allowed = entry.count <= this.config.limit;\n const remaining = Math.max(0, this.config.limit - entry.count);\n\n return {\n allowed,\n limit: this.config.limit,\n remaining,\n reset: entry.resetTime,\n };\n }\n\n /**\n * Clean up expired entries\n */\n cleanup(): void {\n const now = Date.now();\n for (const [key, entry] of this.cache.entries()) {\n if (now > entry.resetTime) {\n this.cache.delete(key);\n }\n }\n }\n}\n\n/**\n * Edge geolocation caching\n */\nexport interface GeoLocation {\n country?: string;\n region?: string;\n city?: string;\n latitude?: number;\n longitude?: number;\n}\n\nexport function getGeoLocation(request: NextRequest): GeoLocation | null {\n // Vercel edge headers\n const country = request.headers.get('x-vercel-ip-country');\n const region = request.headers.get('x-vercel-ip-country-region');\n const city = request.headers.get('x-vercel-ip-city');\n const latitude = request.headers.get('x-vercel-ip-latitude');\n const longitude = request.headers.get('x-vercel-ip-longitude');\n\n if (!country) {\n // Cloudflare headers\n const cfCountry = request.headers.get('cf-ipcountry');\n if (cfCountry) {\n return {\n country: cfCountry,\n };\n }\n\n return null;\n }\n\n return {\n country: country || undefined,\n region: region || undefined,\n city: city ? decodeURIComponent(city) : undefined,\n latitude: latitude ? parseFloat(latitude) : undefined,\n longitude: longitude ? parseFloat(longitude) : undefined,\n };\n}\n\n/**\n * Edge A/B testing with cache\n */\nexport function getABTestVariant(\n request: NextRequest,\n testName: string,\n variants: string[],\n): string {\n // Check cookie first\n const cookieName = `ab-test-${testName}`;\n const cookieVariant = request.cookies.get(cookieName)?.value;\n\n if (cookieVariant && variants.includes(cookieVariant)) {\n return cookieVariant;\n }\n\n // Assign variant based on IP hash\n const ip = request.headers.get('x-forwarded-for') || 'unknown';\n const hash = simpleHash(ip + testName);\n const variantIndex = hash % variants.length;\n const variant = variants[variantIndex];\n\n if (!variant) {\n throw new Error('No variant found for A/B test');\n }\n\n return variant;\n}\n\n/**\n * Simple hash function\n */\nfunction simpleHash(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash = (hash << 5) - hash + char;\n hash = hash & hash;\n }\n return Math.abs(hash);\n}\n\n/**\n * Edge personalization cache\n */\nexport interface PersonalizationConfig {\n userId?: string;\n preferences?: Record<string, unknown>;\n location?: GeoLocation;\n device?: 'mobile' | 'tablet' | 'desktop';\n variant?: string;\n}\n\nexport function getPersonalizationConfig(request: NextRequest): PersonalizationConfig {\n const userAgent = request.headers.get('user-agent') || '';\n const device = getDeviceType(userAgent);\n const location = getGeoLocation(request);\n\n return {\n userId: request.cookies.get('user-id')?.value,\n location: location || undefined,\n device,\n };\n}\n\n/**\n * Detect device type\n */\nfunction getDeviceType(userAgent: string): 'mobile' | 'tablet' | 'desktop' {\n const ua = userAgent.toLowerCase();\n const isTablet = ua.includes('tablet') || ua.includes('ipad');\n if (isTablet) return 'tablet';\n if (ua.includes('mobile')) return 'mobile';\n return 'desktop';\n}\n\n/**\n * Edge cache headers helper\n */\nexport function setEdgeCacheHeaders(\n response: NextResponse,\n config: {\n maxAge?: number;\n sMaxAge?: number;\n staleWhileRevalidate?: number;\n tags?: string[];\n },\n): NextResponse {\n const cacheControl: string[] = [];\n\n if (config.maxAge !== undefined) {\n cacheControl.push(`max-age=${config.maxAge}`);\n }\n\n if (config.sMaxAge !== undefined) {\n cacheControl.push(`s-maxage=${config.sMaxAge}`);\n }\n\n if (config.staleWhileRevalidate !== undefined) {\n cacheControl.push(`stale-while-revalidate=${config.staleWhileRevalidate}`);\n }\n\n if (cacheControl.length > 0) {\n response.headers.set('Cache-Control', cacheControl.join(', '));\n }\n\n if (config.tags && config.tags.length > 0) {\n response.headers.set('Cache-Tag', config.tags.join(','));\n }\n\n return response;\n}\n\n/**\n * Preload links for critical resources\n */\nexport function addPreloadLinks(\n response: NextResponse,\n resources: Array<{\n href: string;\n as: string;\n type?: string;\n crossorigin?: boolean;\n }>,\n): NextResponse {\n const links = resources.map((resource) => {\n const attrs = [`<${resource.href}>`, `rel=\"preload\"`, `as=\"${resource.as}\"`];\n\n if (resource.type) {\n attrs.push(`type=\"${resource.type}\"`);\n }\n\n if (resource.crossorigin) {\n attrs.push('crossorigin');\n }\n\n return attrs.join('; ');\n });\n\n if (links.length > 0) {\n response.headers.set('Link', links.join(', '));\n }\n\n return response;\n}\n\n/**\n * Cache warming for ISR pages\n */\nexport async function warmISRCache(\n paths: string[],\n baseURL: string = process.env.NEXT_PUBLIC_URL || 'http://localhost:3000',\n): Promise<{\n warmed: number;\n failed: number;\n errors: Array<{ path: string; error: string }>;\n}> {\n const results = await Promise.allSettled(\n paths.map(async (path) => {\n const url = new URL(path, baseURL);\n const response = await fetch(url.toString());\n\n if (!response.ok) {\n throw new Error(`${response.status} ${response.statusText}`);\n }\n\n return true;\n }),\n );\n\n let warmed = 0;\n let failed = 0;\n const errors: Array<{ path: string; error: string }> = [];\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n const path = paths[i];\n\n if (!(result && path)) {\n continue;\n }\n\n if (result.status === 'fulfilled') {\n warmed++;\n } else {\n failed++;\n errors.push({\n path,\n error:\n result.reason instanceof Error\n ? result.reason.message\n : String(result.reason) || 'Unknown error',\n });\n }\n }\n\n return { warmed, failed, errors };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/cdn-config.ts","../src/logger.ts","../src/edge-cache.ts","../src/invalidation-channel.ts"],"sourcesContent":["/**\n * CDN Configuration and Cache Management\n *\n * Utilities for CDN caching, edge caching, and cache invalidation\n */\n\n/**\n * CDN Cache Configuration\n */\nexport interface CDNCacheConfig {\n provider?: 'cloudflare' | 'vercel' | 'fastly' | 'custom';\n zones?: string[];\n ttl?: number;\n staleWhileRevalidate?: number;\n staleIfError?: number;\n bypassCache?: boolean;\n cacheKey?: string[];\n varyHeaders?: string[];\n}\n\nexport const DEFAULT_CDN_CONFIG: CDNCacheConfig = {\n provider: 'vercel',\n ttl: 31536000, // 1 year for static assets\n staleWhileRevalidate: 86400, // 1 day\n staleIfError: 604800, // 1 week\n bypassCache: false,\n cacheKey: ['url', 'headers.accept', 'headers.accept-encoding'],\n varyHeaders: ['Accept', 'Accept-Encoding'],\n};\n\n/**\n * Generate Cache-Control header\n */\nexport function generateCacheControl(config: {\n maxAge?: number;\n sMaxAge?: number;\n staleWhileRevalidate?: number;\n staleIfError?: number;\n public?: boolean;\n private?: boolean;\n immutable?: boolean;\n noCache?: boolean;\n noStore?: boolean;\n}): string {\n const directives: string[] = [];\n\n // Visibility\n if (config.noStore) {\n directives.push('no-store');\n return directives.join(', ');\n }\n\n if (config.noCache) {\n directives.push('no-cache');\n return directives.join(', ');\n }\n\n if (config.public) {\n directives.push('public');\n } else if (config.private) {\n directives.push('private');\n }\n\n // Max age\n if (config.maxAge !== undefined) {\n directives.push(`max-age=${config.maxAge}`);\n }\n\n // Shared max age (CDN)\n if (config.sMaxAge !== undefined) {\n directives.push(`s-maxage=${config.sMaxAge}`);\n }\n\n // Stale-while-revalidate\n if (config.staleWhileRevalidate !== undefined) {\n directives.push(`stale-while-revalidate=${config.staleWhileRevalidate}`);\n }\n\n // Stale-if-error\n if (config.staleIfError !== undefined) {\n directives.push(`stale-if-error=${config.staleIfError}`);\n }\n\n // Immutable\n if (config.immutable) {\n directives.push('immutable');\n }\n\n return directives.join(', ');\n}\n\n/**\n * Cache presets for different asset types\n */\nexport const CDN_CACHE_PRESETS = {\n // Static assets with hashed filenames (immutable)\n immutable: {\n maxAge: 31536000, // 1 year\n sMaxAge: 31536000,\n public: true,\n immutable: true,\n },\n\n // Static assets (images, fonts)\n static: {\n maxAge: 2592000, // 30 days\n sMaxAge: 31536000, // 1 year on CDN\n staleWhileRevalidate: 86400, // 1 day\n public: true,\n },\n\n // API responses (short-lived)\n api: {\n maxAge: 0,\n sMaxAge: 60, // 1 minute on CDN\n staleWhileRevalidate: 30,\n public: true,\n },\n\n // HTML pages (dynamic)\n page: {\n maxAge: 0,\n sMaxAge: 300, // 5 minutes on CDN\n staleWhileRevalidate: 60,\n public: true,\n },\n\n // User-specific data\n private: {\n maxAge: 300, // 5 minutes\n private: true,\n staleWhileRevalidate: 60,\n },\n\n // No caching\n noCache: {\n noStore: true,\n },\n\n // Revalidate every request\n revalidate: {\n maxAge: 0,\n sMaxAge: 0,\n noCache: true,\n },\n} as const;\n\n/**\n * CDN Purge Configuration\n */\nexport interface CDNPurgeConfig {\n provider: 'cloudflare' | 'vercel' | 'fastly';\n apiKey?: string;\n apiSecret?: string;\n zoneId?: string;\n distributionId?: string;\n}\n\n/**\n * Purge CDN cache\n */\nexport async function purgeCDNCache(\n urls: string[],\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; purged: number; errors?: string[] }> {\n const { provider } = config;\n\n switch (provider) {\n case 'cloudflare':\n return purgeCloudflare(urls, config);\n case 'vercel':\n return purgeVercel(urls, config);\n case 'fastly':\n return purgeFastly(urls, config);\n default:\n throw new Error(`Unsupported CDN provider: ${provider}`);\n }\n}\n\n/**\n * Purge Cloudflare cache\n */\nasync function purgeCloudflare(\n urls: string[],\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; purged: number; errors?: string[] }> {\n const { apiKey, zoneId } = config;\n\n if (!(apiKey && zoneId)) {\n throw new Error('Cloudflare API key and zone ID required');\n }\n\n try {\n const response = await fetch(\n `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ files: urls }),\n },\n );\n\n const data = await response.json();\n\n return {\n success: data.success,\n purged: urls.length,\n errors: data.errors,\n };\n } catch (error) {\n return {\n success: false,\n purged: 0,\n errors: [error instanceof Error ? error.message : 'Unknown error'],\n };\n }\n}\n\n/**\n * Purge Vercel cache\n */\nasync function purgeVercel(\n urls: string[],\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; purged: number; errors?: string[] }> {\n const { apiKey } = config;\n\n if (!apiKey) {\n throw new Error('Vercel API token required');\n }\n\n try {\n const response = await fetch('https://api.vercel.com/v1/purge', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ urls }),\n });\n\n const data = await response.json();\n\n return {\n success: response.ok,\n purged: urls.length,\n errors: data.error ? [data.error.message] : undefined,\n };\n } catch (error) {\n return {\n success: false,\n purged: 0,\n errors: [error instanceof Error ? error.message : 'Unknown error'],\n };\n }\n}\n\n/**\n * Purge Fastly cache\n */\nasync function purgeFastly(\n urls: string[],\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; purged: number; errors?: string[] }> {\n const { apiKey } = config;\n\n if (!apiKey) {\n throw new Error('Fastly API key required');\n }\n\n try {\n const results = await Promise.all(\n urls.map(async (url) => {\n const response = await fetch(url, {\n method: 'PURGE',\n headers: {\n 'Fastly-Key': apiKey,\n },\n });\n\n return response.ok;\n }),\n );\n\n const purged = results.filter(Boolean).length;\n\n return {\n success: purged === urls.length,\n purged,\n };\n } catch (error) {\n return {\n success: false,\n purged: 0,\n errors: [error instanceof Error ? error.message : 'Unknown error'],\n };\n }\n}\n\n/**\n * Purge by cache tag\n */\nexport async function purgeCacheByTag(\n tags: string[],\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; purged: number; errors?: string[] }> {\n const { provider, apiKey, zoneId } = config;\n\n if (provider === 'cloudflare') {\n if (!(apiKey && zoneId)) {\n throw new Error('Cloudflare API key and zone ID required');\n }\n\n try {\n const response = await fetch(\n `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ tags }),\n },\n );\n\n const data = await response.json();\n\n return {\n success: data.success,\n purged: tags.length,\n errors: data.errors,\n };\n } catch (error) {\n return {\n success: false,\n purged: 0,\n errors: [error instanceof Error ? error.message : 'Unknown error'],\n };\n }\n }\n\n throw new Error(`Cache tag purging not supported for ${provider}`);\n}\n\n/**\n * Purge everything\n */\nexport async function purgeAllCache(\n config: CDNPurgeConfig,\n): Promise<{ success: boolean; errors?: string[] }> {\n const { provider, apiKey, zoneId } = config;\n\n if (provider === 'cloudflare') {\n if (!(apiKey && zoneId)) {\n throw new Error('Cloudflare API key and zone ID required');\n }\n\n try {\n const response = await fetch(\n `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ purge_everything: true }),\n },\n );\n\n const data = await response.json();\n\n return {\n success: data.success,\n errors: data.errors,\n };\n } catch (error) {\n return {\n success: false,\n errors: [error instanceof Error ? error.message : 'Unknown error'],\n };\n }\n }\n\n throw new Error(`Purge all not supported for ${provider}`);\n}\n\n/**\n * CDN cache warming\n */\nexport async function warmCDNCache(\n urls: string[],\n options: {\n concurrency?: number;\n headers?: Record<string, string>;\n } = {},\n): Promise<{ warmed: number; failed: number; errors: string[] }> {\n const { concurrency = 5, headers = {} } = options;\n\n const results: { success: boolean; error?: string }[] = [];\n const chunks: string[][] = [];\n\n // Split into chunks\n for (let i = 0; i < urls.length; i += concurrency) {\n chunks.push(urls.slice(i, i + concurrency));\n }\n\n // Warm cache in chunks\n for (const chunk of chunks) {\n const chunkResults = await Promise.all(\n chunk.map(async (url) => {\n try {\n const response = await fetch(url, { headers });\n return {\n success: response.ok,\n error: response.ok ? undefined : `${response.status} ${response.statusText}`,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }),\n );\n\n results.push(...chunkResults);\n }\n\n const warmed = results.filter((r) => r.success).length;\n const failed = results.filter((r) => !r.success).length;\n const errors = results.flatMap((r) => (r.error ? [r.error] : []));\n\n return { warmed, failed, errors };\n}\n\n/**\n * Generate cache tags\n */\nexport function generateCacheTags(resource: {\n type: string;\n id?: string | number;\n related?: string[];\n}): string[] {\n const tags: string[] = [];\n\n // Type tag\n tags.push(resource.type);\n\n // ID tag\n if (resource.id) {\n tags.push(`${resource.type}:${resource.id}`);\n }\n\n // Related tags\n if (resource.related) {\n tags.push(...resource.related);\n }\n\n return tags;\n}\n\n/**\n * Edge cache configuration for Vercel\n */\nexport function generateVercelCacheConfig(preset: keyof typeof CDN_CACHE_PRESETS) {\n const config = CDN_CACHE_PRESETS[preset];\n const cacheControl = generateCacheControl(config);\n\n return {\n headers: {\n 'Cache-Control': cacheControl,\n 'CDN-Cache-Control': cacheControl,\n 'Vercel-CDN-Cache-Control': cacheControl,\n },\n };\n}\n\n/**\n * Edge cache configuration for Cloudflare\n */\nexport function generateCloudflareConfig(\n preset: keyof typeof CDN_CACHE_PRESETS,\n options: {\n cacheTags?: string[];\n bypassOnCookie?: string;\n } = {},\n) {\n const config = CDN_CACHE_PRESETS[preset];\n const cacheControl = generateCacheControl(config);\n\n const headers: Record<string, string> = {\n 'Cache-Control': cacheControl,\n };\n\n // Cache tags\n if (options.cacheTags && options.cacheTags.length > 0) {\n headers['Cache-Tag'] = options.cacheTags.join(',');\n }\n\n // Bypass on cookie\n if (options.bypassOnCookie) {\n headers['Cache-Control'] = `${cacheControl}, bypass=${options.bypassOnCookie}`;\n }\n\n return { headers };\n}\n\n/**\n * Check if response should be cached\n */\nexport function shouldCacheResponse(status: number, headers: Headers): boolean {\n // Don't cache errors\n if (status >= 400) {\n return false;\n }\n\n // Check Cache-Control header\n const cacheControl = headers.get('cache-control') || '';\n if (\n cacheControl.includes('no-store') ||\n cacheControl.includes('no-cache') ||\n cacheControl.includes('private')\n ) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Calculate cache TTL from headers\n */\nexport function getCacheTTL(headers: Headers): number {\n const cacheControl = headers.get('cache-control') || '';\n\n // Check s-maxage first (CDN), then max-age\n for (const directive of cacheControl.split(',')) {\n const trimmed = directive.trim();\n if (trimmed.startsWith('s-maxage=')) {\n const val = trimmed.slice('s-maxage='.length);\n const num = Number.parseInt(val, 10);\n if (!Number.isNaN(num)) return num;\n }\n }\n for (const directive of cacheControl.split(',')) {\n const trimmed = directive.trim();\n if (trimmed.startsWith('max-age=')) {\n const val = trimmed.slice('max-age='.length);\n const num = Number.parseInt(val, 10);\n if (!Number.isNaN(num)) return num;\n }\n }\n\n // Check Expires header\n const expires = headers.get('expires');\n if (expires) {\n const expiresDate = new Date(expires);\n const now = new Date();\n return Math.max(0, Math.floor((expiresDate.getTime() - now.getTime()) / 1000));\n }\n\n return 0;\n}\n","/**\n * Internal logger for @revealui/cache.\n *\n * Defaults to `console`. Consumers should call `configureCacheLogger()`\n * to supply a structured logger (e.g. from `@revealui/utils/logger`).\n */\n\nexport interface CacheLogger {\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n info(message: string, ...args: unknown[]): void;\n debug(message: string, ...args: unknown[]): void;\n}\n\nlet cacheLogger: CacheLogger = console;\n\n/**\n * Replace the default console logger with a structured logger.\n */\nexport function configureCacheLogger(logger: CacheLogger): void {\n cacheLogger = logger;\n}\n\n/**\n * Get the current cache logger instance.\n */\nexport function getCacheLogger(): CacheLogger {\n return cacheLogger;\n}\n","/**\n * Edge Caching and ISR (Incremental Static Regeneration)\n *\n * Utilities for Next.js edge caching, ISR, and on-demand revalidation\n */\n\nimport type { NextRequest, NextResponse } from 'next/server';\nimport { getCacheLogger } from './logger.js';\n\n/**\n * Next.js extends the standard RequestInit with a `next` property\n * for ISR revalidation and cache tags.\n */\ninterface NextFetchRequestInit extends RequestInit {\n next?: {\n revalidate?: number | false;\n tags?: string[];\n };\n}\n\n/**\n * ISR Configuration\n */\nexport interface ISRConfig {\n revalidate?: number | false;\n tags?: string[];\n dynamicParams?: boolean;\n}\n\nexport const ISR_PRESETS = {\n // Revalidate every request\n always: {\n revalidate: 0,\n },\n\n // Revalidate every minute\n minute: {\n revalidate: 60,\n },\n\n // Revalidate every 5 minutes\n fiveMinutes: {\n revalidate: 300,\n },\n\n // Revalidate every hour\n hourly: {\n revalidate: 3600,\n },\n\n // Revalidate daily\n daily: {\n revalidate: 86400,\n },\n\n // Never revalidate (static)\n never: {\n revalidate: false,\n },\n} as const;\n\n/**\n * Generate static params for ISR\n */\nexport async function generateStaticParams<T>(\n fetchFn: () => Promise<T[]>,\n mapFn: (item: T) => Record<string, string>,\n): Promise<Array<Record<string, string>>> {\n try {\n const items = await fetchFn();\n return items.map(mapFn);\n } catch (error) {\n getCacheLogger().error(\n 'Failed to generate static params',\n error instanceof Error ? error : new Error(String(error)),\n );\n return [];\n }\n}\n\n/**\n * Revalidate tag\n */\nexport async function revalidateTag(\n tag: string,\n secret?: string,\n): Promise<{ revalidated: boolean; error?: string }> {\n const baseUrl = process.env.NEXT_PUBLIC_URL;\n if (!baseUrl) {\n getCacheLogger().warn('revalidateTag skipped: NEXT_PUBLIC_URL is not configured', { tag });\n return { revalidated: false, error: 'NEXT_PUBLIC_URL is not configured' };\n }\n\n try {\n const url = new URL('/api/revalidate', baseUrl);\n\n const headers: HeadersInit = { 'Content-Type': 'application/json' };\n if (secret) {\n headers['x-revalidate-secret'] = secret;\n }\n\n const response = await fetch(url.toString(), {\n method: 'POST',\n headers,\n body: JSON.stringify({ tag }),\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n getCacheLogger().warn('revalidateTag failed', {\n tag,\n status: response.status,\n error: data.error,\n });\n }\n\n return {\n revalidated: response.ok,\n error: data.error,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n getCacheLogger().warn('revalidateTag error', { tag, error: message });\n return {\n revalidated: false,\n error: message,\n };\n }\n}\n\n/**\n * Revalidate path\n */\nexport async function revalidatePath(\n path: string,\n secret?: string,\n): Promise<{ revalidated: boolean; error?: string }> {\n const baseUrl = process.env.NEXT_PUBLIC_URL;\n if (!baseUrl) {\n getCacheLogger().warn('revalidatePath skipped: NEXT_PUBLIC_URL is not configured', { path });\n return { revalidated: false, error: 'NEXT_PUBLIC_URL is not configured' };\n }\n\n try {\n const url = new URL('/api/revalidate', baseUrl);\n\n const headers: HeadersInit = { 'Content-Type': 'application/json' };\n if (secret) {\n headers['x-revalidate-secret'] = secret;\n }\n\n const response = await fetch(url.toString(), {\n method: 'POST',\n headers,\n body: JSON.stringify({ path }),\n });\n\n const data = await response.json();\n\n return {\n revalidated: response.ok,\n error: data.error,\n };\n } catch (error) {\n return {\n revalidated: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n}\n\n/**\n * Revalidate multiple paths\n */\nexport async function revalidatePaths(\n paths: string[],\n secret?: string,\n): Promise<{\n revalidated: number;\n failed: number;\n errors: Array<{ path: string; error: string }>;\n}> {\n const results = await Promise.allSettled(paths.map((path) => revalidatePath(path, secret)));\n\n let revalidated = 0;\n let failed = 0;\n const errors: Array<{ path: string; error: string }> = [];\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n const path = paths[i];\n\n if (!(result && path)) {\n continue;\n }\n\n if (result.status === 'fulfilled' && result.value.revalidated) {\n revalidated++;\n } else {\n failed++;\n const error =\n result.status === 'fulfilled'\n ? result.value.error || 'Unknown error'\n : String(result.reason) || 'Unknown error';\n\n errors.push({ path, error });\n }\n }\n\n return { revalidated, failed, errors };\n}\n\n/**\n * Revalidate multiple tags\n */\nexport async function revalidateTags(\n tags: string[],\n secret?: string,\n): Promise<{\n revalidated: number;\n failed: number;\n errors: Array<{ tag: string; error: string }>;\n}> {\n const results = await Promise.allSettled(tags.map((tag) => revalidateTag(tag, secret)));\n\n let revalidated = 0;\n let failed = 0;\n const errors: Array<{ tag: string; error: string }> = [];\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n const tag = tags[i];\n\n if (!(result && tag)) {\n continue;\n }\n\n if (result.status === 'fulfilled' && result.value.revalidated) {\n revalidated++;\n } else {\n failed++;\n const error =\n result.status === 'fulfilled'\n ? result.value.error || 'Unknown error'\n : String(result.reason) || 'Unknown error';\n\n errors.push({ tag, error });\n }\n }\n\n return { revalidated, failed, errors };\n}\n\n/**\n * Edge middleware cache configuration\n */\nexport interface EdgeCacheConfig {\n cache?: 'force-cache' | 'no-cache' | 'no-store' | 'only-if-cached';\n next?: {\n revalidate?: number | false;\n tags?: string[];\n };\n}\n\n/**\n * Create edge cached fetch\n */\nexport function createEdgeCachedFetch(config: EdgeCacheConfig = {}) {\n return async <T>(url: string, options?: NextFetchRequestInit): Promise<T> => {\n const fetchOptions: NextFetchRequestInit = {\n ...options,\n ...config,\n next: {\n ...options?.next,\n ...config.next,\n },\n };\n\n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n throw new Error(`Fetch failed: ${response.statusText}`);\n }\n\n return response.json();\n };\n}\n\n/**\n * Unstable cache wrapper (Next.js 14+)\n */\nexport function createCachedFunction<TArgs extends unknown[], TReturn>(\n fn: (...args: TArgs) => Promise<TReturn>,\n options: {\n tags?: string[];\n revalidate?: number | false;\n } = {},\n): (...args: TArgs) => Promise<TReturn> {\n // If revalidation is disabled, bypass cache entirely\n if (options.revalidate === false) {\n return fn;\n }\n\n const ttlMs = (options.revalidate ?? 60) * 1000;\n const cache = new Map<string, { value: TReturn; expiresAt: number }>();\n\n return async (...args: TArgs): Promise<TReturn> => {\n const key = JSON.stringify(args);\n const now = Date.now();\n const cached = cache.get(key);\n\n if (cached && now < cached.expiresAt) {\n return cached.value;\n }\n\n const value = await fn(...args);\n cache.set(key, { value, expiresAt: now + ttlMs });\n return value;\n };\n}\n\n/**\n * Edge rate limiting with cache\n */\nexport interface EdgeRateLimitConfig {\n limit: number;\n window: number;\n key?: (request: NextRequest) => string;\n}\n\nexport class EdgeRateLimiter {\n private cache: Map<string, { count: number; resetTime: number }> = new Map();\n\n constructor(private config: EdgeRateLimitConfig) {}\n\n /**\n * Check rate limit\n */\n check(request: NextRequest): {\n allowed: boolean;\n limit: number;\n remaining: number;\n reset: number;\n } {\n const key = this.config.key\n ? this.config.key(request)\n : request.headers.get('x-forwarded-for') || 'unknown';\n\n const now = Date.now();\n let entry = this.cache.get(key);\n\n // Reset if window expired\n if (!entry || now > entry.resetTime) {\n entry = {\n count: 0,\n resetTime: now + this.config.window,\n };\n this.cache.set(key, entry);\n }\n\n // Increment count\n entry.count++;\n\n const allowed = entry.count <= this.config.limit;\n const remaining = Math.max(0, this.config.limit - entry.count);\n\n return {\n allowed,\n limit: this.config.limit,\n remaining,\n reset: entry.resetTime,\n };\n }\n\n /**\n * Clean up expired entries\n */\n cleanup(): void {\n const now = Date.now();\n for (const [key, entry] of this.cache.entries()) {\n if (now > entry.resetTime) {\n this.cache.delete(key);\n }\n }\n }\n}\n\n/**\n * Edge geolocation caching\n */\nexport interface GeoLocation {\n country?: string;\n region?: string;\n city?: string;\n latitude?: number;\n longitude?: number;\n}\n\nexport function getGeoLocation(request: NextRequest): GeoLocation | null {\n // Vercel edge headers\n const country = request.headers.get('x-vercel-ip-country');\n const region = request.headers.get('x-vercel-ip-country-region');\n const city = request.headers.get('x-vercel-ip-city');\n const latitude = request.headers.get('x-vercel-ip-latitude');\n const longitude = request.headers.get('x-vercel-ip-longitude');\n\n if (!country) {\n // Cloudflare headers\n const cfCountry = request.headers.get('cf-ipcountry');\n if (cfCountry) {\n return {\n country: cfCountry,\n };\n }\n\n return null;\n }\n\n return {\n country: country || undefined,\n region: region || undefined,\n city: city ? decodeURIComponent(city) : undefined,\n latitude: latitude ? parseFloat(latitude) : undefined,\n longitude: longitude ? parseFloat(longitude) : undefined,\n };\n}\n\n/**\n * Edge A/B testing with cache\n */\nexport function getABTestVariant(\n request: NextRequest,\n testName: string,\n variants: string[],\n): string {\n // Check cookie first\n const cookieName = `ab-test-${testName}`;\n const cookieVariant = request.cookies.get(cookieName)?.value;\n\n if (cookieVariant && variants.includes(cookieVariant)) {\n return cookieVariant;\n }\n\n // Assign variant based on IP hash\n const ip = request.headers.get('x-forwarded-for') || 'unknown';\n const hash = simpleHash(ip + testName);\n const variantIndex = hash % variants.length;\n const variant = variants[variantIndex];\n\n if (!variant) {\n throw new Error('No variant found for A/B test');\n }\n\n return variant;\n}\n\n/**\n * Simple hash function\n */\nfunction simpleHash(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash = (hash << 5) - hash + char;\n hash = hash & hash;\n }\n return Math.abs(hash);\n}\n\n/**\n * Edge personalization cache\n */\nexport interface PersonalizationConfig {\n userId?: string;\n preferences?: Record<string, unknown>;\n location?: GeoLocation;\n device?: 'mobile' | 'tablet' | 'desktop';\n variant?: string;\n}\n\nexport function getPersonalizationConfig(request: NextRequest): PersonalizationConfig {\n const userAgent = request.headers.get('user-agent') || '';\n const device = getDeviceType(userAgent);\n const location = getGeoLocation(request);\n\n return {\n userId: request.cookies.get('user-id')?.value,\n location: location || undefined,\n device,\n };\n}\n\n/**\n * Detect device type\n */\nfunction getDeviceType(userAgent: string): 'mobile' | 'tablet' | 'desktop' {\n const ua = userAgent.toLowerCase();\n const isTablet = ua.includes('tablet') || ua.includes('ipad');\n if (isTablet) return 'tablet';\n if (ua.includes('mobile')) return 'mobile';\n return 'desktop';\n}\n\n/**\n * Edge cache headers helper\n */\nexport function setEdgeCacheHeaders(\n response: NextResponse,\n config: {\n maxAge?: number;\n sMaxAge?: number;\n staleWhileRevalidate?: number;\n tags?: string[];\n },\n): NextResponse {\n const cacheControl: string[] = [];\n\n if (config.maxAge !== undefined) {\n cacheControl.push(`max-age=${config.maxAge}`);\n }\n\n if (config.sMaxAge !== undefined) {\n cacheControl.push(`s-maxage=${config.sMaxAge}`);\n }\n\n if (config.staleWhileRevalidate !== undefined) {\n cacheControl.push(`stale-while-revalidate=${config.staleWhileRevalidate}`);\n }\n\n if (cacheControl.length > 0) {\n response.headers.set('Cache-Control', cacheControl.join(', '));\n }\n\n if (config.tags && config.tags.length > 0) {\n response.headers.set('Cache-Tag', config.tags.join(','));\n }\n\n return response;\n}\n\n/**\n * Preload links for critical resources\n */\nexport function addPreloadLinks(\n response: NextResponse,\n resources: Array<{\n href: string;\n as: string;\n type?: string;\n crossorigin?: boolean;\n }>,\n): NextResponse {\n const links = resources.map((resource) => {\n const attrs = [`<${resource.href}>`, `rel=\"preload\"`, `as=\"${resource.as}\"`];\n\n if (resource.type) {\n attrs.push(`type=\"${resource.type}\"`);\n }\n\n if (resource.crossorigin) {\n attrs.push('crossorigin');\n }\n\n return attrs.join('; ');\n });\n\n if (links.length > 0) {\n response.headers.set('Link', links.join(', '));\n }\n\n return response;\n}\n\n/**\n * Cache warming for ISR pages\n */\nexport async function warmISRCache(\n paths: string[],\n baseURL: string = process.env.NEXT_PUBLIC_URL || 'http://localhost:3000',\n): Promise<{\n warmed: number;\n failed: number;\n errors: Array<{ path: string; error: string }>;\n}> {\n const results = await Promise.allSettled(\n paths.map(async (path) => {\n const url = new URL(path, baseURL);\n const response = await fetch(url.toString());\n\n if (!response.ok) {\n throw new Error(`${response.status} ${response.statusText}`);\n }\n\n return true;\n }),\n );\n\n let warmed = 0;\n let failed = 0;\n const errors: Array<{ path: string; error: string }> = [];\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n const path = paths[i];\n\n if (!(result && path)) {\n continue;\n }\n\n if (result.status === 'fulfilled') {\n warmed++;\n } else {\n failed++;\n errors.push({\n path,\n error:\n result.reason instanceof Error\n ? result.reason.message\n : String(result.reason) || 'Unknown error',\n });\n }\n }\n\n return { warmed, failed, errors };\n}\n","/**\n * Cache Invalidation Channel\n *\n * Coordinates cache invalidation across instances using a shared database table.\n * Events are written to `_cache_invalidation_events` and consumed by polling.\n *\n * Architecture:\n * - Publisher: writes invalidation event to shared PGlite/PostgreSQL table\n * - Subscriber: polls the table for new events and forwards to local CacheStore\n * - Events auto-expire after TTL to prevent unbounded table growth\n *\n * Future: Replace polling with ElectricSQL shape subscriptions or LISTEN/NOTIFY\n * for real-time push-based invalidation (Phase 5.10C/E).\n */\n\nimport type { CacheStore } from './adapters/types.js';\nimport { getCacheLogger } from './logger.js';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type InvalidationEventType = 'delete' | 'delete-prefix' | 'delete-tags' | 'clear';\n\nexport interface InvalidationEvent {\n id: string;\n type: InvalidationEventType;\n /** Cache keys to delete (for 'delete' type). */\n keys?: string[];\n /** Prefix to match (for 'delete-prefix' type). */\n prefix?: string;\n /** Tags to match (for 'delete-tags' type). */\n tags?: string[];\n /** Instance ID that published the event (for deduplication). */\n sourceInstance: string;\n /** Timestamp when the event was created. */\n createdAt: number;\n}\n\nexport interface InvalidationChannelOptions {\n /** Unique instance identifier (used to skip self-published events). */\n instanceId: string;\n /** Poll interval in milliseconds (default: 5000). */\n pollIntervalMs?: number;\n /** Event TTL in seconds — events older than this are pruned (default: 60). */\n eventTtlSeconds?: number;\n}\n\n// =============================================================================\n// PGlite interface\n// =============================================================================\n\ninterface PGliteInstance {\n exec(query: string): Promise<unknown>;\n query<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<{ rows: T[] }>;\n close(): Promise<void>;\n}\n\nconst CREATE_EVENTS_TABLE_SQL = `\n CREATE TABLE IF NOT EXISTS _cache_invalidation_events (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n keys TEXT[],\n prefix TEXT,\n tags TEXT[],\n source_instance TEXT NOT NULL,\n created_at BIGINT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS _cache_inv_created_idx ON _cache_invalidation_events (created_at);\n`;\n\n// =============================================================================\n// Invalidation Channel\n// =============================================================================\n\nexport class CacheInvalidationChannel {\n private db: PGliteInstance;\n private store: CacheStore;\n private instanceId: string;\n private pollIntervalMs: number;\n private eventTtlSeconds: number;\n private lastSeenTimestamp: number;\n /** IDs processed at exactly lastSeenTimestamp (prevents re-processing on >= query). */\n private processedAtBoundary: Set<string> = new Set();\n private pollTimer: ReturnType<typeof setInterval> | null = null;\n private ready: Promise<void>;\n\n constructor(db: PGliteInstance, store: CacheStore, options: InvalidationChannelOptions) {\n this.db = db;\n this.store = store;\n this.instanceId = options.instanceId;\n this.pollIntervalMs = options.pollIntervalMs ?? 5000;\n this.eventTtlSeconds = options.eventTtlSeconds ?? 60;\n this.lastSeenTimestamp = Date.now() - 1;\n this.ready = this.init();\n }\n\n private async init(): Promise<void> {\n await this.db.exec(CREATE_EVENTS_TABLE_SQL);\n }\n\n /** Start polling for invalidation events. */\n async start(): Promise<void> {\n await this.ready;\n if (this.pollTimer) return;\n\n this.pollTimer = setInterval(() => {\n void this.poll();\n }, this.pollIntervalMs);\n if (this.pollTimer.unref) this.pollTimer.unref();\n }\n\n /** Stop polling. */\n stop(): void {\n if (this.pollTimer) {\n clearInterval(this.pollTimer);\n this.pollTimer = null;\n }\n }\n\n // ─── Publishing ─────────────────────────────────────────────────────\n\n /** Publish a key deletion event. */\n async publishDelete(...keys: string[]): Promise<void> {\n await this.publish({ type: 'delete', keys });\n }\n\n /** Publish a prefix deletion event. */\n async publishDeletePrefix(prefix: string): Promise<void> {\n await this.publish({ type: 'delete-prefix', prefix });\n }\n\n /** Publish a tag-based deletion event. */\n async publishDeleteTags(tags: string[]): Promise<void> {\n await this.publish({ type: 'delete-tags', tags });\n }\n\n /** Publish a clear-all event. */\n async publishClear(): Promise<void> {\n await this.publish({ type: 'clear' });\n }\n\n private async publish(\n event: Pick<InvalidationEvent, 'type' | 'keys' | 'prefix' | 'tags'>,\n ): Promise<void> {\n await this.ready;\n const id = crypto.randomUUID();\n const now = Date.now();\n\n await this.db.query(\n `INSERT INTO _cache_invalidation_events (id, type, keys, prefix, tags, source_instance, created_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7)`,\n [\n id,\n event.type,\n event.keys ?? null,\n event.prefix ?? null,\n event.tags ?? null,\n this.instanceId,\n now,\n ],\n );\n }\n\n // ─── Polling ────────────────────────────────────────────────────────\n\n /** Poll for new events and apply them to the local cache store. */\n async poll(): Promise<number> {\n await this.ready;\n const logger = getCacheLogger();\n\n // Use >= to avoid missing events with the same millisecond timestamp.\n // Deduplication via processedAtBoundary prevents re-processing.\n const result = await this.db.query<{\n id: string;\n type: string;\n keys: string[] | null;\n prefix: string | null;\n tags: string[] | null;\n source_instance: string;\n created_at: string;\n }>(\n `SELECT id, type, keys, prefix, tags, source_instance, created_at\n FROM _cache_invalidation_events\n WHERE created_at >= $1 AND source_instance != $2\n ORDER BY created_at ASC`,\n [this.lastSeenTimestamp, this.instanceId],\n );\n\n let applied = 0;\n\n for (const row of result.rows) {\n // Skip events we already processed at the boundary timestamp\n if (this.processedAtBoundary.has(row.id)) continue;\n\n const createdAt = Number(row.created_at);\n if (createdAt > this.lastSeenTimestamp) {\n // Timestamp advanced — clear the old boundary set\n this.lastSeenTimestamp = createdAt;\n this.processedAtBoundary.clear();\n }\n this.processedAtBoundary.add(row.id);\n\n try {\n await this.applyEvent(row.type as InvalidationEventType, row);\n applied++;\n } catch (error) {\n logger.error(\n 'Failed to apply invalidation event',\n error instanceof Error ? error : new Error(String(error)),\n );\n }\n }\n\n // Prune old events\n await this.prune();\n\n return applied;\n }\n\n private async applyEvent(\n type: InvalidationEventType,\n row: { keys: string[] | null; prefix: string | null; tags: string[] | null },\n ): Promise<void> {\n switch (type) {\n case 'delete':\n if (row.keys && row.keys.length > 0) {\n await this.store.delete(...row.keys);\n }\n break;\n case 'delete-prefix':\n if (row.prefix) {\n await this.store.deleteByPrefix(row.prefix);\n }\n break;\n case 'delete-tags':\n if (row.tags && row.tags.length > 0) {\n await this.store.deleteByTags(row.tags);\n }\n break;\n case 'clear':\n await this.store.clear();\n break;\n }\n }\n\n /** Remove events older than the TTL. */\n private async prune(): Promise<number> {\n const cutoff = Date.now() - this.eventTtlSeconds * 1000;\n const result = await this.db.query<{ count: string }>(\n `WITH deleted AS (DELETE FROM _cache_invalidation_events WHERE created_at < $1 RETURNING 1)\n SELECT count(*)::text AS count FROM deleted`,\n [cutoff],\n );\n return Number.parseInt(result.rows[0]?.count ?? '0', 10);\n }\n\n /** Release resources. */\n async close(): Promise<void> {\n this.stop();\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Store Adapter Interface
|
|
3
|
+
*
|
|
4
|
+
* Unified interface for pluggable cache backends.
|
|
5
|
+
* Implementations: InMemoryCacheStore (Map), PGliteCacheStore (PostgreSQL-compatible).
|
|
6
|
+
*/
|
|
7
|
+
interface CacheEntry<T = unknown> {
|
|
8
|
+
key: string;
|
|
9
|
+
value: T;
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
tags?: string[];
|
|
12
|
+
}
|
|
13
|
+
interface CacheStore {
|
|
14
|
+
/** Get a cached value by key. Returns null if missing or expired. */
|
|
15
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
16
|
+
/** Set a value with TTL in seconds. Overwrites existing entries. */
|
|
17
|
+
set<T = unknown>(key: string, value: T, ttlSeconds: number, tags?: string[]): Promise<void>;
|
|
18
|
+
/** Delete one or more keys. Returns count of deleted entries. */
|
|
19
|
+
delete(...keys: string[]): Promise<number>;
|
|
20
|
+
/** Delete all entries whose key starts with the given prefix. */
|
|
21
|
+
deleteByPrefix(prefix: string): Promise<number>;
|
|
22
|
+
/** Delete all entries tagged with any of the given tags. */
|
|
23
|
+
deleteByTags(tags: string[]): Promise<number>;
|
|
24
|
+
/** Remove all entries from the store. */
|
|
25
|
+
clear(): Promise<void>;
|
|
26
|
+
/** Return approximate number of live (non-expired) entries. */
|
|
27
|
+
size(): Promise<number>;
|
|
28
|
+
/** Clean up expired entries. Called periodically or on demand. */
|
|
29
|
+
prune(): Promise<number>;
|
|
30
|
+
/** Tear down the store (close connections, free resources). */
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type { CacheStore as C, CacheEntry as a };
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revealui/cache",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Caching infrastructure for RevealUI - CDN config, edge cache, ISR presets, revalidation",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"dependencies": {},
|
|
7
7
|
"devDependencies": {
|
|
8
|
+
"@electric-sql/pglite": "^0.4.2",
|
|
8
9
|
"@types/node": "^25.5.0",
|
|
9
10
|
"tsup": "^8.5.1",
|
|
10
11
|
"typescript": "^6.0.2",
|
|
@@ -18,6 +19,10 @@
|
|
|
18
19
|
".": {
|
|
19
20
|
"types": "./dist/index.d.ts",
|
|
20
21
|
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./adapters": {
|
|
24
|
+
"types": "./dist/adapters/index.d.ts",
|
|
25
|
+
"import": "./dist/adapters/index.js"
|
|
21
26
|
}
|
|
22
27
|
},
|
|
23
28
|
"files": [
|
|
@@ -38,14 +43,19 @@
|
|
|
38
43
|
},
|
|
39
44
|
"type": "module",
|
|
40
45
|
"types": "./dist/index.d.ts",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/RevealUIStudio/revealui.git",
|
|
49
|
+
"directory": "packages/cache"
|
|
50
|
+
},
|
|
41
51
|
"scripts": {
|
|
42
52
|
"build": "tsup",
|
|
43
53
|
"clean": "rm -rf dist",
|
|
44
54
|
"dev": "tsup --watch",
|
|
45
55
|
"lint": "biome check .",
|
|
46
56
|
"lint:fix": "biome check --write .",
|
|
47
|
-
"test": "vitest run
|
|
48
|
-
"test:coverage": "vitest run --coverage --coverage.reporter=json-summary --coverage.reporter=html --coverage.reporter=text
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=json-summary --coverage.reporter=html --coverage.reporter=text",
|
|
49
59
|
"test:watch": "vitest",
|
|
50
60
|
"typecheck": "tsc --noEmit"
|
|
51
61
|
}
|
package/LICENSE.commercial
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
RevealUI Commercial License
|
|
2
|
-
Version 1.0, February 2026
|
|
3
|
-
|
|
4
|
-
Copyright (c) 2025-2026 RevealUI Studio (founder@revealui.com)
|
|
5
|
-
|
|
6
|
-
TERMS AND CONDITIONS
|
|
7
|
-
|
|
8
|
-
1. DEFINITIONS
|
|
9
|
-
|
|
10
|
-
"Software" means the RevealUI source code, documentation, and associated
|
|
11
|
-
files contained in directories and packages designated as commercial,
|
|
12
|
-
including but not limited to: packages/ai, packages/harnesses, and any
|
|
13
|
-
directory named "ee" within the repository.
|
|
14
|
-
|
|
15
|
-
"License Key" means a valid RevealUI license key obtained through an active
|
|
16
|
-
paid subscription at https://revealui.com.
|
|
17
|
-
|
|
18
|
-
"Licensee" means the individual or organization that holds a valid License
|
|
19
|
-
Key through an active subscription.
|
|
20
|
-
|
|
21
|
-
"Production Use" means any use of the Software beyond local development and
|
|
22
|
-
evaluation, including but not limited to: deploying the Software to serve
|
|
23
|
-
end users, integrating the Software into a product or service, or using the
|
|
24
|
-
Software in a revenue-generating capacity.
|
|
25
|
-
|
|
26
|
-
2. GRANT OF RIGHTS
|
|
27
|
-
|
|
28
|
-
Subject to the terms of this License and a valid License Key, the Licensee
|
|
29
|
-
is granted a non-exclusive, non-transferable, revocable license to:
|
|
30
|
-
|
|
31
|
-
(a) Use the Software for internal development and Production Use.
|
|
32
|
-
(b) Modify the Software for internal use.
|
|
33
|
-
(c) Deploy the Software on infrastructure controlled by the Licensee.
|
|
34
|
-
|
|
35
|
-
Enterprise License holders are additionally granted the right to:
|
|
36
|
-
|
|
37
|
-
(d) Deploy the Software in a self-hosted environment.
|
|
38
|
-
(e) Remove or replace RevealUI branding (white-label).
|
|
39
|
-
(f) Use the Software for multiple tenants within the Licensee's
|
|
40
|
-
organization or customer base.
|
|
41
|
-
|
|
42
|
-
3. RESTRICTIONS
|
|
43
|
-
|
|
44
|
-
The Licensee SHALL NOT:
|
|
45
|
-
|
|
46
|
-
(a) Provide the Software, or any portion of it, to third parties as a
|
|
47
|
-
hosted or managed service that competes with RevealUI.
|
|
48
|
-
(b) Redistribute, sublicense, sell, or otherwise transfer the Software
|
|
49
|
-
or any portion of it to third parties.
|
|
50
|
-
(c) Remove, alter, or circumvent the license key verification
|
|
51
|
-
functionality of the Software.
|
|
52
|
-
(d) Use the Software in Production without a valid License Key.
|
|
53
|
-
(e) Share, publish, or make the License Key available to unauthorized
|
|
54
|
-
parties.
|
|
55
|
-
|
|
56
|
-
4. EVALUATION
|
|
57
|
-
|
|
58
|
-
The Software may be used for evaluation and local development purposes
|
|
59
|
-
without a License Key. Evaluation use does not grant any rights to
|
|
60
|
-
Production Use.
|
|
61
|
-
|
|
62
|
-
5. SUBSCRIPTION AND PAYMENT
|
|
63
|
-
|
|
64
|
-
This License is valid only during the term of an active paid subscription.
|
|
65
|
-
Upon cancellation or expiration of the subscription:
|
|
66
|
-
|
|
67
|
-
(a) The License terminates automatically.
|
|
68
|
-
(b) A grace period of fourteen (14) days is provided for the Licensee
|
|
69
|
-
to transition away from Production Use.
|
|
70
|
-
(c) After the grace period, the Licensee must cease all Production Use
|
|
71
|
-
of the Software.
|
|
72
|
-
|
|
73
|
-
6. INTELLECTUAL PROPERTY
|
|
74
|
-
|
|
75
|
-
The Software is and remains the intellectual property of RevealUI Studio.
|
|
76
|
-
This License does not grant any ownership rights. Contributions to
|
|
77
|
-
commercial portions of the Software require a Contributor License Agreement.
|
|
78
|
-
|
|
79
|
-
7. OPEN SOURCE COMPONENTS
|
|
80
|
-
|
|
81
|
-
This License applies only to files and directories designated as commercial.
|
|
82
|
-
Files under the MIT License (as indicated in the root LICENSE file) are not
|
|
83
|
-
subject to this commercial license and may be used freely under MIT terms.
|
|
84
|
-
|
|
85
|
-
8. DISCLAIMER OF WARRANTY
|
|
86
|
-
|
|
87
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
88
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
89
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
90
|
-
|
|
91
|
-
9. LIMITATION OF LIABILITY
|
|
92
|
-
|
|
93
|
-
IN NO EVENT SHALL REVEALUI STUDIO BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
94
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
95
|
-
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
96
|
-
DEALINGS IN THE SOFTWARE, EXCEEDING THE AMOUNT PAID BY THE LICENSEE IN
|
|
97
|
-
THE TWELVE (12) MONTHS PRECEDING THE CLAIM.
|
|
98
|
-
|
|
99
|
-
10. GOVERNING LAW
|
|
100
|
-
|
|
101
|
-
This License shall be governed by the laws of the State of California,
|
|
102
|
-
United States of America, without regard to its conflict of law provisions.
|
|
103
|
-
|
|
104
|
-
11. ENTIRE AGREEMENT
|
|
105
|
-
|
|
106
|
-
This License constitutes the entire agreement between the parties with
|
|
107
|
-
respect to the commercial portions of the Software and supersedes all
|
|
108
|
-
prior agreements, understandings, and communications.
|
|
109
|
-
|
|
110
|
-
For licensing inquiries: founder@revealui.com
|
|
111
|
-
For pricing and subscriptions: https://revealui.com/pricing
|