@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 +60 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/readme.body.md +44 -1
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
|
-
|
|
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
|
-
|
|
8
|
+
[](https://www.npmjs.com/package/@cascivo/storage)
|
|
9
|
+
[](https://www.npmjs.com/package/@cascivo/storage)
|
|
10
|
+
[](https://github.com/cascivo/cascivo/blob/main/LICENSE)
|
|
11
|
+

|
|
6
12
|
|
|
7
|
-
[cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/
|
|
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
|
-
|
|
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/
|
|
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>
|
package/dist/index.mjs.map
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Persisted signals over localStorage/IndexedDB for
|
|
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/
|
|
17
|
-
"bugs": "https://github.com/
|
|
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/
|
|
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.
|
|
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
|
|
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
|
|
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.
|