Initial Commit
This commit is contained in:
177
openspec/changes/enterprise-browser-cache-browser/proposal.md
Normal file
177
openspec/changes/enterprise-browser-cache-browser/proposal.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# OpenSpec Change: enterprise-browser-cache-browser
|
||||
|
||||
- **Change ID**: enterprise-browser-cache-browser
|
||||
- **Package**: `@suaveplan/cache-browser`
|
||||
- **Category**: browser
|
||||
- **Target path**: `browser/storage/cache-browser`
|
||||
- **Status**: IN PROGRESS
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/cache-browser.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/cache-browser` on the genesis branch at enterprise quality. The package provides browser-side cache implementations (`LocalStorageCache`, `SessionStorageCache`, `IndexedDBCache`) plugging into `@suaveplan/cache`, plus quota utilities, pluggable serializers, cross-tab invalidation via `@suaveplan/broadcast-channel`, pluggable eviction strategies, and OTel metrics. Auto-registration contract and `sideEffects` declaration are aligned.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
Fresh build — does not exist on genesis yet.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/cache` (`workspace:^`) | `Cache` interface, `CacheFactory` |
|
||||
| `@suaveplan/events` (`workspace:^`) | Storage-event multiplexer |
|
||||
| `@suaveplan/error` (`workspace:^`) | Typed cache errors |
|
||||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike` |
|
||||
| `@suaveplan/broadcast-channel` (`workspace:^`) | Cross-tab invalidation |
|
||||
| `@suaveplan/crypto` (`workspace:^`) | Optional AES-GCM encryption transform |
|
||||
| `@suaveplan/schema-registry` (`workspace:^`) | Register cache config schemas |
|
||||
| `zod` (`catalog:forms`) | Config schemas |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Cache hit/miss/eviction metrics |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
Dev: `vitest`, `@vitest/coverage-istanbul`, `fake-indexeddb` (`catalog:testing`), `@playwright/test`, `@suaveplan/testing`.
|
||||
|
||||
---
|
||||
|
||||
## Preserved public API surface
|
||||
|
||||
```ts
|
||||
// Three cache implementations
|
||||
class LocalStorageCache implements Cache { constructor(config: LocalStorageCacheConfig) }
|
||||
class SessionStorageCache implements Cache { constructor(config: SessionStorageCacheConfig) }
|
||||
class IndexedDBCache implements Cache { constructor(config: IndexedDBCacheConfig) }
|
||||
|
||||
// Subpath exports
|
||||
// @suaveplan/cache-browser/localstorage
|
||||
// @suaveplan/cache-browser/sessionstorage
|
||||
// @suaveplan/cache-browser/indexeddb
|
||||
// @suaveplan/cache-browser/register (explicit auto-registration)
|
||||
|
||||
// Quota utilities
|
||||
function getStorageQuota(): Promise<StorageQuota>
|
||||
function isStorageNearQuota(threshold?: number): Promise<boolean>
|
||||
function estimateStorageSize(): Promise<number>
|
||||
function handleQuotaExceeded(cache: Cache, strategy: EvictionStrategy): Promise<void>
|
||||
function isStorageAvailable(type: 'localStorage' | 'sessionStorage'): boolean
|
||||
|
||||
// Serialization
|
||||
function defaultSerializer<T>(value: T): string
|
||||
function defaultDeserializer<T>(raw: string): T
|
||||
function isSerializable(value: unknown): boolean
|
||||
function safeDeserialize<T>(raw: string): T | undefined
|
||||
|
||||
// Storage events
|
||||
class StorageEventHandler { ... }
|
||||
const globalStorageEventHandler: StorageEventHandler
|
||||
|
||||
// Config types
|
||||
interface LocalStorageCacheConfig { namespace: string; ttl?: number; serializer?: Serializer<unknown> }
|
||||
interface SessionStorageCacheConfig { namespace: string; ttl?: number; serializer?: Serializer<unknown> }
|
||||
interface IndexedDBCacheConfig { dbName: string; storeName: string; ttl?: number; schemaVersion?: number; serializer?: Serializer<unknown> }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
### 1. `sideEffects` contract
|
||||
|
||||
Auto-registration moves to explicit `@suaveplan/cache-browser/register` subpath; `"sideEffects": false` is then accurate for all other entry points. The `register` subpath is a `sideEffects: ["./dist/register.js"]` entry.
|
||||
|
||||
### 2. Pluggable `Serializer<T>` interface
|
||||
|
||||
```ts
|
||||
interface Serializer<T> { serialize(value: T): string; deserialize(raw: string): T }
|
||||
```
|
||||
|
||||
Default: JSON. Alternatives: `CborSerializer` (via `@msgpack/msgpack`), `StructuredCloneSerializer`.
|
||||
|
||||
### 3. `EvictionStrategy` interface
|
||||
|
||||
```ts
|
||||
interface EvictionStrategy { selectForEviction(entries: CacheEntry[]): CacheEntry[] }
|
||||
class LRUEviction implements EvictionStrategy { ... }
|
||||
class LFUEviction implements EvictionStrategy { ... }
|
||||
class TTLFirstEviction implements EvictionStrategy { ... }
|
||||
```
|
||||
|
||||
### 4. Cross-tab invalidation
|
||||
|
||||
`LocalStorageCache` and `IndexedDBCache` accept an optional `broadcastChannel?: TypedBroadcastChannel` — on write/delete/clear, publishes invalidation events so other tabs update their in-memory views.
|
||||
|
||||
### 5. Split oversized files
|
||||
|
||||
Each implementation splits into: `store.ts` (core CRUD), `ttl.ts` (expiry logic), `quota.ts` (quota guard), `events.ts` (storage-event wiring). All ≤300 lines.
|
||||
|
||||
### 6. Public introspection surface
|
||||
|
||||
`cache.getMetrics(): CacheMetrics` replaces private-property reach-through in tests.
|
||||
|
||||
### 7. Error codes
|
||||
|
||||
`CACHE_QUOTA_EXCEEDED`, `CACHE_SERIALIZATION_FAILED`, `INDEXEDDB_NOT_AVAILABLE`.
|
||||
|
||||
### 8. OTel metrics
|
||||
|
||||
Counter `cache.hits_total{backend}`, counter `cache.misses_total{backend}`, counter `cache.evictions_total{strategy}`, histograms `cache.read_ms`, `cache.write_ms`.
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
browser/storage/cache-browser/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
register.ts (explicit CacheFactory.register calls; sideEffects entry)
|
||||
error-codes.ts
|
||||
indexeddb/
|
||||
store/ (store.ts, store.test.ts, store.md)
|
||||
ttl/ (ttl.ts, ttl.test.ts, ttl.md)
|
||||
quota/ (quota.ts, quota.test.ts, quota.md)
|
||||
events/ (events.ts, events.test.ts, events.md)
|
||||
indexeddb/ (indexeddb.ts — orchestrator, indexeddb.test.ts, indexeddb.md, indexeddb.types.ts)
|
||||
localstorage/
|
||||
store/
|
||||
ttl/
|
||||
quota/
|
||||
events/
|
||||
localstorage/ (localstorage.ts, localstorage.test.ts, localstorage.md, localstorage.types.ts)
|
||||
sessionstorage/
|
||||
store/
|
||||
ttl/
|
||||
sessionstorage/ (sessionstorage.ts, sessionstorage.test.ts, sessionstorage.md, sessionstorage.types.ts)
|
||||
utils/
|
||||
quota/ (getStorageQuota, isStorageNearQuota, estimateStorageSize, handleQuotaExceeded, isStorageAvailable)
|
||||
serialization/ (defaultSerializer/Deserializer, isSerializable, safeDeserialize, Serializer interface)
|
||||
storage-events/(StorageEventHandler, globalStorageEventHandler)
|
||||
eviction/
|
||||
lru-eviction/
|
||||
lfu-eviction/
|
||||
ttl-first-eviction/
|
||||
types/
|
||||
types/ (all shared types)
|
||||
tests/
|
||||
integration.test.ts
|
||||
e2e/
|
||||
cache-browser.spec.ts (Playwright: Chromium/WebKit/Firefox real storage)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Downstream**: `@suaveplan/analytics-browser`, `@suaveplan/pwa`, `@suaveplan/network-discovery-*`, any package using browser caching.
|
||||
|
||||
**Prerequisites**: `@suaveplan/cache`, `@suaveplan/events`, `@suaveplan/error`, `@suaveplan/broadcast-channel` on Verdaccio.
|
||||
116
openspec/changes/enterprise-browser-cache-browser/tasks.md
Normal file
116
openspec/changes/enterprise-browser-cache-browser/tasks.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Tasks: enterprise-browser-cache-browser
|
||||
|
||||
**Change ID**: enterprise-browser-cache-browser
|
||||
**Package**: `@suaveplan/cache-browser`
|
||||
**Target path**: `browser/storage/cache-browser`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/cache`, `@suaveplan/events`, `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/broadcast-channel`
|
||||
|
||||
---
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [ ] Create directory tree per `proposal.md`.
|
||||
- [ ] `package.json` v0.0.0:
|
||||
- `"sideEffects": ["./dist/register.js"]` — only the register entry has side effects
|
||||
- Subpath exports: `"."`, `"./localstorage"`, `"./sessionstorage"`, `"./indexeddb"`, `"./register"`, `"./types"`
|
||||
- `dependencies`: `@suaveplan/cache workspace:^`, `@suaveplan/events workspace:^`, `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/broadcast-channel workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`
|
||||
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fake-indexeddb catalog:testing`, `@playwright/test catalog:testing`, `@suaveplan/testing workspace:^`
|
||||
- [ ] `tsconfig.json`, `vitest.config.ts` (pool: forks, `fileParallelism: false` for browser tests).
|
||||
|
||||
---
|
||||
|
||||
## 2. Error codes
|
||||
|
||||
- [ ] `CACHE_QUOTA_EXCEEDED`, `CACHE_SERIALIZATION_FAILED`, `INDEXEDDB_NOT_AVAILABLE`.
|
||||
- [ ] `registerCacheBrowserErrorCodes()` — call eagerly.
|
||||
|
||||
---
|
||||
|
||||
## 3. Serializer + eviction interfaces (`src/utils/`)
|
||||
|
||||
- [ ] `serialization/`: `Serializer<T>` interface, `defaultSerializer`, `defaultDeserializer`, `isSerializable`, `safeDeserialize`. Tests + md.
|
||||
- [ ] `quota/`: quota utils. Tests + md.
|
||||
- [ ] `storage-events/`: `StorageEventHandler`. Tests + md.
|
||||
- [ ] `src/eviction/lru-eviction/`, `lfu-eviction/`, `ttl-first-eviction/`: each implements `EvictionStrategy`. Tests + md.
|
||||
|
||||
---
|
||||
|
||||
## 4. IndexedDB cache (`src/indexeddb/`)
|
||||
|
||||
Split into four sub-modules ≤300 lines each:
|
||||
|
||||
- [ ] `store/store.ts`: CRUD via IDB transactions, `get`/`set`/`delete`/`clear`/`list`.
|
||||
- [ ] `ttl/ttl.ts`: `expiresAt` index, proactive sweep via `requestIdleCallback`.
|
||||
- [ ] `quota/quota.ts`: quota check before write; `CACHE_QUOTA_EXCEEDED` on fail.
|
||||
- [ ] `events/events.ts`: broadcast-channel invalidation hooks.
|
||||
- [ ] `indexeddb/indexeddb.ts`: orchestrator composing above.
|
||||
- [ ] `getMetrics()`: returns `CacheMetrics` (size, hitCount, missCount, evictionCount) — no private-property access in tests.
|
||||
- [ ] Tests: CRUD, TTL expiry, quota guard, cross-tab invalidation (via broadcast-channel), batch ops. All test files ≤500 lines.
|
||||
- [ ] Write `.md` per module.
|
||||
|
||||
---
|
||||
|
||||
## 5. LocalStorage cache (`src/localstorage/`)
|
||||
|
||||
Split similarly: `store/`, `ttl/`, `quota/`, `events/`, `localstorage/`.
|
||||
|
||||
- [ ] Cross-tab invalidation via `StorageEventHandler` + optional `broadcastChannel`.
|
||||
- [ ] `getMetrics()` introspection.
|
||||
- [ ] Tests ≤500 lines. Write `.md` per module.
|
||||
|
||||
---
|
||||
|
||||
## 6. SessionStorage cache (`src/sessionstorage/`)
|
||||
|
||||
Split: `store/`, `ttl/`, `sessionstorage/`.
|
||||
|
||||
- [ ] `getMetrics()` introspection.
|
||||
- [ ] Tests ≤500 lines. Write `.md` per module.
|
||||
|
||||
---
|
||||
|
||||
## 7. Register entry (`src/register.ts`)
|
||||
|
||||
- [ ] Call `CacheFactory.register('localStorage', () => new LocalStorageCache(...))` etc.
|
||||
- [ ] This file is the only side-effect entry; matches `sideEffects` array.
|
||||
|
||||
---
|
||||
|
||||
## 8. OTel metrics
|
||||
|
||||
- [ ] Wire counters + histograms in each cache implementation's CRUD methods.
|
||||
|
||||
---
|
||||
|
||||
## 9. Playwright E2E (`e2e/cache-browser.spec.ts`)
|
||||
|
||||
- [ ] Three browser contexts (Chromium, WebKit, Firefox via Playwright).
|
||||
- [ ] Assert: IndexedDB CRUD, LocalStorage TTL, cross-tab invalidation (two tabs, one writes, other reads updated value), quota-exceeded eviction.
|
||||
|
||||
---
|
||||
|
||||
## 10. Integration test (`tests/integration.test.ts`)
|
||||
|
||||
- [ ] All three caches via the same `Cache` interface.
|
||||
- [ ] Eviction strategies (LRU, LFU, TTL-first) under simulated quota pressure.
|
||||
- [ ] Serializer swap: replace JSON with a custom `Serializer<User>`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Docs + verify
|
||||
|
||||
- [ ] Write `.md` per module (≥200 words, API reference, examples).
|
||||
- [ ] Write `README.md` with subpath table, serializer guide, eviction strategy table, cross-tab section.
|
||||
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/cache-browser` — green.
|
||||
- [ ] `bunx turbo test:coverage` — 100%.
|
||||
- [ ] No `@ts-expect-error` in `src/` or tests.
|
||||
- [ ] `sideEffects` declaration correctly reflects only `register.ts`.
|
||||
- [ ] Playwright E2E green on Chromium, WebKit, Firefox.
|
||||
- [ ] `bun scripts/publish.ts --package @suaveplan/cache-browser`.
|
||||
- [ ] Archive to `openspec/archive/2026-04-24-enterprise-browser-cache-browser/`.
|
||||
Reference in New Issue
Block a user