Skip to content

Commit

Permalink
adapter pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
Not-Jayden committed Jul 15, 2024
1 parent 41f9495 commit df65dd4
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 62 deletions.
246 changes: 203 additions & 43 deletions packages/runed/src/lib/utilities/PersistedState/PersistedState.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,49 @@
import { untrack } from "svelte";
import { addEventListener } from "$lib/internal/utils/event.js";

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

interface StorageAdapter {
getItem: (key: string) => Promise<string | null>;
setItem: (key: string, value: string) => Promise<void>;
subscribe?: (callback: (key: string, newValue: string | null) => void) => () => void;
}

class BrowserStorageAdapter implements StorageAdapter {
#storage: Storage;

constructor(storage: Storage) {
this.#storage = storage;
}

async getItem(key: string): Promise<string | null> {
return this.#storage.getItem(key);
}

async setItem(key: string, value: string): Promise<void> {
this.#storage.setItem(key, value);
}

subscribe(callback: (key: string, newValue: string | null) => void): () => void {
const listener = (event: StorageEvent) => {
if (event.key === null) {
return;
}

callback(event.key, event.newValue);
};

const unsubscribe = addEventListener(window, "storage", listener.bind(this));

return () => {
unsubscribe();
};
}
}

type GetValueFromStorageResult<T> =
| {
found: true;
Expand All @@ -14,20 +53,21 @@ type GetValueFromStorageResult<T> =
found: false;
value: null;
};
function getValueFromStorage<T>({

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

const value = storage.getItem(key);
const value = await storage.getItem(key);
if (value === null) {
return { found: false, value: null };
}
Expand All @@ -46,46 +86,149 @@ function getValueFromStorage<T>({
}
}

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

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

type StorageType = "local" | "session";
// 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];
// }

// type PersistedStateOptions<T> = {
// /** The storage type to use. Defaults to `local`. */
// storage?: StorageType;
// /** The serializer to use. Defaults to `JSON.stringify` and `JSON.parse`. */
// serializer?: Serializer<T>;
// /** Whether to sync with the state changes from other tabs. Defaults to `true`. */
// syncTabs?: boolean;
// };

// /**
// * Creates reactive state that is persisted and synchronized across browser sessions and tabs using Web Storage.
// * @param key The unique key used to store the state in the storage.
// * @param initialValue The initial value of the state if not already present in the storage.
// * @param options Configuration options including storage type, serializer for complex data types, and whether to sync state changes across tabs.
// *
// * @see {@link https://runed.dev/docs/utilities/persisted-state}
// */
// export class Persisted<T> {
// #current = $state() as T;
// #key: string;
// #storage: Storage | null;
// #serializer: Serializer<T>;

// constructor(key: string, initialValue: T, options: PersistedStateOptions<T> = {}) {
// const {
// storage: storageType = "local",
// serializer = { serialize: JSON.stringify, deserialize: JSON.parse },
// syncTabs = true,
// } = options;

// 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;

function getStorage(storageType: StorageType): Storage | null {
// $effect(() => {
// setValueToStorage({
// key: this.#key,
// value: this.#current,
// storage: this.#storage,
// serializer: this.#serializer,
// });
// });

// $effect(() => {
// if (!syncTabs) {
// return;
// }

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

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

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

// if (valueFromStorage.found) {
// this.#current = valueFromStorage.value;
// }
// }

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

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

function getStorageAdapterForStorageType(storageType: StorageType): StorageAdapter | null {
if (typeof window === "undefined") {
return null;
}

const storageByStorageType = {
local: localStorage,
session: sessionStorage,
} satisfies Record<StorageType, Storage>;
const storageAdapterByStorageType = {
local: new BrowserStorageAdapter(localStorage),
session: new BrowserStorageAdapter(sessionStorage),
};

return storageByStorageType[storageType];
return storageAdapterByStorageType[storageType];
}

type StorageType = "local" | "session";

type PersistedStateOptions<T> = {
/** The storage type to use. Defaults to `local`. */
storage?: StorageType;
storage?: StorageType | StorageAdapter;
/** The serializer to use. Defaults to `JSON.stringify` and `JSON.parse`. */
serializer?: Serializer<T>;
/** Whether to sync with the state changes from other tabs. Defaults to `true`. */
Expand All @@ -100,70 +243,87 @@ type PersistedStateOptions<T> = {
*
* @see {@link https://runed.dev/docs/utilities/persisted-state}
*/
export class PersistedState<T> {
export class Persisted<T> {
#current = $state() as T;
#isInitialized = $state(false);
#initialValue: T;
#key: string;
#storage: Storage | null;
#storageAdapter: StorageAdapter | null;
#serializer: Serializer<T>;

constructor(key: string, initialValue: T, options: PersistedStateOptions<T> = {}) {
const {
storage: storageType = "local",
storage = "local",
serializer = { serialize: JSON.stringify, deserialize: JSON.parse },
syncTabs = true,
} = options;

this.#key = key;
this.#storage = getStorage(storageType);
this.#initialValue = initialValue;
this.#storageAdapter =
typeof storage === "string" ? getStorageAdapterForStorageType(storage) : storage;
this.#serializer = serializer;

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

this.#current = valueFromStorage.found ? valueFromStorage.value : initialValue;

$effect(() => {
if (!this.#isInitialized) {
return;
}

setValueToStorage({
key: this.#key,
value: this.#current,
storage: this.#storage,
storage: this.#storageAdapter,
serializer: this.#serializer,
});
});

$effect(() => {
if (!syncTabs) {
return;
}
if (syncTabs) {
$effect(() => {
return untrack(() => {
if (!this.#storageAdapter?.subscribe) {
return;
}

return addEventListener(window, "storage", this.#handleStorageEvent.bind(this));
});
const unsubscribe = this.#storageAdapter
.subscribe(async (key, newValue) => {
if (key === this.#key && newValue !== null) {
this.#current = this.#serializer.deserialize(newValue);
}
})
.bind(this);

return unsubscribe;
});
});
}

this.init();
}

#handleStorageEvent(event: StorageEvent) {
if (event.key !== this.#key || !this.#storage) {
async init() {
if (!this.#storageAdapter) {
return;
}

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

if (valueFromStorage.found) {
this.#current = valueFromStorage.value;
if (!valueFromStorage.found) {
return;
}

this.#current = valueFromStorage.value;
this.#isInitialized = true;
}

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

set current(newValue: T) {
this.#current = newValue;
this.#isInitialized ||= true;
}
}
Loading

0 comments on commit df65dd4

Please sign in to comment.