221 lines
5.4 KiB
TypeScript
221 lines
5.4 KiB
TypeScript
import {
|
|
JSX,
|
|
Show,
|
|
createContext,
|
|
createSignal,
|
|
onMount,
|
|
useContext,
|
|
} from "solid-js";
|
|
import { SetStoreFunction, createStore } from "solid-js/store";
|
|
|
|
import equal from "fast-deep-equal";
|
|
import localforage from "localforage";
|
|
|
|
import { AbstractStore, Store } from "./stores";
|
|
import { Auth } from "./stores/Auth";
|
|
import { Draft } from "./stores/Draft";
|
|
import { Experiments } from "./stores/Experiments";
|
|
import { Keybinds } from "./stores/Keybinds";
|
|
import { Layout } from "./stores/Layout";
|
|
import { LinkSafety } from "./stores/LinkSafety";
|
|
import { Locale } from "./stores/Locale";
|
|
import { NotificationOptions } from "./stores/NotificationOptions";
|
|
import { Ordering } from "./stores/Ordering";
|
|
import { Settings } from "./stores/Settings";
|
|
import { Sync } from "./stores/Sync";
|
|
import { Theme } from "./stores/Theme";
|
|
import { Voice } from "./stores/Voice";
|
|
|
|
export { SyncWorker } from "./SyncWorker";
|
|
|
|
/**
|
|
* Introduce some delay before writing state to disk
|
|
*/
|
|
const DISK_WRITE_WAIT_MS = 1200;
|
|
|
|
/**
|
|
* Stores for which we don't want to wait to write to
|
|
*/
|
|
const IGNORE_WRITE_DELAY = ["auth"];
|
|
|
|
/**
|
|
* Global application state
|
|
*/
|
|
export class State {
|
|
// internal data management
|
|
private store: Store;
|
|
private setStore: SetStoreFunction<Store>;
|
|
private writeQueue: Record<string, number>;
|
|
|
|
// define all stores
|
|
auth = new Auth(this);
|
|
draft = new Draft(this);
|
|
experiments = new Experiments(this);
|
|
keybinds = new Keybinds(this);
|
|
layout = new Layout(this);
|
|
linkSafety = new LinkSafety(this);
|
|
locale = new Locale(this);
|
|
notifications = new NotificationOptions(this);
|
|
ordering = new Ordering(this);
|
|
settings = new Settings(this);
|
|
sync = new Sync(this);
|
|
theme = new Theme(this);
|
|
voice = new Voice(this);
|
|
|
|
/**
|
|
* Iterate over all available stores
|
|
* @returns Array of stores
|
|
*/
|
|
private iterStores() {
|
|
return (
|
|
Object.keys(this).filter(
|
|
(key) =>
|
|
(this[key as keyof State] as unknown as { _storeHint: boolean })
|
|
?._storeHint,
|
|
) as (keyof Store)[]
|
|
).map((key) => this[key] as AbstractStore<typeof key, Store[typeof key]>);
|
|
}
|
|
|
|
/**
|
|
* Generate all store defaults / initial store
|
|
* @returns Defaults object
|
|
*/
|
|
private defaults() {
|
|
const defaults: Partial<Store> = {};
|
|
|
|
for (const store of this.iterStores()) {
|
|
defaults[store.getKey()] = store.default() as never;
|
|
}
|
|
|
|
return defaults;
|
|
}
|
|
|
|
/**
|
|
* Construct the global application state
|
|
*/
|
|
constructor() {
|
|
const [store, setStore] = createStore(this.defaults() as Store);
|
|
|
|
this.store = store as never;
|
|
this.setStore = setStore;
|
|
this.writeQueue = {};
|
|
}
|
|
|
|
/**
|
|
* Write some data to the store and disk
|
|
*/
|
|
private write: SetStoreFunction<Store> = (...args: unknown[]) => {
|
|
// pass the data to the store
|
|
(this.setStore as (...args: unknown[]) => void)(...args);
|
|
|
|
// resolve key
|
|
const key = args[0] as string;
|
|
|
|
// touch the key if syncable
|
|
this.sync.touchIfSyncable(key);
|
|
|
|
// remove existing queued task if it exists
|
|
if (this.writeQueue[key]) {
|
|
clearTimeout(this.writeQueue[key]);
|
|
}
|
|
|
|
// queue for writing to disk
|
|
this.writeQueue[key] = setTimeout(
|
|
() => {
|
|
// remove from write queue
|
|
delete this.writeQueue[key];
|
|
|
|
// write the entire key to storage
|
|
localforage.setItem(
|
|
key,
|
|
JSON.parse(
|
|
JSON.stringify((this.store as Record<string, unknown>)[key]),
|
|
),
|
|
);
|
|
|
|
if (import.meta.env.DEV) {
|
|
console.info("[store.save] Wrote state to disk.");
|
|
}
|
|
},
|
|
IGNORE_WRITE_DELAY.includes(key) ? 0 : DISK_WRITE_WAIT_MS,
|
|
) as unknown as number;
|
|
};
|
|
|
|
/**
|
|
* Write data to store / disk and then synchronise it
|
|
*/
|
|
set: SetStoreFunction<Store> = (...args: unknown[]) => {
|
|
// write to store and storage
|
|
(this.write as (...args: unknown[]) => void)(...args);
|
|
|
|
// run side-effects
|
|
if (import.meta.env.DEV) {
|
|
console.debug("[store] updated data", args[0]);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get a store's value by its key
|
|
* @param key Store's key
|
|
* @returns Store's value
|
|
*/
|
|
get<T extends keyof Store>(key: T): Store[T] {
|
|
return this.store[key];
|
|
}
|
|
|
|
/**
|
|
* Hydrate the state from disk and run side-effects
|
|
*/
|
|
async hydrate() {
|
|
// load all data first
|
|
for (const store of this.iterStores()) {
|
|
const data = await localforage.getItem(store.getKey());
|
|
|
|
if (data) {
|
|
// validate the incoming data
|
|
const cleanData = store.clean(data);
|
|
|
|
if (!equal(data, cleanData)) {
|
|
// write back to disk if it has changed
|
|
this.write(store.getKey(), cleanData);
|
|
} else {
|
|
this.setStore(store.getKey(), data);
|
|
}
|
|
}
|
|
}
|
|
|
|
// then run side-effects
|
|
for (const store of this.iterStores()) {
|
|
store.hydrate();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* State context
|
|
*/
|
|
const stateContext = createContext<State>(null! as State);
|
|
|
|
/**
|
|
* Mount state context
|
|
*/
|
|
export function StateContext(props: { children: JSX.Element }) {
|
|
const stateLocal = new State();
|
|
const [ready, setReady] = createSignal(false);
|
|
|
|
onMount(() => stateLocal.hydrate().then(() => setReady(true)));
|
|
|
|
return (
|
|
<stateContext.Provider value={stateLocal}>
|
|
<Show when={ready()}>{props.children}</Show>
|
|
</stateContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Use application state
|
|
*/
|
|
export function useState() {
|
|
return useContext(stateContext);
|
|
}
|