@storix-js/persist 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 ADDED
@@ -0,0 +1,24 @@
1
+ # @storix-js/persist
2
+
3
+ Persistence plugin for Storix with `localStorage` and `indexedDB` backends.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @storix-js/core @storix-js/persist
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { createStorix } from '@storix-js/core';
15
+ import { persistPlugin } from '@storix-js/persist';
16
+
17
+ const storix = createStorix({
18
+ plugins: [
19
+ persistPlugin({ storage: 'localStorage', debounce: 100 })
20
+ ]
21
+ });
22
+ ```
23
+
24
+ Use `storage: 'indexedDB'` for larger or more durable client-side state.
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "@storix-js/persist",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
8
15
  "exports": {
9
16
  ".": {
10
17
  "types": "./dist/index.d.ts",
@@ -19,6 +26,6 @@
19
26
  "typecheck": "tsc -p tsconfig.json --noEmit"
20
27
  },
21
28
  "dependencies": {
22
- "@storix-js/core": "workspace:*"
29
+ "@storix-js/core": "^0.1.1"
23
30
  }
24
31
  }
package/dist/index.d.ts DELETED
@@ -1,17 +0,0 @@
1
- type PersistStorage = 'localStorage' | 'indexedDB';
2
- export interface IndexedDBPersistOptions {
3
- dbName?: string;
4
- storeName?: string;
5
- }
6
- export interface PersistPluginOptions {
7
- keyPrefix?: string;
8
- include?: string[];
9
- storage?: PersistStorage;
10
- serialize?: (value: Record<string, unknown>) => string;
11
- deserialize?: (raw: string) => Record<string, unknown>;
12
- debounce?: number;
13
- indexedDB?: IndexedDBPersistOptions;
14
- }
15
- export declare function persistPlugin(options?: PersistPluginOptions): import("@storix-js/core").StorePlugin;
16
- export {};
17
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,KAAK,cAAc,GAAG,cAAc,GAAG,WAAW,CAAC;AAEnD,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC;IACvD,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,uBAAuB,CAAC;CACrC;AASD,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,yCAuL/D"}
package/src/index.ts DELETED
@@ -1,210 +0,0 @@
1
- import { definePlugin } from '@storix-js/core';
2
-
3
- type PersistStorage = 'localStorage' | 'indexedDB';
4
-
5
- export interface IndexedDBPersistOptions {
6
- dbName?: string;
7
- storeName?: string;
8
- }
9
-
10
- export interface PersistPluginOptions {
11
- keyPrefix?: string;
12
- include?: string[];
13
- storage?: PersistStorage;
14
- serialize?: (value: Record<string, unknown>) => string;
15
- deserialize?: (raw: string) => Record<string, unknown>;
16
- debounce?: number;
17
- indexedDB?: IndexedDBPersistOptions;
18
- }
19
-
20
- type ScopedStore = {
21
- $id: string;
22
- $namespace: string;
23
- $scopeId: string;
24
- $patch: (patch: Record<string, unknown>, origin?: 'local' | 'remote') => void;
25
- };
26
-
27
- export function persistPlugin(options: PersistPluginOptions = {}) {
28
- const keyPrefix = options.keyPrefix ?? 'storix';
29
- const include = new Set(options.include ?? []);
30
- const storage = options.storage ?? 'localStorage';
31
- const serialize = options.serialize ?? JSON.stringify;
32
- const deserialize = options.deserialize ?? ((raw: string) => JSON.parse(raw) as Record<string, unknown>);
33
- const debounceMs = options.debounce ?? 100;
34
- const idbDatabaseName = options.indexedDB?.dbName ?? `${keyPrefix}-persist`;
35
- const idbStoreName = options.indexedDB?.storeName ?? 'stores';
36
-
37
- const cache = new Map<string, Record<string, unknown>>();
38
- const timers = new Map<string, ReturnType<typeof setTimeout>>();
39
-
40
- let dbPromise: Promise<IDBDatabase> | null = null;
41
-
42
- const shouldPersistStore = (store: ScopedStore) => {
43
- const ids = [store.$id, store.$scopeId];
44
- if (include.size > 0 && !ids.some((value) => include.has(value))) return false;
45
- return true;
46
- };
47
-
48
- const getKey = (store: ScopedStore) => `${keyPrefix}:${store.$namespace}:${store.$id}`;
49
-
50
- const getDb = () => {
51
- if (dbPromise) return dbPromise;
52
-
53
- dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
54
- const request = window.indexedDB.open(idbDatabaseName, 1);
55
- request.onupgradeneeded = () => {
56
- const db = request.result;
57
- if (!db.objectStoreNames.contains(idbStoreName)) {
58
- db.createObjectStore(idbStoreName);
59
- }
60
- };
61
- request.onsuccess = () => resolve(request.result);
62
- request.onerror = () => reject(request.error ?? new Error('IndexedDB open failed'));
63
- });
64
-
65
- return dbPromise;
66
- };
67
-
68
- const readFromIndexedDb = async (store: ScopedStore) => {
69
- const db = await getDb();
70
- const key = getKey(store);
71
-
72
- return new Promise<Record<string, unknown> | null>((resolve, reject) => {
73
- const tx = db.transaction(idbStoreName, 'readonly');
74
- const req = tx.objectStore(idbStoreName).get(key);
75
-
76
- req.onsuccess = () => {
77
- const result = req.result;
78
- if (typeof result !== 'string') {
79
- resolve(null);
80
- return;
81
- }
82
-
83
- try {
84
- resolve(deserialize(result));
85
- } catch {
86
- resolve(null);
87
- }
88
- };
89
-
90
- req.onerror = () => reject(req.error ?? new Error('IndexedDB read failed'));
91
- });
92
- };
93
-
94
- const writeToIndexedDb = async (store: ScopedStore, value: Record<string, unknown>) => {
95
- const db = await getDb();
96
- const key = getKey(store);
97
- const raw = serialize(value);
98
-
99
- return new Promise<void>((resolve, reject) => {
100
- const tx = db.transaction(idbStoreName, 'readwrite');
101
- tx.objectStore(idbStoreName).put(raw, key);
102
-
103
- tx.oncomplete = () => resolve();
104
- tx.onerror = () => reject(tx.error ?? new Error('IndexedDB write failed'));
105
- tx.onabort = () => reject(tx.error ?? new Error('IndexedDB write aborted'));
106
- });
107
- };
108
-
109
- const readSnapshot = async (store: ScopedStore) => {
110
- if (typeof window === 'undefined') return null;
111
-
112
- if (storage === 'indexedDB' && 'indexedDB' in window) {
113
- try {
114
- return await readFromIndexedDb(store);
115
- } catch {
116
- return null;
117
- }
118
- }
119
-
120
- try {
121
- const raw = window.localStorage.getItem(getKey(store));
122
- if (!raw) return null;
123
- return deserialize(raw);
124
- } catch {
125
- return null;
126
- }
127
- };
128
-
129
- const writeSnapshot = (store: ScopedStore) => {
130
- if (typeof window === 'undefined') return;
131
-
132
- const snapshot = cache.get(store.$scopeId);
133
- if (!snapshot) return;
134
-
135
- if (storage === 'indexedDB' && 'indexedDB' in window) {
136
- void writeToIndexedDb(store, snapshot).catch(() => {
137
- // noop
138
- });
139
- return;
140
- }
141
-
142
- try {
143
- window.localStorage.setItem(getKey(store), serialize(snapshot));
144
- } catch {
145
- // noop
146
- }
147
- };
148
-
149
- const scheduleWrite = (store: ScopedStore) => {
150
- const timerKey = store.$scopeId;
151
- const existing = timers.get(timerKey);
152
- if (existing) {
153
- clearTimeout(existing);
154
- timers.delete(timerKey);
155
- }
156
-
157
- if (debounceMs <= 0) {
158
- writeSnapshot(store);
159
- return;
160
- }
161
-
162
- const timer = setTimeout(() => {
163
- timers.delete(timerKey);
164
- writeSnapshot(store);
165
- }, debounceMs);
166
-
167
- timers.set(timerKey, timer);
168
- };
169
-
170
- const flush = (store: ScopedStore) => {
171
- const timerKey = store.$scopeId;
172
- const timer = timers.get(timerKey);
173
- if (timer) {
174
- clearTimeout(timer);
175
- timers.delete(timerKey);
176
- }
177
- writeSnapshot(store);
178
- };
179
-
180
- return definePlugin({
181
- name: 'persist',
182
- onInit(store) {
183
- const scopedStore = store as ScopedStore;
184
- if (typeof window === 'undefined') return;
185
- if (!shouldPersistStore(scopedStore)) return;
186
-
187
- void (async () => {
188
- const snapshot = await readSnapshot(scopedStore);
189
- if (!snapshot) return;
190
- cache.set(scopedStore.$scopeId, snapshot);
191
- scopedStore.$patch(snapshot, 'remote');
192
- })();
193
- },
194
- onMutation(store, mutation) {
195
- const scopedStore = store as ScopedStore;
196
- if (typeof window === 'undefined') return;
197
- if (!shouldPersistStore(scopedStore)) return;
198
-
199
- const snapshot = cache.get(scopedStore.$scopeId) ?? {};
200
- snapshot[mutation.key] = mutation.newValue;
201
- cache.set(scopedStore.$scopeId, snapshot);
202
- scheduleWrite(scopedStore);
203
- },
204
- onDispose(store) {
205
- const scopedStore = store as ScopedStore;
206
- if (!shouldPersistStore(scopedStore)) return;
207
- flush(scopedStore);
208
- }
209
- });
210
- }
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "src",
5
- "outDir": "dist",
6
- "paths": {}
7
- }
8
- }
package/tsconfig.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "include": ["src"]
4
- }
package/vite.config.ts DELETED
@@ -1,15 +0,0 @@
1
- import { defineConfig } from 'vite';
2
-
3
- export default defineConfig({
4
- build: {
5
- lib: {
6
- entry: 'src/index.ts',
7
- name: 'StorixPersist',
8
- fileName: 'index',
9
- formats: ['es', 'cjs']
10
- }
11
- },
12
- test: {
13
- environment: 'node'
14
- }
15
- });