- commit
- 7970673
- parent
- 38f52cb
- author
- Eric Bower
- date
- 2023-12-01 18:36:19 +0000 UTC
feat(store): redux-persist replacement (#21)
11 files changed,
+278,
-24
+25,
-0
1@@ -1,9 +1,11 @@
2 {
3 "version": "3",
4 "redirects": {
5+ "https://crux.land/router@0.0.5": "https://crux.land/api/get/2KNRVU.ts",
6 "https://esm.sh/v128/@types/react@~18.2/index.d.ts": "https://esm.sh/v128/@types/react@18.2.38/index.d.ts"
7 },
8 "remote": {
9+ "https://crux.land/api/get/2KNRVU.ts": "6a77d55844aba78d01520c5ff0b2f0af7f24cc1716a0de8b3bb6bd918c47b5ba",
10 "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
11 "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49",
12 "https://deno.land/std@0.140.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d",
13@@ -27,6 +29,8 @@
14 "https://deno.land/std@0.158.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c",
15 "https://deno.land/std@0.158.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832",
16 "https://deno.land/std@0.158.0/testing/asserts.ts": "8696c488bc98d8d175e74dc652a0ffbc7fca93858da01edc57ed33c1148345da",
17+ "https://deno.land/std@0.163.0/testing/_test_suite.ts": "2d07073d5460a4e3ec50c55ae822cd9bd136926d7363091379947fef9c73c3e4",
18+ "https://deno.land/std@0.163.0/testing/bdd.ts": "35060cefd9cc21b414f4d89453b3551a3d52ec50aeff25db432503c5485b2f72",
19 "https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
20 "https://deno.land/std@0.181.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
21 "https://deno.land/std@0.181.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32",
22@@ -58,6 +62,17 @@
23 "https://deno.land/std@0.182.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
24 "https://deno.land/std@0.182.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
25 "https://deno.land/std@0.182.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
26+ "https://deno.land/std@0.185.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
27+ "https://deno.land/std@0.185.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea",
28+ "https://deno.land/std@0.185.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
29+ "https://deno.land/std@0.185.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f",
30+ "https://deno.land/std@0.187.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
31+ "https://deno.land/std@0.187.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea",
32+ "https://deno.land/std@0.187.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
33+ "https://deno.land/std@0.187.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f",
34+ "https://deno.land/std@0.97.0/fmt/colors.ts": "db22b314a2ae9430ae7460ce005e0a7130e23ae1c999157e3bb77cf55800f7e4",
35+ "https://deno.land/std@0.97.0/testing/_diff.ts": "961eaf6d9f5b0a8556c9d835bbc6fa74f5addd7d3b02728ba7936ff93364f7a3",
36+ "https://deno.land/std@0.97.0/testing/asserts.ts": "341292d12eebc44be4c3c2ca101ba8b6b5859cef2fa69d50c217f9d0bfbcfd1f",
37 "https://deno.land/x/code_block_writer@12.0.0/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5",
38 "https://deno.land/x/code_block_writer@12.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff",
39 "https://deno.land/x/continuation@0.1.5/mod.ts": "690def2735046367b3e1b4bc6e51b5912f2ed09c41c7df7a55c060f23720ad33",
40@@ -73,6 +88,11 @@
41 "https://deno.land/x/deno_cache@0.5.2/lib/snippets/deno_cache_dir-77bed54ace8005e0/fs.js": "cbe3a976ed63c72c7cb34ef845c27013033a3b11f9d8d3e2c4aa5dda2c0c7af6",
42 "https://deno.land/x/deno_cache@0.5.2/mod.ts": "0b4d071ad095128bdc2b1bc6e5d2095222dcbae08287261690ee9757e6300db6",
43 "https://deno.land/x/deno_cache@0.5.2/util.ts": "f3f5a0cfc60051f09162942fb0ee87a0e27b11a12aec4c22076e3006be4cc1e2",
44+ "https://deno.land/x/deno_graph@0.26.0/lib/deno_graph.generated.js": "2f7ca85b2ceb80ec4b3d1b7f3a504956083258610c7b9a1246238c5b7c68f62d",
45+ "https://deno.land/x/deno_graph@0.26.0/lib/loader.ts": "380e37e71d0649eb50176a9786795988fc3c47063a520a54b616d7727b0f8629",
46+ "https://deno.land/x/deno_graph@0.26.0/lib/media_type.ts": "222626d524fa2f9ebcc0ec7c7a7d5dfc74cc401cc46790f7c5e0eab0b0787707",
47+ "https://deno.land/x/deno_graph@0.26.0/lib/snippets/deno_graph-de651bc9c240ed8d/src/deno_apis.js": "41192baaa550a5c6a146280fae358cede917ae16ec4e4315be51bef6631ca892",
48+ "https://deno.land/x/deno_graph@0.26.0/mod.ts": "11131ae166580a1c7fa8506ff553751465a81c263d94443f18f353d0c320bc14",
49 "https://deno.land/x/dir@1.5.1/data_local_dir/mod.ts": "91eb1c4bfadfbeda30171007bac6d85aadacd43224a5ed721bbe56bc64e9eb66",
50 "https://deno.land/x/dnt@0.38.1/lib/compiler.ts": "209ad2e1b294f93f87ec02ade9a0821f942d2e524104552d0aa8ff87021050a5",
51 "https://deno.land/x/dnt@0.38.1/lib/compiler_transforms.ts": "f21aba052f5dcf0b0595c734450842855c7f572e96165d3d34f8fed2fc1f7ba1",
52@@ -124,6 +144,11 @@
53 "https://deno.land/x/effection@3.0.0-beta.3/lib/sleep.ts": "44e3a80248dad7a47066a99a7daec9b318e87d5d211adf27776145544d455689",
54 "https://deno.land/x/effection@3.0.0-beta.3/lib/types.ts": "9738143fe6bfd5709a6ff10b6dd065582cfaca1167bf57902cb7bcca89b53dc4",
55 "https://deno.land/x/effection@3.0.0-beta.3/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345",
56+ "https://deno.land/x/expect@v0.3.0/expect.ts": "5e6717eddc9df376f7b2c9be6403e016130bb2edbb1acd261a2d6ea9608ee196",
57+ "https://deno.land/x/expect@v0.3.0/matchers.ts": "a37ef4577739247af77a852cdcd69484f999a41ad86ec16bb63a88a7a47a2372",
58+ "https://deno.land/x/expect@v0.3.0/mock.ts": "562d4b1d735d15b0b8e935f342679096b64fe452f86e96714fe8616c0c884914",
59+ "https://deno.land/x/expect@v0.3.0/mod.ts": "0304d2430e1e96ba669a8495e24ba606dcc3d152e1f81aaa8da898cea24e36c2",
60+ "https://deno.land/x/mock_fetch@0.3.0/mod.ts": "7e7806c65ab17b2b684c334c4e565812bdaf504a3e9c938d2bb52bb67428bc89",
61 "https://deno.land/x/ts_morph@18.0.0/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9",
62 "https://deno.land/x/ts_morph@18.0.0/bootstrap/ts_morph_bootstrap.js": "6645ac03c5e6687dfa8c78109dc5df0250b811ecb3aea2d97c504c35e8401c06",
63 "https://deno.land/x/ts_morph@18.0.0/common/DenoRuntime.ts": "6a7180f0c6e90dcf23ccffc86aa8271c20b1c4f34c570588d08a45880b7e172d",
M
deps.ts
+3,
-0
1@@ -43,6 +43,9 @@ export {
2 useDispatch,
3 useSelector,
4 } from "https://esm.sh/react-redux@8.0.5?pin=v122";
5+export type {
6+ TypedUseSelectorHook,
7+} from "https://esm.sh/react-redux@8.0.5?pin=v122";
8 export { createSelector } from "https://esm.sh/reselect@4.1.8?pin=v122";
9
10 export {
M
react.ts
+2,
-0
1@@ -1,2 +1,4 @@
2 export * from "./query/react.ts";
3+export * from "./store/react.ts";
4 export { Provider, useDispatch, useSelector } from "./deps.ts";
5+export type { TypedUseSelectorHook } from "./deps.ts";
+25,
-0
1@@ -3,6 +3,7 @@ import {
2 call,
3 each,
4 Operation,
5+ Result,
6 Signal,
7 SignalQueueFactory,
8 spawn,
9@@ -13,6 +14,8 @@ import type { ActionWPayload, AnyAction, AnyState } from "../types.ts";
10 import type { FxStore, StoreUpdater, UpdaterCtx } from "./types.ts";
11 import { ActionContext, StoreContext } from "./context.ts";
12 import { createFilterQueue } from "../queue.ts";
13+import { LoaderOutput } from "./slice/loader.ts";
14+import { safe } from "../fx/mod.ts";
15
16 export function* updateStore<S extends AnyState>(
17 updater: StoreUpdater<S> | StoreUpdater<S>[],
18@@ -127,3 +130,25 @@ export function* takeLeading<T>(
19 });
20 }
21 export const leading = takeLeading;
22+
23+export function createTracker<T, M extends Record<string, unknown>>(
24+ loader: LoaderOutput<M, AnyState>,
25+) {
26+ return (id: string) => {
27+ return function* (op: () => Operation<Result<T>>) {
28+ yield* updateStore(loader.start({ id }));
29+ const result = yield* safe(op);
30+ if (result.ok) {
31+ yield* updateStore(loader.success({ id }));
32+ } else {
33+ yield* updateStore(
34+ loader.error({
35+ id,
36+ message: result.error.message,
37+ }),
38+ );
39+ }
40+ return result;
41+ };
42+ };
43+}
+1,
-1
1@@ -2,7 +2,7 @@ export * from "./context.ts";
2 export * from "./fx.ts";
3 export * from "./store.ts";
4 export * from "./types.ts";
5-export * from "./slice.ts";
6+export * from "./selectors.ts";
7 export * from "./query.ts";
8 export * from "./supervisor.ts";
9 export { createSelector } from "../deps.ts";
+65,
-0
1@@ -0,0 +1,65 @@
2+import { Ok, Operation } from "../deps.ts";
3+import { parallel } from "../fx/mod.ts";
4+import { asserts, describe, it } from "../test.ts";
5+import { createTracker, put, take } from "./fx.ts";
6+import { configureStore } from "./store.ts";
7+import {
8+ createPersistor,
9+ PERSIST_LOADER_ID,
10+ PersistAdapter,
11+ persistStoreMdw,
12+} from "./persist.ts";
13+import { createSchema } from "./schema.ts";
14+import { slice } from "./slice/mod.ts";
15+
16+const tests = describe("store");
17+
18+it(tests, "can persist to storage adapters", async () => {
19+ const schema = createSchema({
20+ token: slice.str(),
21+ loaders: slice.loader(),
22+ cache: slice.table({ empty: {} }),
23+ });
24+ const db = schema.db;
25+ type State = typeof schema.initialState;
26+ let ls = "{}";
27+ const adapter: PersistAdapter<State> = {
28+ getItem: function* (_: string) {
29+ return Ok(JSON.parse(ls));
30+ },
31+ setItem: function* (_: string, s: Partial<State>) {
32+ ls = JSON.stringify(s);
33+ return Ok(undefined);
34+ },
35+ removeItem: function* (_: string) {
36+ return Ok(undefined);
37+ },
38+ };
39+ const persistor = createPersistor<State>({ adapter, allowlist: ["token"] });
40+ const mdw = persistStoreMdw(persistor);
41+ const store = configureStore({
42+ initialState: schema.initialState,
43+ middleware: [mdw],
44+ });
45+
46+ await store.run(function* (): Operation<void> {
47+ const tracker = createTracker(db.loaders)(PERSIST_LOADER_ID);
48+ yield* tracker(persistor.rehydrate);
49+
50+ const group = yield* parallel([
51+ function* (): Operation<void> {
52+ const action = yield* take<string>("SET_TOKEN");
53+ yield* schema.update(db.token.set(action.payload));
54+ },
55+ function* () {
56+ yield* put({ type: "SET_TOKEN", payload: "1234" });
57+ },
58+ ]);
59+ yield* group;
60+ });
61+
62+ asserts.assertEquals(
63+ ls,
64+ '{"token":"1234"}',
65+ );
66+});
+75,
-0
1@@ -0,0 +1,75 @@
2+import { Err, Ok, Operation, Result } from "../deps.ts";
3+import { Next } from "../query/types.ts";
4+import { AnyState } from "../types.ts";
5+import { select, updateStore } from "./fx.ts";
6+import { UpdaterCtx } from "./types.ts";
7+
8+export const PERSIST_LOADER_ID = "persist";
9+
10+export interface PersistAdapter<S extends AnyState> {
11+ getItem(key: string): Operation<Result<Partial<S>>>;
12+ setItem(key: string, item: Partial<S>): Operation<Result<unknown>>;
13+ removeItem(key: string): Operation<Result<unknown>>;
14+}
15+
16+export interface PersistProps<S extends AnyState> {
17+ adapter: PersistAdapter<S>;
18+ allowlist: (keyof S)[];
19+ key: string;
20+ reconciler: (original: S, rehydrated: Partial<S>) => S;
21+ rehydrate: () => Operation<Result<unknown>>;
22+}
23+
24+export function shallowReconciler<S extends AnyState>(
25+ original: S,
26+ persisted: Partial<S>,
27+): S {
28+ return { ...original, ...persisted };
29+}
30+
31+export function createPersistor<S extends AnyState>(
32+ { adapter, key = "starfx", reconciler = shallowReconciler, allowlist = [] }:
33+ & Pick<PersistProps<S>, "adapter">
34+ & Partial<PersistProps<S>>,
35+): PersistProps<S> {
36+ function* rehydrate(): Operation<Result<undefined>> {
37+ const persistedState = yield* adapter.getItem(key);
38+ if (!persistedState.ok) {
39+ return Err(persistedState.error);
40+ }
41+
42+ yield* updateStore<S>(function (state) {
43+ state = reconciler(state, persistedState.value);
44+ });
45+
46+ return Ok(undefined);
47+ }
48+
49+ return {
50+ key,
51+ adapter,
52+ allowlist,
53+ reconciler,
54+ rehydrate,
55+ };
56+}
57+
58+export function persistStoreMdw<S extends AnyState>(
59+ { allowlist, adapter, key }: PersistProps<S>,
60+) {
61+ return function* (_: UpdaterCtx<S>, next: Next) {
62+ yield* next();
63+ const state = yield* select((s: S) => s);
64+ // empty allowlist list means save entire state
65+ if (allowlist.length === 0) {
66+ yield* adapter.setItem(key, state);
67+ return;
68+ }
69+
70+ const allowedState = allowlist.reduce<Partial<S>>((acc, key) => {
71+ acc[key] = state[key];
72+ return acc;
73+ }, {});
74+ yield* adapter.setItem(key, allowedState);
75+ };
76+}
+32,
-0
1@@ -0,0 +1,32 @@
2+import { React, useSelector } from "../deps.ts";
3+import { PERSIST_LOADER_ID } from "./persist.ts";
4+import type { LoaderOutput } from "./slice/mod.ts";
5+
6+interface PersistGateProps {
7+ children: React.ReactNode;
8+ loading?: JSX.Element;
9+ loader: LoaderOutput<any, any>;
10+}
11+
12+function Loading({ text }: { text: string }) {
13+ return React.createElement("div", null, text);
14+}
15+
16+export function PersistGate(
17+ { loader, children, loading = React.createElement(Loading) }:
18+ PersistGateProps,
19+) {
20+ const ldr = useSelector((s) =>
21+ loader.selectById(s, { id: PERSIST_LOADER_ID })
22+ );
23+
24+ if (ldr.status === "error") {
25+ return React.createElement("div", null, ldr.message);
26+ }
27+
28+ if (ldr.status !== "success") {
29+ return loading;
30+ }
31+
32+ return React.createElement("div", null, children);
33+}
+31,
-0
1@@ -0,0 +1,31 @@
2+import type { IdProp } from "../types.ts";
3+import type { LoaderOutput } from "./slice/loader.ts";
4+import type { TableOutput } from "./slice/table.ts";
5+
6+export interface QueryState {
7+ cache: TableOutput<any, any>["initialState"];
8+ loaders: LoaderOutput<any, any>["initialState"];
9+}
10+
11+export const selectDataTable = (s: QueryState) => {
12+ return s.cache;
13+};
14+
15+export const selectDataById = (s: QueryState, { id }: { id: IdProp }) => {
16+ return selectDataTable(s)[id];
17+};
18+
19+export const addData = (props: { [key: string]: any }) => {
20+ function addDataState(s: QueryState) {
21+ s.cache = { ...s.cache, ...props };
22+ }
23+ return addDataState;
24+};
25+
26+export const selectLoaders = (s: QueryState) => {
27+ return s.loaders;
28+};
29+
30+export const selectLoaderById = (s: QueryState, { id }: { id: IdProp }) => {
31+ return selectLoaders(s)[id];
32+};
+0,
-17
1@@ -1,17 +0,0 @@
2-import type { IdProp } from "../types.ts";
3-import type { QueryState } from "../types.ts";
4-
5-export const selectDataTable = (s: QueryState) => {
6- return s["@@starfx/data"] || {};
7-};
8-
9-export const selectDataById = (s: QueryState, { id }: { id: IdProp }) => {
10- return selectDataTable(s)[id];
11-};
12-
13-export const addData = (props: { [key: string]: unknown }) => {
14- function addDataState(s: QueryState) {
15- s["@@starfx/data"] = { ...s["@@starfx/data"], ...props };
16- }
17- return addDataState;
18-};
+19,
-6
1@@ -1,9 +1,14 @@
2-import { str } from "./str.ts";
3-import { num } from "./num.ts";
4-import { table } from "./table.ts";
5-import { any } from "./any.ts";
6-import { obj } from "./obj.ts";
7-import { defaultLoader, defaultLoaderItem, loader } from "./loader.ts";
8+import { str, StrOutput } from "./str.ts";
9+import { num, NumOutput } from "./num.ts";
10+import { table, TableOutput } from "./table.ts";
11+import { any, AnyOutput } from "./any.ts";
12+import { obj, ObjOutput } from "./obj.ts";
13+import {
14+ defaultLoader,
15+ defaultLoaderItem,
16+ loader,
17+ LoaderOutput,
18+} from "./loader.ts";
19 export const slice = {
20 str,
21 num,
22@@ -13,3 +18,11 @@ export const slice = {
23 loader,
24 };
25 export { defaultLoader, defaultLoaderItem };
26+export type {
27+ AnyOutput,
28+ LoaderOutput,
29+ NumOutput,
30+ ObjOutput,
31+ StrOutput,
32+ TableOutput,
33+};