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 +121 -0
- package/__compiled__/cjs/src/bindValue.d.ts +2 -0
- package/__compiled__/cjs/src/bindValue.js +42 -2
- package/__compiled__/cjs/src/bindValue.js.map +1 -1
- package/__compiled__/esm/src/bindValue.d.mts +2 -0
- package/__compiled__/esm/src/bindValue.mjs +42 -2
- package/__compiled__/esm/src/bindValue.mjs.map +1 -1
- package/package.json +1 -1
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(
|
|
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.
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
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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"}
|