kvozy 0.3.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
@@ -22,6 +22,7 @@ This architecture makes it easy to add connectors for other frameworks (Vue, Sve
22
22
  - Subscription-based reactivity
23
23
  - TypeScript support
24
24
  - Required default values for safety
25
+ - Schema versioning and migration support
25
26
  - Easy to extend to other frameworks
26
27
 
27
28
  ## Installation
@@ -87,6 +88,8 @@ Options for creating a BindValue instance.
87
88
  - `serialize` (function, required) - Convert value to string: `(value: T) => string`
88
89
  - `deserialize` (function, required) - Convert string to value: `(serialized: string) => T`
89
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`
90
93
 
91
94
  ### useStorage<T>(binding)
92
95
 
@@ -322,6 +325,124 @@ const Counter = () => {
322
325
  };
323
326
  ```
324
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
+ });
425
+
426
+ const Counter = () => {
427
+ const { value, setValue } = useStorage(counterBinding);
428
+
429
+ return (
430
+ <div>
431
+ <p>Count: {value}</p>
432
+ <button onClick={() => setValue(value + 1)}>
433
+ Increment
434
+ </button>
435
+ <button onClick={() => setValue(value - 1)}>
436
+ Decrement
437
+ </button>
438
+ <button onClick={() => setValue(0)}>
439
+ Reset
440
+ </button>
441
+ </div>
442
+ );
443
+ };
444
+ ```
445
+
325
446
  ## Architecture
326
447
 
327
448
  ### Core: bindValue
@@ -4,6 +4,8 @@ export interface BindValueOptions<T> {
4
4
  serialize: (value: T) => string;
5
5
  deserialize: (serialized: string) => T;
6
6
  storage?: Storage;
7
+ version?: string;
8
+ migrate?: (oldSerialized: string, oldVersion: string | undefined) => T;
7
9
  }
8
10
  export declare class BindValue<T> {
9
11
  private options;
@@ -54,8 +54,40 @@ class BindValue {
54
54
  if (rawValue === null) {
55
55
  return this.options.defaultValue;
56
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
+ }
57
89
  try {
58
- return this.options.deserialize(rawValue);
90
+ return this.options.deserialize(serializedValue);
59
91
  } catch {
60
92
  return this.options.defaultValue;
61
93
  }
@@ -63,7 +95,15 @@ class BindValue {
63
95
  saveToStorage(value) {
64
96
  try {
65
97
  const serialized = this.options.serialize(value);
66
- this.storage.setItem(this.options.key, serialized);
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
+ }
67
107
  } catch {
68
108
  }
69
109
  }
@@ -1 +1 @@
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}\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 try {\n return this.options.deserialize(rawValue);\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 this.storage.setItem(this.options.key, serialized);\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":";;;;;AAQA,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,UAAa;AAAA,EAKxB,YAAoB,SAA8B;AAJ1C;AACA;AACA;AAEY,SAAA,UAAA;AAClB,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,kCAAkB,IAAA;AACvB,SAAK,QAAQ,KAAK,gBAAA;AAAA,EACpB;AAAA,EAEA,WAAc;AACZ,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,UAAmB;AACrB,SAAK,QAAQ;AACb,SAAK,cAAc,QAAQ;AAC3B,SAAK,kBAAA;AAAA,EACP;AAAA,EAEA,UAAU,UAA0C;AAClD,SAAK,YAAY,IAAI,QAAQ;AAC7B,WAAO,MAAM;AACX,WAAK,YAAY,OAAO,QAAQ;AAAA,IAClC;AAAA,EACF;AAAA,EAEQ,kBAAqB;AAC3B,UAAM,WAAW,KAAK,QAAQ,QAAQ,KAAK,QAAQ,GAAG;AAEtD,QAAI,aAAa,MAAM;AACrB,aAAO,KAAK,QAAQ;AAAA,IACtB;AAEA,QAAI;AACF,aAAO,KAAK,QAAQ,YAAY,QAAQ;AAAA,IAC1C,QAAQ;AACN,aAAO,KAAK,QAAQ;AAAA,IACtB;AAAA,EACF;AAAA,EAEQ,cAAc,OAAgB;AACpC,QAAI;AACF,YAAM,aAAa,KAAK,QAAQ,UAAU,KAAK;AAC/C,WAAK,QAAQ,QAAQ,KAAK,QAAQ,KAAK,UAAU;AAAA,IACnD,QAAQ;AAAA,IAAC;AAAA,EACX;AAAA,EAEQ,oBAA0B;AAChC,eAAW,cAAc,KAAK,aAAa;AACzC,iBAAW,KAAK,KAAK;AAAA,IACvB;AAAA,EACF;AACF;AAEO,SAAS,UAAa,SAA4C;AACvE,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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"}
@@ -4,6 +4,8 @@ export interface BindValueOptions<T> {
4
4
  serialize: (value: T) => string;
5
5
  deserialize: (serialized: string) => T;
6
6
  storage?: Storage;
7
+ version?: string;
8
+ migrate?: (oldSerialized: string, oldVersion: string | undefined) => T;
7
9
  }
8
10
  export declare class BindValue<T> {
9
11
  private options;
@@ -52,8 +52,40 @@ class BindValue {
52
52
  if (rawValue === null) {
53
53
  return this.options.defaultValue;
54
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
+ }
55
87
  try {
56
- return this.options.deserialize(rawValue);
88
+ return this.options.deserialize(serializedValue);
57
89
  } catch {
58
90
  return this.options.defaultValue;
59
91
  }
@@ -61,7 +93,15 @@ class BindValue {
61
93
  saveToStorage(value) {
62
94
  try {
63
95
  const serialized = this.options.serialize(value);
64
- this.storage.setItem(this.options.key, serialized);
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
+ }
65
105
  } catch {
66
106
  }
67
107
  }
@@ -1 +1 @@
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}\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 try {\n return this.options.deserialize(rawValue);\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 this.storage.setItem(this.options.key, serialized);\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":";;;AAQA,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,UAAa;AAAA,EAKxB,YAAoB,SAA8B;AAJ1C;AACA;AACA;AAEY,SAAA,UAAA;AAClB,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,kCAAkB,IAAA;AACvB,SAAK,QAAQ,KAAK,gBAAA;AAAA,EACpB;AAAA,EAEA,WAAc;AACZ,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,UAAmB;AACrB,SAAK,QAAQ;AACb,SAAK,cAAc,QAAQ;AAC3B,SAAK,kBAAA;AAAA,EACP;AAAA,EAEA,UAAU,UAA0C;AAClD,SAAK,YAAY,IAAI,QAAQ;AAC7B,WAAO,MAAM;AACX,WAAK,YAAY,OAAO,QAAQ;AAAA,IAClC;AAAA,EACF;AAAA,EAEQ,kBAAqB;AAC3B,UAAM,WAAW,KAAK,QAAQ,QAAQ,KAAK,QAAQ,GAAG;AAEtD,QAAI,aAAa,MAAM;AACrB,aAAO,KAAK,QAAQ;AAAA,IACtB;AAEA,QAAI;AACF,aAAO,KAAK,QAAQ,YAAY,QAAQ;AAAA,IAC1C,QAAQ;AACN,aAAO,KAAK,QAAQ;AAAA,IACtB;AAAA,EACF;AAAA,EAEQ,cAAc,OAAgB;AACpC,QAAI;AACF,YAAM,aAAa,KAAK,QAAQ,UAAU,KAAK;AAC/C,WAAK,QAAQ,QAAQ,KAAK,QAAQ,KAAK,UAAU;AAAA,IACnD,QAAQ;AAAA,IAAC;AAAA,EACX;AAAA,EAEQ,oBAA0B;AAChC,eAAW,cAAc,KAAK,aAAa;AACzC,iBAAW,KAAK,KAAK;AAAA,IACvB;AAAA,EACF;AACF;AAEO,SAAS,UAAa,SAA4C;AACvE,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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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kvozy",
3
3
  "type": "commonjs",
4
- "version": "0.3.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",