repos / starfx

supercharged async flow control library.
git clone https://github.com/neurosnap/starfx.git

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
M store/persist.ts
+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 }
M test/persist.test.ts
+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+});