repos / starfx

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

commit
442f5d7
parent
b8d1fd5
author
Eric Bower
date
2023-07-14 13:25:01 +0000 UTC
feat: immutable store (#2)

54 files changed,  +2017, -1073
M mod.ts
M npm.ts
A action.ts
+18, -0
 1@@ -0,0 +1,18 @@
 2+import { QueryState } from "./types.ts";
 3+
 4+export const API_ACTION_PREFIX = "@@starfx";
 5+export const createAction = (curType: string) => {
 6+  if (!curType) throw new Error("createAction requires non-empty string");
 7+  const type = `${API_ACTION_PREFIX}/${curType}`;
 8+  const action = () => ({ type });
 9+  action.toString = () => type;
10+  return action;
11+};
12+
13+export const createQueryState = (s: Partial<QueryState> = {}): QueryState => {
14+  return {
15+    "@@starfx/loaders": {},
16+    "@@starfx/data": {},
17+    ...s,
18+  };
19+};
M api-type-template.ts
+1, -1
 1@@ -223,9 +223,9 @@ import type {
 2   Next,
 3   FetchJson,
 4   MiddlewareApiCo,
 5-  Payload,
 6   Supervisor,
 7 } from "./types.ts";
 8+import type { Payload } from "../types.ts";
 9 
10 export type ApiName = string | string[];
11 
M compose.test.ts
+4, -4
 1@@ -1,6 +1,6 @@
 2 import { describe, expect, it } from "./test.ts";
 3 
 4-import { run, sleep } from "./deps.ts";
 5+import { Ok, run, sleep } from "./deps.ts";
 6 import { compose } from "./compose.ts";
 7 
 8 const tests = describe("compose()");
 9@@ -20,11 +20,11 @@ it(tests, "should compose middleware", async () => {
10     return yield* mdw({ one: "", three: "" });
11   });
12 
13-  const expected = {
14+  const expected = Ok({
15     // we should see the mutation
16     one: "two",
17     three: "four",
18-  };
19+  });
20   expect(actual).toEqual(expected);
21 });
22 
23@@ -54,6 +54,6 @@ it(tests, "order of execution", async () => {
24   const actual = await run(function* () {
25     return yield* mdw({ actual: "" });
26   });
27-  const expected = { actual: "abcdefg" };
28+  const expected = Ok({ actual: "abcdefg" });
29   expect(actual).toEqual(expected);
30 });
M compose.ts
+16, -10
 1@@ -1,7 +1,7 @@
 2 import { call } from "./fx/mod.ts";
 3 import type { Next } from "./query/mod.ts";
 4-import type { Instruction, Operation, Result } from "./deps.ts";
 5-import { Err, Ok } from "./deps.ts";
 6+import type { Instruction, Operation } from "./deps.ts";
 7+import { Ok } from "./deps.ts";
 8 
 9 // deno-lint-ignore no-explicit-any
