@revealui/cache 0.2.0 → 0.2.1

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/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { NextRequest, NextResponse } from 'next/server';
1
+ import { C as CacheStore } from './types-CmU1eRbl.js';
2
+ export { a as CacheEntry } from './types-CmU1eRbl.js';
2
3
 
3
4
  /**
4
5
  * CDN Configuration and Cache Management
@@ -158,14 +159,47 @@ declare function getCacheTTL(headers: Headers): number;
158
159
  /**
159
160
  * Edge Caching and ISR (Incremental Static Regeneration)
160
161
  *
161
- * Utilities for Next.js edge caching, ISR, and on-demand revalidation
162
+ * Framework-agnostic helpers for edge caching, ISR-style revalidation, edge
163
+ * rate limiting, geolocation, A/B testing, personalization, and CDN cache
164
+ * headers. Works in any runtime that exposes Web-standard `Request` /
165
+ * `Response` — NextRequest/NextResponse pass via structural typing, Hono's
166
+ * `c.req.raw` / `c.res` pass directly, Cloudflare Workers Request/Response
167
+ * pass directly, etc. The package no longer carries a `next` peer dep.
168
+ */
169
+ /**
170
+ * Framework-agnostic request shape for edge-cache helpers — compatible with
171
+ * NextRequest, Hono `c.req.raw`, Cloudflare Workers Request, and any other
172
+ * Web-standard `Request` subclass that exposes a NextRequest-style `cookies`
173
+ * map. Narrowed to the read-only subset we actually consume.
174
+ *
175
+ * Consumers using a bare Web `Request` (no `cookies` field — e.g., plain
176
+ * `fetch` requests) must wrap it before calling helpers that read cookies
177
+ * (`getABTestVariant`, `getPersonalizationConfig`). Helpers that only read
178
+ * headers (`getGeoLocation`, `EdgeRateLimiter.check` with default key) work
179
+ * with bare `Request` directly via subtype compatibility.
180
+ */
181
+ interface CacheRequest extends Request {
182
+ readonly cookies: {
183
+ get(name: string): {
184
+ readonly value: string;
185
+ } | undefined;
186
+ };
187
+ }
188
+ /**
189
+ * Framework-agnostic response shape for edge-cache helpers — compatible with
190
+ * NextResponse, Hono `c.res`, Cloudflare Workers Response, and any other
191
+ * Web-standard `Response` subclass. Helpers in this file only consume the
192
+ * standard `headers.set()` surface.
162
193
  */
163
-
194
+ type CacheResponse = Response;
164
195
  /**
165
- * Next.js extends the standard RequestInit with a `next` property
166
- * for ISR revalidation and cache tags.
196
+ * Framework-agnostic ISR-style fetch options. Extends Web-standard
197
+ * `RequestInit` with a `next` property that mirrors Next.js's ISR
198
+ * revalidation + cache-tag shape. Runtimes that don't honor the `next`
199
+ * field (anything outside Next.js) ignore it silently — the helper still
200
+ * works as a plain `fetch` wrapper.
167
201
  */
