repos / starfx

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

starfx / store
Vlad · 03 Oct 24

persist.ts

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