Skip to content

Instantly share code, notes, and snippets.

@baetheus
Last active May 14, 2020 22:38
Show Gist options
  • Select an option

  • Save baetheus/7d4d42d774f0b8b5d1163a5a89074e61 to your computer and use it in GitHub Desktop.

Select an option

Save baetheus/7d4d42d774f0b8b5d1163a5a89074e61 to your computer and use it in GitHub Desktop.
A localstorage manager for use with @nll/dux/Store
import * as C from "io-ts/es6/Codec";
import * as E from "fp-ts/es6/Either";
import { draw } from "io-ts/es6/Tree";
import { pipe } from "fp-ts/es6/pipeable";
import { actionCreatorFactory, AsyncActionCreators } from "@nll/dux/Actions";
import { asyncConcatMap } from "@nll/dux/Operators";
import { RunOnce, filterEvery, RunEvery, Store } from "@nll/dux/Store";
import { of, Observable, throwError, interval } from "rxjs";
import { mergeMap, map } from "rxjs/operators";
import { notNil } from "~/libraries/fns";
import { Reducer, caseFn } from "@nll/dux/Reducers";
const trySetState = <A>(codec: C.Codec<A>, key: string) => (s: A) =>
E.tryCatch(
() => {
const encoded = codec.encode(s);
window.localStorage.setItem(key, JSON.stringify(encoded));
return encoded;
},
() => `Failed to set state at localStorage key ${key}`
);
const tryGetState = (key: string) =>
E.tryCatch(
() => window.localStorage.getItem(key),
(_) => "Failed to get state from localStorage"
);
const tryCheckNull = (s: string | null) =>
notNil(s) ? E.right(s) : E.left(`Returned state was ${s}`);
const tryParse = (s: string) =>
E.tryCatch(
(): unknown => JSON.parse(s),
(_) => "Failed to parse json"
);
const tryDecode = <S>(codec: C.Codec<S>) => (s: unknown) => pipe(codec.decode(s), E.mapLeft(draw));
const throwLeft = <E, A>(obs: Observable<E.Either<E, A>>) =>
obs.pipe(mergeMap((v) => (E.isLeft(v) ? throwError(v.left) : of(v.right))));
type StorageAction<A> = AsyncActionCreators<string, A, string>;
const getStateFactory = <A, B extends A>(
codec: C.Codec<A>,
getStateActions: StorageAction<A>
): RunOnce<B> =>
asyncConcatMap(getStateActions, (key) =>
of(
pipe(tryGetState(key), E.chain(tryCheckNull), E.chain(tryParse), E.chain(tryDecode(codec)))
).pipe(throwLeft)
);
const setStateFactory = <A, B extends A>(
codec: C.Codec<A>,
setStateActions: StorageAction<unknown>
): RunEvery<B> =>
filterEvery(setStateActions.pending, (state, { value: params }) => {
const result = trySetState(codec, params)(state);
if (E.isLeft(result)) {
return setStateActions.failure({ error: result.left, params });
}
return setStateActions.success({ result: result.right, params });
});
const intervalFactory = <A>({ pending }: StorageAction<unknown>) => (
key: string,
period: number
): RunOnce<A> => () => interval(period).pipe(map(() => pending(key)));
const setStateCaseFactory = <A, B extends A>({ success }: StorageAction<A>): Reducer<B> =>
caseFn(success, (state: B, { value }) => ({ ...state, ...value }));
/**
* Creates reducers and actions for encoding/decoding parts of store to localstorage.
*
* @example
* import { createStore } from '@nll/dux/Store';
* import { caseFn } from '@nll/dux/Reducers';
* import * as C from 'io-ts/es6/Codec'
*
* type State = { count: number };
* const StateCodec = C.type({ count: C.number });
*
* const store = createStore({ count: 0 });
*
* const { wireup } = createStateRestore<StateCodec, State>(StateCodec, "COUNT_STATE");
* wireup(store, 30 * 1000);
*/
export const createStateRestore = <A, B extends A>(codec: C.Codec<A>, key: string) => {
const creator = actionCreatorFactory(`LOCALSTORAGE_${key}`);
// Set State
const setState = creator.async<string, unknown, string>("SET_STATE");
const setStateRunEvery = setStateFactory<A, B>(codec, setState);
// Get State
const getState = creator.async<string, A, string>("GET_STATE");
const getStateCase = setStateCaseFactory<A, B>(getState);
const getStateRunOnce = getStateFactory<A, B>(codec, getState);
// Set State on Interval
const intervalRunOnce = intervalFactory(setState);
// Wireup Store
const wireup = (store: Store<B>, period = 5 * 1000): Store<B> => {
store
.addReducers(getStateCase)
.addRunEverys(setStateRunEvery)
.addRunOnces(getStateRunOnce, intervalRunOnce(key, period))
.dispatch(getState.pending(key));
return store;
};
return {
setState,
setStateRunEvery,
getState,
getStateCase,
getStateRunOnce,
intervalRunOnce,
wireup,
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment