@revealui/cache 0.2.0 → 0.2.2

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.
@@ -1,112 +1,90 @@
1
- RevealUI Commercial License
2
- Version 1.0, February 2026
1
+ RevealUI Studio — Commercial Licensing for Pro Packages
3
2
 
4
- Copyright (c) 2025-2026 RevealUI Studio (founder@revealui.com)
3
+ This file is an explainer for the dual-license structure of this repository.
4
+ It is NOT a standalone license document; the canonical license terms for
5
+ each Pro package live in that package's own LICENSE file.
5
6
 
6
- TERMS AND CONDITIONS
7
+ ## Repository Licensing Overview
7
8
 
8
- 1. DEFINITIONS
9
+ Most of this repository (the OSS packages, apps, scripts, docs, and tooling)
10
+ is licensed under the MIT License. See LICENSE in the repository root for
11
+ those terms.
9
12
 
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/mcp, packages/editors,
13
- packages/services, packages/harnesses, and any directory named "ee" within
14
- the repository.
13
+ A small number of "Pro" packages are licensed under the Functional Source
14
+ License v1.1 with MIT Future License (FSL-1.1-MIT) instead of MIT. Those
15
+ packages each carry their own LICENSE file containing the canonical FSL
16
+ terms; this file documents which packages are covered and what FSL-1.1-MIT
17
+ permits and restricts.
15
18
 
16
- "License Key" means a valid RevealUI license key obtained through an active
17
- paid subscription at https://revealui.com.
19
+ ## Pro Packages Covered
18
20
 
19
- "Licensee" means the individual or organization that holds a valid License
20
- Key through an active subscription.
21
+ The following packages distributed in this repository are licensed under
22
+ FSL-1.1-MIT, not MIT:
21
23
 
22
- "Production Use" means any use of the Software beyond local development and
23
- evaluation, including but not limited to: deploying the Software to serve
24
- end users, integrating the Software into a product or service, or using the
25
- Software in a revenue-generating capacity.
24
+ - @revealui/ai (see packages/ai/LICENSE)
25
+ - @revealui/harnesses (see packages/harnesses/LICENSE)
26
26
 
27
- 2. GRANT OF RIGHTS
27
+ Additional Pro packages may be added in future. The presence of a LICENSE
28
+ file inside a package directory containing the FSL-1.1-MIT text is the
29
+ authoritative signal that the package is governed by FSL terms rather than
30
+ the root MIT LICENSE.
28
31
 
29
- Subject to the terms of this License and a valid License Key, the Licensee
30
- is granted a non-exclusive, non-transferable, revocable license to:
32
+ ## What FSL-1.1-MIT Permits
31
33
 
32
- (a) Use the Software for internal development and Production Use.
33
- (b) Modify the Software for internal use.
34
- (c) Deploy the Software on infrastructure controlled by the Licensee.
34
+ Source code for Pro packages is publicly available. You may:
35
35
 
36
- Enterprise License holders are additionally granted the right to:
36
+ - Use the package internally for any purpose, including commercial use
37
+ - Modify the package and distribute derivative works under the same terms
38
+ - Inspect, audit, and debug the source
39
+ - Depend on the package from your own software
37
40
 
38
- (d) Deploy the Software in a self-hosted environment.
39
- (e) Remove or replace RevealUI branding (white-label).
40
- (f) Use the Software for multiple tenants within the Licensee's
41
- organization or customer base.
41
+ ## What FSL-1.1-MIT Restricts
42
42
 
43
- 3. RESTRICTIONS
43
+ You may NOT make the functionality of a Pro package available to third
44
+ parties as a service that competes with RevealUI Studio's commercial
45
+ offerings. The full restriction text in each per-package LICENSE controls;
46
+ in plain language, prohibited uses include:
44
47
 
45
- The Licensee SHALL NOT:
48
+ - Hosting the package as part of a SaaS that primarily delivers the
49
+ package's functionality
50
+ - Offering a service whose value derives entirely or primarily from the
51
+ package
52
+ - Repackaging the functionality for redistribution as a competing service
46
53
 
