kvozy 0.2.0 → 0.4.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/README.md CHANGED
@@ -14,13 +14,15 @@ This architecture makes it easy to add connectors for other frameworks (Vue, Sve
14
14
  ## Features
15
15
 
16
16
  - Framework-agnostic core (bindValue)
17
+ - Type-safe generic API with custom serialization/deserialization
17
18
  - Flexible storage support (localStorage, sessionStorage, in-memory)
18
19
  - Graceful fallback to in-memory storage when storage is unavailable
19
20
  - Thin React integration (useStorage)
20
- - String-only values (simple, predictable)
21
21
  - Real localStorage/backend storage
22
22
  - Subscription-based reactivity
23
23
  - TypeScript support
24
+ - Required default values for safety
25
+ - Schema versioning and migration support
24
26
  - Easy to extend to other frameworks
25
27
 
26
28
  ## Installation
@@ -36,14 +38,19 @@ npm install kvozy
36
38
  ```typescript
37
39
  import { bindValue, useStorage } from 'kvozy';
38
40
 
39
- // Define the binding
40
- const myValue = bindValue({ key: 'my-key' });
41
+ // Define the binding with type, serialize, deserialize, and defaultValue
42
+ const myValue = bindValue<string>({
43
+ key: 'my-key',
44
+ defaultValue: '',
45
+ serialize: (v) => v,
46
+ deserialize: (s) => s,
47
+ });
41
48
 
42
49
  // Use in component
43
50
  const Component = () => {
44
51
  const { value, setValue } = useStorage(myValue);
45
52
 
46
- return <input value={value || ''} onChange={(e) => setValue(e.target.value)} />;
53
+ return <input value={value} onChange={(e) => setValue(e.target.value)} />;
47
54
  };
48
55
  ```
49
56
 
@@ -70,23 +77,39 @@ The in-memory storage ensures your application remains functional, maintaining s
70
77
 
71
78
  > ⚠️ **Note:** `bindValue` is an internal API. Use `useStorage` for React components. Direct usage of `bindValue` is not recommended.
72
79
 
73
- ### useStorage(binding)
80
+ ### BindValueOptions<T>
81
+
82
+ Options for creating a BindValue instance.
83
+
84
+ **Parameters:**
85
+
86
+ - `key` (string, required) - Storage key
87
+ - `defaultValue` (T, required) - Default value when key doesn't exist or deserialize fails
88
+ - `serialize` (function, required) - Convert value to string: `(value: T) => string`
89
+ - `deserialize` (function, required) - Convert string to value: `(serialized: string) => T`
90
+ - `storage` (Storage, optional) - localStorage, sessionStorage, or undefined for in-memory
91
+ - `version` (string, optional) - Schema version for migration support
92
+ - `migrate` (function, optional) - Migration function: `(oldSerialized: string, oldVersion: string | undefined) => T`
93
+
94
+ ### useStorage<T>(binding)
74
95
 
75
96
  React hook that connects a BindValue instance to React state.
76
97
 
77
98
  **Parameters:**
78
99
 
79
- - `binding` (BindValue, required) - binding instance from bindValue
100
+ - `binding` (BindValue<T>, required) - binding instance from bindValue
80
101
 
81
102
  **Returns:** `{ value, setValue }`
82
103
 
83
- - `value` - `string | undefined` - current value from localStorage
84
- - `setValue` - `(value: string) => void` - function to update value
104
+ - `value` - `T` - current value from storage
105
+ - `setValue` - `(value: T) => void` - function to update value
85
106
 
86
107
  **Behavior:**
87
108
 
88
109
  - `subscribe()` does NOT call the callback immediately when subscribing
89
110
  - Callbacks are only invoked when the value changes via `set()`
111
+ - If `serialize()` fails, in-memory value is kept but storage is NOT updated
112
+ - If `deserialize()` fails, `defaultValue` is returned
90
113
 
91
114
  **Example:**
92
115
 
@@ -95,7 +118,7 @@ const Component = () => {
95
118
  const { value, setValue } = useStorage(myBinding);
96
119
 
97
120
  return <div>
98
- <p>Current value: {value || '(empty)'}</p>
121
+ <p>Current value: {value}</p>
99
122
  <button onClick={() => setValue('new value')}>
100
123
  Update Value
101
124
  </button>
@@ -110,7 +133,12 @@ const Component = () => {
110
133
  ```typescript
111
134
  import { bindValue, useStorage } from 'kvozy';
112
135
 
113
- const usernameBinding = bindValue({ key: 'username' });
136
+ const usernameBinding = bindValue<string>({
137
+ key: 'username',
138
+ defaultValue: '',
139
+ serialize: (v) => v,
140
+ deserialize: (s) => s,
141
+ });
114
142
 
115
143
  const LoginForm = () => {
116
144
  const { value, setValue } = useStorage(usernameBinding);
@@ -120,7 +148,7 @@ const LoginForm = () => {
120
148
  <label>
121
149
  Username:
122
150
  <input
123
- value={value || ''}
151
+ value={value}
124
152
  onChange={(e) => setValue(e.target.value)}
125
153
  />
126
154
  </label>
@@ -134,7 +162,12 @@ const LoginForm = () => {
134
162
  ```typescript
135
163
  import { bindValue, useStorage } from 'kvozy';
136
164
 
137
- const themeBinding = bindValue({ key: 'theme' });
165
+ const themeBinding = bindValue<string>({
166
+ key: 'theme',
167
+ defaultValue: 'light',
168
+ serialize: (v) => v,
169
+ deserialize: (s) => s,
170
+ });
138
171
 
139
172
  const ThemeToggle = () => {
140
173
  const { value, setValue } = useStorage(themeBinding);
@@ -164,15 +197,20 @@ Both components stay in sync automatically!
164
197
 
165
198
  ### Handling Undefined Values
166
199
 
167
- When a localStorage key doesn't exist, `getValue()` and `useStorage` return `undefined`:
200
+ When a localStorage key doesn't exist, `defaultValue` is returned:
168
201
 
169
202
  ```typescript
170
- const binding = bindValue({ key: 'non-existent-key' });
171
- console.log(binding.getValue()); // undefined
203
+ const binding = bindValue<string>({
204
+ key: 'non-existent-key',
205
+ defaultValue: 'default value',
206
+ serialize: (v) => v,
207
+ deserialize: (s) => s,
208
+ });
209
+ console.log(binding.getValue()); // 'default value'
172
210
 
173
211
  const Component = () => {
174
212
  const { value } = useStorage(binding);
175
- return <div>{value || 'No value set'}</div>;
213
+ return <div>{value}</div>;
176
214
  };
177
215
  ```
178
216
 
@@ -184,17 +222,34 @@ Kvozy supports localStorage, sessionStorage, and in-memory storage:
184
222
  import { bindValue, useStorage } from 'kvozy';
185
223
 
186
224
  // localStorage - persists across browser sessions
187
- const localBinding = bindValue({ key: 'theme', storage: localStorage });
225
+ const localBinding = bindValue<string>({
226
+ key: 'theme',
227
+ defaultValue: 'light',
228
+ serialize: (v) => v,
229
+ deserialize: (s) => s,
230
+ storage: localStorage,
231
+ });
188
232
 
189
233
  // sessionStorage - persists within the same tab
190
- const sessionBinding = bindValue({ key: 'form-data', storage: sessionStorage });
234
+ const sessionBinding = bindValue<string>({
235
+ key: 'form-data',
236
+ defaultValue: '',
237
+ serialize: (v) => v,
238
+ deserialize: (s) => s,
239
+ storage: sessionStorage,
240
+ });
191
241
 
192
242
  // In-memory - no persistence, graceful fallback
193
- const memoryBinding = bindValue({ key: 'temp-state' });
243
+ const memoryBinding = bindValue<string>({
244
+ key: 'temp-state',
245
+ defaultValue: '',
246
+ serialize: (v) => v,
247
+ deserialize: (s) => s,
248
+ });
194
249
 
195
250
  const LocalComponent = () => {
196
251
  const { value, setValue } = useStorage(localBinding);
197
- return <div>Theme: {value || 'light'}</div>;
252
+ return <div>Theme: {value}</div>;
198
253
  };
199
254
 
200
255
  const SessionComponent = () => {
@@ -211,7 +266,12 @@ const MemoryComponent = () => {
211
266
  ### Persisting Form Data
212
267
 
213
268
  ```typescript
214
- const formBinding = bindValue({ key: 'form-data' });
269
+ const formBinding = bindValue<string>({
270
+ key: 'form-data',
271
+ defaultValue: '',
272
+ serialize: (v) => v,
273
+ deserialize: (s) => s,
274
+ });
215
275
 
216
276
  const Form = () => {
217
277
  const { value, setValue } = useStorage(formBinding);
@@ -238,22 +298,144 @@ const Form = () => {
238
298
  ### Counter Example
239
299
 
240
300
  ```typescript
241
- const counterBinding = bindValue({ key: 'counter' });
301
+ const counterBinding = bindValue<number>({
302
+ key: 'counter',
303
+ defaultValue: 0,
304
+ serialize: (v) => String(v),
305
+ deserialize: (s) => Number(s),
306
+ });
307
+
308
+ const Counter = () => {
309
+ const { value, setValue } = useStorage(counterBinding);
310
+
311
+ return (
312
+ <div>
313
+ <p>Count: {value}</p>
314
+ <button onClick={() => setValue(value + 1)}>
315
+ Increment
316
+ </button>
317
+ <button onClick={() => setValue(value - 1)}>
318
+ Decrement
319
+ </button>
320
+ <button onClick={() => setValue(0)}>
321
+ Reset
322
+ </button>
323
+ </div>
324
+ );
325
+ };
326
+ ```
327
+
328
+ ## Schema Versioning and Migration
329
+
330
+ Kvozy supports schema evolution through optional versioning and migration functions. This allows you to safely update your data structure without breaking existing users' stored data.
331
+
332
+ ### Basic Versioning
333
+
334
+ ```typescript
335
+ const userBinding = bindValue<User>({
336
+ key: "user",
337
+ defaultValue: { name: "", age: 0 },
338
+ serialize: (v) => JSON.stringify(v),
339
+ deserialize: (s) => JSON.parse(s),
340
+ version: "1.0.0",
341
+ });
342
+ ```
343
+
344
+ ### Migration Example
345
+
346
+ When you change your data structure, provide a migration function:
347
+
348
+ ```typescript
349
+ // Version 1.0.0: stored as string
350
+ const themeBindingV1 = bindValue<string>({
351
+ key: "theme",
352
+ defaultValue: "light",
353
+ serialize: (v) => v,
354
+ deserialize: (s) => s,
355
+ });
356
+
357
+ // Version 2.0.0: store as object with additional metadata
358
+ const themeBindingV2 = bindValue<{ value: string; lastUpdated: number }>({
359
+ key: "theme",
360
+ defaultValue: { value: "light", lastUpdated: Date.now() },
361
+ serialize: (v) => JSON.stringify(v),
362
+ deserialize: (s) => JSON.parse(s),
363
+ version: "2.0.0",
364
+ migrate: (oldSerialized, oldVersion) => {
365
+ if (oldVersion === "1.0.0" || oldVersion === undefined) {
366
+ // Migrate from string to object
367
+ return {
368
+ value: oldSerialized,
369
+ lastUpdated: Date.now(),
370
+ };
371
+ }
372
+ // Fallback to default for unknown versions
373
+ return { value: "light", lastUpdated: Date.now() };
374
+ },
375
+ });
376
+ ```
377
+
378
+ ### Common Migration Patterns
379
+
380
+ **Add new field:**
381
+
382
+ ```typescript
383
+ migrate: (oldSerialized, oldVersion) => {
384
+ const oldData = JSON.parse(oldSerialized);
385
+ return { ...oldData, newField: "default" };
386
+ };
387
+ ```
388
+
389
+ **Rename field:**
390
+
391
+ ```typescript
392
+ migrate: (oldSerialized) => {
393
+ const oldData = JSON.parse(oldSerialized);
394
+ return { newName: oldData.oldName };
395
+ };
396
+ ```
397
+
398
+ **Change data type:**
399
+
400
+ ```typescript
401
+ migrate: (oldSerialized) => {
402
+ const dateString = oldSerialized;
403
+ return { date: new Date(dateString) };
404
+ };
405
+ ```
406
+
407
+ ### Migration Behavior
408
+
409
+ - When `version` is provided, values are stored with a version prefix
410
+ - On load, if versions mismatch, the `migrate` function is called
411
+ - If `migrate` is undefined or fails, the `defaultValue` is used
412
+ - Old data is automatically cleaned up when using default fallback
413
+ - Migration receives the raw serialized string (not deserialized)
414
+ - Migration failures are handled silently
415
+
416
+ This ensures your application works even when users have old data formats, and new users get the default structure.
417
+
418
+ ```typescript
419
+ const counterBinding = bindValue<number>({
420
+ key: 'counter',
421
+ defaultValue: 0,
422
+ serialize: (v) => String(v),
423
+ deserialize: (s) => Number(s),
424
+ });
242
425
 
243
426
  const Counter = () => {
244
427
  const { value, setValue } = useStorage(counterBinding);
245
- const count = parseInt(value || '0', 10);
246
428
 
247
429
  return (
248
430
  <div>
249
- <p>Count: {count}</p>
250
- <button onClick={() => setValue(String(count + 1))}>
431
+ <p>Count: {value}</p>
432
+ <button onClick={() => setValue(value + 1)}>
251
433
  Increment
252
434
  </button>
253
- <button onClick={() => setValue(String(count - 1))}>
435
+ <button onClick={() => setValue(value - 1)}>
254
436
  Decrement
255
437
  </button>
256
- <button onClick={() => setValue('0')}>
438
+ <button onClick={() => setValue(0)}>
257
439
  Reset
258
440
  </button>
259
441
  </div>
@@ -268,18 +450,19 @@ const Counter = () => {
268
450
  All storage logic lives in `bindValue` class:
269
451
 
270
452
  - Framework-agnostic
453
+ - Type-safe generic API
271
454
  - Manages localStorage operations
272
455
  - Handles subscriptions
273
456
  - Can be used with any UI framework
274
457
 
275
458
  ```typescript
276
- class BindValue {
277
- private value: string | undefined;
278
- private subscribers: Set<(value: string | undefined) => void>;
459
+ class BindValue<T> {
460
+ private value: T;
461
+ private subscribers: Set<(value: T) => void>;
279
462
 
280
- getValue(): string | undefined;
281
- set(value: string): void;
282
- subscribe(callback): () => void;
463
+ getValue(): T;
464
+ set(value: T): void;
465
+ subscribe(callback: (value: T) => void): () => void;
283
466
  }
284
467
  ```
285
468
 
@@ -293,7 +476,7 @@ Thin wrapper that connects `bindValue` to React:
293
476
  - Returns `{ value, setValue }`
294
477
 
295
478
  ```typescript
296
- function useStorage(binding: BindValue): UseStorageReturn {
479
+ function useStorage<T>(binding: BindValue<T>): UseStorageReturn<T> {
297
480
  const [value, setValue] = useState(binding.getValue());
298
481
 
299
482
  useEffect(() => {
@@ -301,7 +484,7 @@ function useStorage(binding: BindValue): UseStorageReturn {
301
484
  return unsubscribe;
302
485
  }, [binding]);
303
486
 
304
- const set = (newValue: string) => binding.set(newValue);
487
+ const set = (newValue: T) => binding.set(newValue);
305
488
 
306
489
  return { value, setValue: set };
307
490
  }
@@ -309,9 +492,9 @@ function useStorage(binding: BindValue): UseStorageReturn {
309
492
 
310
493
  ## Limitations
311
494
 
312
- - **String-only values**: Kvozy only supports string values. For objects or arrays, serialize them as JSON.
313
495
  - **No SSR support**: Currently designed for client-side only (requires `window.localStorage`).
314
496
  - **No cross-tab sync**: Changes in one tab don't update other tabs automatically.
497
+ - **Serialize failures**: If `serialize()` fails, the value is kept in memory but not persisted to storage (graceful degradation).
315
498
 
316
499
  ## Future Plans
317
500
 
@@ -320,3 +503,4 @@ function useStorage(binding: BindValue): UseStorageReturn {
320
503
  - [ ] Angular connector
321
504
  - [ ] SSR support
322
505
  - [ ] Cross-tab synchronization
506
+ - [ ] Console warnings on serialization/deserialization errors for debugging
@@ -1,18 +1,23 @@
1
- export interface BindValueOptions {
1
+ export interface BindValueOptions<T> {
2
2
  key: string;
3
+ defaultValue: T;
4
+ serialize: (value: T) => string;
5
+ deserialize: (serialized: string) => T;
3
6
  storage?: Storage;
7
+ version?: string;
8
+ migrate?: (oldSerialized: string, oldVersion: string | undefined) => T;
4
9
  }
5
- export declare class BindValue {
10
+ export declare class BindValue<T> {
6
11
  private options;
7
12
  private value;
8
13
  private subscribers;
9
14
  private storage;
10
- constructor(options: BindValueOptions);
11
- getValue(): string | undefined;
12
- set(newValue: string): void;
13
- subscribe(callback: (value: string | undefined) => void): () => void;
15
+ constructor(options: BindValueOptions<T>);
16
+ getValue(): T;
17
+ set(newValue: T): void;
18
+ subscribe(callback: (value: T) => void): () => void;
14
19
  private loadFromStorage;
15
20
  private saveToStorage;
16
21
  private notifySubscribers;
17
22
  }
18
- export declare function bindValue(options: BindValueOptions): BindValue;
23
+ export declare function bindValue<T>(options: BindValueOptions<T>): BindValue<T>;
@@ -50,11 +50,62 @@ class BindValue {
50
50
  };
51
51
  }
52
52
  loadFromStorage() {
53
- const value = this.storage.getItem(this.options.key);
54
- return value !== null ? value : void 0;
53
+ const rawValue = this.storage.getItem(this.options.key);
54
+ if (rawValue === null) {
55
+ return this.options.defaultValue;
56
+ }
57
+ let oldVersion;
58
+ let serializedValue;
59
+ if (rawValue.startsWith("\0")) {
60
+ const parts = rawValue.split("\0");
61
+ if (parts.length >= 3) {
62
+ oldVersion = parts[1];
63
+ serializedValue = parts.slice(2).join("\0");
64
+ } else {
65
+ serializedValue = rawValue;
66
+ }
67
+ } else {
68
+ serializedValue = rawValue;
69
+ }
70
+ const currentVersion = this.options.version;
71
+ if (oldVersion !== currentVersion) {
72
+ if (this.options.migrate) {
73
+ try {
74
+ const migratedValue = this.options.migrate(
75
+ serializedValue,
76
+ oldVersion
77
+ );
78
+ this.saveToStorage(migratedValue);
79
+ return migratedValue;
80
+ } catch {
81
+ this.storage.removeItem(this.options.key);
82
+ return this.options.defaultValue;
83
+ }
84
+ } else {
85
+ this.storage.removeItem(this.options.key);
86
+ return this.options.defaultValue;
87
+ }
88
+ }
89
+ try {
90
+ return this.options.deserialize(serializedValue);
91
+ } catch {
92
+ return this.options.defaultValue;
93
+ }
55
94
  }
56
95
  saveToStorage(value) {
57
- this.storage.setItem(this.options.key, value);
96
+ try {
97
+ const serialized = this.options.serialize(value);
98
+ const version = this.options.version;
99
+ if (version) {
100
+ this.storage.setItem(
101
+ this.options.key,
102
+ `\0${version}\0${serialized}`
103
+ );
104
+ } else {
105
+ this.storage.setItem(this.options.key, serialized);
106
+ }
107
+ } catch {
108
+ }
58
109
  }
59
110
  notifySubscribers() {
60
111
  for (const subscriber of this.subscribers) {
@@ -1 +1 @@
1
- {"version":3,"file":"bindValue.js","sources":["../../../../src/bindValue.ts"],"sourcesContent":["export interface BindValueOptions {\n key: string;\n storage?: Storage;\n}\n\nconst memoryStorage = new Map<string, string>();\n\nconst inMemoryStorage: Storage = {\n get length() {\n return memoryStorage.size;\n },\n clear() {\n memoryStorage.clear();\n },\n getItem(key: string) {\n return memoryStorage.get(key) ?? null;\n },\n key(index: number) {\n const keys = Array.from(memoryStorage.keys());\n return keys[index] ?? null;\n },\n removeItem(key: string) {\n memoryStorage.delete(key);\n },\n setItem(key: string, value: string) {\n memoryStorage.set(key, value);\n },\n};\n\nexport class BindValue {\n private value: string | undefined;\n private subscribers: Set<(value: string | undefined) => void>;\n private storage: Storage;\n\n constructor(private options: BindValueOptions) {\n this.storage = options.storage ?? inMemoryStorage;\n this.subscribers = new Set();\n this.value = this.loadFromStorage();\n }\n\n getValue(): string | undefined {\n return this.value;\n }\n\n set(newValue: string): void {\n this.value = newValue;\n this.saveToStorage(newValue);\n this.notifySubscribers();\n }\n\n subscribe(callback: (value: string | undefined) => void): () => void {\n this.subscribers.add(callback);\n return () => {\n this.subscribers.delete(callback);\n };\n }\n\n private loadFromStorage(): string | undefined {\n const value = this.storage.getItem(this.options.key);\n return value !== null ? value : undefined;\n }\n\n private saveToStorage(value: string): void {\n this.storage.setItem(this.options.key, value);\n }\n\n private notifySubscribers(): void {\n for (const subscriber of this.subscribers) {\n subscriber(this.value);\n }\n }\n}\n\nexport function bindValue(options: BindValueOptions): BindValue {\n return new BindValue(options);\n}\n"],"names":[],"mappings":";;;;;AAKA,MAAM,oCAAoB,IAAA;AAE1B,MAAM,kBAA2B;AAAA,EAC/B,IAAI,SAAS;AACX,WAAO,cAAc;AAAA,EACvB;AAAA,EACA,QAAQ;AACN,kBAAc,MAAA;AAAA,EAChB;AAAA,EACA,QAAQ,KAAa;AACnB,WAAO,cAAc,IAAI,GAAG,KAAK;AAAA,EACnC;AAAA,EACA,IAAI,OAAe;AACjB,UAAM,OAAO,MAAM,KAAK,cAAc,MAAM;AAC5C,WAAO,KAAK,KAAK,KAAK;AAAA,EACxB;AAAA,EACA,WAAW,KAAa;AACtB,kBAAc,OAAO,GAAG;AAAA,EAC1B;AAAA,EACA,QAAQ,KAAa,OAAe;AAClC,kBAAc,IAAI,KAAK,KAAK;AAAA,EAC9B;AACF;AAEO,MAAM,UAAU;AAAA,EAKrB,YAAoB,SAA2B;AAJvC;AACA;AACA;AAEY,SAAA,UAAA;AAClB,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,kCAAkB,IAAA;AACvB,SAAK,QAAQ,KAAK,gBAAA;AAAA,EACpB;AAAA,EAEA,WAA+B;AAC7B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,UAAwB;AAC1B,SAAK,QAAQ;AACb,SAAK,cAAc,QAAQ;AAC3B,SAAK,kBAAA;AAAA,EACP;AAAA,EAEA,UAAU,UAA2D;AACnE,SAAK,YAAY,IAAI,QAAQ;AAC7B,WAAO,MAAM;AACX,WAAK,YAAY,OAAO,QAAQ;AAAA,IAClC;AAAA,EACF;AAAA,EAEQ,kBAAsC;AAC5C,UAAM,QAAQ,KAAK,QAAQ,QAAQ,KAAK,QAAQ,GAAG;AACnD,WAAO,UAAU,OAAO,QAAQ;AAAA,EAClC;AAAA,EAEQ,cAAc,OAAqB;AACzC,SAAK,QAAQ,QAAQ,KAAK,QAAQ,KAAK,KAAK;AAAA,EAC9C;AAAA,EAEQ,oBAA0B;AAChC,eAAW,cAAc,KAAK,aAAa;AACzC,iBAAW,KAAK,KAAK;AAAA,IACvB;AAAA,EACF;AACF;AAEO,SAAS,UAAU,SAAsC;AAC9D,SAAO,IAAI,UAAU,OAAO;AAC9B;;;"}
1
+ {"version":3,"file":"bindValue.js","sources":["../../../../src/bindValue.ts"],"sourcesContent":["export interface BindValueOptions<T> {\n key: string;\n defaultValue: T;\n serialize: (value: T) => string;\n deserialize: (serialized: string) => T;\n storage?: Storage;\n version?: string;\n migrate?: (oldSerialized: string, oldVersion: string | undefined) => T;\n}\n\nconst memoryStorage = new Map<string, string>();\n\nconst inMemoryStorage: Storage = {\n get length() {\n return memoryStorage.size;\n },\n clear() {\n memoryStorage.clear();\n },\n getItem(key: string) {\n return memoryStorage.get(key) ?? null;\n },\n key(index: number) {\n const keys = Array.from(memoryStorage.keys());\n return keys[index] ?? null;\n },\n removeItem(key: string) {\n memoryStorage.delete(key);\n },\n setItem(key: string, value: string) {\n memoryStorage.set(key, value);\n },\n};\n\nexport class BindValue<T> {\n private value: T;\n private subscribers: Set<(value: T) => void>;\n private storage: Storage;\n\n constructor(private options: BindValueOptions<T>) {\n this.storage = options.storage ?? inMemoryStorage;\n this.subscribers = new Set();\n this.value = this.loadFromStorage();\n }\n\n getValue(): T {\n return this.value;\n }\n\n set(newValue: T): void {\n this.value = newValue;\n this.saveToStorage(newValue);\n this.notifySubscribers();\n }\n\n subscribe(callback: (value: T) => void): () => void {\n this.subscribers.add(callback);\n return () => {\n this.subscribers.delete(callback);\n };\n }\n\n private loadFromStorage(): T {\n const rawValue = this.storage.getItem(this.options.key);\n\n if (rawValue === null) {\n return this.options.defaultValue;\n }\n\n let oldVersion: string | undefined;\n let serializedValue: string;\n\n if (rawValue.startsWith(\"\\x00\")) {\n const parts = rawValue.split(\"\\x00\");\n if (parts.length >= 3) {\n oldVersion = parts[1];\n serializedValue = parts.slice(2).join(\"\\x00\");\n } else {\n serializedValue = rawValue;\n }\n } else {\n serializedValue = rawValue;\n }\n\n const currentVersion = this.options.version;\n\n if (oldVersion !== currentVersion) {\n if (this.options.migrate) {\n try {\n const migratedValue = this.options.migrate(\n serializedValue,\n oldVersion,\n );\n this.saveToStorage(migratedValue);\n return migratedValue;\n } catch {\n this.storage.removeItem(this.options.key);\n return this.options.defaultValue;\n }\n } else {\n this.storage.removeItem(this.options.key);\n return this.options.defaultValue;\n }\n }\n\n try {\n return this.options.deserialize(serializedValue);\n } catch {\n return this.options.defaultValue;\n }\n }\n\n private saveToStorage(value: T): void {\n try {\n const serialized = this.options.serialize(value);\n const version = this.options.version;\n\n if (version) {\n this.storage.setItem(\n this.options.key,\n `\\x00${version}\\x00${serialized}`,\n );\n } else {\n this.storage.setItem(this.options.key, serialized);\n }\n } catch {}\n }\n\n private notifySubscribers(): void {\n for (const subscriber of this.subscribers) {\n subscriber(this.value);\n }\n }\n}\n\nexport function bindValue<T>(options: BindValueOptions<T>): BindValue<T> {\n return new BindValue(options);\n}\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { type BindValue } from "./bindValue.js";
2
- export interface UseStorageReturn {
3
- value: string | undefined;
4
- setValue: (value: string) => void;
2
+ export interface UseStorageReturn<T> {
3
+ value: T;
4
+ setValue: (value: T) => void;
5
5
  }
6
- export declare function useStorage(binding: BindValue): UseStorageReturn;
6
+ export declare function useStorage<T>(binding: BindValue<T>): UseStorageReturn<T>;
@@ -1 +1 @@
1
- {"version":3,"file":"useStorage.js","sources":["../../../../src/useStorage.ts"],"sourcesContent":["import { useState, useEffect } from \"react\";\nimport { type BindValue } from \"./bindValue.js\";\n\nexport interface UseStorageReturn {\n value: string | undefined;\n setValue: (value: string) => void;\n}\n\nexport function useStorage(binding: BindValue): UseStorageReturn {\n const [value, setValue] = useState(binding.getValue());\n\n useEffect(() => {\n const unsubscribe = binding.subscribe(setValue);\n return unsubscribe;\n }, [binding]);\n\n const set = (newValue: string) => {\n binding.set(newValue);\n };\n\n return { value, setValue: set };\n}\n"],"names":["useState","useEffect"],"mappings":";;;AAQO,SAAS,WAAW,SAAsC;AAC/D,QAAM,CAAC,OAAO,QAAQ,IAAIA,MAAAA,SAAS,QAAQ,UAAU;AAErDC,QAAAA,UAAU,MAAM;AACd,UAAM,cAAc,QAAQ,UAAU,QAAQ;AAC9C,WAAO;AAAA,EACT,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,MAAM,CAAC,aAAqB;AAChC,YAAQ,IAAI,QAAQ;AAAA,EACtB;AAEA,SAAO,EAAE,OAAO,UAAU,IAAA;AAC5B;;"}
1
+ {"version":3,"file":"useStorage.js","sources":["../../../../src/useStorage.ts"],"sourcesContent":["import { useState, useEffect } from \"react\";\nimport { type BindValue } from \"./bindValue.js\";\n\nexport interface UseStorageReturn<T> {\n value: T;\n setValue: (value: T) => void;\n}\n\nexport function useStorage<T>(binding: BindValue<T>): UseStorageReturn<T> {\n const [value, setValue] = useState(binding.getValue());\n\n useEffect(() => {\n const unsubscribe = binding.subscribe(setValue);\n return unsubscribe;\n }, [binding]);\n\n const set = (newValue: T) => {\n binding.set(newValue);\n };\n\n return { value, setValue: set };\n}\n"],"names":["useState","useEffect"],"mappings":";;;AAQO,SAAS,WAAc,SAA4C;AACxE,QAAM,CAAC,OAAO,QAAQ,IAAIA,MAAAA,SAAS,QAAQ,UAAU;AAErDC,QAAAA,UAAU,MAAM;AACd,UAAM,cAAc,QAAQ,UAAU,QAAQ;AAC9C,WAAO;AAAA,EACT,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,MAAM,CAAC,aAAgB;AAC3B,YAAQ,IAAI,QAAQ;AAAA,EACtB;AAEA,SAAO,EAAE,OAAO,UAAU,IAAA;AAC5B;;"}
@@ -1,18 +1,23 @@
1
- export interface BindValueOptions {
1
+ export interface BindValueOptions<T> {
2
2
  key: string;
3
+ defaultValue: T;
4
+ serialize: (value: T) => string;
5
+ deserialize: (serialized: string) => T;
3
6
  storage?: Storage;
7
+ version?: string;
8
+ migrate?: (oldSerialized: string, oldVersion: string | undefined) => T;
4
9
  }
5
- export declare class BindValue {
10
+ export declare class BindValue<T> {
6
11
  private options;
7
12
  private value;
8
13
  private subscribers;
9
14
  private storage;
10
- constructor(options: BindValueOptions);
11
- getValue(): string | undefined;
12
- set(newValue: string): void;
13
- subscribe(callback: (value: string | undefined) => void): () => void;
15
+ constructor(options: BindValueOptions<T>);
16
+ getValue(): T;
17
+ set(newValue: T): void;
18
+ subscribe(callback: (value: T) => void): () => void;
14
19
  private loadFromStorage;
15
20
  private saveToStorage;
16
21
  private notifySubscribers;
17
22
  }
18
- export declare function bindValue(options: BindValueOptions): BindValue;
23
+ export declare function bindValue<T>(options: BindValueOptions<T>): BindValue<T>;
@@ -48,11 +48,62 @@ class BindValue {
48
48
  };
49
49
  }
50
50
  loadFromStorage() {
51
- const value = this.storage.getItem(this.options.key);
52
- return value !== null ? value : void 0;
51
+ const rawValue = this.storage.getItem(this.options.key);
52
+ if (rawValue === null) {
53
+ return this.options.defaultValue;
54
+ }
55
+ let oldVersion;
56
+ let serializedValue;
57
+ if (rawValue.startsWith("\0")) {
58
+ const parts = rawValue.split("\0");
59
+ if (parts.length >= 3) {
60
+ oldVersion = parts[1];
61
+ serializedValue = parts.slice(2).join("\0");
62
+ } else {
63
+ serializedValue = rawValue;
64
+ }
65
+ } else {
66
+ serializedValue = rawValue;
67
+ }
68
+ const currentVersion = this.options.version;
69
+ if (oldVersion !== currentVersion) {
70
+ if (this.options.migrate) {
71
+ try {
72
+ const migratedValue = this.options.migrate(
73
+ serializedValue,
74
+ oldVersion
75
+ );
76
+ this.saveToStorage(migratedValue);
77
+ return migratedValue;
78
+ } catch {
79
+ this.storage.removeItem(this.options.key);
80
+ return this.options.defaultValue;
81
+ }
82
+ } else {
83
+ this.storage.removeItem(this.options.key);
84
+ return this.options.defaultValue;
85
+ }
86
+ }
87
+ try {
88
+ return this.options.deserialize(serializedValue);
89
+ } catch {
90
+ return this.options.defaultValue;
91
+ }
53
92
  }
54
93
  saveToStorage(value) {
55
- this.storage.setItem(this.options.key, value);
94
+ try {
95
+ const serialized = this.options.serialize(value);
96
+ const version = this.options.version;
97
+ if (version) {
98
+ this.storage.setItem(
99
+ this.options.key,
100
+ `\0${version}\0${serialized}`
101
+ );
102
+ } else {
103
+ this.storage.setItem(this.options.key, serialized);
104
+ }
105
+ } catch {
106
+ }
56
107
  }
57
108
  notifySubscribers() {
58
109
  for (const subscriber of this.subscribers) {
@@ -1 +1 @@
1
- {"version":3,"file":"bindValue.mjs","sources":["../../../../src/bindValue.ts"],"sourcesContent":["export interface BindValueOptions {\n key: string;\n storage?: Storage;\n}\n\nconst memoryStorage = new Map<string, string>();\n\nconst inMemoryStorage: Storage = {\n get length() {\n return memoryStorage.size;\n },\n clear() {\n memoryStorage.clear();\n },\n getItem(key: string) {\n return memoryStorage.get(key) ?? null;\n },\n key(index: number) {\n const keys = Array.from(memoryStorage.keys());\n return keys[index] ?? null;\n },\n removeItem(key: string) {\n memoryStorage.delete(key);\n },\n setItem(key: string, value: string) {\n memoryStorage.set(key, value);\n },\n};\n\nexport class BindValue {\n private value: string | undefined;\n private subscribers: Set<(value: string | undefined) => void>;\n private storage: Storage;\n\n constructor(private options: BindValueOptions) {\n this.storage = options.storage ?? inMemoryStorage;\n this.subscribers = new Set();\n this.value = this.loadFromStorage();\n }\n\n getValue(): string | undefined {\n return this.value;\n }\n\n set(newValue: string): void {\n this.value = newValue;\n this.saveToStorage(newValue);\n this.notifySubscribers();\n }\n\n subscribe(callback: (value: string | undefined) => void): () => void {\n this.subscribers.add(callback);\n return () => {\n this.subscribers.delete(callback);\n };\n }\n\n private loadFromStorage(): string | undefined {\n const value = this.storage.getItem(this.options.key);\n return value !== null ? value : undefined;\n }\n\n private saveToStorage(value: string): void {\n this.storage.setItem(this.options.key, value);\n }\n\n private notifySubscribers(): void {\n for (const subscriber of this.subscribers) {\n subscriber(this.value);\n }\n }\n}\n\nexport function bindValue(options: BindValueOptions): BindValue {\n return new BindValue(options);\n}\n"],"names":[],"mappings":";;;AAKA,MAAM,oCAAoB,IAAA;AAE1B,MAAM,kBAA2B;AAAA,EAC/B,IAAI,SAAS;AACX,WAAO,cAAc;AAAA,EACvB;AAAA,EACA,QAAQ;AACN,kBAAc,MAAA;AAAA,EAChB;AAAA,EACA,QAAQ,KAAa;AACnB,WAAO,cAAc,IAAI,GAAG,KAAK;AAAA,EACnC;AAAA,EACA,IAAI,OAAe;AACjB,UAAM,OAAO,MAAM,KAAK,cAAc,MAAM;AAC5C,WAAO,KAAK,KAAK,KAAK;AAAA,EACxB;AAAA,EACA,WAAW,KAAa;AACtB,kBAAc,OAAO,GAAG;AAAA,EAC1B;AAAA,EACA,QAAQ,KAAa,OAAe;AAClC,kBAAc,IAAI,KAAK,KAAK;AAAA,EAC9B;AACF;AAEO,MAAM,UAAU;AAAA,EAKrB,YAAoB,SAA2B;AAJvC;AACA;AACA;AAEY,SAAA,UAAA;AAClB,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,kCAAkB,IAAA;AACvB,SAAK,QAAQ,KAAK,gBAAA;AAAA,EACpB;AAAA,EAEA,WAA+B;AAC7B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,UAAwB;AAC1B,SAAK,QAAQ;AACb,SAAK,cAAc,QAAQ;AAC3B,SAAK,kBAAA;AAAA,EACP;AAAA,EAEA,UAAU,UAA2D;AACnE,SAAK,YAAY,IAAI,QAAQ;AAC7B,WAAO,MAAM;AACX,WAAK,YAAY,OAAO,QAAQ;AAAA,IAClC;AAAA,EACF;AAAA,EAEQ,kBAAsC;AAC5C,UAAM,QAAQ,KAAK,QAAQ,QAAQ,KAAK,QAAQ,GAAG;AACnD,WAAO,UAAU,OAAO,QAAQ;AAAA,EAClC;AAAA,EAEQ,cAAc,OAAqB;AACzC,SAAK,QAAQ,QAAQ,KAAK,QAAQ,KAAK,KAAK;AAAA,EAC9C;AAAA,EAEQ,oBAA0B;AAChC,eAAW,cAAc,KAAK,aAAa;AACzC,iBAAW,KAAK,KAAK;AAAA,IACvB;AAAA,EACF;AACF;AAEO,SAAS,UAAU,SAAsC;AAC9D,SAAO,IAAI,UAAU,OAAO;AAC9B;"}
1
+ {"version":3,"file":"bindValue.mjs","sources":["../../../../src/bindValue.ts"],"sourcesContent":["export interface BindValueOptions<T> {\n key: string;\n defaultValue: T;\n serialize: (value: T) => string;\n deserialize: (serialized: string) => T;\n storage?: Storage;\n version?: string;\n migrate?: (oldSerialized: string, oldVersion: string | undefined) => T;\n}\n\nconst memoryStorage = new Map<string, string>();\n\nconst inMemoryStorage: Storage = {\n get length() {\n return memoryStorage.size;\n },\n clear() {\n memoryStorage.clear();\n },\n getItem(key: string) {\n return memoryStorage.get(key) ?? null;\n },\n key(index: number) {\n const keys = Array.from(memoryStorage.keys());\n return keys[index] ?? null;\n },\n removeItem(key: string) {\n memoryStorage.delete(key);\n },\n setItem(key: string, value: string) {\n memoryStorage.set(key, value);\n },\n};\n\nexport class BindValue<T> {\n private value: T;\n private subscribers: Set<(value: T) => void>;\n private storage: Storage;\n\n constructor(private options: BindValueOptions<T>) {\n this.storage = options.storage ?? inMemoryStorage;\n this.subscribers = new Set();\n this.value = this.loadFromStorage();\n }\n\n getValue(): T {\n return this.value;\n }\n\n set(newValue: T): void {\n this.value = newValue;\n this.saveToStorage(newValue);\n this.notifySubscribers();\n }\n\n subscribe(callback: (value: T) => void): () => void {\n this.subscribers.add(callback);\n return () => {\n this.subscribers.delete(callback);\n };\n }\n\n private loadFromStorage(): T {\n const rawValue = this.storage.getItem(this.options.key);\n\n if (rawValue === null) {\n return this.options.defaultValue;\n }\n\n let oldVersion: string | undefined;\n let serializedValue: string;\n\n if (rawValue.startsWith(\"\\x00\")) {\n const parts = rawValue.split(\"\\x00\");\n if (parts.length >= 3) {\n oldVersion = parts[1];\n serializedValue = parts.slice(2).join(\"\\x00\");\n } else {\n serializedValue = rawValue;\n }\n } else {\n serializedValue = rawValue;\n }\n\n const currentVersion = this.options.version;\n\n if (oldVersion !== currentVersion) {\n if (this.options.migrate) {\n try {\n const migratedValue = this.options.migrate(\n serializedValue,\n oldVersion,\n );\n this.saveToStorage(migratedValue);\n return migratedValue;\n } catch {\n this.storage.removeItem(this.options.key);\n return this.options.defaultValue;\n }\n } else {\n this.storage.removeItem(this.options.key);\n return this.options.defaultValue;\n }\n }\n\n try {\n return this.options.deserialize(serializedValue);\n } catch {\n return this.options.defaultValue;\n }\n }\n\n private saveToStorage(value: T): void {\n try {\n const serialized = this.options.serialize(value);\n const version = this.options.version;\n\n if (version) {\n this.storage.setItem(\n this.options.key,\n `\\x00${version}\\x00${serialized}`,\n );\n } else {\n this.storage.setItem(this.options.key, serialized);\n }\n } catch {}\n }\n\n private notifySubscribers(): void {\n for (const subscriber of this.subscribers) {\n subscriber(this.value);\n }\n }\n}\n\nexport function bindValue<T>(options: BindValueOptions<T>): BindValue<T> {\n return new BindValue(options);\n}\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { type BindValue } from "./bindValue.js";
2
- export interface UseStorageReturn {
3
- value: string | undefined;
4
- setValue: (value: string) => void;
2
+ export interface UseStorageReturn<T> {
3
+ value: T;
4
+ setValue: (value: T) => void;
5
5
  }
6
- export declare function useStorage(binding: BindValue): UseStorageReturn;
6
+ export declare function useStorage<T>(binding: BindValue<T>): UseStorageReturn<T>;
@@ -1 +1 @@
1
- {"version":3,"file":"useStorage.mjs","sources":["../../../../src/useStorage.ts"],"sourcesContent":["import { useState, useEffect } from \"react\";\nimport { type BindValue } from \"./bindValue.js\";\n\nexport interface UseStorageReturn {\n value: string | undefined;\n setValue: (value: string) => void;\n}\n\nexport function useStorage(binding: BindValue): UseStorageReturn {\n const [value, setValue] = useState(binding.getValue());\n\n useEffect(() => {\n const unsubscribe = binding.subscribe(setValue);\n return unsubscribe;\n }, [binding]);\n\n const set = (newValue: string) => {\n binding.set(newValue);\n };\n\n return { value, setValue: set };\n}\n"],"names":[],"mappings":";AAQO,SAAS,WAAW,SAAsC;AAC/D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,QAAQ,UAAU;AAErD,YAAU,MAAM;AACd,UAAM,cAAc,QAAQ,UAAU,QAAQ;AAC9C,WAAO;AAAA,EACT,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,MAAM,CAAC,aAAqB;AAChC,YAAQ,IAAI,QAAQ;AAAA,EACtB;AAEA,SAAO,EAAE,OAAO,UAAU,IAAA;AAC5B;"}
1
+ {"version":3,"file":"useStorage.mjs","sources":["../../../../src/useStorage.ts"],"sourcesContent":["import { useState, useEffect } from \"react\";\nimport { type BindValue } from \"./bindValue.js\";\n\nexport interface UseStorageReturn<T> {\n value: T;\n setValue: (value: T) => void;\n}\n\nexport function useStorage<T>(binding: BindValue<T>): UseStorageReturn<T> {\n const [value, setValue] = useState(binding.getValue());\n\n useEffect(() => {\n const unsubscribe = binding.subscribe(setValue);\n return unsubscribe;\n }, [binding]);\n\n const set = (newValue: T) => {\n binding.set(newValue);\n };\n\n return { value, setValue: set };\n}\n"],"names":[],"mappings":";AAQO,SAAS,WAAc,SAA4C;AACxE,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,QAAQ,UAAU;AAErD,YAAU,MAAM;AACd,UAAM,cAAc,QAAQ,UAAU,QAAQ;AAC9C,WAAO;AAAA,EACT,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,MAAM,CAAC,aAAgB;AAC3B,YAAQ,IAAI,QAAQ;AAAA,EACtB;AAEA,SAAO,EAAE,OAAO,UAAU,IAAA;AAC5B;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kvozy",
3
3
  "type": "commonjs",
4
- "version": "0.2.0",
4
+ "version": "0.4.0",
5
5
  "types": "./__compiled__/cjs/src/index.d.ts",
6
6
  "module": "./__compiled__/esm/src/index.mjs",
7
7
  "main": "./__compiled__/cjs/src/index.js",