10 export type BaseCtx = Record<string, any>;
11@@ -23,26 +23,32 @@ export function compose<Ctx extends BaseCtx = BaseCtx, T = unknown>(
12     }
13   }
14 
15-  return function* composeFn(context: Ctx, next?: BaseMiddleware<Ctx, T>) {
16+  return function* composeFn(context: Ctx, mdw?: BaseMiddleware<Ctx, T>) {
17     // last called middleware #
18     let index = -1;
19 
20-    function* dispatch(i: number): Generator<Instruction, Result<T>, void> {
21+    function* dispatch(i: number): Generator<Instruction, T | undefined, void> {
22       if (i <= index) {
23-        return Err(new Error("next() called multiple times"));
24+        throw new Error("next() called multiple times");
25       }
26       index = i;
27       let fn: BaseMiddleware<Ctx, T> | undefined = middleware[i];
28       if (i === middleware.length) {
29-        fn = next;
30+        fn = mdw;
31+      }
32+      if (!fn) {
33+        return;
34       }
35-      if (!fn) return Err(new Error("fn is falsy"));
36       const nxt = dispatch.bind(null, i + 1);
37       const result = yield* fn(context, nxt);
38-      return Ok(result);
39+      return result;
40     }
41 
42-    yield* call(() => dispatch(0));
43-    return context;
44+    const result = yield* call(() => dispatch(0));
45+    if (result.ok) {
46+      return Ok(context);
47+    } else {
48+      return result;
49+    }
50   };
51 }
D context.ts
+0, -6
1@@ -1,6 +0,0 @@
2-import { getframe } from "./deps.ts";
3-
4-export function* contextualize(context: string, value: unknown) {
5-  const frame = yield* getframe();
6-  frame.context[context] = value;
7-}
M deno.lock
+4, -33
 1@@ -148,11 +148,11 @@
 2     "https://deno.land/x/ts_morph@18.0.0/common/typescript.d.ts": "21c0786dddf52537611499340166278507eb9784628d321c2cb6acc696cba0f6",
 3     "https://deno.land/x/ts_morph@18.0.0/common/typescript.js": "d5c598b6a2db2202d0428fca5fd79fc9a301a71880831a805d778797d2413c59",
 4     "https://esm.sh/@reduxjs/toolkit@1.9.5?pin=v122": "90302cd219f485943878c9f023338f069f3141b8118438fa70eb5759bac6e4b4",
 5-    "https://esm.sh/@testing-library/react@14.0.0?pin=v122": "dd4b49857ea73e0471e4fd9839cc142d48b4df0b53c6e5f21f6ca8861bd97989",
 6+    "https://esm.sh/immer@10.0.2?pin=v122": "7ac87b9c76176de8384a67f8cd93d44f75be1a7496c92707252acb669595c393",
 7     "https://esm.sh/react-redux@8.0.5?pin=v122": "fa98e94dc8803fb84bee9eb08a13f11833f634d381003247207682823887dc51",
 8-    "https://esm.sh/react@18.2.0": "8950a34a030620fce8349d6bd3913b3bdb186c5ec7968fa5ba4d054e22d78e6c",
 9     "https://esm.sh/react@18.2.0?pin=v122": "8950a34a030620fce8349d6bd3913b3bdb186c5ec7968fa5ba4d054e22d78e6c",
10     "https://esm.sh/redux-batched-actions@0.5.0?pin=v122": "bb9ba7abde4cbba4352e5d25cf8407795f962e6f7c47b59657ee91834fd6744c",
11+    "https://esm.sh/reselect@4.1.8?pin=v122": "486407fec8db8f0c87ba540ff6457dbec3c8ec8fa93a4845383bc8cdb33c6008",
12     "https://esm.sh/robodux@15.0.1?pin=v122": "51ac2aa6f6fbaac2795f3d34117ba7e77c37e9e3e48bf667a994f6851399bf76",
13     "https://esm.sh/stable/react@18.2.0/denonext/react.mjs": "3c4f23bcfc53b256fcfaf6f834fa9f584c3bb7be667b2682c6cb6ba8ef88f8e6",
14     "https://esm.sh/v118/@types/prop-types@15.7.5/index.d.ts": "6a386ff939f180ae8ef064699d8b7b6e62bc2731a62d7fbf5e02589383838dea",
15@@ -187,35 +187,13 @@
16     "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/serializableStateInvariantMiddleware.d.ts": "f9ff1b947da0074054d30826aaa38e876d1b7027b92324a346ef27c8e0865267",
17     "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/tsHelpers.d.ts": "1d7bf5f58c4a24f4a654a2af61207662b910fa429fafa1d32518c4c6cc925c99",
18     "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/utils.d.ts": "9f14472b238598bdf200a0de9f05dd6568e1171fd753fbd72c01a46bf336145d",
19-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/config.d.ts": "2a90177ebaef25de89351de964c2c601ab54d6e3a157cba60d9cd3eaf5a5ee1a",
20-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/events.d.ts": "1d2699a343a347a830be26eb17ab340d7875c6f549c8d7477efb1773060cc7e5",
21-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/get-node-text.d.ts": "a0a6f0095f25f08a7129bc4d7cb8438039ec422dc341218d274e1e5131115988",
22-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/get-queries-for-element.d.ts": "61b0bd9a20e0738fd87e67017a69df89106f12e516fdd15ce0a889f7c60d479f",
23-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/index.d.ts": "561aeabb2e1fa95bc9d1f9153ccb5e8cd8fb7ffd5a412616c3cfb24dfa613d79",
24-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/matches.d.ts": "0f4c3516347edf0800b6cbb38c84addc96de419019ea3e440d8866cc7b87733d",
25-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/pretty-dom.d.ts": "2d0c0bee7f35288ccb19c673ad8ffdf06b5f6052be8ddc4df0782b1412b5f22a",
26-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/queries.d.ts": "a2a6a00d5bd1e4a1c4782a291309c0269112492641c7167904af156a0bc7ece7",
27-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/query-helpers.d.ts": "56f39bce2cd0e3f3cdcb4bea7175eddba13ee18b91aa3ecc5d42dadc5fb64ccc",
28-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/role-helpers.d.ts": "a3ce619711ff1bcdaaf4b5187d1e3f84e76064909a7c7ecb2e2f404f145b7b5c",
29-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/screen.d.ts": "678c9811a30c4d5d94e185c4e2526bb0194b852eef79b10722920048a10b8edb",
30-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/suggestions.d.ts": "82200e963d3c767976a5a9f41ecf8c65eca14a6b33dcbe00214fcbe959698c46",
31-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/wait-for-element-to-be-removed.d.ts": "278ba90329ef4874f485dbd9f4e2ede0a71f0b10dbca0a6b0562d013f343d247",
32-    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/wait-for.d.ts": "8387ec1601cf6b8948672537cf8d430431ba0d87b1f9537b4597c1ab8d3ade5b",
33-    "https://esm.sh/v122/@testing-library/dom@9.3.0/denonext/dom.mjs": "bc1b9ef199c3d07f513942175d5f563cbc9b392f5be608d263abd06aa1950985",
34-    "https://esm.sh/v122/@testing-library/react@14.0.0/denonext/react.mjs": "60b2b640e50332a27d7bbb8a1ee75e397821726eacd3d0ff3850cf8f6b2559b2",
35-    "https://esm.sh/v122/@testing-library/react@14.0.0/types/index.d.ts": "73edecc15b451ea14df5b698d53ce96549ae0ac1911cdfcd40e61581ac695f73",
36-    "https://esm.sh/v122/@types/aria-query@5.0.1/index.d.ts": "21522c0f405e58c8dd89cd97eb3d1aa9865ba017fde102d01f86ab50b44e5610",
37     "https://esm.sh/v122/@types/hoist-non-react-statics@3.3.1/index.d.ts": "a84059e8ce2394008282b5a395b28820e4c2fd2da0cd4a15d0d50631b9993854",
38     "https://esm.sh/v122/@types/react-dom@18.2.4/index.d.ts": "7f1ca21337a49885088a365081b652451342e45d8c29dfbf97243c66a3f60196",
39-    "https://esm.sh/v122/@types/react-dom@18.2.4/test-utils/index.d.ts": "00f67209791dc2295fce29aa9c7632621dd7605b83a94346275c232b33fd2927",
40     "https://esm.sh/v122/@types/use-sync-external-store@0.0.3/index.d.ts": "61f41da9aaa809e5142b1d849d4e70f3e09913a5cb32c629bf6e61ef27967ff7",
41     "https://esm.sh/v122/@types/use-sync-external-store@0.0.3/with-selector.d.ts": "da0195f35a277ff34bb5577062514ce75b7a1b12f476d6be3d4489e26fcf00d8",
42-    "https://esm.sh/v122/ansi-regex@5.0.1/denonext/ansi-regex.mjs": "738d194ad7037256cfcd3f87d3716d28096261492d163395badfbdab3f72ce15",
43-    "https://esm.sh/v122/ansi-styles@5.2.0/denonext/ansi-styles.mjs": "8a10e68165432fcfd0929bdeb4e33a194af8b790702f60b2ea0477749990964c",
44-    "https://esm.sh/v122/aria-query@5.3.0/denonext/aria-query.mjs": "403e8892efc7c54a89afe84852e7c932a72afe78891eb67eef67bb1e233e5cda",
45-    "https://esm.sh/v122/dequal@2.0.3/denonext/lite.js": "6a3dedd0ac89564a613bd65c79813852e4f234d4d3122b4f9cfb153622256b82",
46-    "https://esm.sh/v122/dom-accessibility-api@0.5.16/denonext/dom-accessibility-api.mjs": "3d546b0f6750cf4c72ad9d049eeb454226e76b6a56d03f6018aa7bad8798871b",
47     "https://esm.sh/v122/hoist-non-react-statics@3.3.2/denonext/hoist-non-react-statics.mjs": "41018d0142e45a133637f9a3e4da6b8babc22cab2b3ec05cfb202a727da5a0cb",
48+    "https://esm.sh/v122/immer@10.0.2/denonext/immer.mjs": "694ebf85b769db4d026ec4b9655a8caaa6ce51776d857df7cedf3fa4774d5297",
49+    "https://esm.sh/v122/immer@10.0.2/dist/immer.d.ts": "058561900fef84704cbc45a29355da6487be9380b8ed0072f3a03693769a5dd0",
50     "https://esm.sh/v122/immer@9.0.21/denonext/immer.mjs": "3819c7f2cc0f19de974517bd2421b80f800a9bc8bcdb87e3b3aaf022640bd7d6",
51     "https://esm.sh/v122/immer@9.0.21/dist/core/current.d.ts": "99cf6d12499eac02b32c6fa20a8d361a43575ff2fbb774091921c1fa948d046a",
52     "https://esm.sh/v122/immer@9.0.21/dist/core/finalize.d.ts": "4e39c4f3c1a84861990fdf88c2ae5fd629e2445f6a8f24bff38fcfd95992b3b6",
53@@ -234,15 +212,8 @@
54     "https://esm.sh/v122/immer@9.0.21/dist/utils/env.d.ts": "f5a35aab17a4fb73b9bca92b3db33cc10649f2af281862a01231ce8d0d500d40",
55     "https://esm.sh/v122/immer@9.0.21/dist/utils/errors.d.ts": "7a43554d2b957482977f76623246b78fab4bdfee55daae1531300f37602d4a88",
56     "https://esm.sh/v122/immer@9.0.21/dist/utils/plugins.d.ts": "282ca31db57b351ef685ea90d97dea434755390329b1297f3d67ee06dae144d0",
57-    "https://esm.sh/v122/lz-string@1.5.0/denonext/lz-string.mjs": "d7c5e1cdf048a845f4da9563ecc9fca66d2caddf625d6632512bc8a477ce75d7",
58-    "https://esm.sh/v122/pretty-format@27.5.1/build/index.d.ts": "56a50e283257c7fc16958f9e31d8260262505551782a6afc31030970ff48775a",
59-    "https://esm.sh/v122/pretty-format@27.5.1/build/types.d.ts": "462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094",
60-    "https://esm.sh/v122/pretty-format@27.5.1/denonext/pretty-format.mjs": "720565b4d3cfdf52b0fdcb2042fef473341db961d34bfa8658ba5b7212af6f69",
61-    "https://esm.sh/v122/react-dom@18.2.0/denonext/client.js": "19e10c27f3a6e07c66bd37e59bf77de53dc62e0956c04eb97ffe84805cb05832",
62     "https://esm.sh/v122/react-dom@18.2.0/denonext/react-dom.mjs": "6efaa0beeb36e4c6567824ef5b24ed7856b64e30ec245fbaedf8bed748e6e121",
63-    "https://esm.sh/v122/react-dom@18.2.0/denonext/test-utils.js": "fff128c551af42604a3efbb593e0d6f37e33644ad3e2d8cd4207a19f603084cf",
64     "https://esm.sh/v122/react-is@16.13.1/denonext/react-is.mjs": "4fc2bb5cfecb53c0f28cd7b730101ff83c05ca06fdd373bc2d6a9bdf662f96d4",
65-    "https://esm.sh/v122/react-is@17.0.2/denonext/react-is.mjs": "b56e6628b118f3621e68402d70b71e691e69f83de02d092b1997070e1070b799",
66     "https://esm.sh/v122/react-is@18.2.0/denonext/react-is.mjs": "07ca6a7473ccbb06466e805c8253ade80bd664a0f486cf020864c22c27a60479",
67     "https://esm.sh/v122/react-redux@8.0.5/denonext/react-redux.mjs": "eeba4c3485907ed356f889ce071f63fde42e8397f14dede20d9732e5be188294",
68     "https://esm.sh/v122/react-redux@8.0.5/es/components/Context.d.ts": "7c66a0e01cd2d48baf1572a2f697565765140cc5cdff8e5a04b06a4295591845",
M deps.ts
+8, -10
 1@@ -36,6 +36,14 @@ export {
 2   useDispatch,
 3   useSelector,
 4 } from "https://esm.sh/react-redux@8.0.5?pin=v122";
 5+export { createSelector } from "https://esm.sh/reselect@4.1.8?pin=v122";
 6+
 7+export {
 8+  enablePatches,
 9+  produce,
10+  produceWithPatches,
11+} from "https://esm.sh/immer@10.0.2?pin=v122";
12+export type { Patch } from "https://esm.sh/immer@10.0.2?pin=v122";
13 
14 export type {
15   Action,
16@@ -58,18 +66,8 @@ export {
17   batchActions,
18   enableBatching,
19 } from "https://esm.sh/redux-batched-actions@0.5.0?pin=v122";
20-export type {
21-  LoadingItemState,
22-  LoadingMapPayload,
23-  LoadingState,
24-  MapEntity,
25-} from "https://esm.sh/robodux@15.0.1?pin=v122";
26 export {
27-  createAction,
28-  createAssign,
29   createLoaderTable,
30   createReducerMap,
31   createTable,
32-  defaultLoader,
33-  defaultLoadingItem,
34 } from "https://esm.sh/robodux@15.0.1?pin=v122";
D fx/cancel.ts
+0, -7
1@@ -1,7 +0,0 @@
2-import { contextualize } from "../context.ts";
3-import type { Task } from "../deps.ts";
4-
5-export function* cancel<T>(task: Task<T>) {
6-  yield* task.halt();
7-  yield* contextualize("cancelled", true);
8-}
D fx/emit.ts
+0, -21
 1@@ -1,21 +0,0 @@
 2-import type { Action, Channel, Operation } from "../deps.ts";
 3-
 4-import { parallel } from "./parallel.ts";
 5-
 6-export function* emit({
 7-  channel,
 8-  action,
 9-}: {
10-  channel: Operation<Channel<Action, void>>;
11-  action: Action | Action[];
12-}) {
13-  const { input } = yield* channel;
14-  if (Array.isArray(action)) {
15-    if (action.length === 0) {
16-      return;
17-    }
18-    yield* parallel(action.map((a) => () => input.send(a)));
19-  } else {
20-    yield* input.send(action);
21-  }
22-}
M fx/mod.ts
+0, -2
 1@@ -1,9 +1,7 @@
 2 export * from "./parallel.ts";
 3 export * from "./call.ts";
 4-export * from "./cancel.ts";
 5 export * from "./cancelled.ts";
 6 export * from "./race.ts";
 7-export * from "./emit.ts";
 8 export * from "./request.ts";
 9 export * from "./watch.ts";
10 export * from "./defer.ts";
A matcher.ts
+43, -0
 1@@ -0,0 +1,43 @@
 2+import type { AnyAction } from "./store/types.ts";
 3+
 4+type ActionType = string;
 5+type GuardPredicate<G extends T, T = unknown> = (arg: T) => arg is G;
 6+type Predicate = (action: AnyAction) => boolean;
 7+type StringableActionCreator<A extends AnyAction = AnyAction> = {
 8+  (...args: unknown[]): A;
 9+  toString(): string;
10+};
11+type SubPattern = Predicate | StringableActionCreator | ActionType;
12+export type Pattern = SubPattern | SubPattern[];
13+type ActionSubPattern<Guard extends AnyAction = AnyAction> =
14+  | GuardPredicate<Guard, AnyAction>
15+  | StringableActionCreator<Guard>
16+  | Predicate
17+  | ActionType;
18+export type ActionPattern<Guard extends AnyAction = AnyAction> =
19+  | ActionSubPattern<Guard>
20+  | ActionSubPattern<Guard>[];
21+
22+export function matcher(pattern: ActionPattern): (input: AnyAction) => boolean {
23+  if (pattern === "*") {
24+    return (input: AnyAction) => !!input;
25+  }
26+
27+  if (typeof pattern === "string") {
28+    return (input: AnyAction) => pattern === input.type;
29+  }
30+
31+  if (Array.isArray(pattern)) {
32+    return (input: AnyAction) => pattern.some((p) => matcher(p)(input));
33+  }
34+
35+  if (typeof pattern === "function" && Object.hasOwn(pattern, "toString")) {
36+    return (input: AnyAction) => pattern.toString() === input.type;
37+  }
38+
39+  if (typeof pattern === "function") {
40+    return (input: AnyAction) => pattern(input) as boolean;
41+  }
42+
43+  throw new Error("invalid pattern");
44+}
M mod.ts
+2, -1
 1@@ -1,8 +1,9 @@
 2 export * from "./fx/mod.ts";
 3+export * from "./query/mod.ts";
 4 export * from "./types.ts";
 5 export * from "./iter.ts";
 6-export * from "./context.ts";
 7 export * from "./compose.ts";
 8+export * from "./action.ts";
 9 export {
10   action,
11   createChannel,
M npm.ts
+20, -11
 1@@ -1,4 +1,4 @@
 2-import { assert, build, emptyDir } from "./test.ts";
 3+import { build, emptyDir } from "./test.ts";
 4 
 5 main().then(console.log).catch(console.error);
 6 
 7@@ -6,7 +6,9 @@ async function main() {
 8   await emptyDir("./npm");
 9 
10   const version = Deno.env.get("NPM_VERSION");
11-  assert(version, "NPM_VERSION is required to build npm package");
12+  if (!version) {
13+    throw new Error("NPM_VERSION is required to build npm package");
14+  }
15 
16   await build({
17     entryPoints: [
18@@ -19,12 +21,12 @@ async function main() {
19         path: "react.ts",
20       },
21       {
22-        name: "./redux",
23-        path: "redux/mod.ts",
24+        name: "./store",
25+        path: "./store/mod.ts",
26       },
27       {
28-        name: "./query",
29-        path: "./query/mod.ts",
30+        name: "./redux",
31+        path: "./redux/mod.ts",
32       },
33     ],
34     mappings: {
35@@ -37,6 +39,18 @@ async function main() {
36         version: "^18.2.0",
37         peerDependency: true,
38       },
39+      "https://esm.sh/react-redux@8.0.5?pin=v122": {
40+        name: "react-redux",
41+        version: "^8.0.5",
42+      },
43+      "https://esm.sh/immer@10.0.2?pin=v122": {
44+        name: "immer",
45+        version: "^10.0.2",
46+      },
47+      "https://esm.sh/reselect@4.1.8?pin=v122": {
48+        name: "reselect",
49+        version: "^4.1.8",
50+      },
51       "https://esm.sh/robodux@15.0.1?pin=v122": {
52         name: "robodux",
53         version: "^15.0.1",
54@@ -49,11 +63,6 @@ async function main() {
55         name: "redux-batched-actions",
56         version: "^0.5.0",
57       },
58-      "https://esm.sh/react-redux@8.0.5?pin=v122": {
59-        name: "react-redux",
60-        version: "^8.0.5",
61-        peerDependency: true,
62-      },
63     },
64     outDir: "./npm",
65     shims: {
M query/api-types.ts
+1, -1
 1@@ -10,9 +10,9 @@ import type {
 2   FetchJson,
 3   MiddlewareApiCo,
 4   Next,
 5-  Payload,
 6   Supervisor,
 7 } from "./types.ts";
 8+import type { Payload } from "../types.ts";
 9 
10 export type ApiName = string | string[];
11 
M query/api.test.ts
+61, -71
  1@@ -1,16 +1,19 @@
  2 import { describe, expect, it } from "../test.ts";
  3 
  4 import { call, keepAlive } from "../fx/mod.ts";
  5-import { configureStore, put, takeEvery } from "../redux/mod.ts";
  6-import { createAction, createReducerMap, createTable } from "../deps.ts";
  7-import type { MapEntity } from "../deps.ts";
  8+import {
  9+  configureStore,
 10+  storeMdw,
 11+  takeEvery,
 12+  updateStore,
 13+} from "../store/mod.ts";
 14+import { createQueryState } from "../action.ts";
 15 
 16 import { queryCtx, requestMonitor, urlParser } from "./middleware.ts";
 17 import { createApi } from "./api.ts";
 18-import { sleep } from "./util.ts";
 19+import { sleep } from "../test.ts";
 20 import { createKey } from "./create-key.ts";
 21 import type { ApiCtx } from "./types.ts";
 22-import { poll } from "./supervisor.ts";
 23 
 24 interface User {
 25   id: string;
 26@@ -24,13 +27,9 @@ const jsonBlob = (data: unknown) => {
 27   return JSON.stringify(data);
 28 };
 29 
 30-const reducers = { init: () => null };
 31-
 32 const tests = describe("createApi()");
 33 
 34 it(tests, "createApi - POST", async () => {
 35-  const name = "users";
 36-  const cache = createTable<User>({ name });
 37   const query = createApi();
 38 
 39   query.use(queryCtx);
 40@@ -74,17 +73,17 @@ it(tests, "createApi - POST", async () => {
 41       const result = new TextDecoder("utf-8").decode(buff.value);
 42       const { users } = JSON.parse(result);
 43       if (!users) return;
 44-      const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
 45-        acc[u.id] = u;
 46-        return acc;
 47-      }, {});
 48-      yield* put(cache.actions.add(curUsers));
 49+
 50+      yield* updateStore<{ users: { [key: string]: User } }>((state) => {
 51+        (users as User[]).forEach((u) => {
 52+          state.users[u.id] = u;
 53+        });
 54+      });
 55     },
 56   );
 57 
 58-  const reducers = createReducerMap(cache);
 59-  const { store, fx } = configureStore({ reducers });
 60-  fx.run(query.bootup);
 61+  const store = await configureStore({ initialState: { users: {} } });
 62+  store.run(query.bootup);
 63 
 64   store.dispatch(createUser({ email: mockUser.email }));
 65   await sleep(150);
 66@@ -93,11 +92,8 @@ it(tests, "createApi - POST", async () => {
 67   });
 68 });
 69 
 70-it(tests, "POST with uri", () => {
 71-  const name = "users";
 72-  const cache = createTable<User>({ name });
 73+it(tests, "POST with uri", async () => {
 74   const query = createApi();
 75-
 76   query.use(queryCtx);
 77   query.use(urlParser);
 78   query.use(query.routes());
 79@@ -128,21 +124,19 @@ it(tests, "POST with uri", () => {
 80     yield* next();
 81     if (!ctx.json.ok) return;
 82     const { users } = ctx.json.data;
 83-    const curUsers = users.reduce<MapEntity<User>>((acc, u) => {
 84-      acc[u.id] = u;
 85-      return acc;
 86-    }, {});
 87-    yield* put(cache.actions.add(curUsers));
 88+    yield* updateStore<{ users: { [key: string]: User } }>((state) => {
 89+      users.forEach((u) => {
 90+        state.users[u.id] = u;
 91+      });
 92+    });
 93   });
 94 
 95-  const reducers = createReducerMap(cache);
 96-  const { store, fx } = configureStore({ reducers });
 97-  fx.run(query.bootup);
 98-
 99+  const store = await configureStore({ initialState: { users: {} } });
100+  store.run(query.bootup);
101   store.dispatch(createUser({ email: mockUser.email }));
102 });
103 
104-it(tests, "middleware - with request fn", () => {
105+it(tests, "middleware - with request fn", async () => {
106   const query = createApi();
107   query.use(queryCtx);
108   query.use(urlParser);
109@@ -153,13 +147,12 @@ it(tests, "middleware - with request fn", () => {
110     yield* next();
111   });
112   const createUser = query.create("/users", query.request({ method: "POST" }));
113-  const { store, fx } = configureStore({ reducers });
114-  fx.run(query.bootup);
115-
116+  const store = await configureStore({ initialState: { users: {} } });
117+  store.run(query.bootup);
118   store.dispatch(createUser());
119 });
120 
121-it(tests, "run() on endpoint action - should run the effect", () => {
122+it(tests, "run() on endpoint action - should run the effect", async () => {
123   const api = createApi<TestCtx>();
124   api.use(api.routes());
125   let acc = "";
126@@ -177,13 +170,12 @@ it(tests, "run() on endpoint action - should run the effect", () => {
127     expect(acc).toEqual("ab");
128   });
129 
130-  const { store, fx } = configureStore({ reducers });
131-  fx.run(api.bootup);
132-
133+  const store = await configureStore({ initialState: { users: {} } });
134+  store.run(api.bootup);
135   store.dispatch(action2());
136 });
137 
138-it(tests, "run() from a normal saga", () => {
139+it(tests, "run() from a normal saga", async () => {
140   const api = createApi();
141   api.use(api.routes());
142   let acc = "";
143@@ -191,7 +183,7 @@ it(tests, "run() from a normal saga", () => {
144     yield* next();
145     acc += "a";
146   });
147-  const action2 = createAction("ACTION");
148+  const action2 = () => ({ type: "ACTION" });
149   function* onAction() {
150     const ctx = yield* call(() => action1.run(action1({ id: "1" })));
151     if (!ctx.ok) {
152@@ -211,15 +203,15 @@ it(tests, "run() from a normal saga", () => {
153     yield* task;
154   }
155 
156-  const { store, fx } = configureStore({ reducers });
157-  fx.run(() => keepAlive([api.bootup, watchAction]));
158-
159+  const store = await configureStore({ initialState: { users: {} } });
160+  store.run(() => keepAlive([api.bootup, watchAction]));
161   store.dispatch(action2());
162 });
163 
164 it(tests, "createApi with hash key on a large post", async () => {
165   const query = createApi();
166   query.use(requestMonitor());
167+  query.use(storeMdw());
168   query.use(query.routes());
169   query.use(function* fetchApi(ctx, next) {
170     const data = {
171@@ -246,10 +238,13 @@ it(tests, "createApi with hash key on a large post", async () => {
172       const result = new TextDecoder("utf-8").decode(buff.value);
173       const { users } = JSON.parse(result);
174       if (!users) return;
175-      const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
176-        acc[u.id] = u;
177-        return acc;
178-      }, {});
179+      const curUsers = (users as User[]).reduce<Record<string, User>>(
180+        (acc, u) => {
181+          acc[u.id] = u;
182+          return acc;
183+        },
184+        {},
185+      );
186       ctx.response = new Response();
187       ctx.json = {
188         ok: true,
189@@ -260,13 +255,15 @@ it(tests, "createApi with hash key on a large post", async () => {
190 
191   const email = mockUser.email + "9";
192   const largetext = "abc-def-ghi-jkl-mno-pqr".repeat(100);
193-  const reducers = createReducerMap();
194-
195-  const { store, fx } = configureStore({ reducers });
196-  fx.run(query.bootup);
197 
198+  const store = await configureStore({
199+    initialState: { ...createQueryState(), users: {} },
200+  });
201+  store.run(query.bootup);
202   store.dispatch(createUserDefaultKey({ email, largetext }));
203+
204   await sleep(150);
205+
206   const s = store.getState();
207   const expectedKey = createKey(`${createUserDefaultKey}`, {
208     email,
209@@ -274,8 +271,7 @@ it(tests, "createApi with hash key on a large post", async () => {
210   });
211 
212   expect([8, 9].includes(expectedKey.split("|")[1].length)).toBeTruthy();
213-
214-  expect(s["@@saga-query/data"][expectedKey]).toEqual({
215+  expect(s["@@starfx/data"][expectedKey]).toEqual({
216     "1": { id: "1", name: "test", email: email, largetext: largetext },
217   });
218 });
219@@ -284,6 +280,7 @@ it(tests, "createApi - two identical endpoints", async () => {
220   const actual: string[] = [];
221   const api = createApi();
222   api.use(requestMonitor());
223+  api.use(storeMdw());
224   api.use(api.routes());
225 
226   const first = api.get("/health", function* (ctx, next) {
227@@ -293,24 +290,19 @@ it(tests, "createApi - two identical endpoints", async () => {
228 
229   const second = api.get(
230     ["/health", "poll"],
231-    { supervisor: poll(1 * 1000) },
232     function* (ctx, next) {
233       actual.push(ctx.req().url);
234       yield* next();
235     },
236   );
237 
238-  const { store, fx } = configureStore({ reducers });
239-  fx.run(api.bootup);
240-
241+  const store = await configureStore({ initialState: { users: {} } });
242+  store.run(api.bootup);
243   store.dispatch(first());
244   store.dispatch(second());
245 
246   await sleep(150);
247 
248-  // stop poll
249-  store.dispatch(second());
250-
251   expect(actual).toEqual(["/health", "/health"]);
252 });
253 
254@@ -319,7 +311,7 @@ interface TestCtx<P = any, S = any, E = any> extends ApiCtx<P, S, E> {
255 }
256 
257 // this is strictly for testing types
258-it(tests, "ensure types for get() endpoint", () => {
259+it(tests, "ensure types for get() endpoint", async () => {
260   const api = createApi<TestCtx>();
261   api.use(api.routes());
262   api.use(function* (ctx, next) {
263@@ -342,8 +334,8 @@ it(tests, "ensure types for get() endpoint", () => {
264     },
265   );
266 
267-  const { store, fx } = configureStore({ reducers });
268-  fx.run(api.bootup);
269+  const store = await configureStore({ initialState: { users: {} } });
270+  store.run(api.bootup);
271 
272   store.dispatch(action1({ id: "1" }));
273   expect(acc).toEqual(["1", "wow"]);
274@@ -355,7 +347,7 @@ interface FetchUserProps {
275 type FetchUserCtx = TestCtx<FetchUserProps>;
276 
277 // this is strictly for testing types
278-it(tests, "ensure ability to cast `ctx` in function definition", () => {
279+it(tests, "ensure ability to cast `ctx` in function definition", async () => {
280   const api = createApi<TestCtx>();
281   api.use(api.routes());
282   api.use(function* (ctx, next) {
283@@ -378,9 +370,8 @@ it(tests, "ensure ability to cast `ctx` in function definition", () => {
284     },
285   );
286 
287-  const { store, fx } = configureStore({ reducers });
288-  fx.run(api.bootup);
289-
290+  const store = await configureStore({ initialState: { users: {} } });
291+  store.run(api.bootup);
292   store.dispatch(action1({ id: "1" }));
293   expect(acc).toEqual(["1", "wow"]);
294 });
295@@ -391,7 +382,7 @@ type FetchUserSecondCtx = TestCtx<any, { result: string }>;
296 it(
297   tests,
298   "ensure ability to cast `ctx` in function definition with no props",
299-  () => {
300+  async () => {
301     const api = createApi<TestCtx>();
302     api.use(api.routes());
303     api.use(function* (ctx, next) {
304@@ -413,9 +404,8 @@ it(
305       },
306     );
307 
308-    const { store, fx } = configureStore({ reducers });
309-    fx.run(api.bootup);
310-
311+    const store = await configureStore({ initialState: { users: {} } });
312+    store.run(api.bootup);
313     store.dispatch(action1());
314     expect(acc).toEqual(["wow"]);
315   },
D query/constant.ts
+0, -1
1@@ -1 +0,0 @@
2-export const API_ACTION_PREFIX = "@@starfx";
M query/create-key.test.ts
+0, -3
 1@@ -2,7 +2,6 @@ import { describe, expect, it } from "../test.ts";
 2 
 3 import type { ActionWithPayload } from "./types.ts";
 4 import { createApi } from "./api.ts";
 5-import { poll } from "./supervisor.ts";
 6 
 7 const getKeyOf = (action: ActionWithPayload<{ key: string }>): string =>
 8   action.payload.key;
 9@@ -18,7 +17,6 @@ it(
10     // no param
11     const action0 = api.get(
12       "/users",
13-      { supervisor: poll(5 * 1000) }, // with poll middleware
14       function* (ctx, next) {
15         ctx.request = {
16           method: "GET",
17@@ -43,7 +41,6 @@ it(
18       [key: string]: string | boolean | number | null | undefined;
19     }>(
20       "/users",
21-      { supervisor: poll(5 * 1000) }, // with poll middleware
22       function* (ctx, next) {
23         ctx.request = {
24           method: "GET",
M query/fetch.test.ts
+57, -30
  1@@ -1,5 +1,7 @@
  2 import { describe, expect, install, it, mock } from "../test.ts";
  3-import { configureStore } from "../redux/mod.ts";
  4+import { configureStore, storeMdw } from "../store/mod.ts";
  5+import { createQueryState } from "../action.ts";
  6+import type { QueryState } from "../types.ts";
  7 
  8 import { fetcher, fetchRetry } from "./fetch.ts";
  9 import { createApi } from "./api.ts";
 10@@ -9,7 +11,6 @@ install();
 11 
 12 const baseUrl = "https://saga-query.com";
 13 const mockUser = { id: "1", email: "test@saga-query.com" };
 14-const reducers = { init: () => null };
 15 
 16 const delay = (n = 200) =>
 17   new Promise((resolve) => {
 18@@ -28,6 +29,7 @@ it(
 19 
 20     const api = createApi();
 21     api.use(requestMonitor());
 22+    api.use(storeMdw());
 23     api.use(api.routes());
 24     api.use(fetcher({ baseUrl }));
 25 
 26@@ -46,8 +48,10 @@ it(
 27       expect(ctx.json).toEqual({ ok: true, data: mockUser });
 28     });
 29 
 30-    const { store, fx } = configureStore({ reducers });
 31-    fx.run(api.bootup);
 32+    const store = await configureStore<QueryState>({
 33+      initialState: createQueryState(),
 34+    });
 35+    store.run(api.bootup);
 36 
 37     const action = fetchUsers();
 38     store.dispatch(action);
 39@@ -55,7 +59,7 @@ it(
 40     await delay();
 41 
 42     const state = store.getState();
 43-    expect(state["@@saga-query/data"][action.payload.key]).toEqual(mockUser);
 44+    expect(state["@@starfx/data"][action.payload.key]).toEqual(mockUser);
 45   },
 46 );
 47 
 48@@ -69,6 +73,7 @@ it(
 49 
 50     const api = createApi();
 51     api.use(requestMonitor());
 52+    api.use(storeMdw());
 53     api.use(api.routes());
 54     api.use(fetcher({ baseUrl }));
 55 
 56@@ -79,8 +84,10 @@ it(
 57       expect(ctx.json).toEqual({ ok: true, data: "this is some text" });
 58     });
 59 
 60-    const { store, fx } = configureStore({ reducers });
 61-    fx.run(api.bootup);
 62+    const store = await configureStore<QueryState>({
 63+      initialState: createQueryState(),
 64+    });
 65+    store.run(api.bootup);
 66 
 67     const action = fetchUsers();
 68     store.dispatch(action);
 69@@ -97,6 +104,7 @@ it(tests, "fetch - error handling", async () => {
 70 
 71   const api = createApi();
 72   api.use(requestMonitor());
 73+  api.use(storeMdw());
 74   api.use(api.routes());
 75   api.use(function* (ctx, next) {
 76     const url = ctx.req().url;
 77@@ -112,8 +120,10 @@ it(tests, "fetch - error handling", async () => {
 78     expect(ctx.json).toEqual({ ok: false, data: errMsg });
 79   });
 80 
 81-  const { store, fx } = configureStore({ reducers });
 82-  fx.run(api.bootup);
 83+  const store = await configureStore<QueryState>({
 84+    initialState: createQueryState(),
 85+  });
 86+  store.run(api.bootup);
 87 
 88   const action = fetchUsers();
 89   store.dispatch(action);
 90@@ -121,7 +131,7 @@ it(tests, "fetch - error handling", async () => {
 91   await delay();
 92 
 93   const state = store.getState();
 94-  expect(state["@@saga-query/data"][action.payload.key]).toEqual(errMsg);
 95+  expect(state["@@starfx/data"][action.payload.key]).toEqual(errMsg);
 96 });
 97 
 98 it(tests, "fetch - status 204", async () => {
 99@@ -131,6 +141,7 @@ it(tests, "fetch - status 204", async () => {
100 
101   const api = createApi();
102   api.use(requestMonitor());
103+  api.use(storeMdw());
104   api.use(api.routes());
105   api.use(function* (ctx, next) {
106     const url = ctx.req().url;
107@@ -146,8 +157,10 @@ it(tests, "fetch - status 204", async () => {
108     expect(ctx.json).toEqual({ ok: true, data: {} });
109   });
110 
111-  const { store, fx } = configureStore({ reducers });
112-  fx.run(api.bootup);
113+  const store = await configureStore<QueryState>({
114+    initialState: createQueryState(),
115+  });
116+  store.run(api.bootup);
117 
118   const action = fetchUsers();
119   store.dispatch(action);
120@@ -155,7 +168,7 @@ it(tests, "fetch - status 204", async () => {
121   await delay();
122 
123   const state = store.getState();
124-  expect(state["@@saga-query/data"][action.payload.key]).toEqual({});
125+  expect(state["@@starfx/data"][action.payload.key]).toEqual({});
126 });
127 
128 it(tests, "fetch - malformed json", async () => {
129@@ -165,6 +178,7 @@ it(tests, "fetch - malformed json", async () => {
130 
131   const api = createApi();
132   api.use(requestMonitor());
133+  api.use(storeMdw());
134   api.use(api.routes());
135   api.use(function* (ctx, next) {
136     const url = ctx.req().url;
137@@ -186,9 +200,10 @@ it(tests, "fetch - malformed json", async () => {
138     });
139   });
140 
141-  const { store, fx } = configureStore({ reducers });
142-  fx.run(api.bootup);
143-
144+  const store = await configureStore<QueryState>({
145+    initialState: createQueryState(),
146+  });
147+  store.run(api.bootup);
148   const action = fetchUsers();
149   store.dispatch(action);
150 
151@@ -202,6 +217,7 @@ it(tests, "fetch - POST", async () => {
152 
153   const api = createApi();
154   api.use(requestMonitor());
155+  api.use(storeMdw());
156   api.use(api.routes());
157   api.use(fetcher({ baseUrl }));
158 
159@@ -222,9 +238,10 @@ it(tests, "fetch - POST", async () => {
160     expect(ctx.json).toEqual({ ok: true, data: mockUser });
161   });
162 
163-  const { store, fx } = configureStore({ reducers });
164-  fx.run(api.bootup);
165-
166+  const store = await configureStore<QueryState>({
167+    initialState: createQueryState(),
168+  });
169+  store.run(api.bootup);
170   const action = fetchUsers();
171   store.dispatch(action);
172 
173@@ -238,6 +255,7 @@ it(tests, "fetch - POST multiple endpoints with same uri", async () => {
174 
175   const api = createApi();
176   api.use(requestMonitor());
177+  api.use(storeMdw());
178   api.use(api.routes());
179   api.use(fetcher({ baseUrl }));
180 
181@@ -281,8 +299,10 @@ it(tests, "fetch - POST multiple endpoints with same uri", async () => {
182     },
183   );
184 
185-  const { store, fx } = configureStore({ reducers });
186-  fx.run(api.bootup);
187+  const store = await configureStore<QueryState>({
188+    initialState: createQueryState(),
189+  });
190+  store.run(api.bootup);
191 
192   store.dispatch(fetchUsers({ id: "1" }));
193   store.dispatch(fetchUsersSecond({ id: "1" }));
194@@ -296,6 +316,7 @@ it(
195   async () => {
196     const api = createApi();
197     api.use(requestMonitor());
198+    api.use(storeMdw());
199     api.use(api.routes());
200     api.use(fetcher({ baseUrl }));
201 
202@@ -315,9 +336,10 @@ it(
203       },
204     );
205 
206-    const { store, fx } = configureStore({ reducers });
207-    fx.run(api.bootup);
208-
209+    const store = await configureStore<QueryState>({
210+      initialState: createQueryState(),
211+    });
212+    store.run(api.bootup);
213     const action = fetchUsers({ id: "" });
214     store.dispatch(action);
215 
216@@ -342,6 +364,7 @@ it(
217 
218     const api = createApi();
219     api.use(requestMonitor());
220+    api.use(storeMdw());
221     api.use(api.routes());
222     api.use(fetcher({ baseUrl }));
223 
224@@ -359,8 +382,10 @@ it(
225       fetchRetry((n) => (n > 4 ? -1 : 10)),
226     ]);
227 
228-    const { store, fx } = configureStore({ reducers });
229-    fx.run(api.bootup);
230+    const store = await configureStore<QueryState>({
231+      initialState: createQueryState(),
232+    });
233+    store.run(api.bootup);
234 
235     const action = fetchUsers();
236     store.dispatch(action);
237@@ -368,7 +393,7 @@ it(
238     await delay();
239 
240     const state = store.getState();
241-    expect(state["@@saga-query/data"][action.payload.key]).toEqual(mockUser);
242+    expect(state["@@starfx/data"][action.payload.key]).toEqual(mockUser);
243   },
244 );
245 
246@@ -384,6 +409,7 @@ it.ignore(
247 
248     const api = createApi();
249     api.use(requestMonitor());
250+    api.use(storeMdw());
251     api.use(api.routes());
252     api.use(fetcher({ baseUrl }));
253 
254@@ -396,9 +422,10 @@ it.ignore(
255       fetchRetry((n) => (n > 2 ? -1 : 10)),
256     ]);
257 
258-    const { store, fx } = configureStore({ reducers });
259-    fx.run(api.bootup);
260-
261+    const store = await configureStore<QueryState>({
262+      initialState: createQueryState(),
263+    });
264+    store.run(api.bootup);
265     const action = fetchUsers();
266     store.dispatch(action);
267 
M query/middleware.test.ts
+135, -119
  1@@ -1,33 +1,30 @@
  2 import { assertLike, asserts, describe, expect, it } from "../test.ts";
  3-import { configureStore, put, takeLatest } from "../redux/mod.ts";
  4-import {
  5-  createReducerMap,
  6-  createTable,
  7-  defaultLoadingItem,
  8-  sleep as delay,
  9-} from "../deps.ts";
 10-import type { MapEntity } from "../deps.ts";
 11+import { sleep as delay } from "../deps.ts";
 12 import { call } from "../fx/mod.ts";
 13-
 14-import { createApi } from "./api.ts";
 15 import {
 16+  createApi,
 17+  createKey,
 18   customKey,
 19   queryCtx,
 20   requestMonitor,
 21-  undo,
 22-  undoer,
 23   urlParser,
 24-} from "./middleware.ts";
 25-import type { UndoCtx } from "./middleware.ts";
 26-import type { ApiCtx } from "./types.ts";
 27-import { sleep } from "./util.ts";
 28-import { createKey } from "./create-key.ts";
 29+} from "../query/mod.ts";
 30+import type { ApiCtx } from "../query/mod.ts";
 31+import { createQueryState } from "../action.ts";
 32+import type { QueryState } from "../types.ts";
 33+import { sleep } from "../test.ts";
 34+
 35+import type { UndoCtx } from "../store/mod.ts";
 36 import {
 37-  createQueryState,
 38-  DATA_NAME,
 39-  LOADERS_NAME,
 40+  configureStore,
 41+  defaultLoader,
 42   selectDataById,
 43-} from "./slice.ts";
 44+  storeMdw,
 45+  takeLatest,
 46+  undo,
 47+  undoer,
 48+  updateStore,
 49+} from "../store/mod.ts";
 50 
 51 interface User {
 52   id: string;
 53@@ -35,6 +32,10 @@ interface User {
 54   email: string;
 55 }
 56 
 57+interface UserState extends QueryState {
 58+  users: { [key: string]: User };
 59+}
 60+
 61 const mockUser: User = { id: "1", name: "test", email: "test@test.com" };
 62 const mockUser2: User = { id: "2", name: "two", email: "two@test.com" };
 63 
 64@@ -45,11 +46,8 @@ const jsonBlob = (data: any) => {
 65 
 66 const tests = describe("middleware");
 67 
 68-it(tests, "basic", () => {
 69-  const name = "users";
 70-  const cache = createTable<User>({ name });
 71+it(tests, "basic", async () => {
 72   const query = createApi<ApiCtx>();
 73-
 74   query.use(queryCtx);
 75   query.use(urlParser);
 76   query.use(query.routes());
 77@@ -72,11 +70,12 @@ it(tests, "basic", () => {
 78       yield* next();
 79       if (!ctx.json.ok) return;
 80       const { users } = ctx.json.data;
 81-      const curUsers = users.reduce<MapEntity<User>>((acc, u) => {
 82-        acc[u.id] = u;
 83-        return acc;
 84-      }, {});
 85-      yield* put(cache.actions.add(curUsers));
 86+
 87+      yield* updateStore<UserState>((state) => {
 88+        users.forEach((u) => {
 89+          state.users[u.id] = u;
 90+        });
 91+      });
 92     },
 93   );
 94 
 95@@ -90,14 +89,19 @@ it(tests, "basic", () => {
 96       yield* next();
 97       if (!ctx.json.ok) return;
 98       const curUser = ctx.json.data;
 99-      const curUsers = { [curUser.id]: curUser };
100-      yield* put(cache.actions.add(curUsers));
101+      yield* updateStore<UserState>((state) => {
102+        state.users[curUser.id] = curUser;
103+      });
104     },
105   );
106 
107-  const reducers = createReducerMap(cache);
108-  const { store, fx } = configureStore({ reducers });
109-  fx.run(query.bootup);
110+  const store = await configureStore({
111+    initialState: {
112+      ...createQueryState(),
113+      users: {},
114+    },
115+  });
116+  store.run(query.bootup);
117 
118   store.dispatch(fetchUsers());
119   expect(store.getState()).toEqual({
120@@ -111,11 +115,10 @@ it(tests, "basic", () => {
121   });
122 });
123 
124-it(tests, "with loader", () => {
125-  const users = createTable<User>({ name: "users" });
126-
127+it(tests, "with loader", async () => {
128   const api = createApi<ApiCtx>();
129   api.use(requestMonitor());
130+  api.use(storeMdw());
131   api.use(api.routes());
132   api.use(function* fetchApi(ctx, next) {
133     ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
134@@ -130,23 +133,24 @@ it(tests, "with loader", () => {
135       if (!ctx.json.ok) return;
136 
137       const { data } = ctx.json;
138-      const curUsers = data.users.reduce<MapEntity<User>>((acc, u) => {
139-        acc[u.id] = u;
140-        return acc;
141-      }, {});
142 
143-      ctx.actions.push(users.actions.add(curUsers));
144+      yield* updateStore<UserState>((state) => {
145+        data.users.forEach((u) => {
146+          state.users[u.id] = u;
147+        });
148+      });
149     },
150   );
151 
152-  const reducers = createReducerMap(users);
153-  const { store, fx } = configureStore({ reducers });
154-  fx.run(api.bootup);
155+  const store = await configureStore<UserState>({
156+    initialState: { ...createQueryState(), users: {} },
157+  });
158+  store.run(api.bootup);
159 
160   store.dispatch(fetchUsers());
161   assertLike(store.getState(), {
162-    [users.name]: { [mockUser.id]: mockUser },
163-    [LOADERS_NAME]: {
164+    users: { [mockUser.id]: mockUser },
165+    "@@starfx/loaders": {
166       "/users": {
167         status: "success",
168       },
169@@ -154,11 +158,10 @@ it(tests, "with loader", () => {
170   });
171 });
172 
173-it(tests, "with item loader", () => {
174-  const users = createTable<User>({ name: "users" });
175-
176+it(tests, "with item loader", async () => {
177   const api = createApi<ApiCtx>();
178   api.use(requestMonitor());
179+  api.use(storeMdw());
180   api.use(api.routes());
181   api.use(function* fetchApi(ctx, next) {
182     ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
183@@ -173,24 +176,24 @@ it(tests, "with item loader", () => {
184       if (!ctx.json.ok) return;
185 
186       const { data } = ctx.json;
187-      const curUsers = data.users.reduce<MapEntity<User>>((acc, u) => {
188-        acc[u.id] = u;
189-        return acc;
190-      }, {});
191-
192-      ctx.actions.push(users.actions.add(curUsers));
193+      yield* updateStore<UserState>((state) => {
194+        data.users.forEach((u) => {
195+          state.users[u.id] = u;
196+        });
197+      });
198     },
199   );
200 
201-  const reducers = createReducerMap(users);
202-  const { store, fx } = configureStore({ reducers });
203-  fx.run(api.bootup);
204+  const store = await configureStore<UserState>({
205+    initialState: { ...createQueryState(), users: {} },
206+  });
207+  store.run(api.bootup);
208 
209   const action = fetchUser({ id: mockUser.id });
210   store.dispatch(action);
211   assertLike(store.getState(), {
212-    [users.name]: { [mockUser.id]: mockUser },
213-    [LOADERS_NAME]: {
214+    users: { [mockUser.id]: mockUser },
215+    "@@starfx/loaders": {
216       "/users/:id": {
217         status: "success",
218       },
219@@ -201,11 +204,8 @@ it(tests, "with item loader", () => {
220   });
221 });
222 
223-it(tests, "with POST", () => {
224-  const name = "users";
225-  const cache = createTable<User>({ name });
226+it(tests, "with POST", async () => {
227   const query = createApi();
228-
229   query.use(queryCtx);
230   query.use(urlParser);
231   query.use(query.routes());
232@@ -241,24 +241,26 @@ it(tests, "with POST", () => {
233       if (!ctx.json.ok) return;
234 
235       const { users } = ctx.json.data;
236-      const curUsers = users.reduce<MapEntity<User>>((acc, u) => {
237-        acc[u.id] = u;
238-        return acc;
239-      }, {});
240-      yield* put(cache.actions.add(curUsers));
241+      yield* updateStore<UserState>((state) => {
242+        users.forEach((u) => {
243+          state.users[u.id] = u;
244+        });
245+      });
246     },
247   );
248 
249-  const reducers = createReducerMap(cache);
250-  const { store, fx } = configureStore({ reducers });
251-  fx.run(query.bootup);
252+  const store = await configureStore<UserState>({
253+    initialState: { ...createQueryState(), users: {} },
254+  });
255+  store.run(query.bootup);
256 
257   store.dispatch(createUser({ email: mockUser.email }));
258 });
259 
260-it(tests, "simpleCache", () => {
261+it(tests, "simpleCache", async () => {
262   const api = createApi<ApiCtx>();
263   api.use(requestMonitor());
264+  api.use(storeMdw());
265   api.use(api.routes());
266   api.use(function* fetchApi(ctx, next) {
267     const data = { users: [mockUser] };
268@@ -268,16 +270,18 @@ it(tests, "simpleCache", () => {
269   });
270 
271   const fetchUsers = api.get("/users", api.cache());
272-  const { store, fx } = configureStore({ reducers: { init: () => null } });
273-  fx.run(api.bootup);
274+  const store = await configureStore<UserState>({
275+    initialState: { ...createQueryState(), users: {} },
276+  });
277+  store.run(api.bootup);
278 
279   const action = fetchUsers();
280   store.dispatch(action);
281   assertLike(store.getState(), {
282-    [DATA_NAME]: {
283+    "@@starfx/data": {
284       [action.payload.key]: { users: [mockUser] },
285     },
286-    [LOADERS_NAME]: {
287+    "@@starfx/loaders": {
288       [`${fetchUsers}`]: {
289         status: "success",
290       },
291@@ -285,11 +289,10 @@ it(tests, "simpleCache", () => {
292   });
293 });
294 
295-it(tests, "overriding default loader behavior", () => {
296-  const users = createTable<User>({ name: "users" });
297-
298+it(tests, "overriding default loader behavior", async () => {
299   const api = createApi<ApiCtx>();
300   api.use(requestMonitor());
301+  api.use(storeMdw());
302   api.use(api.routes());
303   api.use(function* fetchApi(ctx, next) {
304     const data = { users: [mockUser] };
305@@ -308,24 +311,24 @@ it(tests, "overriding default loader behavior", () => {
306         return;
307       }
308       const { data } = ctx.json;
309-      const curUsers = data.users.reduce<MapEntity<User>>((acc, u) => {
310-        acc[u.id] = u;
311-        return acc;
312-      }, {});
313-
314       ctx.loader = { id, message: "yes", meta: { wow: true } };
315-      ctx.actions.push(users.actions.add(curUsers));
316+      yield* updateStore<UserState>((state) => {
317+        data.users.forEach((u) => {
318+          state.users[u.id] = u;
319+        });
320+      });
321     },
322   );
323 
324-  const reducers = createReducerMap(users);
325-  const { store, fx } = configureStore({ reducers });
326-  fx.run(api.bootup);
327+  const store = await configureStore<UserState>({
328+    initialState: { ...createQueryState(), users: {} },
329+  });
330+  store.run(api.bootup);
331 
332   store.dispatch(fetchUsers());
333   assertLike(store.getState(), {
334-    [users.name]: { [mockUser.id]: mockUser },
335-    [LOADERS_NAME]: {
336+    users: { [mockUser.id]: mockUser },
337+    "@@starfx/loaders": {
338       [`${fetchUsers}`]: {
339         status: "success",
340         message: "yes",
341@@ -335,9 +338,10 @@ it(tests, "overriding default loader behavior", () => {
342   });
343 });
344 
345-it(tests, "undo", () => {
346+it(tests, "undo", async () => {
347   const api = createApi<UndoCtx>();
348   api.use(requestMonitor());
349+  api.use(storeMdw());
350   api.use(api.routes());
351   api.use(undoer());
352 
353@@ -354,23 +358,25 @@ it(tests, "undo", () => {
354     yield* next();
355   });
356 
357-  const { store, fx } = configureStore({ reducers: { init: () => null } });
358-  fx.run(api.bootup);
359+  const store = await configureStore<UserState>({
360+    initialState: { ...createQueryState(), users: {} },
361+  });
362+  store.run(api.bootup);
363 
364   const action = createUser();
365   store.dispatch(action);
366   store.dispatch(undo());
367   assertLike(store.getState(), {
368     ...createQueryState({
369-      [LOADERS_NAME]: {
370-        [`${createUser}`]: defaultLoadingItem(),
371-        [action.payload.name]: defaultLoadingItem(),
372+      "@@starfx/loaders": {
373+        [`${createUser}`]: defaultLoader(),
374+        [action.payload.name]: defaultLoader(),
375       },
376     }),
377   });
378 });
379 
380-it(tests, "requestMonitor - error handler", () => {
381+it(tests, "requestMonitor - error handler", async () => {
382   let err = false;
383   console.error = (msg: string) => {
384     if (err) return;
385@@ -380,11 +386,10 @@ it(tests, "requestMonitor - error handler", () => {
386     );
387     err = true;
388   };
389-  const name = "users";
390-  const cache = createTable<User>({ name });
391   const query = createApi<ApiCtx>();
392 
393   query.use(requestMonitor());
394+  query.use(storeMdw());
395   query.use(query.routes());
396   query.use(function* () {
397     throw new Error("something happened");
398@@ -392,9 +397,10 @@ it(tests, "requestMonitor - error handler", () => {
399 
400   const fetchUsers = query.create(`/users`);
401 
402-  const reducers = createReducerMap(cache);
403-  const { store, fx } = configureStore({ reducers });
404-  fx.run(query.bootup);
405+  const store = await configureStore<UserState>({
406+    initialState: { ...createQueryState(), users: {} },
407+  });
408+  store.run(query.bootup);
409 
410   store.dispatch(fetchUsers());
411 });
412@@ -402,6 +408,7 @@ it(tests, "requestMonitor - error handler", () => {
413 it(tests, "createApi with own key", async () => {
414   const query = createApi();
415   query.use(requestMonitor());
416+  query.use(storeMdw());
417   query.use(query.routes());
418   query.use(customKey);
419   query.use(function* fetchApi(ctx, next) {
420@@ -431,10 +438,13 @@ it(tests, "createApi with own key", async () => {
421       const result = new TextDecoder("utf-8").decode(buff.value);
422       const { users } = JSON.parse(result);
423       if (!users) return;
424-      const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
425-        acc[u.id] = u;
426-        return acc;
427-      }, {});
428+      const curUsers = (users as User[]).reduce<Record<string, User>>(
429+        (acc, u) => {
430+          acc[u.id] = u;
431+          return acc;
432+        },
433+        {},
434+      );
435       ctx.response = new Response();
436       ctx.json = {
437         ok: true,
438@@ -443,9 +453,10 @@ it(tests, "createApi with own key", async () => {
439     },
440   );
441   const newUEmail = mockUser.email + ".org";
442-  const reducers = createReducerMap();
443-  const { store, fx } = configureStore({ reducers });
444-  fx.run(query.bootup);
445+  const store = await configureStore<UserState>({
446+    initialState: { ...createQueryState(), users: {} },
447+  });
448+  store.run(query.bootup);
449 
450   store.dispatch(createUserCustomKey({ email: newUEmail }));
451   await sleep(150);
452@@ -467,6 +478,7 @@ it(tests, "createApi with own key", async () => {
453 it(tests, "createApi with custom key but no payload", async () => {
454   const query = createApi();
455   query.use(requestMonitor());
456+  query.use(storeMdw());
457   query.use(query.routes());
458   query.use(customKey);
459   query.use(function* fetchApi(ctx, next) {
460@@ -496,10 +508,13 @@ it(tests, "createApi with custom key but no payload", async () => {
461       const result = new TextDecoder("utf-8").decode(buff.value);
462       const { users } = JSON.parse(result);
463       if (!users) return;
464-      const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
465-        acc[u.id] = u;
466-        return acc;
467-      }, {});
468+      const curUsers = (users as User[]).reduce<Record<string, User>>(
469+        (acc, u) => {
470+          acc[u.id] = u;
471+          return acc;
472+        },
473+        {},
474+      );
475       ctx.response = new Response();
476       ctx.json = {
477         ok: true,
478@@ -508,9 +523,10 @@ it(tests, "createApi with custom key but no payload", async () => {
479     },
480   );
481 
482-  const reducers = createReducerMap();
483-  const { store, fx } = configureStore({ reducers });
484-  fx.run(query.bootup);
485+  const store = await configureStore<UserState>({
486+    initialState: { ...createQueryState(), users: {} },
487+  });
488+  store.run(query.bootup);
489 
490   store.dispatch(getUsers());
491   await sleep(150);
M query/middleware.ts
+4, -192
  1@@ -1,6 +1,4 @@
  2-import { call, race } from "../fx/mod.ts";
  3-import { put, select, take } from "../redux/mod.ts";
  4-import { batchActions, sleep } from "../deps.ts";
  5+import { call } from "../fx/mod.ts";
  6 import { compose } from "../compose.ts";
  7 import type { OpFn } from "../types.ts";
  8 
  9@@ -8,20 +6,11 @@ import type {
 10   Action,
 11   ApiCtx,
 12   ApiRequest,
 13-  LoaderCtx,
 14   Next,
 15   PipeCtx,
 16   RequiredApiRequest,
 17 } from "./types.ts";
 18-import { createAction, isObject, mergeRequest } from "./util.ts";
 19-import {
 20-  addData,
 21-  resetLoaderById,
 22-  selectDataById,
 23-  setLoaderError,
 24-  setLoaderStart,
 25-  setLoaderSuccess,
 26-} from "./slice.ts";
 27+import { isObject, mergeRequest } from "./util.ts";
 28 
 29 /**
 30  * This middleware will catch any errors in the pipeline
 31@@ -103,176 +92,6 @@ export function* urlParser<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
 32   yield* next();
 33 }
 34 
 35-/**
 36- * This middleware will take the result of `ctx.actions` and dispatch them
 37- * as a single batch.
 38- *
 39- * @remarks This is useful because sometimes there are a lot of actions that need dispatched
 40- * within the pipeline of the middleware and instead of dispatching them serially this
 41- * improves performance by only hitting the reducers once.
 42- */
 43-export function* dispatchActions(ctx: { actions: Action[] }, next: Next) {
 44-  if (!ctx.actions) ctx.actions = [];
 45-  yield* next();
 46-  if (ctx.actions.length === 0) return;
 47-  yield* put(batchActions(ctx.actions));
 48-}
 49-
 50-/**
 51- * This middleware creates a loader for a generator function which allows us to track
 52- * the status of a pipeline function.
 53- */
 54-export function* loadingMonitorSimple<Ctx extends LoaderCtx = LoaderCtx>(
 55-  ctx: Ctx,
 56-  next: Next,
 57-) {
 58-  yield* put(
 59-    batchActions([
 60-      setLoaderStart({ id: ctx.name }),
 61-      setLoaderStart({ id: ctx.key }),
 62-    ]),
 63-  );
 64-  if (!ctx.loader) {
 65-    ctx.loader = {};
 66-  }
 67-
 68-  yield* next();
 69-
 70-  yield* put(
 71-    batchActions([
 72-      setLoaderSuccess({ ...ctx.loader, id: ctx.name }),
 73-      setLoaderSuccess({ ...ctx.loader, id: ctx.key }),
 74-    ]),
 75-  );
 76-}
 77-
 78-/**
 79- * This middleware will track the status of a fetch request.
 80- */
 81-export function loadingMonitor<Ctx extends ApiCtx = ApiCtx>(
 82-  errorFn: (ctx: Ctx) => string = (ctx) => ctx.json?.data?.message || "",
 83-) {
 84-  return function* trackLoading(ctx: Ctx, next: Next) {
 85-    yield* put(
 86-      batchActions([
 87-        setLoaderStart({ id: ctx.name }),
 88-        setLoaderStart({ id: ctx.key }),
 89-      ]),
 90-    );
 91-    if (!ctx.loader) ctx.loader = {} as any;
 92-
 93-    yield* next();
 94-
 95-    if (!ctx.response) {
 96-      ctx.actions.push(resetLoaderById(ctx.name), resetLoaderById(ctx.key));
 97-      return;
 98-    }
 99-
100-    const payload = ctx.loader || {};
101-    if (!ctx.response.ok) {
102-      ctx.actions.push(
103-        setLoaderError({ id: ctx.name, message: errorFn(ctx), ...payload }),
104-        setLoaderError({ id: ctx.key, message: errorFn(ctx), ...payload }),
105-      );
106-      return;
107-    }
108-
109-    ctx.actions.push(
110-      setLoaderSuccess({ id: ctx.name, ...payload }),
111-      setLoaderSuccess({ id: ctx.key, ...payload }),
112-    );
113-  };
114-}
115-
116-export interface UndoCtx<P = any, S = any, E = any> extends ApiCtx<P, S, E> {
117-  undoable: boolean;
118-}
119-
120-export const doIt = createAction("DO_IT");
121-export const undo = createAction("UNDO");
122-/**
123- * This middleware will allow pipeline functions to be undoable which means before they are activated
124- * we have a timeout that allows the function to be cancelled.
125- */
126-export function undoer<Ctx extends UndoCtx = UndoCtx>(
127-  doItType = `${doIt}`,
128-  undoType = `${undo}`,
129-  timeout = 30 * 1000,
130-) {
131-  return function* onUndo(ctx: Ctx, next: Next) {
132-    if (!ctx.undoable) {
133-      yield* next();
134-      return;
135-    }
136-
137-    const winner = yield* race({
138-      doIt: () => take(`${doItType}`),
139-      undo: () => take(`${undoType}`),
140-      timeout: () => sleep(timeout),
141-    });
142-
143-    if (winner.undo || winner.timeout) {
144-      return;
145-    }
146-
147-    yield* next();
148-  };
149-}
150-
151-export interface OptimisticCtx<
152-  A extends Action = Action,
153-  R extends Action = Action,
154-> extends ApiCtx {
155-  optimistic: {
156-    apply: A;
157-    revert: R;
158-  };
159-}
160-
161-/**
162- * This middleware performs an optimistic update for a middleware pipeline.
163- * It accepts an `apply` and `revert` action.
164- *
165- * @remarks This means that we will first `apply` and then if the request is successful we
166- * keep the change or we `revert` if there's an error.
167- */
168-export function* optimistic<
169-  Ctx extends OptimisticCtx = OptimisticCtx,
170->(ctx: Ctx, next: Next) {
171-  if (!ctx.optimistic) {
172-    yield* next();
173-    return;
174-  }
175-
176-  const { apply, revert } = ctx.optimistic;
177-  // optimistically update user
178-  yield* put(apply);
179-
180-  yield* next();
181-
182-  if (!ctx.response || !ctx.response.ok) {
183-    yield* put(revert);
184-  }
185-}
186-
187-/**
188- * This middleware will automatically cache any data found inside `ctx.json`
189- * which is where we store JSON data from the `fetcher` middleware.
190- */
191-export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
192-  ctx: Ctx,
193-  next: Next,
194-) {
195-  ctx.cacheData = yield* select((state) =>
196-    selectDataById(state, { id: ctx.key })
197-  );
198-  yield* next();
199-  if (!ctx.cache) return;
200-  const { data } = ctx.json;
201-  ctx.actions.push(addData({ [ctx.key]: data }));
202-  ctx.cacheData = data;
203-}
204-
205 /**
206  * This middleware allows the user to override the default key provided to every pipeline function
207  * and instead use whatever they want.
208@@ -305,16 +124,11 @@ export function* customKey<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
209 /**
210  * This middleware is a composition of many middleware used to faciliate the {@link createApi}
211  */
212-export function requestMonitor<Ctx extends ApiCtx = ApiCtx>(
213-  errorFn?: (ctx: Ctx) => string,
214-) {
215+export function requestMonitor<Ctx extends ApiCtx = ApiCtx>() {
216   return compose<Ctx>([
217     errorHandler,
218     queryCtx,
219     urlParser,
220-    dispatchActions,
221-    loadingMonitor(errorFn),
222-    simpleCache,
223     customKey,
224   ]);
225 }
226@@ -339,9 +153,7 @@ export function* performanceMonitor<Ctx extends PerfCtx = PerfCtx>(
227 /**
228  * This middleware will call the `saga` provided with the action sent to the middleware pipeline.
229  */
230-export function wrap<Ctx extends PipeCtx = PipeCtx>(
231-  op: (a: Action) => OpFn,
232-) {
233+export function wrap<Ctx extends PipeCtx = PipeCtx>(op: (a: Action) => OpFn) {
234   return function* (ctx: Ctx, next: Next) {
235     yield* call(() => op(ctx.action));
236     yield* next();
M query/mod.ts
+0, -5
 1@@ -1,11 +1,6 @@
 2-export { BATCH, batchActions } from "../deps.ts";
 3-
 4 export * from "./pipe.ts";
 5 export * from "./api.ts";
 6 export * from "./types.ts";
 7 export * from "./fetch.ts";
 8 export * from "./middleware.ts";
 9-export * from "./constant.ts";
10-export * from "./slice.ts";
11 export * from "./create-key.ts";
12-export * from "./supervisor.ts";
M query/pipe.test.ts
+64, -85
  1@@ -1,21 +1,20 @@
  2 import { assertLike, asserts, describe, it } from "../test.ts";
  3 import { call } from "../fx/mod.ts";
  4-import { configureStore, put } from "../redux/mod.ts";
  5-import { createReducerMap, createTable, sleep as delay } from "../deps.ts";
  6-import type { Action, MapEntity } from "../deps.ts";
  7+import { configureStore, put } from "../store/mod.ts";
  8+import { sleep as delay } from "../deps.ts";
  9+import type { QueryState } from "../types.ts";
 10+import { createQueryState } from "../action.ts";
 11 
 12+import { sleep } from "../test.ts";
 13 import { createPipe } from "./pipe.ts";
 14 import type { Next, PipeCtx } from "./types.ts";
 15-import { sleep } from "./util.ts";
 16-import { createQueryState } from "./slice.ts";
 17-import { OpFn } from "../types.ts";
 18+import { updateStore } from "../store/fx.ts";
 19 
 20 // deno-lint-ignore no-explicit-any
 21 interface RoboCtx<D = Record<string, unknown>, P = any> extends PipeCtx<P> {
 22   url: string;
 23   request: { method: string; body?: Record<string, unknown> };
 24   response: D;
 25-  actions: Action[];
 26 }
 27 
 28 interface User {
 29@@ -55,9 +54,10 @@ const deserializeTicket = (u: TicketResponse): Ticket => {
 30   };
 31 };
 32 
 33-const users = createTable<User>({ name: "USER" });
 34-const tickets = createTable<Ticket>({ name: "TICKET" });
 35-const reducers = createReducerMap(users, tickets);
 36+interface TestState extends QueryState {
 37+  users: { [key: string]: User };
 38+  tickets: { [key: string]: Ticket };
 39+}
 40 
 41 const mockUser = { id: "1", name: "test", email_address: "test@test.com" };
 42 const mockTicket = { id: "2", name: "test-ticket" };
 43@@ -88,21 +88,18 @@ function* onFetchApi(ctx: RoboCtx, next: Next) {
 44   yield* next();
 45 }
 46 
 47-function* setupActionState(ctx: RoboCtx, next: Next) {
 48-  ctx.actions = [];
 49-  yield* next();
 50-}
 51-
 52 function* processUsers(ctx: RoboCtx<{ users?: UserResponse[] }>, next: Next) {
 53   if (!ctx.response.users) {
 54     yield* next();
 55     return;
 56   }
 57-  const curUsers = ctx.response.users.reduce<MapEntity<User>>((acc, u) => {
 58-    acc[u.id] = deserializeUser(u);
 59-    return acc;
 60-  }, {});
 61-  ctx.actions.push(users.actions.add(curUsers));
 62+  yield* updateStore<TestState>((state) => {
 63+    if (!ctx.response.users) return;
 64+    ctx.response.users.forEach((u) => {
 65+      state.users[u.id] = deserializeUser(u);
 66+    });
 67+  });
 68+
 69   yield* next();
 70 }
 71 
 72@@ -114,71 +111,55 @@ function* processTickets(
 73     yield* next();
 74     return;
 75   }
 76-  const curTickets = ctx.response.tickets.reduce<MapEntity<Ticket>>(
 77-    (acc, u) => {
 78-      acc[u.id] = deserializeTicket(u);
 79-      return acc;
 80-    },
 81-    {},
 82-  );
 83-  ctx.actions.push(tickets.actions.add(curTickets));
 84-  yield* next();
 85-}
 86+  yield* updateStore<TestState>((state) => {
 87+    if (!ctx.response.tickets) return;
 88+    ctx.response.tickets.forEach((u) => {
 89+      state.tickets[u.id] = deserializeTicket(u);
 90+    });
 91+  });
 92 
 93-function* saveToRedux(ctx: RoboCtx, next: Next) {
 94-  for (let i = 0; i < ctx.actions.length; i += 1) {
 95-    const action = ctx.actions[i];
 96-    yield* put(action);
 97-  }
 98   yield* next();
 99 }
100 
101-function setupStore(op: OpFn) {
102-  const { store, fx } = configureStore({ reducers });
103-  return { store, run: () => fx.run(op) };
104-}
105-
106 const tests = describe("createPipe()");
107 
108 it(
109   tests,
110   "when create a query fetch pipeline - execute all middleware and save to redux",
111-  () => {
112+  async () => {
113     const api = createPipe<RoboCtx>();
114     api.use(api.routes());
115     api.use(convertNameToUrl);
116     api.use(onFetchApi);
117-    api.use(setupActionState);
118     api.use(processUsers);
119     api.use(processTickets);
120-    api.use(saveToRedux);
121     const fetchUsers = api.create(`/users`);
122 
123-    const { store, run } = setupStore(api.bootup);
124-    run();
125+    const store = await configureStore<TestState>({
126+      initialState: { ...createQueryState(), users: {}, tickets: {} },
127+    });
128+    store.run(api.bootup);
129 
130     store.dispatch(fetchUsers());
131 
132     asserts.assertEquals(store.getState(), {
133       ...createQueryState(),
134-      [users.name]: { [mockUser.id]: deserializeUser(mockUser) },
135-      [tickets.name]: {},
136+      users: { [mockUser.id]: deserializeUser(mockUser) },
137+      tickets: {},
138     });
139   },
140 );
141 
142-it(
143+it.only(
144   tests,
145   "when providing a generator the to api.create function - should call that generator before all other middleware",
146-  () => {
147+  async () => {
148     const api = createPipe<RoboCtx>();
149     api.use(api.routes());
150     api.use(convertNameToUrl);
151     api.use(onFetchApi);
152-    api.use(setupActionState);
153     api.use(processUsers);
154     api.use(processTickets);
155-    api.use(saveToRedux);
156     const fetchUsers = api.create(`/users`);
157     const fetchTickets = api.create(`/ticket-wrong-url`, function* (ctx, next) {
158       // before middleware has been triggered
159@@ -187,28 +168,24 @@ it(
160       // triggers all middleware
161       yield* next();
162 
163-      // after middleware has been triggered
164-      asserts.assertEquals(ctx.actions, [
165-        tickets.actions.add({
166-          [mockTicket.id]: deserializeTicket(mockTicket),
167-        }),
168-      ]);
169       yield* put(fetchUsers());
170     });
171 
172-    const { store, run } = setupStore(api.bootup);
173-    run();
174+    const store = await configureStore<TestState>({
175+      initialState: { ...createQueryState(), users: {}, tickets: {} },
176+    });
177+    store.run(api.bootup);
178 
179     store.dispatch(fetchTickets());
180     asserts.assertEquals(store.getState(), {
181       ...createQueryState(),
182-      [users.name]: { [mockUser.id]: deserializeUser(mockUser) },
183-      [tickets.name]: { [mockTicket.id]: deserializeTicket(mockTicket) },
184+      users: { [mockUser.id]: deserializeUser(mockUser) },
185+      tickets: { [mockTicket.id]: deserializeTicket(mockTicket) },
186     });
187   },
188 );
189 
190-it(tests, "error handling", () => {
191+it(tests, "error handling", async () => {
192   const api = createPipe<RoboCtx>();
193   api.use(api.routes());
194   api.use(function* upstream(_, next) {
195@@ -223,12 +200,13 @@ it(tests, "error handling", () => {
196   });
197 
198   const action = api.create(`/error`);
199-  const { store, run } = setupStore(api.bootup);
200-  run();
201+
202+  const store = await configureStore({ initialState: {} });
203+  store.run(api.bootup);
204   store.dispatch(action());
205 });
206 
207-it(tests, "error handling inside create", () => {
208+it(tests, "error handling inside create", async () => {
209   const api = createPipe<RoboCtx>();
210   api.use(api.routes());
211   api.use(function* fail() {
212@@ -242,12 +220,12 @@ it(tests, "error handling inside create", () => {
213       asserts.assert(true);
214     }
215   });
216-  const { store, run } = setupStore(api.bootup);
217-  run();
218+  const store = await configureStore({ initialState: {} });
219+  store.run(api.bootup);
220   store.dispatch(action());
221 });
222 
223-it(tests, "error handling - error handler", () => {
224+it(tests, "error handling - error handler", async () => {
225   const api = createPipe<RoboCtx>();
226   api.use(api.routes());
227   api.use(function* upstream() {
228@@ -255,12 +233,13 @@ it(tests, "error handling - error handler", () => {
229   });
230 
231   const action = api.create(`/error`);
232-  const { store, run } = setupStore(api.bootup);
233-  run();
234+  const store = await configureStore({ initialState: {} });
235+  store.run(api.bootup);
236+
237   store.dispatch(action());
238 });
239 
240-it(tests, "create fn is an array", () => {
241+it(tests, "create fn is an array", async () => {
242   const api = createPipe<RoboCtx>();
243   api.use(api.routes());
244   api.use(function* (ctx, next) {
245@@ -285,12 +264,12 @@ it(tests, "create fn is an array", () => {
246     },
247   ]);
248 
249-  const { store, run } = setupStore(api.bootup);
250-  run();
251+  const store = await configureStore({ initialState: {} });
252+  store.run(api.bootup);
253   store.dispatch(action());
254 });
255 
256-it(tests, "run() on endpoint action - should run the effect", () => {
257+it(tests, "run() on endpoint action - should run the effect", async () => {
258   const api = createPipe<RoboCtx>();
259   api.use(api.routes());
260   let acc = "";
261@@ -316,8 +295,8 @@ it(tests, "run() on endpoint action - should run the effect", () => {
262     });
263   });
264 
265-  const { store, run } = setupStore(api.bootup);
266-  run();
267+  const store = await configureStore({ initialState: {} });
268+  store.run(api.bootup);
269   store.dispatch(action2());
270 });
271 
272@@ -348,8 +327,8 @@ it(tests, "middleware order of execution", async () => {
273     acc += "g";
274   });
275 
276-  const { store, run } = setupStore(api.bootup);
277-  run();
278+  const store = await configureStore({ initialState: {} });
279+  store.run(api.bootup);
280   store.dispatch(action());
281 
282   await sleep(150);
283@@ -374,8 +353,8 @@ it.ignore(tests, "retry with actionFn", async () => {
284     }
285   });
286 
287-  const { store, run } = setupStore(api.bootup);
288-  run();
289+  const store = await configureStore({ initialState: {} });
290+  store.run(api.bootup);
291   store.dispatch(action());
292 
293   await sleep(150);
294@@ -400,8 +379,8 @@ it.ignore(tests, "retry with actionFn with payload", async () => {
295     acc += "g";
296   });
297 
298-  const { store, run } = setupStore(api.bootup);
299-  const task = run();
300+  const store = await configureStore({ initialState: {} });
301+  const task = store.run(api.bootup);
302   store.dispatch(action({ page: 1 }));
303 
304   await sleep(150);
305@@ -409,7 +388,7 @@ it.ignore(tests, "retry with actionFn with payload", async () => {
306   await task;
307 });
308 
309-it.only(tests, "should only call thunk once", () => {
310+it(tests, "should only call thunk once", async () => {
311   const api = createPipe<RoboCtx>();
312   api.use(api.routes());
313   let acc = "";
314@@ -423,8 +402,8 @@ it.only(tests, "should only call thunk once", () => {
315     yield* put(action1(1));
316   });
317 
318-  const { store, run } = setupStore(api.bootup);
319-  run();
320+  const store = await configureStore({ initialState: {} });
321+  store.run(api.bootup);
322   store.dispatch(action2());
323   asserts.assertEquals(acc, "a");
324 });
M query/pipe.ts
+7, -6
 1@@ -1,9 +1,11 @@
 2 import { call } from "../fx/mod.ts";
 3-import { takeEvery } from "../redux/mod.ts";
 4 import { compose } from "../compose.ts";
 5-import type { OpFn } from "../types.ts";
 6+import type { OpFn, Payload } from "../types.ts";
 7 import { parallel } from "../mod.ts";
 8 
 9+// TODO: remove store deps
10+import { takeEvery } from "../store/mod.ts";
11+
12 import { isFn, isObject } from "./util.ts";
13 import { createKey } from "./create-key.ts";
14 import type {
15@@ -14,11 +16,10 @@ import type {
16   Middleware,
17   MiddlewareCo,
18   Next,
19-  Payload,
20   PipeCtx,
21   Supervisor,
22 } from "./types.ts";
23-import { API_ACTION_PREFIX } from "./constant.ts";
24+import { API_ACTION_PREFIX } from "../action.ts";
25 
26 export interface SagaApi<Ctx extends PipeCtx> {
27   use: (fn: Middleware<Ctx>) => void;
28@@ -213,8 +214,8 @@ export function createPipe<Ctx extends PipeCtx = PipeCtx<any>>(
29   }
30 
31   function* bootup() {
32-    const results = yield* parallel(Object.values(visors));
33-    yield* results;
34+    const group = yield* parallel(Object.values(visors));
35+    return yield* group;
36   }
37 
38   function routes() {
M query/react.test.ts
+20, -19
 1@@ -7,13 +7,8 @@
 2 
 3 import { React } from "../deps.ts";
 4 import { asserts, beforeEach, describe, it } from "../test.ts";
 5-import {
 6-  createAssign,
 7-  Provider,
 8-  sleep as delay,
 9-  useSelector,
10-} from "../deps.ts";
11-import { configureStore } from "../redux/mod.ts";
12+import { Provider, sleep as delay, useSelector } from "../deps.ts";
13+import { configureStore, updateStore } from "../store/mod.ts";
14 
15 import { createApi } from "./api.ts";
16 import { requestMonitor } from "./middleware.ts";
17@@ -29,12 +24,13 @@ const jsonBlob = (data: any) => {
18   return JSON.stringify(data);
19 };
20 
21-const setupTest = () => {
22-  const slice = createAssign({
23-    name: "user",
24-    initialState: { id: "", email: "" },
25-  });
26+interface User {
27+  id: string;
28+  name: string;
29+  email: string;
30+}
31 
32+const setupTest = async () => {
33   const api = createApi();
34   api.use(requestMonitor());
35   api.use(api.routes());
36@@ -49,11 +45,15 @@ const setupTest = () => {
37     ctx.cache = true;
38     yield next();
39     if (!ctx.json.ok) return;
40-    slice.actions.set(ctx.json.data);
41+    yield* updateStore<{ user: User }>((state) => {
42+      state.user = ctx.json.data;
43+    });
44   });
45 
46-  const { store, fx } = configureStore({ reducers: { user: slice.reducer } });
47-  fx.run(api.bootup);
48+  const store = await configureStore<{ user?: User }>({
49+    initialState: {},
50+  });
51+  store.run(api.bootup);
52 
53   return { store, fetchUser, api };
54 };
55@@ -61,13 +61,13 @@ const setupTest = () => {
56 describe.ignore("useApi()", () => {
57   beforeEach(() => cleanup());
58   it("with action", async () => {
59-    const { fetchUser, store } = setupTest();
60+    const { fetchUser, store } = await setupTest();
61     const App = () => {
62       const action = fetchUser({ id: "1" });
63       const query = useApi(action);
64       const user = useSelector((s: any) =>
65         selectDataById(s, { id: action.payload.key })
66-      );
67+      ) as User;
68 
69       return h("div", null, [
70         h("div", { key: "1" }, user?.email || ""),
71@@ -91,13 +91,14 @@ describe.ignore("useApi()", () => {
72   });
73 
74   it("with action creator", async () => {
75-    const { fetchUser, store } = setupTest();
76+    const { fetchUser, store } = await setupTest();
77     const App = () => {
78       const query = useApi(fetchUser);
79       const user = useSelector((s: any) => {
80         const id = createKey(`${fetchUser}`, { id: "1" });
81         return selectDataById(s, { id });
82-      });
83+      }) as User;
84+
85       return h("div", null, [
86         h("div", { key: "1" }, user?.email || "no user"),
87         h(
M query/react.ts
+5, -5
 1@@ -1,9 +1,9 @@
 2-import type { LoadingState } from "../deps.ts";
 3+import type { LoadingState, QueryState } from "../types.ts";
 4 import { React, useDispatch, useSelector } from "../deps.ts";
 5 const { useState, useEffect } = React;
 6 
 7-import type { QueryState } from "./slice.ts";
 8-import { selectDataById, selectLoaderById } from "./slice.ts";
 9+// TODO: remove store deps
10+import { selectDataById, selectLoaderById } from "../store/mod.ts";
11 
12 type ActionFn<P = any> = (p: P) => { toString: () => string };
13 type ActionFnSimple = () => { toString: () => string };
14@@ -102,7 +102,7 @@ export function useApi<P = any, A extends SagaAction = SagaAction<P>>(
15 export function useApi<A extends SagaAction = SagaAction>(
16   action: ActionFnSimple,
17 ): UseApiSimpleProps;
18-export function useApi(action: any) {
19+export function useApi(action: any): any {
20   const dispatch = useDispatch();
21   const loader = useLoader(action);
22   const trigger = (p: any) => {
23@@ -166,7 +166,7 @@ export function useCache<D = any, A extends SagaAction = SagaAction>(
24   action: A,
25 ): UseCacheResult<D, A> {
26   const id = action.payload.key;
27-  const data = useSelector((s: any) => selectDataById(s, { id }));
28+  const data: any = useSelector((s: any) => selectDataById(s, { id }));
29   const query = useQuery(action);
30   return { ...query, data: data || null };
31 }
D query/slice.ts
+0, -50
 1@@ -1,50 +0,0 @@
 2-import { createLoaderTable, createReducerMap, createTable } from "../deps.ts";
 3-import type { LoadingItemState } from "../deps.ts";
 4-export { defaultLoader, defaultLoadingItem } from "../deps.ts";
 5-
 6-import { createKey } from "./create-key.ts";
 7-
 8-export interface QueryState {
 9-  "@@saga-query/loaders": { [key: string]: LoadingItemState };
10-  "@@saga-query/data": { [key: string]: any };
11-}
12-
13-export const LOADERS_NAME = `@@saga-query/loaders`;
14-export const loaders = createLoaderTable({ name: LOADERS_NAME });
15-export const {
16-  loading: setLoaderStart,
17-  error: setLoaderError,
18-  success: setLoaderSuccess,
19-  resetById: resetLoaderById,
20-} = loaders.actions;
21-export const { selectTable: selectLoaders, selectById: selectLoaderById } =
22-  loaders.getSelectors((state: any) => state[LOADERS_NAME] || {});
23-
24-export const DATA_NAME = `@@saga-query/data`;
25-export const data = createTable<any>({ name: DATA_NAME });
26-export const { add: addData, reset: resetData } = data.actions;
27-
28-export const { selectTable: selectData, selectById: selectDataById } = data
29-  .getSelectors((s: any) => s[DATA_NAME] || {});
30-
31-/**
32- * Returns data from the saga-query slice of redux from an action.
33- */
34-export const selectDataByName = (
35-  s: any,
36-  p: { name: string; payload?: any },
37-) => {
38-  const id = createKey(p.name, p.payload);
39-  const data = selectDataById(s, { id });
40-  return data;
41-};
42-
43-export const reducers = createReducerMap(loaders, data);
44-
45-export const createQueryState = (s: Partial<QueryState> = {}): QueryState => {
46-  return {
47-    [LOADERS_NAME]: {},
48-    [DATA_NAME]: {},
49-    ...s,
50-  };
51-};
M query/types.ts
+3, -13
 1@@ -1,18 +1,8 @@
 2-import type {
 3-  LoadingItemState,
 4-  LoadingMapPayload,
 5-  LoadingState,
 6-  Operation,
 7-} from "../deps.ts";
 8-
 9-export type { LoadingItemState, LoadingState };
10+import type { Operation } from "../deps.ts";
11+import type { LoadingItemState, LoadingPayload, Payload } from "../types.ts";
12 
13 type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
14 
15-export interface Payload<P = any> {
16-  payload: P;
17-}
18-
19 export interface PipeCtx<P = any> extends Payload<P> {
20   name: string;
21   key: string;
22@@ -65,7 +55,7 @@ export interface FetchJsonCtx<P = any, ApiSuccess = any, ApiError = any>
23 export interface ApiCtx<Payload = any, ApiSuccess = any, ApiError = any>
24   extends FetchJsonCtx<Payload, ApiSuccess, ApiError> {
25   actions: Action[];
26-  loader: LoadingMapPayload<Record<string, any>> | null;
27+  loader: LoadingPayload | null;
28   cache: boolean;
29   cacheData: any;
30 }
M query/util.test.ts
+1, -3
1@@ -1,7 +1,5 @@
2 import { describe, expect, it } from "../test.ts";
3-
4-import { createAction } from "./util.ts";
5-import { API_ACTION_PREFIX } from "./constant.ts";
6+import { API_ACTION_PREFIX, createAction } from "../action.ts";
7 
8 const tests = describe("createAction()");
9 
M query/util.ts
+0, -15
 1@@ -1,18 +1,10 @@
 2 import type { ApiRequest, RequiredApiRequest } from "./types.ts";
 3-import { API_ACTION_PREFIX } from "./constant.ts";
 4 
 5 export const noop = () => {};
 6 // deno-lint-ignore no-explicit-any
 7 export const isFn = (fn?: any) => fn && typeof fn === "function";
 8 // deno-lint-ignore no-explicit-any
 9 export const isObject = (obj?: any) => typeof obj === "object" && obj !== null;
10-export const createAction = (curType: string) => {
11-  if (!curType) throw new Error("createAction requires non-empty string");
12-  const type = `${API_ACTION_PREFIX}/${curType}`;
13-  const action = () => ({ type });
14-  action.toString = () => type;
15-  return action;
16-};
17 
18 export const mergeHeaders = (
19   cur?: HeadersInit,
20@@ -39,10 +31,3 @@ export const mergeRequest = (
21     headers: mergeHeaders(cur?.headers, next?.headers),
22   };
23 };
24-
25-export const sleep = (n: number) =>
26-  new Promise<void>((resolve) => {
27-    setTimeout(() => {
28-      resolve();
29-    }, n);
30-  });
M react.ts
+1, -44
 1@@ -1,45 +1,2 @@
 2-import { React } from "./deps.ts";
 3-const { createContext, createElement: h, useContext } = React;
 4-import type { Action, Operation, Scope } from "./deps.ts";
 5-import { ActionContext } from "./redux/mod.ts";
 6-
 7 export * from "./query/react.ts";
 8-
 9-const ScopeContext = createContext<Scope | null>(null);
10-
11-export function Provider({
12-  scope,
13-  children,
14-}: {
15-  scope: Scope;
16-  children: React.ReactNode;
17-}) {
18-  return h(ScopeContext.Provider, { value: scope }, children);
19-}
20-
21-export function useScope(): Scope {
22-  const scope = useContext(ScopeContext);
23-  if (!scope) {
24-    throw new Error("scope is null");
25-  }
26-  return scope;
27-}
28-
29-/**
30- * This hook dispatches actions directly to the Action channel we use
31- * for redux.  This makes it so you don't have to dispatch a redux action
32- * in order to trigger an fx.
33- */
34-export function useDispatchFx() {
35-  const scope = useScope();
36-  return (action: Action) =>
37-    scope.run(function* () {
38-      const { input } = yield* ActionContext;
39-      yield* input.send(action);
40-    });
41-}
42-
43-export function useFx<T>(op: () => Operation<T>) {
44-  const scope = useScope();
45-  return scope.run(op);
46-}
47+export * from "./store/react.ts";
A redux/fx.ts
+124, -0
  1@@ -0,0 +1,124 @@
  2+import { Action, AnyAction, Channel, Operation } from "../deps.ts";
  3+import { createChannel, createContext, spawn } from "../deps.ts";
  4+import { call, parallel } from "../fx/mod.ts";
  5+import { ActionPattern, matcher } from "../matcher.ts";
  6+
  7+export interface ActionWPayload<P> {
  8+  type: string;
  9+  payload: P;
 10+}
 11+
 12+export interface StoreLike<S = unknown> {
 13+  getState: () => S;
 14+  dispatch: (action: Action) => void;
 15+}
 16+
 17+export const ActionContext = createContext<Channel<Action, void>>(
 18+  "redux:action",
 19+  createChannel<Action, void>(),
 20+);
 21+
 22+export const StoreContext = createContext<StoreLike>("redux:store");
 23+
 24+export function* emit({
 25+  channel,
 26+  action,
 27+}: {
 28+  channel: Operation<Channel<AnyAction, void>>;
 29+  action: AnyAction | AnyAction[];
 30+}) {
 31+  const { input } = yield* channel;
 32+  if (Array.isArray(action)) {
 33+    if (action.length === 0) {
 34+      return;
 35+    }
 36+    yield* parallel(action.map((a) => () => input.send(a)));
 37+  } else {
 38+    yield* input.send(action);
 39+  }
 40+}
 41+
 42+export function* once({
 43+  channel,
 44+  pattern,
 45+}: {
 46+  channel: Operation<Channel<Action, void>>;
 47+  pattern: ActionPattern;
 48+}) {
 49+  const { output } = yield* channel;
 50+  const msgList = yield* output;
 51+  let next = yield* msgList.next();
 52+  while (!next.done) {
 53+    const match = matcher(pattern);
 54+    if (match(next.value)) {
 55+      return next.value;
 56+    }
 57+    next = yield* msgList.next();
 58+  }
 59+}
 60+
 61+export function* select<S, R>(selectorFn: (s: S) => R) {
 62+  const store = yield* StoreContext;
 63+  return selectorFn(store.getState() as S);
 64+}
 65+
 66+export function take<P>(pattern: ActionPattern): Operation<ActionWPayload<P>>;
 67+export function* take(pattern: ActionPattern): Operation<Action> {
 68+  const action = yield* once({
 69+    channel: ActionContext,
 70+    pattern,
 71+  });
 72+  return action as Action;
 73+}
 74+
 75+export function* takeEvery<T>(
 76+  pattern: ActionPattern,
 77+  op: (action: Action) => Operation<T>,
 78+) {
 79+  return yield* spawn(function* () {
 80+    while (true) {
 81+      const action = yield* take(pattern);
 82+      if (!action) continue;
 83+      yield* spawn(() => op(action));
 84+    }
 85+  });
 86+}
 87+
 88+export function* takeLatest<T>(
 89+  pattern: ActionPattern,
 90+  op: (action: Action) => Operation<T>,
 91+) {
 92+  return yield* spawn(function* () {
 93+    let lastTask;
 94+    while (true) {
 95+      const action = yield* take(pattern);
 96+      if (lastTask) {
 97+        yield* lastTask.halt();
 98+      }
 99+      if (!action) continue;
100+      lastTask = yield* spawn(() => op(action));
101+    }
102+  });
103+}
104+
105+export function* takeLeading<T>(
106+  pattern: ActionPattern,
107+  op: (action: Action) => Operation<T>,
108+) {
109+  return yield* spawn(function* () {
110+    while (true) {
111+      const action = yield* take(pattern);
112+      if (!action) continue;
113+      yield* call(() => op(action));
114+    }
115+  });
116+}
117+
118+export function* put(action: AnyAction | AnyAction[]) {
119+  const store = yield* StoreContext;
120+  if (Array.isArray(action)) {
121+    action.map((act) => store.dispatch(act));
122+  } else {
123+    store.dispatch(action);
124+  }
125+}
D redux/matcher.ts
+0, -43
 1@@ -1,43 +0,0 @@
 2-import type { Action } from "../deps.ts";
 3-
 4-type ActionType = string;
 5-type GuardPredicate<G extends T, T = unknown> = (arg: T) => arg is G;
 6-type Predicate = (action: Action) => boolean;
 7-type StringableActionCreator<A extends Action = Action> = {
 8-  (...args: unknown[]): A;
 9-  toString(): string;
10-};
11-type SubPattern = Predicate | StringableActionCreator | ActionType;
12-export type Pattern = SubPattern | SubPattern[];
13-type ActionSubPattern<Guard extends Action = Action> =
14-  | GuardPredicate<Guard, Action>
15-  | StringableActionCreator<Guard>
16-  | Predicate
17-  | ActionType;
18-export type ActionPattern<Guard extends Action = Action> =
19-  | ActionSubPattern<Guard>
20-  | ActionSubPattern<Guard>[];
21-
22-export function matcher(pattern: ActionPattern): (input: Action) => boolean {
23-  if (pattern === "*") {
24-    return (input: Action) => !!input;
25-  }
26-
27-  if (typeof pattern === "string") {
28-    return (input: Action) => pattern === input.type;
29-  }
30-
31-  if (Array.isArray(pattern)) {
32-    return (input: Action) => pattern.some((p) => matcher(p)(input));
33-  }
34-
35-  if (typeof pattern === "function" && Object.hasOwn(pattern, "toString")) {
36-    return (input: Action) => pattern.toString() === input.type;
37-  }
38-
39-  if (typeof pattern === "function") {
40-    return (input: Action) => pattern(input) as boolean;
41-  }
42-
43-  throw new Error("invalid pattern");
44-}
A redux/middleware.ts
+117, -0
  1@@ -0,0 +1,117 @@
  2+import {
  3+  Action,
  4+  AnyAction,
  5+  BATCH,
  6+  ConfigureEnhancersCallback,
  7+  Middleware,
  8+  ReducersMapObject,
  9+  Scope,
 10+  StoreEnhancer,
 11+} from "../deps.ts";
 12+import {
 13+  combineReducers,
 14+  configureStore as reduxStore,
 15+  createScope,
 16+  enableBatching,
 17+} from "../deps.ts";
 18+import type { OpFn } from "../types.ts";
 19+import { call, parallel } from "../fx/mod.ts";
 20+
 21+import { ActionContext, emit, StoreContext, StoreLike } from "./fx.ts";
 22+import { reducers as queryReducers } from "./query.ts";
 23+
 24+function* send(action: AnyAction) {
 25+  if (action.type === BATCH) {
 26+    const actions: Action[] = action.payload;
 27+    const group = yield* parallel(
 28+      actions.map(
 29+        (a) =>
 30+          function* () {
 31+            yield* emit({
 32+              channel: ActionContext,
 33+              action: a,
 34+            });
 35+          },
 36+      ),
 37+    );
 38+    yield* group;
 39+  } else {
 40+    yield* emit({
 41+      channel: ActionContext,
 42+      action,
 43+    });
 44+  }
 45+}
 46+
 47+export function createFxMiddleware(scope: Scope = createScope()) {
 48+  function run<T>(op: OpFn<T>) {
 49+    const task = scope.run(function* runner() {
 50+      return yield* call(op);
 51+    });
 52+
 53+    return task;
 54+  }
 55+
 56+  function middleware<S = unknown, T = unknown>(store: StoreLike<S>) {
 57+    scope.run(function* () {
 58+      yield* StoreContext.set(store);
 59+    });
 60+
 61+    return (next: (a: Action) => T) => (action: Action) => {
 62+      const result = next(action); // hit reducers
 63+      scope.run(function* () {
 64+        yield* send(action);
 65+      });
 66+      return result;
 67+    };
 68+  }
 69+
 70+  return { run, scope, middleware };
 71+}
 72+
 73+// deno-lint-ignore no-explicit-any
 74+interface SetupStoreProps<S = any> {
 75+  reducers: ReducersMapObject<S>;
 76+  middleware?: Middleware[];
 77+  enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback;
 78+  initialState?: S;
 79+}
 80+
 81+/**
 82+ * This function will integrate `starfx` and `redux`.
 83+ *
 84+ * In order to enable `starfx/query`, it will add some reducers to your `redux`
 85+ * store for decoupled loaders and a simple data cache.
 86+ *
 87+ * It also adds `redux-batched-actions` which is critical for `starfx`.
 88+ *
 89+ * @example
 90+ * ```ts
 91+ * import { configureStore } from 'starfx/redux';
 92+ *
 93+ * const { store, fx } = prepareStore({
 94+ *  reducers: { users: (state, action) => state },
 95+ * });
 96+ *
 97+ * fx.run(function*() {
 98+ *  yield* put({ type: 'LOADING' });
 99+ *  yield* fetch('https://bower.sh');
100+ *  yield* put({ type: 'LOADING_COMPLETE' });
101+ * });
102+ * ```
103+ */
104+export function configureStore(
105+  { reducers, middleware = [], enhancers = [], initialState }: SetupStoreProps,
106+) {
107+  const fx = createFxMiddleware();
108+  const rootReducer = combineReducers({ ...queryReducers, ...reducers });
109+  const store = reduxStore({
110+    reducer: enableBatching(rootReducer),
111+    middleware: (getDefaultMiddleware) =>
112+      getDefaultMiddleware().concat([fx.middleware, ...middleware]),
113+    enhancers,
114+    preloadedState: initialState,
115+  });
116+
117+  return { store, fx };
118+}
M redux/mod.ts
+5, -222
  1@@ -1,222 +1,5 @@
  2-import {
  3-  Action,
  4-  AnyAction,
  5-  BATCH,
  6-  Channel,
  7-  ConfigureEnhancersCallback,
  8-  Middleware,
  9-  Operation,
 10-  ReducersMapObject,
 11-  Scope,
 12-  StoreEnhancer,
 13-} from "../deps.ts";
 14-import type { OpFn } from "../types.ts";
 15-import {
 16-  combineReducers,
 17-  configureStore as reduxStore,
 18-  createChannel,
 19-  createContext,
 20-  createScope,
 21-  enableBatching,
 22-  spawn,
 23-} from "../deps.ts";
 24-import { contextualize } from "../context.ts";
 25-import { call, cancel, emit, parallel } from "../fx/mod.ts";
 26-import { reducers as queryReducers } from "../query/mod.ts";
 27-
 28-import { ActionPattern, matcher } from "./matcher.ts";
 29-
 30-export interface ActionWPayload<P> {
 31-  type: string;
 32-  payload: P;
 33-}
 34-
 35-export interface StoreLike<S = unknown> {
 36-  getState: () => S;
 37-  dispatch: (action: Action) => void;
 38-}
 39-
 40-export const ActionContext = createContext<Channel<Action, void>>(
 41-  "redux:action",
 42-  createChannel<Action, void>(),
 43-);
 44-
 45-export const StoreContext = createContext<StoreLike>("redux:store");
 46-
 47-export function* once({
 48-  channel,
 49-  pattern,
 50-}: {
 51-  channel: Operation<Channel<Action, void>>;
 52-  pattern: ActionPattern;
 53-}) {
 54-  const { output } = yield* channel;
 55-  const msgList = yield* output;
 56-  let next = yield* msgList.next();
 57-  while (!next.done) {
 58-    const match = matcher(pattern);
 59-    if (match(next.value)) {
 60-      return next.value;
 61-    }
 62-    next = yield* msgList.next();
 63-  }
 64-}
 65-
 66-export function* select<S, R>(selectorFn: (s: S) => R) {
 67-  const store = yield* StoreContext;
 68-  return selectorFn(store.getState() as S);
 69-}
 70-
 71-export function take<P>(pattern: ActionPattern): Operation<ActionWPayload<P>>;
 72-export function* take(pattern: ActionPattern): Operation<Action> {
 73-  const action = yield* once({
 74-    channel: ActionContext,
 75-    pattern,
 76-  });
 77-  return action as Action;
 78-}
 79-
 80-export function* takeEvery<T>(
 81-  pattern: ActionPattern,
 82-  op: (action: Action) => Operation<T>,
 83-) {
 84-  return yield* spawn(function* () {
 85-    while (true) {
 86-      const action = yield* take(pattern);
 87-      if (!action) continue;
 88-      yield* spawn(() => op(action));
 89-    }
 90-  });
 91-}
 92-
 93-export function* takeLatest<T>(
 94-  pattern: ActionPattern,
 95-  op: (action: Action) => Operation<T>,
 96-) {
 97-  return yield* spawn(function* () {
 98-    let lastTask;
 99-    while (true) {
100-      const action = yield* take(pattern);
101-      if (lastTask) {
102-        yield* cancel(lastTask);
103-      }
104-      if (!action) continue;
105-      lastTask = yield* spawn(() => op(action));
106-    }
107-  });
108-}
109-
110-export function* takeLeading<T>(
111-  pattern: ActionPattern,
112-  op: (action: Action) => Operation<T>,
113-) {
114-  return yield* spawn(function* () {
115-    while (true) {
116-      const action = yield* take(pattern);
117-      if (!action) continue;
118-      yield* call(() => op(action));
119-    }
120-  });
121-}
122-
123-export function* put(action: AnyAction | AnyAction[]) {
124-  const store = yield* StoreContext;
125-  if (Array.isArray(action)) {
126-    action.map((act) => store.dispatch(act));
127-  } else {
128-    store.dispatch(action);
129-  }
130-}
131-
132-function* send(action: AnyAction) {
133-  if (action.type === BATCH) {
134-    const actions: Action[] = action.payload;
135-    yield* parallel(
136-      actions.map(
137-        (a) =>
138-          function* () {
139-            yield* emit({
140-              channel: ActionContext,
141-              action: a,
142-            });
143-          },
144-      ),
145-    );
146-  } else {
147-    yield* emit({
148-      channel: ActionContext,
149-      action,
150-    });
151-  }
152-}
153-
154-export function createFxMiddleware(scope: Scope = createScope()) {
155-  function run<T>(op: OpFn<T>) {
156-    const task = scope.run(function* runner() {
157-      return yield* call(op);
158-    });
159-
160-    return task;
161-  }
162-
163-  function middleware<S = unknown, T = unknown>(store: StoreLike<S>) {
164-    scope.run(function* () {
165-      yield* contextualize("redux:store", store);
166-    });
167-
168-    return (next: (a: Action) => T) => (action: Action) => {
169-      const result = next(action); // hit reducers
170-      scope.run(function* () {
171-        yield* send(action);
172-      });
173-      return result;
174-    };
175-  }
176-
177-  return { run, scope, middleware };
178-}
179-
180-// deno-lint-ignore no-explicit-any
181-interface SetupStoreProps<S = any> {
182-  reducers: ReducersMapObject<S>;
183-  middleware?: Middleware[];
184-  enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback;
185-}
186-
187-/**
188- * This function will integrate `starfx` and `redux`.
189- *
190- * In order to enable `starfx/query`, it will add some reducers to your `redux`
191- * store for decoupled loaders and a simple data cache.
192- *
193- * It also adds `redux-batched-actions` which is critical for `starfx`.
194- *
195- * @example
196- * ```ts
197- * import { configureStore } from 'starfx/redux';
198- *
199- * const { store, fx } = prepareStore({
200- *  reducers: { users: (state, action) => state },
201- * });
202- *
203- * fx.run(function*() {
204- *  yield* put({ type: 'LOADING' });
205- *  yield* fetch('https://bower.sh');
206- *  yield* put({ type: 'LOADING_COMPLETE' });
207- * });
208- * ```
209- */
210-export function configureStore(
211-  { reducers, middleware = [], enhancers = [] }: SetupStoreProps,
212-) {
213-  const fx = createFxMiddleware();
214-  const rootReducer = combineReducers({ ...queryReducers, ...reducers });
215-  const store = reduxStore({
216-    reducer: enableBatching(rootReducer),
217-    middleware: (getDefaultMiddleware) =>
218-      getDefaultMiddleware().concat([fx.middleware, ...middleware]),
219-    enhancers: enhancers,
220-  });
221-
222-  return { store, fx };
223-}
224+export * from "./fx.ts";
225+export * from "./query.ts";
226+export * from "./middleware.ts";
227+export type { ActionWPayload, AnyAction, AnyState } from "../store/types.ts";
228+export { createSelector } from "../deps.ts";
M redux/put.test.ts
+18, -10
 1@@ -1,7 +1,7 @@
 2-import { describe, expect, it, setupReduxScope } from "../test.ts";
 3+import { describe, expect, it } from "../test.ts";
 4 import { sleep, spawn } from "../deps.ts";
 5 
 6-import { ActionContext, put, take } from "./mod.ts";
 7+import { ActionContext, configureStore, put, take } from "./mod.ts";
 8 
 9 const putTests = describe("put()");
10 
11@@ -27,8 +27,10 @@ it(putTests, "should send actions through channel", async () => {
12     });
13   }
14 
15-  const scope = setupReduxScope();
16-  await scope.run(() => genFn("arg"));
17+  const store = configureStore({
18+    reducers: { def: (s = null, _) => s },
19+  });
20+  await store.fx.run(() => genFn("arg"));
21 
22   const expected = ["arg", "2"];
23   expect(actual).toEqual(expected);
24@@ -57,8 +59,10 @@ it(putTests, "should handle nested puts", async () => {
25     yield* spawn(genA);
26   }
27 
28-  const scope = setupReduxScope();
29-  await scope.run(() => root());
30+  const store = configureStore({
31+    reducers: { def: (s = null, _) => s },
32+  });
33+  await store.fx.run(root);
34 
35   const expected = ["put b", "put a"];
36   expect(actual).toEqual(expected);
37@@ -75,8 +79,10 @@ it(
38       yield* sleep(0);
39     }
40 
41-    const scope = setupReduxScope();
42-    await scope.run(() => root());
43+    const store = configureStore({
44+      reducers: { def: (s = null, _) => s },
45+    });
46+    await store.fx.run(root);
47     expect(true).toBe(true);
48   },
49 );
50@@ -103,8 +109,10 @@ it(
51       yield* tsk;
52     }
53 
54-    const scope = setupReduxScope();
55-    await scope.run(() => root());
56+    const store = configureStore({
57+      reducers: { def: (s = null, _) => s },
58+    });
59+    await store.fx.run(root);
60     const expected = ["didn't get missed"];
61     expect(actual).toEqual(expected);
62   },
A redux/query.ts
+113, -0
  1@@ -0,0 +1,113 @@
  2+import { createLoaderTable, createReducerMap, createTable } from "../deps.ts";
  3+import { compose } from "../compose.ts";
  4+export { defaultLoader } from "../store/mod.ts";
  5+import { dispatchActions } from "../store/mod.ts";
  6+import { ApiCtx, createKey, Next } from "../query/mod.ts";
  7+import { put, select } from "./mod.ts";
  8+import type { QueryState } from "../types.ts";
  9+
 10+export function reduxMdw<Ctx extends ApiCtx = ApiCtx>(
 11+  errorFn?: (ctx: Ctx) => string,
 12+) {
 13+  return compose<Ctx>([dispatchActions, loadingMonitor(errorFn), simpleCache]);
 14+}
 15+
 16+export const LOADERS_NAME = "@@starfx/loaders";
 17+export const loaders = createLoaderTable({ name: LOADERS_NAME });
 18+export const {
 19+  loading: setLoaderStart,
 20+  error: setLoaderError,
 21+  success: setLoaderSuccess,
 22+  resetById: resetLoaderById,
 23+} = loaders.actions;
 24+export const { selectTable: selectLoaders, selectById: selectLoaderById } =
 25+  loaders.getSelectors((state: any) => state[LOADERS_NAME] || {});
 26+
 27+export const DATA_NAME = "@@starfx/data";
 28+export const data = createTable<any>({ name: DATA_NAME });
 29+export const { add: addData, reset: resetData } = data.actions;
 30+
 31+export const { selectTable: selectData, selectById: selectDataById } = data
 32+  .getSelectors((s: any) => s[DATA_NAME] || {});
 33+
 34+/**
 35+ * Returns data from the saga-query slice of redux from an action.
 36+ */
 37+export const selectDataByName = (
 38+  s: any,
 39+  p: { name: string; payload?: any },
 40+) => {
 41+  const id = createKey(p.name, p.payload);
 42+  const data = selectDataById(s, { id });
 43+  return data;
 44+};
 45+
 46+export const reducers = createReducerMap(loaders, data);
 47+
 48+/**
 49+ * This middleware will automatically cache any data found inside `ctx.json`
 50+ * which is where we store JSON data from the `fetcher` middleware.
 51+ */
 52+export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
 53+  ctx: Ctx,
 54+  next: Next,
 55+) {
 56+  ctx.cacheData = yield* select((state: QueryState) =>
 57+    selectDataById(state, { id: ctx.key })
 58+  );
 59+  yield* next();
 60+  if (!ctx.cache) return;
 61+  const { data } = ctx.json;
 62+  yield* put(addData({ [ctx.key]: data }));
 63+  ctx.cacheData = data;
 64+}
 65+
 66+/**
 67+ * This middleware will track the status of a fetch request.
 68+ */
 69+export function loadingMonitor<Ctx extends ApiCtx = ApiCtx>(
 70+  errorFn: (ctx: Ctx) => string = (ctx) => ctx.json?.data?.message || "",
 71+) {
 72+  return function* trackLoading(ctx: Ctx, next: Next) {
 73+    yield* put([
 74+      setLoaderStart({ id: ctx.name }),
 75+      setLoaderStart({ id: ctx.key }),
 76+    ]);
 77+    if (!ctx.loader) ctx.loader = {} as any;
 78+
 79+    yield* next();
 80+
 81+    if (!ctx.response) {
 82+      yield* put([
 83+        resetLoaderById(ctx.name),
 84+        resetLoaderById(ctx.key),
 85+      ]);
 86+      return;
 87+    }
 88+
 89+    if (!ctx.loader) {
 90+      ctx.loader || {};
 91+    }
 92+
 93+    if (!ctx.response.ok) {
 94+      yield* put([
 95+        setLoaderError({
 96+          id: ctx.name as any,
 97+          message: errorFn(ctx),
 98+          ...ctx.loader,
 99+        }),
100+        setLoaderError({
101+          id: ctx.key as any,
102+          message: errorFn(ctx),
103+          ...ctx.loader,
104+        }),
105+      ]);
106+      return;
107+    }
108+
109+    yield* put([
110+      setLoaderSuccess({ id: ctx.name as any, ...ctx.loader }),
111+      setLoaderSuccess({ id: ctx.key as any, ...ctx.loader }),
112+    ]);
113+  };
114+}
M redux/take-helper.test.ts
+1, -2
 1@@ -1,5 +1,4 @@
 2 import { describe, expect, it } from "../test.ts";
 3-import { cancel } from "../fx/mod.ts";
 4 import type { AnyAction } from "../deps.ts";
 5 
 6 import { configureStore, take, takeEvery } from "./mod.ts";
 7@@ -17,7 +16,7 @@ it(testEvery, "should work", async () => {
 8       (action) => worker("a1", "a2", action),
 9     );
10     yield* take("CANCEL_WATCHER");
11-    yield* cancel(task);
12+    yield* task.halt();
13   }
14 
15   function* worker(arg1: string, arg2: string, action: AnyAction) {
M redux/take.test.ts
+10, -6
 1@@ -1,7 +1,7 @@
 2-import { describe, expect, it, setupReduxScope } from "../test.ts";
 3+import { describe, expect, it } from "../test.ts";
 4 import { AnyAction, sleep, spawn } from "../deps.ts";
 5 
 6-import { put, take } from "./mod.ts";
 7+import { configureStore, put, take } from "./mod.ts";
 8 
 9 const takeTests = describe("take()");
10 
11@@ -24,8 +24,10 @@ it(
12       actual.push(yield* take("action-1"));
13     }
14 
15-    const scope = setupReduxScope();
16-    await scope.run(root);
17+    const store = configureStore({
18+      reducers: { def: (s = null, _) => s },
19+    });
20+    await store.fx.run(root);
21 
22     expect(actual).toEqual([
23       { type: "action-1", payload: 1 },
24@@ -84,8 +86,10 @@ it(takeTests, "take from default channel", async () => {
25     }
26   }
27 
28-  const scope = setupReduxScope();
29-  await scope.run(genFn);
30+  const store = configureStore({
31+    reducers: { def: (s = null, _) => s },
32+  });
33+  await store.fx.run(genFn);
34 
35   const expected = [
36     {
A store/configureStore.test.ts
+29, -0
 1@@ -0,0 +1,29 @@
 2+import { describe, expect, it } from "../test.ts";
 3+import { call } from "../fx/mod.ts";
 4+
 5+import { select } from "./mod.ts";
 6+import { configureStore } from "./store.ts";
 7+
 8+const tests = describe("configureStore()");
 9+
10+interface TestState {
11+  user: { id: string };
12+}
13+
14+it(tests, "should be able to grab values from store", async () => {
15+  const store = await configureStore({ initialState: { user: { id: "1" } } });
16+  await store.run(function* () {
17+    const actual = yield* select((s: TestState) => s.user);
18+    expect(actual).toEqual({ id: "1" });
19+  });
20+});
21+
22+it(tests, "should be able to grab store from a nested call", async () => {
23+  const store = await configureStore({ initialState: { user: { id: "2" } } });
24+  await store.run(function* () {
25+    const actual = yield* call(function* () {
26+      return yield* select((s: TestState) => s.user);
27+    });
28+    expect(actual).toEqual({ id: "2" });
29+  });
30+});
A store/context.ts
+15, -0
 1@@ -0,0 +1,15 @@
 2+import { Channel, createChannel, createContext } from "../deps.ts";
 3+
 4+import type { AnyAction, AnyState, FxStore } from "./types.ts";
 5+
 6+export const ActionContext = createContext<Channel<AnyAction, void>>(
 7+  "store:action",
 8+  createChannel<AnyAction, void>(),
 9+);
10+
11+export const StoreUpdateContext = createContext<Channel<void, void>>(
12+  "store:update",
13+  createChannel<void, void>(),
14+);
15+
16+export const StoreContext = createContext<FxStore<AnyState>>("store");
A store/fx.ts
+122, -0
  1@@ -0,0 +1,122 @@
  2+import { Channel, Operation, spawn, Task } from "../deps.ts";
  3+import { call, parallel } from "../fx/mod.ts";
  4+import { ActionPattern, matcher } from "../matcher.ts";
  5+
  6+import type {
  7+  ActionWPayload,
  8+  AnyAction,
  9+  AnyState,
 10+  StoreUpdater,
 11+  UpdaterCtx,
 12+} from "./types.ts";
 13+import { ActionContext, StoreContext } from "./context.ts";
 14+
 15+export function* updateStore<S extends AnyState>(
 16+  updater: StoreUpdater<S> | StoreUpdater<S>[],
 17+): Operation<UpdaterCtx<S>> {
 18+  const store = yield* StoreContext;
 19+  const ctx = yield* store.update(updater as any);
 20+  // TODO: fix type
 21+  return ctx as any;
 22+}
 23+
 24+export function* emit({
 25+  channel,
 26+  action,
 27+}: {
 28+  channel: Operation<Channel<AnyAction, void>>;
 29+  action: AnyAction | AnyAction[];
 30+}) {
 31+  const { input } = yield* channel;
 32+  if (Array.isArray(action)) {
 33+    if (action.length === 0) {
 34+      return;
 35+    }
 36+    yield* parallel(action.map((a) => () => input.send(a)));
 37+  } else {
 38+    yield* input.send(action);
 39+  }
 40+}
 41+
 42+export function* once({
 43+  channel,
 44+  pattern,
 45+}: {
 46+  channel: Operation<Channel<AnyAction, void>>;
 47+  pattern: ActionPattern;
 48+}) {
 49+  const { output } = yield* channel;
 50+  const msgList = yield* output;
 51+  let next = yield* msgList.next();
 52+  while (!next.done) {
 53+    const match = matcher(pattern);
 54+    if (match(next.value)) {
 55+      return next.value;
 56+    }
 57+    next = yield* msgList.next();
 58+  }
 59+}
 60+
 61+export function* select<S, R, P>(selectorFn: (s: S, p?: P) => R, p?: P) {
 62+  const store = yield* StoreContext;
 63+  return selectorFn(store.getState() as S, p);
 64+}
 65+
 66+export function* put(action: AnyAction | AnyAction[]) {
 67+  return yield* emit({
 68+    channel: ActionContext,
 69+    action,
 70+  });
 71+}
 72+
 73+export function take<P>(pattern: ActionPattern): Operation<ActionWPayload<P>>;
 74+export function* take(pattern: ActionPattern): Operation<AnyAction> {
 75+  const action = yield* once({
 76+    channel: ActionContext,
 77+    pattern,
 78+  });
 79+  return action as AnyAction;
 80+}
 81+
 82+export function* takeEvery<T>(
 83+  pattern: ActionPattern,
 84+  op: (action: AnyAction) => Operation<T>,
 85+): Operation<Task<void>> {
 86+  return yield* spawn(function* () {
 87+    while (true) {
 88+      const action = yield* take(pattern);
 89+      if (!action) continue;
 90+      yield* spawn(() => op(action));
 91+    }
 92+  });
 93+}
 94+
 95+export function* takeLatest<T>(
 96+  pattern: ActionPattern,
 97+  op: (action: AnyAction) => Operation<T>,
 98+): Operation<Task<void>> {
 99+  return yield* spawn(function* () {
100+    let lastTask;
101+    while (true) {
102+      const action = yield* take(pattern);
103+      if (lastTask) {
104+        yield* lastTask.halt();
105+      }
106+      if (!action) continue;
107+      lastTask = yield* spawn(() => op(action));
108+    }
109+  });
110+}
111+
112+export function* takeLeading<T>(
113+  pattern: ActionPattern,
114+  op: (action: AnyAction) => Operation<T>,
115+): Operation<Task<void>> {
116+  return yield* spawn(function* () {
117+    while (true) {
118+      const action = yield* take(pattern);
119+      if (!action) continue;
120+      yield* call(() => op(action));
121+    }
122+  });
123+}
A store/mod.ts
+8, -0
1@@ -0,0 +1,8 @@
2+export * from "./context.ts";
3+export * from "./fx.ts";
4+export * from "./store.ts";
5+export * from "./types.ts";
6+export * from "./slice.ts";
7+export * from "./query.ts";
8+export * from "./supervisor.ts";
9+export { createSelector } from "../deps.ts";
A store/put.test.ts
+112, -0
  1@@ -0,0 +1,112 @@
  2+import { describe, expect, it } from "../test.ts";
  3+import { sleep, spawn } from "../deps.ts";
  4+
  5+import { ActionContext, put, take } from "./mod.ts";
  6+import { configureStore } from "./store.ts";
  7+
  8+const putTests = describe("put()");
  9+
 10+it(putTests, "should send actions through channel", async () => {
 11+  const actual: string[] = [];
 12+
 13+  function* genFn(arg: string) {
 14+    yield* spawn(function* () {
 15+      const actions = yield* ActionContext;
 16+      const msgs = yield* actions.output;
 17+      let action = yield* msgs.next();
 18+      while (!action.done) {
 19+        actual.push(action.value.type);
 20+        action = yield* msgs.next();
 21+      }
 22+    });
 23+
 24+    yield* put({
 25+      type: arg,
 26+    });
 27+    yield* put({
 28+      type: "2",
 29+    });
 30+  }
 31+
 32+  const store = await configureStore({ initialState: {} });
 33+  await store.run(() => genFn("arg"));
 34+
 35+  const expected = ["arg", "2"];
 36+  expect(actual).toEqual(expected);
 37+});
 38+
 39+it(putTests, "should handle nested puts", async () => {
 40+  const actual: string[] = [];
 41+
 42+  function* genA() {
 43+    yield* put({
 44+      type: "a",
 45+    });
 46+    actual.push("put a");
 47+  }
 48+
 49+  function* genB() {
 50+    yield* take(["a"]);
 51+    yield* put({
 52+      type: "b",
 53+    });
 54+    actual.push("put b");
 55+  }
 56+
 57+  function* root() {
 58+    yield* spawn(genB);
 59+    yield* spawn(genA);
 60+  }
 61+
 62+  const store = await configureStore({ initialState: {} });
 63+  await store.run(() => root());
 64+
 65+  const expected = ["put b", "put a"];
 66+  expect(actual).toEqual(expected);
 67+});
 68+
 69+it(
 70+  putTests,
 71+  "should not cause stack overflow when puts are emitted while dispatching saga",
 72+  async () => {
 73+    function* root() {
 74+      for (let i = 0; i < 40_000; i += 1) {
 75+        yield* put({ type: "test" });
 76+      }
 77+      yield* sleep(0);
 78+    }
 79+
 80+    const store = await configureStore({ initialState: {} });
 81+    await store.run(() => root());
 82+    expect(true).toBe(true);
 83+  },
 84+);
 85+
 86+it(
 87+  putTests,
 88+  "should not miss `put` that was emitted directly after creating a task (caused by another `put`)",
 89+  async () => {
 90+    const actual: string[] = [];
 91+
 92+    function* root() {
 93+      yield* spawn(function* firstspawn() {
 94+        yield* sleep(1000);
 95+        yield* put({ type: "c" });
 96+        yield* put({ type: "do not miss" });
 97+      });
 98+
 99+      yield* take("c");
100+
101+      const tsk = yield* spawn(function* () {
102+        yield* take("do not miss");
103+        actual.push("didn't get missed");
104+      });
105+      yield* tsk;
106+    }
107+
108+    const store = await configureStore({ initialState: {} });
109+    await store.run(() => root());
110+    const expected = ["didn't get missed"];
111+    expect(actual).toEqual(expected);
112+  },
113+);
A store/query.ts
+195, -0
  1@@ -0,0 +1,195 @@
  2+import { race } from "../fx/mod.ts";
  3+import { sleep } from "../deps.ts";
  4+import type { ApiCtx, LoaderCtx, Next } from "../query/mod.ts";
  5+import { compose } from "../compose.ts";
  6+import type { QueryState } from "../types.ts";
  7+import { createAction } from "../action.ts";
  8+
  9+import { put, select, take, updateStore } from "./fx.ts";
 10+import type { AnyAction } from "./types.ts";
 11+import {
 12+  addData,
 13+  resetLoaderById,
 14+  selectDataById,
 15+  setLoaderError,
 16+  setLoaderStart,
 17+  setLoaderSuccess,
 18+} from "./slice.ts";
 19+
 20+export function storeMdw<Ctx extends ApiCtx = ApiCtx>(
 21+  errorFn?: (ctx: Ctx) => string,
 22+) {
 23+  return compose<Ctx>([dispatchActions, loadingMonitor(errorFn), simpleCache]);
 24+}
 25+
 26+/**
 27+ * This middleware will automatically cache any data found inside `ctx.json`
 28+ * which is where we store JSON data from the `fetcher` middleware.
 29+ */
 30+export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
 31+  ctx: Ctx,
 32+  next: Next,
 33+) {
 34+  ctx.cacheData = yield* select((state: QueryState) =>
 35+    selectDataById(state, { id: ctx.key })
 36+  );
 37+  yield* next();
 38+  if (!ctx.cache) return;
 39+  const { data } = ctx.json;
 40+  yield* updateStore(addData({ [ctx.key]: data }));
 41+  ctx.cacheData = data;
 42+}
 43+
 44+/**
 45+ * This middleware will take the result of `ctx.actions` and dispatch them
 46+ * as a single batch.
 47+ *
 48+ * @remarks This is useful because sometimes there are a lot of actions that need dispatched
 49+ * within the pipeline of the middleware and instead of dispatching them serially this
 50+ * improves performance by only hitting the reducers once.
 51+ */
 52+export function* dispatchActions(ctx: { actions: AnyAction[] }, next: Next) {
 53+  if (!ctx.actions) ctx.actions = [];
 54+  yield* next();
 55+  if (ctx.actions.length === 0) return;
 56+  yield* put(ctx.actions);
 57+}
 58+
 59+export interface OptimisticCtx<
 60+  A extends AnyAction = AnyAction,
 61+  R extends AnyAction = AnyAction,
 62+> extends ApiCtx {
 63+  optimistic: {
 64+    apply: A;
 65+    revert: R;
 66+  };
 67+}
 68+
 69+/**
 70+ * This middleware performs an optimistic update for a middleware pipeline.
 71+ * It accepts an `apply` and `revert` action.
 72+ *
 73+ * @remarks This means that we will first `apply` and then if the request is successful we
 74+ * keep the change or we `revert` if there's an error.
 75+ */
 76+export function* optimistic<Ctx extends OptimisticCtx = OptimisticCtx>(
 77+  ctx: Ctx,
 78+  next: Next,
 79+) {
 80+  if (!ctx.optimistic) {
 81+    yield* next();
 82+    return;
 83+  }
 84+
 85+  const { apply, revert } = ctx.optimistic;
 86+  // optimistically update user
 87+  yield* put(apply);
 88+
 89+  yield* next();
 90+
 91+  if (!ctx.response || !ctx.response.ok) {
 92+    yield* put(revert);
 93+  }
 94+}
 95+
 96+export interface UndoCtx<P = any, S = any, E = any> extends ApiCtx<P, S, E> {
 97+  undoable: boolean;
 98+}
 99+
100+export const doIt = createAction("DO_IT");
101+export const undo = createAction("UNDO");
102+/**
103+ * This middleware will allow pipeline functions to be undoable which means before they are activated
104+ * we have a timeout that allows the function to be cancelled.
105+ */
106+export function undoer<Ctx extends UndoCtx = UndoCtx>(
107+  doItType = `${doIt}`,
108+  undoType = `${undo}`,
109+  timeout = 30 * 1000,
110+) {
111+  return function* onUndo(ctx: Ctx, next: Next) {
112+    if (!ctx.undoable) {
113+      yield* next();
114+      return;
115+    }
116+
117+    const winner = yield* race({
118+      doIt: () => take(`${doItType}`),
119+      undo: () => take(`${undoType}`),
120+      timeout: () => sleep(timeout),
121+    });
122+
123+    if (winner.undo || winner.timeout) {
124+      return;
125+    }
126+
127+    yield* next();
128+  };
129+}
130+
131+/**
132+ * This middleware creates a loader for a generator function which allows us to track
133+ * the status of a pipeline function.
134+ */
135+export function* loadingMonitorSimple<Ctx extends LoaderCtx = LoaderCtx>(
136+  ctx: Ctx,
137+  next: Next,
138+) {
139+  yield* updateStore([
140+    setLoaderStart({ id: ctx.name }),
141+    setLoaderStart({ id: ctx.key }),
142+  ]);
143+
144+  if (!ctx.loader) {
145+    ctx.loader = {};
146+  }
147+
148+  yield* next();
149+
150+  yield* updateStore([
151+    setLoaderSuccess({ ...ctx.loader, id: ctx.name }),
152+    setLoaderSuccess({ ...ctx.loader, id: ctx.key }),
153+  ]);
154+}
155+
156+/**
157+ * This middleware will track the status of a fetch request.
158+ */
159+export function loadingMonitor<Ctx extends ApiCtx = ApiCtx>(
160+  errorFn: (ctx: Ctx) => string = (ctx) => ctx.json?.data?.message || "",
161+) {
162+  return function* trackLoading(ctx: Ctx, next: Next) {
163+    yield* updateStore([
164+      setLoaderStart({ id: ctx.name }),
165+      setLoaderStart({ id: ctx.key }),
166+    ]);
167+    if (!ctx.loader) ctx.loader = {} as any;
168+
169+    yield* next();
170+
171+    if (!ctx.response) {
172+      yield* updateStore([
173+        resetLoaderById({ id: ctx.name }),
174+        resetLoaderById({ id: ctx.key }),
175+      ]);
176+      return;
177+    }
178+
179+    if (!ctx.loader) {
180+      ctx.loader || {};
181+    }
182+
183+    if (!ctx.response.ok) {
184+      yield* updateStore([
185+        setLoaderError({ id: ctx.name, message: errorFn(ctx), ...ctx.loader }),
186+        setLoaderError({ id: ctx.key, message: errorFn(ctx), ...ctx.loader }),
187+      ]);
188+      return;
189+    }
190+
191+    yield* updateStore([
192+      setLoaderSuccess({ id: ctx.name, ...ctx.loader }),
193+      setLoaderSuccess({ id: ctx.key, ...ctx.loader }),
194+    ]);
195+  };
196+}
A store/react.ts
+1, -0
1@@ -0,0 +1 @@
2+export { Provider, useDispatch, useSelector } from "../deps.ts";
A store/slice.ts
+91, -0
 1@@ -0,0 +1,91 @@
 2+import type {
 3+  IdProp,
 4+  LoadingItemState,
 5+  LoadingPayload,
 6+  LoadingState,
 7+  LoadingStatus,
 8+} from "../types.ts";
 9+import type { QueryState } from "../types.ts";
10+
11+export const defaultLoader = (
12+  p: Partial<LoadingItemState> = {},
13+): LoadingItemState => {
14+  return {
15+    id: "",
16+    status: "idle",
17+    message: "",
18+    lastRun: 0,
19+    lastSuccess: 0,
20+    meta: {},
21+    ...p,
22+  };
23+};
24+
25+export const selectLoaderTable = (s: QueryState) => {
26+  return s["@@starfx/loaders"] || {};
27+};
28+
29+const initLoader = defaultLoader();
30+export const selectLoaderById = (
31+  s: QueryState,
32+  { id }: { id: IdProp },
33+): LoadingState => {
34+  const base = selectLoaderTable(s)[id] || initLoader;
35+  return {
36+    ...base,
37+    isIdle: base.status === "idle",
38+    isError: base.status === "error",
39+    isSuccess: base.status === "success",
40+    isLoading: base.status === "loading",
41+    isInitialLoading: (base.status === "idle" || base.status === "loading") &&
42+      base.lastSuccess === 0,
43+  };
44+};
45+
46+const setLoaderState = (status: LoadingStatus) => {
47+  return (props: LoadingPayload) => {
48+    function updateLoadingState(s: QueryState) {
49+      if (!props.id) return;
50+      const loaders = selectLoaderTable(s);
51+      if (!loaders[props.id]) {
52+        loaders[props.id] = defaultLoader({ ...props });
53+        return;
54+      }
55+
56+      const loader = loaders[props.id];
57+      loader.status = status;
58+      if (props.meta) {
59+        loader.meta = props.meta;
60+      }
61+      if (props.message) {
62+        loader.message = props.message;
63+      }
64+    }
65+    return updateLoadingState;
66+  };
67+};
68+export const setLoaderStart = setLoaderState("loading");
69+export const setLoaderSuccess = setLoaderState("success");
70+export const setLoaderError = setLoaderState("error");
71+export const resetLoaderById = ({ id }: { id: string }) => {
72+  function resetLoader(s: QueryState) {
73+    const loaders = selectLoaderTable(s);
74+    delete loaders[id];
75+  }
76+  return resetLoader;
77+};
78+
79+export const selectDataTable = (s: QueryState) => {
80+  return s["@@starfx/data"] || {};
81+};
82+
83+export const selectDataById = (s: QueryState, { id }: { id: IdProp }) => {
84+  return selectDataTable(s)[id];
85+};
86+
87+export const addData = (props: { [key: string]: unknown }) => {
88+  function addDataState(s: QueryState) {
89+    s["@@starfx/data"] = { ...s["@@starfx/data"], ...props };
90+  }
91+  return addDataState;
92+};
A store/store.test.ts
+144, -0
  1@@ -0,0 +1,144 @@
  2+import { createScope } from "../deps.ts";
  3+import { parallel } from "../fx/mod.ts";
  4+import { asserts, describe, it } from "../test.ts";
  5+
  6+import { StoreContext, StoreUpdateContext } from "./context.ts";
  7+import { put, take, updateStore } from "./fx.ts";
  8+import { createStore, register } from "./store.ts";
  9+
 10+const tests = describe("store");
 11+
 12+interface User {
 13+  id: string;
 14+  name: string;
 15+}
 16+
 17+interface State {
 18+  users: { [key: string]: User };
 19+  theme: string;
 20+  token: string;
 21+  dev: boolean;
 22+}
 23+
 24+function findUserById(state: State, { id }: { id: string }) {
 25+  return state.users[id];
 26+}
 27+
 28+function findUsers(state: State) {
 29+  return state.users;
 30+}
 31+
 32+interface UpdateUserProps {
 33+  id: string;
 34+  name: string;
 35+}
 36+
 37+const updateUser = ({ id, name }: UpdateUserProps) => (state: State) => {
 38+  // use selectors to find the data you want to mutate
 39+  const user = findUserById(state, { id });
 40+  user.name = name;
 41+
 42+  // different ways to update a `zod` record
 43+  const users = findUsers(state);
 44+  users[id].name = name;
 45+
 46+  delete users[2];
 47+  users[3] = { id: "", name: "" };
 48+
 49+  // or mutate state directly without selectors
 50+  state.dev = true;
 51+};
 52+
 53+it(
 54+  tests,
 55+  "update store and receives update from channel `StoreUpdateContext`",
 56+  async () => {
 57+    const scope = createScope();
 58+    const initialState: Partial<State> = {
 59+      users: { 1: { id: "1", name: "testing" }, 2: { id: "2", name: "wow" } },
 60+      dev: false,
 61+    };
 62+    const store = createStore({ scope, initialState });
 63+    await register(store);
 64+
 65+    await scope.run(function* (): any {
 66+      const result = yield* parallel([
 67+        function* () {
 68+          const store = yield* StoreContext;
 69+          const chan = yield* StoreUpdateContext;
 70+          const msgList = yield* chan.output;
 71+          yield* msgList.next();
 72+          asserts.assertEquals(store.getState(), {
 73+            users: { 1: { id: "1", name: "eric" }, 3: { id: "", name: "" } },
 74+            theme: "",
 75+            token: null,
 76+            dev: true,
 77+          });
 78+        },
 79+
 80+        function* () {
 81+          yield* updateStore(updateUser({ id: "1", name: "eric" }));
 82+        },
 83+      ]);
 84+
 85+      return yield* result;
 86+    });
 87+  },
 88+);
 89+
 90+it(tests, "update store and receives update from `subscribe()`", async () => {
 91+  const scope = createScope();
 92+  const initialState: Partial<State> = {
 93+    users: { 1: { id: "1", name: "testing" }, 2: { id: "2", name: "wow" } },
 94+    dev: false,
 95+    theme: "",
 96+    token: "",
 97+  };
 98+  const store = createStore({ scope, initialState });
 99+  await register(store);
100+
101+  store.subscribe(() => {
102+    asserts.assertEquals(store.getState(), {
103+      users: { 1: { id: "1", name: "eric" }, 3: { id: "", name: "" } },
104+      theme: "",
105+      token: "",
106+      dev: true,
107+    });
108+  });
109+
110+  await scope.run(function* () {
111+    yield* updateStore(updateUser({ id: "1", name: "eric" }));
112+  });
113+});
114+
115+it(tests, "emit Action and update store", async () => {
116+  const scope = createScope();
117+  const initialState: Partial<State> = {
118+    users: { 1: { id: "1", name: "testing" }, 2: { id: "2", name: "wow" } },
119+    dev: false,
120+    theme: "",
121+    token: "",
122+  };
123+  const store = createStore({ scope, initialState });
124+  await register(store);
125+
126+  await scope.run(function* (): any {
127+    const result = yield* parallel([
128+      function* (): any {
129+        const action = yield* take<UpdateUserProps>("UPDATE_USER");
130+        yield* updateStore(updateUser(action.payload));
131+      },
132+      function* () {
133+        yield* put({ type: "UPDATE_USER", payload: { id: "1", name: "eric" } });
134+      },
135+    ]);
136+    yield* result;
137+  });
138+
139+  asserts.assertEquals(store.getState(), {
140+    users: { 1: { id: "1", name: "eric" }, 3: { id: "", name: "" } },
141+    theme: "",
142+    token: "",
143+    dev: true,
144+  });
145+});
A store/store.ts
+171, -0
  1@@ -0,0 +1,171 @@
  2+import {
  3+  createScope,
  4+  enablePatches,
  5+  produceWithPatches,
  6+  Result,
  7+  Scope,
  8+  Task,
  9+} from "../deps.ts";
 10+import { BaseMiddleware, compose } from "../compose.ts";
 11+import type { OpFn } from "../types.ts";
 12+import { call } from "../fx/mod.ts";
 13+
 14+import type {
 15+  AnyAction,
 16+  AnyState,
 17+  FxStore,
 18+  Listener,
 19+  StoreUpdater,
 20+  UpdaterCtx,
 21+} from "./types.ts";
 22+import { StoreContext, StoreUpdateContext } from "./context.ts";
 23+import { put } from "./fx.ts";
 24+import { Next } from "../query/types.ts";
 25+
 26+const stubMsg = "This is merely a stub, not implemented";
 27+
 28+// https://github.com/reduxjs/redux/blob/4a6d2fb227ba119d3498a43fab8f53fe008be64c/src/createStore.ts#L344
 29+function observable() {
 30+  return {
 31+    subscribe: (_observer: unknown) => {
 32+      throw new Error(stubMsg);
 33+    },
 34+    [Symbol.observable]() {
 35+      return this;
 36+    },
 37+  };
 38+}
 39+
 40+export interface CreateStore<S extends AnyState> {
 41+  scope?: Scope;
 42+  initialState: S;
 43+  middleware?: BaseMiddleware[];
 44+}
 45+
 46+export function createStore<S extends AnyState>({
 47+  initialState,
 48+  scope = createScope(),
 49+  middleware = [],
 50+}: CreateStore<S>): FxStore<S> {
 51+  let state = initialState;
 52+  const listeners = new Set<Listener>();
 53+  enablePatches();
 54+
 55+  function getScope() {
 56+    return scope;
 57+  }
 58+
 59+  function getState() {
 60+    return state;
 61+  }
 62+
 63+  function subscribe(fn: Listener) {
 64+    listeners.add(fn);
 65+    return () => listeners.delete(fn);
 66+  }
 67+
 68+  function* updateMdw(ctx: UpdaterCtx<S>, next: Next) {
 69+    const upds: StoreUpdater<S>[] = [];
 70+
 71+    if (Array.isArray(ctx.updater)) {
 72+      upds.push(...ctx.updater);
 73+    } else {
 74+      upds.push(ctx.updater);
 75+    }
 76+
 77+    const [nextState, patches, _] = produceWithPatches(getState(), (draft) => {
 78+      // TODO: check for return value inside updater
 79+      upds.forEach((updater) => updater(draft as any));
 80+    });
 81+    ctx.patches = patches;
 82+
 83+    // set the state!
 84+    state = nextState;
 85+
 86+    yield* next();
 87+  }
 88+
 89+  function* notifyChannelMdw(_: UpdaterCtx<S>, next: Next) {
 90+    const chan = yield* StoreUpdateContext;
 91+    yield* chan.input.send();
 92+    yield* next();
 93+  }
 94+
 95+  function* notifyListenersMdw(_: UpdaterCtx<S>, next: Next) {
 96+    listeners.forEach((f) => f());
 97+    yield* next();
 98+  }
 99+
100+  function createUpdater() {
101+    const fn = compose<UpdaterCtx<S>>([
102+      ...middleware,
103+      updateMdw,
104+      notifyChannelMdw,
105+      notifyListenersMdw,
106+    ]);
107+
108+    return fn;
109+  }
110+
111+  const mdw = createUpdater();
112+  function* update(updater: StoreUpdater<S> | StoreUpdater<S>[]) {
113+    const ctx = {
114+      updater,
115+      patches: [],
116+    };
117+    const result = yield* mdw(ctx);
118+    // TODO: dev mode only?
119+    if (!result.ok) {
120+      console.error(result.error);
121+    }
122+    return result;
123+  }
124+
125+  function dispatch(action: AnyAction | AnyAction[]): Task<any> {
126+    return scope.run(function* () {
127+      yield* put(action);
128+    });
129+  }
130+
131+  function run<T>(op: OpFn<T>): Task<Result<T>> {
132+    return scope.run(function* () {
133+      return yield* call(op);
134+    });
135+  }
136+
137+  return {
138+    getScope,
139+    getState,
140+    subscribe,
141+    update,
142+    run,
143+    // instead of actions relating to store mutation, they
144+    // refer to pieces of business logic -- that can also mutate state
145+    dispatch,
146+    // stubs so `react-redux` is happy
147+    replaceReducer<S = any>(
148+      _nextReducer: (_s: S, _a: AnyAction) => void,
149+    ): void {
150+      throw new Error(stubMsg);
151+    },
152+    [Symbol.observable]: observable,
153+  };
154+}
155+
156+export function register<S extends AnyState>(store: FxStore<S>) {
157+  const scope = store.getScope();
158+  return scope.run(function* () {
159+    // TODO: fix type
160+    yield* StoreContext.set(store as any);
161+  });
162+}
163+
164+const defaultScope = createScope();
165+export async function configureStore<S extends AnyState>({
166+  scope = defaultScope,
167+  ...props
168+}: CreateStore<S>): Promise<FxStore<S>> {
169+  const store = createStore<S>({ scope, ...props });
170+  await register(store);
171+  return store;
172+}
R query/supervisor.ts => store/supervisor.ts
+9, -6
 1@@ -1,9 +1,9 @@
 2 import { call, race } from "../fx/mod.ts";
 3-import { ActionWPayload, take } from "../redux/mod.ts";
 4-import { Action, Operation, sleep, spawn, Task } from "../deps.ts";
 5+import { ActionWPayload, take } from "../store/mod.ts";
 6+import type { AnyAction } from "../store/types.ts";
 7+import { Operation, sleep, spawn, Task } from "../deps.ts";
 8 import type { OpFn } from "../types.ts";
 9-
10-import type { CreateActionPayload } from "./types.ts";
11+import type { CreateActionPayload } from "../query/types.ts";
12 
13 const MS = 1000;
14 const SECONDS = 1 * MS;
15@@ -12,7 +12,7 @@ const MINUTES = 60 * SECONDS;
16 export function poll(parentTimer: number = 5 * 1000, cancelType?: string) {
17   return function* poller<T>(
18     actionType: string,
19-    op: (action: Action) => Operation<T>,
20+    op: (action: AnyAction) => Operation<T>,
21   ): Operation<T> {
22     const cancel = cancelType || actionType;
23     function* fire(action: { type: string }, timer: number) {
24@@ -43,7 +43,10 @@ export function poll(parentTimer: number = 5 * 1000, cancelType?: string) {
25  * cache timer then the second call will not send an http request.
26  */
27 export function timer(timer: number = 5 * MINUTES) {
28-  return function* onTimer(actionType: string, op: (action: Action) => OpFn) {
29+  return function* onTimer(
30+    actionType: string,
31+    op: (action: AnyAction) => OpFn,
32+  ) {
33     const map: { [key: string]: Task<unknown> } = {};
34 
35     function* activate(action: ActionWPayload<CreateActionPayload>) {
A store/take-helper.test.ts
+55, -0
 1@@ -0,0 +1,55 @@
 2+import { describe, expect, it } from "../test.ts";
 3+
 4+import type { AnyAction } from "./types.ts";
 5+import { configureStore, take, takeEvery } from "./mod.ts";
 6+
 7+const testEvery = describe("takeEvery()");
 8+
 9+it(testEvery, "should work", async () => {
10+  const loop = 10;
11+  const actual: string[][] = [];
12+
13+  function* root() {
14+    const task = yield* takeEvery(
15+      "ACTION",
16+      (action) => worker("a1", "a2", action),
17+    );
18+    yield* take("CANCEL_WATCHER");
19+    yield* task.halt();
20+  }
21+
22+  function* worker(arg1: string, arg2: string, action: AnyAction) {
23+    actual.push([arg1, arg2, action.payload]);
24+  }
25+
26+  const store = await configureStore({ initialState: {} });
27+  const task = store.run(root);
28+
29+  for (let i = 1; i <= loop / 2; i += 1) {
30+    store.dispatch({
31+      type: "ACTION",
32+      payload: i,
33+    });
34+  }
35+
36+  // no further task should be forked after this
37+  store.dispatch({
38+    type: "CANCEL_WATCHER",
39+  });
40+
41+  for (let i = loop / 2 + 1; i <= loop; i += 1) {
42+    store.dispatch({
43+      type: "ACTION",
44+      payload: i,
45+    });
46+  }
47+  await task;
48+
49+  expect(actual).toEqual([
50+    ["a1", "a2", 1],
51+    ["a1", "a2", 2],
52+    ["a1", "a2", 3],
53+    ["a1", "a2", 4],
54+    ["a1", "a2", 5],
55+  ]);
56+});
A store/take.test.ts
+120, -0
  1@@ -0,0 +1,120 @@
  2+import { describe, expect, it } from "../test.ts";
  3+import { sleep, spawn } from "../deps.ts";
  4+
  5+import type { AnyAction } from "./types.ts";
  6+import { put, take } from "./mod.ts";
  7+import { configureStore } from "./store.ts";
  8+
  9+const takeTests = describe("take()");
 10+
 11+it(
 12+  takeTests,
 13+  "a put should complete before more `take` are added and then consumed automatically",
 14+  async () => {
 15+    const actual: AnyAction[] = [];
 16+
 17+    function* channelFn() {
 18+      yield* sleep(10);
 19+      yield* put({ type: "action-1", payload: 1 });
 20+      yield* put({ type: "action-1", payload: 2 });
 21+    }
 22+
 23+    function* root() {
 24+      yield* spawn(channelFn);
 25+
 26+      actual.push(yield* take("action-1"));
 27+      actual.push(yield* take("action-1"));
 28+    }
 29+
 30+    const store = await configureStore({ initialState: {} });
 31+    await store.run(root);
 32+
 33+    expect(actual).toEqual([
 34+      { type: "action-1", payload: 1 },
 35+      { type: "action-1", payload: 2 },
 36+    ]);
 37+  },
 38+);
 39+
 40+it(takeTests, "take from default channel", async () => {
 41+  function* channelFn() {
 42+    yield* sleep(10);
 43+    yield* put({ type: "action-*" });
 44+    yield* put({ type: "action-1" });
 45+    yield* put({ type: "action-2" });
 46+    yield* put({ type: "unnoticeable-action" });
 47+    yield* put({
 48+      type: "",
 49+      payload: {
 50+        isAction: true,
 51+      },
 52+    });
 53+    yield* put({
 54+      type: "",
 55+      payload: {
 56+        isMixedWithPredicate: true,
 57+      },
 58+    });
 59+    yield* put({
 60+      type: "action-3",
 61+    });
 62+  }
 63+
 64+  const actual: AnyAction[] = [];
 65+  function* genFn() {
 66+    yield* spawn(channelFn);
 67+
 68+    try {
 69+      actual.push(yield* take("*")); // take all actions
 70+      actual.push(yield* take("action-1")); // take only actions of type 'action-1'
 71+      actual.push(yield* take(["action-2", "action-2222"])); // take either type
 72+      actual.push(yield* take((a: AnyAction) => a.payload?.isAction)); // take if match predicate
 73+      actual.push(
 74+        yield* take([
 75+          "action-3",
 76+          (a: AnyAction) => a.payload?.isMixedWithPredicate,
 77+        ]),
 78+      ); // take if match any from the mixed array
 79+      actual.push(
 80+        yield* take([
 81+          "action-3",
 82+          (a: AnyAction) => a.payload?.isMixedWithPredicate,
 83+        ]),
 84+      ); // take if match any from the mixed array
 85+    } finally {
 86+      actual.push({ type: "auto ended" });
 87+    }
 88+  }
 89+
 90+  const store = await configureStore({ initialState: {} });
 91+  await store.run(genFn);
 92+
 93+  const expected = [
 94+    {
 95+      type: "action-*",
 96+    },
 97+    {
 98+      type: "action-1",
 99+    },
100+    {
101+      type: "action-2",
102+    },
103+    {
104+      type: "",
105+      payload: {
106+        isAction: true,
107+      },
108+    },
109+    {
110+      type: "",
111+      payload: {
112+        isMixedWithPredicate: true,
113+      },
114+    },
115+    {
116+      type: "action-3",
117+    },
118+    { type: "auto ended" },
119+  ];
120+  expect(actual).toEqual(expected);
121+});
A store/types.ts
+40, -0
 1@@ -0,0 +1,40 @@
 2+import type { Operation, Patch, Result, Scope, Task } from "../deps.ts";
 3+import type { OpFn } from "../types.ts";
 4+
 5+export type StoreUpdater<S extends AnyState> = (s: S) => S | void;
 6+
 7+export type Listener = () => void;
 8+
 9+export interface UpdaterCtx<S extends AnyState> {
10+  updater: StoreUpdater<S> | StoreUpdater<S>[];
11+  patches: Patch[];
12+}
13+
14+export interface AnyAction {
15+  type: string;
16+  [key: string]: any;
17+}
18+
19+export interface ActionWPayload<P> {
20+  type: string;
21+  payload: P;
22+}
23+
24+export type AnyState = Record<string, any>;
25+
26+declare global {
27+  interface SymbolConstructor {
28+    readonly observable: symbol;
29+  }
30+}
31+
32+export interface FxStore<S extends AnyState> {
33+  getScope: () => Scope;
34+  getState: () => S;
35+  subscribe: (fn: Listener) => () => void;
36+  update: (u: StoreUpdater<S>) => Operation<Result<UpdaterCtx<S>>>;
37+  run: <T>(op: OpFn<T>) => Task<Result<T>>;
38+  dispatch: (a: AnyAction) => Task<any>;
39+  replaceReducer: (r: (s: S, a: AnyAction) => S) => void;
40+  [Symbol.observable]: () => any;
41+}
M test.ts
+6, -16
 1@@ -13,9 +13,12 @@ export {
 2   mockedFetch,
 3 } from "https://deno.land/x/mock_fetch@0.3.0/mod.ts";
 4 
 5-import { contextualize } from "./context.ts";
 6-import { configureStore, createScope } from "./deps.ts";
 7-import { createFxMiddleware } from "./redux/mod.ts";
 8+export const sleep = (n: number) =>
 9+  new Promise<void>((resolve) => {
10+    setTimeout(() => {
11+      resolve();
12+    }, n);
13+  });
14 
15 export function isLikeSelector(selector: unknown) {
16   return (
17@@ -54,16 +57,3 @@ export function assertLike(
18 
19   return comparable;
20 }
21-
22-export function setupReduxScope() {
23-  const scope = createScope();
24-  const mdw = createFxMiddleware(scope);
25-  const store = configureStore({
26-    reducer: () => null,
27-    middleware: [mdw.middleware],
28-  });
29-  scope.run(function* () {
30-    yield* contextualize("redux:store", store);
31-  });
32-  return { run: mdw.run };
33-}
M types.ts
+36, -0
 1@@ -9,3 +9,39 @@ export type OpFn<T = unknown> =
 2   | (() => Operation<T>)
 3   | (() => PromiseLike<T>)
 4   | (() => T);
 5+
 6+export interface QueryState {
 7+  "@@starfx/loaders": Record<IdProp, LoadingItemState>;
 8+  "@@starfx/data": Record<string, unknown>;
 9+}
10+
11+export type IdProp = string | number;
12+export type LoadingStatus = "loading" | "success" | "error" | "idle";
13+export interface LoadingItemState<
14+  M extends Record<IdProp, unknown> = Record<IdProp, unknown>,
15+> {
16+  id: IdProp;
17+  status: LoadingStatus;
18+  message: string;
19+  lastRun: number;
20+  lastSuccess: number;
21+  meta: M;
22+}
23+
24+export interface LoadingState<
25+  M extends Record<IdProp, unknown> = Record<IdProp, unknown>,
26+> extends LoadingItemState<M> {
27+  isIdle: boolean;
28+  isLoading: boolean;
29+  isError: boolean;
30+  isSuccess: boolean;
31+  isInitialLoading: boolean;
32+}
33+
34+export type LoadingPayload =
35+  & Pick<LoadingItemState, "id">
36+  & Partial<Pick<LoadingItemState, "message" | "meta">>;
37+
38+export interface Payload<P = any> {
39+  payload: P;
40+}