repos / starfx

a micro-mvc framework for react apps
git clone https://github.com/neurosnap/starfx.git

starfx / src / store
Eric Bower  ·  2025-06-06

persist.ts

  1import { Err, Ok, type Operation, type Result } from "effection";
  2import type { AnyState, Next } from "../types.js";
  3import { select, updateStore } from "./fx.js";
  4import type { UpdaterCtx } from "./types.js";
  5
  6export const PERSIST_LOADER_ID = "@@starfx/persist";
  7
  8export interface PersistAdapter<S extends AnyState> {
  9  getItem(key: string): Operation<Result<Partial<S>>>;
 10  setItem(key: string, item: Partial<S>): Operation<Result<unknown>>;
 11  removeItem(key: string): Operation<Result<unknown>>;
 12}
 13
 14export interface PersistProps<S extends AnyState> {
 15  adapter: PersistAdapter<S>;
 16  allowlist: (keyof S)[];
 17  key: string;
 18  reconciler: (original: S, rehydrated: Partial<S>) => S;
 19  rehydrate: () => Operation<Result<unknown>>;
 20  transform?: TransformFunctions<S>;
 21}
 22interface TransformFunctions<S extends AnyState> {
 23  in(s: Partial<S>): Partial<S>;
 24  out(s: Partial<S>): Partial<S>;
 25}
 26
 27export function createTransform<S extends AnyState>() {
 28  const transformers: TransformFunctions<S> = {
 29    in: (currentState: Partial<S>): Partial<S> => currentState,
 30    out: (currentState: Partial<S>): Partial<S> => currentState,
 31  };
 32
 33  const inTransformer = (state: Partial<S>): Partial<S> =>
 34    transformers.in(state);
 35
 36  const outTransformer = (state: Partial<S>): Partial<S> =>
 37    transformers.out(state);
 38
 39  return {
 40    in: inTransformer,
 41    out: outTransformer,
 42  };
 43}
 44
 45export function createLocalStorageAdapter<
 46  S extends AnyState,
 47>(): PersistAdapter<S> {
 48  return {
 49    getItem: function* (key: string) {
 50      const storage = localStorage.getItem(key) || "{}";
 51      return Ok(JSON.parse(storage));
 52    },
 53    setItem: function* (key: string, s: Partial<S>) {
 54      const state = JSON.stringify(s);
 55      try {
 56        localStorage.setItem(key, state);
 57      } catch (err: any) {
 58        return Err(err);
 59      }
 60      return Ok(undefined);
 61    },
 62    removeItem: function* (key: string) {
 63      localStorage.removeItem(key);
 64      return Ok(undefined);
 65    },
 66  };
 67}
 68
 69export function shallowReconciler<S extends AnyState>(
 70  original: S,
 71  persisted: Partial<S>,
 72): S {
 73  return { ...original, ...persisted };
 74}
 75
 76export function createPersistor<S extends AnyState>({
 77  adapter,
 78  key = "starfx",
 79  reconciler = shallowReconciler,
 80  allowlist = [],
 81  transform,
 82}: Pick<PersistProps<S>, "adapter"> &
 83  Partial<PersistProps<S>>): PersistProps<S> {
 84  function* rehydrate(): Operation<Result<undefined>> {
 85    const persistedState = yield* adapter.getItem(key);
 86    if (!persistedState.ok) {
 87      return Err(persistedState.error);
 88    }
 89    let stateFromStorage = persistedState.value as Partial<S>;
 90
 91    if (transform) {
 92      try {
 93        stateFromStorage = transform.out(persistedState.value);
 94      } catch (err: any) {
 95        console.error("Persistor outbound transformer error:", err);
 96      }
 97    }
 98
 99    const state = yield* select((s) => s);
100    const nextState = reconciler(state as S, stateFromStorage);
101    yield* updateStore<S>((state) => {
102      Object.keys(nextState).forEach((key: keyof S) => {
103        state[key] = nextState[key];
104      });
105    });
106
107    return Ok(undefined);
108  }
109
110  return {
111    key,
112    adapter,
113    allowlist,
114    reconciler,
115    rehydrate,
116    transform,
117  };
118}
119
120export function persistStoreMdw<S extends AnyState>({
121  allowlist,
122  adapter,
123  key,
124  transform,
125}: PersistProps<S>) {
126  return function* (_: UpdaterCtx<S>, next: Next) {
127    yield* next();
128    const state = yield* select((s: S) => s);
129
130    let transformedState: Partial<S> = state;
131    if (transform) {
132      try {
133        transformedState = transform.in(state);
134      } catch (err: any) {
135        console.error("Persistor inbound transformer error:", err);
136      }
137    }
138
139    // empty allowlist list means save entire state
140    if (allowlist.length === 0) {
141      yield* adapter.setItem(key, transformedState);
142      return;
143    }
144
145    const allowedState = allowlist.reduce<Partial<S>>((acc, key) => {
146      if (key in transformedState) {
147        acc[key] = transformedState[key] as S[keyof S];
148      }
149      return acc;
150    }, {});
151
152    yield* adapter.setItem(key, allowedState);
153  };
154}