@cascivo/storage 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 urbanisierung
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ <!-- generated by scripts/readme/generate.ts — edit readme.body.md, not this file -->
2
+
3
+ # @cascivo/storage
4
+
5
+ > Persisted signals over localStorage/IndexedDB for cascade — SSR-safe
6
+
7
+ [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/urbanisierung/cascivo)
8
+
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.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pnpm add @cascivo/storage
15
+ ```
16
+
17
+ ---
18
+
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
@@ -0,0 +1,31 @@
1
+ import { ReadonlySignal, Signal } from "@cascivo/core";
2
+
3
+ //#region src/drivers.d.ts
4
+ interface StorageDriver {
5
+ get(key: string): string | null | Promise<string | null>;
6
+ set(key: string, value: string): void;
7
+ remove(key: string): void;
8
+ /** Optional change feed (e.g. the cross-tab `storage` event). */
9
+ subscribe?(key: string, onChange: (raw: string | null) => void): () => void;
10
+ }
11
+ declare function memoryDriver(): StorageDriver;
12
+ declare function localStorageDriver(): StorageDriver;
13
+ //#endregion
14
+ //#region src/persisted-signal.d.ts
15
+ interface PersistedSignalOptions<T> {
16
+ driver?: StorageDriver;
17
+ /** Bump together with `migrate` when the stored shape changes. */
18
+ version?: number;
19
+ migrate?: (value: unknown, fromVersion: number) => T;
20
+ }
21
+ type PersistedSignal<T> = Signal<T> & {
22
+ ready: ReadonlySignal<boolean>;
23
+ };
24
+ declare function persistedSignal<T>(key: string, initial: T, options?: PersistedSignalOptions<T>): PersistedSignal<T>;
25
+ //#endregion
26
+ //#region src/indexed-db.d.ts
27
+ /** Minimal key-value driver over indexedDB — async hydration, no `idb` dependency. */
28
+ declare function indexedDBDriver(dbName?: string, storeName?: string): StorageDriver;
29
+ //#endregion
30
+ export { type PersistedSignal, type PersistedSignalOptions, type StorageDriver, indexedDBDriver, localStorageDriver, memoryDriver, persistedSignal };
31
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/drivers.ts","../src/persisted-signal.ts","../src/indexed-db.ts"],"mappings":";;;UAAiB,aAAA;EACf,GAAA,CAAI,GAAA,2BAA8B,OAAO;EACzC,GAAA,CAAI,GAAA,UAAa,KAAA;EACjB,MAAA,CAAO,GAAA;EAHqB;EAK5B,SAAA,EAAW,GAAA,UAAa,QAAA,GAAW,GAAA;AAAA;AAAA,iBAGrB,YAAA,IAAgB,aAAa;AAAA,iBAa7B,kBAAA,IAAsB,aAAa;;;UCZlC,sBAAA;EACf,MAAA,GAAS,aAAA;EDVmB;ECY5B,OAAA;EACA,OAAA,IAAW,KAAA,WAAgB,WAAA,aAAwB,CAAC;AAAA;AAAA,KAG1C,eAAA,MAAqB,MAAA,CAAO,CAAA;EAAO,KAAA,EAAO,cAAA;AAAA;AAAA,iBAEtC,eAAA,IACd,GAAA,UACA,OAAA,EAAS,CAAA,EACT,OAAA,GAAS,sBAAA,CAAuB,CAAA,IAC/B,eAAA,CAAgB,CAAA;;;;iBCnBH,eAAA,CAAgB,MAAA,WAAoB,SAAA,YAAmB,aAAa"}
package/dist/index.mjs ADDED
@@ -0,0 +1,129 @@
1
+ import { effect, signal } from "@cascivo/core";
2
+ //#region src/drivers.ts
3
+ function memoryDriver() {
4
+ const store = /* @__PURE__ */ new Map();
5
+ return {
6
+ get: (key) => store.get(key) ?? null,
7
+ set: (key, value) => {
8
+ store.set(key, value);
9
+ },
10
+ remove: (key) => {
11
+ store.delete(key);
12
+ }
13
+ };
14
+ }
15
+ function localStorageDriver() {
16
+ if (typeof window === "undefined") return memoryDriver();
17
+ return {
18
+ get: (key) => window.localStorage.getItem(key),
19
+ set: (key, value) => {
20
+ try {
21
+ window.localStorage.setItem(key, value);
22
+ } catch {}
23
+ },
24
+ remove: (key) => {
25
+ window.localStorage.removeItem(key);
26
+ },
27
+ subscribe(key, onChange) {
28
+ const listener = (event) => {
29
+ if (event.storageArea === window.localStorage && event.key === key) onChange(event.newValue);
30
+ };
31
+ window.addEventListener("storage", listener);
32
+ return () => window.removeEventListener("storage", listener);
33
+ }
34
+ };
35
+ }
36
+ //#endregion
37
+ //#region src/persisted-signal.ts
38
+ function persistedSignal(key, initial, options = {}) {
39
+ const driver = options.driver ?? localStorageDriver();
40
+ const version = options.version ?? 1;
41
+ const value = signal(initial);
42
+ const ready = signal(false);
43
+ let adopting = false;
44
+ function decode(raw) {
45
+ if (raw === null) return void 0;
46
+ try {
47
+ const envelope = JSON.parse(raw);
48
+ if (typeof envelope !== "object" || envelope === null || typeof envelope.v !== "number") return;
49
+ if (envelope.v !== version) {
50
+ if (!options.migrate) return void 0;
51
+ const migrated = options.migrate(envelope.value, envelope.v);
52
+ driver.set(key, JSON.stringify({
53
+ v: version,
54
+ value: migrated
55
+ }));
56
+ return migrated;
57
+ }
58
+ return envelope.value;
59
+ } catch {
60
+ return;
61
+ }
62
+ }
63
+ function adopt(raw) {
64
+ const decoded = decode(raw);
65
+ if (decoded !== void 0) {
66
+ adopting = true;
67
+ value.value = decoded;
68
+ adopting = false;
69
+ }
70
+ }
71
+ const raw = driver.get(key);
72
+ if (raw instanceof Promise) raw.then((resolved) => {
73
+ adopt(resolved);
74
+ ready.value = true;
75
+ });
76
+ else {
77
+ adopt(raw);
78
+ ready.value = true;
79
+ }
80
+ effect(() => {
81
+ const next = value.value;
82
+ if (!ready.value || adopting) return;
83
+ driver.set(key, JSON.stringify({
84
+ v: version,
85
+ value: next
86
+ }));
87
+ });
88
+ driver.subscribe?.(key, adopt);
89
+ const persisted = value;
90
+ persisted.ready = ready;
91
+ return persisted;
92
+ }
93
+ //#endregion
94
+ //#region src/indexed-db.ts
95
+ /** Minimal key-value driver over indexedDB — async hydration, no `idb` dependency. */
96
+ function indexedDBDriver(dbName = "cascivo", storeName = "kv") {
97
+ if (typeof indexedDB === "undefined") return memoryDriver();
98
+ let dbPromise;
99
+ function open() {
100
+ dbPromise ??= new Promise((resolve, reject) => {
101
+ const request = indexedDB.open(dbName, 1);
102
+ request.onupgradeneeded = () => request.result.createObjectStore(storeName);
103
+ request.onsuccess = () => resolve(request.result);
104
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error(`indexedDB open failed: ${dbName}`));
105
+ });
106
+ return dbPromise;
107
+ }
108
+ async function withStore(mode, run) {
109
+ const db = await open();
110
+ return new Promise((resolve, reject) => {
111
+ const request = run(db.transaction(storeName, mode).objectStore(storeName));
112
+ request.onsuccess = () => resolve(request.result);
113
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("indexedDB request failed"));
114
+ });
115
+ }
116
+ return {
117
+ get: async (key) => await withStore("readonly", (store) => store.get(key)) ?? null,
118
+ set: (key, value) => {
119
+ withStore("readwrite", (store) => store.put(value, key));
120
+ },
121
+ remove: (key) => {
122
+ withStore("readwrite", (store) => store.delete(key));
123
+ }
124
+ };
125
+ }
126
+ //#endregion
127
+ export { indexedDBDriver, localStorageDriver, memoryDriver, persistedSignal };
128
+
129
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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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"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@cascivo/storage",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Persisted signals over localStorage/IndexedDB for cascade — SSR-safe",
6
+ "keywords": [
7
+ "cascivo",
8
+ "css",
9
+ "design-system",
10
+ "indexeddb",
11
+ "localstorage",
12
+ "react",
13
+ "signals",
14
+ "storage"
15
+ ],
16
+ "homepage": "https://github.com/urbanisierung/cascivo/tree/main/packages/storage#readme",
17
+ "bugs": "https://github.com/urbanisierung/cascivo/issues",
18
+ "license": "MIT",
19
+ "author": "urbanisierung",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/urbanisierung/cascivo.git",
23
+ "directory": "packages/storage"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "types": "./dist/index.d.mts",
30
+ "exports": {
31
+ ".": {
32
+ "import": "./dist/index.mjs",
33
+ "types": "./dist/index.d.mts",
34
+ "default": "./dist/index.mjs"
35
+ }
36
+ },
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "provenance": true
40
+ },
41
+ "dependencies": {
42
+ "@cascivo/core": "^0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@preact/signals-react": "^3",
46
+ "jsdom": "^29",
47
+ "typescript": "^5",
48
+ "vite-plus": "^0.1.24"
49
+ },
50
+ "peerDependencies": {
51
+ "@preact/signals-react": ">=2.0.0"
52
+ },
53
+ "scripts": {
54
+ "build": "vp pack",
55
+ "check": "tsc --noEmit",
56
+ "test": "vp test",
57
+ "dev": "vp pack --watch"
58
+ }
59
+ }
package/readme.body.md ADDED
@@ -0,0 +1 @@
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.