@cascivo/storage 0.1.0 → 0.1.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/README.md CHANGED
@@ -1,12 +1,65 @@
1
1
  <!-- generated by scripts/readme/generate.ts — edit readme.body.md, not this file -->
2
2
 
3
- # @cascivo/storage
3
+ <div align="center">
4
+ <a href="https://cascivo.com"><img src="https://cascivo.com/favicon.svg" width="72" height="72" alt="cascivo logo"></a>
5
+ <h1>@cascivo/storage</h1>
6
+ <p><strong>Persisted signals over localStorage/IndexedDB for cascivo — SSR-safe</strong></p>
4
7
 
5
- > Persisted signals over localStorage/IndexedDB for cascade — SSR-safe
8
+ [![npm](https://img.shields.io/npm/v/%40cascivo%2Fstorage?style=flat-square&color=0079bf)](https://www.npmjs.com/package/@cascivo/storage)
9
+ [![downloads](https://img.shields.io/npm/dm/%40cascivo%2Fstorage?style=flat-square&color=0079bf)](https://www.npmjs.com/package/@cascivo/storage)
10
+ [![license](https://img.shields.io/npm/l/%40cascivo%2Fstorage?style=flat-square&color=0079bf)](https://github.com/cascivo/cascivo/blob/main/LICENSE)
11
+ ![types](https://img.shields.io/badge/types-included-0079bf?style=flat-square&logo=typescript&logoColor=white)
6
12
 
7
- [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/urbanisierung/cascivo)
13
+ [npm](https://www.npmjs.com/package/@cascivo/storage) · [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/cascivo/cascivo)
8
14
 
9
- Persisted signals for cascade — sync signal state to `localStorage` or `IndexedDB` with SSR-safe initialisation. Drop-in replacement for plain signals when persistence is needed.
15
+ </div>
16
+
17
+ ---
18
+
19
+ Persisted signals for cascivo — sync signal state to `localStorage` or `IndexedDB` with SSR-safe initialisation. A drop-in replacement for a plain signal whenever the value should survive a reload.
20
+
21
+ ## Usage
22
+
23
+ ```tsx
24
+ import { persistedSignal } from '@cascivo/storage'
25
+
26
+ // reads the stored value on the client, falls back to the default on the server
27
+ const theme = persistedSignal('theme', 'light')
28
+
29
+ theme.value = 'dark' // written through to storage automatically
30
+ ```
31
+
32
+ Use it exactly like a signal — read `.value`, assign `.value`, derive with `useComputed`. Writes are envelope-encoded (with a version stamp) and persisted for you.
33
+
34
+ ## SSR safety
35
+
36
+ On the server there is no storage, so `persistedSignal` returns the default and adopts the stored value on the client without a hydration mismatch. The returned signal carries a `ready` signal you can read to know when async drivers have hydrated:
37
+
38
+ ```ts
39
+ const draft = persistedSignal('editor-draft', '')
40
+ draft.ready.value // false until the driver has loaded
41
+ ```
42
+
43
+ ## Drivers & migrations
44
+
45
+ Choose where values live, and migrate old shapes safely when they change:
46
+
47
+ ```ts
48
+ import { persistedSignal } from '@cascivo/storage'
49
+ import { indexedDBDriver } from '@cascivo/storage'
50
+
51
+ const prefs = persistedSignal(
52
+ 'prefs',
53
+ { density: 'comfortable' },
54
+ {
55
+ driver: indexedDBDriver(),
56
+ version: 2,
57
+ migrate: (old) => ({ density: 'comfortable', ...(old as object) }),
58
+ },
59
+ )
60
+ ```
61
+
62
+ Available drivers: `localStorageDriver` (default), `indexedDBDriver`, and `memoryDriver` (tests/SSR). Drivers that support a `subscribe` hook propagate cross-tab updates automatically.
10
63
 
11
64
  ## Install
12
65
 
@@ -16,4 +69,6 @@ pnpm add @cascivo/storage
16
69
 
17
70
  ---
18
71
 
19
- [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/urbanisierung/cascivo) · AI agents: use [`@cascivo/mcp`](https://github.com/urbanisierung/cascivo/tree/main/packages/mcp) and [`registry.json`](https://github.com/urbanisierung/cascivo/blob/main/registry.json) · MIT
72
+ [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/cascivo/cascivo) · AI agents: use [`@cascivo/mcp`](https://github.com/cascivo/cascivo/tree/main/packages/mcp) and [`registry.json`](https://github.com/cascivo/cascivo/blob/main/registry.json) · MIT
73
+
74
+ <div align="center"><a href="https://cascivo.com"><img src="https://cascivo.com/favicon.svg" width="28" height="28" alt="cascivo"></a></div>
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/drivers.ts","../src/persisted-signal.ts","../src/indexed-db.ts"],"sourcesContent":["export interface StorageDriver {\n get(key: string): string | null | Promise<string | null>\n set(key: string, value: string): void\n remove(key: string): void\n /** Optional change feed (e.g. the cross-tab `storage` event). */\n subscribe?(key: string, onChange: (raw: string | null) => void): () => void\n}\n\nexport function memoryDriver(): StorageDriver {\n const store = new Map<string, string>()\n return {\n get: (key) => store.get(key) ?? null,\n set: (key, value) => {\n store.set(key, value)\n },\n remove: (key) => {\n store.delete(key)\n },\n }\n}\n\nexport function localStorageDriver(): StorageDriver {\n // SSR: no window — persistence becomes a no-op, the signal still works.\n if (typeof window === 'undefined') return memoryDriver()\n return {\n get: (key) => window.localStorage.getItem(key),\n set: (key, value) => {\n try {\n window.localStorage.setItem(key, value)\n } catch {\n // Quota exceeded / private mode: persistence is best-effort.\n }\n },\n remove: (key) => {\n window.localStorage.removeItem(key)\n },\n subscribe(key, onChange) {\n const listener = (event: StorageEvent) => {\n if (event.storageArea === window.localStorage && event.key === key) {\n onChange(event.newValue)\n }\n }\n window.addEventListener('storage', listener)\n return () => window.removeEventListener('storage', listener)\n },\n }\n}\n","import { effect, signal } from '@cascivo/core'\nimport type { ReadonlySignal, Signal } from '@cascivo/core'\nimport { localStorageDriver, type StorageDriver } from './drivers'\n\ninterface Envelope {\n v: number\n value: unknown\n}\n\nexport interface PersistedSignalOptions<T> {\n driver?: StorageDriver\n /** Bump together with `migrate` when the stored shape changes. */\n version?: number\n migrate?: (value: unknown, fromVersion: number) => T\n}\n\nexport type PersistedSignal<T> = Signal<T> & { ready: ReadonlySignal<boolean> }\n\nexport function persistedSignal<T>(\n key: string,\n initial: T,\n options: PersistedSignalOptions<T> = {},\n): PersistedSignal<T> {\n const driver = options.driver ?? localStorageDriver()\n const version = options.version ?? 1\n const value = signal(initial)\n const ready = signal(false)\n // Suppresses the write-through effect while adopting a value FROM storage.\n let adopting = false\n\n function decode(raw: string | null): T | undefined {\n if (raw === null) return undefined\n try {\n const envelope = JSON.parse(raw) as Envelope\n if (typeof envelope !== 'object' || envelope === null || typeof envelope.v !== 'number') {\n return undefined\n }\n if (envelope.v !== version) {\n if (!options.migrate) return undefined\n const migrated = options.migrate(envelope.value, envelope.v)\n // Persist the migration immediately so old-format data doesn't linger.\n driver.set(key, JSON.stringify({ v: version, value: migrated } satisfies Envelope))\n return migrated\n }\n return envelope.value as T\n } catch {\n return undefined\n }\n }\n\n function adopt(raw: string | null): void {\n const decoded = decode(raw)\n if (decoded !== undefined) {\n adopting = true\n value.value = decoded\n adopting = false\n }\n }\n\n const raw = driver.get(key)\n if (raw instanceof Promise) {\n void raw.then((resolved) => {\n adopt(resolved)\n ready.value = true\n })\n } else {\n adopt(raw)\n ready.value = true\n }\n\n effect(() => {\n const next = value.value\n if (!ready.value || adopting) return\n driver.set(key, JSON.stringify({ v: version, value: next } satisfies Envelope))\n })\n\n driver.subscribe?.(key, adopt)\n\n const persisted = value as PersistedSignal<T>\n ;(persisted as { ready: ReadonlySignal<boolean> }).ready = ready\n return persisted\n}\n","import { memoryDriver, type StorageDriver } from './drivers'\n\n/** Minimal key-value driver over indexedDB — async hydration, no `idb` dependency. */\nexport function indexedDBDriver(dbName = 'cascivo', storeName = 'kv'): StorageDriver {\n if (typeof indexedDB === 'undefined') return memoryDriver()\n\n let dbPromise: Promise<IDBDatabase> | undefined\n function open(): Promise<IDBDatabase> {\n dbPromise ??= new Promise((resolve, reject) => {\n const request = indexedDB.open(dbName, 1)\n request.onupgradeneeded = () => request.result.createObjectStore(storeName)\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error ?? new Error(`indexedDB open failed: ${dbName}`))\n })\n return dbPromise\n }\n\n async function withStore<T>(\n mode: IDBTransactionMode,\n run: (store: IDBObjectStore) => IDBRequest<T>,\n ): Promise<T> {\n const db = await open()\n return new Promise<T>((resolve, reject) => {\n const request = run(db.transaction(storeName, mode).objectStore(storeName))\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error ?? new Error('indexedDB request failed'))\n })\n }\n\n return {\n get: async (key) =>\n ((await withStore('readonly', (store) => store.get(key))) as string | undefined) ?? null,\n set: (key, value) => {\n void withStore('readwrite', (store) => store.put(value, key))\n },\n remove: (key) => {\n void withStore('readwrite', (store) => store.delete(key))\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/drivers.ts","../src/persisted-signal.ts","../src/indexed-db.ts"],"sourcesContent":["export interface StorageDriver {\n get(key: string): string | null | Promise<string | null>\n set(key: string, value: string): void\n remove(key: string): void\n /** Optional change feed (e.g. the cross-tab `storage` event). */\n subscribe?(key: string, onChange: (raw: string | null) => void): () => void\n}\n\nexport function memoryDriver(): StorageDriver {\n const store = new Map<string, string>()\n return {\n get: (key) => store.get(key) ?? null,\n set: (key, value) => {\n store.set(key, value)\n },\n remove: (key) => {\n store.delete(key)\n },\n }\n}\n\nexport function localStorageDriver(): StorageDriver {\n // SSR: no window — persistence becomes a no-op, the signal still works.\n if (typeof window === 'undefined') return memoryDriver()\n return {\n get: (key) => window.localStorage.getItem(key),\n set: (key, value) => {\n try {\n window.localStorage.setItem(key, value)\n } catch {\n // Quota exceeded / private mode: persistence is best-effort.\n }\n },\n remove: (key) => {\n window.localStorage.removeItem(key)\n },\n subscribe(key, onChange) {\n const listener = (event: StorageEvent) => {\n if (event.storageArea === window.localStorage && event.key === key) {\n onChange(event.newValue)\n }\n }\n window.addEventListener('storage', listener)\n return () => window.removeEventListener('storage', listener)\n },\n }\n}\n","import { effect, signal } from '@cascivo/core'\nimport type { ReadonlySignal, Signal } from '@cascivo/core'\nimport { localStorageDriver, type StorageDriver } from './drivers'\n\ninterface Envelope {\n v: number\n value: unknown\n}\n\nexport interface PersistedSignalOptions<T> {\n driver?: StorageDriver\n /** Bump together with `migrate` when the stored shape changes. */\n version?: number\n migrate?: (value: unknown, fromVersion: number) => T\n}\n\nexport type PersistedSignal<T> = Signal<T> & { ready: ReadonlySignal<boolean> }\n\nexport function persistedSignal<T>(\n key: string,\n initial: T,\n options: PersistedSignalOptions<T> = {},\n): PersistedSignal<T> {\n const driver = options.driver ?? localStorageDriver()\n const version = options.version ?? 1\n const value = signal(initial)\n const ready = signal(false)\n // Suppresses the write-through effect while adopting a value FROM storage.\n let adopting = false\n\n function decode(raw: string | null): T | undefined {\n if (raw === null) return undefined\n try {\n const envelope = JSON.parse(raw) as Envelope\n if (typeof envelope !== 'object' || envelope === null || typeof envelope.v !== 'number') {\n return undefined\n }\n if (envelope.v !== version) {\n if (!options.migrate) return undefined\n const migrated = options.migrate(envelope.value, envelope.v)\n // Persist the migration immediately so old-format data doesn't linger.\n driver.set(key, JSON.stringify({ v: version, value: migrated } satisfies Envelope))\n return migrated\n }\n return envelope.value as T\n } catch {\n return undefined\n }\n }\n\n function adopt(raw: string | null): void {\n const decoded = decode(raw)\n if (decoded !== undefined) {\n adopting = true\n value.value = decoded\n adopting = false\n }\n }\n\n const raw = driver.get(key)\n if (raw instanceof Promise) {\n void raw.then((resolved) => {\n adopt(resolved)\n ready.value = true\n })\n } else {\n adopt(raw)\n ready.value = true\n }\n\n effect(() => {\n const next = value.value\n if (!ready.value || adopting) return\n driver.set(key, JSON.stringify({ v: version, value: next } satisfies Envelope))\n })\n\n driver.subscribe?.(key, adopt)\n\n const persisted = value as PersistedSignal<T>\n ;(persisted as { ready: ReadonlySignal<boolean> }).ready = ready\n return persisted\n}\n","import { memoryDriver, type StorageDriver } from './drivers'\n\n/** Minimal key-value driver over indexedDB — async hydration, no `idb` dependency. */\nexport function indexedDBDriver(dbName = 'cascivo', storeName = 'kv'): StorageDriver {\n if (typeof indexedDB === 'undefined') return memoryDriver()\n\n let dbPromise: Promise<IDBDatabase> | undefined\n function open(): Promise<IDBDatabase> {\n dbPromise ??= new Promise((resolve, reject) => {\n const request = indexedDB.open(dbName, 1)\n request.onupgradeneeded = () => request.result.createObjectStore(storeName)\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error ?? new Error(`indexedDB open failed: ${dbName}`))\n })\n return dbPromise\n }\n\n async function withStore<T>(\n mode: IDBTransactionMode,\n run: (store: IDBObjectStore) => IDBRequest<T>,\n ): Promise<T> {\n const db = await open()\n return new Promise<T>((resolve, reject) => {\n const request = run(db.transaction(storeName, mode).objectStore(storeName))\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error ?? new Error('indexedDB request failed'))\n })\n }\n\n return {\n get: async (key) =>\n ((await withStore('readonly', (store) => store.get(key))) as string | undefined) ?? null,\n set: (key, value) => {\n void withStore('readwrite', (store) => store.put(value, key))\n },\n remove: (key) => {\n void withStore('readwrite', (store) => store.delete(key))\n },\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@cascivo/storage",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
- "description": "Persisted signals over localStorage/IndexedDB for cascade — SSR-safe",
5
+ "description": "Persisted signals over localStorage/IndexedDB for cascivo — SSR-safe",
6
6
  "keywords": [
7
7
  "cascivo",
8
8
  "css",
@@ -13,13 +13,13 @@
13
13
  "signals",
14
14
  "storage"
15
15
  ],
16
- "homepage": "https://github.com/urbanisierung/cascivo/tree/main/packages/storage#readme",
17
- "bugs": "https://github.com/urbanisierung/cascivo/issues",
16
+ "homepage": "https://github.com/cascivo/cascivo/tree/main/packages/storage#readme",
17
+ "bugs": "https://github.com/cascivo/cascivo/issues",
18
18
  "license": "MIT",
19
19
  "author": "urbanisierung",
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "git+https://github.com/urbanisierung/cascivo.git",
22
+ "url": "git+https://github.com/cascivo/cascivo.git",
23
23
  "directory": "packages/storage"
24
24
  },
25
25
  "files": [
@@ -39,13 +39,13 @@
39
39
  "provenance": true
40
40
  },
41
41
  "dependencies": {
42
- "@cascivo/core": "^0.1.0"
42
+ "@cascivo/core": "^0.1.2"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@preact/signals-react": "^3",
46
46
  "jsdom": "^29",
47
47
  "typescript": "^5",
48
- "vite-plus": "^0.1.24"
48
+ "vite-plus": "^0.2.1"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@preact/signals-react": ">=2.0.0"
package/readme.body.md CHANGED
@@ -1 +1,44 @@
1
- Persisted signals for cascade — sync signal state to `localStorage` or `IndexedDB` with SSR-safe initialisation. Drop-in replacement for plain signals when persistence is needed.
1
+ Persisted signals for cascivo — sync signal state to `localStorage` or `IndexedDB` with SSR-safe initialisation. A drop-in replacement for a plain signal whenever the value should survive a reload.
2
+
3
+ ## Usage
4
+
5
+ ```tsx
6
+ import { persistedSignal } from '@cascivo/storage'
7
+
8
+ // reads the stored value on the client, falls back to the default on the server
9
+ const theme = persistedSignal('theme', 'light')
10
+
11
+ theme.value = 'dark' // written through to storage automatically
12
+ ```
13
+
14
+ Use it exactly like a signal — read `.value`, assign `.value`, derive with `useComputed`. Writes are envelope-encoded (with a version stamp) and persisted for you.
15
+
16
+ ## SSR safety
17
+
18
+ On the server there is no storage, so `persistedSignal` returns the default and adopts the stored value on the client without a hydration mismatch. The returned signal carries a `ready` signal you can read to know when async drivers have hydrated:
19
+
20
+ ```ts
21
+ const draft = persistedSignal('editor-draft', '')
22
+ draft.ready.value // false until the driver has loaded
23
+ ```
24
+
25
+ ## Drivers & migrations
26
+
27
+ Choose where values live, and migrate old shapes safely when they change:
28
+
29
+ ```ts
30
+ import { persistedSignal } from '@cascivo/storage'
31
+ import { indexedDBDriver } from '@cascivo/storage'
32
+
33
+ const prefs = persistedSignal(
34
+ 'prefs',
35
+ { density: 'comfortable' },
36
+ {
37
+ driver: indexedDBDriver(),
38
+ version: 2,
39
+ migrate: (old) => ({ density: 'comfortable', ...(old as object) }),
40
+ },
41
+ )
42
+ ```
43
+
44
+ Available drivers: `localStorageDriver` (default), `indexedDBDriver`, and `memoryDriver` (tests/SSR). Drivers that support a `subscribe` hook propagate cross-tab updates automatically.