Skip to content

Commit e2eecf9

Browse files
Abdelrahmanhuntabyte
Abdelrahman
andauthored
Fix: PersistedState (#177)
Co-authored-by: Hunter Johnston <[email protected]>
1 parent f7cbda7 commit e2eecf9

File tree

8 files changed

+225
-450
lines changed

8 files changed

+225
-450
lines changed

.changeset/happy-keys-obey.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"runed": minor
3+
---
4+
5+
Breaking: Set minimum peer dep to `[email protected]` or greater to support [`createSubscriber`](https://svelte.dev/docs/svelte/svelte-reactivity#createSubscriber) API

.changeset/pink-buttons-perform.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"runed": minor
3+
---
4+
5+
Enable `PersistedState` to be used in `.svelte.[ts|js]` files

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"prettier-plugin-svelte": "^3.3.2",
5656
"prettier-plugin-tailwindcss": "^0.6.8",
5757
"readline-sync": "^1.4.10",
58-
"svelte": "^5.2.11",
58+
"svelte": "^5.11.0",
5959
"svelte-eslint-parser": "^0.43.0",
6060
"typescript": "^5.6.3",
6161
"typescript-eslint": "^8.10.0",

packages/runed/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"!dist/**/*.spec.*"
4646
],
4747
"peerDependencies": {
48-
"svelte": "^5.0.0-next.1"
48+
"svelte": "^5.7.0"
4949
},
5050
"devDependencies": {
5151
"@sveltejs/kit": "^2.5.3",
@@ -61,8 +61,8 @@
6161
"jsdom": "^24.0.0",
6262
"publint": "^0.1.9",
6363
"resize-observer-polyfill": "^1.5.1",
64-
"svelte": "^5.0.0-next.243",
65-
"svelte-check": "^3.6.0",
64+
"svelte": "^5.11.0",
65+
"svelte-check": "^4.1.1",
6666
"tslib": "^2.4.1",
6767
"typescript": "^5.0.0",
6868
"vite": "^5.0.3",
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,20 @@
1-
import { addEventListener } from "$lib/internal/utils/event.js";
1+
import { on } from "svelte/events";
2+
import { createSubscriber } from "svelte/reactivity";
23

34
type Serializer<T> = {
45
serialize: (value: T) => string;
56
deserialize: (value: string) => T;
67
};
78

8-
type GetValueFromStorageResult<T> =
9-
| {
10-
found: true;
11-
value: T;
12-
}
13-
| {
14-
found: false;
15-
value: null;
16-
};
17-
function getValueFromStorage<T>({
18-
key,
19-
storage,
20-
serializer,
21-
}: {
22-
key: string;
23-
storage: Storage | null;
24-
serializer: Serializer<T>;
25-
}): GetValueFromStorageResult<T> {
26-
if (!storage) return { found: false, value: null };
27-
28-
const value = storage.getItem(key);
29-
30-
if (value === null) return { found: false, value: null };
31-
32-
try {
33-
return {
34-
found: true,
35-
value: serializer.deserialize(value),
36-
};
37-
} catch (e) {
38-
console.error(`Error when parsing ${value} from persisted store "${key}"`, e);
39-
return {
40-
found: false,
41-
value: null,
42-
};
43-
}
44-
}
45-
46-
function setValueToStorage<T>({
47-
key,
48-
value,
49-
storage,
50-
serializer,
51-
}: {
52-
key: string;
53-
value: T;
54-
storage: Storage | null;
55-
serializer: Serializer<T>;
56-
}) {
57-
if (!storage) return;
58-
59-
try {
60-
storage.setItem(key, serializer.serialize(value));
61-
} catch (e) {
62-
console.error(`Error when writing value from persisted store "${key}" to ${storage}`, e);
63-
}
64-
}
65-
669
type StorageType = "local" | "session";
6710

68-
function getStorage(storageType: StorageType): Storage | null {
69-
if (typeof window === "undefined") return null;
70-
71-
const storageByStorageType = {
72-
local: localStorage,
73-
session: sessionStorage,
74-
} satisfies Record<StorageType, Storage>;
75-
76-
return storageByStorageType[storageType];
11+
function getStorage(storageType: StorageType): Storage {
12+
switch (storageType) {
13+
case "local":
14+
return localStorage;
15+
case "session":
16+
return sessionStorage;
17+
}
7718
}
7819

7920
type PersistedStateOptions<T> = {
@@ -96,8 +37,9 @@ type PersistedStateOptions<T> = {
9637
export class PersistedState<T> {
9738
#current: T = $state()!;
9839
#key: string;
99-
#storage: Storage | null;
10040
#serializer: Serializer<T>;
41+
#storage?: Storage;
42+
#subscribe?: VoidFunction;
10143

10244
constructor(key: string, initialValue: T, options: PersistedStateOptions<T> = {}) {
10345
const {
@@ -106,53 +48,59 @@ export class PersistedState<T> {
10648
syncTabs = true,
10749
} = options;
10850

51+
this.#current = initialValue;
10952
this.#key = key;
110-
this.#storage = getStorage(storageType);
11153
this.#serializer = serializer;
11254

113-
const valueFromStorage = getValueFromStorage({
114-
key: this.#key,
115-
storage: this.#storage,
116-
serializer: this.#serializer,
117-
});
118-
119-
this.#current = valueFromStorage.found ? valueFromStorage.value : initialValue;
55+
if (typeof window === "undefined") return;
12056

121-
$effect(() => {
122-
setValueToStorage({
123-
key: this.#key,
124-
value: this.#current,
125-
storage: this.#storage,
126-
serializer: this.#serializer,
127-
});
128-
});
129-
130-
$effect(() => {
131-
if (!syncTabs || storageType !== "local") return;
57+
const storage = getStorage(storageType);
58+
this.#storage = storage;
13259

133-
return addEventListener(window, "storage", this.#handleStorageEvent.bind(this));
134-
});
135-
}
136-
137-
#handleStorageEvent(event: StorageEvent): void {
138-
if (event.key !== this.#key || !this.#storage) return;
139-
140-
const valueFromStorage = getValueFromStorage({
141-
key: this.#key,
142-
storage: this.#storage,
143-
serializer: this.#serializer,
144-
});
60+
const existingValue = storage.getItem(key);
61+
if (existingValue !== null) {
62+
this.#deserialize(existingValue);
63+
}
14564

146-
if (valueFromStorage.found) {
147-
this.#current = valueFromStorage.value;
65+
if (syncTabs && storageType === "local") {
66+
this.#subscribe = createSubscriber(() => {
67+
return on(window, "storage", this.#handleStorageEvent);
68+
});
14869
}
14970
}
15071

15172
get current(): T {
73+
this.#subscribe?.();
15274
return this.#current;
15375
}
15476

15577
set current(newValue: T) {
15678
this.#current = newValue;
79+
this.#serialize(newValue);
80+
}
81+
82+
#handleStorageEvent = (event: StorageEvent): void => {
83+
if (event.key !== this.#key || event.newValue === null) return;
84+
85+
this.#deserialize(event.newValue);
86+
};
87+
88+
#deserialize(value: string): void {
89+
try {
90+
this.#current = this.#serializer.deserialize(value);
91+
} catch (error) {
92+
console.error(`Error when parsing "${value}" from persisted store "${this.#key}"`, error);
93+
}
94+
}
95+
96+
#serialize(value: T): void {
97+
try {
98+
this.#storage?.setItem(this.#key, this.#serializer.serialize(value));
99+
} catch (error) {
100+
console.error(
101+
`Error when writing value from persisted store "${this.#key}" to ${this.#storage}`,
102+
error
103+
);
104+
}
157105
}
158106
}

0 commit comments

Comments
 (0)