Skip to content

Commit 8c59c1b

Browse files
gyszalaiTGlide
andauthored
fix(persisted-state): write state to storage even if only a nested property is changed. fixes #224 (#225)
* fix(persisted-state): write state to storage even if only a nested property is changed. fixes #224 * Create calm-pumpkins-dress.md --------- Co-authored-by: Thomas G. Lopes <[email protected]>
1 parent 3963961 commit 8c59c1b

File tree

3 files changed

+72
-13
lines changed

3 files changed

+72
-13
lines changed

.changeset/calm-pumpkins-dress.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"runed": patch
3+
---
4+
5+
fix(persisted-state): write state to storage even if only a nested property is changed. fixes #224

packages/runed/src/lib/utilities/persisted-state/persisted-state.svelte.ts

+41-12
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createSubscriber } from "svelte/reactivity";
44

55
type Serializer<T> = {
66
serialize: (value: T) => string;
7-
deserialize: (value: string) => T;
7+
deserialize: (value: string) => T | undefined;
88
};
99

1010
type StorageType = "local" | "session";
@@ -36,11 +36,12 @@ type PersistedStateOptions<T> = {
3636
* @see {@link https://runed.dev/docs/utilities/persisted-state}
3737
*/
3838
export class PersistedState<T> {
39-
#current: T = $state()!;
39+
#current: T | undefined;
4040
#key: string;
4141
#serializer: Serializer<T>;
4242
#storage?: Storage;
4343
#subscribe?: VoidFunction;
44+
#version = $state(0)
4445

4546
constructor(key: string, initialValue: T, options: PersistedStateOptions<T> = {}) {
4647
const {
@@ -61,7 +62,9 @@ export class PersistedState<T> {
6162

6263
const existingValue = storage.getItem(key);
6364
if (existingValue !== null) {
64-
this.#deserialize(existingValue);
65+
this.#current = this.#deserialize(existingValue);
66+
} else {
67+
this.#serialize(initialValue)
6568
}
6669

6770
if (syncTabs && storageType === "local") {
@@ -73,36 +76,62 @@ export class PersistedState<T> {
7376

7477
get current(): T {
7578
this.#subscribe?.();
76-
return this.#current;
79+
const root = this.#deserialize(this.#storage?.getItem(this.#key) as string) ?? this.#current
80+
const proxies = new WeakMap();
81+
const proxy = (value: unknown) => {
82+
if (value === null || value?.constructor.name === 'Date' || typeof value !== 'object') {
83+
return value;
84+
}
85+
let p = proxies.get(value);
86+
if (!p) {
87+
p = new Proxy(value, {
88+
get: (target, property) => {
89+
this.#version;
90+
return proxy(Reflect.get(target, property));
91+
},
92+
set: (target, property, value) => {
93+
this.#version += 1;
94+
Reflect.set(target, property, value);
95+
this.#serialize(root)
96+
return true;
97+
}
98+
});
99+
proxies.set(value, p);
100+
}
101+
return p;
102+
}
103+
return proxy(root);
77104
}
78105

79106
set current(newValue: T) {
80-
this.#current = newValue;
81107
this.#serialize(newValue);
108+
this.#version += 1;
82109
}
83110

84111
#handleStorageEvent = (event: StorageEvent): void => {
85112
if (event.key !== this.#key || event.newValue === null) return;
86-
87-
this.#deserialize(event.newValue);
113+
this.#current = this.#deserialize(event.newValue);
88114
};
89115

90-
#deserialize(value: string): void {
116+
#deserialize(value: string): T | undefined {
91117
try {
92-
this.#current = this.#serializer.deserialize(value);
118+
return this.#serializer.deserialize(value);
93119
} catch (error) {
94120
console.error(`Error when parsing "${value}" from persisted store "${this.#key}"`, error);
121+
return
95122
}
96123
}
97124

98-
#serialize(value: T): void {
125+
#serialize(value: T | undefined): void {
99126
try {
100-
this.#storage?.setItem(this.#key, this.#serializer.serialize(value));
127+
if (value != undefined) {
128+
this.#storage?.setItem(this.#key, this.#serializer.serialize(value));
129+
}
101130
} catch (error) {
102131
console.error(
103132
`Error when writing value from persisted store "${this.#key}" to ${this.#storage}`,
104133
error
105134
);
106135
}
107136
}
108-
}
137+
}

packages/runed/src/lib/utilities/persisted-state/persisted-state.test.svelte.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,31 @@ describe("PersistedState", () => {
3434
expect(persistedState.current).toBe(newValue);
3535
expect(localStorage.getItem(key)).toBe(JSON.stringify(newValue));
3636
});
37+
38+
testWithEffect("updates localStorage when a nested property in current value changes", () => {
39+
const propValue = "test"
40+
const initialValue = { prop: { nested: propValue } };
41+
const newPropValue = "new test"
42+
const newValue = { prop: { nested: newPropValue } };
43+
const persistedState = new PersistedState(key, initialValue);
44+
expect(persistedState.current).toEqual(initialValue);
45+
46+
persistedState.current.prop.nested = newPropValue;
47+
expect(persistedState.current).toEqual(newValue);
48+
expect(localStorage.getItem(key)).toBe(JSON.stringify(newValue));
49+
});
50+
51+
testWithEffect("updates current value when localStorage changes", () => {
52+
const propValue = "test"
53+
const initialValue = { prop: { nested: propValue } };
54+
const newPropValue = "new test"
55+
const newValue = { prop: { nested: newPropValue } };
56+
const persistedState = new PersistedState(key, initialValue);
57+
expect(persistedState.current).toEqual(initialValue);
58+
localStorage.setItem(key, JSON.stringify(newValue))
59+
expect(persistedState.current).toEqual(newValue);
60+
});
61+
3762
});
3863

3964
describe("sessionStorage", () => {
@@ -73,7 +98,7 @@ describe("PersistedState", () => {
7398
const iso2024FebFirst = "2024-02-01T00:00:00.000Z";
7499
const date2024FebFirst = new Date(iso2024FebFirst);
75100
persistedState.current = date2024FebFirst;
76-
expect(persistedState.current).toBe(date2024FebFirst);
101+
expect(persistedState.current).toEqual(date2024FebFirst);
77102
expect(localStorage.getItem(key)).toBe(iso2024FebFirst);
78103
});
79104
});

0 commit comments

Comments
 (0)