repos / starfx

supercharged async flow control library.
git clone https://github.com/neurosnap/starfx.git

starfx / store
Eric Bower · 19 Jan 24

persist.ts

  1import { Err, Ok, type Operation, type Result } from "../deps.ts";
  2import type { AnyState, Next } from "../types.ts";
  3import { select, updateStore } from "./fx.ts";
  4import type { UpdaterCtx } from "./types.ts";
  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}
 21
 22export function createLocalStorageAdapter<S extends AnyState>(): PersistAdapter<
 23  S
 24> {
 25  return {
 26    getItem: function* (key: string) {
 27      const storage = localStorage.getItem(key) || "{}";
 28      return Ok(JSON.parse(storage));
 29    },
 30    setItem: function* (key: string, s: Partial<S>) {
 31      const state = JSON.stringify(s);
 32      try {
 33        localStorage.setItem(key, state);
 34      } catch (err: any) {
 35        return Err(err);
 36      }
 37      return Ok(undefined);
 38    },
 39    removeItem: function* (key: string) {
 40      localStorage.removeItem(key);
 41      return Ok(undefined);
 42    },
 43  };
 44}
 45
 46export function shallowReconciler<S extends AnyState>(
 47  original: S,
 48  persisted: Partial<S>,
 49): S {
 50  return { ...original, ...persisted };
 51}
 52
 53export function createPersistor<S extends AnyState>(
 54  { adapter, key = "starfx", reconciler = shallowReconciler, allowlist = [] }:
 55    & Pick<PersistProps<S>, "adapter">
 56    & Partial<PersistProps<S>>,
 57): PersistProps<S> {
 58  function* rehydrate(): Operation<Result<undefined>> {
 59    const persistedState = yield* adapter.getItem(key);
 60    if (!persistedState.ok) {
 61      return Err(persistedState.error);
 62    }
 63
 64    const state = yield* select((s) => s);
 65    const nextState = reconciler(state as S, persistedState.value);
 66    yield* updateStore<S>(function (state) {
 67      Object.keys(nextState).forEach((key: keyof S) => {
 68        state[key] = nextState[key];
 69      });
 70    });
 71
 72    return Ok(undefined);
 73  }
 74
 75  return {
 76    key,
 77    adapter,
 78    allowlist,
 79    reconciler,
 80    rehydrate,
 81  };
 82}
 83
 84export function persistStoreMdw<S extends AnyState>(
 85  { allowlist, adapter, key }: PersistProps<S>,
 86) {
 87  return function* (_: UpdaterCtx<S>, next: Next) {
 88    yield* next();
 89    const state = yield* select((s: S) => s);
 90    // empty allowlist list means save entire state
 91    if (allowlist.length === 0) {
 92      yield* adapter.setItem(key, state);
 93      return;
 94    }
 95
 96    const allowedState = allowlist.reduce<Partial<S>>((acc, key) => {
 97      acc[key] = state[key];
 98      return acc;
 99    }, {});
100    yield* adapter.setItem(key, allowedState);
101  };
102}