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});