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}