Remote storage for storywriter
This commit is contained in:
parent
2871c19177
commit
ba3b500063
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { useEffect, useReducer, useState, type Dispatch, type Reducer, type StateUpdater } from "preact/hooks";
|
||||||
|
import { loadObject, saveObject } from "@common/storage";
|
||||||
|
|
||||||
|
export const useRemoteState = <T>(key: string, initialValue: T) => {
|
||||||
|
const storedKey = `useRemoteState.${key}`;
|
||||||
|
|
||||||
|
const [value, setValue] = useState<T>(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadObject<T>(storedKey, initialValue).then(setValue);
|
||||||
|
}, [storedKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
saveObject(storedKey, value);
|
||||||
|
}, [storedKey, value]);
|
||||||
|
|
||||||
|
return [value, setValue] as [T, Dispatch<StateUpdater<T>>];
|
||||||
|
};
|
||||||
|
|
||||||
|
const hydrate = Symbol('hydrate');
|
||||||
|
|
||||||
|
type HydrateAction<T> = { type: typeof hydrate; payload: T };
|
||||||
|
|
||||||
|
const isHydrateAction = <T>(action: unknown): action is HydrateAction<T> =>
|
||||||
|
typeof action === 'object' && action !== null && 'type' in action && action.type === hydrate;
|
||||||
|
|
||||||
|
const wrapReducer = <T, A>(reducer: Reducer<T, A>): Reducer<T, A | HydrateAction<T>> =>
|
||||||
|
(state, action) => {
|
||||||
|
if (isHydrateAction<T>(action)) {
|
||||||
|
return action.payload;
|
||||||
|
}
|
||||||
|
return reducer(state, action);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemoteReducer = <T, A>(key: string, reducer: Reducer<T, A>, initialValue: T) => {
|
||||||
|
const storedKey = `useRemoteReducer.${key}`;
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(wrapReducer(reducer), initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadObject<T>(storedKey, initialValue).then((loaded) => {
|
||||||
|
dispatch({ type: hydrate, payload: loaded });
|
||||||
|
});
|
||||||
|
}, [storedKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
saveObject(storedKey, state);
|
||||||
|
}, [storedKey, state]);
|
||||||
|
|
||||||
|
return [state, dispatch] as [T, Dispatch<A>];
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
const API_KEY = 'awoorwa32';
|
const API_KEY = 'awoorwa32';
|
||||||
|
|
||||||
|
const saveThrottleDelay = 2000;
|
||||||
|
const pendingSaves = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
export const loadObject = async <T>(key: string, defaultObject: T): Promise<T> => {
|
export const loadObject = async <T>(key: string, defaultObject: T): Promise<T> => {
|
||||||
let localObject: Partial<T> = {};
|
let localObject: Partial<T> = {};
|
||||||
|
|
||||||
|
|
@ -23,7 +26,16 @@ export const loadObject = async <T>(key: string, defaultObject: T): Promise<T> =
|
||||||
return { ...defaultObject, ...localObject, ...remoteObject };
|
return { ...defaultObject, ...localObject, ...remoteObject };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const saveObject = async <T>(key: string, obj: T) => {
|
export const saveObject = <T>(key: string, obj: T) => {
|
||||||
|
const existing = pendingSaves.get(key);
|
||||||
|
if (existing) clearTimeout(existing);
|
||||||
|
pendingSaves.set(key, setTimeout(() => {
|
||||||
|
pendingSaves.delete(key);
|
||||||
|
doSaveObject(key, obj);
|
||||||
|
}, saveThrottleDelay));
|
||||||
|
};
|
||||||
|
|
||||||
|
const doSaveObject = async <T>(key: string, obj: T) => {
|
||||||
const saveData = JSON.stringify(obj);
|
const saveData = JSON.stringify(obj);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
import { useContext, useMemo, useReducer } from "preact/hooks";
|
import { useContext, useMemo } from "preact/hooks";
|
||||||
|
|
||||||
import LLM from "../utils/llm";
|
import LLM from "../utils/llm";
|
||||||
import Chapters from "../utils/chapters";
|
import Chapters from "../utils/chapters";
|
||||||
import { useStoredReducer } from "@common/hooks/useStoredState";
|
import { useRemoteReducer } from "@common/hooks/useRemote";
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -561,7 +561,7 @@ export const useAppState = () => useContext(StateContext);
|
||||||
// ─── Provider ────────────────────────────────────────────────────────────────
|
// ─── Provider ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const StateContextProvider = ({ children }: { children?: any }) => {
|
export const StateContextProvider = ({ children }: { children?: any }) => {
|
||||||
const [state, dispatch] = useStoredReducer('storywriter.state', reducer, DEFAULT_STATE);
|
const [state, dispatch] = useRemoteReducer('storywriter.state', reducer, DEFAULT_STATE);
|
||||||
|
|
||||||
const value = useMemo<AppState>(() => ({
|
const value = useMemo<AppState>(() => ({
|
||||||
stories: state.stories,
|
stories: state.stories,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue