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}