- commit
- fbe29cb
- parent
- eb28818
- author
- Vlad
- date
- 2024-10-03 13:56:32 +0000 UTC
refactor(persist): update persist.ts (#50) Refactor persist.ts to improve code organization and readability. - Add support for transform functions to modify the state before storing and after retrieving from storage. - Implement inbound and outbound transformers to handle state transformations. - Handle errors thrown by transformers and log them to the console. - Use the transformed state when reconciling and persisting the store. - Update persistStoreMdw function to use the transformed state when saving to storage. - Add tests for persist transformers
2 files changed,
+851,
-10
+66,
-8
1@@ -1,8 +1,8 @@
2-import { Err, Ok, type Operation, type Result } from "../deps.ts";
3-import type { AnyState, Next } from "../types.ts";
4+import { Err, Ok, Operation, Result } from "../deps.ts";
5 import { select, updateStore } from "./fx.ts";
6-import type { UpdaterCtx } from "./types.ts";
7
8+import type { AnyState, Next } from "../types.ts";
9+import type { UpdaterCtx } from "./types.ts";
10 export const PERSIST_LOADER_ID = "@@starfx/persist";
11
12 export interface PersistAdapter<S extends AnyState> {
13@@ -17,6 +17,35 @@ export interface PersistProps<S extends AnyState> {
14 key: string;
15 reconciler: (original: S, rehydrated: Partial<S>) => S;
16 rehydrate: () => Operation<Result<unknown>>;
17+ transform?: TransformFunctions<S>;
18+}
19+interface TransformFunctions<S extends AnyState> {
20+ in(s: Partial<S>): Partial<S>;
21+ out(s: Partial<S>): Partial<S>;
22+}
23+
24+export function createTransform<S extends AnyState>() {
25+ const transformers: TransformFunctions<S> = {
26+ in: function (currentState: Partial<S>): Partial<S> {
27+ return currentState;
28+ },
29+ out: function (currentState: Partial<S>): Partial<S> {
30+ return currentState;
31+ },
32+ };
33+
34+ const inTransformer = function (state: Partial<S>): Partial<S> {
35+ return transformers.in(state);
36+ };
37+
38+ const outTransformer = function (state: Partial<S>): Partial<S> {
39+ return transformers.out(state);
40+ };
41+
42+ return {
43+ in: inTransformer,
44+ out: outTransformer,
45+ };
46 }
47
48 export function createLocalStorageAdapter<S extends AnyState>(): PersistAdapter<
49@@ -51,7 +80,13 @@ export function shallowReconciler<S extends AnyState>(
50 }
51
52 export function createPersistor<S extends AnyState>(
53- { adapter, key = "starfx", reconciler = shallowReconciler, allowlist = [] }:
54+ {
55+ adapter,
56+ key = "starfx",
57+ reconciler = shallowReconciler,
58+ allowlist = [],
59+ transform,
60+ }:
61 & Pick<PersistProps<S>, "adapter">
62 & Partial<PersistProps<S>>,
63 ): PersistProps<S> {
64@@ -60,9 +95,18 @@ export function createPersistor<S extends AnyState>(
65 if (!persistedState.ok) {
66 return Err(persistedState.error);
67 }
68+ let stateFromStorage = persistedState.value as Partial<S>;
69+
70+ if (transform) {
71+ try {
72+ stateFromStorage = transform.out(persistedState.value);
73+ } catch (err: any) {
74+ console.error("Persistor outbound transformer error:", err);
75+ }
76+ }
77
78 const state = yield* select((s) => s);
79- const nextState = reconciler(state as S, persistedState.value);
80+ const nextState = reconciler(state as S, stateFromStorage);
81 yield* updateStore<S>(function (state) {
82 Object.keys(nextState).forEach((key: keyof S) => {
83 state[key] = nextState[key];
84@@ -78,25 +122,39 @@ export function createPersistor<S extends AnyState>(
85 allowlist,
86 reconciler,
87 rehydrate,
88+ transform,
89 };
90 }
91
92 export function persistStoreMdw<S extends AnyState>(
93- { allowlist, adapter, key }: PersistProps<S>,
94+ { allowlist, adapter, key, transform }: PersistProps<S>,
95 ) {
96 return function* (_: UpdaterCtx<S>, next: Next) {
97 yield* next();
98 const state = yield* select((s: S) => s);
99+
100+ let transformedState: Partial<S> = state;
101+ if (transform) {
102+ try {
103+ transformedState = transform.in(state);
104+ } catch (err: any) {
105+ console.error("Persistor inbound transformer error:", err);
106+ }
107+ }
108+
109 // empty allowlist list means save entire state
110 if (allowlist.length === 0) {
111- yield* adapter.setItem(key, state);
112+ yield* adapter.setItem(key, transformedState);
113 return;
114 }
115
116 const allowedState = allowlist.reduce<Partial<S>>((acc, key) => {
117- acc[key] = state[key];
118+ if (key in transformedState) {
119+ acc[key] = transformedState[key] as S[keyof S];
120+ }
121 return acc;
122 }, {});
123+
124 yield* adapter.setItem(key, allowedState);
125 };
126 }
+785,
-2
1@@ -1,14 +1,17 @@
2-import { asserts, describe, it } from "../test.ts";
3+import { sleep } from "../deps.ts";
4+import { Ok, Operation, parallel, put, take } from "../mod.ts";
5 import {
6 createPersistor,
7 createSchema,
8 createStore,
9+ createTransform,
10 PERSIST_LOADER_ID,
11 PersistAdapter,
12 persistStoreMdw,
13 slice,
14 } from "../store/mod.ts";
15-import { Ok, Operation, parallel, put, take } from "../mod.ts";
16+import { asserts, describe, it } from "../test.ts";
17+import { LoaderItemState } from "../types.ts";
18
19 const tests = describe("store");
20
21@@ -97,3 +100,783 @@ it(tests, "rehydrates state", async () => {
22 "123",
23 );
24 });
25+
26+it(tests, "persists inbound state using transform 'in' function", async () => {
27+ const [schema, initialState] = createSchema({
28+ token: slice.str(),
29+ loaders: slice.loaders(),
30+ cache: slice.table({ empty: {} }),
31+ });
32+ type State = typeof initialState;
33+ let ls = "{}";
34+
35+ const adapter: PersistAdapter<State> = {
36+ getItem: function* (_: string) {
37+ return Ok(JSON.parse(ls));
38+ },
39+ setItem: function* (_: string, s: Partial<State>) {
40+ ls = JSON.stringify(s);
41+ return Ok(undefined);
42+ },
43+ removeItem: function* (_: string) {
44+ return Ok(undefined);
45+ },
46+ };
47+
48+ const transform = createTransform<State>();
49+
50+ transform.in = function (state) {
51+ return { ...state, token: state?.token?.split("").reverse().join("") };
52+ };
53+
54+ const persistor = createPersistor<State>({
55+ adapter,
56+ allowlist: ["token", "cache"],
57+ transform,
58+ });
59+
60+ const mdw = persistStoreMdw(persistor);
61+ const store = createStore({
62+ initialState,
63+ middleware: [mdw],
64+ });
65+
66+ await store.run(function* (): Operation<void> {
67+ yield* persistor.rehydrate();
68+
69+ const group = yield* parallel([
70+ function* (): Operation<void> {
71+ const action = yield* take<string>("SET_TOKEN");
72+ yield* schema.update(schema.token.set(action.payload));
73+ },
74+ function* () {
75+ yield* put({ type: "SET_TOKEN", payload: "1234" });
76+ },
77+ ]);
78+ yield* group;
79+ });
80+ asserts.assertEquals(
81+ ls,
82+ '{"token":"4321","cache":{}}',
83+ );
84+});
85+
86+it(
87+ tests,
88+ "persists inbound state using tranform in (2)",
89+ async () => {
90+ const [schema, initialState] = createSchema({
91+ token: slice.str(),
92+ loaders: slice.loaders(),
93+ cache: slice.table({ empty: {} }),
94+ });
95+ type State = typeof initialState;
96+ let ls = "{}";
97+
98+ const adapter: PersistAdapter<State> = {
99+ getItem: function* (_: string) {
100+ return Ok(JSON.parse(ls));
101+ },
102+ setItem: function* (_: string, s: Partial<State>) {
103+ ls = JSON.stringify(s);
104+ return Ok(undefined);
105+ },
106+ removeItem: function* (_: string) {
107+ return Ok(undefined);
108+ },
109+ };
110+
111+ function revertToken(state: Partial<State>) {
112+ const res = {
113+ ...state,
114+ token: state?.token?.split("").reverse().join(""),
115+ };
116+ return res;
117+ }
118+ const transform = createTransform<State>();
119+ transform.in = revertToken;
120+
121+ const persistor = createPersistor<State>({
122+ adapter,
123+ allowlist: ["token", "cache"],
124+ transform,
125+ });
126+
127+ const mdw = persistStoreMdw(persistor);
128+ const store = createStore({
129+ initialState,
130+ middleware: [mdw],
131+ });
132+
133+ await store.run(function* (): Operation<void> {
134+ yield* persistor.rehydrate();
135+
136+ const group = yield* parallel([
137+ function* (): Operation<void> {
138+ const action = yield* take<string>("SET_TOKEN");
139+ yield* schema.update(schema.token.set(action.payload));
140+ },
141+ function* () {
142+ yield* put({ type: "SET_TOKEN", payload: "1234" });
143+ },
144+ ]);
145+ yield* group;
146+ });
147+ asserts.assertEquals(
148+ ls,
149+ '{"token":"4321","cache":{}}',
150+ );
151+ },
152+);
153+
154+it(tests, "persists a filtered nested part of a slice", async () => {
155+ const [schema, initialState] = createSchema({
156+ token: slice.str(),
157+ loaders: slice.loaders(),
158+ cache: slice.table({ empty: {} }),
159+ });
160+ type State = typeof initialState;
161+ let ls = "{}";
162+
163+ const adapter: PersistAdapter<State> = {
164+ getItem: function* (_: string) {
165+ return Ok(JSON.parse(ls));
166+ },
167+ setItem: function* (_: string, s: Partial<State>) {
168+ ls = JSON.stringify(s);
169+ return Ok(undefined);
170+ },
171+ removeItem: function* (_: string) {
172+ return Ok(undefined);
173+ },
174+ };
175+
176+ function pickLatestOfLoadersAandC(
177+ state: Partial<State>,
178+ ): Partial<State> {
179+ const nextState = { ...state };
180+
181+ if (state.loaders) {
182+ const maxLastRun: Record<string, number> = {};
183+ const entryWithMaxLastRun: Record<string, LoaderItemState<any>> = {};
184+
185+ for (const entryKey in state.loaders) {
186+ const entry = state.loaders[entryKey] as LoaderItemState<any>;
187+ const sliceName = entryKey.split("[")[0].trim();
188+ if (sliceName.includes("A") || sliceName.includes("C")) {
189+ if (!maxLastRun[sliceName] || entry.lastRun > maxLastRun[sliceName]) {
190+ maxLastRun[sliceName] = entry.lastRun;
191+ entryWithMaxLastRun[sliceName] = entry;
192+ }
193+ }
194+ }
195+ nextState.loaders = entryWithMaxLastRun;
196+ }
197+ return nextState;
198+ }
199+
200+ const transform = createTransform<State>();
201+ transform.in = pickLatestOfLoadersAandC;
202+
203+ const persistor = createPersistor<State>({
204+ adapter,
205+ transform,
206+ });
207+
208+ const mdw = persistStoreMdw(persistor);
209+ const store = createStore({
210+ initialState,
211+ middleware: [mdw],
212+ });
213+
214+ await store.run(function* (): Operation<void> {
215+ yield* persistor.rehydrate();
216+ const group = yield* parallel([
217+ function* () {
218+ yield* schema.update(schema.token.set("1234"));
219+ yield* schema.update(
220+ schema.loaders.start({
221+ id: "A [POST]|1234",
222+ message: "loading A-first",
223+ }),
224+ );
225+ yield* schema.update(schema.loaders.start({ id: "B" }));
226+ yield* schema.update(schema.loaders.start({ id: "C" }));
227+ yield* sleep(300);
228+ yield* schema.update(schema.loaders.success({ id: "A" }));
229+ yield* schema.update(schema.loaders.success({ id: "B" }));
230+ yield* schema.update(schema.loaders.success({ id: "C" }));
231+ yield* schema.update(
232+ schema.loaders.start({
233+ id: "A [POST]|5678",
234+ message: "loading A-second",
235+ }),
236+ );
237+ yield* schema.update(schema.loaders.start({ id: "B" }));
238+ yield* schema.update(schema.loaders.start({ id: "C" }));
239+ yield* sleep(300);
240+ yield* schema.update(schema.loaders.success({ id: "A" }));
241+ yield* schema.update(schema.loaders.success({ id: "B" }));
242+ yield* schema.update(schema.loaders.success({ id: "C" }));
243+ yield* schema.update(schema.token.set("1"));
244+ },
245+ ]);
246+ yield* group;
247+ });
248+ asserts.assertStringIncludes(
249+ ls,
250+ '{"token":"1"',
251+ );
252+ asserts.assertStringIncludes(
253+ ls,
254+ '"message":"loading A-second"',
255+ );
256+ asserts.assertStringIncludes(
257+ ls,
258+ '"id":"C"',
259+ );
260+ asserts.assertNotMatch(
261+ ls,
262+ /"message":"loading A-first"/,
263+ );
264+ asserts.assertNotMatch(
265+ ls,
266+ /"id":"B"/,
267+ );
268+});
269+
270+it(tests, "handles the empty state correctly", async () => {
271+ const [_schema, initialState] = createSchema({
272+ token: slice.str(),
273+ loaders: slice.loaders(),
274+ cache: slice.table({ empty: {} }),
275+ });
276+
277+ type State = typeof initialState;
278+ let ls = "{}";
279+
280+ const adapter: PersistAdapter<State> = {
281+ getItem: function* (_: string) {
282+ return Ok(JSON.parse(ls));
283+ },
284+ setItem: function* (_: string, s: Partial<State>) {
285+ ls = JSON.stringify(s);
286+ return Ok(undefined);
287+ },
288+ removeItem: function* (_: string) {
289+ return Ok(undefined);
290+ },
291+ };
292+
293+ const transform = createTransform<State>();
294+ transform.in = function (_: Partial<State>) {
295+ return {};
296+ };
297+
298+ const persistor = createPersistor<State>({
299+ adapter,
300+ transform,
301+ });
302+
303+ const mdw = persistStoreMdw(persistor);
304+ const store = createStore({
305+ initialState,
306+ middleware: [mdw],
307+ });
308+
309+ await store.run(function* (): Operation<void> {
310+ yield* persistor.rehydrate();
311+ });
312+
313+ asserts.assertEquals(
314+ ls,
315+ "{}",
316+ );
317+});
318+
319+it(
320+ tests,
321+ "in absence of the inbound transformer, persists as it is",
322+ async () => {
323+ const [schema, initialState] = createSchema({
324+ token: slice.str(),
325+ loaders: slice.loaders(),
326+ cache: slice.table({ empty: {} }),
327+ });
328+ type State = typeof initialState;
329+ let ls = "{}";
330+ const adapter: PersistAdapter<State> = {
331+ getItem: function* (_: string) {
332+ return Ok(JSON.parse(ls));
333+ },
334+ setItem: function* (_: string, s: Partial<State>) {
335+ ls = JSON.stringify(s);
336+ return Ok(undefined);
337+ },
338+ removeItem: function* (_: string) {
339+ return Ok(undefined);
340+ },
341+ };
342+ const persistor = createPersistor<State>({
343+ adapter,
344+ allowlist: ["token"],
345+ transform: createTransform<State>(), // we deliberately do not set the inbound transformer
346+ });
347+
348+ const mdw = persistStoreMdw(persistor);
349+ const store = createStore({
350+ initialState,
351+ middleware: [mdw],
352+ });
353+
354+ await store.run(function* (): Operation<void> {
355+ yield* persistor.rehydrate();
356+
357+ const group = yield* parallel([
358+ function* (): Operation<void> {
359+ const action = yield* take<string>("SET_TOKEN");
360+ yield* schema.update(schema.token.set(action.payload));
361+ },
362+ function* () {
363+ yield* put({ type: "SET_TOKEN", payload: "1234" });
364+ },
365+ ]);
366+ yield* group;
367+ });
368+
369+ asserts.assertEquals(
370+ ls,
371+ '{"token":"1234"}',
372+ );
373+ },
374+);
375+
376+it(
377+ tests,
378+ "handles errors gracefully, defaluts to identity function",
379+ async () => {
380+ const [schema, initialState] = createSchema({
381+ token: slice.str(),
382+ loaders: slice.loaders(),
383+ cache: slice.table({ empty: {} }),
384+ });
385+ type State = typeof initialState;
386+ let ls = "{}";
387+ const adapter: PersistAdapter<State> = {
388+ getItem: function* (_: string) {
389+ return Ok(JSON.parse(ls));
390+ },
391+ setItem: function* (_: string, s: Partial<State>) {
392+ ls = JSON.stringify(s);
393+ return Ok(undefined);
394+ },
395+ removeItem: function* (_: string) {
396+ return Ok(undefined);
397+ },
398+ };
399+
400+ const transform = createTransform<State>();
401+ transform.in = function (_: Partial<State>) {
402+ throw new Error("testing the transform error");
403+ };
404+ const persistor = createPersistor<State>({
405+ adapter,
406+ transform,
407+ });
408+ const mdw = persistStoreMdw(persistor);
409+ const store = createStore({
410+ initialState,
411+ middleware: [mdw],
412+ });
413+
414+ await store.run(function* (): Operation<void> {
415+ yield* persistor.rehydrate();
416+ yield* schema.update(schema.loaders.success({ id: PERSIST_LOADER_ID }));
417+ yield* schema.update(schema.token.set("1234"));
418+ });
419+ asserts.assertEquals(
420+ store.getState().token,
421+ "1234",
422+ );
423+ },
424+);
425+
426+it(
427+ tests,
428+ "allowdList is filtered out after the inbound transformer is applied",
429+ async () => {
430+ const [schema, initialState] = createSchema({
431+ token: slice.str(),
432+ counter: slice.num(0),
433+ loaders: slice.loaders(),
434+ cache: slice.table({ empty: {} }),
435+ });
436+ type State = typeof initialState;
437+ let ls = "{}";
438+ const adapter: PersistAdapter<State> = {
439+ getItem: function* (_: string) {
440+ return Ok(JSON.parse(ls));
441+ },
442+ setItem: function* (_: string, s: Partial<State>) {
443+ ls = JSON.stringify(s);
444+ return Ok(undefined);
445+ },
446+ removeItem: function* (_: string) {
447+ return Ok(undefined);
448+ },
449+ };
450+
451+ const transform = createTransform<State>();
452+ transform.in = function (state) {
453+ return {
454+ ...state,
455+ token: `${state.counter}${state?.token?.split("").reverse().join("")}`,
456+ };
457+ };
458+
459+ const persistor = createPersistor<State>({
460+ adapter,
461+ allowlist: ["token"],
462+ transform,
463+ });
464+
465+ const mdw = persistStoreMdw(persistor);
466+ const store = createStore({
467+ initialState,
468+ middleware: [mdw],
469+ });
470+
471+ await store.run(function* (): Operation<void> {
472+ yield* persistor.rehydrate();
473+ yield* schema.update(schema.loaders.success({ id: PERSIST_LOADER_ID }));
474+ yield* schema.update(schema.token.set("1234"));
475+ yield* schema.update(schema.counter.set(5));
476+ });
477+
478+ asserts.assertEquals(
479+ ls,
480+ '{"token":"54321"}',
481+ );
482+ },
483+);
484+
485+it(
486+ tests,
487+ "the inbound transformer can be redifined during runtime",
488+ async () => {
489+ const [schema, initialState] = createSchema({
490+ token: slice.str(),
491+ loaders: slice.loaders(),
492+ cache: slice.table({ empty: {} }),
493+ });
494+ type State = typeof initialState;
495+ let ls = "{}";
496+ const adapter: PersistAdapter<State> = {
497+ getItem: function* (_: string) {
498+ return Ok(JSON.parse(ls));
499+ },
500+ setItem: function* (_: string, s: Partial<State>) {
501+ ls = JSON.stringify(s);
502+ return Ok(undefined);
503+ },
504+ removeItem: function* (_: string) {
505+ return Ok(undefined);
506+ },
507+ };
508+
509+ const transform = createTransform<State>();
510+ transform.in = function (state) {
511+ return {
512+ ...state,
513+ token: `${state?.token?.split("").reverse().join("")}`,
514+ };
515+ };
516+
517+ const persistor = createPersistor<State>({
518+ adapter,
519+ allowlist: ["token"],
520+ transform,
521+ });
522+
523+ const mdw = persistStoreMdw(persistor);
524+ const store = createStore({
525+ initialState,
526+ middleware: [mdw],
527+ });
528+
529+ await store.run(function* (): Operation<void> {
530+ yield* persistor.rehydrate();
531+ yield* schema.update(schema.loaders.success({ id: PERSIST_LOADER_ID }));
532+ yield* schema.update(schema.token.set("01234"));
533+ });
534+
535+ asserts.assertEquals(
536+ ls,
537+ '{"token":"43210"}',
538+ );
539+
540+ transform.in = function (state) {
541+ return {
542+ ...state,
543+ token: `${state?.token}56789`,
544+ };
545+ };
546+
547+ await store.run(function* (): Operation<void> {
548+ yield* schema.update(schema.token.set("01234"));
549+ });
550+
551+ asserts.assertEquals(
552+ ls,
553+ '{"token":"0123456789"}',
554+ );
555+ },
556+);
557+
558+it(tests, "persists state using transform 'out' function", async () => {
559+ const [schema, initialState] = createSchema({
560+ token: slice.str(),
561+ counter: slice.num(0),
562+ loaders: slice.loaders(),
563+ cache: slice.table({ empty: {} }),
564+ });
565+ type State = typeof initialState;
566+ let ls = '{"token": "01234"}';
567+
568+ const adapter: PersistAdapter<State> = {
569+ getItem: function* (_: string) {
570+ return Ok(JSON.parse(ls));
571+ },
572+ setItem: function* (_: string, s: Partial<State>) {
573+ ls = JSON.stringify(s);
574+ return Ok(undefined);
575+ },
576+ removeItem: function* (_: string) {
577+ return Ok(undefined);
578+ },
579+ };
580+
581+ function revertToken(state: Partial<State>) {
582+ return { ...state, token: state?.token?.split("").reverse().join("") };
583+ }
584+ const transform = createTransform<State>();
585+ transform.out = revertToken;
586+
587+ const persistor = createPersistor<State>({
588+ adapter,
589+ allowlist: ["token"],
590+ transform,
591+ });
592+
593+ const mdw = persistStoreMdw(persistor);
594+ const store = createStore({
595+ initialState,
596+ middleware: [mdw],
597+ });
598+
599+ await store.run(function* (): Operation<void> {
600+ yield* persistor.rehydrate();
601+ yield* schema.update(schema.loaders.success({ id: PERSIST_LOADER_ID }));
602+ });
603+
604+ asserts.assertEquals(
605+ store.getState().token,
606+ "43210",
607+ );
608+});
609+
610+it("persists outbound state using tranform setOutTransformer", async () => {
611+ const [schema, initialState] = createSchema({
612+ token: slice.str(),
613+ counter: slice.num(0),
614+ loaders: slice.loaders(),
615+ cache: slice.table({ empty: {} }),
616+ });
617+ type State = typeof initialState;
618+ let ls = '{"token": "43210"}';
619+
620+ const adapter: PersistAdapter<State> = {
621+ getItem: function* (_: string) {
622+ return Ok(JSON.parse(ls));
623+ },
624+ setItem: function* (_: string, s: Partial<State>) {
625+ ls = JSON.stringify(s);
626+ return Ok(undefined);
627+ },
628+ removeItem: function* (_: string) {
629+ return Ok(undefined);
630+ },
631+ };
632+
633+ function revertToken(state: Partial<State>) {
634+ return {
635+ ...state,
636+ token: (["5"].concat(...state?.token?.split("") || [])).reverse().join(
637+ "",
638+ ),
639+ };
640+ }
641+ const transform = createTransform<State>();
642+ transform.out = revertToken;
643+
644+ const persistor = createPersistor<State>({
645+ adapter,
646+ allowlist: ["token"],
647+ transform,
648+ });
649+
650+ const mdw = persistStoreMdw(persistor);
651+ const store = createStore({
652+ initialState,
653+ middleware: [mdw],
654+ });
655+
656+ await store.run(function* (): Operation<void> {
657+ yield* persistor.rehydrate();
658+ yield* schema.update(schema.loaders.success({ id: PERSIST_LOADER_ID }));
659+ });
660+
661+ asserts.assertEquals(
662+ ls,
663+ '{"token":"012345"}',
664+ );
665+});
666+
667+it(tests, "persists outbound a filtered nested part of a slice", async () => {
668+ const [schema, initialState] = createSchema({
669+ token: slice.str(),
670+ loaders: slice.loaders(),
671+ cache: slice.table({ empty: {} }),
672+ });
673+ type State = typeof initialState;
674+ let ls =
675+ '{"loaders":{"A":{"id":"A [POST]|5678","status":"loading","message":"loading A-second","lastRun":1725048721168,"lastSuccess":0,"meta":{"flag":"01234_FLAG_PERSISTED"}}}}';
676+
677+ const adapter: PersistAdapter<State> = {
678+ getItem: function* (_: string) {
679+ return Ok(JSON.parse(ls));
680+ },
681+ setItem: function* (_: string, s: Partial<State>) {
682+ ls = JSON.stringify(s);
683+ return Ok(undefined);
684+ },
685+ removeItem: function* (_: string) {
686+ return Ok(undefined);
687+ },
688+ };
689+
690+ function extractMetaAndSetToken(
691+ state: Partial<State>,
692+ ): Partial<State> {
693+ const nextState = { ...state };
694+ if (state.loaders) {
695+ const savedLoader = state.loaders["A"];
696+ if (savedLoader?.meta?.flag) {
697+ nextState.token = savedLoader.meta.flag;
698+ }
699+ }
700+ return nextState;
701+ }
702+
703+ const transform = createTransform<State>();
704+ transform.out = extractMetaAndSetToken;
705+
706+ const persistor = createPersistor<State>({
707+ adapter,
708+ transform,
709+ });
710+
711+ const mdw = persistStoreMdw(persistor);
712+ const store = createStore({
713+ initialState,
714+ middleware: [mdw],
715+ });
716+
717+ await store.run(function* (): Operation<void> {
718+ yield* persistor.rehydrate();
719+ yield* schema.update(schema.loaders.success({ id: PERSIST_LOADER_ID }));
720+ });
721+ asserts.assertEquals(
722+ store.getState().token,
723+ "01234_FLAG_PERSISTED",
724+ );
725+});
726+
727+it(tests, "the outbound transformer can be reset during runtime", async () => {
728+ const [schema, initialState] = createSchema({
729+ token: slice.str(),
730+ counter: slice.num(0),
731+ loaders: slice.loaders(),
732+ cache: slice.table({ empty: {} }),
733+ });
734+ type State = typeof initialState;
735+ let ls = '{"token": "_1234"}';
736+
737+ const adapter: PersistAdapter<State> = {
738+ getItem: function* (_: string) {
739+ return Ok(JSON.parse(ls));
740+ },
741+ setItem: function* (_: string, s: Partial<State>) {
742+ ls = JSON.stringify(s);
743+ return Ok(undefined);
744+ },
745+ removeItem: function* (_: string) {
746+ return Ok(undefined);
747+ },
748+ };
749+
750+ function revertToken(state: Partial<State>) {
751+ return { ...state, token: state?.token?.split("").reverse().join("") };
752+ }
753+ function postpendToken(state: Partial<State>) {
754+ return {
755+ ...state,
756+ token: `${state?.token}56789`,
757+ };
758+ }
759+ const transform = createTransform<State>();
760+ transform.out = revertToken;
761+
762+ const persistor = createPersistor<State>({
763+ adapter,
764+ allowlist: ["token"],
765+ transform,
766+ });
767+
768+ const mdw = persistStoreMdw(persistor);
769+ const store = createStore({
770+ initialState,
771+ middleware: [mdw],
772+ });
773+
774+ await store.run(function* (): Operation<void> {
775+ yield* persistor.rehydrate();
776+ yield* schema.update(schema.loaders.success({ id: PERSIST_LOADER_ID }));
777+ });
778+
779+ asserts.assertEquals(
780+ store.getState().token,
781+ "4321_",
782+ );
783+
784+ await store.run(function* (): Operation<void> {
785+ yield* schema.update(schema.token.set("01234"));
786+ });
787+
788+ asserts.assertEquals(
789+ ls,
790+ '{"token":"01234"}',
791+ );
792+
793+ transform.out = postpendToken;
794+
795+ await store.run(function* (): Operation<void> {
796+ yield* persistor.rehydrate();
797+ yield* schema.update(schema.loaders.success({ id: PERSIST_LOADER_ID }));
798+ });
799+
800+ asserts.assertEquals(
801+ store.getState().token,
802+ "0123456789",
803+ );
804+});