Skip to content

Commit

Permalink
Fix: PersistedState (#177)
Browse files Browse the repository at this point in the history
Co-authored-by: Hunter Johnston <[email protected]>
abdel-17 and huntabyte authored Dec 16, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent f7cbda7 commit e2eecf9
Showing 8 changed files with 225 additions and 450 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-keys-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"runed": minor
---

Breaking: Set minimum peer dep to `[email protected]` or greater to support [`createSubscriber`](https://svelte.dev/docs/svelte/svelte-reactivity#createSubscriber) API
5 changes: 5 additions & 0 deletions .changeset/pink-buttons-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"runed": minor
---

Enable `PersistedState` to be used in `.svelte.[ts|js]` files
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -55,7 +55,7 @@
"prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.8",
"readline-sync": "^1.4.10",
"svelte": "^5.2.11",
"svelte": "^5.11.0",
"svelte-eslint-parser": "^0.43.0",
"typescript": "^5.6.3",
"typescript-eslint": "^8.10.0",
6 changes: 3 additions & 3 deletions packages/runed/package.json
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@
"!dist/**/*.spec.*"
],
"peerDependencies": {
"svelte": "^5.0.0-next.1"
"svelte": "^5.7.0"
},
"devDependencies": {
"@sveltejs/kit": "^2.5.3",
@@ -61,8 +61,8 @@
"jsdom": "^24.0.0",
"publint": "^0.1.9",
"resize-observer-polyfill": "^1.5.1",
"svelte": "^5.0.0-next.243",
"svelte-check": "^3.6.0",
"svelte": "^5.11.0",
"svelte-check": "^4.1.1",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3",
152 changes: 50 additions & 102 deletions packages/runed/src/lib/utilities/PersistedState/PersistedState.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,20 @@
import { addEventListener } from "$lib/internal/utils/event.js";
import { on } from "svelte/events";
import { createSubscriber } from "svelte/reactivity";

type Serializer<T> = {
serialize: (value: T) => string;
deserialize: (value: string) => T;
};

type GetValueFromStorageResult<T> =
| {
found: true;
value: T;
}
| {
found: false;
value: null;
};
function getValueFromStorage<T>({
key,
storage,
serializer,
}: {
key: string;
storage: Storage | null;
serializer: Serializer<T>;
}): GetValueFromStorageResult<T> {
if (!storage) return { found: false, value: null };

const value = storage.getItem(key);

if (value === null) return { found: false, value: null };

try {
return {
found: true,
value: serializer.deserialize(value),
};
} catch (e) {
console.error(`Error when parsing ${value} from persisted store "${key}"`, e);
return {
found: false,
value: null,
};
}
}

function setValueToStorage<T>({
key,
value,
storage,
serializer,
}: {
key: string;
value: T;
storage: Storage | null;
serializer: Serializer<T>;
}) {
if (!storage) return;

try {
storage.setItem(key, serializer.serialize(value));
} catch (e) {
console.error(`Error when writing value from persisted store "${key}" to ${storage}`, e);
}
}

type StorageType = "local" | "session";

function getStorage(storageType: StorageType): Storage | null {
if (typeof window === "undefined") return null;

const storageByStorageType = {
local: localStorage,
session: sessionStorage,
} satisfies Record<StorageType, Storage>;

return storageByStorageType[storageType];
function getStorage(storageType: StorageType): Storage {
switch (storageType) {
case "local":
return localStorage;
case "session":
return sessionStorage;
}
}

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

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

this.#current = initialValue;
this.#key = key;
this.#storage = getStorage(storageType);
this.#serializer = serializer;

const valueFromStorage = getValueFromStorage({
key: this.#key,
storage: this.#storage,
serializer: this.#serializer,
});

this.#current = valueFromStorage.found ? valueFromStorage.value : initialValue;
if (typeof window === "undefined") return;

$effect(() => {
setValueToStorage({
key: this.#key,
value: this.#current,
storage: this.#storage,
serializer: this.#serializer,
});
});

$effect(() => {
if (!syncTabs || storageType !== "local") return;
const storage = getStorage(storageType);
this.#storage = storage;

return addEventListener(window, "storage", this.#handleStorageEvent.bind(this));
});
}

#handleStorageEvent(event: StorageEvent): void {
if (event.key !== this.#key || !this.#storage) return;

const valueFromStorage = getValueFromStorage({
key: this.#key,
storage: this.#storage,
serializer: this.#serializer,
});
const existingValue = storage.getItem(key);
if (existingValue !== null) {
this.#deserialize(existingValue);
}

if (valueFromStorage.found) {
this.#current = valueFromStorage.value;
if (syncTabs && storageType === "local") {
this.#subscribe = createSubscriber(() => {
return on(window, "storage", this.#handleStorageEvent);
});
}
}

get current(): T {
this.#subscribe?.();
return this.#current;
}

set current(newValue: T) {
this.#current = newValue;
this.#serialize(newValue);
}

#handleStorageEvent = (event: StorageEvent): void => {
if (event.key !== this.#key || event.newValue === null) return;

this.#deserialize(event.newValue);
};

#deserialize(value: string): void {
try {
this.#current = this.#serializer.deserialize(value);
} catch (error) {
console.error(`Error when parsing "${value}" from persisted store "${this.#key}"`, error);
}
}

#serialize(value: T): void {
try {
this.#storage?.setItem(this.#key, this.#serializer.serialize(value));
} catch (error) {
console.error(
`Error when writing value from persisted store "${this.#key}" to ${this.#storage}`,
error
);
}
}
}
Loading

0 comments on commit e2eecf9

Please sign in to comment.