@revealui/cache 0.0.0-canary-20260409021642
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +88 -0
- 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 +448 -0
- package/dist/index.js +926 -0
- package/dist/index.js.map +1 -0
- package/dist/types-CmU1eRbl.d.ts +34 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 RevealUI Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @revealui/cache
|
|
2
|
+
|
|
3
|
+
Caching infrastructure for RevealUI applications. Provides CDN cache configuration, edge cache helpers, ISR presets, tag-based revalidation, and rate limiting at the edge.
|
|
4
|
+
|
|
5
|
+
## When to Use This
|
|
6
|
+
|
|
7
|
+
- You need Cache-Control headers for CDN responses (Vercel, Cloudflare)
|
|
8
|
+
- You want ISR presets for Next.js pages (static, dynamic, real-time)
|
|
9
|
+
- You need tag-based cache invalidation when content changes
|
|
10
|
+
- You want edge-level rate limiting or A/B test variant assignment
|
|
11
|
+
- You need cache warming for static paths
|
|
12
|
+
|
|
13
|
+
If you're caching in-memory data within a single request, use standard `Map` or LRU — this package is for HTTP-layer and CDN caching.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @revealui/cache
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Optional peer dependency: `next` (>=14.0.0) — required for ISR helpers.
|
|
22
|
+
|
|
23
|
+
## API Reference
|
|
24
|
+
|
|
25
|
+
### CDN Configuration
|
|
26
|
+
|
|
27
|
+
| Export | Type | Purpose |
|
|
28
|
+
|--------|------|---------|
|
|
29
|
+
| `generateCacheControl` | Function | Build Cache-Control header string from config |
|
|
30
|
+
| `getCacheTTL` | Function | Get TTL for a content type |
|
|
31
|
+
| `CDN_CACHE_PRESETS` | Object | Pre-built configs (static, api, dynamic, immutable) |
|
|
32
|
+
| `DEFAULT_CDN_CONFIG` | Object | Default CDN configuration |
|
|
33
|
+
| `generateCacheTags` | Function | Generate cache tags for content-based invalidation |
|
|
34
|
+
| `generateVercelCacheConfig` | Function | Vercel-specific cache headers |
|
|
35
|
+
| `generateCloudflareConfig` | Function | Cloudflare-specific cache config |
|
|
36
|
+
| `shouldCacheResponse` | Function | Determine if a response should be cached |
|
|
37
|
+
|
|
38
|
+
### CDN Purge
|
|
39
|
+
|
|
40
|
+
| Export | Type | Purpose |
|
|
41
|
+
|--------|------|---------|
|
|
42
|
+
| `purgeCDNCache` | Function | Purge CDN cache by URL patterns |
|
|
43
|
+
| `purgeCacheByTag` | Function | Purge by cache tag (content-type based) |
|
|
44
|
+
| `purgeAllCache` | Function | Full CDN cache purge |
|
|
45
|
+
| `warmCDNCache` | Function | Pre-warm cache for a list of URLs |
|
|
46
|
+
|
|
47
|
+
### Edge Cache & ISR
|
|
48
|
+
|
|
49
|
+
| Export | Type | Purpose |
|
|
50
|
+
|--------|------|---------|
|
|
51
|
+
| `ISR_PRESETS` | Object | Next.js ISR configs (static: 1h, dynamic: 60s, realtime: 10s, immutable: 1y) |
|
|
52
|
+
| `revalidatePath` | Function | Revalidate a single Next.js path |
|
|
53
|
+
| `revalidatePaths` | Function | Batch path revalidation |
|
|
54
|
+
| `revalidateTag` | Function | Revalidate by cache tag |
|
|
55
|
+
| `revalidateTags` | Function | Batch tag revalidation |
|
|
56
|
+
| `generateStaticParams` | Function | Helper for Next.js static generation |
|
|
57
|
+
| `setEdgeCacheHeaders` | Function | Set edge-specific cache headers on response |
|
|
58
|
+
| `createEdgeCachedFetch` | Function | Fetch wrapper with edge caching |
|
|
59
|
+
| `createCachedFunction` | Function | Memoize an async function with TTL |
|
|
60
|
+
| `warmISRCache` | Function | Pre-warm ISR cache for static paths |
|
|
61
|
+
| `addPreloadLinks` | Function | Add `Link: <url>; rel=preload` headers |
|
|
62
|
+
|
|
63
|
+
### Edge Utilities
|
|
64
|
+
|
|
65
|
+
| Export | Type | Purpose |
|
|
66
|
+
|--------|------|---------|
|
|
67
|
+
| `EdgeRateLimiter` | Class | Token bucket rate limiter for edge functions |
|
|
68
|
+
| `getGeoLocation` | Function | Extract geo data from edge request headers |
|
|
69
|
+
| `getABTestVariant` | Function | Deterministic A/B test variant assignment |
|
|
70
|
+
| `getPersonalizationConfig` | Function | Edge personalization based on geo/device |
|
|
71
|
+
|
|
72
|
+
### Configuration
|
|
73
|
+
|
|
74
|
+
| Export | Type | Purpose |
|
|
75
|
+
|--------|------|---------|
|
|
76
|
+
| `configureCacheLogger` | Function | Set custom logger (defaults to console) |
|
|
77
|
+
|
|
78
|
+
## JOSHUA Alignment
|
|
79
|
+
|
|
80
|
+
- **Adaptive**: ISR presets scale from real-time (10s) to immutable (1y) based on content volatility
|
|
81
|
+
- **Unified**: Cache tags follow the same taxonomy as CMS collections — invalidation is automatic
|
|
82
|
+
- **Orthogonal**: Caching is a separate concern from content serving — swap CDN providers without changing business logic
|
|
83
|
+
|
|
84
|
+
## Related Packages
|
|
85
|
+
|
|
86
|
+
- `apps/api` — Applies cache headers to REST responses
|
|
87
|
+
- `apps/marketing` — Uses ISR presets for marketing pages
|
|
88
|
+
- `@revealui/core` — Triggers cache invalidation on content changes
|
|
@@ -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":[]}
|