repos / starfx

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

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
M deno.lock
+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";
M store/fx.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+}
M store/mod.ts
+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";
A store/persist.test.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+});
A store/persist.ts
+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+}
A store/react.ts
+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+}
A store/selectors.ts
+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+};
D store/slice.ts
+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-};
M store/slice/mod.ts
+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+};