repos / starfx

a micro-mvc framework for react apps
git clone https://github.com/neurosnap/starfx.git

starfx / src / test
Eric Bower  ·  2025-06-06

persist.test.ts

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