UNPKG

nitropage

Version:

A free and open source, extensible visual page builder based on SolidStart.

317 lines (272 loc) 7.79 kB
import { cloneDeep, debounce } from "es-toolkit"; import { batch, createEffect, createSignal, onMount } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; export type StorageApi<T = any> = () => Promise<{ store: T; set: (key: string, value: any) => void | Promise<any>; get: (key: string) => any; keys: () => string[] | Promise<string[]>; remove: (key: string) => void; serializer?: (v: any) => any; deserializer?: (v: any) => any; }>; const localStorage: StorageApi<globalThis.Storage> = async () => { const store = globalThis.localStorage; return { store, set(key, value) { store.setItem(key, value); }, get(key) { return store.getItem(key); }, keys() { return Object.keys(store); }, remove(key) { store.removeItem(key); }, serializer(v: any) { return JSON.stringify(v); }, deserializer(v: any) { return JSON.parse(v); }, }; }; export const indexedDbStorage: (args: { name: string }) => StorageApi<any> = (args) => async () => { if (typeof window === "undefined") { return null as any; } const { openDB } = await import("idb"); const store = await openDB<any>(args.name, 1, { upgrade(db) { db.createObjectStore("kv"); }, }); return { store, set(key, value) { return store.put("kv", value, key); }, get(key) { return store.get("kv", key); }, keys() { return store.getAllKeys("kv"); }, remove(key) { return store.delete("kv", key); }, serializer(v: any) { // solid unwrap doesnt return a clean enough object :/ const value = cloneDeep(v); return value; }, }; }; export const sessionStorage: StorageApi<globalThis.Storage> = async () => { const store = globalThis.sessionStorage; return { store, set(key, value) { store.setItem(key, value); }, get(key) { return store.getItem(key); }, keys() { return Object.keys(store); }, remove(key) { store.removeItem(key); }, serializer(v: any) { return JSON.stringify(v); }, deserializer(v: any) { return JSON.parse(v); }, }; }; export const createStorage = function <T extends Record<string, any>>( initState: T, { prefix = "app", timeout = 0, storageApi = localStorage, migrate = undefined as | ((getState: () => Record<string, any>) => boolean) | undefined, } = {}, ) { const [state, setState] = createStore(initState); const [mounted, setMounted] = createSignal(false); const storePrefix = `${prefix}-`; const [getStorageApi, setStorageApi] = createSignal< Awaited<ReturnType<StorageApi>> | undefined >(); const initApi = async () => { const api = await storageApi(); const keys = await api.keys(); if (migrate) { let migratedState: Record<string, any> | undefined = undefined; const getMigrateState = async () => { if (migratedState) return migratedState; migratedState = {}; for (const key of keys) { if (!key.startsWith(storePrefix)) { continue; } const value = await api.get(key); migratedState[key.slice(storePrefix.length)] = api.deserializer ? api.deserializer(value) : value; } return migratedState; }; const migrated = await migrate(getMigrateState); if (migrated && migratedState != undefined) { for (const [key, value] of Object.entries(migratedState)) { await api.set( `${storePrefix}${key}`, api.serializer ? api.serializer(value) : value, ); } } migratedState = undefined; } setStorageApi(api); }; if (typeof window !== "undefined") { initApi(); } let mounted_ = false; const updating = {} as Record<string, any>; const keys = Object.keys(state); const changedBeforeMount = {} as Record<string, any>; const [keyStore, setKeyStore] = createStore( keys.reduce( function (acc, k) { acc[k] = { updateCount: 0, }; return acc; }, {} as Record<string, { updateCount: number }>, ), ); const mountValuePerKey = {} as Record<string, any>; const mountValuePromises = {} as Record<string, Promise<any>>; for (const key of keys) { let storeKey = `${storePrefix}${key}`; let initRun = true; // TODO: Implement localStorage listener // TODO: Clean this up, setState_ is a simpler solution createEffect(() => { const api = getStorageApi(); if (!api) { return; } // During mounts we want to always run this effect even if the state value hasnt changed // We need to run it always to reset updating[key] keyStore[key].updateCount; const value = state[key]; (async () => { if (initRun) { mountValuePromises[key] = api.get(storeKey); mountValuePerKey[key] = await mountValuePromises[key]; } const mountValue = mountValuePerKey[key]; const isInitRun = initRun; initRun = false; // const value = serializer(state[key]); if (isInitRun && mountValue) { return; } // If the key is getting mounted at the moment, we skip the storage set if (updating[key]) { updating[key] = false; return; } if (!mounted_) { changedBeforeMount[key] = true; } if (value === undefined) { api.remove(storeKey); } else { api.set(storeKey, api.serializer ? api.serializer(value) : value); } })(); }); } const timeoutByKey: Record<string, any> = {}; const updateDebouncedByKey = function (key: string) { if (timeoutByKey[key]) { clearTimeout(timeoutByKey[key]); } timeoutByKey[key] = setTimeout(function () { requestAnimationFrame(function () { const api = getStorageApi(); if (!api) { return; } const storeKey = `${storePrefix}${key}`; const value = state[key]; if (value === undefined) { api.remove(storeKey); } else { api.set(storeKey, api.serializer ? api.serializer(value) : value); } }); }, timeout); }; const updateAllKeys = debounce(function () { console.info("reconcile, save all storage keys"); keys.forEach(updateDebouncedByKey); }, timeout); const setState_ = function (...args: any) { const result = (setState as any)(...args); if (typeof args[0] === "string") { updateDebouncedByKey(args[0]); } else { updateAllKeys(); } return result; }; onMount(() => { createEffect(() => { const api = getStorageApi(); if (!api) { return; } (async () => { for (const key of keys) { const mountValue = await mountValuePromises[key]; if (!changedBeforeMount[key] && mountValue) { updating[key] = true; batch(function () { setKeyStore(key, "updateCount", (v) => v + 1); //setUpdatingCount(updatingCount() + 1); setState( key as any, reconcile( api.deserializer ? api.deserializer(mountValue) : mountValue, ), ); }); } mountValuePerKey[key] = null; } setMounted(true); mounted_ = true; })(); }); }); return [state, setState_, mounted] as [ typeof state, typeof setState, typeof mounted, ]; };