repos / starfx

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

starfx / test
Vlad · 03 Oct 24

persist.test.ts

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