47
- (a) Provide the Software, or any portion of it, to third parties as a
48
- hosted or managed service that competes with RevealUI.
49
- (b) Redistribute, sublicense, sell, or otherwise transfer the Software
50
- or any portion of it to third parties.
51
- (c) Remove, alter, or circumvent the license key verification
52
- functionality of the Software.
53
- (d) Use the Software in Production without a valid License Key.
54
- (e) Share, publish, or make the License Key available to unauthorized
55
- parties.
54
+ If you are unsure whether your intended use is restricted, contact
55
+ founder@revealui.com before relying on the package.
56
56
 
57
- 4. EVALUATION
57
+ ## Change Date and MIT Conversion
58
58
 
59
- The Software may be used for evaluation and local development purposes
60
- without a License Key. Evaluation use does not grant any rights to
61
- Production Use.
59
+ Each Pro package's LICENSE file specifies a Change Date. On the earlier of
60
+ that date or the fourth anniversary of the first publicly-available
61
+ distribution of a specific version of the package under FSL, the package
62
+ automatically converts to the MIT License (the "Change License"). After
63
+ conversion, all FSL restrictions are removed for the version in question
64
+ and going forward.
62
65
 
63
- 5. SUBSCRIPTION AND PAYMENT
66
+ This means every Pro package in this repository will eventually become MIT;
67
+ FSL-1.1-MIT is a time-limited restriction, not a permanent one.
64
68
 
65
- This License is valid only during the term of an active paid subscription.
66
- Upon cancellation or expiration of the subscription:
69
+ ## Commercial Licensing Alternatives
67
70
 
68
- (a) The License terminates automatically.
69
- (b) A grace period of fourteen (14) days is provided for the Licensee
70
- to transition away from Production Use.
71
- (c) After the grace period, the Licensee must cease all Production Use
72
- of the Software.
71
+ If FSL-1.1-MIT terms do not fit your use case (for example, you are
72
+ building a service that the FSL would prohibit, or you need contractual
73
+ guarantees beyond what an open license provides), commercial licensing is
74
+ available.
73
75
 
74
- 6. INTELLECTUAL PROPERTY
76
+ Contact: founder@revealui.com
75
77
 
76
- The Software is and remains the intellectual property of RevealUI Studio.
77
- This License does not grant any ownership rights. Contributions to
78
- commercial portions of the Software require a Contributor License Agreement.
78
+ ## Canonical FSL-1.1-MIT Text
79
79
 
80
- 7. OPEN SOURCE COMPONENTS
80
+ The Functional Source License v1.1 with MIT Future License is published at:
81
81
 
82
- This License applies only to files and directories designated as commercial.
83
- Files under the MIT License (as indicated in the root LICENSE file) are not
84
- subject to this commercial license and may be used freely under MIT terms.
82
+ https://fsl.software/FSL-1.1-MIT.template.md
85
83
 
86
- 8. DISCLAIMER OF WARRANTY
84
+ The full canonical text is reproduced verbatim in each Pro package's
85
+ LICENSE file. If there is any conflict between this explainer and a
86
+ per-package LICENSE file, the per-package LICENSE file controls.
87
87
 
88
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
89
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
90
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
88
+ ---
91
89
 
