1
0
Fork 0

Remote storage for storywriter

This commit is contained in:
Pabloader 2026-03-30 11:40:27 +00:00
parent 2871c19177
commit ba3b500063
4 changed files with 67 additions and 4 deletions

View File

@ -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>];
};

View File

@ -1,5 +1,8 @@
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> => {
let localObject: Partial<T> = {};
@ -23,7 +26,16 @@ export const loadObject = async <T>(key: string, defaultObject: T): Promise<T> =
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);
try {

View File

@ -1,9 +1,9 @@
import { createContext } from "preact";
import { useContext, useMemo, useReducer } from "preact/hooks";
import { useContext, useMemo } from "preact/hooks";
import LLM from "../utils/llm";
import Chapters from "../utils/chapters";
import { useStoredReducer } from "@common/hooks/useStoredState";
import { useRemoteReducer } from "@common/hooks/useRemote";
// ─── Types ────────────────────────────────────────────────────────────────────
@ -561,7 +561,7 @@ export const useAppState = () => useContext(StateContext);
// ─── Provider ────────────────────────────────────────────────────────────────
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>(() => ({
stories: state.stories,