168
- interface NextFetchRequestInit extends RequestInit {
202
+ interface CachedFetchRequestInit extends RequestInit {
169
203
  next?: {
170
204
  revalidate?: number | false;
171
205
  tags?: string[];
@@ -252,9 +286,11 @@ interface EdgeCacheConfig {
252
286
  /**
253
287
  * Create edge cached fetch
254
288
  */
255
- declare function createEdgeCachedFetch(config?: EdgeCacheConfig): <T>(url: string, options?: NextFetchRequestInit) => Promise<T>;
289
+ declare function createEdgeCachedFetch(config?: EdgeCacheConfig): <T>(url: string, options?: CachedFetchRequestInit) => Promise<T>;
256
290
  /**
257
- * Unstable cache wrapper (Next.js 14+)
291
+ * Memoizing cache wrapper for any async function. TTL-based eviction.
292
+ * Framework-agnostic — does NOT use Next.js's `unstable_cache`; this is
293
+ * a plain in-memory wrapper that works in any runtime.
258
294
  */
259
295
  declare function createCachedFunction<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, options?: {
260
296
  tags?: string[];
@@ -266,7 +302,7 @@ declare function createCachedFunction<TArgs extends unknown[], TReturn>(fn: (...
266
302
  interface EdgeRateLimitConfig {
267
303
  limit: number;
268
304
  window: number;
269
- key?: (request: NextRequest) => string;
305
+ key?: (request: CacheRequest) => string;
270
306
  }
271
307
  declare class EdgeRateLimiter {
272
308
  private config;
@@ -275,7 +311,7 @@ declare class EdgeRateLimiter {
275
311
  /**
276
312
  * Check rate limit
277
313
  */
278
- check(request: NextRequest): {
314
+ check(request: CacheRequest): {
279
315
  allowed: boolean;
280
316
  limit: number;
281
317
  remaining: number;
@@ -296,11 +332,11 @@ interface GeoLocation {
296
332
  latitude?: number;
297
333
  longitude?: number;
298
334
  }
299
- declare function getGeoLocation(request: NextRequest): GeoLocation | null;
335
+ declare function getGeoLocation(request: CacheRequest): GeoLocation | null;
300
336
  /**
301
337
  * Edge A/B testing with cache
302
338
  */
303
- declare function getABTestVariant(request: NextRequest, testName: string, variants: string[]): string;
339
+ declare function getABTestVariant(request: CacheRequest, testName: string, variants: string[]): string;
304
340
  /**
305
341
  * Edge personalization cache
306
342
  */
@@ -311,25 +347,25 @@ interface PersonalizationConfig {
311
347
  device?: 'mobile' | 'tablet' | 'desktop';
312
348
  variant?: string;
313
349
  }
314
- declare function getPersonalizationConfig(request: NextRequest): PersonalizationConfig;
350
+ declare function getPersonalizationConfig(request: CacheRequest): PersonalizationConfig;
315
351
  /**
316
352
  * Edge cache headers helper
317
353
  */
318
- declare function setEdgeCacheHeaders(response: NextResponse, config: {
354
+ declare function setEdgeCacheHeaders(response: CacheResponse, config: {
319
355
  maxAge?: number;
320
356
  sMaxAge?: number;
321
357
  staleWhileRevalidate?: number;
322
358
  tags?: string[];
323
- }): NextResponse;
359
+ }): CacheResponse;
324
360
  /**
325
361
  * Preload links for critical resources
326
362
  */
327
- declare function addPreloadLinks(response: NextResponse, resources: Array<{
363
+ declare function addPreloadLinks(response: CacheResponse, resources: Array<{
328
364
  href: string;
329
365
  as: string;
330
366
  type?: string;
331
367
  crossorigin?: boolean;
332
- }>): NextResponse;
368
+ }>): CacheResponse;
333
369
  /**
334
370
  * Cache warming for ISR pages
335
371
  */
@@ -342,6 +378,86 @@ declare function warmISRCache(paths: string[], baseURL?: string): Promise<{
342
378
  }>;
343
379
  }>;
344
380
 
381
+ /**
382
+ * Cache Invalidation Channel
383
+ *
384
+ * Coordinates cache invalidation across instances using a shared database table.
385
+ * Events are written to `_cache_invalidation_events` and consumed by polling.
386
+ *
387
+ * Architecture:
388
+ * - Publisher: writes invalidation event to shared PGlite/PostgreSQL table
389
+ * - Subscriber: polls the table for new events and forwards to local CacheStore
390
+ * - Events auto-expire after TTL to prevent unbounded table growth
391
+ *
392
+ * Future: Replace polling with ElectricSQL shape subscriptions or LISTEN/NOTIFY
393
+ * for real-time push-based invalidation (Phase 5.10C/E).
394
+ */
395
+
396
+ type InvalidationEventType = 'delete' | 'delete-prefix' | 'delete-tags' | 'clear';
397
+ interface InvalidationEvent {
398
+ id: string;
399
+ type: InvalidationEventType;
400
+ /** Cache keys to delete (for 'delete' type). */
401
+ keys?: string[];
402
+ /** Prefix to match (for 'delete-prefix' type). */
403
+ prefix?: string;
404
+ /** Tags to match (for 'delete-tags' type). */
405
+ tags?: string[];
406
+ /** Instance ID that published the event (for deduplication). */
407
+ sourceInstance: string;
408
+ /** Timestamp when the event was created. */
409
+ createdAt: number;
410
+ }
411
+ interface InvalidationChannelOptions {
412
+ /** Unique instance identifier (used to skip self-published events). */
413
+ instanceId: string;
414
+ /** Poll interval in milliseconds (default: 5000). */
415
+ pollIntervalMs?: number;
416
+ /** Event TTL in seconds - events older than this are pruned (default: 60). */
417
+ eventTtlSeconds?: number;
418
+ }
419
+ interface PGliteInstance {
420
+ exec(query: string): Promise<unknown>;
421
+ query<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<{
422
+ rows: T[];
423
+ }>;
424
+ close(): Promise<void>;
425
+ }
426
+ declare class CacheInvalidationChannel {
427
+ private db;
428
+ private store;
429
+ private instanceId;
430
+ private pollIntervalMs;
431
+ private eventTtlSeconds;
432
+ private lastSeenTimestamp;
433
+ /** IDs processed at exactly lastSeenTimestamp (prevents re-processing on >= query). */
434
+ private processedAtBoundary;
435
+ private pollTimer;
436
+ private ready;
437
+ constructor(db: PGliteInstance, store: CacheStore, options: InvalidationChannelOptions);
438
+ private init;
439
+ /** Start polling for invalidation events. */
440
+ start(): Promise<void>;
441
+ /** Stop polling. */
442
+ stop(): void;
443
+ /** Publish a key deletion event. */
444
+ publishDelete(...keys: string[]): Promise<void>;
445
+ /** Publish a prefix deletion event. */
446
+ publishDeletePrefix(prefix: string): Promise<void>;
447
+ /** Publish a tag-based deletion event. */
448
+ publishDeleteTags(tags: string[]): Promise<void>;
449
+ /** Publish a clear-all event. */
450
+ publishClear(): Promise<void>;
451
+ private publish;
452
+ /** Poll for new events and apply them to the local cache store. */
453
+ poll(): Promise<number>;
454
+ private applyEvent;
455
+ /** Remove events older than the TTL. */
456
+ private prune;
457
+ /** Release resources. */
458
+ close(): Promise<void>;
459
+ }
460
+
345
461
  /**
346
462
  * Internal logger for @revealui/cache.
347
463
  *
@@ -363,4 +479,4 @@ declare function configureCacheLogger(logger: CacheLogger): void;
363
479
  */
364
480
  declare function getCacheLogger(): CacheLogger;
365
481
 
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 };
482
+ export { type CDNCacheConfig, type CDNPurgeConfig, CDN_CACHE_PRESETS, CacheInvalidationChannel, type CacheLogger, type CacheRequest, type CacheResponse, 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
@@ -120,7 +120,6 @@ async function purgeCloudflare(urls, config) {
120
120
  {
121
121
  method: "POST",
122
122
  headers: {
123
- // biome-ignore lint/style/useNamingConvention: HTTP header convention
124
123
  Authorization: `Bearer ${apiKey}`,
125
124
  "Content-Type": "application/json"
126
125
  },
@@ -150,7 +149,6 @@ async function purgeVercel(urls, config) {
150
149
  const response = await fetch("https://api.vercel.com/v1/purge", {
151
150
  method: "POST",
152
151
  headers: {
153
- // biome-ignore lint/style/useNamingConvention: HTTP header convention
154
152
  Authorization: `Bearer ${apiKey}`,
155
153
  "Content-Type": "application/json"
156
154
  },
@@ -212,7 +210,6 @@ async function purgeCacheByTag(tags, config) {
212
210
  {
213
211
  method: "POST",
214
212
  headers: {
215
- // biome-ignore lint/style/useNamingConvention: HTTP header convention
216
213
  Authorization: `Bearer ${apiKey}`,
217
214
  "Content-Type": "application/json"
218
215
  },
@@ -247,7 +244,6 @@ async function purgeAllCache(config) {
247
244
  {
248
245
  method: "POST",
249
246
  headers: {
250
- // biome-ignore lint/style/useNamingConvention: HTTP header convention
251
247
  Authorization: `Bearer ${apiKey}`,
252
248
  "Content-Type": "application/json"
253
249
  },
@@ -347,13 +343,21 @@ function shouldCacheResponse(status, headers) {
347
343
  }
348
344
  function getCacheTTL(headers) {
349
345
  const cacheControl = headers.get("cache-control") || "";
350
- const sMaxAgeMatch = cacheControl.match(/s-maxage=(\d+)/);
351
- if (sMaxAgeMatch?.[1]) {
352
- return parseInt(sMaxAgeMatch[1], 10);
346
+ for (const directive of cacheControl.split(",")) {
347
+ const trimmed = directive.trim();
348
+ if (trimmed.startsWith("s-maxage=")) {
349
+ const val = trimmed.slice("s-maxage=".length);
350
+ const num = Number.parseInt(val, 10);
351
+ if (!Number.isNaN(num)) return num;
352
+ }
353
353
  }
354
- const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
355
- if (maxAgeMatch?.[1]) {
356
- return parseInt(maxAgeMatch[1], 10);
354
+ for (const directive of cacheControl.split(",")) {
355
+ const trimmed = directive.trim();
356
+ if (trimmed.startsWith("max-age=")) {
357
+ const val = trimmed.slice("max-age=".length);
358
+ const num = Number.parseInt(val, 10);
359
+ if (!Number.isNaN(num)) return num;
360
+ }
357
361
  }
358
362
  const expires = headers.get("expires");
359
363
  if (expires) {
@@ -364,6 +368,9 @@ function getCacheTTL(headers) {
364
368
  return 0;
365
369
  }
366
370
 
371
+ // src/edge-cache.ts
372
+ import { getClientIp } from "@revealui/security";
373
+
367
374
  // src/logger.ts
368
375
  var cacheLogger = console;
369
376
  function configureCacheLogger(logger) {
@@ -560,12 +567,13 @@ var EdgeRateLimiter = class {
560
567
  constructor(config) {
561
568
  this.config = config;
562
569
  }
570
+ config;
563
571
  cache = /* @__PURE__ */ new Map();
564
572
  /**
565
573
  * Check rate limit
566
574
  */
567
575
  check(request) {
568
- const key = this.config.key ? this.config.key(request) : request.headers.get("x-forwarded-for") || "unknown";
576
+ const key = this.config.key ? this.config.key(request) : getClientIp(request);
569
577
  const now = Date.now();
570
578
  let entry = this.cache.get(key);
571
579
  if (!entry || now > entry.resetTime) {
@@ -626,7 +634,7 @@ function getABTestVariant(request, testName, variants) {
626
634
  if (cookieVariant && variants.includes(cookieVariant)) {
627
635
  return cookieVariant;
628
636
  }
629
- const ip = request.headers.get("x-forwarded-for") || "unknown";
637
+ const ip = getClientIp(request);
630
638
  const hash = simpleHash(ip + testName);
631
639
  const variantIndex = hash % variants.length;
632
640
  const variant = variants[variantIndex];
@@ -655,12 +663,10 @@ function getPersonalizationConfig(request) {
655
663
  };
656
664
  }
657
665
  function getDeviceType(userAgent) {
658
- if (/mobile/i.test(userAgent) && !/tablet|ipad/i.test(userAgent)) {
659
- return "mobile";
660
- }
661
- if (/tablet|ipad/i.test(userAgent)) {
662
- return "tablet";
663
- }
666
+ const ua = userAgent.toLowerCase();
667
+ const isTablet = ua.includes("tablet") || ua.includes("ipad");
668
+ if (isTablet) return "tablet";
669
+ if (ua.includes("mobile")) return "mobile";
664
670
  return "desktop";
665
671
  }
666
672
  function setEdgeCacheHeaders(response, config) {
@@ -730,8 +736,168 @@ async function warmISRCache(paths, baseURL = process.env.NEXT_PUBLIC_URL || "htt
730
736
  }
731
737
  return { warmed, failed, errors };
732
738
  }
739
+
740
+ // src/invalidation-channel.ts
741
+ var CREATE_EVENTS_TABLE_SQL = `
742
+ CREATE TABLE IF NOT EXISTS _cache_invalidation_events (
743
+ id TEXT PRIMARY KEY,
744
+ type TEXT NOT NULL,
745
+ keys TEXT[],
746
+ prefix TEXT,
747
+ tags TEXT[],
748
+ source_instance TEXT NOT NULL,
749
+ created_at BIGINT NOT NULL
750
+ );
751
+ CREATE INDEX IF NOT EXISTS _cache_inv_created_idx ON _cache_invalidation_events (created_at);
752
+ `;
753
+ var CacheInvalidationChannel = class {
754
+ db;
755
+ store;
756
+ instanceId;
757
+ pollIntervalMs;
758
+ eventTtlSeconds;
759
+ lastSeenTimestamp;
760
+ /** IDs processed at exactly lastSeenTimestamp (prevents re-processing on >= query). */
761
+ processedAtBoundary = /* @__PURE__ */ new Set();
762
+ pollTimer = null;
763
+ ready;
764
+ constructor(db, store, options) {
765
+ this.db = db;
766
+ this.store = store;
767
+ this.instanceId = options.instanceId;
768
+ this.pollIntervalMs = options.pollIntervalMs ?? 5e3;
769
+ this.eventTtlSeconds = options.eventTtlSeconds ?? 60;
770
+ this.lastSeenTimestamp = Date.now() - 1;
771
+ this.ready = this.init();
772
+ }
773
+ async init() {
774
+ await this.db.exec(CREATE_EVENTS_TABLE_SQL);
775
+ }
776
+ /** Start polling for invalidation events. */
777
+ async start() {
778
+ await this.ready;
779
+ if (this.pollTimer) return;
780
+ this.pollTimer = setInterval(() => {
781
+ void this.poll();
782
+ }, this.pollIntervalMs);
783
+ if (this.pollTimer.unref) this.pollTimer.unref();
784
+ }
785
+ /** Stop polling. */
786
+ stop() {
787
+ if (this.pollTimer) {
788
+ clearInterval(this.pollTimer);
789
+ this.pollTimer = null;
790
+ }
791
+ }
792
+ // ─── Publishing ─────────────────────────────────────────────────────
793
+ /** Publish a key deletion event. */
794
+ async publishDelete(...keys) {
795
+ await this.publish({ type: "delete", keys });
796
+ }
797
+ /** Publish a prefix deletion event. */
798
+ async publishDeletePrefix(prefix) {
799
+ await this.publish({ type: "delete-prefix", prefix });
800
+ }
801
+ /** Publish a tag-based deletion event. */
802
+ async publishDeleteTags(tags) {
803
+ await this.publish({ type: "delete-tags", tags });
804
+ }
805
+ /** Publish a clear-all event. */
806
+ async publishClear() {
807
+ await this.publish({ type: "clear" });
808
+ }
809
+ async publish(event) {
810
+ await this.ready;
811
+ const id = crypto.randomUUID();
812
+ const now = Date.now();
813
+ await this.db.query(
814
+ `INSERT INTO _cache_invalidation_events (id, type, keys, prefix, tags, source_instance, created_at)
815
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
816
+ [
817
+ id,
818
+ event.type,
819
+ event.keys ?? null,
820
+ event.prefix ?? null,
821
+ event.tags ?? null,
822
+ this.instanceId,
823
+ now
824
+ ]
825
+ );
826
+ }
827
+ // ─── Polling ────────────────────────────────────────────────────────
828
+ /** Poll for new events and apply them to the local cache store. */
829
+ async poll() {
830
+ await this.ready;
831
+ const logger = getCacheLogger();
832
+ const result = await this.db.query(
833
+ `SELECT id, type, keys, prefix, tags, source_instance, created_at
834
+ FROM _cache_invalidation_events
835
+ WHERE created_at >= $1 AND source_instance != $2
836
+ ORDER BY created_at ASC`,
837
+ [this.lastSeenTimestamp, this.instanceId]
838
+ );
839
+ let applied = 0;
840
+ for (const row of result.rows) {
841
+ if (this.processedAtBoundary.has(row.id)) continue;
842
+ const createdAt = Number(row.created_at);
843
+ if (createdAt > this.lastSeenTimestamp) {
844
+ this.lastSeenTimestamp = createdAt;
845
+ this.processedAtBoundary.clear();
846
+ }
847
+ this.processedAtBoundary.add(row.id);
848
+ try {
849
+ await this.applyEvent(row.type, row);
850
+ applied++;
851
+ } catch (error) {
852
+ logger.error(
853
+ "Failed to apply invalidation event",
854
+ error instanceof Error ? error : new Error(String(error))
855
+ );
856
+ }
857
+ }
858
+ await this.prune();
859
+ return applied;
860
+ }
861
+ async applyEvent(type, row) {
862
+ switch (type) {
863
+ case "delete":
864
+ if (row.keys && row.keys.length > 0) {
865
+ await this.store.delete(...row.keys);
866
+ }
867
+ break;
868
+ case "delete-prefix":
869
+ if (row.prefix) {
870
+ await this.store.deleteByPrefix(row.prefix);
871
+ }
872
+ break;
873
+ case "delete-tags":
874
+ if (row.tags && row.tags.length > 0) {
875
+ await this.store.deleteByTags(row.tags);
876
+ }
877
+ break;
878
+ case "clear":
879
+ await this.store.clear();
880
+ break;
881
+ }
882
+ }
883
+ /** Remove events older than the TTL. */
884
+ async prune() {
885
+ const cutoff = Date.now() - this.eventTtlSeconds * 1e3;
886
+ const result = await this.db.query(
887
+ `WITH deleted AS (DELETE FROM _cache_invalidation_events WHERE created_at < $1 RETURNING 1)
888
+ SELECT count(*)::text AS count FROM deleted`,
889
+ [cutoff]
890
+ );
891
+ return Number.parseInt(result.rows[0]?.count ?? "0", 10);
892
+ }
893
+ /** Release resources. */
894
+ async close() {
895
+ this.stop();
896
+ }
897
+ };
733
898
  export {
734
899
  CDN_CACHE_PRESETS,
900
+ CacheInvalidationChannel,
735
901
  DEFAULT_CDN_CONFIG,
736
902
  EdgeRateLimiter,
737
903
  ISR_PRESETS,
@@ -761,4 +927,3 @@ export {
761
927
  warmCDNCache,
762
928
  warmISRCache
763
929
  };
764
- //# sourceMappingURL=index.js.map
@@ -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,23 +1,18 @@
1
1
  {
2
2
  "name": "@revealui/cache",
3
- "version": "0.2.0",
4
- "description": "Caching infrastructure for RevealUI - CDN config, edge cache, ISR presets, revalidation",
3
+ "version": "0.2.1",
4
+ "description": "Framework-agnostic CDN config, edge cache, ISR-style presets, and revalidation helpers. Compatible with NextRequest/NextResponse, Hono, and Cloudflare Workers via structural typing — no `next` peer dep.",
5
5
  "license": "MIT",
6
- "dependencies": {},
6
+ "dependencies": {
7
+ "@revealui/security": "0.4.0"
8
+ },
7
9
  "devDependencies": {
8
- "@types/node": "^25.3.0",
10
+ "@electric-sql/pglite": "^0.4.5",
11
+ "@types/node": "^25.6.0",
9
12
  "tsup": "^8.5.1",
10
- "typescript": "^5.9.3",
11
- "vitest": "^4.0.18",
12
- "dev": "0.0.1"
13
- },
14
- "peerDependencies": {
15
- "next": "^14.0.0 || ^15.0.0 || ^16.0.0"
16
- },
17
- "peerDependenciesMeta": {
18
- "next": {
19
- "optional": true
20
- }
13
+ "typescript": "^6.0.3",
14
+ "vitest": "^4.1.5",
15
+ "@revealui/dev": "0.1.0"
21
16
  },
22
17
  "engines": {
23
18
  "node": ">=24.13.0"
@@ -26,6 +21,10 @@
26
21
  ".": {
27
22
  "types": "./dist/index.d.ts",
28
23
  "import": "./dist/index.js"
24
+ },
25
+ "./adapters": {
26
+ "types": "./dist/adapters/index.d.ts",
27
+ "import": "./dist/adapters/index.js"
29
28
  }
30
29
  },
31
30
  "files": [
@@ -38,14 +37,32 @@
38
37
  },
39
38
  "type": "module",
40
39
  "types": "./dist/index.d.ts",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/RevealUIStudio/revealui.git",
43
+ "directory": "packages/cache"
44
+ },
45
+ "homepage": "https://revealui.com",
46
+ "author": "RevealUI Studio <founder@revealui.com>",
47
+ "bugs": {
48
+ "url": "https://github.com/RevealUIStudio/revealui/issues"
49
+ },
50
+ "keywords": [
51
+ "revealui",
52
+ "cache",
53
+ "cdn",
54
+ "edge-cache",
55
+ "isr",
56
+ "revalidation"
57
+ ],
41
58
  "scripts": {
42
59
  "build": "tsup",
43
60
  "clean": "rm -rf dist",
44
61
  "dev": "tsup --watch",
45
62
  "lint": "biome check .",
46
63
  "lint:fix": "biome check --write .",
47
- "test": "vitest run --passWithNoTests",
48
- "test:coverage": "vitest run --coverage --coverage.reporter=json-summary --coverage.reporter=html --coverage.reporter=text --passWithNoTests",
64
+ "test": "vitest run",
65
+ "test:coverage": "vitest run --coverage --coverage.reporter=json-summary --coverage.reporter=html --coverage.reporter=text",
49
66
  "test:watch": "vitest",
50
67
  "typecheck": "tsc --noEmit"
51
68
  }