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