92
- 9. LIMITATION OF LIABILITY
93
-
94
- IN NO EVENT SHALL REVEALUI STUDIO BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
95
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
96
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
97
- DEALINGS IN THE SOFTWARE, EXCEEDING THE AMOUNT PAID BY THE LICENSEE IN
98
- THE TWELVE (12) MONTHS PRECEDING THE CLAIM.
99
-
100
- 10. GOVERNING LAW
101
-
102
- This License shall be governed by the laws of the State of California,
103
- United States of America, without regard to its conflict of law provisions.
104
-
105
- 11. ENTIRE AGREEMENT
106
-
107
- This License constitutes the entire agreement between the parties with
108
- respect to the commercial portions of the Software and supersedes all
109
- prior agreements, understandings, and communications.
110
-
111
- For licensing inquiries: founder@revealui.com
112
- For pricing and subscriptions: https://revealui.com/pricing
90
+ Copyright (c) 2025-2026 RevealUI Studio
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ ---
2
+ title: "@revealui/cache"
3
+ description: "Caching infrastructure for RevealUI applications. Provides CDN cache configuration, edge cache helpers, ISR presets, tag-based revalidation, and rate limiting at the edge."
4
+ visibility: public
5
+ status: verified
6
+ audience: user
7
+ ---
8
+
9
+ # @revealui/cache
10
+
11
+ Caching infrastructure for RevealUI applications. Provides CDN cache configuration, edge cache helpers, ISR presets, tag-based revalidation, and rate limiting at the edge.
12
+
13
+ ## When to Use This
14
+
15
+ - You need Cache-Control headers for CDN responses (Vercel, Cloudflare)
16
+ - You want ISR presets for Next.js pages (static, dynamic, real-time)
17
+ - You need tag-based cache invalidation when content changes
18
+ - You want edge-level rate limiting or A/B test variant assignment
19
+ - You need cache warming for static paths
20
+
21
+ 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.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pnpm add @revealui/cache
27
+ ```
28
+
29
+ No peer dependencies. ISR helpers use structural typing compatible with `NextRequest`/`NextResponse`, Hono, and Cloudflare Workers - no `next` package required.
30
+
31
+ ## API Reference
32
+
33
+ ### CDN Configuration
34
+
35
+ | Export | Type | Purpose |
36
+ |--------|------|---------|
37
+ | `generateCacheControl` | Function | Build Cache-Control header string from config |
38
+ | `getCacheTTL` | Function | Get TTL for a content type |
39
+ | `CDN_CACHE_PRESETS` | Object | Pre-built configs (static, api, dynamic, immutable) |
40
+ | `DEFAULT_CDN_CONFIG` | Object | Default CDN configuration |
41
+ | `generateCacheTags` | Function | Generate cache tags for content-based invalidation |
42
+ | `generateVercelCacheConfig` | Function | Vercel-specific cache headers |
43
+ | `generateCloudflareConfig` | Function | Cloudflare-specific cache config |
44
+ | `shouldCacheResponse` | Function | Determine if a response should be cached |
45
+
46
+ ### CDN Purge
47
+
48
+ | Export | Type | Purpose |
49
+ |--------|------|---------|
50
+ | `purgeCDNCache` | Function | Purge CDN cache by URL patterns |
51
+ | `purgeCacheByTag` | Function | Purge by cache tag (content-type based) |
52
+ | `purgeAllCache` | Function | Full CDN cache purge |
53
+ | `warmCDNCache` | Function | Pre-warm cache for a list of URLs |
54
+
55
+ ### Edge Cache & ISR
56
+
57
+ | Export | Type | Purpose |
58
+ |--------|------|---------|
59
+ | `ISR_PRESETS` | Object | Next.js ISR configs (static: 1h, dynamic: 60s, realtime: 10s, immutable: 1y) |
60
+ | `revalidatePath` | Function | Revalidate a single Next.js path |
61
+ | `revalidatePaths` | Function | Batch path revalidation |
62
+ | `revalidateTag` | Function | Revalidate by cache tag |
63
+ | `revalidateTags` | Function | Batch tag revalidation |
64
+ | `generateStaticParams` | Function | Helper for Next.js static generation |
65
+ | `setEdgeCacheHeaders` | Function | Set edge-specific cache headers on response |
66
+ | `createEdgeCachedFetch` | Function | Fetch wrapper with edge caching |
67
+ | `createCachedFunction` | Function | Memoize an async function with TTL |
68
+ | `warmISRCache` | Function | Pre-warm ISR cache for static paths |
69
+ | `addPreloadLinks` | Function | Add `Link: <url>; rel=preload` headers |
70
+
71
+ ### Edge Utilities
72
+
73
+ | Export | Type | Purpose |
74
+ |--------|------|---------|
75
+ | `EdgeRateLimiter` | Class | Token bucket rate limiter for edge functions |
76
+ | `getGeoLocation` | Function | Extract geo data from edge request headers |
77
+ | `getABTestVariant` | Function | Deterministic A/B test variant assignment |
78
+ | `getPersonalizationConfig` | Function | Edge personalization based on geo/device |
79
+
80
+ ### Invalidation Channel
81
+
82
+ | Export | Type | Purpose |
83
+ |--------|------|---------|
84
+ | `CacheInvalidationChannel` | Class | Distributed cache busting via pub/sub channel |
85
+
86
+ ### Configuration
87
+
88
+ | Export | Type | Purpose |
89
+ |--------|------|---------|
90
+ | `configureCacheLogger` | Function | Set custom logger (defaults to console) |
91
+
92
+ ### Subpath Exports
93
+
94
+ | Subpath | Contents |
95
+ |---------|----------|
96
+ | `@revealui/cache` | All cache utilities |
97
+ | `@revealui/cache/adapters` | `CacheStore` interface + implementations (for custom backends) |
98
+
99
+ ## Design Principles
100
+
101
+ - **Adaptive**: ISR presets scale from real-time (10s) to immutable (1y) based on content volatility
102
+ - **Unified**: Cache tags follow the same taxonomy as CMS collections - invalidation is automatic
103
+ - **Orthogonal**: Caching is a separate concern from content serving - swap CDN providers without changing business logic
104
+
105
+ ## Related Packages
106
+
107
+ - `apps/server` - Applies cache headers to REST responses
108
+ - `apps/marketing` - Uses ISR presets for marketing pages
109
+ - `@revealui/core` - Triggers cache invalidation on content changes
@@ -0,0 +1,126 @@
1
+ import { C as CacheStore } from '../types-CmU1eRbl.js';
2
+ export { a as CacheEntry } from '../types-CmU1eRbl.js';
3
+
4
+ /**
5
+ * Browser PGlite Cache Store
6
+ *
7
+ * Creates a PGlite WASM instance in the browser backed by IndexedDB,
8
+ * then wraps it with PGliteCacheStore for SQL-powered client-side caching.
9
+ *
10
+ * Benefits over localStorage:
11
+ * - SQL queries for filtering cached data
12
+ * - Tag-based and prefix-based invalidation
13
+ * - IndexedDB storage (much larger than localStorage's ~5MB)
14
+ * - Shared CacheStore interface with server-side cache
15
+ *
16
+ * Usage:
17
+ * const cache = await createBrowserCache();
18
+ * await cache.set('posts:123', postData, 3600, ['posts']);
19
+ * const data = await cache.get('posts:123');
20
+ * await cache.close(); // on unmount
21
+ */
22
+
23
+ interface BrowserCacheOptions {
24
+ /** IndexedDB database name for persistence (default: 'revealui-cache') */
25
+ dbName?: string;
26
+ }
27
+ /**
28
+ * Create a browser-compatible PGlite cache store.
29
+ *
30
+ * Dynamically imports @electric-sql/pglite (WASM) to avoid bundling
31
+ * it in server builds. The PGlite instance uses IndexedDB for persistence
32
+ * so cached data survives page reloads.
33
+ */
34
+ declare function createBrowserCache(options?: BrowserCacheOptions): Promise<CacheStore>;
35
+
36
+ /**
37
+ * In-Memory Cache Store
38
+ *
39
+ * Map-backed cache store. Fast, zero-dependency, single-instance only.
40
+ * Use for development, testing, or when distributed state isn't needed.
41
+ */
42
+
43
+ declare class InMemoryCacheStore implements CacheStore {
44
+ private store;
45
+ private maxEntries;
46
+ constructor(options?: {
47
+ maxEntries?: number;
48
+ });
49
+ get<T = unknown>(key: string): Promise<T | null>;
50
+ set<T = unknown>(key: string, value: T, ttlSeconds: number, tags?: string[]): Promise<void>;
51
+ delete(...keys: string[]): Promise<number>;
52
+ deleteByPrefix(prefix: string): Promise<number>;
53
+ deleteByTags(tags: string[]): Promise<number>;
54
+ clear(): Promise<void>;
55
+ size(): Promise<number>;
56
+ prune(): Promise<number>;
57
+ close(): Promise<void>;
58
+ }
59
+
60
+ /**
61
+ * PGlite Cache Store
62
+ *
63
+ * PostgreSQL-compatible cache store backed by PGlite (in-memory or file-based).
64
+ * Provides the same CacheStore interface as InMemoryCacheStore but uses SQL
65
+ * for persistence and querying - enabling distributed invalidation via
66
+ * ElectricSQL shape subscriptions in Phase 5.10C.
67
+ *
68
+ * Table schema is auto-created on first use (no external migrations needed).
69
+ */
70
+
71
+ /** Minimal PGlite interface - avoids importing the full @electric-sql/pglite package. */
72
+ interface PGliteInstance {
73
+ exec(query: string): Promise<unknown>;
74
+ query<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<{
75
+ rows: T[];
76
+ }>;
77
+ close(): Promise<void>;
78
+ }
79
+ interface PGliteCacheStoreOptions {
80
+ /** PGlite instance (caller owns lifecycle unless closeOnDestroy is true). */
81
+ db: PGliteInstance;
82
+ /** Table name prefix to avoid collisions (default: none). */
83
+ tablePrefix?: string;
84
+ /** Close the PGlite instance when close() is called (default: false). */
85
+ closeOnDestroy?: boolean;
86
+ }
87
+ declare class PGliteCacheStore implements CacheStore {
88
+ private db;
89
+ private ready;
90
+ private closeOnDestroy;
91
+ constructor(options: PGliteCacheStoreOptions);
92
+ private init;
93
+ get<T = unknown>(key: string): Promise<T | null>;
94
+ set<T = unknown>(key: string, value: T, ttlSeconds: number, tags?: string[]): Promise<void>;
95
+ delete(...keys: string[]): Promise<number>;
96
+ deleteByPrefix(prefix: string): Promise<number>;
97
+ deleteByTags(tags: string[]): Promise<number>;
98
+ clear(): Promise<void>;
99
+ size(): Promise<number>;
100
+ prune(): Promise<number>;
101
+ close(): Promise<void>;
102
+ }
103
+
104
+ interface UseBrowserCacheResult {
105
+ /** The PGlite-backed CacheStore instance. Null while initializing. */
106
+ cache: CacheStore | null;
107
+ /** Whether the cache is still being initialized. */
108
+ loading: boolean;
109
+ /** Initialization error, if any. */
110
+ error: Error | null;
111
+ }
112
+ /**
113
+ * Access the browser-side PGlite cache store.
114
+ *
115
+ * Returns a shared singleton CacheStore. Multiple components can
116
+ * use this hook without creating duplicate PGlite instances.
117
+ *
118
+ * Example:
119
+ * const { cache, loading } = useBrowserCache();
120
+ * if (!loading && cache) {
121
+ * const data = await cache.get('posts:recent');
122
+ * }
123
+ */
124
+ declare function useBrowserCache(): UseBrowserCacheResult;
125
+
126
+ export { CacheStore, InMemoryCacheStore, PGliteCacheStore, createBrowserCache, useBrowserCache };
@@ -0,0 +1,144 @@
1
+ import {
2
+ PGliteCacheStore,
3
+ createBrowserCache
4
+ } from "../chunk-EPAGOXMX.js";
5
+
6
+ // src/adapters/memory.ts
7
+ var InMemoryCacheStore = class {
8
+ store = /* @__PURE__ */ new Map();
9
+ maxEntries;
10
+ constructor(options) {
11
+ this.maxEntries = options?.maxEntries ?? 1e4;
12
+ }
13
+ async get(key) {
14
+ const entry = this.store.get(key);
15
+ if (!entry) return null;
16
+ if (Date.now() > entry.expiresAt) {
17
+ this.store.delete(key);
18
+ return null;
19
+ }
20
+ return JSON.parse(entry.value);
21
+ }
22
+ async set(key, value, ttlSeconds, tags) {
23
+ if (this.store.size >= this.maxEntries && !this.store.has(key)) {
24
+ const firstKey = this.store.keys().next().value;
25
+ if (firstKey !== void 0) {
26
+ this.store.delete(firstKey);
27
+ }
28
+ }
29
+ this.store.set(key, {
30
+ value: JSON.stringify(value),
31
+ expiresAt: Date.now() + ttlSeconds * 1e3,
32
+ tags: tags ?? []
33
+ });
34
+ }
35
+ async delete(...keys) {
36
+ let count = 0;
37
+ for (const key of keys) {
38
+ if (this.store.delete(key)) count++;
39
+ }
40
+ return count;
41
+ }
42
+ async deleteByPrefix(prefix) {
43
+ let count = 0;
44
+ for (const key of this.store.keys()) {
45
+ if (key.startsWith(prefix)) {
46
+ this.store.delete(key);
47
+ count++;
48
+ }
49
+ }
50
+ return count;
51
+ }
52
+ async deleteByTags(tags) {
53
+ const tagSet = new Set(tags);
54
+ let count = 0;
55
+ for (const [key, entry] of this.store.entries()) {
56
+ if (entry.tags.some((t) => tagSet.has(t))) {
57
+ this.store.delete(key);
58
+ count++;
59
+ }
60
+ }
61
+ return count;
62
+ }
63
+ async clear() {
64
+ this.store.clear();
65
+ }
66
+ async size() {
67
+ const now = Date.now();
68
+ let count = 0;
69
+ for (const entry of this.store.values()) {
70
+ if (entry.expiresAt > now) count++;
71
+ }
72
+ return count;
73
+ }
74
+ async prune() {
75
+ const now = Date.now();
76
+ let pruned = 0;
77
+ for (const [key, entry] of this.store.entries()) {
78
+ if (entry.expiresAt <= now) {
79
+ this.store.delete(key);
80
+ pruned++;
81
+ }
82
+ }
83
+ return pruned;
84
+ }
85
+ async close() {
86
+ this.store.clear();
87
+ }
88
+ };
89
+
90
+ // src/adapters/use-browser-cache.ts
91
+ import { useEffect, useRef, useState } from "react";
92
+ var sharedCache = null;
93
+ var initPromise = null;
94
+ var refCount = 0;
95
+ async function getOrCreateCache() {
96
+ if (sharedCache) return sharedCache;
97
+ if (initPromise) return initPromise;
98
+ initPromise = import("../browser-7BTPENLH.js").then(async (mod) => {
99
+ const cache = await mod.createBrowserCache();
100
+ sharedCache = cache;
101
+ return cache;
102
+ });
103
+ return initPromise;
104
+ }
105
+ function useBrowserCache() {
106
+ const [cache, setCache] = useState(sharedCache);
107
+ const [loading, setLoading] = useState(!sharedCache);
108
+ const [error, setError] = useState(null);
109
+ const mounted = useRef(true);
110
+ useEffect(() => {
111
+ mounted.current = true;
112
+ refCount++;
113
+ if (!sharedCache) {
114
+ getOrCreateCache().then((c) => {
115
+ if (mounted.current) {
116
+ setCache(c);
117
+ setLoading(false);
118
+ }
119
+ }).catch((err) => {
120
+ if (mounted.current) {
121
+ setError(err instanceof Error ? err : new Error(String(err)));
122
+ setLoading(false);
123
+ }
124
+ });
125
+ }
126
+ return () => {
127
+ mounted.current = false;
128
+ refCount--;
129
+ if (refCount === 0 && sharedCache) {
130
+ sharedCache.close().catch(() => {
131
+ });
132
+ sharedCache = null;
133
+ initPromise = null;
134
+ }
135
+ };
136
+ }, []);
137
+ return { cache, loading, error };
138
+ }
139
+ export {
140
+ InMemoryCacheStore,
141
+ PGliteCacheStore,
142
+ createBrowserCache,
143
+ useBrowserCache
144
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ createBrowserCache
3
+ } from "./chunk-EPAGOXMX.js";
4
+ export {
5
+ createBrowserCache
6
+ };
@@ -0,0 +1,123 @@
1
+ // src/adapters/pglite.ts
2
+ var CREATE_TABLE_SQL = `
3
+ CREATE TABLE IF NOT EXISTS _cache_entries (
4
+ key TEXT PRIMARY KEY,
5
+ value TEXT NOT NULL,
6
+ expires_at BIGINT NOT NULL,
7
+ tags TEXT[] NOT NULL DEFAULT '{}'
8
+ );
9
+ CREATE INDEX IF NOT EXISTS _cache_entries_expires_idx ON _cache_entries (expires_at);
10
+ `;
11
+ var PGliteCacheStore = class {
12
+ db;
13
+ ready;
14
+ closeOnDestroy;
15
+ constructor(options) {
16
+ this.db = options.db;
17
+ this.closeOnDestroy = options.closeOnDestroy ?? false;
18
+ this.ready = this.init();
19
+ }
20
+ async init() {
21
+ await this.db.exec(CREATE_TABLE_SQL);
22
+ }
23
+ async get(key) {
24
+ await this.ready;
25
+ const now = Date.now();
26
+ const result = await this.db.query(
27
+ "SELECT value FROM _cache_entries WHERE key = $1 AND expires_at > $2",
28
+ [key, now]
29
+ );
30
+ const row = result.rows[0];
31
+ if (!row) return null;
32
+ return JSON.parse(row.value);
33
+ }
34
+ async set(key, value, ttlSeconds, tags) {
35
+ await this.ready;
36
+ const expiresAt = Date.now() + ttlSeconds * 1e3;
37
+ const serialized = JSON.stringify(value);
38
+ const tagArray = tags ?? [];
39
+ await this.db.query(
40
+ `INSERT INTO _cache_entries (key, value, expires_at, tags)
41
+ VALUES ($1, $2, $3, $4)
42
+ ON CONFLICT (key) DO UPDATE
43
+ SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at, tags = EXCLUDED.tags`,
44
+ [key, serialized, expiresAt, tagArray]
45
+ );
46
+ }
47
+ async delete(...keys) {
48
+ await this.ready;
49
+ if (keys.length === 0) return 0;
50
+ const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
51
+ const result = await this.db.query(
52
+ `WITH deleted AS (DELETE FROM _cache_entries WHERE key IN (${placeholders}) RETURNING 1)
53
+ SELECT count(*)::text AS count FROM deleted`,
54
+ keys
55
+ );
56
+ return Number.parseInt(result.rows[0]?.count ?? "0", 10);
57
+ }
58
+ async deleteByPrefix(prefix) {
59
+ await this.ready;
60
+ const escaped = prefix.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
61
+ const result = await this.db.query(
62
+ `WITH deleted AS (DELETE FROM _cache_entries WHERE key LIKE $1 ESCAPE '\\' RETURNING 1)
63
+ SELECT count(*)::text AS count FROM deleted`,
64
+ [`${escaped}%`]
65
+ );
66
+ return Number.parseInt(result.rows[0]?.count ?? "0", 10);
67
+ }
68
+ async deleteByTags(tags) {
69
+ await this.ready;
70
+ if (tags.length === 0) return 0;
71
+ const result = await this.db.query(
72
+ `WITH deleted AS (DELETE FROM _cache_entries WHERE tags && $1 RETURNING 1)
73
+ SELECT count(*)::text AS count FROM deleted`,
74
+ [tags]
75
+ );
76
+ return Number.parseInt(result.rows[0]?.count ?? "0", 10);
77
+ }
78
+ async clear() {
79
+ await this.ready;
80
+ await this.db.exec("DELETE FROM _cache_entries");
81
+ }
82
+ async size() {
83
+ await this.ready;
84
+ const now = Date.now();
85
+ const result = await this.db.query(
86
+ "SELECT count(*)::text AS count FROM _cache_entries WHERE expires_at > $1",
87
+ [now]
88
+ );
89
+ return Number.parseInt(result.rows[0]?.count ?? "0", 10);
90
+ }
91
+ async prune() {
92
+ await this.ready;
93
+ const now = Date.now();
94
+ const result = await this.db.query(
95
+ `WITH deleted AS (DELETE FROM _cache_entries WHERE expires_at <= $1 RETURNING 1)
96
+ SELECT count(*)::text AS count FROM deleted`,
97
+ [now]
98
+ );
99
+ return Number.parseInt(result.rows[0]?.count ?? "0", 10);
100
+ }
101
+ async close() {
102
+ if (this.closeOnDestroy) {
103
+ await this.db.close();
104
+ }
105
+ }
106
+ };
107
+
108
+ // src/adapters/browser.ts
109
+ async function createBrowserCache(options) {
110
+ const dbName = options?.dbName ?? "revealui-cache";
111
+ const { PGlite } = await import("@electric-sql/pglite");
112
+ const db = new PGlite(`idb://${dbName}`);
113
+ await db.waitReady;
114
+ return new PGliteCacheStore({
115
+ db,
116
+ closeOnDestroy: true
117
+ });
118
+ }
119
+
120
+ export {
121
+ PGliteCacheStore,
122
+ createBrowserCache
123
+ };