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