repos / starfx

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

commit
a9ff967
parent
2fd2abe
author
Eric Bower
date
2023-05-20 21:48:09 +0000 UTC
feat(query): Data synchronization using a middleware system for front-end apps (#1)

55 files changed,  +6579, -324
D mod.ts
M npm.ts
A .github/workflows/release.yml
+36, -0
 1@@ -0,0 +1,36 @@
 2+name: release to npm
 3+
 4+on:
 5+  release:
 6+    types: [published]
 7+
 8+permissions:
 9+  contents: read
10+
11+jobs:
12+  release:
13+    runs-on: ubuntu-latest
14+    steps:
15+      - name: checkout
16+        uses: actions/checkout@v3
17+      - name: setup deno
18+        uses: denoland/setup-deno@v1
19+        with:
20+          deno-version: v1.x
21+      - name: get version
22+        id: vars
23+        run: echo ::set-output name=version::$(echo ${{github.ref_name}} | sed 's/^v//')
24+      - name: setup node
25+        uses: actions/setup-node@v2
26+        with:
27+          node-version: 18.x
28+          registry-url: https://registry.npmjs.com
29+      - name: build
30+        run: deno task npm $NPM_VERSION
31+        env:
32+          NPM_VERSION: ${{steps.vars.outputs.version}}
33+      - name: publish
34+        run: npm publish --access=public
35+        working-directory: ./build/npm
36+        env:
37+          NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}
M .github/workflows/test.yml
+1, -1
1@@ -28,4 +28,4 @@ jobs:
2         run: deno lint
3 
4       - name: test
5-        run: deno test
6+        run: deno task test
A LICENSE.md
+20, -0
 1@@ -0,0 +1,20 @@
 2+# The MIT License (MIT)
 3+
 4+Copyright © `2023` `Eric Bower`
 5+
 6+Permission is hereby granted, free of charge, to any person obtaining a copy of
 7+this software and associated documentation files (the “Software”), to deal in
 8+the Software without restriction, including without limitation the rights to
 9+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10+the Software, and to permit persons to whom the Software is furnished to do so,
11+subject to the following conditions:
12+
13+The above copyright notice and this permission notice shall be included in all
14+copies or substantial portions of the Software.
15+
16+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
D Makefile
+0, -3
1@@ -1,3 +0,0 @@
2-npm:
3-	NPM_VERSION=0.0.1 deno run --allow-env --allow-read --allow-write --allow-run ./npm.ts
4-.PHONY: npm
A api-type-template.ts
+256, -0
  1@@ -0,0 +1,256 @@
  2+function createSagaQueryApi() {
  3+  const methods = [
  4+    "get",
  5+    "post",
  6+    "put",
  7+    "patch",
  8+    "delete",
  9+    "options",
 10+    "head",
 11+    "connect",
 12+    "trace",
 13+  ];
 14+
 15+  const uriTmpl = (method: string) =>
 16+    `/**
 17+ * Options only
 18+ */
 19+${method}(req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 20+${method}<P>(
 21+  req: { supervisor?: Supervisor }
 22+): CreateActionWithPayload<Omit<Ctx, 'payload'> & Payload<P>, P>;
 23+${method}<P extends never, ApiSuccess, ApiError = unknown>(
 24+  req: { supervisor?: Supervisor }
 25+): CreateAction<Omit<Ctx, 'json'> & FetchJson<ApiSuccess, ApiError>>;
 26+${method}<P, ApiSuccess, ApiError = unknown>(req: {
 27+  supervisor?: Supervisor;
 28+}): CreateActionWithPayload<
 29+  Omit<Ctx, 'payload' | 'json'> &
 30+    Payload<P> &
 31+    FetchJson<ApiSuccess, ApiError>,
 32+  P
 33+>;
 34+
 35+/**
 36+* Middleware only
 37+*/
 38+${method}(fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 39+${method}<Gtx extends Ctx = Ctx>(
 40+  fn: MiddlewareApiCo<Gtx>,
 41+): CreateAction<Gtx>;
 42+${method}<P>(
 43+  fn: MiddlewareApiCo<Omit<Ctx, 'payload'> & Payload<P>>,
 44+): CreateActionWithPayload<Omit<Ctx, 'payload'> & Payload<P>, P>;
 45+${method}<P, Gtx extends Ctx = Ctx>(
 46+  fn: MiddlewareApiCo<Gtx>,
 47+): CreateActionWithPayload<Gtx, P>;
 48+${method}<P extends never, ApiSuccess, ApiError = unknown>(
 49+  fn: MiddlewareApiCo<Omit<Ctx, 'json'> & FetchJson<ApiSuccess, ApiError>>,
 50+): CreateAction<Omit<Ctx, 'json'> & FetchJson<ApiSuccess, ApiError>>;
 51+${method}<P, ApiSuccess, ApiError = unknown>(
 52+  fn: MiddlewareApiCo<Ctx>,
 53+): CreateActionWithPayload<
 54+  Omit<Ctx, 'payload' | 'json'> &
 55+    Payload<P> &
 56+    FetchJson<ApiSuccess, ApiError>,
 57+  P
 58+>;
 59+
 60+/**
 61+* Options and Middleware
 62+*/
 63+${method}(req: { supervisor?: Supervisor }, fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 64+${method}<Gtx extends Ctx = Ctx>(
 65+  req: { supervisor?: Supervisor },
 66+  fn: MiddlewareApiCo<Gtx>,
 67+): CreateAction<Gtx>;
 68+${method}<P>(
 69+  req: { supervisor?: Supervisor },
 70+  fn: MiddlewareApiCo<Omit<Ctx, 'payload'> & Payload<P>>,
 71+): CreateActionWithPayload<Omit<Ctx, 'payload'> & Payload<P>, P>;
 72+${method}<P, Gtx extends Ctx = Ctx>(
 73+  req: { supervisor?: Supervisor },
 74+  fn: MiddlewareApiCo<Gtx>,
 75+): CreateActionWithPayload<Gtx, P>;
 76+${method}<P extends never, ApiSuccess, ApiError = unknown>(
 77+  req: { supervisor?: Supervisor },
 78+  fn: MiddlewareApiCo<Omit<Ctx, 'json'> & FetchJson<ApiSuccess, ApiError>>,
 79+): CreateAction<Omit<Ctx, 'json'> & FetchJson<ApiSuccess, ApiError>>;
 80+${method}<P, ApiSuccess, ApiError = unknown>(
 81+  req: { supervisor?: Supervisor },
 82+  fn: MiddlewareApiCo<Ctx>,
 83+): CreateActionWithPayload<
 84+  Omit<Ctx, 'payload' | 'json'> &
 85+    Payload<P> &
 86+    FetchJson<ApiSuccess, ApiError>,
 87+  P
 88+>;`;
 89+  const uriMethods = methods.map((m) => uriTmpl(m)).join("\n\n");
 90+
 91+  const methodTmpl = (method: string) =>
 92+    `/**
 93+ * Only name
 94+ */
 95+${method}(name: ApiName): CreateAction<Ctx>;
 96+${method}<P>(
 97+  name: ApiName,
 98+): CreateActionWithPayload<Omit<Ctx, 'payload'> & Payload<P>, P>;
 99+${method}<P extends never, ApiSuccess, ApiError = unknown>(
100+  name: ApiName,
101+): CreateAction<Omit<Ctx, 'json'> & FetchJson<ApiSuccess, ApiError>>;
102+${method}<P, ApiSuccess, ApiError = unknown>(
103+  name: ApiName,
104+): CreateActionWithPayload<
105+  Omit<Ctx, 'payload' | 'json'> &
106+    Payload<P> &
107+    FetchJson<ApiSuccess, ApiError>,
108+  P
109+>;
110+
111+/**
112+ * Name and options
113+ */
114+${method}(name: ApiName, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
115+${method}<P>(
116+  name: ApiName,
117+  req: { supervisor?: Supervisor }
118+): CreateActionWithPayload<Omit<Ctx, 'payload'> & Payload<P>, P>;
119+${method}<P, Gtx extends Ctx = Ctx>(
120+  name: ApiName,
121+  req: { supervisor?: Supervisor }
122+): CreateActionWithPayload<Gtx, P>;
123+${method}<P extends never, ApiSuccess, ApiError = unknown>(
124+  name: ApiName,
125+  req: { supervisor?: Supervisor }
126+): CreateAction<Omit<Ctx, 'json'> & FetchJson<ApiSuccess, ApiError>>;
127+${method}<P, ApiSuccess, ApiError = unknown>(
128+  name: ApiName,
129+  req: { supervisor?: Supervisor },
130+): CreateActionWithPayload<
131+  Omit<Ctx, 'payload' | 'json'> &
132+    Payload<P> &
133+    FetchJson<ApiSuccess, ApiError>,
134+  P
135+>;
136+
137+/**
138+ * Name and middleware
139+ */
140+${method}(name: ApiName, fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
141+${method}<Gtx extends Ctx = Ctx>(
142+  name: ApiName,
143+  fn: MiddlewareApiCo<Gtx>,
144+): CreateAction<Gtx>;
145+${method}<P>(
146+  name: ApiName,
147+  fn: MiddlewareApiCo<Omit<Ctx, 'payload'> & Payload<P>>,
148+): CreateActionWithPayload<Omit<Ctx, 'payload'> & Payload<P>, P>;
149+${method}<P, Gtx extends Ctx = Ctx>(
150+  name: ApiName,
151+  fn: MiddlewareApiCo<Gtx>,
152+): CreateActionWithPayload<Gtx, P>;
153+${method}<P extends never, ApiSuccess, ApiError = unknown>(
154+  name: ApiName,
155+  fn: MiddlewareApiCo<Omit<Ctx, 'json'> & FetchJson<ApiSuccess, ApiError>>,
156+): CreateAction<Omit<Ctx, 'json'> & FetchJson<ApiSuccess, ApiError>>;
157+${method}<P, ApiSuccess, ApiError = unknown>(
158+  name: ApiName,
159+  fn: MiddlewareApiCo<
160+    Omit<Ctx, 'payload' | 'json'> &
161+      Payload<P> &
162+      FetchJson<ApiSuccess, ApiError>
163+  >,
164+): CreateActionWithPayload<
165+  Omit<Ctx, 'payload' | 'json'> &
166+    Payload<P> &
167+    FetchJson<ApiSuccess, ApiError>,
168+  P
169+>;
170+
171+/**
172+ * Name, options, and middleware
173+ */
174+${method}(
175+  name: ApiName,
176+  req: { supervisor?: Supervisor },
177+  fn: MiddlewareApiCo<Ctx>,
178+): CreateAction<Ctx>;
179+${method}<Gtx extends Ctx = Ctx>(
180+  name: ApiName,
181+  req: { supervisor?: Supervisor },
182+  fn: MiddlewareApiCo<Gtx>,
183+): CreateAction<Gtx>;
184+${method}<P>(
185+  name: ApiName,
186+  req: { supervisor?: Supervisor },
187+  fn: MiddlewareApiCo<Omit<Ctx, 'payload'> & Payload<P>>,
188+): CreateActionWithPayload<Omit<Ctx, 'payload'> & Payload<P>, P>;
189+${method}<P, Gtx extends Ctx = Ctx>(
190+  name: ApiName,
191+  req: { supervisor?: Supervisor },
192+  fn: MiddlewareApiCo<Gtx>,
193+): CreateActionWithPayload<Gtx, P>;
194+${method}<P extends never, ApiSuccess, ApiError = unknown>(
195+  name: ApiName,
196+  req: { supervisor?: Supervisor },
197+  fn: MiddlewareApiCo<Omit<Ctx, 'json'> & FetchJson<ApiSuccess, ApiError>>,
198+): CreateAction<Omit<Ctx, 'json'> & FetchJson<ApiSuccess, ApiError>>;
199+${method}<P, ApiSuccess, ApiError = unknown>(
200+  name: ApiName,
201+  req: { supervisor?: Supervisor },
202+  fn: MiddlewareApiCo<
203+    Omit<Ctx, 'payload' | 'json'> &
204+      Payload<P> &
205+      FetchJson<ApiSuccess, ApiError>
206+  >,
207+): CreateActionWithPayload<
208+  Omit<Ctx, 'payload' | 'json'> &
209+    Payload<P> &
210+    FetchJson<ApiSuccess, ApiError>,
211+  P
212+>;`;
213+  const regMethods = methods.map((m) => methodTmpl(m)).join("\n\n");
214+
215+  const tmpl = `/**
216+* This is an auto-generated file, do not edit directly!
217+* Run "yarn template" to generate this file.
218+*/
219+import type { SagaApi } from "./pipe.ts";
220+import type {
221+  ApiCtx,
222+  CreateAction,
223+  CreateActionWithPayload,
224+  Next,
225+  FetchJson,
226+  MiddlewareApiCo,
227+  Payload,
228+  Supervisor,
229+} from "./types.ts";
230+
231+export type ApiName = string | string[];
232+
233+export interface SagaQueryApi<Ctx extends ApiCtx = ApiCtx> extends SagaApi<Ctx> {
234+  request: (
235+    r: Partial<RequestInit>,
236+  ) => (ctx: Ctx, next: Next) => Iterator<unknown>;
237+  cache: () => (ctx: Ctx, next: Next) => Iterator<unknown>;
238+
239+  uri: (uri: string) => {
240+    ${uriMethods}
241+  }
242+
243+${regMethods}
244+}`;
245+
246+  return tmpl;
247+}
248+
249+async function createTemplateFile(tmpl: string) {
250+  try {
251+    await Deno.writeTextFile("./query/api-types.ts", tmpl);
252+  } catch (err) {
253+    console.error(err);
254+  }
255+}
256+
257+createTemplateFile(createSagaQueryApi()).then(console.log).catch(console.error);
R test/compose.test.ts => compose.test.ts
+3, -3
 1@@ -1,7 +1,7 @@
 2-import { describe, expect, it } from "../test.ts";
 3+import { describe, expect, it } from "./test.ts";
 4 
 5-import { run, sleep } from "../deps.ts";
 6-import { compose } from "../compose.ts";
 7+import { run, sleep } from "./deps.ts";
 8+import { compose } from "./compose.ts";
 9 
10 const tests = describe("compose()");
11 
M compose.ts
+1, -1
1@@ -1,4 +1,4 @@
2-import { call } from "./fx/mod.ts";
3+import { call } from "./fx/index.ts";
4 import type { Instruction, Result } from "./deps.ts";
5 import { Err } from "./deps.ts";
6 
M context.ts
+1, -7
 1@@ -1,10 +1,4 @@
 2-import { createChannel, createContext, getframe } from "./deps.ts";
 3-import type { Channel } from "./deps.ts";
 4-
 5-export const ErrContext = createContext<Channel<Error, void>>(
 6-  "fx:err",
 7-  createChannel<Error, void>(),
 8-);
 9+import { getframe } from "./deps.ts";
10 
11 export function* contextualize(context: string, value: unknown) {
12   const frame = yield* getframe();
M deno.json
+7, -6
 1@@ -1,16 +1,17 @@
 2 {
 3+  "tasks": {
 4+    "types": "deno run --allow-write ./api-type-template.ts",
 5+    "npm": "deno run --allow-write --allow-read --allow-env --allow-run --allow-net --no-check ./npm.ts",
 6+    "test": "deno test --allow-env --allow-read"
 7+  },
 8   "lint": {
 9-    "files": {
10-      "exclude": ["npm/", "examples/"]
11-    },
12+    "exclude": ["npm/", "examples/"],
13     "rules": {
14       "tags": ["recommended"],
15       "exclude": ["no-explicit-any", "require-yield"]
16     }
17   },
18   "fmt": {
19-    "files": {
20-      "exclude": ["npm/", "examples/"]
21-    }
22+    "exclude": ["npm/", "examples/"]
23   }
24 }
M deno.lock
+290, -190
  1@@ -1,60 +1,28 @@
  2 {
  3   "version": "2",
  4   "remote": {
  5-    "https://deno.land/std@0.106.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58",
  6-    "https://deno.land/std@0.106.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac",
  7-    "https://deno.land/std@0.106.0/fs/_util.ts": "f2ce811350236ea8c28450ed822a5f42a0892316515b1cd61321dec13569c56b",
  8-    "https://deno.land/std@0.106.0/fs/ensure_dir.ts": "b7c103dc41a3d1dbbb522bf183c519c37065fdc234831a4a0f7d671b1ed5fea7",
  9-    "https://deno.land/std@0.106.0/fs/expand_glob.ts": "73e7b13f01097b04ed782b3d63863379b718417417758ba622e282b1e5300b91",
 10-    "https://deno.land/std@0.106.0/fs/walk.ts": "b91c655c60d048035f9cae0e6177991ab3245e786e3ab7d20a5b60012edf2126",
 11-    "https://deno.land/std@0.106.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853",
 12-    "https://deno.land/std@0.106.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4",
 13-    "https://deno.land/std@0.106.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b",
 14-    "https://deno.land/std@0.106.0/path/common.ts": "eaf03d08b569e8a87e674e4e265e099f237472b6fd135b3cbeae5827035ea14a",
 15-    "https://deno.land/std@0.106.0/path/glob.ts": "3b84af55c53febacf6afe214c095624b22a56b6f57d7312157479cc783a0de65",
 16-    "https://deno.land/std@0.106.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12",
 17-    "https://deno.land/std@0.106.0/path/posix.ts": "b81974c768d298f8dcd2c720229639b3803ca4a241fa9a355c762fa2bc5ef0c1",
 18-    "https://deno.land/std@0.106.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c",
 19-    "https://deno.land/std@0.106.0/path/win32.ts": "f4a3d4a3f2c9fe894da046d5eac48b5e789a0ebec5152b2c0985efe96a9f7ae1",
 20-    "https://deno.land/std@0.111.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58",
 21-    "https://deno.land/std@0.111.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac",
 22-    "https://deno.land/std@0.111.0/bytes/bytes_list.ts": "3bff6a09c72b2e0b1e92e29bd3b135053894196cca07a2bba842901073efe5cb",
 23-    "https://deno.land/std@0.111.0/bytes/equals.ts": "69f55fdbd45c71f920c1a621e6c0865dc780cd8ae34e0f5e55a9497b70c31c1b",
 24-    "https://deno.land/std@0.111.0/bytes/mod.ts": "fedb80b8da2e7ad8dd251148e65f92a04c73d6c5a430b7d197dc39588c8dda6f",
 25-    "https://deno.land/std@0.111.0/fmt/colors.ts": "8368ddf2d48dfe413ffd04cdbb7ae6a1009cf0dccc9c7ff1d76259d9c61a0621",
 26-    "https://deno.land/std@0.111.0/fs/_util.ts": "f2ce811350236ea8c28450ed822a5f42a0892316515b1cd61321dec13569c56b",
 27-    "https://deno.land/std@0.111.0/fs/ensure_dir.ts": "b7c103dc41a3d1dbbb522bf183c519c37065fdc234831a4a0f7d671b1ed5fea7",
 28-    "https://deno.land/std@0.111.0/hash/sha256.ts": "bd85257c68d1fdd9da8457284c4fbb04efa9f4f2229b5f41a638d5b71a3a8d5c",
 29-    "https://deno.land/std@0.111.0/io/buffer.ts": "fdf93ba9e5d20ff3369e2c42443efd89131f8a73066f7f59c033cc588a0e2cfe",
 30-    "https://deno.land/std@0.111.0/io/types.d.ts": "89a27569399d380246ca7cdd9e14d5e68459f11fb6110790cc5ecbd4ee7f3215",
 31-    "https://deno.land/std@0.111.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853",
 32-    "https://deno.land/std@0.111.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4",
 33-    "https://deno.land/std@0.111.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b",
 34-    "https://deno.land/std@0.111.0/path/common.ts": "f41a38a0719a1e85aa11c6ba3bea5e37c15dd009d705bd8873f94c833568cbc4",
 35-    "https://deno.land/std@0.111.0/path/glob.ts": "ea87985765b977cc284b92771003b2070c440e0807c90e1eb0ff3e095911a820",
 36-    "https://deno.land/std@0.111.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12",
 37-    "https://deno.land/std@0.111.0/path/posix.ts": "34349174b9cd121625a2810837a82dd8b986bbaaad5ade690d1de75bbb4555b2",
 38-    "https://deno.land/std@0.111.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c",
 39-    "https://deno.land/std@0.111.0/path/win32.ts": "11549e8c6df8307a8efcfa47ad7b2a75da743eac7d4c89c9723a944661c8bd2e",
 40-    "https://deno.land/std@0.111.0/streams/conversion.ts": "fe0059ed9d3c53eda4ba44eb71a6a9acb98c5fdb5ba1b6c6ab28004724c7641b",
 41-    "https://deno.land/std@0.119.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58",
 42-    "https://deno.land/std@0.119.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac",
 43-    "https://deno.land/std@0.119.0/fmt/colors.ts": "8368ddf2d48dfe413ffd04cdbb7ae6a1009cf0dccc9c7ff1d76259d9c61a0621",
 44-    "https://deno.land/std@0.119.0/fs/empty_dir.ts": "5f08b263dd064dc7917c4bbeb13de0f5505a664b9cdfe312fa86e7518cfaeb84",
 45-    "https://deno.land/std@0.119.0/fs/expand_glob.ts": "f6f64ef54addcc84c9012d6dfcf66d39ecc2565e40c4d2b7644d585fe4d12a04",
 46-    "https://deno.land/std@0.119.0/fs/walk.ts": "31464d75099aa3fc7764212576a8772dfabb2692783e6eabb910f874a26eac54",
 47-    "https://deno.land/std@0.119.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853",
 48-    "https://deno.land/std@0.119.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4",
 49-    "https://deno.land/std@0.119.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b",
 50-    "https://deno.land/std@0.119.0/path/common.ts": "f41a38a0719a1e85aa11c6ba3bea5e37c15dd009d705bd8873f94c833568cbc4",
 51-    "https://deno.land/std@0.119.0/path/glob.ts": "ea87985765b977cc284b92771003b2070c440e0807c90e1eb0ff3e095911a820",
 52-    "https://deno.land/std@0.119.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12",
 53-    "https://deno.land/std@0.119.0/path/posix.ts": "34349174b9cd121625a2810837a82dd8b986bbaaad5ade690d1de75bbb4555b2",
 54-    "https://deno.land/std@0.119.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c",
 55-    "https://deno.land/std@0.119.0/path/win32.ts": "11549e8c6df8307a8efcfa47ad7b2a75da743eac7d4c89c9723a944661c8bd2e",
 56-    "https://deno.land/std@0.129.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37",
 57-    "https://deno.land/std@0.129.0/testing/_diff.ts": "9d849cd6877694152e01775b2d93f9d6b7aef7e24bfe3bfafc4d7a1ac8e9f392",
 58-    "https://deno.land/std@0.129.0/testing/asserts.ts": "0a95d9e8076dd3e7f0eeb605a67c148078b4b11f4abcd5eef115b0361b0736a2",
 59+    "https://crux.land/api/get/2KNRVU.ts": "6a77d55844aba78d01520c5ff0b2f0af7f24cc1716a0de8b3bb6bd918c47b5ba",
 60+    "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
 61+    "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49",
 62+    "https://deno.land/std@0.140.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d",
 63+    "https://deno.land/std@0.140.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9",
 64+    "https://deno.land/std@0.140.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf",
 65+    "https://deno.land/std@0.140.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37",
 66+    "https://deno.land/std@0.140.0/fs/_util.ts": "0fb24eb4bfebc2c194fb1afdb42b9c3dda12e368f43e8f2321f84fc77d42cb0f",
 67+    "https://deno.land/std@0.140.0/fs/ensure_dir.ts": "9dc109c27df4098b9fc12d949612ae5c9c7169507660dcf9ad90631833209d9d",
 68+    "https://deno.land/std@0.140.0/hash/sha256.ts": "803846c7a5a8a5a97f31defeb37d72f519086c880837129934f5d6f72102a8e8",
 69+    "https://deno.land/std@0.140.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b",
 70+    "https://deno.land/std@0.140.0/io/types.d.ts": "01f60ae7ec02675b5dbed150d258fc184a78dfe5c209ef53ba4422b46b58822c",
 71+    "https://deno.land/std@0.140.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3",
 72+    "https://deno.land/std@0.140.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09",
 73+    "https://deno.land/std@0.140.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b",
 74+    "https://deno.land/std@0.140.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633",
 75+    "https://deno.land/std@0.140.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee",
 76+    "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d",
 77+    "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44",
 78+    "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9",
 79+    "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757",
 80+    "https://deno.land/std@0.140.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21",
 81     "https://deno.land/std@0.158.0/fmt/colors.ts": "ff7dc9c9f33a72bd48bc24b21bbc1b4545d8494a431f17894dbc5fe92a938fc4",
 82     "https://deno.land/std@0.158.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c",
 83     "https://deno.land/std@0.158.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832",
 84@@ -187,46 +155,82 @@
 85     "https://deno.land/std@0.177.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
 86     "https://deno.land/std@0.177.0/streams/write_all.ts": "3b2e1ce44913f966348ce353d02fa5369e94115181037cd8b602510853ec3033",
 87     "https://deno.land/std@0.177.0/types.d.ts": "220ed56662a0bd393ba5d124aa6ae2ad36a00d2fcbc0e8666a65f4606aaa9784",
 88+    "https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
 89+    "https://deno.land/std@0.181.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
 90+    "https://deno.land/std@0.181.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32",
 91+    "https://deno.land/std@0.181.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40",
 92+    "https://deno.land/std@0.181.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a",
 93+    "https://deno.land/std@0.181.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32",
 94+    "https://deno.land/std@0.181.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
 95+    "https://deno.land/std@0.181.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
 96+    "https://deno.land/std@0.181.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
 97+    "https://deno.land/std@0.181.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
 98+    "https://deno.land/std@0.181.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
 99+    "https://deno.land/std@0.181.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c",
100+    "https://deno.land/std@0.181.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
101+    "https://deno.land/std@0.181.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
102+    "https://deno.land/std@0.181.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
103+    "https://deno.land/std@0.182.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
104+    "https://deno.land/std@0.182.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
105+    "https://deno.land/std@0.182.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
106+    "https://deno.land/std@0.182.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32",
107+    "https://deno.land/std@0.182.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688",
108+    "https://deno.land/std@0.182.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a",
109+    "https://deno.land/std@0.182.0/fs/walk.ts": "920be35a7376db6c0b5b1caf1486fb962925e38c9825f90367f8f26b5e5d0897",
110+    "https://deno.land/std@0.182.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
111+    "https://deno.land/std@0.182.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
112+    "https://deno.land/std@0.182.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
113+    "https://deno.land/std@0.182.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
114+    "https://deno.land/std@0.182.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
115+    "https://deno.land/std@0.182.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c",
116+    "https://deno.land/std@0.182.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
117+    "https://deno.land/std@0.182.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
118+    "https://deno.land/std@0.182.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
119+    "https://deno.land/std@0.185.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
120+    "https://deno.land/std@0.185.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea",
121+    "https://deno.land/std@0.185.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
122+    "https://deno.land/std@0.185.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f",
123+    "https://deno.land/std@0.187.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
124+    "https://deno.land/std@0.187.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea",
125+    "https://deno.land/std@0.187.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
126+    "https://deno.land/std@0.187.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f",
127     "https://deno.land/std@0.97.0/fmt/colors.ts": "db22b314a2ae9430ae7460ce005e0a7130e23ae1c999157e3bb77cf55800f7e4",
128     "https://deno.land/std@0.97.0/testing/_diff.ts": "961eaf6d9f5b0a8556c9d835bbc6fa74f5addd7d3b02728ba7936ff93364f7a3",
129     "https://deno.land/std@0.97.0/testing/asserts.ts": "341292d12eebc44be4c3c2ca101ba8b6b5859cef2fa69d50c217f9d0bfbcfd1f",
130-    "https://deno.land/x/code_block_writer@11.0.0/comment_char.ts": "22b66890bbdf7a2d59777ffd8231710c1fda1c11fadada67632a596937a1a314",
131-    "https://deno.land/x/code_block_writer@11.0.0/mod.ts": "dc43d56c3487bae02886a09754fb09c607da4ea866817e80f3e60632f3391d70",
132-    "https://deno.land/x/code_block_writer@11.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff",
133+    "https://deno.land/x/code_block_writer@12.0.0/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5",
134+    "https://deno.land/x/code_block_writer@12.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff",
135     "https://deno.land/x/continuation@0.1.5/mod.ts": "690def2735046367b3e1b4bc6e51b5912f2ed09c41c7df7a55c060f23720ad33",
136-    "https://deno.land/x/deno_cache@0.2.1/auth_tokens.ts": "01b94d25abd974153a3111653998b9a43c66d84a0e4b362fc5f4bbbf40a6e0f7",
137-    "https://deno.land/x/deno_cache@0.2.1/cache.ts": "67e301c20161546fea45405316314f4c3d85cc7a367b2fb72042903f308f55b7",
138-    "https://deno.land/x/deno_cache@0.2.1/deno_dir.ts": "e4dc68da5641aa337bcc06fb1df28fcb086b366dcbea7d8aaed7ac7c853fedb1",
139-    "https://deno.land/x/deno_cache@0.2.1/deps.ts": "2ebaba0ad86fff8b9027c6afd4c3909a17cd8bf8c9e263151c980c15c56a18ee",
140-    "https://deno.land/x/deno_cache@0.2.1/dirs.ts": "e07003fabed7112375d4a50040297aae768f9d06bb6c2655ca46880653b576b4",
141-    "https://deno.land/x/deno_cache@0.2.1/disk_cache.ts": "d7a361f0683a032bcca28513a7bbedc28c77cfcc6719e6f6cea156c0ff1108df",
142-    "https://deno.land/x/deno_cache@0.2.1/file_fetcher.ts": "352702994c190c45215f3b8086621e117e88bc2174b020faefb5eca653d71d6a",
143-    "https://deno.land/x/deno_cache@0.2.1/http_cache.ts": "af1500149496e2d0acadec24569e2a9c86a3f600cceef045dcf6f5ce8de72b3a",
144-    "https://deno.land/x/deno_cache@0.2.1/mod.ts": "709ab9d1068be5fd77b020b33e7a9394f1e9b453553b1e2336b72c90283cf3c0",
145-    "https://deno.land/x/deno_cache@0.2.1/util.ts": "652479928551259731686686ff2df6f26bc04e8e4d311137b2bf3bc10f779f48",
146-    "https://deno.land/x/deno_graph@0.6.0/lib/deno_graph.generated.js": "3e1cccd6376d4ad0ea789d66aa0f6b19f737fa8da37b5e6185ef5c269c974f54",
147-    "https://deno.land/x/deno_graph@0.6.0/lib/loader.ts": "13a11c1dea0d85e0ad211be77217b8c06138bbb916afef6f50a04cca415084a9",
148-    "https://deno.land/x/deno_graph@0.6.0/lib/media_type.ts": "36be751aa63d6ae36475b90dca5fae8fd7c3a77cf13684c48cf23a85ee607b31",
149-    "https://deno.land/x/deno_graph@0.6.0/lib/snippets/deno_graph-1c138d6136337537/src/deno_apis.js": "f13f2678d875372cf8489ceb7124623a39fa5bf8de8ee1ec722dbb2ec5ec7845",
150-    "https://deno.land/x/deno_graph@0.6.0/lib/types.d.ts": "68cb232e02a984658b40ffaf6cafb979a06fbfdce7f5bd4c7a83ed1a32a07687",
151-    "https://deno.land/x/deno_graph@0.6.0/mod.ts": "8fe3d39bdcb273adfb41a0bafbbaabec4c6fe6c611b47fed8f46f218edb37e8e",
152-    "https://deno.land/x/dnt@0.17.0/lib/compiler.ts": "b741c149adaa3ed87877324ce4365fbb66e8ecbfb5bc417495dcb1ac2936f9ff",
153-    "https://deno.land/x/dnt@0.17.0/lib/compiler_transforms.ts": "b14a398776f08d57a3a011c8c4b5a6a557032c4b125264f86f7877d3f0eef657",
154-    "https://deno.land/x/dnt@0.17.0/lib/mod.deps.ts": "d6c6eb09013c471584bc271261ed896d9f0f6552620ac7d1d65cb46e57841236",
155-    "https://deno.land/x/dnt@0.17.0/lib/npm_ignore.ts": "36fe32008cd71e995bc08569d2b43e8fba816cbada82fa37d1fe52358d5a2e17",
156-    "https://deno.land/x/dnt@0.17.0/lib/package_json.ts": "c8ea56c948fa2379de2a1cf76d335d1b1c70a8c122e1d11932a84f4b70905275",
157-    "https://deno.land/x/dnt@0.17.0/lib/pkg/dnt_wasm.js": "fc7ae54f777566ef7769825491cb868da9f92ec4453326c075ce1b3b2bdf06f3",
158-    "https://deno.land/x/dnt@0.17.0/lib/pkg/dnt_wasm_bg.ts": "51742660365970445ca1e010d340085c7b1188166cc7cdb0d2ba72b3328f276d",
159-    "https://deno.land/x/dnt@0.17.0/lib/pkg/snippets/dnt-wasm-a15ef721fa5290c5/helpers.js": "2f623f83602d4fbb30caa63444b10e35b45e9c2b267e49585ec9bb790a4888d8",
160-    "https://deno.land/x/dnt@0.17.0/lib/shims.ts": "f9f7defa1cadaec4f04f47ac62757ffa47f83b4a5825fe68cbbfcf14086fcce7",
161-    "https://deno.land/x/dnt@0.17.0/lib/test_runner/get_test_runner_code.ts": "5fe5543c8479b5f17c58db4d994de3f3d573e3ca7e4c32c7cf8e338e8e900ba7",
162-    "https://deno.land/x/dnt@0.17.0/lib/test_runner/test_runner.ts": "fc93277907a49bdc098237619da7ff6898547f6f25b71c026375a81efbc8907b",
163-    "https://deno.land/x/dnt@0.17.0/lib/transform.deps.ts": "0b86d5ad77af5b53ad72308fbf5b694abea1f0593652a662be7529ae60c540cb",
164-    "https://deno.land/x/dnt@0.17.0/lib/types.ts": "8506b5ced3921a6ac2a1d5a2bb381bfdbf818c68207f14a1a1fffbf48ee95886",
165-    "https://deno.land/x/dnt@0.17.0/lib/utils.ts": "d2681d634dfa6bd4ad2a32ad15bd419f6f1f895e06c0bf479455fbf1c5f49cd9",
166-    "https://deno.land/x/dnt@0.17.0/mod.ts": "1cd16a64fa1861d624d6e9e02eaa0dfa733f8e2101686fb90e05bbb96ce5221d",
167-    "https://deno.land/x/dnt@0.17.0/transform.ts": "0b1dc1fdf860b594fb50a3aee54bc8c22220f4601f1d227f4113b9b4c0c5166f",
168-    "https://deno.land/x/effection@3.0.0-alpha.6/mod.ts": "b443a1e17d89f59ad847b1d1efc90465f81891d8e55c04c063d0c1c6c80dd7ed",
169+    "https://deno.land/x/deno_cache@0.4.1/auth_tokens.ts": "5fee7e9155e78cedf3f6ff3efacffdb76ac1a76c86978658d9066d4fb0f7326e",
170+    "https://deno.land/x/deno_cache@0.4.1/cache.ts": "51f72f4299411193d780faac8c09d4e8cbee951f541121ef75fcc0e94e64c195",
171+    "https://deno.land/x/deno_cache@0.4.1/deno_dir.ts": "f2a9044ce8c7fe1109004cda6be96bf98b08f478ce77e7a07f866eff1bdd933f",
172+    "https://deno.land/x/deno_cache@0.4.1/deps.ts": "8974097d6c17e65d9a82d39377ae8af7d94d74c25c0cbb5855d2920e063f2343",
173+    "https://deno.land/x/deno_cache@0.4.1/dirs.ts": "d2fa473ef490a74f2dcb5abb4b9ab92a48d2b5b6320875df2dee64851fa64aa9",
174+    "https://deno.land/x/deno_cache@0.4.1/disk_cache.ts": "1f3f5232cba4c56412d93bdb324c624e95d5dd179d0578d2121e3ccdf55539f9",
175+    "https://deno.land/x/deno_cache@0.4.1/file_fetcher.ts": "07a6c5f8fd94bf50a116278cc6012b4921c70d2251d98ce1c9f3c352135c39f7",
176+    "https://deno.land/x/deno_cache@0.4.1/http_cache.ts": "f632e0d6ec4a5d61ae3987737a72caf5fcdb93670d21032ddb78df41131360cd",
177+    "https://deno.land/x/deno_cache@0.4.1/mod.ts": "ef1cda9235a93b89cb175fe648372fc0f785add2a43aa29126567a05e3e36195",
178+    "https://deno.land/x/deno_cache@0.4.1/util.ts": "8cb686526f4be5205b92c819ca2ce82220aa0a8dd3613ef0913f6dc269dbbcfe",
179+    "https://deno.land/x/deno_graph@0.26.0/lib/deno_graph.generated.js": "2f7ca85b2ceb80ec4b3d1b7f3a504956083258610c7b9a1246238c5b7c68f62d",
180+    "https://deno.land/x/deno_graph@0.26.0/lib/loader.ts": "380e37e71d0649eb50176a9786795988fc3c47063a520a54b616d7727b0f8629",
181+    "https://deno.land/x/deno_graph@0.26.0/lib/media_type.ts": "222626d524fa2f9ebcc0ec7c7a7d5dfc74cc401cc46790f7c5e0eab0b0787707",
182+    "https://deno.land/x/deno_graph@0.26.0/lib/snippets/deno_graph-de651bc9c240ed8d/src/deno_apis.js": "41192baaa550a5c6a146280fae358cede917ae16ec4e4315be51bef6631ca892",
183+    "https://deno.land/x/deno_graph@0.26.0/lib/types.d.ts": "2bbdbf895321d1df8db511fab00160a0211c09c2e7cac56c522dd6e9ed6d2ef7",
184+    "https://deno.land/x/deno_graph@0.26.0/mod.ts": "11131ae166580a1c7fa8506ff553751465a81c263d94443f18f353d0c320bc14",
185+    "https://deno.land/x/dnt@0.35.0/lib/compiler.ts": "3259975196f44391525e4e3e96c1f95c3a90dd25ec31a1948ccd7c6b71323d76",
186+    "https://deno.land/x/dnt@0.35.0/lib/compiler_transforms.ts": "cbb1fd5948f5ced1aa5c5aed9e45134e2357ce1e7220924c1d7bded30dcd0dd0",
187+    "https://deno.land/x/dnt@0.35.0/lib/mod.deps.ts": "30367fc68bcd2acf3b7020cf5cdd26f817f7ac9ac35c4bfb6c4551475f91bc3e",
188+    "https://deno.land/x/dnt@0.35.0/lib/npm_ignore.ts": "b430caa1905b65ae89b119d84857b3ccc3cb783a53fc083d1970e442f791721d",
189+    "https://deno.land/x/dnt@0.35.0/lib/package_json.ts": "2d629dbaef8004971e38ce3661f04b915a35342b905c3d98ff4a25343c2a8293",
190+    "https://deno.land/x/dnt@0.35.0/lib/pkg/dnt_wasm.generated.js": "ad5c205f018b2bc6258d00d6a0539c2ffa94275f16f106f0f072bcf77f3c786b",
191+    "https://deno.land/x/dnt@0.35.0/lib/pkg/snippets/dnt-wasm-a15ef721fa5290c5/helpers.js": "a6b95adc943a68d513fe8ed9ec7d260ac466b7a4bced4e942f733e494bb9f1be",
192+    "https://deno.land/x/dnt@0.35.0/lib/shims.ts": "d12e2c3c70eab1b0b95c46f35771f9798d7be7b3b6d0af6b82cad068d26d746f",
193+    "https://deno.land/x/dnt@0.35.0/lib/test_runner/get_test_runner_code.ts": "2a4e26aa33120f3cc9e03b8538211a5047a4bad4c64e895944b87f2dcd55d904",
194+    "https://deno.land/x/dnt@0.35.0/lib/test_runner/test_runner.ts": "b91d77d9d4b82984cb2ba7431ba6935756ba72f62e7dd4db22cd47a680ebd952",
195+    "https://deno.land/x/dnt@0.35.0/lib/transform.deps.ts": "e42f2bdef46d098453bdba19261a67cf90b583f5d868f7fe83113c1380d9b85c",
196+    "https://deno.land/x/dnt@0.35.0/lib/types.ts": "5735f10d9f1ff741dd3a5518b46365a38bc6e037fc0996d5bcaf2d6e8c2f805b",
197+    "https://deno.land/x/dnt@0.35.0/lib/utils.ts": "878b7ac7003a10c16e6061aa49dbef9b42bd43174853ebffc9b67ea47eeb11d8",
198+    "https://deno.land/x/dnt@0.35.0/mod.ts": "3ee53f4d4d41df72e57ecbca9f3c2b7cf86166ef57fa04452865780f83c555a9",
199+    "https://deno.land/x/dnt@0.35.0/transform.ts": "1b127c5f22699c8ab2545b98aeca38c4e5c21405b0f5342ea17e9c46280ed277",
200     "https://deno.land/x/effection@3.0.0-alpha.7/lib/abort-signal.ts": "31224b5f61f1ef58a2acf72bfc1cb9c9bf589f15c263b6555eb06f1b49f77b54",
201     "https://deno.land/x/effection@3.0.0-alpha.7/lib/async.ts": "f12c5d8f4b21d9b6281dec505cbd40455144017f1087d637cecddb6b41d978c1",
202     "https://deno.land/x/effection@3.0.0-alpha.7/lib/call.ts": "69c465573031e6315e375c17e01e820239e01a93107866dd9f5ef584b79d13dd",
203@@ -253,106 +257,202 @@
204     "https://deno.land/x/expect@v0.3.0/matchers.ts": "a37ef4577739247af77a852cdcd69484f999a41ad86ec16bb63a88a7a47a2372",
205     "https://deno.land/x/expect@v0.3.0/mock.ts": "562d4b1d735d15b0b8e935f342679096b64fe452f86e96714fe8616c0c884914",
206     "https://deno.land/x/expect@v0.3.0/mod.ts": "0304d2430e1e96ba669a8495e24ba606dcc3d152e1f81aaa8da898cea24e36c2",
207-    "https://deno.land/x/ts_morph@13.0.0/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9",
208-    "https://deno.land/x/ts_morph@13.0.0/bootstrap/ts_morph_bootstrap.d.ts": "1dd46307a28a8e689ce214cb0fe260a280e7ecbaceb6fbae3cbcaa8a25a9fd3f",
209-    "https://deno.land/x/ts_morph@13.0.0/bootstrap/ts_morph_bootstrap.js": "5b0c39c5a35d1445bfccfda2a31bf6235e269aacc304b2c6f21ec6fb3c346b26",
210-    "https://deno.land/x/ts_morph@13.0.0/common/DenoRuntime.ts": "b9ac7200ac980c1aea503cf8302e6581c8ddcdc3dbca850d0dd5a43969d7d16e",
211-    "https://deno.land/x/ts_morph@13.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed",
212-    "https://deno.land/x/ts_morph@13.0.0/common/ts_morph_common.d.ts": "32345569d0356af0870227ca7cbc61a25d7f5e9e6b47ec2b3dca267f8f711840",
213-    "https://deno.land/x/ts_morph@13.0.0/common/ts_morph_common.js": "7a63dede12ac30195099f42a07f8ae139799738351ed66e34d872e842d2ca687",
214-    "https://deno.land/x/ts_morph@13.0.0/common/typescript.d.ts": "bbfbfa714a8f9fdf10e9483b4abd82ccb546fc0fc069ad80d0a254fa40930e98",
215-    "https://deno.land/x/ts_morph@13.0.0/common/typescript.js": "dca27ae23ee7eaa46a03e7e7216755186a4c5aff76273e0a66d30358be0e121b",
216-    "https://esm.sh/@reduxjs/toolkit@1.9.5": "ff3c8c03a33f48803490eb006f7ffdb725a40cd1957e6d3b61ed846ffcc32b9d",
217-    "https://esm.sh/react@18.2.0": "742d8246041966ba1137ec8c60888c35882a9d2478bce63583875f86c1e3687c",
218+    "https://deno.land/x/mock_fetch@0.3.0/mod.ts": "7e7806c65ab17b2b684c334c4e565812bdaf504a3e9c938d2bb52bb67428bc89",
219+    "https://deno.land/x/ts_morph@18.0.0/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9",
220+    "https://deno.land/x/ts_morph@18.0.0/bootstrap/ts_morph_bootstrap.d.ts": "607e651c5ae5aa57c2ac4090759a6379e809c0cdc42114742ac67353b1a75038",
221+    "https://deno.land/x/ts_morph@18.0.0/bootstrap/ts_morph_bootstrap.js": "6645ac03c5e6687dfa8c78109dc5df0250b811ecb3aea2d97c504c35e8401c06",
222+    "https://deno.land/x/ts_morph@18.0.0/common/DenoRuntime.ts": "6a7180f0c6e90dcf23ccffc86aa8271c20b1c4f34c570588d08a45880b7e172d",
223+    "https://deno.land/x/ts_morph@18.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed",
224+    "https://deno.land/x/ts_morph@18.0.0/common/ts_morph_common.d.ts": "42a92b8263878ef48b60042dbb55adda3face9abdb8d503be4b4f0fe242f25f4",
225+    "https://deno.land/x/ts_morph@18.0.0/common/ts_morph_common.js": "845671ca951073400ce142f8acefa2d39ea9a51e29ca80928642f3f8cf2b7700",
226+    "https://deno.land/x/ts_morph@18.0.0/common/typescript.d.ts": "21c0786dddf52537611499340166278507eb9784628d321c2cb6acc696cba0f6",
227+    "https://deno.land/x/ts_morph@18.0.0/common/typescript.js": "d5c598b6a2db2202d0428fca5fd79fc9a301a71880831a805d778797d2413c59",
228+    "https://esm.sh/@reduxjs/toolkit@1.9.5?pin=v122": "416b310f0a0a16d47e456062344b65039acfd879b3e71bc2350208b8dfa6c5f5",
229+    "https://esm.sh/@testing-library/react@14.0.0?pin=v122": "d3ef63d4106b8399a0fe975d206cd5bb61bd5739525128fa7f67014461b5cf46",
230+    "https://esm.sh/react-redux@8.0.5?pin=v122": "f653f94604572dbd2cbd69d882e97172421b6a2e8c89444aff64ac696f50bac0",
231+    "https://esm.sh/react@18.2.0?pin=v122": "742d8246041966ba1137ec8c60888c35882a9d2478bce63583875f86c1e3687c",
232+    "https://esm.sh/redux-batched-actions@0.5.0?pin=v122": "beb7e1f38fd689d68208138d857b4c39aafe7ab66c423ce0c3d5398e77545afc",
233+    "https://esm.sh/robodux@15.0.1?pin=v122": "eeb98b8e64edf0cf5f73fbd462565383f94cba3a0b4891265c8c1feb64af370e",
234     "https://esm.sh/stable/react@18.2.0/deno/react.mjs": "a5a73ee24acca4744ee22c51d9357f31968d1f684ce253bde222b4e26d09f49f",
235-    "https://esm.sh/v113/@types/prop-types@15.7.5/index.d.ts": "6a386ff939f180ae8ef064699d8b7b6e62bc2731a62d7fbf5e02589383838dea",
236-    "https://esm.sh/v113/@types/react@18.0.28/global.d.ts": "bbdf156fea2fabed31a569445835aeedcc33643d404fcbaa54541f06c109df3f",
237-    "https://esm.sh/v113/@types/react@18.0.28/index.d.ts": "bb79735dbe388c3c7446e3436a22e8161ff87b4eed1932a97dc08e5edc1ab798",
238-    "https://esm.sh/v113/@types/scheduler@0.16.3/tracing.d.ts": "f5a8b384f182b3851cec3596ccc96cb7464f8d3469f48c74bf2befb782a19de5",
239-    "https://esm.sh/v113/csstype@3.1.1/index.d.ts": "1c29793071152b207c01ea1954e343be9a44d85234447b2b236acae9e709a383",
240-    "https://esm.sh/v115/@types/prop-types@15.7.5/index.d.ts": "6a386ff939f180ae8ef064699d8b7b6e62bc2731a62d7fbf5e02589383838dea",
241-    "https://esm.sh/v115/@types/react@18.0.34/global.d.ts": "49a253ec027e56c55c7450a0c331cfe96212b3d1cc215b1710ba94a083404cf3",
242-    "https://esm.sh/v115/@types/react@18.0.34/index.d.ts": "d5208107882e0e3409a5ebc1f1cd1b7cd8b22865688ac9184aab8abc1ce6231d",
243-    "https://esm.sh/v115/@types/scheduler@0.16.3/tracing.d.ts": "f5a8b384f182b3851cec3596ccc96cb7464f8d3469f48c74bf2befb782a19de5",
244-    "https://esm.sh/v115/csstype@3.1.2/index.d.ts": "4c68749a564a6facdf675416d75789ee5a557afda8960e0803cf6711fa569288",
245-    "https://esm.sh/v116/@babel/runtime@7.21.0/deno/helpers/esm/objectSpread2.js": "d6df47b28fa0ea915b43fd191239c2a303ea2d9c27319247e6330cbb3f3300d5",
246-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/deno/toolkit.mjs": "9a9e4ff26a321ac112f19166c2fdef4f7c5764f2f22dcdf44edc07c0c7afd5fc",
247-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/autoBatchEnhancer.d.ts": "1588dc63251f2ff156856e7d372c8fcf91ce2aba4214b7554af0741d48f55dba",
248-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/configureStore.d.ts": "529b36eb303f42a6fe5ac2762101089c13b317266ea2aee807db58b085c1bf29",
249-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/createAction.d.ts": "da69802abe75b0d872254de829659d2d0abde88087bd6615e71570d9df393537",
250-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/createAsyncThunk.d.ts": "52f167b5fe797d104a06fd547c52524ed1e47ed0e133f908bb858fa8d3200c39",
251-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/createDraftSafeSelector.d.ts": "7c30461494df47caf22d491c61b4c163bfa41a8cd0dd3187a5fd3ecdfe80d064",
252-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/createReducer.d.ts": "bfb74d106fcdbe7d3e8980ec4dad56beac15bdbc789834e406ac156b7aedce98",
253-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/createSlice.d.ts": "be53d34fe419fdf7328c61fac39a6b4dc67dbe72d685df4c6e87d3bb67fb7ace",
254-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/devtoolsExtension.d.ts": "42b582be2f188c44ef9c3ea7f1e4a331f96aa295ad7a8458d5510c2ac0cf5ba4",
255-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/entities/create_adapter.d.ts": "86047e291068bd62b05fd24479a1b8dc3b25f0e5a7b045c8e7836b937b410a10",
256-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/entities/models.d.ts": "573a8601ded5b0d4cb13710deaba6f587730963b5eef0616fa2833eaf95a2c12",
257-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/getDefaultMiddleware.d.ts": "72a93432821fa87e8fe3b4caa49a1300099c62b9532bb8906f226fd15cc144d7",
258-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/immutableStateInvariantMiddleware.d.ts": "b4826e2f70b1f969f31ca101e846ac2b975fe52ae415d069e0270eb71eecc4ef",
259-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/index.d.ts": "94d426259dcdaa9fd1a4dd6cf6fc68e43b849be4430bcd5cc4f718b4eb1933bf",
260-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/isPlainObject.d.ts": "9eb137d865a0d950d6ca3464986491ff4630bebfcad9bb8a51693e141414d5b5",
261-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/listenerMiddleware/exceptions.d.ts": "ea4d6bde3b49be5fffdf07ea5378141b254fd9ae32082ff1c1b407e645bf1466",
262-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/listenerMiddleware/index.d.ts": "aa3769da6c50472cde1f6b23e52e4d2c6c5534dd6f77ce5f2a71557a2c011154",
263-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/listenerMiddleware/types.d.ts": "34933f88d0e39efbf49fc4590f6552b06a133d782060281d12ad718ab3e24bc3",
264-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/mapBuilders.d.ts": "4764ddd4bc8fa66d605bbdc3917ab982da9bbaa82853e64625cf0ef14a677560",
265-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/matchers.d.ts": "4ef992436eae16230a9188019008a43b3264768c126a007e5476f4cb337967f9",
266-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/nanoid.d.ts": "cba4372698930577bb0f3e9f8a959bb9fc9c0d0de9b118d4225584132fea3134",
267-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/serializableStateInvariantMiddleware.d.ts": "0b8f76b39f169a03258d1c90456d1662d016f58129423bb232d4df2945826fc9",
268-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/tsHelpers.d.ts": "a36ad3bb45c9ce0b2c8f0a9a45a6f66450f99dd178f2b2c50bf8eafa351f2147",
269-    "https://esm.sh/v116/@reduxjs/toolkit@1.9.5/dist/utils.d.ts": "d1f2b2ed3cad61db965c649bb3de54eba971a076d52dcc1cd3db3adde2fe3af6",
270-    "https://esm.sh/v116/immer@9.0.21/deno/immer.mjs": "72a04644488470cf7cdd867cc83736a7eb1b591ac522aad760a76f335ad33244",
271-    "https://esm.sh/v116/immer@9.0.21/dist/core/current.d.ts": "99cf6d12499eac02b32c6fa20a8d361a43575ff2fbb774091921c1fa948d046a",
272-    "https://esm.sh/v116/immer@9.0.21/dist/core/finalize.d.ts": "4e39c4f3c1a84861990fdf88c2ae5fd629e2445f6a8f24bff38fcfd95992b3b6",
273-    "https://esm.sh/v116/immer@9.0.21/dist/core/immerClass.d.ts": "16648246bb3e97f7cb5fcc27ed75b158745e05fe838249195d65b37df3c9f4af",
274-    "https://esm.sh/v116/immer@9.0.21/dist/core/proxy.d.ts": "152b6b88eb7d551ddee1aac42693b234c06b7a0914de2c36e947db2080ef8b97",
275-    "https://esm.sh/v116/immer@9.0.21/dist/core/scope.d.ts": "54bbcd36a79afff3b496e8dcba7793bd90756466c41823804f404cec25d6401e",
276-    "https://esm.sh/v116/immer@9.0.21/dist/immer.d.ts": "bc17494a8b7026424ff0c544ea0e3f0502d8f1597daba3c51b7eadee545b321c",
277-    "https://esm.sh/v116/immer@9.0.21/dist/internal.d.ts": "db4029f5b7d82fe41bedb97f6486bc2c64b844e0d0356858679d1aab41c03688",
278-    "https://esm.sh/v116/immer@9.0.21/dist/plugins/all.d.ts": "53361b025e519ff36cbaf171d3d119cb02964a9bf8e5e3a460010067e4048b14",
279-    "https://esm.sh/v116/immer@9.0.21/dist/plugins/es5.d.ts": "20f03fd18464739bf7a688a622c684018c532ed7fdff414da90969d3a4f752f9",
280-    "https://esm.sh/v116/immer@9.0.21/dist/plugins/mapset.d.ts": "72b014db5edcf7c04a88c8eefe7c02664c0513db13aab5810956063ce6716128",
281-    "https://esm.sh/v116/immer@9.0.21/dist/plugins/patches.d.ts": "b7801c3343f3673b0caa4801952d0620b91914d447252aacba88fc1dfb5c7f6a",
282-    "https://esm.sh/v116/immer@9.0.21/dist/types/types-external.d.ts": "9cfa1a46692b51ce18dafbce5259f9c0971832ab1f5201a89a70e23b0b7cf9e2",
283-    "https://esm.sh/v116/immer@9.0.21/dist/types/types-internal.d.ts": "fca1f054dc1076d04d0d98db29996c954e8d02033c9c403ac757e9c10bbe52f3",
284-    "https://esm.sh/v116/immer@9.0.21/dist/utils/common.d.ts": "c4438b8655cc0415fee50e035ce9e238c1c017f2098931cbf2ec425f38a6fce5",
285-    "https://esm.sh/v116/immer@9.0.21/dist/utils/env.d.ts": "f5a35aab17a4fb73b9bca92b3db33cc10649f2af281862a01231ce8d0d500d40",
286-    "https://esm.sh/v116/immer@9.0.21/dist/utils/errors.d.ts": "7a43554d2b957482977f76623246b78fab4bdfee55daae1531300f37602d4a88",
287-    "https://esm.sh/v116/immer@9.0.21/dist/utils/plugins.d.ts": "282ca31db57b351ef685ea90d97dea434755390329b1297f3d67ee06dae144d0",
288-    "https://esm.sh/v116/redux-thunk@2.4.2/deno/redux-thunk.mjs": "687502e01f5778ac5b2405b2d9b9821fc1d78f4e31f468a43ec2f29abb0bf16b",
289-    "https://esm.sh/v116/redux-thunk@2.4.2/es/index.d.ts": "09ea3a885943344abff8c2abd11dd2644b923128cf8496f7153bfd27acb3a9d8",
290-    "https://esm.sh/v116/redux-thunk@2.4.2/es/types.d.ts": "16d082070829e6986cc7e73958a398c39fc01359fb00ca14a4b29fdf5e4c1283",
291-    "https://esm.sh/v116/redux@4.2.1/deno/redux.mjs": "47f30326af233ec342f863df2d327f074fa2b7b9c71ce648079fe0f8c46a1dc7",
292-    "https://esm.sh/v116/redux@4.2.1/index.d.ts": "fd624f7d7b264922476685870f08c5e1c6d6a0f05dee2429a9747b41f6b699d4",
293-    "https://esm.sh/v116/reselect@4.1.8/deno/reselect.mjs": "2de96350507153619ce7cdbd1a760c814b3974395f4b5bc8bd887d3b04b09028",
294-    "https://esm.sh/v116/reselect@4.1.8/es/defaultMemoize.d.ts": "2d8a1d85b6155ca124eb6ce4d1043471e77b7b8aedda8f2eedb631aac1057416",
295-    "https://esm.sh/v116/reselect@4.1.8/es/index.d.ts": "55a54814090dda59f242a9c891a6a57a985350e5dc0156646a42e88f400c8dd4",
296-    "https://esm.sh/v116/reselect@4.1.8/es/types.d.ts": "0cf2ff23a287d45bf80b88e0c3aefa60881546a0184cae70af91fbc308d016dd",
297-    "https://esm.sh/v116/reselect@4.1.8/es/versionedTypes/index.d.ts": "ed1c984ea8ee3824ab9e262580b845d407f8ca998d7e324a468981e2b461bad3",
298-    "https://esm.sh/v116/reselect@4.1.8/es/versionedTypes/ts47-mergeParameters.d.ts": "1b3a9c3b73e5b61b48c6fc131806b5197a8243e2fa863956717173fbef934420",
299-    "https://esm.sh/v117/@types/prop-types@15.7.5/index.d.ts": "6a386ff939f180ae8ef064699d8b7b6e62bc2731a62d7fbf5e02589383838dea",
300-    "https://esm.sh/v117/@types/react@18.0.37/global.d.ts": "49a253ec027e56c55c7450a0c331cfe96212b3d1cc215b1710ba94a083404cf3",
301-    "https://esm.sh/v117/@types/react@18.0.37/index.d.ts": "f901ba0415ae35c8f062758b2a849fe3aef2ce219323d2495114232c3afe9227",
302-    "https://esm.sh/v117/@types/scheduler@0.16.3/tracing.d.ts": "f5a8b384f182b3851cec3596ccc96cb7464f8d3469f48c74bf2befb782a19de5",
303-    "https://esm.sh/v117/csstype@3.1.2/index.d.ts": "4c68749a564a6facdf675416d75789ee5a557afda8960e0803cf6711fa569288",
304-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/abort-signal.ts": "31224b5f61f1ef58a2acf72bfc1cb9c9bf589f15c263b6555eb06f1b49f77b54",
305-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/async.ts": "c3eae4b79fdd0338ca77915bea79e7df142293a55903c0ef774f5cf633b20171",
306-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/callback.ts": "db306b0b8860cb40dc8e47f18467b24161cfa1bbc7f99fc15fbdd4665fa02a88",
307-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/channel.ts": "ee820fb6bb716ec1c964ac8dea80ffc065a52a940a99cd72b8ae9f7aeca3a665",
308-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/context.ts": "2b7bcdb756adc12f6f431a250435492d8010e2eec9f75e0dacfdaeb68cd8559a",
309-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/deps.ts": "91062b4b97089a8cf36550d4f9605d325a0fd19bebc72d15524481a3b56ea669",
310-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/first.ts": "480621c722e8578c392d690647841785f333397f4eea32df2ecae47de5885279",
311-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/future.ts": "b8c222e9c6952be2dd66cad9c5737a5080f0ca37c9df9e986abd80fdea43ee97",
312-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/instructions.ts": "5ff5c044e2184552972536ddc5de0b8967610c9886e7ee4dd6077b52bff709a5",
313-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/lazy.ts": "92ea526c5ad7d88290f2a87168e038d482f97421379508d85cf2e049ee60639b",
314-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/log.ts": "312b6fb4226be5554b945bc9eb7b05ed7b2dd53dd139ca86971bef256eb78997",
315-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/mod.ts": "d10ab86eaa29695fc3ba4714d2788d85939c78e6b00184e659433ef8ae52384f",
316-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/run.ts": "5c1eb89e19246ee4d47ec55c1b7b88842366151c1caabd5f3144b3d59e9d30db",
317-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/run/block.ts": "50d8b1e85690f957abb4b1c4345e98c78c111c71edc8f5ff96a5a4d39f62e8a2",
318-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/run/create.ts": "be9139af2fbe15908256d2d159dec8dca079f94cf02d488074c94fa26fc651fa",
319-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/run/event-stream.ts": "8c6d40bf315652535a85aa02a61256678dc4e537a4fb436fbe5fecdad794c076",
320-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/run/frame.ts": "01296da446da0c23527fc6242eb0a007dc2d3d1cd590d589f647cb571f96a364",
321-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/run/scope.ts": "3b24605305f033778307b327f14f8f1e9b7c5badf7791bd37eb3b25826f023e5",
322-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/sleep.ts": "d7000c05ace8143efd80f1e35243f5521316bc334e4051afe692e1a0c7268304",
323-    "https://raw.githubusercontent.com/cowboyd/instructional-effection/v0.0.3/types.ts": "f6ac6ddd71a6464c78a0fb64c189903a148d97973e673868b9be98d4d8dd42d9"
324+    "https://esm.sh/v118/@types/prop-types@15.7.5/index.d.ts": "6a386ff939f180ae8ef064699d8b7b6e62bc2731a62d7fbf5e02589383838dea",
325+    "https://esm.sh/v118/@types/react@18.2.6/global.d.ts": "549df62b64a71004aee17685b445a8289013daf96246ce4d9b087d13d7a27a61",
326+    "https://esm.sh/v118/@types/react@18.2.6/index.d.ts": "2ef11908f0e5f1cae0a42694e5e38852c976ca99a5d7eecfb71ed6a1187736c7",
327+    "https://esm.sh/v118/@types/scheduler@0.16.3/tracing.d.ts": "f5a8b384f182b3851cec3596ccc96cb7464f8d3469f48c74bf2befb782a19de5",
328+    "https://esm.sh/v118/csstype@3.1.2/index.d.ts": "4c68749a564a6facdf675416d75789ee5a557afda8960e0803cf6711fa569288",
329+    "https://esm.sh/v122/@babel/runtime@7.21.5/deno/helpers/esm/extends.js": "084ce29d7ef3c5b201cb94c2226b48b24d752ce0f513fa2e6d943a10fee65744",
330+    "https://esm.sh/v122/@babel/runtime@7.21.5/deno/helpers/esm/objectSpread2.js": "1d4171276a85bdbb08076396490d12e880715d119f109cd1052b15b6dccd1425",
331+    "https://esm.sh/v122/@babel/runtime@7.21.5/deno/helpers/esm/objectWithoutPropertiesLoose.js": "68e69ae7b19401eb69fed31a606a16a571a656ce9969d1decc94a01777d96157",
332+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/deno/toolkit.mjs": "5323beb1a41a4e43c583a0d8dae67e88faa57a0f9742a4ca3272239dd3e776ab",
333+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/autoBatchEnhancer.d.ts": "163311515f4f9c1affba0f76627c04cac57c5c6ab4631cddd3e8fe63e15f6c38",
334+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/configureStore.d.ts": "b4621f57bea1e090c2bf884b79ba024c1f08ec7f55aaafa73ce6437294d170a7",
335+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/createAction.d.ts": "40cd61ce7d19c36d58c2b2723d0ce98a117da61446109c5606f5855ba70af2e3",
336+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/createAsyncThunk.d.ts": "96373f0e963f9660e847f33e339ea6fe5d925cf968786bcaf06530a1a5fe5e52",
337+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/createDraftSafeSelector.d.ts": "76ce4e1118a699d33257c4744e9104ce60259e2aa7c587ee6492e8b6c70eed9d",
338+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/createReducer.d.ts": "640131a2d58a76a0527855c6d2e7a39b9efbb26315c58d6b13885d5e6c584b2e",
339+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/createSlice.d.ts": "e4eeaed797c053bcb4d701cbf6ed0ca494d68df41a56cda1440f035743b83df6",
340+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/devtoolsExtension.d.ts": "c698439b3014a87eeed236e3b0d73af3c65d64daa1c9ee776d179f29fc914b21",
341+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/entities/create_adapter.d.ts": "86047e291068bd62b05fd24479a1b8dc3b25f0e5a7b045c8e7836b937b410a10",
342+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/entities/models.d.ts": "573a8601ded5b0d4cb13710deaba6f587730963b5eef0616fa2833eaf95a2c12",
343+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/getDefaultMiddleware.d.ts": "cd206f3e3df4e2664effd6f20b736497794d93977f2b29a6928a30bc3cb259ae",
344+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/immutableStateInvariantMiddleware.d.ts": "09b07b1bd91a39b16f971bbfe3cec6b85c39fb114127a9ac5264b44673c54e34",
345+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/index.d.ts": "2dd91a9c89118d471af0fae8ab84086b2c3005287176f6d257e728bd3b34513b",
346+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/isPlainObject.d.ts": "9eb137d865a0d950d6ca3464986491ff4630bebfcad9bb8a51693e141414d5b5",
347+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/listenerMiddleware/exceptions.d.ts": "b240581208fa099b57df45ec7b2aed98de1fdec5835122c69c701818dbdb9d99",
348+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/listenerMiddleware/index.d.ts": "1e9413df5d26e38d9b2570595a1d839d49aa7d3daebee8221b3bcb43158a9bf2",
349+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/listenerMiddleware/types.d.ts": "bd18316ccf53436aa56ac095ecb9100ed23a2afe11fdbc77461a1dda9eede8c1",
350+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/mapBuilders.d.ts": "2ce69dce79f37d194e6dc0e610ec62ca2a1a5906e9186748a9fce46299923c9e",
351+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/matchers.d.ts": "4ef992436eae16230a9188019008a43b3264768c126a007e5476f4cb337967f9",
352+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/nanoid.d.ts": "cba4372698930577bb0f3e9f8a959bb9fc9c0d0de9b118d4225584132fea3134",
353+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/serializableStateInvariantMiddleware.d.ts": "f9ff1b947da0074054d30826aaa38e876d1b7027b92324a346ef27c8e0865267",
354+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/tsHelpers.d.ts": "1d7bf5f58c4a24f4a654a2af61207662b910fa429fafa1d32518c4c6cc925c99",
355+    "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/utils.d.ts": "9f14472b238598bdf200a0de9f05dd6568e1171fd753fbd72c01a46bf336145d",
356+    "https://esm.sh/v122/@testing-library/dom@9.2.0/deno/dom.mjs": "f86d604327cdb2cfc8b36665af13a055f18fdc024f8b7624fff38f74f41f5fbc",
357+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/config.d.ts": "2a90177ebaef25de89351de964c2c601ab54d6e3a157cba60d9cd3eaf5a5ee1a",
358+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/events.d.ts": "1d2699a343a347a830be26eb17ab340d7875c6f549c8d7477efb1773060cc7e5",
359+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/get-node-text.d.ts": "a0a6f0095f25f08a7129bc4d7cb8438039ec422dc341218d274e1e5131115988",
360+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/get-queries-for-element.d.ts": "61b0bd9a20e0738fd87e67017a69df89106f12e516fdd15ce0a889f7c60d479f",
361+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/index.d.ts": "561aeabb2e1fa95bc9d1f9153ccb5e8cd8fb7ffd5a412616c3cfb24dfa613d79",
362+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/matches.d.ts": "0f4c3516347edf0800b6cbb38c84addc96de419019ea3e440d8866cc7b87733d",
363+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/pretty-dom.d.ts": "2d0c0bee7f35288ccb19c673ad8ffdf06b5f6052be8ddc4df0782b1412b5f22a",
364+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/queries.d.ts": "a2a6a00d5bd1e4a1c4782a291309c0269112492641c7167904af156a0bc7ece7",
365+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/query-helpers.d.ts": "56f39bce2cd0e3f3cdcb4bea7175eddba13ee18b91aa3ecc5d42dadc5fb64ccc",
366+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/role-helpers.d.ts": "a3ce619711ff1bcdaaf4b5187d1e3f84e76064909a7c7ecb2e2f404f145b7b5c",
367+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/screen.d.ts": "678c9811a30c4d5d94e185c4e2526bb0194b852eef79b10722920048a10b8edb",
368+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/suggestions.d.ts": "82200e963d3c767976a5a9f41ecf8c65eca14a6b33dcbe00214fcbe959698c46",
369+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/wait-for-element-to-be-removed.d.ts": "278ba90329ef4874f485dbd9f4e2ede0a71f0b10dbca0a6b0562d013f343d247",
370+    "https://esm.sh/v122/@testing-library/dom@9.2.0/types/wait-for.d.ts": "8387ec1601cf6b8948672537cf8d430431ba0d87b1f9537b4597c1ab8d3ade5b",
371+    "https://esm.sh/v122/@testing-library/react@14.0.0/deno/react.mjs": "630c48de164598d9bc2f26e7a87ae30cb89287d223ea47c034421a7c30b50f76",
372+    "https://esm.sh/v122/@testing-library/react@14.0.0/types/index.d.ts": "73edecc15b451ea14df5b698d53ce96549ae0ac1911cdfcd40e61581ac695f73",
373+    "https://esm.sh/v122/@types/aria-query@5.0.1/index.d.ts": "21522c0f405e58c8dd89cd97eb3d1aa9865ba017fde102d01f86ab50b44e5610",
374+    "https://esm.sh/v122/@types/hoist-non-react-statics@3.3.1/index.d.ts": "a84059e8ce2394008282b5a395b28820e4c2fd2da0cd4a15d0d50631b9993854",
375+    "https://esm.sh/v122/@types/react-dom@18.2.4/index.d.ts": "7f1ca21337a49885088a365081b652451342e45d8c29dfbf97243c66a3f60196",
376+    "https://esm.sh/v122/@types/react-dom@18.2.4/test-utils/index.d.ts": "00f67209791dc2295fce29aa9c7632621dd7605b83a94346275c232b33fd2927",
377+    "https://esm.sh/v122/@types/use-sync-external-store@0.0.3/index.d.ts": "61f41da9aaa809e5142b1d849d4e70f3e09913a5cb32c629bf6e61ef27967ff7",
378+    "https://esm.sh/v122/@types/use-sync-external-store@0.0.3/with-selector.d.ts": "da0195f35a277ff34bb5577062514ce75b7a1b12f476d6be3d4489e26fcf00d8",
379+    "https://esm.sh/v122/ansi-regex@5.0.1/deno/ansi-regex.mjs": "e9046a2f3bf92744288c81913b2eb394b3d92b324e27eed580c0e6402b2718ff",
380+    "https://esm.sh/v122/ansi-styles@5.2.0/deno/ansi-styles.mjs": "239ee56aafe37c20d095ae8f1690f79789b94130308e5507d7940f00d3b7553e",
381+    "https://esm.sh/v122/aria-query@5.1.3/deno/aria-query.mjs": "252fbc2c2b5798aac9cd4d4f00b8be8b4ae5a0366a7ee189a0f8768923e0201c",
382+    "https://esm.sh/v122/array-buffer-byte-length@1.0.0/deno/array-buffer-byte-length.mjs": "3a0090b61ca65d0fbee70c0ce5c0bc302e56119dd7626bcbeacd95c2f91582e8",
383+    "https://esm.sh/v122/available-typed-arrays@1.0.5/deno/available-typed-arrays.mjs": "af54a91db025307077322fbd7a753d45c16da66979d5b9f0bad3070a47c346f5",
384+    "https://esm.sh/v122/call-bind@1.0.2/deno/call-bind.mjs": "380a550c592e2c393378afd7da2b1f3cb25190ed90df015fe29cb90ce1bfcb29",
385+    "https://esm.sh/v122/call-bind@1.0.2/deno/callBound.js": "d8320a1cd92e56689c01bd6d5fa5e18b90359ee8face0987fa99902b7df80974",
386+    "https://esm.sh/v122/deep-equal@2.2.1/deno/deep-equal.mjs": "eff0dcdde8940f4fe247ba2408462b66830e4537689e2fb915cdd65225188b30",
387+    "https://esm.sh/v122/define-properties@1.2.0/deno/define-properties.mjs": "a70149a22ca1ec3924660c72bae7d5f7bb593e52bbe1257bd23947d6f5230dc8",
388+    "https://esm.sh/v122/dom-accessibility-api@0.5.16/deno/dom-accessibility-api.mjs": "e1cef803e4622d4f5c4707c572b004407d476165656c7dbfbafedf75d4bac98e",
389+    "https://esm.sh/v122/es-get-iterator@1.1.3/deno/es-get-iterator.mjs": "52ba4c2af9acd03d01931925036e1f850ed8fb4099963a2e5e0f62745e4dc7a5",
390+    "https://esm.sh/v122/for-each@0.3.3/deno/for-each.mjs": "61efde1a03cf53cf4bb08fd840451b2a168f93485be881c780bb2b9cdf094b48",
391+    "https://esm.sh/v122/function-bind@1.1.1/deno/function-bind.mjs": "85ea0ce74df7d28231e143fc7c177e6933ab9a86ea61b5675d4eea6c62de01df",
392+    "https://esm.sh/v122/functions-have-names@1.2.3/deno/functions-have-names.mjs": "de051469d191cd9819a0d88e3584d42336c041f4bc19e6c2aff3ea2c543d9bea",
393+    "https://esm.sh/v122/get-intrinsic@1.2.1/deno/get-intrinsic.mjs": "487e8637302d49c1559f967a4baf823e3e1c320dd9c094dc6cd8a089d0926ac7",
394+    "https://esm.sh/v122/gopd@1.0.1/deno/gopd.mjs": "29ce793cf3e91b96e7f8a3622982cfac19cfb55e16832f5c28ea918442c80220",
395+    "https://esm.sh/v122/has-bigints@1.0.2/deno/has-bigints.mjs": "b0acdf47c90cd03b2e54d59eea16103748bd50a78f9a1de84b1d7b0929c3a673",
396+    "https://esm.sh/v122/has-property-descriptors@1.0.0/deno/has-property-descriptors.mjs": "fd11355cd2b9aac567550e1471be95c9f82b6e53b14d845d7b8f6f67c25c42a8",
397+    "https://esm.sh/v122/has-proto@1.0.1/deno/has-proto.mjs": "1f7413341b9935520f658d9fb241eb671a13ed73378863ee118eb52c74867eed",
398+    "https://esm.sh/v122/has-symbols@1.0.3/deno/shams.js": "25687c8bedcbefa7ad0d261ace3024b8ff5e7cb058f1871aa9b600b9743d0584",
399+    "https://esm.sh/v122/has-tostringtag@1.0.0/deno/shams.js": "829db6f56bc746da2a23201f9e7753e70958fa81d3671a7fce9e63b12964fdf5",
400+    "https://esm.sh/v122/hoist-non-react-statics@3.3.2/deno/hoist-non-react-statics.mjs": "7dbd22e052d4b2e27307cd90b2080e998682532c89ef41a1d96f88efc6c7b4e1",
401+    "https://esm.sh/v122/immer@9.0.21/deno/immer.mjs": "72a04644488470cf7cdd867cc83736a7eb1b591ac522aad760a76f335ad33244",
402+    "https://esm.sh/v122/immer@9.0.21/dist/core/current.d.ts": "99cf6d12499eac02b32c6fa20a8d361a43575ff2fbb774091921c1fa948d046a",
403+    "https://esm.sh/v122/immer@9.0.21/dist/core/finalize.d.ts": "4e39c4f3c1a84861990fdf88c2ae5fd629e2445f6a8f24bff38fcfd95992b3b6",
404+    "https://esm.sh/v122/immer@9.0.21/dist/core/immerClass.d.ts": "16648246bb3e97f7cb5fcc27ed75b158745e05fe838249195d65b37df3c9f4af",
405+    "https://esm.sh/v122/immer@9.0.21/dist/core/proxy.d.ts": "152b6b88eb7d551ddee1aac42693b234c06b7a0914de2c36e947db2080ef8b97",
406+    "https://esm.sh/v122/immer@9.0.21/dist/core/scope.d.ts": "54bbcd36a79afff3b496e8dcba7793bd90756466c41823804f404cec25d6401e",
407+    "https://esm.sh/v122/immer@9.0.21/dist/immer.d.ts": "bc17494a8b7026424ff0c544ea0e3f0502d8f1597daba3c51b7eadee545b321c",
408+    "https://esm.sh/v122/immer@9.0.21/dist/internal.d.ts": "db4029f5b7d82fe41bedb97f6486bc2c64b844e0d0356858679d1aab41c03688",
409+    "https://esm.sh/v122/immer@9.0.21/dist/plugins/all.d.ts": "53361b025e519ff36cbaf171d3d119cb02964a9bf8e5e3a460010067e4048b14",
410+    "https://esm.sh/v122/immer@9.0.21/dist/plugins/es5.d.ts": "20f03fd18464739bf7a688a622c684018c532ed7fdff414da90969d3a4f752f9",
411+    "https://esm.sh/v122/immer@9.0.21/dist/plugins/mapset.d.ts": "72b014db5edcf7c04a88c8eefe7c02664c0513db13aab5810956063ce6716128",
412+    "https://esm.sh/v122/immer@9.0.21/dist/plugins/patches.d.ts": "b7801c3343f3673b0caa4801952d0620b91914d447252aacba88fc1dfb5c7f6a",
413+    "https://esm.sh/v122/immer@9.0.21/dist/types/types-external.d.ts": "9cfa1a46692b51ce18dafbce5259f9c0971832ab1f5201a89a70e23b0b7cf9e2",
414+    "https://esm.sh/v122/immer@9.0.21/dist/types/types-internal.d.ts": "fca1f054dc1076d04d0d98db29996c954e8d02033c9c403ac757e9c10bbe52f3",
415+    "https://esm.sh/v122/immer@9.0.21/dist/utils/common.d.ts": "c4438b8655cc0415fee50e035ce9e238c1c017f2098931cbf2ec425f38a6fce5",
416+    "https://esm.sh/v122/immer@9.0.21/dist/utils/env.d.ts": "f5a35aab17a4fb73b9bca92b3db33cc10649f2af281862a01231ce8d0d500d40",
417+    "https://esm.sh/v122/immer@9.0.21/dist/utils/errors.d.ts": "7a43554d2b957482977f76623246b78fab4bdfee55daae1531300f37602d4a88",
418+    "https://esm.sh/v122/immer@9.0.21/dist/utils/plugins.d.ts": "282ca31db57b351ef685ea90d97dea434755390329b1297f3d67ee06dae144d0",
419+    "https://esm.sh/v122/internal-slot@1.0.5/deno/internal-slot.mjs": "12db06def16dd59958a915f833ded3eeb7f958143dcd3941617a71ef6bfa7b05",
420+    "https://esm.sh/v122/is-arguments@1.1.1/deno/is-arguments.mjs": "5da115ea0d2747a4bebeb3fccf077a5ed9def5a85fe5f0de60989afde26b1dbc",
421+    "https://esm.sh/v122/is-array-buffer@3.0.2/deno/is-array-buffer.mjs": "56f8916d5300d88bb56c604c721a2b33217629757d186d8a9255c949d054a296",
422+    "https://esm.sh/v122/is-bigint@1.0.4/deno/is-bigint.mjs": "d384b790df301872790cdddcc9f8650354ec48748efd154653a54a0a4575f625",
423+    "https://esm.sh/v122/is-boolean-object@1.1.2/deno/is-boolean-object.mjs": "16fe4a5077e89fa9baf6a6b820b1aa0df89d4e18ff40ccb778ba62466957c11e",
424+    "https://esm.sh/v122/is-callable@1.2.7/deno/is-callable.mjs": "c36722455e75f6492e0006897afed4c0288a2045045acdff5801aeb27ee1ebad",
425+    "https://esm.sh/v122/is-date-object@1.0.5/deno/is-date-object.mjs": "8ffb0dbd0434dabfd74fdb036c6381cb53a3048b2fd7f8c16357c66f7b231809",
426+    "https://esm.sh/v122/is-map@2.0.2/deno/is-map.mjs": "e8cadfdf3d613dfea00940656584ad3ad0897534da815b7840fed2815fc93aec",
427+    "https://esm.sh/v122/is-number-object@1.0.7/deno/is-number-object.mjs": "b5550d43dfa128620d92ef4f0cbe1a5d361d39175ceb6c02e4865d357cab4e88",
428+    "https://esm.sh/v122/is-regex@1.1.4/deno/is-regex.mjs": "2a180b3fc43c77a84c73caaef62a8445cdc57c74a05b199e6792edb787ae2d25",
429+    "https://esm.sh/v122/is-set@2.0.2/deno/is-set.mjs": "88f15482f021bf438158a6b945788b6c35dea27507a14bdafd33b4668815b387",
430+    "https://esm.sh/v122/is-shared-array-buffer@1.0.2/deno/is-shared-array-buffer.mjs": "bfefdb478f4c85b22acac15e11a81f84adebc712e927287b5a7be29d2a17bc14",
431+    "https://esm.sh/v122/is-string@1.0.7/deno/is-string.mjs": "6b4683ec301751b102d3565ab2d9b74a68cc5760c9c8bc3a1abdb8bc4f957c27",
432+    "https://esm.sh/v122/is-symbol@1.0.4/deno/is-symbol.mjs": "452b3ba89c04b95ddd51653f70bc91657c5901c5cbd2f7fdea93b1d524c79dfc",
433+    "https://esm.sh/v122/is-typed-array@1.1.10/deno/is-typed-array.mjs": "0d11c17cc14088554153f1b850cbf0d3f1fb3264e5c12b0d9227cfced4f150db",
434+    "https://esm.sh/v122/is-weakmap@2.0.1/deno/is-weakmap.mjs": "20fae3683116b3b8ff78a87fdd5c65ca3c5386ff30b9652abf2af24c1eb28f95",
435+    "https://esm.sh/v122/is-weakset@2.0.2/deno/is-weakset.mjs": "b8c808f905f598cf9bab89febb15e2e63a22f951eafaf302c4fb6159137fe80c",
436+    "https://esm.sh/v122/isarray@2.0.5/deno/isarray.mjs": "1cc4c24399952c70babf37f531295df4355e3bd64a7cd14406facf8404f51666",
437+    "https://esm.sh/v122/lz-string@1.5.0/deno/lz-string.mjs": "4b47a5999d6135dcddb4de33bb49e9010bfd8c3284dc26079f434900088d294e",
438+    "https://esm.sh/v122/object-inspect@1.12.3/deno/object-inspect.mjs": "8eec27956657c7c22ce22b84cba68df4dd7513570024c87f62c8e7df1731b86d",
439+    "https://esm.sh/v122/object-is@1.1.5/deno/object-is.mjs": "e1276d267efbf13cb140dae101c8389858333f86c7b1cf5c73cfaecae1dd4223",
440+    "https://esm.sh/v122/object-keys@1.1.1/deno/object-keys.mjs": "102b4ac2862d00d8e34e6b5e9f82a1d3ed82aec7ac726e15d1cb40828c8ced7f",
441+    "https://esm.sh/v122/object.assign@4.1.4/deno/object.assign.mjs": "657c265b2123f0462796bc126dcbf613bc7394cc09160aa569652b32cd702c4c",
442+    "https://esm.sh/v122/pretty-format@27.5.1/build/index.d.ts": "56a50e283257c7fc16958f9e31d8260262505551782a6afc31030970ff48775a",
443+    "https://esm.sh/v122/pretty-format@27.5.1/build/types.d.ts": "462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094",
444+    "https://esm.sh/v122/pretty-format@27.5.1/deno/pretty-format.mjs": "0e9dbbd2dc90301221d47700ee633bad42cbca2bb34d45bd1f881e2dc1cf2310",
445+    "https://esm.sh/v122/react-dom@18.2.0/deno/client.js": "b1ece336dd563def20ab5a55dc80babe034770cdd1df22a6d6f84e22853613d7",
446+    "https://esm.sh/v122/react-dom@18.2.0/deno/react-dom.mjs": "e1a40770411a2fe42b594ededa8238d31f3cac1334b0cb3fcd9ab7a34d4eafee",
447+    "https://esm.sh/v122/react-dom@18.2.0/deno/test-utils.js": "b7fa07cc73bced0dec05ed30e7cb3aaca14f97e983b7a87fbc86420bea73f7eb",
448+    "https://esm.sh/v122/react-is@16.13.1/deno/react-is.mjs": "5b3810c2d2c7421f78f37df755a1440996b54c04e5cfa7b9ca10efefbe5f329f",
449+    "https://esm.sh/v122/react-is@17.0.2/deno/react-is.mjs": "4b2a2ef2487512ba9113abf7cae5d60eb8ee6f4fde18049639204edf84ab0833",
450+    "https://esm.sh/v122/react-is@18.2.0/deno/react-is.mjs": "efe49ec2efb7a2d0d8f360e7ddb8c865c1194bf7f42125027205d68b4e7c2095",
451+    "https://esm.sh/v122/react-redux@8.0.5/deno/react-redux.mjs": "d02bb23a68461f8a6818a1b564ccb520c0fd0ca5092ecd5aaecc355bd9a7c2b7",
452+    "https://esm.sh/v122/react-redux@8.0.5/es/components/Context.d.ts": "7c66a0e01cd2d48baf1572a2f697565765140cc5cdff8e5a04b06a4295591845",
453+    "https://esm.sh/v122/react-redux@8.0.5/es/components/Provider.d.ts": "311529a26ec91ca60f064079e4133b54e2be281c707326fd86f1f6787969d403",
454+    "https://esm.sh/v122/react-redux@8.0.5/es/components/connect.d.ts": "299ff0a06a3833c39a5766c07d6635eddab9b3ed3c297dd84c122a57480e0f2f",
455+    "https://esm.sh/v122/react-redux@8.0.5/es/connect/selectorFactory.d.ts": "cba4b94121af5365d4ce689e6d711c913748634ef12bf657fd3d28f20d10c0bd",
456+    "https://esm.sh/v122/react-redux@8.0.5/es/exports.d.ts": "bf60c690ff0e05557fa8c1a7439870274741fe8a049ad532339e02f2e0b5d63b",
457+    "https://esm.sh/v122/react-redux@8.0.5/es/hooks/useDispatch.d.ts": "9c6d98ae248560494f7a8e0b1ef2b7cd1eef8a1f5231a8c62d4324d902ff5dd3",
458+    "https://esm.sh/v122/react-redux@8.0.5/es/hooks/useSelector.d.ts": "665c9bbb2ac098a5236f7582fb62e26d1ab9e0a9c8213850494cf3611cc2a0a4",
459+    "https://esm.sh/v122/react-redux@8.0.5/es/hooks/useStore.d.ts": "1d31c2e06d653151138715cc8201527ca7ea2a20e8776ee1c07049233884de72",
460+    "https://esm.sh/v122/react-redux@8.0.5/es/index.d.ts": "a4c3e41066fc4cd9c74c2f98f9e8ee0acbbe6d8f5e59ae403c7f75b0c8fe428e",
461+    "https://esm.sh/v122/react-redux@8.0.5/es/types.d.ts": "bcb9e4bebdccb122cadfab1a87adf15760694d306f9fd9ec24f5238b7f72ccf6",
462+    "https://esm.sh/v122/react-redux@8.0.5/es/utils/Subscription.d.ts": "05be35caefacdcbbc14ff6ec605c9a1458c92491448034c174a401ecb463b4f3",
463+    "https://esm.sh/v122/react-redux@8.0.5/es/utils/reactBatchedUpdates.d.ts": "3a61d355ee0e26e20e6dc05898cfc5b1f57f10996e532b22240bb3d2c5e9730f",
464+    "https://esm.sh/v122/react-redux@8.0.5/es/utils/shallowEqual.d.ts": "42d3863cb9aa6d1ffc8bfe897a1f2203a603e826848c8c41b0c8a786a2f7ecef",
465+    "https://esm.sh/v122/react-redux@8.0.5/es/utils/useSyncExternalStore.d.ts": "6d3d2341958a5fc3beab09a73d9d9280a16ce5412e0dc4b5b7a237af430c5f30",
466+    "https://esm.sh/v122/redux-batched-actions@0.5.0/deno/redux-batched-actions.mjs": "e786d6cb927e25e09cd0a08d562c7788e2943a3c45c4edf96e0a3d45f4e1d6cd",
467+    "https://esm.sh/v122/redux-batched-actions@0.5.0/lib/index.d.ts": "38139476cb2f3e6be4947e6e97c293f61c6858b5604f59b1a9908a39421e8bef",
468+    "https://esm.sh/v122/redux-thunk@2.4.2/deno/redux-thunk.mjs": "687502e01f5778ac5b2405b2d9b9821fc1d78f4e31f468a43ec2f29abb0bf16b",
469+    "https://esm.sh/v122/redux-thunk@2.4.2/es/index.d.ts": "c2319b7067346ca83427b59d3e2f63501f71d92bf7559a4ba91be5e00ec12324",
470+    "https://esm.sh/v122/redux-thunk@2.4.2/es/types.d.ts": "ea3b2c86e942ca9808195d90bda3aae12dfb8fddd43b67668e238aa6753ce3a8",
471+    "https://esm.sh/v122/redux@4.2.1/deno/redux.mjs": "5c2067c80d9b36b4ff7e19fa1ebb5bcdf4875fcdf0840035746a4654450d1a09",
472+    "https://esm.sh/v122/redux@4.2.1/index.d.ts": "fd624f7d7b264922476685870f08c5e1c6d6a0f05dee2429a9747b41f6b699d4",
473+    "https://esm.sh/v122/regexp.prototype.flags@1.5.0/deno/regexp.prototype.flags.mjs": "48819674cd2886ccd5b7cb8c882606d06b76e0382dc8f35a499b29cf07b2efa4",
474+    "https://esm.sh/v122/reselect@4.1.8/deno/reselect.mjs": "2de96350507153619ce7cdbd1a760c814b3974395f4b5bc8bd887d3b04b09028",
475+    "https://esm.sh/v122/reselect@4.1.8/es/defaultMemoize.d.ts": "2d8a1d85b6155ca124eb6ce4d1043471e77b7b8aedda8f2eedb631aac1057416",
476+    "https://esm.sh/v122/reselect@4.1.8/es/index.d.ts": "55a54814090dda59f242a9c891a6a57a985350e5dc0156646a42e88f400c8dd4",
477+    "https://esm.sh/v122/reselect@4.1.8/es/types.d.ts": "0cf2ff23a287d45bf80b88e0c3aefa60881546a0184cae70af91fbc308d016dd",
478+    "https://esm.sh/v122/reselect@4.1.8/es/versionedTypes/index.d.ts": "ed1c984ea8ee3824ab9e262580b845d407f8ca998d7e324a468981e2b461bad3",
479+    "https://esm.sh/v122/reselect@4.1.8/es/versionedTypes/ts47-mergeParameters.d.ts": "1b3a9c3b73e5b61b48c6fc131806b5197a8243e2fa863956717173fbef934420",
480+    "https://esm.sh/v122/robodux@15.0.1/deno/robodux.mjs": "670792c90500620b7d2edcf1890f3e589f2765c15f4b6bca3babd69361a29289",
481+    "https://esm.sh/v122/robodux@15.0.1/dist/combine.d.ts": "f450e144d24536a0793cf1775aae2356705245c5e724047d4bedc2f8001ad788",
482+    "https://esm.sh/v122/robodux@15.0.1/dist/create-action.d.ts": "61f663dd8b8fd207a4a2e274e86601f92400c2979b9691b71a982f353c19d2f0",
483+    "https://esm.sh/v122/robodux@15.0.1/dist/create-assign.d.ts": "bf1f5a48d6b40310799d392178973eba60ae39273e85a8e9ffcbff8cded20200",
484+    "https://esm.sh/v122/robodux@15.0.1/dist/create-list-table.d.ts": "300926e447d1ddb582a1a086483719a9f07ee3af50c51727bf534d029936f1ca",
485+    "https://esm.sh/v122/robodux@15.0.1/dist/create-list.d.ts": "131c84be65e76bff5be662ef3d7e08d4a665e2bd1a3b7c3f6dcc82004b4ccd51",
486+    "https://esm.sh/v122/robodux@15.0.1/dist/create-loader-table.d.ts": "0f6f08d4db08212f2dd3dadb2cd84190fac29d376428e0df7e9155ecdeed4efb",
487+    "https://esm.sh/v122/robodux@15.0.1/dist/create-loader.d.ts": "f2325a8588a434f725dd621c3d0d16d6c7f023c5158cd87dca8f70c4f2bd98bb",
488+    "https://esm.sh/v122/robodux@15.0.1/dist/create-map.d.ts": "2851c7ad3861c25035b8bd4e61d894af7164cb04b921e8258432ef601be9eeb8",
489+    "https://esm.sh/v122/robodux@15.0.1/dist/create-reducer.d.ts": "a0f32cafe42727eef6c67eb211dc77f19ad5a2d53d6dceaf1c87d0468df5cd13",
490+    "https://esm.sh/v122/robodux@15.0.1/dist/create-slice.d.ts": "56a3d4d6469100edfb88ba672ee2035bba2e1f807b2cbca9b88b1047952b16d6",
491+    "https://esm.sh/v122/robodux@15.0.1/dist/create-table.d.ts": "b72f2a6924ff17af20ff9198366261322c3d8793741188ee387bad675306b253",
492+    "https://esm.sh/v122/robodux@15.0.1/dist/index.d.ts": "b87f84ed5171496f497914f3cc7d65121f59bd0e8982b6d392f63f19006b0645",
493+    "https://esm.sh/v122/robodux@15.0.1/dist/types.d.ts": "f3a61fa962ed1917c97804a3127b2e6ccea283bdbe7b990dc4d3c2f0434eeca6",
494+    "https://esm.sh/v122/scheduler@0.23.0/deno/scheduler.mjs": "585bd99e523275a2447f9d1c29f36b43be0a20123e7c7915f6f4d5405ecc2884",
495+    "https://esm.sh/v122/side-channel@1.0.4/deno/side-channel.mjs": "09064d7f47f5cbfd3a8fcce2f03191ab65654e63ec670545d0519e7a3bafa347",
496+    "https://esm.sh/v122/stop-iteration-iterator@1.0.0/deno/stop-iteration-iterator.mjs": "74f2c3bd4ec754f3665cc303a5e9c512286d12cc3455da2985ad08fca697685e",
497+    "https://esm.sh/v122/use-sync-external-store@1.2.0/deno/cjs/use-sync-external-store-shim.production.min.js": "ef11625e3f8af74256cdbfb9586a54928b24429786dd9e1e10bb98808b8e1646",
498+    "https://esm.sh/v122/use-sync-external-store@1.2.0/deno/cjs/use-sync-external-store-shim/with-selector.production.min.js": "58c96b48e3170c737357ecd7b6637f9dcf1d3d7cb2407e626a1bfc9fd7eff994",
499+    "https://esm.sh/v122/use-sync-external-store@1.2.0/deno/shim.js": "00b99d32c77998883615ca14d43947d65a7102fe13f9dd1464addd74a72a3cec",
500+    "https://esm.sh/v122/use-sync-external-store@1.2.0/deno/shim/with-selector.js": "b510c0190c8491339d243df109585b6f53d49d23c7c08a92bbc66392b4b804d2",
501+    "https://esm.sh/v122/which-boxed-primitive@1.0.2/deno/which-boxed-primitive.mjs": "ea1bdc1ef3c6df1a7171bd96431111ec117967454fe2784a597db475020e27d9",
502+    "https://esm.sh/v122/which-collection@1.0.1/deno/which-collection.mjs": "f08c0f7a244d3c9caf2a369844ba61c3eabb002f74054a7630474901ac431393",
503+    "https://esm.sh/v122/which-typed-array@1.1.9/deno/which-typed-array.mjs": "1ff946187cdb2822f2d85509ef7752c6120ed209b9a726cdfd25e629bc907666"
504   }
505 }
M deps.ts
+42, -3
 1@@ -1,12 +1,10 @@
 2-import React from "https://esm.sh/react@18.2.0";
 3-export { React };
 4-export { configureStore } from "https://esm.sh/@reduxjs/toolkit@1.9.5";
 5 export type {
 6   Channel,
 7   Instruction,
 8   Operation,
 9   Result,
10   Scope,
11+  Stream,
12   Task,
13 } from "https://deno.land/x/effection@3.0.0-alpha.7/mod.ts";
14 export {
15@@ -24,3 +22,44 @@ export {
16   spawn,
17   useAbortSignal,
18 } from "https://deno.land/x/effection@3.0.0-alpha.7/mod.ts";
19+
20+import React from "https://esm.sh/react@18.2.0?pin=v122";
21+export { React };
22+export {
23+  Provider,
24+  useDispatch,
25+  useSelector,
26+} from "https://esm.sh/react-redux@8.0.5?pin=v122";
27+
28+export type {
29+  Action,
30+  Middleware,
31+  Reducer,
32+} from "https://esm.sh/@reduxjs/toolkit@1.9.5?pin=v122";
33+export {
34+  combineReducers,
35+  configureStore,
36+  createImmutableStateInvariantMiddleware,
37+  createSerializableStateInvariantMiddleware,
38+  getDefaultMiddleware,
39+} from "https://esm.sh/@reduxjs/toolkit@1.9.5?pin=v122";
40+export {
41+  BATCH,
42+  batchActions,
43+  enableBatching,
44+} from "https://esm.sh/redux-batched-actions@0.5.0?pin=v122";
45+export type {
46+  LoadingItemState,
47+  LoadingMapPayload,
48+  LoadingState,
49+  MapEntity,
50+} from "https://esm.sh/robodux@15.0.1?pin=v122";
51+export {
52+  createAction,
53+  createAssign,
54+  createLoaderTable,
55+  createReducerMap,
56+  createTable,
57+  defaultLoader,
58+  defaultLoadingItem,
59+} from "https://esm.sh/robodux@15.0.1?pin=v122";
M examples/basic/app.tsx
+2, -0
 1@@ -61,6 +61,7 @@ function main() {
 2   function* users() {
 3     while (true) {
 4       const action = yield* take("fetch-user");
 5+      console.log(action);
 6       yield* go(function* () {
 7         console.log(action);
 8       });
 9@@ -69,6 +70,7 @@ function main() {
10   function* mailboxes() {
11     while (true) {
12       const action = yield* take("fetch-mailboxes");
13+      console.log(action);
14       yield* go(function* () {
15         console.log(action);
16         yield* sleep(1000);
R test/call.test.ts => fx/call.test.ts
+1, -1
1@@ -1,7 +1,7 @@
2 import { describe, expect, it } from "../test.ts";
3 
4 import { run } from "../deps.ts";
5-import { call } from "../mod.ts";
6+import { call } from "./call.ts";
7 
8 const tests = describe("call()");
9 
M fx/call.ts
+0, -7
 1@@ -1,7 +1,6 @@
 2 import type { OpFn } from "../types.ts";
 3 import type { Operation, Result, Task } from "../deps.ts";
 4 import { action, Err, expect, Ok, spawn } from "../deps.ts";
 5-import { ErrContext } from "../context.ts";
 6 
 7 export const isFunc = (f: unknown) => typeof f === "function";
 8 export const isPromise = (p: unknown) =>
 9@@ -36,8 +35,6 @@ export function* call<T>(opFn: OpFn<T>): Operation<Result<T>> {
10     const value = yield* unsafeCall(opFn);
11     return Ok(value);
12   } catch (error) {
13-    const { input } = yield* ErrContext;
14-    yield* input.send(error);
15     return Err(error);
16   }
17 }
18@@ -45,10 +42,6 @@ export function* call<T>(opFn: OpFn<T>): Operation<Result<T>> {
19 export function* go<T>(op: OpFn<T>): Operation<Task<Result<T>>> {
20   return yield* spawn(function* () {
21     const result = yield* call(op);
22-    if (!result.ok) {
23-      const { input } = yield* ErrContext;
24-      yield* input.send(result.error);
25-    }
26     return result;
27   });
28 }
A fx/defer.ts
+12, -0
 1@@ -0,0 +1,12 @@
 2+import { resource } from "../deps.ts";
 3+import type { Operation } from "../deps.ts";
 4+
 5+export function defer(op: () => Operation<unknown>): Operation<void> {
 6+  return resource(function* (provide) {
 7+    try {
 8+      yield* provide();
 9+    } finally {
10+      yield* op();
11+    }
12+  });
13+}
R fx/mod.ts => fx/index.ts
+1, -0
1@@ -6,3 +6,4 @@ export * from "./race.ts";
2 export * from "./emit.ts";
3 export * from "./request.ts";
4 export * from "./watch.ts";
5+export * from "./defer.ts";
R test/parallel.test.ts => fx/parallel.test.ts
+6, -4
 1@@ -1,8 +1,9 @@
 2 import { describe, expect, it } from "../test.ts";
 3 import type { Operation, Result } from "../deps.ts";
 4 import { Err, Ok, run, sleep, spawn } from "../deps.ts";
 5-import { parallel } from "../fx/mod.ts";
 6-import { cforEach } from "../iter.ts";
 7+import { forEach } from "../iter.ts";
 8+
 9+import { parallel } from "./parallel.ts";
10 
11 const test = describe("parallel()");
12 
13@@ -40,7 +41,7 @@ it(
14       ]);
15 
16       const res: Result<string>[] = [];
17-      yield* cforEach(results.immediate, function* (val) {
18+      yield* forEach(results.immediate.output, function* (val) {
19         res.push(val);
20       });
21 
22@@ -69,7 +70,8 @@ it(
23       ]);
24 
25       const res: Result<string>[] = [];
26-      yield* cforEach(results.sequence, function* (val) {
27+      const { output } = results.sequence;
28+      yield* forEach(output, function* (val) {
29         res.push(val);
30       });
31 
M fx/parallel.ts
+2, -1
 1@@ -1,9 +1,10 @@
 2 import type { Channel, Operation, Result } from "../deps.ts";
 3 import type { Computation, OpFn } from "../types.ts";
 4 import { createChannel, resource, spawn } from "../deps.ts";
 5+
 6 import { call } from "./call.ts";
 7 
 8-interface ParallelRet<T> extends Computation<Result<T>[]> {
 9+export interface ParallelRet<T> extends Computation<Result<T>[]> {
10   sequence: Channel<Result<T>, void>;
11   immediate: Channel<Result<T>, void>;
12 }
M fx/race.ts
+26, -14
 1@@ -1,31 +1,43 @@
 2-import type { Operation } from "../deps.ts";
 3+import type { Operation, Task } from "../deps.ts";
 4 import { action, resource, spawn } from "../deps.ts";
 5 
 6 import type { OpFn } from "../types.ts";
 7-import { map } from "../iter.ts";
 8 import { toOperation } from "./call.ts";
 9 
10-export function race<T>(operations: OpFn<T>[]): Operation<T> {
11+interface OpMap<T = unknown> {
12+  [key: string]: OpFn<T>;
13+}
14+
15+export function race(
16+  opMap: OpMap,
17+): Operation<{ [K in keyof OpMap<unknown>]: ReturnType<OpMap[K]> }> {
18   return resource(function* Race(provide) {
19-    const tasks = yield* map(
20-      operations.map((o) => () => toOperation(o)),
21-      spawn,
22-    );
23+    const keys = Object.keys(opMap);
24+    const taskMap: { [key: string]: Task<unknown> } = {};
25+    const resultMap: { [key: keyof OpMap]: OpMap[keyof OpMap] } = {};
26 
27-    const winner = yield* action<T>(function* (resolve) {
28-      for (const task of tasks) {
29+    const winner = yield* action<Task<unknown>>(function* (resolve) {
30+      for (let i = 0; i < keys.length; i += 1) {
31+        const key = keys[i];
32         yield* spawn(function* () {
33-          resolve(yield* task);
34+          const task = yield* spawn(() => toOperation(opMap[key]));
35+          taskMap[key] = task;
36+          (resultMap as any)[key] = yield* task;
37+          resolve(task);
38         });
39       }
40     });
41 
42-    for (const task of tasks) {
43-      if (task !== winner) {
44-        yield* spawn(() => task.halt());
45+    for (let i = 0; i < keys.length; i += 1) {
46+      const key = keys[i];
47+      const task = taskMap[key];
48+      if (task === winner) {
49+        continue;
50       }
51+
52+      yield* spawn(() => task.halt());
53     }
54 
55-    yield* provide(winner);
56+    yield* provide(resultMap);
57   });
58 }
M fx/request.ts
+0, -3
 1@@ -1,4 +1,3 @@
 2-import { ErrContext } from "../context.ts";
 3 import { Err, expect, Ok, useAbortSignal } from "../deps.ts";
 4 
 5 export function* request(url: string | URL | Request, opts?: RequestInit) {
 6@@ -12,8 +11,6 @@ export function* json(response: Response) {
 7     const result = yield* expect(response.json());
 8     return Ok(result);
 9   } catch (error) {
10-    const { input } = yield* ErrContext;
11-    yield* input.send(error);
12     return Err(error);
13   }
14 }
M fx/watch.ts
+2, -1
1@@ -12,5 +12,6 @@ export function supervise<T>(op: OpFn<T>) {
2 }
3 
4 export function* keepAlive(ops: OpFn[]) {
5-  return yield* parallel(ops.map(supervise));
6+  const results = yield* parallel(ops.map(supervise));
7+  yield* results;
8 }
A index.ts
+26, -0
 1@@ -0,0 +1,26 @@
 2+export * from "./fx/index.ts";
 3+export * from "./types.ts";
 4+export * from "./iter.ts";
 5+export * from "./context.ts";
 6+export * from "./compose.ts";
 7+export {
 8+  action,
 9+  createChannel,
10+  createContext,
11+  createScope,
12+  Err,
13+  getframe,
14+  Ok,
15+  resource,
16+  run,
17+  sleep,
18+  spawn,
19+} from "./deps.ts";
20+export type {
21+  Channel,
22+  Instruction,
23+  Operation,
24+  Result,
25+  Scope,
26+  Task,
27+} from "./deps.ts";
M iter.ts
+3, -20
 1@@ -1,4 +1,4 @@
 2-import type { Channel, Operation } from "./deps.ts";
 3+import type { Channel, Operation, Stream } from "./deps.ts";
 4 import type { Action } from "./types.ts";
 5 import { ActionPattern, matcher } from "./matcher.ts";
 6 
 7@@ -21,28 +21,11 @@ export function* once({
 8   }
 9 }
10 
11-export function* cforEach<T>(
12-  chan: Channel<T, void>,
13-  each?: (val: T) => Operation<void>,
14-) {
15-  const { output } = chan;
16-  const msgList = yield* output;
17-  while (true) {
18-    const next = yield* msgList;
19-    if (next.done) {
20-      return next.value;
21-    } else if (each) {
22-      yield* each(next.value);
23-    }
24-  }
25-}
26-
27 export function* forEach<T>(
28-  chan: Operation<Channel<T, void>>,
29+  stream: Stream<T, void>,
30   each?: (val: T) => Operation<void>,
31 ) {
32-  const { output } = yield* chan;
33-  const msgList = yield* output;
34+  const msgList = yield* stream;
35   while (true) {
36     const next = yield* msgList;
37     if (next.done) {
M matcher.ts
+4, -4
 1@@ -1,17 +1,17 @@
 2 import type { Action, ActionType } from "./types.ts";
 3 
 4 type GuardPredicate<G extends T, T = unknown> = (arg: T) => arg is G;
 5-type Predicate<T> = (arg: T) => boolean;
 6+type Predicate = (action: Action) => boolean;
 7 type StringableActionCreator<A extends Action = Action> = {
 8   (...args: unknown[]): A;
 9   toString(): string;
10 };
11-type SubPattern<T> = Predicate<T> | StringableActionCreator | ActionType;
12-export type Pattern<T> = SubPattern<T> | SubPattern<T>[];
13+type SubPattern = Predicate | StringableActionCreator | ActionType;
14+export type Pattern = SubPattern | SubPattern[];
15 type ActionSubPattern<Guard extends Action = Action> =
16   | GuardPredicate<Guard, Action>
17   | StringableActionCreator<Guard>
18-  | Predicate<Action>
19+  | Predicate
20   | ActionType;
21 export type ActionPattern<Guard extends Action = Action> =
22   | ActionSubPattern<Guard>
D mod.ts
+0, -5
1@@ -1,5 +0,0 @@
2-export * from "./fx/mod.ts";
3-export * from "./types.ts";
4-export * from "./iter.ts";
5-export * from "./context.ts";
6-export * from "./compose.ts";
M npm.ts
+16, -6
 1@@ -1,15 +1,21 @@
 2-import { build, emptyDir } from "https://deno.land/x/dnt@0.17.0/mod.ts";
 3-import { assert } from "https://deno.land/std@0.129.0/testing/asserts.ts";
 4+import { assert, build, emptyDir } from "./test.ts";
 5+
 6 await emptyDir("./npm");
 7 
 8 const version = Deno.env.get("NPM_VERSION");
 9 assert(version, "NPM_VERSION is required to build npm package");
10 
11 await build({
12-  entryPoints: ["./mod.ts", "./react.ts", "./redux.ts"],
13+  entryPoints: [
14+    "./index.ts",
15+    "./react.ts",
16+    "./redux/index.ts",
17+    "./query/index.ts",
18+  ],
19   outDir: "./npm",
20   shims: {
21     deno: false,
22+    undici: true,
23   },
24   test: false,
25   typeCheck: false,
26@@ -18,10 +24,9 @@ await build({
27     sourceMap: true,
28   },
29   package: {
30-    // package.json properties
31     name: "starfx",
32     version,
33-    description: "Declarative side-effects for your apps",
34+    description: "",
35     license: "MIT",
36     repository: {
37       author: "me@erock.io",
38@@ -32,8 +37,13 @@ await build({
39       url: "https://github.com/neurosnap/starfx/issues",
40     },
41     engines: {
42-      node: ">= 14",
43+      node: ">= 18",
44     },
45+    sideEffects: false,
46+  },
47+  postBuild() {
48+    Deno.copyFileSync("LICENSE.md", "npm/LICENSE.md");
49+    Deno.copyFileSync("README.md", "npm/README.md");
50   },
51 });
52 
A query/api-types.ts
+1818, -0
   1@@ -0,0 +1,1818 @@
   2+/**
   3+ * This is an auto-generated file, do not edit directly!
   4+ * Run "yarn template" to generate this file.
   5+ */
   6+import type { SagaApi } from "./pipe.ts";
   7+import type {
   8+  ApiCtx,
   9+  CreateAction,
  10+  CreateActionWithPayload,
  11+  FetchJson,
  12+  MiddlewareApiCo,
  13+  Next,
  14+  Payload,
  15+  Supervisor,
  16+} from "./types.ts";
  17+
  18+export type ApiName = string | string[];
  19+
  20+export interface SagaQueryApi<Ctx extends ApiCtx = ApiCtx>
  21+  extends SagaApi<Ctx> {
  22+  request: (
  23+    r: Partial<RequestInit>,
  24+  ) => (ctx: Ctx, next: Next) => Iterator<unknown>;
  25+  cache: () => (ctx: Ctx, next: Next) => Iterator<unknown>;
  26+
  27+  uri: (uri: string) => {
  28+    /**
  29+     * Options only
  30+     */
  31+    get(req: { supervisor?: Supervisor }): CreateAction<Ctx>;
  32+    get<P>(
  33+      req: { supervisor?: Supervisor },
  34+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
  35+    get<P extends never, ApiSuccess, ApiError = unknown>(
  36+      req: { supervisor?: Supervisor },
  37+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
  38+    get<P, ApiSuccess, ApiError = unknown>(req: {
  39+      supervisor?: Supervisor;
  40+    }): CreateActionWithPayload<
  41+      & Omit<Ctx, "payload" | "json">
  42+      & Payload<P>
  43+      & FetchJson<ApiSuccess, ApiError>,
  44+      P
  45+    >;
  46+
  47+    /**
  48+     * Middleware only
  49+     */
  50+    get(fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
  51+    get<Gtx extends Ctx = Ctx>(
  52+      fn: MiddlewareApiCo<Gtx>,
  53+    ): CreateAction<Gtx>;
  54+    get<P>(
  55+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
  56+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
  57+    get<P, Gtx extends Ctx = Ctx>(
  58+      fn: MiddlewareApiCo<Gtx>,
  59+    ): CreateActionWithPayload<Gtx, P>;
  60+    get<P extends never, ApiSuccess, ApiError = unknown>(
  61+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
  62+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
  63+    get<P, ApiSuccess, ApiError = unknown>(
  64+      fn: MiddlewareApiCo<Ctx>,
  65+    ): CreateActionWithPayload<
  66+      & Omit<Ctx, "payload" | "json">
  67+      & Payload<P>
  68+      & FetchJson<ApiSuccess, ApiError>,
  69+      P
  70+    >;
  71+
  72+    /**
  73+     * Options and Middleware
  74+     */
  75+    get(
  76+      req: { supervisor?: Supervisor },
  77+      fn: MiddlewareApiCo<Ctx>,
  78+    ): CreateAction<Ctx>;
  79+    get<Gtx extends Ctx = Ctx>(
  80+      req: { supervisor?: Supervisor },
  81+      fn: MiddlewareApiCo<Gtx>,
  82+    ): CreateAction<Gtx>;
  83+    get<P>(
  84+      req: { supervisor?: Supervisor },
  85+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
  86+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
  87+    get<P, Gtx extends Ctx = Ctx>(
  88+      req: { supervisor?: Supervisor },
  89+      fn: MiddlewareApiCo<Gtx>,
  90+    ): CreateActionWithPayload<Gtx, P>;
  91+    get<P extends never, ApiSuccess, ApiError = unknown>(
  92+      req: { supervisor?: Supervisor },
  93+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
  94+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
  95+    get<P, ApiSuccess, ApiError = unknown>(
  96+      req: { supervisor?: Supervisor },
  97+      fn: MiddlewareApiCo<Ctx>,
  98+    ): CreateActionWithPayload<
  99+      & Omit<Ctx, "payload" | "json">
 100+      & Payload<P>
 101+      & FetchJson<ApiSuccess, ApiError>,
 102+      P
 103+    >;
 104+
 105+    /**
 106+     * Options only
 107+     */
 108+    post(req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 109+    post<P>(
 110+      req: { supervisor?: Supervisor },
 111+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 112+    post<P extends never, ApiSuccess, ApiError = unknown>(
 113+      req: { supervisor?: Supervisor },
 114+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 115+    post<P, ApiSuccess, ApiError = unknown>(req: {
 116+      supervisor?: Supervisor;
 117+    }): CreateActionWithPayload<
 118+      & Omit<Ctx, "payload" | "json">
 119+      & Payload<P>
 120+      & FetchJson<ApiSuccess, ApiError>,
 121+      P
 122+    >;
 123+
 124+    /**
 125+     * Middleware only
 126+     */
 127+    post(fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 128+    post<Gtx extends Ctx = Ctx>(
 129+      fn: MiddlewareApiCo<Gtx>,
 130+    ): CreateAction<Gtx>;
 131+    post<P>(
 132+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 133+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 134+    post<P, Gtx extends Ctx = Ctx>(
 135+      fn: MiddlewareApiCo<Gtx>,
 136+    ): CreateActionWithPayload<Gtx, P>;
 137+    post<P extends never, ApiSuccess, ApiError = unknown>(
 138+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 139+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 140+    post<P, ApiSuccess, ApiError = unknown>(
 141+      fn: MiddlewareApiCo<Ctx>,
 142+    ): CreateActionWithPayload<
 143+      & Omit<Ctx, "payload" | "json">
 144+      & Payload<P>
 145+      & FetchJson<ApiSuccess, ApiError>,
 146+      P
 147+    >;
 148+
 149+    /**
 150+     * Options and Middleware
 151+     */
 152+    post(
 153+      req: { supervisor?: Supervisor },
 154+      fn: MiddlewareApiCo<Ctx>,
 155+    ): CreateAction<Ctx>;
 156+    post<Gtx extends Ctx = Ctx>(
 157+      req: { supervisor?: Supervisor },
 158+      fn: MiddlewareApiCo<Gtx>,
 159+    ): CreateAction<Gtx>;
 160+    post<P>(
 161+      req: { supervisor?: Supervisor },
 162+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 163+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 164+    post<P, Gtx extends Ctx = Ctx>(
 165+      req: { supervisor?: Supervisor },
 166+      fn: MiddlewareApiCo<Gtx>,
 167+    ): CreateActionWithPayload<Gtx, P>;
 168+    post<P extends never, ApiSuccess, ApiError = unknown>(
 169+      req: { supervisor?: Supervisor },
 170+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 171+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 172+    post<P, ApiSuccess, ApiError = unknown>(
 173+      req: { supervisor?: Supervisor },
 174+      fn: MiddlewareApiCo<Ctx>,
 175+    ): CreateActionWithPayload<
 176+      & Omit<Ctx, "payload" | "json">
 177+      & Payload<P>
 178+      & FetchJson<ApiSuccess, ApiError>,
 179+      P
 180+    >;
 181+
 182+    /**
 183+     * Options only
 184+     */
 185+    put(req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 186+    put<P>(
 187+      req: { supervisor?: Supervisor },
 188+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 189+    put<P extends never, ApiSuccess, ApiError = unknown>(
 190+      req: { supervisor?: Supervisor },
 191+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 192+    put<P, ApiSuccess, ApiError = unknown>(req: {
 193+      supervisor?: Supervisor;
 194+    }): CreateActionWithPayload<
 195+      & Omit<Ctx, "payload" | "json">
 196+      & Payload<P>
 197+      & FetchJson<ApiSuccess, ApiError>,
 198+      P
 199+    >;
 200+
 201+    /**
 202+     * Middleware only
 203+     */
 204+    put(fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 205+    put<Gtx extends Ctx = Ctx>(
 206+      fn: MiddlewareApiCo<Gtx>,
 207+    ): CreateAction<Gtx>;
 208+    put<P>(
 209+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 210+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 211+    put<P, Gtx extends Ctx = Ctx>(
 212+      fn: MiddlewareApiCo<Gtx>,
 213+    ): CreateActionWithPayload<Gtx, P>;
 214+    put<P extends never, ApiSuccess, ApiError = unknown>(
 215+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 216+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 217+    put<P, ApiSuccess, ApiError = unknown>(
 218+      fn: MiddlewareApiCo<Ctx>,
 219+    ): CreateActionWithPayload<
 220+      & Omit<Ctx, "payload" | "json">
 221+      & Payload<P>
 222+      & FetchJson<ApiSuccess, ApiError>,
 223+      P
 224+    >;
 225+
 226+    /**
 227+     * Options and Middleware
 228+     */
 229+    put(
 230+      req: { supervisor?: Supervisor },
 231+      fn: MiddlewareApiCo<Ctx>,
 232+    ): CreateAction<Ctx>;
 233+    put<Gtx extends Ctx = Ctx>(
 234+      req: { supervisor?: Supervisor },
 235+      fn: MiddlewareApiCo<Gtx>,
 236+    ): CreateAction<Gtx>;
 237+    put<P>(
 238+      req: { supervisor?: Supervisor },
 239+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 240+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 241+    put<P, Gtx extends Ctx = Ctx>(
 242+      req: { supervisor?: Supervisor },
 243+      fn: MiddlewareApiCo<Gtx>,
 244+    ): CreateActionWithPayload<Gtx, P>;
 245+    put<P extends never, ApiSuccess, ApiError = unknown>(
 246+      req: { supervisor?: Supervisor },
 247+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 248+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 249+    put<P, ApiSuccess, ApiError = unknown>(
 250+      req: { supervisor?: Supervisor },
 251+      fn: MiddlewareApiCo<Ctx>,
 252+    ): CreateActionWithPayload<
 253+      & Omit<Ctx, "payload" | "json">
 254+      & Payload<P>
 255+      & FetchJson<ApiSuccess, ApiError>,
 256+      P
 257+    >;
 258+
 259+    /**
 260+     * Options only
 261+     */
 262+    patch(req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 263+    patch<P>(
 264+      req: { supervisor?: Supervisor },
 265+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 266+    patch<P extends never, ApiSuccess, ApiError = unknown>(
 267+      req: { supervisor?: Supervisor },
 268+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 269+    patch<P, ApiSuccess, ApiError = unknown>(req: {
 270+      supervisor?: Supervisor;
 271+    }): CreateActionWithPayload<
 272+      & Omit<Ctx, "payload" | "json">
 273+      & Payload<P>
 274+      & FetchJson<ApiSuccess, ApiError>,
 275+      P
 276+    >;
 277+
 278+    /**
 279+     * Middleware only
 280+     */
 281+    patch(fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 282+    patch<Gtx extends Ctx = Ctx>(
 283+      fn: MiddlewareApiCo<Gtx>,
 284+    ): CreateAction<Gtx>;
 285+    patch<P>(
 286+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 287+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 288+    patch<P, Gtx extends Ctx = Ctx>(
 289+      fn: MiddlewareApiCo<Gtx>,
 290+    ): CreateActionWithPayload<Gtx, P>;
 291+    patch<P extends never, ApiSuccess, ApiError = unknown>(
 292+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 293+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 294+    patch<P, ApiSuccess, ApiError = unknown>(
 295+      fn: MiddlewareApiCo<Ctx>,
 296+    ): CreateActionWithPayload<
 297+      & Omit<Ctx, "payload" | "json">
 298+      & Payload<P>
 299+      & FetchJson<ApiSuccess, ApiError>,
 300+      P
 301+    >;
 302+
 303+    /**
 304+     * Options and Middleware
 305+     */
 306+    patch(
 307+      req: { supervisor?: Supervisor },
 308+      fn: MiddlewareApiCo<Ctx>,
 309+    ): CreateAction<Ctx>;
 310+    patch<Gtx extends Ctx = Ctx>(
 311+      req: { supervisor?: Supervisor },
 312+      fn: MiddlewareApiCo<Gtx>,
 313+    ): CreateAction<Gtx>;
 314+    patch<P>(
 315+      req: { supervisor?: Supervisor },
 316+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 317+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 318+    patch<P, Gtx extends Ctx = Ctx>(
 319+      req: { supervisor?: Supervisor },
 320+      fn: MiddlewareApiCo<Gtx>,
 321+    ): CreateActionWithPayload<Gtx, P>;
 322+    patch<P extends never, ApiSuccess, ApiError = unknown>(
 323+      req: { supervisor?: Supervisor },
 324+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 325+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 326+    patch<P, ApiSuccess, ApiError = unknown>(
 327+      req: { supervisor?: Supervisor },
 328+      fn: MiddlewareApiCo<Ctx>,
 329+    ): CreateActionWithPayload<
 330+      & Omit<Ctx, "payload" | "json">
 331+      & Payload<P>
 332+      & FetchJson<ApiSuccess, ApiError>,
 333+      P
 334+    >;
 335+
 336+    /**
 337+     * Options only
 338+     */
 339+    delete(req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 340+    delete<P>(
 341+      req: { supervisor?: Supervisor },
 342+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 343+    delete<P extends never, ApiSuccess, ApiError = unknown>(
 344+      req: { supervisor?: Supervisor },
 345+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 346+    delete<P, ApiSuccess, ApiError = unknown>(req: {
 347+      supervisor?: Supervisor;
 348+    }): CreateActionWithPayload<
 349+      & Omit<Ctx, "payload" | "json">
 350+      & Payload<P>
 351+      & FetchJson<ApiSuccess, ApiError>,
 352+      P
 353+    >;
 354+
 355+    /**
 356+     * Middleware only
 357+     */
 358+    delete(fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 359+    delete<Gtx extends Ctx = Ctx>(
 360+      fn: MiddlewareApiCo<Gtx>,
 361+    ): CreateAction<Gtx>;
 362+    delete<P>(
 363+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 364+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 365+    delete<P, Gtx extends Ctx = Ctx>(
 366+      fn: MiddlewareApiCo<Gtx>,
 367+    ): CreateActionWithPayload<Gtx, P>;
 368+    delete<P extends never, ApiSuccess, ApiError = unknown>(
 369+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 370+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 371+    delete<P, ApiSuccess, ApiError = unknown>(
 372+      fn: MiddlewareApiCo<Ctx>,
 373+    ): CreateActionWithPayload<
 374+      & Omit<Ctx, "payload" | "json">
 375+      & Payload<P>
 376+      & FetchJson<ApiSuccess, ApiError>,
 377+      P
 378+    >;
 379+
 380+    /**
 381+     * Options and Middleware
 382+     */
 383+    delete(
 384+      req: { supervisor?: Supervisor },
 385+      fn: MiddlewareApiCo<Ctx>,
 386+    ): CreateAction<Ctx>;
 387+    delete<Gtx extends Ctx = Ctx>(
 388+      req: { supervisor?: Supervisor },
 389+      fn: MiddlewareApiCo<Gtx>,
 390+    ): CreateAction<Gtx>;
 391+    delete<P>(
 392+      req: { supervisor?: Supervisor },
 393+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 394+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 395+    delete<P, Gtx extends Ctx = Ctx>(
 396+      req: { supervisor?: Supervisor },
 397+      fn: MiddlewareApiCo<Gtx>,
 398+    ): CreateActionWithPayload<Gtx, P>;
 399+    delete<P extends never, ApiSuccess, ApiError = unknown>(
 400+      req: { supervisor?: Supervisor },
 401+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 402+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 403+    delete<P, ApiSuccess, ApiError = unknown>(
 404+      req: { supervisor?: Supervisor },
 405+      fn: MiddlewareApiCo<Ctx>,
 406+    ): CreateActionWithPayload<
 407+      & Omit<Ctx, "payload" | "json">
 408+      & Payload<P>
 409+      & FetchJson<ApiSuccess, ApiError>,
 410+      P
 411+    >;
 412+
 413+    /**
 414+     * Options only
 415+     */
 416+    options(req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 417+    options<P>(
 418+      req: { supervisor?: Supervisor },
 419+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 420+    options<P extends never, ApiSuccess, ApiError = unknown>(
 421+      req: { supervisor?: Supervisor },
 422+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 423+    options<P, ApiSuccess, ApiError = unknown>(req: {
 424+      supervisor?: Supervisor;
 425+    }): CreateActionWithPayload<
 426+      & Omit<Ctx, "payload" | "json">
 427+      & Payload<P>
 428+      & FetchJson<ApiSuccess, ApiError>,
 429+      P
 430+    >;
 431+
 432+    /**
 433+     * Middleware only
 434+     */
 435+    options(fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 436+    options<Gtx extends Ctx = Ctx>(
 437+      fn: MiddlewareApiCo<Gtx>,
 438+    ): CreateAction<Gtx>;
 439+    options<P>(
 440+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 441+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 442+    options<P, Gtx extends Ctx = Ctx>(
 443+      fn: MiddlewareApiCo<Gtx>,
 444+    ): CreateActionWithPayload<Gtx, P>;
 445+    options<P extends never, ApiSuccess, ApiError = unknown>(
 446+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 447+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 448+    options<P, ApiSuccess, ApiError = unknown>(
 449+      fn: MiddlewareApiCo<Ctx>,
 450+    ): CreateActionWithPayload<
 451+      & Omit<Ctx, "payload" | "json">
 452+      & Payload<P>
 453+      & FetchJson<ApiSuccess, ApiError>,
 454+      P
 455+    >;
 456+
 457+    /**
 458+     * Options and Middleware
 459+     */
 460+    options(
 461+      req: { supervisor?: Supervisor },
 462+      fn: MiddlewareApiCo<Ctx>,
 463+    ): CreateAction<Ctx>;
 464+    options<Gtx extends Ctx = Ctx>(
 465+      req: { supervisor?: Supervisor },
 466+      fn: MiddlewareApiCo<Gtx>,
 467+    ): CreateAction<Gtx>;
 468+    options<P>(
 469+      req: { supervisor?: Supervisor },
 470+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 471+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 472+    options<P, Gtx extends Ctx = Ctx>(
 473+      req: { supervisor?: Supervisor },
 474+      fn: MiddlewareApiCo<Gtx>,
 475+    ): CreateActionWithPayload<Gtx, P>;
 476+    options<P extends never, ApiSuccess, ApiError = unknown>(
 477+      req: { supervisor?: Supervisor },
 478+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 479+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 480+    options<P, ApiSuccess, ApiError = unknown>(
 481+      req: { supervisor?: Supervisor },
 482+      fn: MiddlewareApiCo<Ctx>,
 483+    ): CreateActionWithPayload<
 484+      & Omit<Ctx, "payload" | "json">
 485+      & Payload<P>
 486+      & FetchJson<ApiSuccess, ApiError>,
 487+      P
 488+    >;
 489+
 490+    /**
 491+     * Options only
 492+     */
 493+    head(req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 494+    head<P>(
 495+      req: { supervisor?: Supervisor },
 496+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 497+    head<P extends never, ApiSuccess, ApiError = unknown>(
 498+      req: { supervisor?: Supervisor },
 499+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 500+    head<P, ApiSuccess, ApiError = unknown>(req: {
 501+      supervisor?: Supervisor;
 502+    }): CreateActionWithPayload<
 503+      & Omit<Ctx, "payload" | "json">
 504+      & Payload<P>
 505+      & FetchJson<ApiSuccess, ApiError>,
 506+      P
 507+    >;
 508+
 509+    /**
 510+     * Middleware only
 511+     */
 512+    head(fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 513+    head<Gtx extends Ctx = Ctx>(
 514+      fn: MiddlewareApiCo<Gtx>,
 515+    ): CreateAction<Gtx>;
 516+    head<P>(
 517+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 518+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 519+    head<P, Gtx extends Ctx = Ctx>(
 520+      fn: MiddlewareApiCo<Gtx>,
 521+    ): CreateActionWithPayload<Gtx, P>;
 522+    head<P extends never, ApiSuccess, ApiError = unknown>(
 523+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 524+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 525+    head<P, ApiSuccess, ApiError = unknown>(
 526+      fn: MiddlewareApiCo<Ctx>,
 527+    ): CreateActionWithPayload<
 528+      & Omit<Ctx, "payload" | "json">
 529+      & Payload<P>
 530+      & FetchJson<ApiSuccess, ApiError>,
 531+      P
 532+    >;
 533+
 534+    /**
 535+     * Options and Middleware
 536+     */
 537+    head(
 538+      req: { supervisor?: Supervisor },
 539+      fn: MiddlewareApiCo<Ctx>,
 540+    ): CreateAction<Ctx>;
 541+    head<Gtx extends Ctx = Ctx>(
 542+      req: { supervisor?: Supervisor },
 543+      fn: MiddlewareApiCo<Gtx>,
 544+    ): CreateAction<Gtx>;
 545+    head<P>(
 546+      req: { supervisor?: Supervisor },
 547+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 548+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 549+    head<P, Gtx extends Ctx = Ctx>(
 550+      req: { supervisor?: Supervisor },
 551+      fn: MiddlewareApiCo<Gtx>,
 552+    ): CreateActionWithPayload<Gtx, P>;
 553+    head<P extends never, ApiSuccess, ApiError = unknown>(
 554+      req: { supervisor?: Supervisor },
 555+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 556+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 557+    head<P, ApiSuccess, ApiError = unknown>(
 558+      req: { supervisor?: Supervisor },
 559+      fn: MiddlewareApiCo<Ctx>,
 560+    ): CreateActionWithPayload<
 561+      & Omit<Ctx, "payload" | "json">
 562+      & Payload<P>
 563+      & FetchJson<ApiSuccess, ApiError>,
 564+      P
 565+    >;
 566+
 567+    /**
 568+     * Options only
 569+     */
 570+    connect(req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 571+    connect<P>(
 572+      req: { supervisor?: Supervisor },
 573+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 574+    connect<P extends never, ApiSuccess, ApiError = unknown>(
 575+      req: { supervisor?: Supervisor },
 576+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 577+    connect<P, ApiSuccess, ApiError = unknown>(req: {
 578+      supervisor?: Supervisor;
 579+    }): CreateActionWithPayload<
 580+      & Omit<Ctx, "payload" | "json">
 581+      & Payload<P>
 582+      & FetchJson<ApiSuccess, ApiError>,
 583+      P
 584+    >;
 585+
 586+    /**
 587+     * Middleware only
 588+     */
 589+    connect(fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 590+    connect<Gtx extends Ctx = Ctx>(
 591+      fn: MiddlewareApiCo<Gtx>,
 592+    ): CreateAction<Gtx>;
 593+    connect<P>(
 594+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 595+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 596+    connect<P, Gtx extends Ctx = Ctx>(
 597+      fn: MiddlewareApiCo<Gtx>,
 598+    ): CreateActionWithPayload<Gtx, P>;
 599+    connect<P extends never, ApiSuccess, ApiError = unknown>(
 600+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 601+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 602+    connect<P, ApiSuccess, ApiError = unknown>(
 603+      fn: MiddlewareApiCo<Ctx>,
 604+    ): CreateActionWithPayload<
 605+      & Omit<Ctx, "payload" | "json">
 606+      & Payload<P>
 607+      & FetchJson<ApiSuccess, ApiError>,
 608+      P
 609+    >;
 610+
 611+    /**
 612+     * Options and Middleware
 613+     */
 614+    connect(
 615+      req: { supervisor?: Supervisor },
 616+      fn: MiddlewareApiCo<Ctx>,
 617+    ): CreateAction<Ctx>;
 618+    connect<Gtx extends Ctx = Ctx>(
 619+      req: { supervisor?: Supervisor },
 620+      fn: MiddlewareApiCo<Gtx>,
 621+    ): CreateAction<Gtx>;
 622+    connect<P>(
 623+      req: { supervisor?: Supervisor },
 624+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 625+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 626+    connect<P, Gtx extends Ctx = Ctx>(
 627+      req: { supervisor?: Supervisor },
 628+      fn: MiddlewareApiCo<Gtx>,
 629+    ): CreateActionWithPayload<Gtx, P>;
 630+    connect<P extends never, ApiSuccess, ApiError = unknown>(
 631+      req: { supervisor?: Supervisor },
 632+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 633+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 634+    connect<P, ApiSuccess, ApiError = unknown>(
 635+      req: { supervisor?: Supervisor },
 636+      fn: MiddlewareApiCo<Ctx>,
 637+    ): CreateActionWithPayload<
 638+      & Omit<Ctx, "payload" | "json">
 639+      & Payload<P>
 640+      & FetchJson<ApiSuccess, ApiError>,
 641+      P
 642+    >;
 643+
 644+    /**
 645+     * Options only
 646+     */
 647+    trace(req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 648+    trace<P>(
 649+      req: { supervisor?: Supervisor },
 650+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 651+    trace<P extends never, ApiSuccess, ApiError = unknown>(
 652+      req: { supervisor?: Supervisor },
 653+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 654+    trace<P, ApiSuccess, ApiError = unknown>(req: {
 655+      supervisor?: Supervisor;
 656+    }): CreateActionWithPayload<
 657+      & Omit<Ctx, "payload" | "json">
 658+      & Payload<P>
 659+      & FetchJson<ApiSuccess, ApiError>,
 660+      P
 661+    >;
 662+
 663+    /**
 664+     * Middleware only
 665+     */
 666+    trace(fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 667+    trace<Gtx extends Ctx = Ctx>(
 668+      fn: MiddlewareApiCo<Gtx>,
 669+    ): CreateAction<Gtx>;
 670+    trace<P>(
 671+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 672+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 673+    trace<P, Gtx extends Ctx = Ctx>(
 674+      fn: MiddlewareApiCo<Gtx>,
 675+    ): CreateActionWithPayload<Gtx, P>;
 676+    trace<P extends never, ApiSuccess, ApiError = unknown>(
 677+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 678+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 679+    trace<P, ApiSuccess, ApiError = unknown>(
 680+      fn: MiddlewareApiCo<Ctx>,
 681+    ): CreateActionWithPayload<
 682+      & Omit<Ctx, "payload" | "json">
 683+      & Payload<P>
 684+      & FetchJson<ApiSuccess, ApiError>,
 685+      P
 686+    >;
 687+
 688+    /**
 689+     * Options and Middleware
 690+     */
 691+    trace(
 692+      req: { supervisor?: Supervisor },
 693+      fn: MiddlewareApiCo<Ctx>,
 694+    ): CreateAction<Ctx>;
 695+    trace<Gtx extends Ctx = Ctx>(
 696+      req: { supervisor?: Supervisor },
 697+      fn: MiddlewareApiCo<Gtx>,
 698+    ): CreateAction<Gtx>;
 699+    trace<P>(
 700+      req: { supervisor?: Supervisor },
 701+      fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 702+    ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 703+    trace<P, Gtx extends Ctx = Ctx>(
 704+      req: { supervisor?: Supervisor },
 705+      fn: MiddlewareApiCo<Gtx>,
 706+    ): CreateActionWithPayload<Gtx, P>;
 707+    trace<P extends never, ApiSuccess, ApiError = unknown>(
 708+      req: { supervisor?: Supervisor },
 709+      fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 710+    ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 711+    trace<P, ApiSuccess, ApiError = unknown>(
 712+      req: { supervisor?: Supervisor },
 713+      fn: MiddlewareApiCo<Ctx>,
 714+    ): CreateActionWithPayload<
 715+      & Omit<Ctx, "payload" | "json">
 716+      & Payload<P>
 717+      & FetchJson<ApiSuccess, ApiError>,
 718+      P
 719+    >;
 720+  };
 721+
 722+  /**
 723+   * Only name
 724+   */
 725+  get(name: ApiName): CreateAction<Ctx>;
 726+  get<P>(
 727+    name: ApiName,
 728+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 729+  get<P extends never, ApiSuccess, ApiError = unknown>(
 730+    name: ApiName,
 731+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 732+  get<P, ApiSuccess, ApiError = unknown>(
 733+    name: ApiName,
 734+  ): CreateActionWithPayload<
 735+    & Omit<Ctx, "payload" | "json">
 736+    & Payload<P>
 737+    & FetchJson<ApiSuccess, ApiError>,
 738+    P
 739+  >;
 740+
 741+  /**
 742+   * Name and options
 743+   */
 744+  get(name: ApiName, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 745+  get<P>(
 746+    name: ApiName,
 747+    req: { supervisor?: Supervisor },
 748+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 749+  get<P, Gtx extends Ctx = Ctx>(
 750+    name: ApiName,
 751+    req: { supervisor?: Supervisor },
 752+  ): CreateActionWithPayload<Gtx, P>;
 753+  get<P extends never, ApiSuccess, ApiError = unknown>(
 754+    name: ApiName,
 755+    req: { supervisor?: Supervisor },
 756+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 757+  get<P, ApiSuccess, ApiError = unknown>(
 758+    name: ApiName,
 759+    req: { supervisor?: Supervisor },
 760+  ): CreateActionWithPayload<
 761+    & Omit<Ctx, "payload" | "json">
 762+    & Payload<P>
 763+    & FetchJson<ApiSuccess, ApiError>,
 764+    P
 765+  >;
 766+
 767+  /**
 768+   * Name and middleware
 769+   */
 770+  get(name: ApiName, fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 771+  get<Gtx extends Ctx = Ctx>(
 772+    name: ApiName,
 773+    fn: MiddlewareApiCo<Gtx>,
 774+  ): CreateAction<Gtx>;
 775+  get<P>(
 776+    name: ApiName,
 777+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 778+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 779+  get<P, Gtx extends Ctx = Ctx>(
 780+    name: ApiName,
 781+    fn: MiddlewareApiCo<Gtx>,
 782+  ): CreateActionWithPayload<Gtx, P>;
 783+  get<P extends never, ApiSuccess, ApiError = unknown>(
 784+    name: ApiName,
 785+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 786+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 787+  get<P, ApiSuccess, ApiError = unknown>(
 788+    name: ApiName,
 789+    fn: MiddlewareApiCo<
 790+      & Omit<Ctx, "payload" | "json">
 791+      & Payload<P>
 792+      & FetchJson<ApiSuccess, ApiError>
 793+    >,
 794+  ): CreateActionWithPayload<
 795+    & Omit<Ctx, "payload" | "json">
 796+    & Payload<P>
 797+    & FetchJson<ApiSuccess, ApiError>,
 798+    P
 799+  >;
 800+
 801+  /**
 802+   * Name, options, and middleware
 803+   */
 804+  get(
 805+    name: ApiName,
 806+    req: { supervisor?: Supervisor },
 807+    fn: MiddlewareApiCo<Ctx>,
 808+  ): CreateAction<Ctx>;
 809+  get<Gtx extends Ctx = Ctx>(
 810+    name: ApiName,
 811+    req: { supervisor?: Supervisor },
 812+    fn: MiddlewareApiCo<Gtx>,
 813+  ): CreateAction<Gtx>;
 814+  get<P>(
 815+    name: ApiName,
 816+    req: { supervisor?: Supervisor },
 817+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 818+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 819+  get<P, Gtx extends Ctx = Ctx>(
 820+    name: ApiName,
 821+    req: { supervisor?: Supervisor },
 822+    fn: MiddlewareApiCo<Gtx>,
 823+  ): CreateActionWithPayload<Gtx, P>;
 824+  get<P extends never, ApiSuccess, ApiError = unknown>(
 825+    name: ApiName,
 826+    req: { supervisor?: Supervisor },
 827+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 828+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 829+  get<P, ApiSuccess, ApiError = unknown>(
 830+    name: ApiName,
 831+    req: { supervisor?: Supervisor },
 832+    fn: MiddlewareApiCo<
 833+      & Omit<Ctx, "payload" | "json">
 834+      & Payload<P>
 835+      & FetchJson<ApiSuccess, ApiError>
 836+    >,
 837+  ): CreateActionWithPayload<
 838+    & Omit<Ctx, "payload" | "json">
 839+    & Payload<P>
 840+    & FetchJson<ApiSuccess, ApiError>,
 841+    P
 842+  >;
 843+
 844+  /**
 845+   * Only name
 846+   */
 847+  post(name: ApiName): CreateAction<Ctx>;
 848+  post<P>(
 849+    name: ApiName,
 850+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 851+  post<P extends never, ApiSuccess, ApiError = unknown>(
 852+    name: ApiName,
 853+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 854+  post<P, ApiSuccess, ApiError = unknown>(
 855+    name: ApiName,
 856+  ): CreateActionWithPayload<
 857+    & Omit<Ctx, "payload" | "json">
 858+    & Payload<P>
 859+    & FetchJson<ApiSuccess, ApiError>,
 860+    P
 861+  >;
 862+
 863+  /**
 864+   * Name and options
 865+   */
 866+  post(name: ApiName, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 867+  post<P>(
 868+    name: ApiName,
 869+    req: { supervisor?: Supervisor },
 870+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 871+  post<P, Gtx extends Ctx = Ctx>(
 872+    name: ApiName,
 873+    req: { supervisor?: Supervisor },
 874+  ): CreateActionWithPayload<Gtx, P>;
 875+  post<P extends never, ApiSuccess, ApiError = unknown>(
 876+    name: ApiName,
 877+    req: { supervisor?: Supervisor },
 878+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 879+  post<P, ApiSuccess, ApiError = unknown>(
 880+    name: ApiName,
 881+    req: { supervisor?: Supervisor },
 882+  ): CreateActionWithPayload<
 883+    & Omit<Ctx, "payload" | "json">
 884+    & Payload<P>
 885+    & FetchJson<ApiSuccess, ApiError>,
 886+    P
 887+  >;
 888+
 889+  /**
 890+   * Name and middleware
 891+   */
 892+  post(name: ApiName, fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
 893+  post<Gtx extends Ctx = Ctx>(
 894+    name: ApiName,
 895+    fn: MiddlewareApiCo<Gtx>,
 896+  ): CreateAction<Gtx>;
 897+  post<P>(
 898+    name: ApiName,
 899+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 900+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 901+  post<P, Gtx extends Ctx = Ctx>(
 902+    name: ApiName,
 903+    fn: MiddlewareApiCo<Gtx>,
 904+  ): CreateActionWithPayload<Gtx, P>;
 905+  post<P extends never, ApiSuccess, ApiError = unknown>(
 906+    name: ApiName,
 907+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 908+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 909+  post<P, ApiSuccess, ApiError = unknown>(
 910+    name: ApiName,
 911+    fn: MiddlewareApiCo<
 912+      & Omit<Ctx, "payload" | "json">
 913+      & Payload<P>
 914+      & FetchJson<ApiSuccess, ApiError>
 915+    >,
 916+  ): CreateActionWithPayload<
 917+    & Omit<Ctx, "payload" | "json">
 918+    & Payload<P>
 919+    & FetchJson<ApiSuccess, ApiError>,
 920+    P
 921+  >;
 922+
 923+  /**
 924+   * Name, options, and middleware
 925+   */
 926+  post(
 927+    name: ApiName,
 928+    req: { supervisor?: Supervisor },
 929+    fn: MiddlewareApiCo<Ctx>,
 930+  ): CreateAction<Ctx>;
 931+  post<Gtx extends Ctx = Ctx>(
 932+    name: ApiName,
 933+    req: { supervisor?: Supervisor },
 934+    fn: MiddlewareApiCo<Gtx>,
 935+  ): CreateAction<Gtx>;
 936+  post<P>(
 937+    name: ApiName,
 938+    req: { supervisor?: Supervisor },
 939+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
 940+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 941+  post<P, Gtx extends Ctx = Ctx>(
 942+    name: ApiName,
 943+    req: { supervisor?: Supervisor },
 944+    fn: MiddlewareApiCo<Gtx>,
 945+  ): CreateActionWithPayload<Gtx, P>;
 946+  post<P extends never, ApiSuccess, ApiError = unknown>(
 947+    name: ApiName,
 948+    req: { supervisor?: Supervisor },
 949+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
 950+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 951+  post<P, ApiSuccess, ApiError = unknown>(
 952+    name: ApiName,
 953+    req: { supervisor?: Supervisor },
 954+    fn: MiddlewareApiCo<
 955+      & Omit<Ctx, "payload" | "json">
 956+      & Payload<P>
 957+      & FetchJson<ApiSuccess, ApiError>
 958+    >,
 959+  ): CreateActionWithPayload<
 960+    & Omit<Ctx, "payload" | "json">
 961+    & Payload<P>
 962+    & FetchJson<ApiSuccess, ApiError>,
 963+    P
 964+  >;
 965+
 966+  /**
 967+   * Only name
 968+   */
 969+  put(name: ApiName): CreateAction<Ctx>;
 970+  put<P>(
 971+    name: ApiName,
 972+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 973+  put<P extends never, ApiSuccess, ApiError = unknown>(
 974+    name: ApiName,
 975+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
 976+  put<P, ApiSuccess, ApiError = unknown>(
 977+    name: ApiName,
 978+  ): CreateActionWithPayload<
 979+    & Omit<Ctx, "payload" | "json">
 980+    & Payload<P>
 981+    & FetchJson<ApiSuccess, ApiError>,
 982+    P
 983+  >;
 984+
 985+  /**
 986+   * Name and options
 987+   */
 988+  put(name: ApiName, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 989+  put<P>(
 990+    name: ApiName,
 991+    req: { supervisor?: Supervisor },
 992+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 993+  put<P, Gtx extends Ctx = Ctx>(
 994+    name: ApiName,
 995+    req: { supervisor?: Supervisor },
 996+  ): CreateActionWithPayload<Gtx, P>;
 997+  put<P extends never, ApiSuccess, ApiError = unknown>(
 998+    name: ApiName,
 999+    req: { supervisor?: Supervisor },
1000+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1001+  put<P, ApiSuccess, ApiError = unknown>(
1002+    name: ApiName,
1003+    req: { supervisor?: Supervisor },
1004+  ): CreateActionWithPayload<
1005+    & Omit<Ctx, "payload" | "json">
1006+    & Payload<P>
1007+    & FetchJson<ApiSuccess, ApiError>,
1008+    P
1009+  >;
1010+
1011+  /**
1012+   * Name and middleware
1013+   */
1014+  put(name: ApiName, fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
1015+  put<Gtx extends Ctx = Ctx>(
1016+    name: ApiName,
1017+    fn: MiddlewareApiCo<Gtx>,
1018+  ): CreateAction<Gtx>;
1019+  put<P>(
1020+    name: ApiName,
1021+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1022+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1023+  put<P, Gtx extends Ctx = Ctx>(
1024+    name: ApiName,
1025+    fn: MiddlewareApiCo<Gtx>,
1026+  ): CreateActionWithPayload<Gtx, P>;
1027+  put<P extends never, ApiSuccess, ApiError = unknown>(
1028+    name: ApiName,
1029+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1030+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1031+  put<P, ApiSuccess, ApiError = unknown>(
1032+    name: ApiName,
1033+    fn: MiddlewareApiCo<
1034+      & Omit<Ctx, "payload" | "json">
1035+      & Payload<P>
1036+      & FetchJson<ApiSuccess, ApiError>
1037+    >,
1038+  ): CreateActionWithPayload<
1039+    & Omit<Ctx, "payload" | "json">
1040+    & Payload<P>
1041+    & FetchJson<ApiSuccess, ApiError>,
1042+    P
1043+  >;
1044+
1045+  /**
1046+   * Name, options, and middleware
1047+   */
1048+  put(
1049+    name: ApiName,
1050+    req: { supervisor?: Supervisor },
1051+    fn: MiddlewareApiCo<Ctx>,
1052+  ): CreateAction<Ctx>;
1053+  put<Gtx extends Ctx = Ctx>(
1054+    name: ApiName,
1055+    req: { supervisor?: Supervisor },
1056+    fn: MiddlewareApiCo<Gtx>,
1057+  ): CreateAction<Gtx>;
1058+  put<P>(
1059+    name: ApiName,
1060+    req: { supervisor?: Supervisor },
1061+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1062+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1063+  put<P, Gtx extends Ctx = Ctx>(
1064+    name: ApiName,
1065+    req: { supervisor?: Supervisor },
1066+    fn: MiddlewareApiCo<Gtx>,
1067+  ): CreateActionWithPayload<Gtx, P>;
1068+  put<P extends never, ApiSuccess, ApiError = unknown>(
1069+    name: ApiName,
1070+    req: { supervisor?: Supervisor },
1071+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1072+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1073+  put<P, ApiSuccess, ApiError = unknown>(
1074+    name: ApiName,
1075+    req: { supervisor?: Supervisor },
1076+    fn: MiddlewareApiCo<
1077+      & Omit<Ctx, "payload" | "json">
1078+      & Payload<P>
1079+      & FetchJson<ApiSuccess, ApiError>
1080+    >,
1081+  ): CreateActionWithPayload<
1082+    & Omit<Ctx, "payload" | "json">
1083+    & Payload<P>
1084+    & FetchJson<ApiSuccess, ApiError>,
1085+    P
1086+  >;
1087+
1088+  /**
1089+   * Only name
1090+   */
1091+  patch(name: ApiName): CreateAction<Ctx>;
1092+  patch<P>(
1093+    name: ApiName,
1094+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1095+  patch<P extends never, ApiSuccess, ApiError = unknown>(
1096+    name: ApiName,
1097+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1098+  patch<P, ApiSuccess, ApiError = unknown>(
1099+    name: ApiName,
1100+  ): CreateActionWithPayload<
1101+    & Omit<Ctx, "payload" | "json">
1102+    & Payload<P>
1103+    & FetchJson<ApiSuccess, ApiError>,
1104+    P
1105+  >;
1106+
1107+  /**
1108+   * Name and options
1109+   */
1110+  patch(name: ApiName, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
1111+  patch<P>(
1112+    name: ApiName,
1113+    req: { supervisor?: Supervisor },
1114+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1115+  patch<P, Gtx extends Ctx = Ctx>(
1116+    name: ApiName,
1117+    req: { supervisor?: Supervisor },
1118+  ): CreateActionWithPayload<Gtx, P>;
1119+  patch<P extends never, ApiSuccess, ApiError = unknown>(
1120+    name: ApiName,
1121+    req: { supervisor?: Supervisor },
1122+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1123+  patch<P, ApiSuccess, ApiError = unknown>(
1124+    name: ApiName,
1125+    req: { supervisor?: Supervisor },
1126+  ): CreateActionWithPayload<
1127+    & Omit<Ctx, "payload" | "json">
1128+    & Payload<P>
1129+    & FetchJson<ApiSuccess, ApiError>,
1130+    P
1131+  >;
1132+
1133+  /**
1134+   * Name and middleware
1135+   */
1136+  patch(name: ApiName, fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
1137+  patch<Gtx extends Ctx = Ctx>(
1138+    name: ApiName,
1139+    fn: MiddlewareApiCo<Gtx>,
1140+  ): CreateAction<Gtx>;
1141+  patch<P>(
1142+    name: ApiName,
1143+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1144+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1145+  patch<P, Gtx extends Ctx = Ctx>(
1146+    name: ApiName,
1147+    fn: MiddlewareApiCo<Gtx>,
1148+  ): CreateActionWithPayload<Gtx, P>;
1149+  patch<P extends never, ApiSuccess, ApiError = unknown>(
1150+    name: ApiName,
1151+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1152+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1153+  patch<P, ApiSuccess, ApiError = unknown>(
1154+    name: ApiName,
1155+    fn: MiddlewareApiCo<
1156+      & Omit<Ctx, "payload" | "json">
1157+      & Payload<P>
1158+      & FetchJson<ApiSuccess, ApiError>
1159+    >,
1160+  ): CreateActionWithPayload<
1161+    & Omit<Ctx, "payload" | "json">
1162+    & Payload<P>
1163+    & FetchJson<ApiSuccess, ApiError>,
1164+    P
1165+  >;
1166+
1167+  /**
1168+   * Name, options, and middleware
1169+   */
1170+  patch(
1171+    name: ApiName,
1172+    req: { supervisor?: Supervisor },
1173+    fn: MiddlewareApiCo<Ctx>,
1174+  ): CreateAction<Ctx>;
1175+  patch<Gtx extends Ctx = Ctx>(
1176+    name: ApiName,
1177+    req: { supervisor?: Supervisor },
1178+    fn: MiddlewareApiCo<Gtx>,
1179+  ): CreateAction<Gtx>;
1180+  patch<P>(
1181+    name: ApiName,
1182+    req: { supervisor?: Supervisor },
1183+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1184+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1185+  patch<P, Gtx extends Ctx = Ctx>(
1186+    name: ApiName,
1187+    req: { supervisor?: Supervisor },
1188+    fn: MiddlewareApiCo<Gtx>,
1189+  ): CreateActionWithPayload<Gtx, P>;
1190+  patch<P extends never, ApiSuccess, ApiError = unknown>(
1191+    name: ApiName,
1192+    req: { supervisor?: Supervisor },
1193+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1194+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1195+  patch<P, ApiSuccess, ApiError = unknown>(
1196+    name: ApiName,
1197+    req: { supervisor?: Supervisor },
1198+    fn: MiddlewareApiCo<
1199+      & Omit<Ctx, "payload" | "json">
1200+      & Payload<P>
1201+      & FetchJson<ApiSuccess, ApiError>
1202+    >,
1203+  ): CreateActionWithPayload<
1204+    & Omit<Ctx, "payload" | "json">
1205+    & Payload<P>
1206+    & FetchJson<ApiSuccess, ApiError>,
1207+    P
1208+  >;
1209+
1210+  /**
1211+   * Only name
1212+   */
1213+  delete(name: ApiName): CreateAction<Ctx>;
1214+  delete<P>(
1215+    name: ApiName,
1216+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1217+  delete<P extends never, ApiSuccess, ApiError = unknown>(
1218+    name: ApiName,
1219+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1220+  delete<P, ApiSuccess, ApiError = unknown>(
1221+    name: ApiName,
1222+  ): CreateActionWithPayload<
1223+    & Omit<Ctx, "payload" | "json">
1224+    & Payload<P>
1225+    & FetchJson<ApiSuccess, ApiError>,
1226+    P
1227+  >;
1228+
1229+  /**
1230+   * Name and options
1231+   */
1232+  delete(name: ApiName, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
1233+  delete<P>(
1234+    name: ApiName,
1235+    req: { supervisor?: Supervisor },
1236+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1237+  delete<P, Gtx extends Ctx = Ctx>(
1238+    name: ApiName,
1239+    req: { supervisor?: Supervisor },
1240+  ): CreateActionWithPayload<Gtx, P>;
1241+  delete<P extends never, ApiSuccess, ApiError = unknown>(
1242+    name: ApiName,
1243+    req: { supervisor?: Supervisor },
1244+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1245+  delete<P, ApiSuccess, ApiError = unknown>(
1246+    name: ApiName,
1247+    req: { supervisor?: Supervisor },
1248+  ): CreateActionWithPayload<
1249+    & Omit<Ctx, "payload" | "json">
1250+    & Payload<P>
1251+    & FetchJson<ApiSuccess, ApiError>,
1252+    P
1253+  >;
1254+
1255+  /**
1256+   * Name and middleware
1257+   */
1258+  delete(name: ApiName, fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
1259+  delete<Gtx extends Ctx = Ctx>(
1260+    name: ApiName,
1261+    fn: MiddlewareApiCo<Gtx>,
1262+  ): CreateAction<Gtx>;
1263+  delete<P>(
1264+    name: ApiName,
1265+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1266+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1267+  delete<P, Gtx extends Ctx = Ctx>(
1268+    name: ApiName,
1269+    fn: MiddlewareApiCo<Gtx>,
1270+  ): CreateActionWithPayload<Gtx, P>;
1271+  delete<P extends never, ApiSuccess, ApiError = unknown>(
1272+    name: ApiName,
1273+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1274+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1275+  delete<P, ApiSuccess, ApiError = unknown>(
1276+    name: ApiName,
1277+    fn: MiddlewareApiCo<
1278+      & Omit<Ctx, "payload" | "json">
1279+      & Payload<P>
1280+      & FetchJson<ApiSuccess, ApiError>
1281+    >,
1282+  ): CreateActionWithPayload<
1283+    & Omit<Ctx, "payload" | "json">
1284+    & Payload<P>
1285+    & FetchJson<ApiSuccess, ApiError>,
1286+    P
1287+  >;
1288+
1289+  /**
1290+   * Name, options, and middleware
1291+   */
1292+  delete(
1293+    name: ApiName,
1294+    req: { supervisor?: Supervisor },
1295+    fn: MiddlewareApiCo<Ctx>,
1296+  ): CreateAction<Ctx>;
1297+  delete<Gtx extends Ctx = Ctx>(
1298+    name: ApiName,
1299+    req: { supervisor?: Supervisor },
1300+    fn: MiddlewareApiCo<Gtx>,
1301+  ): CreateAction<Gtx>;
1302+  delete<P>(
1303+    name: ApiName,
1304+    req: { supervisor?: Supervisor },
1305+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1306+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1307+  delete<P, Gtx extends Ctx = Ctx>(
1308+    name: ApiName,
1309+    req: { supervisor?: Supervisor },
1310+    fn: MiddlewareApiCo<Gtx>,
1311+  ): CreateActionWithPayload<Gtx, P>;
1312+  delete<P extends never, ApiSuccess, ApiError = unknown>(
1313+    name: ApiName,
1314+    req: { supervisor?: Supervisor },
1315+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1316+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1317+  delete<P, ApiSuccess, ApiError = unknown>(
1318+    name: ApiName,
1319+    req: { supervisor?: Supervisor },
1320+    fn: MiddlewareApiCo<
1321+      & Omit<Ctx, "payload" | "json">
1322+      & Payload<P>
1323+      & FetchJson<ApiSuccess, ApiError>
1324+    >,
1325+  ): CreateActionWithPayload<
1326+    & Omit<Ctx, "payload" | "json">
1327+    & Payload<P>
1328+    & FetchJson<ApiSuccess, ApiError>,
1329+    P
1330+  >;
1331+
1332+  /**
1333+   * Only name
1334+   */
1335+  options(name: ApiName): CreateAction<Ctx>;
1336+  options<P>(
1337+    name: ApiName,
1338+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1339+  options<P extends never, ApiSuccess, ApiError = unknown>(
1340+    name: ApiName,
1341+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1342+  options<P, ApiSuccess, ApiError = unknown>(
1343+    name: ApiName,
1344+  ): CreateActionWithPayload<
1345+    & Omit<Ctx, "payload" | "json">
1346+    & Payload<P>
1347+    & FetchJson<ApiSuccess, ApiError>,
1348+    P
1349+  >;
1350+
1351+  /**
1352+   * Name and options
1353+   */
1354+  options(name: ApiName, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
1355+  options<P>(
1356+    name: ApiName,
1357+    req: { supervisor?: Supervisor },
1358+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1359+  options<P, Gtx extends Ctx = Ctx>(
1360+    name: ApiName,
1361+    req: { supervisor?: Supervisor },
1362+  ): CreateActionWithPayload<Gtx, P>;
1363+  options<P extends never, ApiSuccess, ApiError = unknown>(
1364+    name: ApiName,
1365+    req: { supervisor?: Supervisor },
1366+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1367+  options<P, ApiSuccess, ApiError = unknown>(
1368+    name: ApiName,
1369+    req: { supervisor?: Supervisor },
1370+  ): CreateActionWithPayload<
1371+    & Omit<Ctx, "payload" | "json">
1372+    & Payload<P>
1373+    & FetchJson<ApiSuccess, ApiError>,
1374+    P
1375+  >;
1376+
1377+  /**
1378+   * Name and middleware
1379+   */
1380+  options(name: ApiName, fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
1381+  options<Gtx extends Ctx = Ctx>(
1382+    name: ApiName,
1383+    fn: MiddlewareApiCo<Gtx>,
1384+  ): CreateAction<Gtx>;
1385+  options<P>(
1386+    name: ApiName,
1387+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1388+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1389+  options<P, Gtx extends Ctx = Ctx>(
1390+    name: ApiName,
1391+    fn: MiddlewareApiCo<Gtx>,
1392+  ): CreateActionWithPayload<Gtx, P>;
1393+  options<P extends never, ApiSuccess, ApiError = unknown>(
1394+    name: ApiName,
1395+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1396+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1397+  options<P, ApiSuccess, ApiError = unknown>(
1398+    name: ApiName,
1399+    fn: MiddlewareApiCo<
1400+      & Omit<Ctx, "payload" | "json">
1401+      & Payload<P>
1402+      & FetchJson<ApiSuccess, ApiError>
1403+    >,
1404+  ): CreateActionWithPayload<
1405+    & Omit<Ctx, "payload" | "json">
1406+    & Payload<P>
1407+    & FetchJson<ApiSuccess, ApiError>,
1408+    P
1409+  >;
1410+
1411+  /**
1412+   * Name, options, and middleware
1413+   */
1414+  options(
1415+    name: ApiName,
1416+    req: { supervisor?: Supervisor },
1417+    fn: MiddlewareApiCo<Ctx>,
1418+  ): CreateAction<Ctx>;
1419+  options<Gtx extends Ctx = Ctx>(
1420+    name: ApiName,
1421+    req: { supervisor?: Supervisor },
1422+    fn: MiddlewareApiCo<Gtx>,
1423+  ): CreateAction<Gtx>;
1424+  options<P>(
1425+    name: ApiName,
1426+    req: { supervisor?: Supervisor },
1427+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1428+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1429+  options<P, Gtx extends Ctx = Ctx>(
1430+    name: ApiName,
1431+    req: { supervisor?: Supervisor },
1432+    fn: MiddlewareApiCo<Gtx>,
1433+  ): CreateActionWithPayload<Gtx, P>;
1434+  options<P extends never, ApiSuccess, ApiError = unknown>(
1435+    name: ApiName,
1436+    req: { supervisor?: Supervisor },
1437+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1438+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1439+  options<P, ApiSuccess, ApiError = unknown>(
1440+    name: ApiName,
1441+    req: { supervisor?: Supervisor },
1442+    fn: MiddlewareApiCo<
1443+      & Omit<Ctx, "payload" | "json">
1444+      & Payload<P>
1445+      & FetchJson<ApiSuccess, ApiError>
1446+    >,
1447+  ): CreateActionWithPayload<
1448+    & Omit<Ctx, "payload" | "json">
1449+    & Payload<P>
1450+    & FetchJson<ApiSuccess, ApiError>,
1451+    P
1452+  >;
1453+
1454+  /**
1455+   * Only name
1456+   */
1457+  head(name: ApiName): CreateAction<Ctx>;
1458+  head<P>(
1459+    name: ApiName,
1460+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1461+  head<P extends never, ApiSuccess, ApiError = unknown>(
1462+    name: ApiName,
1463+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1464+  head<P, ApiSuccess, ApiError = unknown>(
1465+    name: ApiName,
1466+  ): CreateActionWithPayload<
1467+    & Omit<Ctx, "payload" | "json">
1468+    & Payload<P>
1469+    & FetchJson<ApiSuccess, ApiError>,
1470+    P
1471+  >;
1472+
1473+  /**
1474+   * Name and options
1475+   */
1476+  head(name: ApiName, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
1477+  head<P>(
1478+    name: ApiName,
1479+    req: { supervisor?: Supervisor },
1480+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1481+  head<P, Gtx extends Ctx = Ctx>(
1482+    name: ApiName,
1483+    req: { supervisor?: Supervisor },
1484+  ): CreateActionWithPayload<Gtx, P>;
1485+  head<P extends never, ApiSuccess, ApiError = unknown>(
1486+    name: ApiName,
1487+    req: { supervisor?: Supervisor },
1488+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1489+  head<P, ApiSuccess, ApiError = unknown>(
1490+    name: ApiName,
1491+    req: { supervisor?: Supervisor },
1492+  ): CreateActionWithPayload<
1493+    & Omit<Ctx, "payload" | "json">
1494+    & Payload<P>
1495+    & FetchJson<ApiSuccess, ApiError>,
1496+    P
1497+  >;
1498+
1499+  /**
1500+   * Name and middleware
1501+   */
1502+  head(name: ApiName, fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
1503+  head<Gtx extends Ctx = Ctx>(
1504+    name: ApiName,
1505+    fn: MiddlewareApiCo<Gtx>,
1506+  ): CreateAction<Gtx>;
1507+  head<P>(
1508+    name: ApiName,
1509+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1510+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1511+  head<P, Gtx extends Ctx = Ctx>(
1512+    name: ApiName,
1513+    fn: MiddlewareApiCo<Gtx>,
1514+  ): CreateActionWithPayload<Gtx, P>;
1515+  head<P extends never, ApiSuccess, ApiError = unknown>(
1516+    name: ApiName,
1517+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1518+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1519+  head<P, ApiSuccess, ApiError = unknown>(
1520+    name: ApiName,
1521+    fn: MiddlewareApiCo<
1522+      & Omit<Ctx, "payload" | "json">
1523+      & Payload<P>
1524+      & FetchJson<ApiSuccess, ApiError>
1525+    >,
1526+  ): CreateActionWithPayload<
1527+    & Omit<Ctx, "payload" | "json">
1528+    & Payload<P>
1529+    & FetchJson<ApiSuccess, ApiError>,
1530+    P
1531+  >;
1532+
1533+  /**
1534+   * Name, options, and middleware
1535+   */
1536+  head(
1537+    name: ApiName,
1538+    req: { supervisor?: Supervisor },
1539+    fn: MiddlewareApiCo<Ctx>,
1540+  ): CreateAction<Ctx>;
1541+  head<Gtx extends Ctx = Ctx>(
1542+    name: ApiName,
1543+    req: { supervisor?: Supervisor },
1544+    fn: MiddlewareApiCo<Gtx>,
1545+  ): CreateAction<Gtx>;
1546+  head<P>(
1547+    name: ApiName,
1548+    req: { supervisor?: Supervisor },
1549+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1550+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1551+  head<P, Gtx extends Ctx = Ctx>(
1552+    name: ApiName,
1553+    req: { supervisor?: Supervisor },
1554+    fn: MiddlewareApiCo<Gtx>,
1555+  ): CreateActionWithPayload<Gtx, P>;
1556+  head<P extends never, ApiSuccess, ApiError = unknown>(
1557+    name: ApiName,
1558+    req: { supervisor?: Supervisor },
1559+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1560+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1561+  head<P, ApiSuccess, ApiError = unknown>(
1562+    name: ApiName,
1563+    req: { supervisor?: Supervisor },
1564+    fn: MiddlewareApiCo<
1565+      & Omit<Ctx, "payload" | "json">
1566+      & Payload<P>
1567+      & FetchJson<ApiSuccess, ApiError>
1568+    >,
1569+  ): CreateActionWithPayload<
1570+    & Omit<Ctx, "payload" | "json">
1571+    & Payload<P>
1572+    & FetchJson<ApiSuccess, ApiError>,
1573+    P
1574+  >;
1575+
1576+  /**
1577+   * Only name
1578+   */
1579+  connect(name: ApiName): CreateAction<Ctx>;
1580+  connect<P>(
1581+    name: ApiName,
1582+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1583+  connect<P extends never, ApiSuccess, ApiError = unknown>(
1584+    name: ApiName,
1585+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1586+  connect<P, ApiSuccess, ApiError = unknown>(
1587+    name: ApiName,
1588+  ): CreateActionWithPayload<
1589+    & Omit<Ctx, "payload" | "json">
1590+    & Payload<P>
1591+    & FetchJson<ApiSuccess, ApiError>,
1592+    P
1593+  >;
1594+
1595+  /**
1596+   * Name and options
1597+   */
1598+  connect(name: ApiName, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
1599+  connect<P>(
1600+    name: ApiName,
1601+    req: { supervisor?: Supervisor },
1602+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1603+  connect<P, Gtx extends Ctx = Ctx>(
1604+    name: ApiName,
1605+    req: { supervisor?: Supervisor },
1606+  ): CreateActionWithPayload<Gtx, P>;
1607+  connect<P extends never, ApiSuccess, ApiError = unknown>(
1608+    name: ApiName,
1609+    req: { supervisor?: Supervisor },
1610+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1611+  connect<P, ApiSuccess, ApiError = unknown>(
1612+    name: ApiName,
1613+    req: { supervisor?: Supervisor },
1614+  ): CreateActionWithPayload<
1615+    & Omit<Ctx, "payload" | "json">
1616+    & Payload<P>
1617+    & FetchJson<ApiSuccess, ApiError>,
1618+    P
1619+  >;
1620+
1621+  /**
1622+   * Name and middleware
1623+   */
1624+  connect(name: ApiName, fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
1625+  connect<Gtx extends Ctx = Ctx>(
1626+    name: ApiName,
1627+    fn: MiddlewareApiCo<Gtx>,
1628+  ): CreateAction<Gtx>;
1629+  connect<P>(
1630+    name: ApiName,
1631+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1632+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1633+  connect<P, Gtx extends Ctx = Ctx>(
1634+    name: ApiName,
1635+    fn: MiddlewareApiCo<Gtx>,
1636+  ): CreateActionWithPayload<Gtx, P>;
1637+  connect<P extends never, ApiSuccess, ApiError = unknown>(
1638+    name: ApiName,
1639+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1640+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1641+  connect<P, ApiSuccess, ApiError = unknown>(
1642+    name: ApiName,
1643+    fn: MiddlewareApiCo<
1644+      & Omit<Ctx, "payload" | "json">
1645+      & Payload<P>
1646+      & FetchJson<ApiSuccess, ApiError>
1647+    >,
1648+  ): CreateActionWithPayload<
1649+    & Omit<Ctx, "payload" | "json">
1650+    & Payload<P>
1651+    & FetchJson<ApiSuccess, ApiError>,
1652+    P
1653+  >;
1654+
1655+  /**
1656+   * Name, options, and middleware
1657+   */
1658+  connect(
1659+    name: ApiName,
1660+    req: { supervisor?: Supervisor },
1661+    fn: MiddlewareApiCo<Ctx>,
1662+  ): CreateAction<Ctx>;
1663+  connect<Gtx extends Ctx = Ctx>(
1664+    name: ApiName,
1665+    req: { supervisor?: Supervisor },
1666+    fn: MiddlewareApiCo<Gtx>,
1667+  ): CreateAction<Gtx>;
1668+  connect<P>(
1669+    name: ApiName,
1670+    req: { supervisor?: Supervisor },
1671+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1672+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1673+  connect<P, Gtx extends Ctx = Ctx>(
1674+    name: ApiName,
1675+    req: { supervisor?: Supervisor },
1676+    fn: MiddlewareApiCo<Gtx>,
1677+  ): CreateActionWithPayload<Gtx, P>;
1678+  connect<P extends never, ApiSuccess, ApiError = unknown>(
1679+    name: ApiName,
1680+    req: { supervisor?: Supervisor },
1681+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1682+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1683+  connect<P, ApiSuccess, ApiError = unknown>(
1684+    name: ApiName,
1685+    req: { supervisor?: Supervisor },
1686+    fn: MiddlewareApiCo<
1687+      & Omit<Ctx, "payload" | "json">
1688+      & Payload<P>
1689+      & FetchJson<ApiSuccess, ApiError>
1690+    >,
1691+  ): CreateActionWithPayload<
1692+    & Omit<Ctx, "payload" | "json">
1693+    & Payload<P>
1694+    & FetchJson<ApiSuccess, ApiError>,
1695+    P
1696+  >;
1697+
1698+  /**
1699+   * Only name
1700+   */
1701+  trace(name: ApiName): CreateAction<Ctx>;
1702+  trace<P>(
1703+    name: ApiName,
1704+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1705+  trace<P extends never, ApiSuccess, ApiError = unknown>(
1706+    name: ApiName,
1707+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1708+  trace<P, ApiSuccess, ApiError = unknown>(
1709+    name: ApiName,
1710+  ): CreateActionWithPayload<
1711+    & Omit<Ctx, "payload" | "json">
1712+    & Payload<P>
1713+    & FetchJson<ApiSuccess, ApiError>,
1714+    P
1715+  >;
1716+
1717+  /**
1718+   * Name and options
1719+   */
1720+  trace(name: ApiName, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
1721+  trace<P>(
1722+    name: ApiName,
1723+    req: { supervisor?: Supervisor },
1724+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1725+  trace<P, Gtx extends Ctx = Ctx>(
1726+    name: ApiName,
1727+    req: { supervisor?: Supervisor },
1728+  ): CreateActionWithPayload<Gtx, P>;
1729+  trace<P extends never, ApiSuccess, ApiError = unknown>(
1730+    name: ApiName,
1731+    req: { supervisor?: Supervisor },
1732+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1733+  trace<P, ApiSuccess, ApiError = unknown>(
1734+    name: ApiName,
1735+    req: { supervisor?: Supervisor },
1736+  ): CreateActionWithPayload<
1737+    & Omit<Ctx, "payload" | "json">
1738+    & Payload<P>
1739+    & FetchJson<ApiSuccess, ApiError>,
1740+    P
1741+  >;
1742+
1743+  /**
1744+   * Name and middleware
1745+   */
1746+  trace(name: ApiName, fn: MiddlewareApiCo<Ctx>): CreateAction<Ctx>;
1747+  trace<Gtx extends Ctx = Ctx>(
1748+    name: ApiName,
1749+    fn: MiddlewareApiCo<Gtx>,
1750+  ): CreateAction<Gtx>;
1751+  trace<P>(
1752+    name: ApiName,
1753+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1754+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1755+  trace<P, Gtx extends Ctx = Ctx>(
1756+    name: ApiName,
1757+    fn: MiddlewareApiCo<Gtx>,
1758+  ): CreateActionWithPayload<Gtx, P>;
1759+  trace<P extends never, ApiSuccess, ApiError = unknown>(
1760+    name: ApiName,
1761+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1762+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1763+  trace<P, ApiSuccess, ApiError = unknown>(
1764+    name: ApiName,
1765+    fn: MiddlewareApiCo<
1766+      & Omit<Ctx, "payload" | "json">
1767+      & Payload<P>
1768+      & FetchJson<ApiSuccess, ApiError>
1769+    >,
1770+  ): CreateActionWithPayload<
1771+    & Omit<Ctx, "payload" | "json">
1772+    & Payload<P>
1773+    & FetchJson<ApiSuccess, ApiError>,
1774+    P
1775+  >;
1776+
1777+  /**
1778+   * Name, options, and middleware
1779+   */
1780+  trace(
1781+    name: ApiName,
1782+    req: { supervisor?: Supervisor },
1783+    fn: MiddlewareApiCo<Ctx>,
1784+  ): CreateAction<Ctx>;
1785+  trace<Gtx extends Ctx = Ctx>(
1786+    name: ApiName,
1787+    req: { supervisor?: Supervisor },
1788+    fn: MiddlewareApiCo<Gtx>,
1789+  ): CreateAction<Gtx>;
1790+  trace<P>(
1791+    name: ApiName,
1792+    req: { supervisor?: Supervisor },
1793+    fn: MiddlewareApiCo<Omit<Ctx, "payload"> & Payload<P>>,
1794+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
1795+  trace<P, Gtx extends Ctx = Ctx>(
1796+    name: ApiName,
1797+    req: { supervisor?: Supervisor },
1798+    fn: MiddlewareApiCo<Gtx>,
1799+  ): CreateActionWithPayload<Gtx, P>;
1800+  trace<P extends never, ApiSuccess, ApiError = unknown>(
1801+    name: ApiName,
1802+    req: { supervisor?: Supervisor },
1803+    fn: MiddlewareApiCo<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>,
1804+  ): CreateAction<Omit<Ctx, "json"> & FetchJson<ApiSuccess, ApiError>>;
1805+  trace<P, ApiSuccess, ApiError = unknown>(
1806+    name: ApiName,
1807+    req: { supervisor?: Supervisor },
1808+    fn: MiddlewareApiCo<
1809+      & Omit<Ctx, "payload" | "json">
1810+      & Payload<P>
1811+      & FetchJson<ApiSuccess, ApiError>
1812+    >,
1813+  ): CreateActionWithPayload<
1814+    & Omit<Ctx, "payload" | "json">
1815+    & Payload<P>
1816+    & FetchJson<ApiSuccess, ApiError>,
1817+    P
1818+  >;
1819+}
A query/api.test.ts
+423, -0
  1@@ -0,0 +1,423 @@
  2+import { describe, expect, it } from "../test.ts";
  3+
  4+import { call } from "../fx/index.ts";
  5+import { put, takeEvery } from "../redux/index.ts";
  6+import { createAction, createReducerMap, createTable } from "../deps.ts";
  7+import type { MapEntity } from "../deps.ts";
  8+
  9+import { queryCtx, requestMonitor, urlParser } from "./middleware.ts";
 10+import { createApi } from "./api.ts";
 11+import { setupStore, sleep } from "./util.ts";
 12+import { createKey } from "./create-key.ts";
 13+import type { ApiCtx } from "./types.ts";
 14+import { poll } from "./saga.ts";
 15+
 16+interface User {
 17+  id: string;
 18+  name: string;
 19+  email: string;
 20+}
 21+
 22+const mockUser: User = { id: "1", name: "test", email: "test@test.com" };
 23+
 24+const jsonBlob = (data: unknown) => {
 25+  return JSON.stringify(data);
 26+};
 27+
 28+const tests = describe("createApi()");
 29+
 30+it(tests, "createApi - POST", async () => {
 31+  const name = "users";
 32+  const cache = createTable<User>({ name });
 33+  const query = createApi();
 34+
 35+  query.use(queryCtx);
 36+  query.use(urlParser);
 37+  query.use(query.routes());
 38+  query.use(function* fetchApi(ctx, next): Iterator<unknown> {
 39+    expect(ctx.req()).toEqual({
 40+      url: "/users",
 41+      headers: {},
 42+      method: "POST",
 43+      body: JSON.stringify({ email: mockUser.email }),
 44+    });
 45+    const data = {
 46+      users: [mockUser],
 47+    };
 48+
 49+    ctx.response = new Response(jsonBlob(data), { status: 200 });
 50+
 51+    yield* next();
 52+  });
 53+
 54+  const createUser = query.post<{ email: string }, { users: User[] }>(
 55+    `/users`,
 56+    function* processUsers(ctx, next) {
 57+      ctx.request = ctx.req({
 58+        method: "POST",
 59+        body: JSON.stringify({ email: ctx.payload.email }),
 60+      });
 61+      yield* next();
 62+
 63+      const buff = yield* call(() => {
 64+        if (!ctx.response) throw new Error("no response");
 65+        const res = ctx.response.arrayBuffer();
 66+        return res;
 67+      });
 68+
 69+      if (!buff.ok) {
 70+        throw buff.error;
 71+      }
 72+
 73+      const result = new TextDecoder("utf-8").decode(buff.value);
 74+      const { users } = JSON.parse(result);
 75+      if (!users) return;
 76+      const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
 77+        acc[u.id] = u;
 78+        return acc;
 79+      }, {});
 80+      yield* put(cache.actions.add(curUsers));
 81+    },
 82+  );
 83+
 84+  const reducers = createReducerMap(cache);
 85+  const { store, run } = setupStore(reducers, { fx: query.bootup });
 86+  run();
 87+
 88+  store.dispatch(createUser({ email: mockUser.email }));
 89+  await sleep(150);
 90+  expect(store.getState().users).toEqual({
 91+    "1": { id: "1", name: "test", email: "test@test.com" },
 92+  });
 93+});
 94+
 95+it(tests, "POST with uri", () => {
 96+  const name = "users";
 97+  const cache = createTable<User>({ name });
 98+  const query = createApi();
 99+
100+  query.use(queryCtx);
101+  query.use(urlParser);
102+  query.use(query.routes());
103+  query.use(function* fetchApi(ctx, next) {
104+    expect(ctx.req()).toEqual({
105+      url: "/users",
106+      headers: {},
107+      method: "POST",
108+      body: JSON.stringify({ email: mockUser.email }),
109+    });
110+
111+    const data = {
112+      users: [mockUser],
113+    };
114+    ctx.response = new Response(jsonBlob(data), { status: 200 });
115+    yield* next();
116+  });
117+
118+  const userApi = query.uri("/users");
119+  const createUser = userApi.post<{ email: string }>(function* processUsers(
120+    ctx: ApiCtx<{ email: string }, { users: User[] }>,
121+    next,
122+  ) {
123+    ctx.request = ctx.req({
124+      body: JSON.stringify({ email: ctx.payload.email }),
125+    });
126+
127+    yield* next();
128+    if (!ctx.json.ok) return;
129+    const { users } = ctx.json.data;
130+    const curUsers = users.reduce<MapEntity<User>>((acc, u) => {
131+      acc[u.id] = u;
132+      return acc;
133+    }, {});
134+    yield* put(cache.actions.add(curUsers));
135+  });
136+
137+  const reducers = createReducerMap(cache);
138+  const { store, run } = setupStore(reducers, { fx: query.bootup });
139+  run();
140+
141+  store.dispatch(createUser({ email: mockUser.email }));
142+});
143+
144+it(tests, "middleware - with request fn", () => {
145+  const query = createApi();
146+  query.use(queryCtx);
147+  query.use(urlParser);
148+  query.use(query.routes());
149+  query.use(function* (ctx, next) {
150+    expect(ctx.req().method).toEqual("POST");
151+    expect(ctx.req().url).toEqual("/users");
152+    yield* next();
153+  });
154+  const createUser = query.create("/users", query.request({ method: "POST" }));
155+  const { store, run } = setupStore({ def: (s) => s || null }, {
156+    fx: query.bootup,
157+  });
158+  run();
159+
160+  store.dispatch(createUser());
161+});
162+
163+it(tests, "run() on endpoint action - should run the effect", () => {
164+  const api = createApi<TestCtx>();
165+  api.use(api.routes());
166+  let acc = "";
167+  const action1 = api.get<{ id: string }, { result: boolean }>(
168+    "/users/:id",
169+    function* (_, next) {
170+      yield* next();
171+      acc += "a";
172+    },
173+  );
174+  const action2 = api.get("/users2", function* (_, next) {
175+    yield* next();
176+    yield* call(() => action1.run(action1({ id: "1" })));
177+    acc += "b";
178+    expect(acc).toEqual("ab");
179+  });
180+
181+  const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
182+  run();
183+
184+  store.dispatch(action2());
185+});
186+
187+it(tests, "run() from a normal saga", () => {
188+  const api = createApi();
189+  api.use(api.routes());
190+  let acc = "";
191+  const action1 = api.get<{ id: string }>("/users/:id", function* (_, next) {
192+    yield* next();
193+    acc += "a";
194+  });
195+  const action2 = createAction("ACTION");
196+  function* onAction() {
197+    const ctx = yield* call(() => action1.run(action1({ id: "1" })));
198+    if (!ctx.ok) {
199+      throw new Error("no ctx");
200+    }
201+    const payload = { name: "/users/:id [GET]", options: { id: "1" } };
202+    expect(ctx.value.action.type).toEqual(`@@saga-query${action1}`);
203+    expect(ctx.value.action.payload).toEqual(payload);
204+    expect(ctx.value.name).toEqual("/users/:id [GET]");
205+    expect(ctx.value.payload).toEqual({ id: "1" });
206+    acc += "b";
207+    expect(acc).toEqual("ab");
208+  }
209+
210+  function* watchAction() {
211+    const task = yield* takeEvery(`${action2}`, onAction);
212+    yield* task;
213+  }
214+
215+  const { store, run } = setupStore({ def: () => null }, {
216+    api: api.bootup,
217+    watchAction,
218+  });
219+  run();
220+
221+  store.dispatch(action2());
222+});
223+
224+it(tests, "createApi with hash key on a large post", async () => {
225+  const query = createApi();
226+  query.use(requestMonitor());
227+  query.use(query.routes());
228+  query.use(function* fetchApi(ctx, next) {
229+    const data = {
230+      users: [{ ...mockUser, ...ctx.action.payload.options }],
231+    };
232+    ctx.response = new Response(jsonBlob(data), { status: 200 });
233+    yield* next();
234+  });
235+  const createUserDefaultKey = query.post<{ email: string; largetext: string }>(
236+    `/users`,
237+    function* processUsers(ctx, next) {
238+      ctx.cache = true;
239+      yield* next();
240+      const buff = yield* call(() => {
241+        if (!ctx.response) {
242+          throw new Error("no response");
243+        }
244+        return ctx.response.arrayBuffer();
245+      });
246+
247+      if (!buff.ok) {
248+        throw buff.error;
249+      }
250+      const result = new TextDecoder("utf-8").decode(buff.value);
251+      const { users } = JSON.parse(result);
252+      if (!users) return;
253+      const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
254+        acc[u.id] = u;
255+        return acc;
256+      }, {});
257+      ctx.response = new Response();
258+      ctx.json = {
259+        ok: true,
260+        data: curUsers,
261+      };
262+    },
263+  );
264+
265+  const email = mockUser.email + "9";
266+  const largetext = "abc-def-ghi-jkl-mno-pqr".repeat(100);
267+  const reducers = createReducerMap();
268+  const { store, run } = setupStore(reducers, { fx: query.bootup });
269+  run();
270+
271+  store.dispatch(createUserDefaultKey({ email, largetext }));
272+  await sleep(150);
273+  const s = store.getState();
274+  const expectedKey = createKey(`${createUserDefaultKey}`, {
275+    email,
276+    largetext,
277+  });
278+
279+  expect([8, 9].includes(expectedKey.split("|")[1].length)).toBeTruthy();
280+
281+  expect(s["@@saga-query/data"][expectedKey]).toEqual({
282+    "1": { id: "1", name: "test", email: email, largetext: largetext },
283+  });
284+});
285+
286+it(tests, "createApi - two identical endpoints", async () => {
287+  const actual: string[] = [];
288+  const api = createApi();
289+  api.use(requestMonitor());
290+  api.use(api.routes());
291+
292+  const first = api.get("/health", function* (ctx, next) {
293+    actual.push(ctx.req().url);
294+    yield* next();
295+  });
296+
297+  const second = api.get(
298+    ["/health", "poll"],
299+    { supervisor: poll(1 * 1000) },
300+    function* (ctx, next) {
301+      actual.push(ctx.req().url);
302+      yield* next();
303+    },
304+  );
305+
306+  const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
307+  run();
308+  store.dispatch(first());
309+  store.dispatch(second());
310+
311+  await sleep(150);
312+
313+  // stop poll
314+  store.dispatch(second());
315+
316+  expect(actual).toEqual(["/health", "/health"]);
317+});
318+
319+interface TestCtx<P = any, S = any, E = any> extends ApiCtx<P, S, E> {
320+  something: boolean;
321+}
322+
323+// this is strictly for testing types
324+it(tests, "ensure types for get() endpoint", () => {
325+  const api = createApi<TestCtx>();
326+  api.use(api.routes());
327+  api.use(function* (ctx, next) {
328+    yield* next();
329+    ctx.json = { ok: true, data: { result: "wow" } };
330+  });
331+
332+  const acc: string[] = [];
333+  const action1 = api.get<{ id: string }, { result: string }>(
334+    "/users/:id",
335+    function* (ctx, next) {
336+      ctx.something = false;
337+      acc.push(ctx.payload.id);
338+
339+      yield* next();
340+
341+      if (ctx.json.ok) {
342+        acc.push(ctx.json.data.result);
343+      }
344+    },
345+  );
346+
347+  const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
348+  run();
349+
350+  store.dispatch(action1({ id: "1" }));
351+  expect(acc).toEqual(["1", "wow"]);
352+});
353+
354+interface FetchUserProps {
355+  id: string;
356+}
357+type FetchUserCtx = TestCtx<FetchUserProps>;
358+
359+// this is strictly for testing types
360+it(tests, "ensure ability to cast `ctx` in function definition", () => {
361+  const api = createApi<TestCtx>();
362+  api.use(api.routes());
363+  api.use(function* (ctx, next) {
364+    yield* next();
365+    ctx.json = { ok: true, data: { result: "wow" } };
366+  });
367+
368+  const acc: string[] = [];
369+  const action1 = api.get<FetchUserProps>(
370+    "/users/:id",
371+    function* (ctx: FetchUserCtx, next) {
372+      ctx.something = false;
373+      acc.push(ctx.payload.id);
374+
375+      yield* next();
376+
377+      if (ctx.json.ok) {
378+        acc.push(ctx.json.data.result);
379+      }
380+    },
381+  );
382+
383+  const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
384+  run();
385+
386+  store.dispatch(action1({ id: "1" }));
387+  expect(acc).toEqual(["1", "wow"]);
388+});
389+
390+type FetchUserSecondCtx = TestCtx<any, { result: string }>;
391+
392+// this is strictly for testing types
393+it(
394+  tests,
395+  "ensure ability to cast `ctx` in function definition with no props",
396+  () => {
397+    const api = createApi<TestCtx>();
398+    api.use(api.routes());
399+    api.use(function* (ctx, next) {
400+      yield* next();
401+      ctx.json = { ok: true, data: { result: "wow" } };
402+    });
403+
404+    const acc: string[] = [];
405+    const action1 = api.get<never, { result: string }>(
406+      "/users",
407+      function* (ctx: FetchUserSecondCtx, next) {
408+        ctx.something = false;
409+
410+        yield* next();
411+
412+        if (ctx.json.ok) {
413+          acc.push(ctx.json.data.result);
414+        }
415+      },
416+    );
417+
418+    const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
419+    run();
420+
421+    store.dispatch(action1());
422+    expect(acc).toEqual(["wow"]);
423+  },
424+);
A query/api.ts
+93, -0
 1@@ -0,0 +1,93 @@
 2+// deno-lint-ignore-file no-explicit-any
 3+import type { ApiCtx, ApiRequest, Next } from "./types.ts";
 4+import { createPipe } from "./pipe.ts";
 5+import type { SagaApi } from "./pipe.ts";
 6+import type { ApiName, SagaQueryApi } from "./api-types.ts";
 7+
 8+/**
 9+ * Creates a middleware pipeline for HTTP requests.
10+ *
11+ * @remarks
12+ * It uses {@link createPipe} under the hood.
13+ *
14+ * @example
15+ * ```ts
16+ * import { createApi, requestMonitor, fetcher } from 'saga-query';
17+ *
18+ * const api = createApi();
19+ * api.use(requestMonitor());
20+ * api.use(api.routes());
21+ * api.use(fetcher({ baseUrl: 'https://api.com' }));
22+ *
23+ * const fetchUsers = api.get('/users', function*(ctx, next) {
24+ *   yield next();
25+ * });
26+ *
27+ * store.dispatch(fetchUsers());
28+ * ```
29+ */
30+export function createApi<Ctx extends ApiCtx = ApiCtx>(
31+  basePipe?: SagaApi<Ctx>,
32+): SagaQueryApi<Ctx> {
33+  const pipe = basePipe || createPipe<Ctx>();
34+  const uri = (prename: ApiName) => {
35+    const create = pipe.create as any;
36+
37+    let name = prename;
38+    let remainder = "";
39+    if (Array.isArray(name)) {
40+      if (name.length === 0) {
41+        throw new Error(
42+          "createApi requires a non-empty array for the name of the endpoint",
43+        );
44+      }
45+      name = prename[0];
46+      if (name.length > 1) {
47+        const [_, ...other] = prename;
48+        remainder = ` ${other.join("|")}`;
49+      }
50+    }
51+    const tmpl = (method: string) => `${name} [${method}]${remainder}`;
52+
53+    return {
54+      get: (...args: any[]) => create(tmpl("GET"), ...args),
55+      post: (...args: any[]) => create(tmpl("POST"), ...args),
56+      put: (...args: any[]) => create(tmpl("PUT"), ...args),
57+      patch: (...args: any[]) => create(tmpl("PATCH"), ...args),
58+      delete: (...args: any[]) => create(tmpl("DELETE"), ...args),
59+      options: (...args: any[]) => create(tmpl("OPTIONS"), ...args),
60+      head: (...args: any[]) => create(tmpl("HEAD"), ...args),
61+      connect: (...args: any[]) => create(tmpl("CONNECT"), ...args),
62+      trace: (...args: any[]) => create(tmpl("TRACE"), ...args),
63+    };
64+  };
65+
66+  return {
67+    use: pipe.use,
68+    bootup: pipe.bootup,
69+    create: pipe.create,
70+    routes: pipe.routes,
71+    cache: () => {
72+      return function* onCache(ctx: Ctx, next: Next) {
73+        ctx.cache = true;
74+        yield next();
75+      };
76+    },
77+    request: (req: ApiRequest) => {
78+      return function* onRequest(ctx: Ctx, next: Next) {
79+        ctx.request = ctx.req(req);
80+        yield next();
81+      };
82+    },
83+    uri,
84+    get: (name: ApiName, ...args: any[]) => uri(name).get(...args),
85+    post: (name: ApiName, ...args: any[]) => uri(name).post(...args),
86+    put: (name: ApiName, ...args: any[]) => uri(name).put(...args),
87+    patch: (name: ApiName, ...args: any[]) => uri(name).patch(...args),
88+    delete: (name: ApiName, ...args: any[]) => uri(name).delete(...args),
89+    options: (name: ApiName, ...args: any[]) => uri(name).options(...args),
90+    head: (name: ApiName, ...args: any[]) => uri(name).head(...args),
91+    connect: (name: ApiName, ...args: any[]) => uri(name).connect(...args),
92+    trace: (name: ApiName, ...args: any[]) => uri(name).trace(...args),
93+  };
94+}
A query/constant.ts
+1, -0
1@@ -0,0 +1 @@
2+export const API_ACTION_PREFIX = "@@starfx";
A query/create-key.test.ts
+357, -0
  1@@ -0,0 +1,357 @@
  2+import { describe, expect, it } from "../test.ts";
  3+import type { ActionWithPayload } from "./types.ts";
  4+import { createApi } from "./api.ts";
  5+import { poll } from "./saga.ts";
  6+
  7+const getKeyOf = (action: ActionWithPayload<{ key: string }>): string =>
  8+  action.payload.key;
  9+
 10+const tests = describe("create-key");
 11+
 12+it(
 13+  tests,
 14+  "options object keys order for action key identity - 0: empty options",
 15+  () => {
 16+    const api = createApi();
 17+    api.use(api.routes());
 18+    // no param
 19+    const action0 = api.get(
 20+      "/users",
 21+      { supervisor: poll(5 * 1000) }, // with poll middleware
 22+      function* (ctx, next) {
 23+        ctx.request = {
 24+          method: "GET",
 25+        };
 26+        yield* next();
 27+      },
 28+    );
 29+    const sendNop0 = action0();
 30+    const sendNop1 = action0();
 31+    expect(getKeyOf(sendNop0)).toEqual(getKeyOf(sendNop1));
 32+  },
 33+);
 34+
 35+it(
 36+  tests,
 37+  "options object keys order for action key identity - 1: simple object",
 38+  () => {
 39+    const api = createApi();
 40+    api.use(api.routes());
 41+    // no param
 42+    const action0 = api.get<{
 43+      [key: string]: string | boolean | number | null | undefined;
 44+    }>(
 45+      "/users",
 46+      { supervisor: poll(5 * 1000) }, // with poll middleware
 47+      function* (ctx, next) {
 48+        ctx.request = {
 49+          method: "GET",
 50+        };
 51+        yield* next();
 52+      },
 53+    );
 54+    const sendPojo0 = action0({
 55+      a: "a",
 56+      b: "b",
 57+      c: 1,
 58+      d: 2,
 59+      e: true,
 60+      f: false,
 61+      "100": 100,
 62+      101: "101",
 63+    });
 64+    const sendPojo1 = action0({
 65+      a: "a",
 66+      b: "b",
 67+      c: 1,
 68+      d: 2,
 69+      e: true,
 70+      f: false,
 71+      100: 100,
 72+      101: "101",
 73+    });
 74+    const sendPojo2 = action0({
 75+      e: true,
 76+      f: false,
 77+      "100": 100,
 78+      "101": "101",
 79+      a: "a",
 80+      b: "b",
 81+      c: 1,
 82+      d: 2,
 83+    });
 84+    const sendPojo3 = action0({
 85+      e: true,
 86+      f: false,
 87+      "100": 100,
 88+      "101": "101",
 89+      a: "a",
 90+      b: "b",
 91+      c: 1,
 92+      d: 2000000,
 93+    });
 94+    const sendPojo4 = action0({
 95+      e: null,
 96+      f: false,
 97+      "100": undefined,
 98+      "101": "101",
 99+      a: "a",
100+      b: "b",
101+      c: 1,
102+      d: `Thomas O'Malley`,
103+    });
104+    const sendPojo5 = action0({
105+      d: `Thomas O'Malley`,
106+      e: null,
107+      f: false,
108+      "100": undefined,
109+      "101": "101",
110+      a: "a",
111+      b: "b",
112+      c: 1,
113+    });
114+    expect(getKeyOf(sendPojo0)).toEqual(getKeyOf(sendPojo1));
115+    expect(getKeyOf(sendPojo0)).toEqual(getKeyOf(sendPojo2));
116+    expect(getKeyOf(sendPojo0)).not.toEqual(getKeyOf(sendPojo3));
117+    expect(getKeyOf(sendPojo4)).toEqual(getKeyOf(sendPojo5));
118+  },
119+);
120+
121+it(
122+  tests,
123+  "options object keys order for action key identity - 2: object (with array values)",
124+  () => {
125+    interface Ip0 {
126+      param1: string;
127+      param2: string[];
128+    }
129+    const api = createApi();
130+    api.use(api.routes());
131+    const action = api.get<Ip0>(
132+      "/users/:param1/:param2",
133+      function* (ctx, next) {
134+        ctx.request = {
135+          method: "GET",
136+        };
137+        yield* next();
138+      },
139+    );
140+    const sendFirst = action({ param1: "1", param2: ["2", "e", "f"] });
141+    const sendSecond = action({ param2: ["2", "f", "e"], param1: "1" });
142+    const sendThird = action({ param2: ["2", "e", "f"], param1: "1" });
143+    expect(getKeyOf(sendFirst)).not.toEqual(getKeyOf(sendSecond));
144+    expect(getKeyOf(sendFirst)).toEqual(getKeyOf(sendThird));
145+  },
146+);
147+
148+it(
149+  tests,
150+  "options object keys order for action key identity - 3: nested object",
151+  () => {
152+    interface Ip0 {
153+      param1: string;
154+      param2: string[];
155+    }
156+    interface Ip1 {
157+      param1: string;
158+      param2: string;
159+      param3: number;
160+      param4: Ip0;
161+      param5: boolean;
162+    }
163+    const o1: Ip1 = {
164+      param1: "1",
165+      param2: "2",
166+      param3: 3,
167+      param4: {
168+        param1: "4",
169+        param2: ["5", "6"],
170+      },
171+      param5: true,
172+    };
173+    const o2: Ip1 = {
174+      param4: {
175+        param1: "4",
176+        param2: ["5", "6"],
177+      },
178+      param5: true,
179+      param2: "2",
180+      param1: "1",
181+      param3: 3,
182+    };
183+    const api = createApi();
184+    api.use(api.routes());
185+    //nested with array
186+    const action2 = api.get<Ip1>(
187+      "/users/:param1/:param2/:param3/:param4/:param5",
188+      function* (ctx, next) {
189+        ctx.request = {
190+          method: "GET",
191+        };
192+        yield* next();
193+      },
194+    );
195+    const sendO1 = action2(o1);
196+    const sendO2 = action2(o2);
197+    const sendO3 = action2({
198+      ...o1,
199+      param4: { ...o1.param4, param2: ["5", "6", "7"] },
200+    });
201+    expect(getKeyOf(sendO1)).toEqual(getKeyOf(sendO2));
202+    expect(getKeyOf(sendO1)).not.toEqual(getKeyOf(sendO3));
203+  },
204+);
205+
206+it(
207+  tests,
208+  "options object keys order for action key identity - 4: deepNested object",
209+  () => {
210+    interface Ip0 {
211+      param1: string;
212+      param2: string[];
213+    }
214+    interface Ip1 {
215+      param1: string;
216+      param2: string;
217+      param3: number;
218+      param4: Ip0;
219+      param5: boolean;
220+    }
221+    interface Ip3 {
222+      param1: string;
223+      param2: {
224+        param3: Ip1;
225+        param4: Ip0;
226+      };
227+    }
228+    const o1: Ip1 = {
229+      param1: "1",
230+      param2: "2",
231+      param3: 3,
232+      param4: {
233+        param1: "4",
234+        param2: ["5", "6"],
235+      },
236+      param5: true,
237+    };
238+    const oo1: Ip3 = {
239+      param1: "1",
240+      param2: {
241+        param3: o1,
242+        param4: {
243+          param1: "4",
244+          param2: ["5", "6"],
245+        },
246+      },
247+    };
248+    const oo2: Ip3 = {
249+      param1: "1",
250+      param2: {
251+        param4: {
252+          param1: "4",
253+          param2: ["5", "6"],
254+        },
255+        param3: o1,
256+      },
257+    };
258+    const api = createApi();
259+    api.use(api.routes());
260+    // deepNested
261+    const action4 = api.get<Ip3>(
262+      "/users/:param1/:param2/:param3/:param4/:param5",
263+      function* (ctx, next) {
264+        ctx.request = {
265+          method: "GET",
266+        };
267+        yield* next();
268+      },
269+    );
270+    const send_oo1 = action4(oo1);
271+    const send_oo1_shuff = action4({ param2: oo1.param2, param1: oo1.param1 });
272+    const send_oo1_value_changed = action4({ ...oo1, param1: "x" });
273+    const send_oo2 = action4(oo2);
274+    expect(send_oo1.payload.options).toEqual(send_oo2.payload.options);
275+    expect(getKeyOf(send_oo1)).toEqual(getKeyOf(send_oo1_shuff));
276+    expect(getKeyOf(send_oo1)).not.toEqual(getKeyOf(send_oo1_value_changed));
277+    expect(getKeyOf(send_oo1)).toEqual(getKeyOf(send_oo2));
278+  },
279+);
280+
281+it(
282+  tests,
283+  "options object keys order for action key identity - 5: other",
284+  () => {
285+    const api = createApi();
286+    api.use(api.routes());
287+    //array options
288+    const action5 = api.post<
289+      | number
290+      | boolean
291+      | string
292+      | undefined
293+      | null
294+      | { param1: string; param2: (string | number)[] }[]
295+      | string[]
296+    >("/users/:allRecords", function* (ctx, next) {
297+      ctx.request = {
298+        method: "POST",
299+        body: JSON.stringify(ctx.action.payload),
300+      };
301+      yield* next();
302+    });
303+    const falsy0 = action5(0);
304+    const falsy1 = action5(false);
305+    const falsy2 = action5("");
306+    const falsy3 = action5(undefined);
307+    const falsy4 = action5(null);
308+    const primNo0 = action5(NaN);
309+    const primNo1 = action5(1);
310+    const primNo1bis = action5(1);
311+    const primNo2 = action5(2);
312+    const str1 = action5("1234");
313+    const str1bis = action5("1234");
314+    const str2 = action5("2345");
315+    const aStrings1 = action5(["1", "2", "3"]);
316+    const aStrings2 = action5(["1", "2", "3"]);
317+    const aStrings3 = action5(["1", "2", "1"]);
318+    const aObjects1 = action5([
319+      { param1: "1", param2: ["2", "3"] },
320+      { param1: "2", param2: ["2", "3"] },
321+    ]);
322+    const aObjects2 = action5([
323+      { param1: "1", param2: ["2", "3"] },
324+      { param1: "2", param2: ["2", "3"] },
325+    ]);
326+    // the objects are not identical.
327+    const aObjects3 = action5([
328+      { param1: "1", param2: ["2", "3"] },
329+      { param1: "2", param2: ["2", 3] },
330+    ]);
331+    //object inside the array is shuffled
332+    const aObjects4 = action5([
333+      { param2: ["2", "3"], param1: "1" },
334+      { param2: ["2", "3"], param1: "2" },
335+    ]);
336+    // cont the order of array elements is changed will get different keys.
337+    const aObjects5 = action5([
338+      { param1: "2", param2: ["2", "3"] },
339+      { param1: "1", param2: ["2", "3"] },
340+    ]);
341+    expect(getKeyOf(falsy0)).not.toEqual(getKeyOf(falsy1));
342+    expect(getKeyOf(falsy1)).not.toEqual(getKeyOf(falsy2));
343+    expect(getKeyOf(falsy1)).not.toEqual(getKeyOf(falsy3));
344+    expect(getKeyOf(falsy3)).not.toEqual(getKeyOf(falsy4));
345+    expect(getKeyOf(primNo0)).not.toEqual(getKeyOf(falsy0));
346+    expect(getKeyOf(primNo0)).not.toEqual(getKeyOf(primNo1));
347+    expect(getKeyOf(primNo1)).not.toEqual(getKeyOf(primNo2));
348+    expect(getKeyOf(primNo1)).toEqual(getKeyOf(primNo1bis));
349+    expect(getKeyOf(str1)).not.toEqual(getKeyOf(str2));
350+    expect(getKeyOf(str1)).toEqual(getKeyOf(str1bis));
351+    expect(getKeyOf(aStrings1)).toEqual(getKeyOf(aStrings2));
352+    expect(getKeyOf(aStrings1)).not.toEqual(getKeyOf(aStrings3));
353+    expect(getKeyOf(aObjects1)).toEqual(getKeyOf(aObjects2));
354+    expect(getKeyOf(aObjects1)).not.toEqual(getKeyOf(aObjects3));
355+    expect(getKeyOf(aObjects1)).toEqual(getKeyOf(aObjects4));
356+    expect(getKeyOf(aObjects1)).not.toEqual(getKeyOf(aObjects5));
357+  },
358+);
A query/create-key.ts
+44, -0
 1@@ -0,0 +1,44 @@
 2+import { isObject } from "./util.ts";
 3+
 4+// deno-lint-ignore no-explicit-any
 5+const deepSortObject = (opts?: any) => {
 6+  if (!isObject(opts)) return opts;
 7+  return Object.keys(opts)
 8+    .sort()
 9+    .reduce<Record<string, unknown>>((res, key) => {
10+      res[`${key}`] = opts[key];
11+      if (opts[key] && isObject(opts[key])) {
12+        res[`${key}`] = deepSortObject(opts[key]);
13+      }
14+      return res;
15+    }, {});
16+};
17+
18+function padStart(hash: string, len: number) {
19+  while (hash.length < len) {
20+    hash = "0" + hash;
21+  }
22+  return hash;
23+}
24+//credit to Ivan Perelivskiy: https://gist.github.com/iperelivskiy/4110988
25+const tinySimpleHash = (s: string) => {
26+  let h = 9;
27+  for (let i = 0; i < s.length;) {
28+    h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
29+  }
30+  return h ^ (h >>> 9);
31+};
32+
33+/**
34+ * This function used to set `ctx.key`
35+ */
36+// deno-lint-ignore no-explicit-any
37+export const createKey = (name: string, payload?: any) => {
38+  const normJsonString = typeof payload !== "undefined"
39+    ? JSON.stringify(deepSortObject(payload))
40+    : "";
41+  const hash = normJsonString
42+    ? padStart(tinySimpleHash(normJsonString).toString(16), 8)
43+    : "";
44+  return hash ? `${name}|${hash}` : name;
45+};
A query/fetch.test.ts
+403, -0
  1@@ -0,0 +1,403 @@
  2+import { describe, expect, install, it, mock } from "../test.ts";
  3+
  4+import { fetcher, fetchRetry } from "./fetch.ts";
  5+import { createApi } from "./api.ts";
  6+import { setupStore } from "./util.ts";
  7+import { requestMonitor } from "./middleware.ts";
  8+
  9+install();
 10+
 11+const baseUrl = "https://saga-query.com";
 12+const mockUser = { id: "1", email: "test@saga-query.com" };
 13+
 14+const delay = (n = 200) =>
 15+  new Promise((resolve) => {
 16+    setTimeout(resolve, n);
 17+  });
 18+
 19+const tests = describe("fetcher()");
 20+
 21+it(
 22+  tests,
 23+  "should be able to fetch a resource and save automatically",
 24+  async () => {
 25+    mock(`GET@/users`, () => {
 26+      return new Response(JSON.stringify(mockUser));
 27+    });
 28+
 29+    const api = createApi();
 30+    api.use(requestMonitor());
 31+    api.use(api.routes());
 32+    api.use(fetcher({ baseUrl }));
 33+
 34+    const fetchUsers = api.get("/users", function* (ctx, next) {
 35+      ctx.cache = true;
 36+      yield* next();
 37+
 38+      expect(ctx.request).toEqual({
 39+        url: `${baseUrl}/users`,
 40+        method: "GET",
 41+        headers: {
 42+          "Content-Type": "application/json",
 43+        },
 44+      });
 45+
 46+      expect(ctx.json).toEqual({ ok: true, data: mockUser });
 47+    });
 48+
 49+    const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
 50+    run();
 51+
 52+    const action = fetchUsers();
 53+    store.dispatch(action);
 54+
 55+    await delay();
 56+
 57+    const state = store.getState();
 58+    expect(state["@@saga-query/data"][action.payload.key]).toEqual(mockUser);
 59+  },
 60+);
 61+
 62+it(
 63+  tests,
 64+  "fetch - should be able to fetch a resource and parse as text instead of json",
 65+  async () => {
 66+    mock(`GET@/users`, () => {
 67+      return new Response("this is some text");
 68+    });
 69+
 70+    const api = createApi();
 71+    api.use(requestMonitor());
 72+    api.use(api.routes());
 73+    api.use(fetcher({ baseUrl }));
 74+
 75+    const fetchUsers = api.get("/users", function* (ctx, next) {
 76+      ctx.cache = true;
 77+      ctx.bodyType = "text";
 78+      yield next();
 79+      expect(ctx.json).toEqual({ ok: true, data: "this is some text" });
 80+    });
 81+
 82+    const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
 83+    run();
 84+
 85+    const action = fetchUsers();
 86+    store.dispatch(action);
 87+
 88+    await delay();
 89+  },
 90+);
 91+
 92+it(tests, "fetch - error handling", async () => {
 93+  const errMsg = { message: "something happened" };
 94+  mock(`GET@/users`, () => {
 95+    return new Response(JSON.stringify(errMsg), { status: 500 });
 96+  });
 97+
 98+  const api = createApi();
 99+  api.use(requestMonitor());
100+  api.use(api.routes());
101+  api.use(function* (ctx, next) {
102+    const url = ctx.req().url;
103+    ctx.request = ctx.req({ url: `${baseUrl}${url}` });
104+    yield* next();
105+  });
106+  api.use(fetcher());
107+
108+  const fetchUsers = api.get("/users", function* (ctx, next) {
109+    ctx.cache = true;
110+    yield* next();
111+
112+    expect(ctx.json).toEqual({ ok: false, data: errMsg });
113+  });
114+
115+  const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
116+  run();
117+  const action = fetchUsers();
118+  store.dispatch(action);
119+
120+  await delay();
121+
122+  const state = store.getState();
123+  expect(state["@@saga-query/data"][action.payload.key]).toEqual(errMsg);
124+});
125+
126+it(tests, "fetch - status 204", async () => {
127+  mock(`GET@/users`, () => {
128+    return new Response("", { status: 204 });
129+  });
130+
131+  const api = createApi();
132+  api.use(requestMonitor());
133+  api.use(api.routes());
134+  api.use(function* (ctx, next) {
135+    const url = ctx.req().url;
136+    ctx.request = ctx.req({ url: `${baseUrl}${url}` });
137+    yield* next();
138+  });
139+  api.use(fetcher());
140+
141+  const fetchUsers = api.get("/users", function* (ctx, next) {
142+    ctx.cache = true;
143+    yield* next();
144+
145+    expect(ctx.json).toEqual({ ok: true, data: {} });
146+  });
147+
148+  const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
149+  run();
150+  const action = fetchUsers();
151+  store.dispatch(action);
152+
153+  await delay();
154+
155+  const state = store.getState();
156+  expect(state["@@saga-query/data"][action.payload.key]).toEqual({});
157+});
158+
159+it(tests, "fetch - malformed json", async () => {
160+  mock(`GET@/users`, () => {
161+    return new Response("not json", { status: 200 });
162+  });
163+
164+  const api = createApi();
165+  api.use(requestMonitor());
166+  api.use(api.routes());
167+  api.use(function* (ctx, next) {
168+    const url = ctx.req().url;
169+    ctx.request = ctx.req({ url: `${baseUrl}${url}` });
170+    yield* next();
171+  });
172+  api.use(fetcher());
173+
174+  const fetchUsers = api.get("/users", function* (ctx, next) {
175+    ctx.cache = true;
176+    yield* next();
177+
178+    expect(ctx.json).toEqual({
179+      ok: false,
180+      data: {
181+        message:
182+          "invalid json response body at https://saga-query.com/users reason: Unexpected token o in JSON at position 1",
183+      },
184+    });
185+  });
186+
187+  const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
188+  run();
189+
190+  const action = fetchUsers();
191+  store.dispatch(action);
192+
193+  await delay();
194+});
195+
196+it(tests, "fetch - POST", async () => {
197+  mock(`POST@/users`, () => {
198+    return new Response(JSON.stringify(mockUser));
199+  });
200+
201+  const api = createApi();
202+  api.use(requestMonitor());
203+  api.use(api.routes());
204+  api.use(fetcher({ baseUrl }));
205+
206+  const fetchUsers = api.post("/users", function* (ctx, next) {
207+    ctx.cache = true;
208+    ctx.request = ctx.req({ body: JSON.stringify(mockUser) });
209+    yield* next();
210+
211+    expect(ctx.req()).toEqual({
212+      url: `${baseUrl}/users`,
213+      headers: {
214+        "Content-Type": "application/json",
215+      },
216+      method: "POST",
217+      body: JSON.stringify(mockUser),
218+    });
219+
220+    expect(ctx.json).toEqual({ ok: true, data: mockUser });
221+  });
222+
223+  const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
224+  run();
225+  const action = fetchUsers();
226+  store.dispatch(action);
227+
228+  await delay();
229+});
230+
231+it(tests, "fetch - POST multiple endpoints with same uri", async () => {
232+  mock(`POST@/users/1/something`, () => {
233+    return new Response(JSON.stringify(mockUser));
234+  });
235+
236+  const api = createApi();
237+  api.use(requestMonitor());
238+  api.use(api.routes());
239+  api.use(fetcher({ baseUrl }));
240+
241+  const fetchUsers = api.post<{ id: string }>(
242+    "/users/:id/something",
243+    function* (ctx, next) {
244+      ctx.cache = true;
245+      ctx.request = ctx.req({ body: JSON.stringify(mockUser) });
246+      yield* next();
247+
248+      expect(ctx.req()).toEqual({
249+        url: `${baseUrl}/users/1/something`,
250+        headers: {
251+          "Content-Type": "application/json",
252+        },
253+        method: "POST",
254+        body: JSON.stringify(mockUser),
255+      });
256+
257+      expect(ctx.json).toEqual({ ok: true, data: mockUser });
258+    },
259+  );
260+
261+  const fetchUsersSecond = api.post<{ id: string }>(
262+    ["/users/:id/something", "next"],
263+    function* (ctx, next) {
264+      ctx.cache = true;
265+      ctx.request = ctx.req({ body: JSON.stringify(mockUser) });
266+      yield* next();
267+
268+      expect(ctx.req()).toEqual({
269+        url: `${baseUrl}/users/1/something`,
270+        headers: {
271+          "Content-Type": "application/json",
272+        },
273+        method: "POST",
274+        body: JSON.stringify(mockUser),
275+      });
276+
277+      expect(ctx.json).toEqual({ ok: true, data: mockUser });
278+    },
279+  );
280+
281+  const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
282+  run();
283+
284+  store.dispatch(fetchUsers({ id: "1" }));
285+  store.dispatch(fetchUsersSecond({ id: "1" }));
286+
287+  await delay();
288+});
289+
290+it(
291+  tests,
292+  "fetch - slug in url but payload has empty string for slug value",
293+  async () => {
294+    const api = createApi();
295+    api.use(requestMonitor());
296+    api.use(api.routes());
297+    api.use(fetcher({ baseUrl }));
298+
299+    const fetchUsers = api.post<{ id: string }>(
300+      "/users/:id",
301+      function* (ctx, next) {
302+        ctx.cache = true;
303+        ctx.request = ctx.req({ body: JSON.stringify(mockUser) });
304+
305+        yield* next();
306+
307+        expect(ctx.json).toEqual({
308+          ok: false,
309+          data:
310+            "found :id in endpoint name (/users/:id [POST]) but payload has falsy value ()",
311+        });
312+      },
313+    );
314+
315+    const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
316+    run();
317+
318+    const action = fetchUsers({ id: "" });
319+    store.dispatch(action);
320+
321+    await delay();
322+  },
323+);
324+
325+it(
326+  tests,
327+  "fetch retry - with success - should keep retrying fetch request",
328+  async () => {
329+    let counter = 0;
330+    mock(`GET@/users`, () => {
331+      counter += 1;
332+      if (counter > 4) {
333+        return new Response(JSON.stringify(mockUser));
334+      }
335+      return new Response(JSON.stringify({ message: "error" }), {
336+        status: 400,
337+      });
338+    });
339+
340+    const api = createApi();
341+    api.use(requestMonitor());
342+    api.use(api.routes());
343+    api.use(fetcher({ baseUrl }));
344+
345+    const fetchUsers = api.get("/users", [
346+      function* (ctx, next) {
347+        ctx.cache = true;
348+        yield* next();
349+
350+        if (!ctx.json.ok) {
351+          expect(true).toBe(false);
352+        }
353+
354+        expect(ctx.json).toEqual({ ok: true, data: mockUser });
355+      },
356+      fetchRetry((n) => (n > 4 ? -1 : 10)),
357+    ]);
358+
359+    const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
360+    run();
361+
362+    const action = fetchUsers();
363+    store.dispatch(action);
364+
365+    await delay();
366+
367+    const state = store.getState();
368+    expect(state["@@saga-query/data"][action.payload.key]).toEqual(mockUser);
369+  },
370+);
371+
372+it.ignore(
373+  tests,
374+  "fetch retry - with failure - should keep retrying and then quit",
375+  async () => {
376+    mock(`GET@/users`, () => {
377+      return new Response(JSON.stringify({ message: "error" }), {
378+        status: 400,
379+      });
380+    });
381+
382+    const api = createApi();
383+    api.use(requestMonitor());
384+    api.use(api.routes());
385+    api.use(fetcher({ baseUrl }));
386+
387+    const fetchUsers = api.get("/users", [
388+      function* (ctx, next) {
389+        ctx.cache = true;
390+        yield* next();
391+        expect(ctx.json).toEqual({ ok: false, data: { message: "error" } });
392+      },
393+      fetchRetry((n) => (n > 2 ? -1 : 10)),
394+    ]);
395+
396+    const { store, run } = setupStore({ def: () => null }, { fx: api.bootup });
397+    run();
398+
399+    const action = fetchUsers();
400+    store.dispatch(action);
401+
402+    await delay();
403+  },
404+);
A query/fetch.ts
+245, -0
  1@@ -0,0 +1,245 @@
  2+import { call } from "../fx/index.ts";
  3+import { sleep } from "../deps.ts";
  4+import { compose } from "../compose.ts";
  5+import { noop } from "./util.ts";
  6+import type { FetchCtx, FetchJsonCtx, Next } from "./types.ts";
  7+
  8+/**
  9+ * Automatically sets `content-type` to `application/json` when
 10+ * that header is not already present.
 11+ */
 12+export function* headersMdw<CurCtx extends FetchCtx = FetchCtx>(
 13+  ctx: CurCtx,
 14+  next: Next,
 15+) {
 16+  if (!ctx.request) {
 17+    yield* next();
 18+    return;
 19+  }
 20+
 21+  const cur = ctx.req();
 22+  if (!Object.hasOwn(cur.headers, "Content-Type")) {
 23+    ctx.request = ctx.req({
 24+      headers: { "Content-Type": "application/json" },
 25+    });
 26+  }
 27+
 28+  yield* next();
 29+}
 30+
 31+/**
 32+ * This middleware takes the `ctx.response` and sets `ctx.json` to the body representation
 33+ * requested.  It uses the `ctx.bodyType` property to determine how to represent the body.
 34+ * The default is set to `json` which calls `Response.json()`.
 35+ *
 36+ * @example
 37+ * ```ts
 38+ * const fetchUsers = api.get('/users', function*(ctx, next) {
 39+ *  ctx.bodyType = 'text'; // calls Response.text();
 40+ *  yield next();
 41+ * })
 42+ * ```
 43+ */
 44+export function* jsonMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
 45+  ctx: CurCtx,
 46+  next: Next,
 47+) {
 48+  if (!ctx.response) {
 49+    yield* next();
 50+    return;
 51+  }
 52+
 53+  if (ctx.response.status === 204) {
 54+    ctx.json = {
 55+      ok: ctx.response.ok,
 56+      data: {},
 57+    };
 58+    yield* next();
 59+    return;
 60+  }
 61+
 62+  const data = yield* call(() => {
 63+    const resp = ctx.response;
 64+    if (!resp) throw new Error("response is falsy");
 65+    return resp[ctx.bodyType]();
 66+  });
 67+
 68+  if (data.ok) {
 69+    ctx.json = {
 70+      ok: ctx.response.ok,
 71+      data: data.value,
 72+    };
 73+  } else {
 74+    ctx.json = {
 75+      ok: false,
 76+      data: { message: data.error.message },
 77+    };
 78+  }
 79+
 80+  yield* next();
 81+}
 82+
 83+/*
 84+ * This middleware takes the `baseUrl` provided to `fetcher()` and combines it
 85+ * with the url from `ctx.request.url`.
 86+ */
 87+export function apiUrlMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
 88+  baseUrl = "",
 89+) {
 90+  return function* (ctx: CurCtx, next: Next) {
 91+    const req = ctx.req();
 92+    ctx.request = ctx.req({ url: `${baseUrl}${req.url}` });
 93+    yield* next();
 94+  };
 95+}
 96+
 97+/**
 98+ * If there's a slug inside the ctx.name (which is the URL segement in this case)
 99+ * and there is *not* a corresponding truthy value in the payload, then that means
100+ * the user has an empty value (e.g. empty string) which means we want to abort the
101+ * fetch request.
102+ *
103+ * e.g. `ctx.name = "/apps/:id"` with `payload = { id: '' }`
104+ *
105+ * Ideally the action wouldn't have been dispatched at all but that is *not* a
106+ * gaurantee we can make here.
107+ */
108+export function* payloadMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
109+  ctx: CurCtx,
110+  next: Next,
111+) {
112+  const payload = ctx.payload;
113+  if (!payload) {
114+    yield* next();
115+    return;
116+  }
117+
118+  const keys = Object.keys(payload);
119+  for (let i = 0; i < keys.length; i += 1) {
120+    const key = keys[i];
121+    if (!ctx.name.includes(`:${key}`)) {
122+      continue;
123+    }
124+
125+    const val = payload[key];
126+    if (!val) {
127+      ctx.json = {
128+        ok: false,
129+        data:
130+          `found :${key} in endpoint name (${ctx.name}) but payload has falsy value (${val})`,
131+      };
132+      return;
133+    }
134+  }
135+
136+  yield* next();
137+}
138+
139+/*
140+ * This middleware makes the `fetch` http request using `ctx.request` and
141+ * assigns the response to `ctx.response`.
142+ */
143+export function* fetchMdw<CurCtx extends FetchCtx = FetchCtx>(
144+  ctx: CurCtx,
145+  next: Next,
146+) {
147+  const { url, ...req } = ctx.req();
148+  const request = new Request(url, req);
149+  const response = yield* call(() => fetch(request));
150+  if (response.ok) {
151+    ctx.response = response.value;
152+  } else {
153+    throw response.error;
154+  }
155+  yield* next();
156+}
157+
158+function backoffExp(attempt: number): number {
159+  if (attempt > 5) return -1;
160+  // 1s, 1s, 1s, 2s, 4s
161+  return Math.max(2 ** attempt * 125, 1000);
162+}
163+
164+/**
165+ * This middleware will retry failed `Fetch` request if `response.ok` is `false`.
166+ * It accepts a backoff function to determine how long to continue retrying.
167+ * The default is an exponential backoff {@link backoffExp} where the minimum is
168+ * 1sec between attempts and it'll reach 4s between attempts at the end with a
169+ * max of 5 attempts.
170+ *
171+ * An example backoff:
172+ * @example
173+ * ```ts
174+ *  // Any value less than 0 will stop the retry middleware.
175+ *  // Each attempt will wait 1s
176+ *  const backoff = (attempt: number) => {
177+ *    if (attempt > 5) return -1;
178+ *    return 1000;
179+ *  }
180+ *
181+ * const api = createApi();
182+ * api.use(requestMonitor());
183+ * api.use(api.routes());
184+ * api.use(fetcher());
185+ *
186+ * const fetchUsers = api.get('/users', [
187+ *  function*(ctx, next) {
188+ *    // ...
189+ *    yield next();
190+ *  },
191+ *  // fetchRetry should be after your endpoint function because
192+ *  // the retry middleware will update `ctx.json` before it reaches your middleware
193+ *  fetchRetry(backoff),
194+ * ])
195+ * ```
196+ */
197+export function fetchRetry<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
198+  backoff: (attempt: number) => number = backoffExp,
199+) {
200+  return function* (ctx: CurCtx, next: Next) {
201+    yield* next();
202+
203+    if (!ctx.response) {
204+      return;
205+    }
206+
207+    if (ctx.response.ok) {
208+      return;
209+    }
210+
211+    let attempt = 1;
212+    let waitFor = backoff(attempt);
213+    while (waitFor >= 1) {
214+      yield* sleep(waitFor);
215+      yield* call(() => fetchMdw(ctx, noop));
216+      yield* call(() => jsonMdw(ctx, noop));
217+
218+      if (ctx.response.ok) {
219+        return;
220+      }
221+
222+      attempt += 1;
223+      waitFor = backoff(attempt);
224+    }
225+  };
226+}
227+
228+/**
229+ * This middleware is a composition of other middleware required to use `window.fetch`
230+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API} with {@link createApi}
231+ */
232+export function fetcher<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
233+  {
234+    baseUrl = "",
235+  }: {
236+    baseUrl?: string;
237+  } = { baseUrl: "" },
238+) {
239+  return compose<CurCtx>([
240+    headersMdw,
241+    apiUrlMdw(baseUrl),
242+    payloadMdw,
243+    fetchMdw,
244+    jsonMdw,
245+  ]);
246+}
A query/index.ts
+12, -0
 1@@ -0,0 +1,12 @@
 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 "./store.ts";
11+export * from "./slice.ts";
12+export * from "./create-key.ts";
13+export * from "./saga.ts";
A query/middleware.test.ts
+533, -0
  1@@ -0,0 +1,533 @@
  2+import { assertLike, asserts, describe, expect, it } from "../test.ts";
  3+
  4+import { put, takeLatest } from "../redux/index.ts";
  5+import {
  6+  createReducerMap,
  7+  createTable,
  8+  defaultLoadingItem,
  9+  sleep as delay,
 10+} from "../deps.ts";
 11+import type { MapEntity } from "../deps.ts";
 12+
 13+import { createApi } from "./api.ts";
 14+import {
 15+  customKey,
 16+  queryCtx,
 17+  requestMonitor,
 18+  undo,
 19+  undoer,
 20+  urlParser,
 21+} from "./middleware.ts";
 22+import type { UndoCtx } from "./middleware.ts";
 23+import type { ApiCtx } from "./types.ts";
 24+import { setupStore, sleep } from "./util.ts";
 25+import { createKey } from "./create-key.ts";
 26+import {
 27+  createQueryState,
 28+  DATA_NAME,
 29+  LOADERS_NAME,
 30+  selectDataById,
 31+} from "./slice.ts";
 32+import { call } from "../fx/index.ts";
 33+
 34+interface User {
 35+  id: string;
 36+  name: string;
 37+  email: string;
 38+}
 39+
 40+const mockUser: User = { id: "1", name: "test", email: "test@test.com" };
 41+const mockUser2: User = { id: "2", name: "two", email: "two@test.com" };
 42+
 43+// deno-lint-ignore no-explicit-any
 44+const jsonBlob = (data: any) => {
 45+  return JSON.stringify(data);
 46+};
 47+
 48+const tests = describe("middleware");
 49+
 50+it(tests, "basic", () => {
 51+  const name = "users";
 52+  const cache = createTable<User>({ name });
 53+  const query = createApi<ApiCtx>();
 54+
 55+  query.use(queryCtx);
 56+  query.use(urlParser);
 57+  query.use(query.routes());
 58+  query.use(function* fetchApi(ctx, next) {
 59+    if (`${ctx.req().url}`.startsWith("/users/")) {
 60+      ctx.json = { ok: true, data: mockUser2 };
 61+      yield* next();
 62+      return;
 63+    }
 64+    const data = {
 65+      users: [mockUser],
 66+    };
 67+    ctx.json = { ok: true, data };
 68+    yield* next();
 69+  });
 70+
 71+  const fetchUsers = query.create(
 72+    `/users`,
 73+    function* processUsers(ctx: ApiCtx<unknown, { users: User[] }>, next) {
 74+      yield* next();
 75+      if (!ctx.json.ok) return;
 76+      const { users } = ctx.json.data;
 77+      const curUsers = users.reduce<MapEntity<User>>((acc, u) => {
 78+        acc[u.id] = u;
 79+        return acc;
 80+      }, {});
 81+      yield* put(cache.actions.add(curUsers));
 82+    },
 83+  );
 84+
 85+  const fetchUser = query.create<{ id: string }>(
 86+    `/users/:id`,
 87+    {
 88+      supervisor: takeLatest,
 89+    },
 90+    function* processUser(ctx, next) {
 91+      ctx.request = ctx.req({ method: "POST" });
 92+      yield* next();
 93+      if (!ctx.json.ok) return;
 94+      const curUser = ctx.json.data;
 95+      const curUsers = { [curUser.id]: curUser };
 96+      yield* put(cache.actions.add(curUsers));
 97+    },
 98+  );
 99+
100+  const reducers = createReducerMap(cache);
101+  const { store, run } = setupStore(reducers, { fx: query.bootup });
102+  run();
103+
104+  store.dispatch(fetchUsers());
105+  expect(store.getState()).toEqual({
106+    ...createQueryState(),
107+    users: { [mockUser.id]: mockUser },
108+  });
109+  store.dispatch(fetchUser({ id: "2" }));
110+  expect(store.getState()).toEqual({
111+    ...createQueryState(),
112+    users: { [mockUser.id]: mockUser, [mockUser2.id]: mockUser2 },
113+  });
114+});
115+
116+it(tests, "with loader", () => {
117+  const users = createTable<User>({ name: "users" });
118+
119+  const api = createApi<ApiCtx>();
120+  api.use(requestMonitor());
121+  api.use(api.routes());
122+  api.use(function* fetchApi(ctx, next) {
123+    ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
124+    ctx.json = { ok: true, data: { users: [mockUser] } };
125+    yield* next();
126+  });
127+
128+  const fetchUsers = api.create(
129+    `/users`,
130+    function* processUsers(ctx: ApiCtx<unknown, { users: User[] }>, next) {
131+      yield* next();
132+      if (!ctx.json.ok) return;
133+
134+      const { data } = ctx.json;
135+      const curUsers = data.users.reduce<MapEntity<User>>((acc, u) => {
136+        acc[u.id] = u;
137+        return acc;
138+      }, {});
139+
140+      ctx.actions.push(users.actions.add(curUsers));
141+    },
142+  );
143+
144+  const reducers = createReducerMap(users);
145+  const { store, run } = setupStore(reducers, { fx: api.bootup });
146+  run();
147+
148+  store.dispatch(fetchUsers());
149+  assertLike(store.getState(), {
150+    [users.name]: { [mockUser.id]: mockUser },
151+    [LOADERS_NAME]: {
152+      "/users": {
153+        status: "success",
154+      },
155+    },
156+  });
157+});
158+
159+it(tests, "with item loader", () => {
160+  const users = createTable<User>({ name: "users" });
161+
162+  const api = createApi<ApiCtx>();
163+  api.use(requestMonitor());
164+  api.use(api.routes());
165+  api.use(function* fetchApi(ctx, next) {
166+    ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
167+    ctx.json = { ok: true, data: { users: [mockUser] } };
168+    yield* next();
169+  });
170+
171+  const fetchUser = api.create<{ id: string }>(
172+    `/users/:id`,
173+    function* processUsers(ctx: ApiCtx<unknown, { users: User[] }>, next) {
174+      yield* next();
175+      if (!ctx.json.ok) return;
176+
177+      const { data } = ctx.json;
178+      const curUsers = data.users.reduce<MapEntity<User>>((acc, u) => {
179+        acc[u.id] = u;
180+        return acc;
181+      }, {});
182+
183+      ctx.actions.push(users.actions.add(curUsers));
184+    },
185+  );
186+
187+  const reducers = createReducerMap(users);
188+  const { store, run } = setupStore(reducers, { fx: api.bootup });
189+  run();
190+
191+  const action = fetchUser({ id: mockUser.id });
192+  store.dispatch(action);
193+  assertLike(store.getState(), {
194+    [users.name]: { [mockUser.id]: mockUser },
195+    [LOADERS_NAME]: {
196+      "/users/:id": {
197+        status: "success",
198+      },
199+      [action.payload.key]: {
200+        status: "success",
201+      },
202+    },
203+  });
204+});
205+
206+it(tests, "with POST", () => {
207+  const name = "users";
208+  const cache = createTable<User>({ name });
209+  const query = createApi();
210+
211+  query.use(queryCtx);
212+  query.use(urlParser);
213+  query.use(query.routes());
214+  query.use(function* fetchApi(ctx, next) {
215+    const request = ctx.req();
216+    asserts.assertEquals(request, {
217+      url: "/users",
218+      headers: {},
219+      method: "POST",
220+      body: JSON.stringify({ email: "test@test.com" }),
221+    });
222+
223+    const data = {
224+      users: [mockUser],
225+    };
226+    ctx.response = new Response(jsonBlob(data), { status: 200 });
227+    yield* next();
228+  });
229+
230+  const createUser = query.create<{ email: string }>(
231+    `/users [POST]`,
232+    function* processUsers(
233+      ctx: ApiCtx<{ email: string }, { users: User[] }>,
234+      next,
235+    ) {
236+      ctx.request = ctx.req({
237+        method: "POST",
238+        body: JSON.stringify({ email: ctx.payload.email }),
239+      });
240+
241+      yield* next();
242+
243+      if (!ctx.json.ok) return;
244+
245+      const { users } = ctx.json.data;
246+      const curUsers = users.reduce<MapEntity<User>>((acc, u) => {
247+        acc[u.id] = u;
248+        return acc;
249+      }, {});
250+      yield* put(cache.actions.add(curUsers));
251+    },
252+  );
253+
254+  const reducers = createReducerMap(cache);
255+  const { store, run } = setupStore(reducers, { fx: query.bootup });
256+  run();
257+  store.dispatch(createUser({ email: mockUser.email }));
258+});
259+
260+it(tests, "simpleCache", () => {
261+  const api = createApi<ApiCtx>();
262+  api.use(requestMonitor());
263+  api.use(api.routes());
264+  api.use(function* fetchApi(ctx, next) {
265+    const data = { users: [mockUser] };
266+    ctx.response = new Response(jsonBlob(data));
267+    ctx.json = { ok: true, data };
268+    yield* next();
269+  });
270+
271+  const fetchUsers = api.get("/users", api.cache());
272+  const { store, run } = setupStore({ def: (s) => s || null }, {
273+    fx: api.bootup,
274+  });
275+  run();
276+
277+  const action = fetchUsers();
278+  store.dispatch(action);
279+  assertLike(store.getState(), {
280+    [DATA_NAME]: {
281+      [action.payload.key]: { users: [mockUser] },
282+    },
283+    [LOADERS_NAME]: {
284+      [`${fetchUsers}`]: {
285+        status: "success",
286+      },
287+    },
288+  });
289+});
290+
291+it(tests, "overriding default loader behavior", () => {
292+  const users = createTable<User>({ name: "users" });
293+
294+  const api = createApi<ApiCtx>();
295+  api.use(requestMonitor());
296+  api.use(api.routes());
297+  api.use(function* fetchApi(ctx, next) {
298+    const data = { users: [mockUser] };
299+    ctx.response = new Response(jsonBlob(data));
300+    ctx.json = { ok: true, data };
301+    yield* next();
302+  });
303+
304+  const fetchUsers = api.create(
305+    `/users`,
306+    function* (ctx: ApiCtx<unknown, { users: User[] }>, next) {
307+      const id = ctx.name;
308+      yield* next();
309+
310+      if (!ctx.json.ok) {
311+        return;
312+      }
313+      const { data } = ctx.json;
314+      const curUsers = data.users.reduce<MapEntity<User>>((acc, u) => {
315+        acc[u.id] = u;
316+        return acc;
317+      }, {});
318+
319+      ctx.loader = { id, message: "yes", meta: { wow: true } };
320+      ctx.actions.push(users.actions.add(curUsers));
321+    },
322+  );
323+
324+  const reducers = createReducerMap(users);
325+  const { store, run } = setupStore(reducers, { fx: api.bootup });
326+  run();
327+
328+  store.dispatch(fetchUsers());
329+  assertLike(store.getState(), {
330+    [users.name]: { [mockUser.id]: mockUser },
331+    [LOADERS_NAME]: {
332+      [`${fetchUsers}`]: {
333+        status: "success",
334+        message: "yes",
335+        meta: { wow: true },
336+      },
337+    },
338+  });
339+});
340+
341+it(tests, "undo", () => {
342+  const api = createApi<UndoCtx>();
343+  api.use(requestMonitor());
344+  api.use(api.routes());
345+  api.use(undoer());
346+
347+  api.use(function* fetchApi(ctx, next) {
348+    yield* delay(500);
349+    ctx.response = new Response(jsonBlob({ users: [mockUser] }), {
350+      status: 200,
351+    });
352+    yield* next();
353+  });
354+
355+  const createUser = api.post("/users", function* (ctx, next) {
356+    ctx.undoable = true;
357+    yield* next();
358+  });
359+
360+  const { store, run } = setupStore({ def: (s) => s || null }, {
361+    fx: api.bootup,
362+  });
363+  run();
364+
365+  const action = createUser();
366+  store.dispatch(action);
367+  store.dispatch(undo());
368+  assertLike(store.getState(), {
369+    ...createQueryState({
370+      [LOADERS_NAME]: {
371+        [`${createUser}`]: defaultLoadingItem(),
372+        [action.payload.name]: defaultLoadingItem(),
373+      },
374+    }),
375+  });
376+});
377+
378+it(tests, "requestMonitor - error handler", () => {
379+  let err = false;
380+  console.error = (msg: string) => {
381+    if (err) return;
382+    asserts.assertEquals(
383+      msg,
384+      "Error: something happened.  Check the endpoint [/users]",
385+    );
386+    err = true;
387+  };
388+  const name = "users";
389+  const cache = createTable<User>({ name });
390+  const query = createApi<ApiCtx>();
391+
392+  query.use(requestMonitor());
393+  query.use(query.routes());
394+  query.use(function* () {
395+    throw new Error("something happened");
396+  });
397+
398+  const fetchUsers = query.create(`/users`);
399+
400+  const reducers = createReducerMap(cache);
401+  const { store, run } = setupStore(reducers, { fx: query.bootup });
402+  run();
403+  store.dispatch(fetchUsers());
404+});
405+
406+it(tests, "createApi with own key", async () => {
407+  const query = createApi();
408+  query.use(requestMonitor());
409+  query.use(query.routes());
410+  query.use(customKey);
411+  query.use(function* fetchApi(ctx, next) {
412+    const data = {
413+      users: [{ ...mockUser, ...ctx.action.payload.options }],
414+    };
415+    ctx.response = new Response(jsonBlob(data), { status: 200 });
416+    yield* next();
417+  });
418+
419+  const theTestKey = `some-custom-key-${Math.ceil(Math.random() * 1000)}`;
420+
421+  const createUserCustomKey = query.post<{ email: string }>(
422+    `/users`,
423+    function* processUsers(ctx: ApiCtx, next) {
424+      ctx.cache = true;
425+      ctx.key = theTestKey; // or some calculated key //
426+      yield* next();
427+      const buff = yield* call(() => {
428+        if (!ctx.response) throw new Error("no response");
429+        return ctx.response.arrayBuffer();
430+      });
431+      if (!buff.ok) {
432+        throw buff.error;
433+      }
434+
435+      const result = new TextDecoder("utf-8").decode(buff.value);
436+      const { users } = JSON.parse(result);
437+      if (!users) return;
438+      const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
439+        acc[u.id] = u;
440+        return acc;
441+      }, {});
442+      ctx.response = new Response();
443+      ctx.json = {
444+        ok: true,
445+        data: curUsers,
446+      };
447+    },
448+  );
449+  const newUEmail = mockUser.email + ".org";
450+  const reducers = createReducerMap();
451+  const { store, run } = setupStore(reducers, { fx: query.bootup });
452+  run();
453+
454+  store.dispatch(createUserCustomKey({ email: newUEmail }));
455+  await sleep(150);
456+  const expectedKey = theTestKey
457+    ? `/users [POST]|${theTestKey}`
458+    : createKey("/users [POST]", { email: newUEmail });
459+
460+  const s = store.getState();
461+  asserts.assertEquals(selectDataById(s, { id: expectedKey }), {
462+    "1": { id: "1", name: "test", email: newUEmail },
463+  });
464+
465+  asserts.assert(
466+    expectedKey.split("|")[1] === theTestKey,
467+    "the keypart should match the input",
468+  );
469+});
470+
471+it(tests, "createApi with custom key but no payload", async () => {
472+  const query = createApi();
473+  query.use(requestMonitor());
474+  query.use(query.routes());
475+  query.use(customKey);
476+  query.use(function* fetchApi(ctx, next) {
477+    const data = {
478+      users: [mockUser],
479+    };
480+    ctx.response = new Response(jsonBlob(data), { status: 200 });
481+    yield* next();
482+  });
483+
484+  const theTestKey = `some-custom-key-${Math.ceil(Math.random() * 1000)}`;
485+
486+  const getUsers = query.get(
487+    `/users`,
488+    function* processUsers(ctx: ApiCtx, next) {
489+      ctx.cache = true;
490+      ctx.key = theTestKey; // or some calculated key //
491+      yield* next();
492+      const buff = yield* call(() => {
493+        if (!ctx.response) throw new Error("no response");
494+        return ctx.response?.arrayBuffer();
495+      });
496+      if (!buff.ok) {
497+        throw buff.error;
498+      }
499+
500+      const result = new TextDecoder("utf-8").decode(buff.value);
501+      const { users } = JSON.parse(result);
502+      if (!users) return;
503+      const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
504+        acc[u.id] = u;
505+        return acc;
506+      }, {});
507+      ctx.response = new Response();
508+      ctx.json = {
509+        ok: true,
510+        data: curUsers,
511+      };
512+    },
513+  );
514+
515+  const reducers = createReducerMap();
516+  const { store, run } = setupStore(reducers, { fx: query.bootup });
517+  run();
518+
519+  store.dispatch(getUsers());
520+  await sleep(150);
521+  const expectedKey = theTestKey
522+    ? `/users [GET]|${theTestKey}`
523+    : createKey("/users [GET]", null);
524+
525+  const s = store.getState();
526+  asserts.assertEquals(selectDataById(s, { id: expectedKey }), {
527+    "1": mockUser,
528+  });
529+
530+  asserts.assert(
531+    expectedKey.split("|")[1] === theTestKey,
532+    "the keypart should match the input",
533+  );
534+});
A query/middleware.ts
+348, -0
  1@@ -0,0 +1,348 @@
  2+import { call, race } from "../fx/index.ts";
  3+import { put, select, take } from "../redux/index.ts";
  4+import { batchActions, sleep } from "../deps.ts";
  5+import { compose } from "../compose.ts";
  6+import type { OpFn } from "../types.ts";
  7+
  8+import type {
  9+  Action,
 10+  ApiCtx,
 11+  ApiRequest,
 12+  LoaderCtx,
 13+  Next,
 14+  PipeCtx,
 15+  RequiredApiRequest,
 16+} from "./types.ts";
 17+import { createAction, isObject, mergeRequest } from "./util.ts";
 18+import {
 19+  addData,
 20+  resetLoaderById,
 21+  selectDataById,
 22+  setLoaderError,
 23+  setLoaderStart,
 24+  setLoaderSuccess,
 25+} from "./slice.ts";
 26+
 27+/**
 28+ * This middleware will catch any errors in the pipeline
 29+ * and return the context object.
 30+ */
 31+export function* errorHandler<Ctx extends PipeCtx = PipeCtx>(
 32+  ctx: Ctx,
 33+  next: Next,
 34+) {
 35+  try {
 36+    yield* next();
 37+  } catch (err) {
 38+    console.error(
 39+      `Error: ${err.message}.  Check the endpoint [${ctx.name}]`,
 40+      ctx,
 41+    );
 42+    throw err;
 43+  }
 44+}
 45+
 46+/**
 47+ * This middleware sets up the context object with some values that are
 48+ * necessary for {@link createApi} to work properly.
 49+ */
 50+export function* queryCtx<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
 51+  if (!ctx.req) {
 52+    ctx.req = (r?: ApiRequest): RequiredApiRequest =>
 53+      mergeRequest(ctx.request, r);
 54+  }
 55+  if (!ctx.request) ctx.request = ctx.req();
 56+  if (!ctx.response) ctx.response = null;
 57+  if (!ctx.json) ctx.json = { ok: false, data: {} };
 58+  if (!ctx.actions) ctx.actions = [];
 59+  if (!ctx.bodyType) ctx.bodyType = "json";
 60+  yield* next();
 61+}
 62+
 63+/**
 64+ * This middleware converts the name provided to {@link createApi} into data for the fetch request.
 65+ */
 66+export function* urlParser<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
 67+  const httpMethods = [
 68+    "get",
 69+    "head",
 70+    "post",
 71+    "put",
 72+    "delete",
 73+    "connect",
 74+    "options",
 75+    "trace",
 76+    "patch",
 77+  ];
 78+
 79+  const options = ctx.payload || {};
 80+  if (!isObject(options)) {
 81+    yield* next();
 82+    return;
 83+  }
 84+
 85+  let url = Object.keys(options).reduce((acc, key) => {
 86+    return acc.replace(`:${key}`, options[key]);
 87+  }, ctx.name);
 88+
 89+  let method = "";
 90+  httpMethods.forEach((curMethod) => {
 91+    const pattern = new RegExp(`\\s*\\[` + curMethod + `\\]\\s*\\w*`, "i");
 92+    const tmpUrl = url.replace(pattern, "");
 93+    if (tmpUrl.length !== url.length) {
 94+      method = curMethod.toLocaleUpperCase();
 95+    }
 96+    url = tmpUrl;
 97+  }, url);
 98+
 99+  ctx.request = ctx.req({ url });
100+  if (method) {
101+    ctx.request = ctx.req({ method });
102+  }
103+
104+  yield* next();
105+}
106+
107+/**
108+ * This middleware will take the result of `ctx.actions` and dispatch them
109+ * as a single batch.
110+ *
111+ * @remarks This is useful because sometimes there are a lot of actions that need dispatched
112+ * within the pipeline of the middleware and instead of dispatching them serially this
113+ * improves performance by only hitting the reducers once.
114+ */
115+export function* dispatchActions(ctx: { actions: Action[] }, next: Next) {
116+  if (!ctx.actions) ctx.actions = [];
117+  yield* next();
118+  if (ctx.actions.length === 0) return;
119+  yield* put(batchActions(ctx.actions));
120+}
121+
122+/**
123+ * This middleware creates a loader for a generator function which allows us to track
124+ * the status of a pipeline function.
125+ */
126+export function* loadingMonitorSimple<Ctx extends LoaderCtx = LoaderCtx>(
127+  ctx: Ctx,
128+  next: Next,
129+) {
130+  yield* put(
131+    batchActions([
132+      setLoaderStart({ id: ctx.name }),
133+      setLoaderStart({ id: ctx.key }),
134+    ]),
135+  );
136+  if (!ctx.loader) ctx.loader = {} as any;
137+
138+  yield* next();
139+
140+  const payload = ctx.loader || {};
141+  yield* put(
142+    batchActions([
143+      setLoaderSuccess({ id: ctx.name, ...payload }),
144+      setLoaderSuccess({ id: ctx.key, ...payload }),
145+    ]),
146+  );
147+}
148+
149+/**
150+ * This middleware will track the status of a fetch request.
151+ */
152+export function loadingMonitor<Ctx extends ApiCtx = ApiCtx>(
153+  errorFn: (ctx: Ctx) => string = (ctx) => ctx.json?.data?.message || "",
154+) {
155+  return function* trackLoading(ctx: Ctx, next: Next) {
156+    yield* put(
157+      batchActions([
158+        setLoaderStart({ id: ctx.name }),
159+        setLoaderStart({ id: ctx.key }),
160+      ]),
161+    );
162+    if (!ctx.loader) ctx.loader = {} as any;
163+
164+    yield* next();
165+
166+    if (!ctx.response) {
167+      ctx.actions.push(resetLoaderById(ctx.name), resetLoaderById(ctx.key));
168+      return;
169+    }
170+
171+    const payload = ctx.loader || {};
172+    if (!ctx.response.ok) {
173+      ctx.actions.push(
174+        setLoaderError({ id: ctx.name, message: errorFn(ctx), ...payload }),
175+        setLoaderError({ id: ctx.key, message: errorFn(ctx), ...payload }),
176+      );
177+      return;
178+    }
179+
180+    ctx.actions.push(
181+      setLoaderSuccess({ id: ctx.name, ...payload }),
182+      setLoaderSuccess({ id: ctx.key, ...payload }),
183+    );
184+  };
185+}
186+
187+export interface UndoCtx<P = any, S = any, E = any> extends ApiCtx<P, S, E> {
188+  undoable: boolean;
189+}
190+
191+export const doIt = createAction("DO_IT");
192+export const undo = createAction("UNDO");
193+/**
194+ * This middleware will allow pipeline functions to be undoable which means before they are activated
195+ * we have a timeout that allows the function to be cancelled.
196+ */
197+export function undoer<Ctx extends UndoCtx = UndoCtx>(
198+  doItType = `${doIt}`,
199+  undoType = `${undo}`,
200+  timeout = 30 * 1000,
201+) {
202+  return function* onUndo(ctx: Ctx, next: Next) {
203+    if (!ctx.undoable) {
204+      yield* next();
205+      return;
206+    }
207+
208+    const winner = yield* race({
209+      doIt: () => take(`${doItType}`),
210+      undo: () => take(`${undoType}`),
211+      timeout: () => sleep(timeout),
212+    });
213+
214+    if (winner.undo || winner.timeout) {
215+      return;
216+    }
217+
218+    yield* next();
219+  };
220+}
221+
222+export interface OptimisticCtx<
223+  A extends Action = Action,
224+  R extends Action = Action,
225+> extends ApiCtx {
226+  optimistic: {
227+    apply: A;
228+    revert: R;
229+  };
230+}
231+
232+/**
233+ * This middleware performs an optimistic update for a middleware pipeline.
234+ * It accepts an `apply` and `revert` action.
235+ *
236+ * @remarks This means that we will first `apply` and then if the request is successful we
237+ * keep the change or we `revert` if there's an error.
238+ */
239+export function* optimistic<
240+  Ctx extends OptimisticCtx = OptimisticCtx,
241+>(ctx: Ctx, next: Next) {
242+  if (!ctx.optimistic) {
243+    yield* next();
244+    return;
245+  }
246+
247+  const { apply, revert } = ctx.optimistic;
248+  // optimistically update user
249+  yield* put(apply);
250+
251+  yield* next();
252+
253+  if (!ctx.response || !ctx.response.ok) {
254+    yield* put(revert);
255+  }
256+}
257+
258+/**
259+ * This middleware will automatically cache any data found inside `ctx.json`
260+ * which is where we store JSON data from the `fetcher` middleware.
261+ */
262+export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
263+  ctx: Ctx,
264+  next: Next,
265+) {
266+  ctx.cacheData = yield* select((state) =>
267+    selectDataById(state, { id: ctx.key })
268+  );
269+  yield* next();
270+  if (!ctx.cache) return;
271+  const { data } = ctx.json;
272+  ctx.actions.push(addData({ [ctx.key]: data }));
273+  ctx.cacheData = data;
274+}
275+
276+/**
277+ * This middleware allows the user to override the default key provided to every pipeline function
278+ * and instead use whatever they want.
279+ *
280+ * @example
281+ * ```ts
282+ * import { createPipe } from 'saga-query';
283+ *
284+ * const thunk = createPipe();
285+ * thunk.use(customKey);
286+ *
287+ * const doit = thunk.create('some-action', function*(ctx, next) {
288+ *   ctx.key = 'something-i-want';
289+ * })
290+ * ```
291+ */
292+export function* customKey<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
293+  if (
294+    ctx?.key &&
295+    ctx?.action?.payload?.key &&
296+    ctx.key !== ctx.action.payload.key
297+  ) {
298+    const newKey = ctx.name.split("|")[0] + "|" + ctx.key;
299+    ctx.key = newKey;
300+    ctx.action.payload.key = newKey;
301+  }
302+  yield* next();
303+}
304+
305+/**
306+ * This middleware is a composition of many middleware used to faciliate the {@link createApi}
307+ */
308+export function requestMonitor<Ctx extends ApiCtx = ApiCtx>(
309+  errorFn?: (ctx: Ctx) => string,
310+) {
311+  return compose<Ctx>([
312+    errorHandler,
313+    queryCtx,
314+    urlParser,
315+    dispatchActions,
316+    loadingMonitor(errorFn),
317+    simpleCache,
318+    customKey,
319+  ]);
320+}
321+
322+export interface PerfCtx<P = unknown> extends PipeCtx<P> {
323+  performance: number;
324+}
325+
326+/**
327+ * This middleware will add `performance.now()` before and after your middleware pipeline.
328+ */
329+export function* performanceMonitor<Ctx extends PerfCtx = PerfCtx>(
330+  ctx: Ctx,
331+  next: Next,
332+) {
333+  const t0 = performance.now();
334+  yield* next();
335+  const t1 = performance.now();
336+  ctx.performance = t1 - t0;
337+}
338+
339+/**
340+ * This middleware will call the `saga` provided with the action sent to the middleware pipeline.
341+ */
342+export function wrap<Ctx extends PipeCtx = PipeCtx>(
343+  op: (a: Action) => OpFn,
344+) {
345+  return function* (ctx: Ctx, next: Next) {
346+    yield* call(() => op(ctx.action));
347+    yield* next();
348+  };
349+}
A query/pipe.test.ts
+410, -0
  1@@ -0,0 +1,410 @@
  2+import { assertLike, asserts, describe, it } from "../test.ts";
  3+import { call } from "../fx/index.ts";
  4+import { put } from "../redux/index.ts";
  5+import { createReducerMap, createTable, sleep as delay } from "../deps.ts";
  6+import type { Action, MapEntity } from "../deps.ts";
  7+
  8+import { createPipe } from "./pipe.ts";
  9+import type { Next, PipeCtx } from "./types.ts";
 10+import { setupStore as prepStore, sleep } from "./util.ts";
 11+import { createQueryState } from "./slice.ts";
 12+import { OpFn } from "../types.ts";
 13+
 14+// deno-lint-ignore no-explicit-any
 15+interface RoboCtx<D = Record<string, unknown>, P = any> extends PipeCtx<P> {
 16+  url: string;
 17+  request: { method: string; body?: Record<string, unknown> };
 18+  response: D;
 19+  actions: Action[];
 20+}
 21+
 22+interface User {
 23+  id: string;
 24+  name: string;
 25+  email: string;
 26+}
 27+
 28+interface UserResponse {
 29+  id: string;
 30+  name: string;
 31+  email_address: string;
 32+}
 33+
 34+const deserializeUser = (u: UserResponse): User => {
 35+  return {
 36+    id: u.id,
 37+    name: u.name,
 38+    email: u.email_address,
 39+  };
 40+};
 41+
 42+interface Ticket {
 43+  id: string;
 44+  name: string;
 45+}
 46+
 47+interface TicketResponse {
 48+  id: string;
 49+  name: string;
 50+}
 51+
 52+const deserializeTicket = (u: TicketResponse): Ticket => {
 53+  return {
 54+    id: u.id,
 55+    name: u.name,
 56+  };
 57+};
 58+
 59+const users = createTable<User>({ name: "USER" });
 60+const tickets = createTable<Ticket>({ name: "TICKET" });
 61+const reducers = createReducerMap(users, tickets);
 62+
 63+const mockUser = { id: "1", name: "test", email_address: "test@test.com" };
 64+const mockTicket = { id: "2", name: "test-ticket" };
 65+
 66+function* convertNameToUrl(ctx: RoboCtx, next: Next) {
 67+  if (!ctx.url) {
 68+    ctx.url = ctx.name;
 69+  }
 70+  yield* next();
 71+}
 72+
 73+function* onFetchApi(ctx: RoboCtx, next: Next) {
 74+  const url = ctx.url;
 75+  let json = {};
 76+  if (url === "/users") {
 77+    json = {
 78+      users: [mockUser],
 79+    };
 80+  }
 81+
 82+  if (url === "/tickets") {
 83+    json = {
 84+      tickets: [mockTicket],
 85+    };
 86+  }
 87+
 88+  ctx.response = json;
 89+  yield* next();
 90+}
 91+
 92+function* setupActionState(ctx: RoboCtx, next: Next) {
 93+  ctx.actions = [];
 94+  yield* next();
 95+}
 96+
 97+function* processUsers(ctx: RoboCtx<{ users?: UserResponse[] }>, next: Next) {
 98+  if (!ctx.response.users) {
 99+    yield* next();
100+    return;
101+  }
102+  const curUsers = ctx.response.users.reduce<MapEntity<User>>((acc, u) => {
103+    acc[u.id] = deserializeUser(u);
104+    return acc;
105+  }, {});
106+  ctx.actions.push(users.actions.add(curUsers));
107+  yield* next();
108+}
109+
110+function* processTickets(
111+  ctx: RoboCtx<{ tickets?: UserResponse[] }>,
112+  next: Next,
113+) {
114+  if (!ctx.response.tickets) {
115+    yield* next();
116+    return;
117+  }
118+  const curTickets = ctx.response.tickets.reduce<MapEntity<Ticket>>(
119+    (acc, u) => {
120+      acc[u.id] = deserializeTicket(u);
121+      return acc;
122+    },
123+    {},
124+  );
125+  ctx.actions.push(tickets.actions.add(curTickets));
126+  yield* next();
127+}
128+
129+function* saveToRedux(ctx: RoboCtx, next: Next) {
130+  for (let i = 0; i < ctx.actions.length; i += 1) {
131+    const action = ctx.actions[i];
132+    yield* put(action);
133+  }
134+  yield* next();
135+}
136+
137+function setupStore(fx: OpFn) {
138+  const store = prepStore(reducers, { fx });
139+  return store;
140+}
141+
142+const tests = describe("createPipe()");
143+
144+it(
145+  tests,
146+  "when create a query fetch pipeline - execute all middleware and save to redux",
147+  () => {
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+
158+    const { store, run } = setupStore(api.bootup);
159+    run();
160+
161+    store.dispatch(fetchUsers());
162+
163+    asserts.assertEquals(store.getState(), {
164+      ...createQueryState(),
165+      [users.name]: { [mockUser.id]: deserializeUser(mockUser) },
166+      [tickets.name]: {},
167+    });
168+  },
169+);
170+
171+it(
172+  tests,
173+  "when providing a generator the to api.create function - should call that generator before all other middleware",
174+  () => {
175+    const api = createPipe<RoboCtx>();
176+    api.use(api.routes());
177+    api.use(convertNameToUrl);
178+    api.use(onFetchApi);
179+    api.use(setupActionState);
180+    api.use(processUsers);
181+    api.use(processTickets);
182+    api.use(saveToRedux);
183+    const fetchUsers = api.create(`/users`);
184+    const fetchTickets = api.create(`/ticket-wrong-url`, function* (ctx, next) {
185+      // before middleware has been triggered
186+      ctx.url = "/tickets";
187+
188+      // triggers all middleware
189+      yield* next();
190+
191+      // after middleware has been triggered
192+      asserts.assertEquals(ctx.actions, [
193+        tickets.actions.add({
194+          [mockTicket.id]: deserializeTicket(mockTicket),
195+        }),
196+      ]);
197+      yield* put(fetchUsers());
198+    });
199+
200+    const { store, run } = setupStore(api.bootup);
201+    run();
202+
203+    store.dispatch(fetchTickets());
204+    asserts.assertEquals(store.getState(), {
205+      ...createQueryState(),
206+      [users.name]: { [mockUser.id]: deserializeUser(mockUser) },
207+      [tickets.name]: { [mockTicket.id]: deserializeTicket(mockTicket) },
208+    });
209+  },
210+);
211+
212+it(tests, "error handling", () => {
213+  const api = createPipe<RoboCtx>();
214+  api.use(api.routes());
215+  api.use(function* upstream(_, next) {
216+    try {
217+      yield* next();
218+    } catch (_) {
219+      asserts.assert(true);
220+    }
221+  });
222+  api.use(function* fail() {
223+    throw new Error("some error");
224+  });
225+
226+  const action = api.create(`/error`);
227+  const { store, run } = setupStore(api.bootup);
228+  run();
229+  store.dispatch(action());
230+});
231+
232+it(tests, "error handling inside create", () => {
233+  const api = createPipe<RoboCtx>();
234+  api.use(api.routes());
235+  api.use(function* fail() {
236+    throw new Error("some error");
237+  });
238+
239+  const action = api.create(`/error`, function* (_, next) {
240+    try {
241+      yield* next();
242+    } catch (_) {
243+      asserts.assert(true);
244+    }
245+  });
246+  const { store, run } = setupStore(api.bootup);
247+  run();
248+  store.dispatch(action());
249+});
250+
251+it(tests, "error handling - error handler", () => {
252+  const api = createPipe<RoboCtx>();
253+  api.use(api.routes());
254+  api.use(function* upstream() {
255+    throw new Error("failure");
256+  });
257+
258+  const action = api.create(`/error`);
259+  const { store, run } = setupStore(api.bootup);
260+  run();
261+  store.dispatch(action());
262+});
263+
264+it(tests, "create fn is an array", () => {
265+  const api = createPipe<RoboCtx>();
266+  api.use(api.routes());
267+  api.use(function* (ctx, next) {
268+    asserts.assertEquals(ctx.request, {
269+      method: "POST",
270+      body: {
271+        test: "me",
272+      },
273+    });
274+    yield* next();
275+  });
276+  const action = api.create("/users", [
277+    function* (ctx, next) {
278+      ctx.request = {
279+        method: "POST",
280+      };
281+      yield* next();
282+    },
283+    function* (ctx, next) {
284+      ctx.request.body = { test: "me" };
285+      yield* next();
286+    },
287+  ]);
288+
289+  const { store, run } = setupStore(api.bootup);
290+  run();
291+  store.dispatch(action());
292+});
293+
294+it(tests, "run() on endpoint action - should run the effect", () => {
295+  const api = createPipe<RoboCtx>();
296+  api.use(api.routes());
297+  let acc = "";
298+  const action1 = api.create("/users", function* (ctx, next) {
299+    yield* next();
300+    ctx.request = { method: "expect this" };
301+    acc += "a";
302+  });
303+  const action2 = api.create("/users2", function* (_, next) {
304+    yield* next();
305+    const curCtx = yield* call(() => action1.run(action1()));
306+    acc += "b";
307+    asserts.assert(acc === "ab");
308+    assertLike(curCtx, {
309+      action: {
310+        type: `@@saga-query${action1}`,
311+        payload: {
312+          name: "/users",
313+        },
314+      },
315+      name: "/users",
316+      request: { method: "expect this" },
317+    });
318+  });
319+
320+  const { store, run } = setupStore(api.bootup);
321+  run();
322+  store.dispatch(action2());
323+});
324+
325+it(tests, "middleware order of execution", async () => {
326+  let acc = "";
327+  const api = createPipe();
328+  api.use(api.routes());
329+
330+  api.use(function* (_, next) {
331+    yield* delay(10);
332+    acc += "b";
333+    yield* next();
334+    yield* delay(10);
335+    acc += "f";
336+  });
337+
338+  api.use(function* (_, next) {
339+    acc += "c";
340+    yield* next();
341+    acc += "d";
342+    yield* delay(30);
343+    acc += "e";
344+  });
345+
346+  const action = api.create("/api", function* (_, next) {
347+    acc += "a";
348+    yield* next();
349+    acc += "g";
350+  });
351+
352+  const { store, run } = setupStore(api.bootup);
353+  run();
354+  store.dispatch(action());
355+
356+  await sleep(150);
357+  asserts.assert(acc === "abcdefg");
358+});
359+
360+it.ignore(tests, "retry with actionFn", async () => {
361+  let acc = "";
362+  let called = false;
363+
364+  const api = createPipe();
365+  api.use(api.routes());
366+
367+  const action = api.create("/api", function* (ctx, next) {
368+    acc += "a";
369+    yield* next();
370+    acc += "g";
371+
372+    if (!called) {
373+      called = true;
374+      yield* put(ctx.actionFn());
375+    }
376+  });
377+
378+  const { store, run } = setupStore(api.bootup);
379+  run();
380+  store.dispatch(action());
381+
382+  await sleep(150);
383+  asserts.assertEquals(acc, "agag");
384+});
385+
386+it.ignore(tests, "retry with actionFn with payload", async () => {
387+  let acc = "";
388+  const api = createPipe();
389+  api.use(api.routes());
390+
391+  api.use(function* (ctx: PipeCtx<{ page: number }>, next) {
392+    yield* next();
393+    if (ctx.payload.page == 1) {
394+      yield* put(ctx.actionFn({ page: 2 }));
395+    }
396+  });
397+
398+  const action = api.create<{ page: number }>("/api", function* (_, next) {
399+    acc += "a";
400+    yield* next();
401+    acc += "g";
402+  });
403+
404+  const { store, run } = setupStore(api.bootup);
405+  const task = run();
406+  store.dispatch(action({ page: 1 }));
407+
408+  await sleep(150);
409+  asserts.assertEquals(acc, "aagg");
410+  await task;
411+});
A query/pipe.ts
+242, -0
  1@@ -0,0 +1,242 @@
  2+import { call } from "../fx/index.ts";
  3+import { takeEvery } from "../redux/index.ts";
  4+import { compose } from "../compose.ts";
  5+import type { OpFn } from "../types.ts";
  6+import { parallel } from "../index.ts";
  7+
  8+import { isFn, isObject } from "./util.ts";
  9+import { createKey } from "./create-key.ts";
 10+import type {
 11+  ActionWithPayload,
 12+  CreateAction,
 13+  CreateActionPayload,
 14+  CreateActionWithPayload,
 15+  Middleware,
 16+  MiddlewareCo,
 17+  Next,
 18+  Payload,
 19+  PipeCtx,
 20+  Supervisor,
 21+} from "./types.ts";
 22+import { API_ACTION_PREFIX } from "./constant.ts";
 23+
 24+export interface SagaApi<Ctx extends PipeCtx> {
 25+  use: (fn: Middleware<Ctx>) => void;
 26+  routes: () => Middleware<Ctx>;
 27+  bootup: OpFn;
 28+
 29+  /**
 30+   * Name only
 31+   */
 32+  create(name: string): CreateAction<Ctx>;
 33+  create<P>(
 34+    name: string,
 35+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 36+
 37+  /**
 38+   * Name and options
 39+   */
 40+  create(name: string, req: { supervisor?: Supervisor }): CreateAction<Ctx>;
 41+  create<P>(
 42+    name: string,
 43+    req: { supervisor?: Supervisor },
 44+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 45+
 46+  /**
 47+   * Name and middleware
 48+   */
 49+  create(name: string, fn: MiddlewareCo<Ctx>): CreateAction<Ctx>;
 50+  create<Gtx extends Ctx = Ctx>(
 51+    name: string,
 52+    fn: MiddlewareCo<Gtx>,
 53+  ): CreateAction<Gtx>;
 54+  create<P>(
 55+    name: string,
 56+    fn: MiddlewareCo<Omit<Ctx, "payload"> & Payload<P>>,
 57+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 58+  create<P, Gtx extends Ctx = Ctx>(
 59+    name: string,
 60+    fn: MiddlewareCo<Gtx>,
 61+  ): CreateActionWithPayload<Gtx, P>;
 62+
 63+  /*
 64+   * Name, options, and middleware
 65+   */
 66+  create(
 67+    name: string,
 68+    req: { supervisor?: Supervisor },
 69+    fn: MiddlewareCo<Ctx>,
 70+  ): CreateAction<Ctx>;
 71+  create<Gtx extends Ctx = Ctx>(
 72+    name: string,
 73+    req: { supervisor?: Supervisor },
 74+    fn: MiddlewareCo<Gtx>,
 75+  ): CreateAction<Gtx>;
 76+  create<P>(
 77+    name: string,
 78+    req: { supervisor?: Supervisor },
 79+    fn: MiddlewareCo<Omit<Ctx, "payload"> & Payload<P>>,
 80+  ): CreateActionWithPayload<Omit<Ctx, "payload"> & Payload<P>, P>;
 81+  create<P, Gtx extends Ctx = Ctx>(
 82+    name: string,
 83+    req: { supervisor?: Supervisor },
 84+    fn: MiddlewareCo<Gtx>,
 85+  ): CreateActionWithPayload<Gtx, P>;
 86+}
 87+
 88+/**
 89+ * Creates a middleware pipeline.
 90+ *
 91+ * @remarks
 92+ * This middleware pipeline is almost exactly like koa's middleware system.
 93+ * See {@link https://koajs.com}
 94+ *
 95+ * @example
 96+ * ```ts
 97+ * import { createPipe } from 'saga-query';
 98+ *
 99+ * const thunk = createPipe();
100+ * thunk.use(function* (ctx, next) {
101+ *   console.log('beginning');
102+ *   yield next();
103+ *   console.log('end');
104+ * });
105+ * thunks.use(thunks.routes());
106+ *
107+ * const doit = thunk.create('do-something', function*(ctx, next) {
108+ *   console.log('middle');
109+ *   yield next();
110+ *   console.log('middle end');
111+ * });
112+ *
113+ * // ...
114+ *
115+ * store.dispatch(doit());
116+ * // beginning
117+ * // middle
118+ * // middle end
119+ * // end
120+ * ```
121+ */
122+export function createPipe<Ctx extends PipeCtx = PipeCtx<any>>(
123+  {
124+    supervisor = takeEvery,
125+  }: {
126+    supervisor?: Supervisor;
127+  } = { supervisor: takeEvery },
128+): SagaApi<Ctx> {
129+  const middleware: Middleware<Ctx>[] = [];
130+  const visors: { [key: string]: OpFn } = {};
131+  const middlewareMap: { [key: string]: Middleware<Ctx> } = {};
132+  const actionMap: {
133+    [key: string]: CreateActionWithPayload<Ctx, any>;
134+  } = {};
135+
136+  function* defaultMiddleware(_: Ctx, next: Next) {
137+    yield* next();
138+  }
139+
140+  const createType = (post: string) =>
141+    `${API_ACTION_PREFIX}${post.startsWith("/") ? "" : "/"}${post}`;
142+
143+  function* onApi<P extends CreateActionPayload>(action: ActionWithPayload<P>) {
144+    const { name, key, options } = action.payload;
145+    const actionFn = actionMap[name];
146+    const ctx = {
147+      action,
148+      name,
149+      key,
150+      payload: options,
151+      actionFn,
152+    } as unknown as Ctx;
153+    const fn = compose(middleware);
154+    yield* call(() => fn(ctx));
155+    return ctx;
156+  }
157+
158+  function create(name: string, ...args: any[]) {
159+    const type = createType(name);
160+    const action = (payload?: any) => {
161+      return { type, payload };
162+    };
163+    let req = null;
164+    let fn = null;
165+    if (args.length === 2) {
166+      req = args[0];
167+      fn = args[1];
168+    }
169+
170+    if (args.length === 1) {
171+      if (isFn(args[0]) || Array.isArray(args[0])) {
172+        fn = args[0];
173+      } else {
174+        req = args[0];
175+      }
176+    }
177+
178+    if (req && !isObject(req)) {
179+      throw new Error("Options must be an object");
180+    }
181+
182+    if (fn && Array.isArray(fn)) {
183+      fn = compose(fn);
184+    }
185+
186+    if (fn && !isFn(fn)) {
187+      throw new Error("Middleware must be a function");
188+    }
189+
190+    if (middlewareMap[name]) {
191+      console.warn(
192+        `[${name}] already exists which means you have two functions with the same name`,
193+      );
194+    }
195+
196+    middlewareMap[name] = fn || defaultMiddleware;
197+
198+    const tt = req ? (req as any).supervisor : supervisor;
199+    function* curVisor() {
200+      const task = yield* tt(type, onApi);
201+      yield* task;
202+    }
203+    visors[name] = curVisor;
204+
205+    const actionFn = (options?: Ctx["payload"]) => {
206+      const key = createKey(name, options);
207+      return action({ name, key, options });
208+    };
209+    actionFn.run = onApi;
210+    actionFn.toString = () => name;
211+    actionMap[name] = actionFn;
212+
213+    return actionFn;
214+  }
215+
216+  function* bootup() {
217+    const results = yield* parallel(Object.values(visors));
218+    yield* results;
219+  }
220+
221+  function routes() {
222+    function* router(ctx: Ctx, next: Next) {
223+      const match = middlewareMap[ctx.name];
224+      if (!match) {
225+        yield* next();
226+        return;
227+      }
228+
229+      yield* call(() => match(ctx, next));
230+    }
231+
232+    return router;
233+  }
234+
235+  return {
236+    use: (fn: Middleware<Ctx>) => {
237+      middleware.push(fn);
238+    },
239+    create,
240+    routes,
241+    bootup,
242+  };
243+}
A query/react.test.ts
+125, -0
  1@@ -0,0 +1,125 @@
  2+import {
  3+  cleanup,
  4+  fireEvent,
  5+  render,
  6+  screen,
  7+} from "https://esm.sh/@testing-library/react@14.0.0?pin=v122";
  8+
  9+import { React } from "../deps.ts";
 10+import { asserts, beforeEach, describe, it } from "../test.ts";
 11+import {
 12+  createAssign,
 13+  Provider,
 14+  sleep as delay,
 15+  useSelector,
 16+} from "../deps.ts";
 17+
 18+import { createApi } from "./api.ts";
 19+import { requestMonitor } from "./middleware.ts";
 20+import { setupStore } from "./util.ts";
 21+import { useApi } from "./react.ts";
 22+import { selectDataById } from "./slice.ts";
 23+import { createKey } from "./create-key.ts";
 24+
 25+const h = React.createElement;
 26+
 27+const mockUser = { id: "1", email: "test@saga-query.com" };
 28+
 29+const jsonBlob = (data: any) => {
 30+  return JSON.stringify(data);
 31+};
 32+
 33+const setupTest = () => {
 34+  const slice = createAssign({
 35+    name: "user",
 36+    initialState: { id: "", email: "" },
 37+  });
 38+
 39+  const api = createApi();
 40+  api.use(requestMonitor());
 41+  api.use(api.routes());
 42+  api.use(function* (ctx, next) {
 43+    yield* delay(10);
 44+    ctx.json = { ok: true, data: mockUser };
 45+    ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
 46+    yield next();
 47+  });
 48+
 49+  const fetchUser = api.get<{ id: string }>("/user/:id", function* (ctx, next) {
 50+    ctx.cache = true;
 51+    yield next();
 52+    if (!ctx.json.ok) return;
 53+    slice.actions.set(ctx.json.data);
 54+  });
 55+
 56+  const { store, run } = setupStore(
 57+    { user: slice.reducer },
 58+    {
 59+      fx: api.bootup,
 60+    },
 61+  );
 62+  run();
 63+  return { store, fetchUser, api };
 64+};
 65+
 66+describe.ignore("useApi()", () => {
 67+  beforeEach(() => cleanup());
 68+  it("with action", async () => {
 69+    const { fetchUser, store } = setupTest();
 70+    const App = () => {
 71+      const action = fetchUser({ id: "1" });
 72+      const query = useApi(action);
 73+      const user = useSelector((s: any) =>
 74+        selectDataById(s, { id: action.payload.key })
 75+      );
 76+
 77+      return h("div", null, [
 78+        h("div", { key: "1" }, user?.email || ""),
 79+        h(
 80+          "button",
 81+          { key: "2", onClick: () => query.trigger() },
 82+          query.isLoading ? "loading" : "fetch",
 83+        ),
 84+        h("div", { key: "3" }, query.isSuccess ? "success" : ""),
 85+      ]);
 86+    };
 87+    render(h(Provider, { store, children: h(App) }));
 88+
 89+    const button = screen.getByText("fetch");
 90+    fireEvent.click(button);
 91+
 92+    await screen.findByText("loading");
 93+    await screen.findByText(mockUser.email);
 94+    await screen.findByText("success");
 95+    asserts.assert(true);
 96+  });
 97+
 98+  it("with action creator", async () => {
 99+    const { fetchUser, store } = setupTest();
100+    const App = () => {
101+      const query = useApi(fetchUser);
102+      const user = useSelector((s: any) => {
103+        const id = createKey(`${fetchUser}`, { id: "1" });
104+        return selectDataById(s, { id });
105+      });
106+      return h("div", null, [
107+        h("div", { key: "1" }, user?.email || "no user"),
108+        h(
109+          "button",
110+          { key: "2", onClick: () => query.trigger({ id: "1" }) },
111+          query.isLoading ? "loading" : "fetch",
112+        ),
113+        h("div", { key: "3" }, query.isSuccess ? "success" : ""),
114+      ]);
115+    };
116+    render(h(Provider, { store, children: h(App) }));
117+
118+    const button = screen.getByText("fetch");
119+    fireEvent.click(button);
120+
121+    await screen.findByText("loading");
122+    await screen.findByText(mockUser.email);
123+    await screen.findByText("success");
124+    asserts.assert(true);
125+  });
126+});
A query/react.ts
+215, -0
  1@@ -0,0 +1,215 @@
  2+import type { LoadingState } from "../deps.ts";
  3+import { React, useDispatch, useSelector } from "../deps.ts";
  4+const { useState, useEffect } = React;
  5+
  6+import type { QueryState } from "./slice.ts";
  7+import { selectDataById, selectLoaderById } from "./slice.ts";
  8+
  9+type ActionFn<P = any> = (p: P) => { toString: () => string };
 10+type ActionFnSimple = () => { toString: () => string };
 11+
 12+interface SagaAction<P = any> {
 13+  type: string;
 14+  payload: { key: string; options: P };
 15+}
 16+
 17+export interface UseApiProps<P = any> extends LoadingState {
 18+  trigger: (p: P) => void;
 19+  action: ActionFn<P>;
 20+}
 21+export interface UseApiSimpleProps extends LoadingState {
 22+  trigger: () => void;
 23+  action: ActionFn;
 24+}
 25+export interface UseApiAction<A extends SagaAction = SagaAction>
 26+  extends LoadingState {
 27+  trigger: () => void;
 28+  action: A;
 29+}
 30+export type UseApiResult<P, A extends SagaAction = SagaAction> =
 31+  | UseApiProps<P>
 32+  | UseApiSimpleProps
 33+  | UseApiAction<A>;
 34+
 35+interface UseCacheResult<D = any, A extends SagaAction = SagaAction>
 36+  extends UseApiAction<A> {
 37+  data: D | null;
 38+}
 39+
 40+/**
 41+ * useLoader will take an action creator or action itself and return the associated
 42+ * loader for it.
 43+ *
 44+ * @returns the loader object for an action creator or action
 45+ *
 46+ * @example
 47+ * ```ts
 48+ * import { useLoader } from 'saga-query/react';
 49+ *
 50+ * import { api } from './api';
 51+ *
 52+ * const fetchUsers = api.get('/users', function*() {
 53+ *   // ...
 54+ * });
 55+ *
 56+ * const View = () => {
 57+ *   const loader = useLoader(fetchUsers);
 58+ *   // or: const loader = useLoader(fetchUsers());
 59+ *   return <div>{loader.isLoader ? 'Loading ...' : 'Done!'}</div>
 60+ * }
 61+ * ```
 62+ */
 63+export function useLoader<S extends QueryState = QueryState>(
 64+  action: SagaAction | ActionFn,
 65+) {
 66+  const id = typeof action === "function" ? `${action}` : action.payload.key;
 67+  return useSelector((s: S) => selectLoaderById(s, { id }));
 68+}
 69+
 70+/**
 71+ * useApi will take an action creator or action itself and fetch
 72+ * the associated loader and create a `trigger` function that you can call
 73+ * later in your react component.
 74+ *
 75+ * This hook will *not* fetch the data for you because it does not know how to fetch
 76+ * data from your redux state.
 77+ *
 78+ * @example
 79+ * ```ts
 80+ * import { useApi } from 'saga-query/react';
 81+ *
 82+ * import { api } from './api';
 83+ *
 84+ * const fetchUsers = api.get('/users', function*() {
 85+ *   // ...
 86+ * });
 87+ *
 88+ * const View = () => {
 89+ *   const { isLoading, trigger } = useApi(fetchUsers);
 90+ *   useEffect(() => {
 91+ *     trigger();
 92+ *   }, []);
 93+ *   return <div>{isLoading ? : 'Loading' : 'Done!'}</div>
 94+ * }
 95+ * ```
 96+ */
 97+export function useApi<P = any, A extends SagaAction = SagaAction<P>>(
 98+  action: A,
 99+): UseApiAction<A>;
100+export function useApi<P = any, A extends SagaAction = SagaAction<P>>(
101+  action: ActionFn<P>,
102+): UseApiProps<P>;
103+export function useApi<A extends SagaAction = SagaAction>(
104+  action: ActionFnSimple,
105+): UseApiSimpleProps;
106+export function useApi(action: any) {
107+  const dispatch = useDispatch();
108+  const loader = useLoader(action);
109+  const trigger = (p: any) => {
110+    if (typeof action === "function") {
111+      dispatch(action(p));
112+    } else {
113+      dispatch(action);
114+    }
115+  };
116+  return { ...loader, trigger, action };
117+}
118+
119+/**
120+ * useQuery uses {@link useApi} and automatically calls `useApi().trigger()`
121+ *
122+ * @example
123+ * ```ts
124+ * import { useQuery } from 'saga-query/react';
125+ *
126+ * import { api } from './api';
127+ *
128+ * const fetchUsers = api.get('/users', function*() {
129+ *   // ...
130+ * });
131+ *
132+ * const View = () => {
133+ *   const { isLoading } = useQuery(fetchUsers);
134+ *   return <div>{isLoading ? : 'Loading' : 'Done!'}</div>
135+ * }
136+ * ```
137+ */
138+export function useQuery<P = any, A extends SagaAction = SagaAction<P>>(
139+  action: A,
140+): UseApiAction<A> {
141+  const api = useApi(action);
142+  useEffect(() => {
143+    api.trigger();
144+  }, [action.payload.key]);
145+  return api;
146+}
147+
148+/**
149+ * useCache uses {@link useQuery} and automatically selects the cached data associated
150+ * with the action creator or action provided.
151+ *
152+ * @example
153+ * ```ts
154+ * import { useCache } from 'saga-query/react';
155+ *
156+ * import { api } from './api';
157+ *
158+ * const fetchUsers = api.get('/users', api.cache());
159+ *
160+ * const View = () => {
161+ *   const { isLoading, data } = useCache(fetchUsers());
162+ *   return <div>{isLoading ? : 'Loading' : data.length}</div>
163+ * }
164+ * ```
165+ */
166+export function useCache<D = any, A extends SagaAction = SagaAction>(
167+  action: A,
168+): UseCacheResult<D, A> {
169+  const id = action.payload.key;
170+  const data = useSelector((s: any) => selectDataById(s, { id }));
171+  const query = useQuery(action);
172+  return { ...query, data: data || null };
173+}
174+
175+/**
176+ * useLoaderSuccess will activate the callback provided when the loader transitions
177+ * from some state to success.
178+ *
179+ * @example
180+ * ```ts
181+ * import { useLoaderSuccess, useApi } from 'saga-query/react';
182+ *
183+ * import { api } from './api';
184+ *
185+ * const createUser = api.post('/users', function*(ctx, next) {
186+ *   // ...
187+ * });
188+ *
189+ * const View = () => {
190+ *  const { loader, trigger } = useApi(createUser);
191+ *  const onSubmit = () => {
192+ *    trigger({ name: 'bob' });
193+ *  };
194+ *
195+ *  useLoaderSuccess(loader, () => {
196+ *    // success!
197+ *    // Use this callback to navigate to another view
198+ *  });
199+ *
200+ *  return <button onClick={onSubmit}>Create user!</button>
201+ * }
202+ * ```
203+ */
204+export function useLoaderSuccess(
205+  cur: Pick<LoadingState, "isLoading" | "isSuccess">,
206+  success: () => any,
207+) {
208+  const [prev, setPrev] = useState(cur);
209+  useEffect(() => {
210+    const curSuccess = !cur.isLoading && cur.isSuccess;
211+    if (prev.isLoading && curSuccess) {
212+      success();
213+    }
214+    setPrev(cur);
215+  }, [cur.isSuccess, cur.isLoading]);
216+}
A query/saga.ts
+66, -0
 1@@ -0,0 +1,66 @@
 2+import { call, race } from "../fx/index.ts";
 3+import { take } from "../redux/index.ts";
 4+import { Operation, sleep, spawn, Task } from "../deps.ts";
 5+import type { Action, ActionWPayload, OpFn } from "../types.ts";
 6+
 7+import type { CreateActionPayload } from "./types.ts";
 8+
 9+const MS = 1000;
10+const SECONDS = 1 * MS;
11+const MINUTES = 60 * SECONDS;
12+
13+export function poll(parentTimer: number = 5 * 1000, cancelType?: string) {
14+  return function* poller<T>(
15+    actionType: string,
16+    op: (action: Action) => Operation<T>,
17+  ): Operation<T> {
18+    const cancel = cancelType || actionType;
19+    function* fire(action: { type: string }, timer: number) {
20+      while (true) {
21+        yield* call(() => op(action));
22+        yield* sleep(timer);
23+      }
24+    }
25+
26+    while (true) {
27+      const action = yield* take<{ timer?: number }>(actionType);
28+      const timer = action.payload?.timer || parentTimer;
29+      yield* race({
30+        fire: () => call(() => fire(action, timer)),
31+        cancel: () => take(`${cancel}`),
32+      });
33+    }
34+  };
35+}
36+
37+/**
38+ * timer() will create a cache timer for each `key` inside
39+ * of a saga-query api endpoint.  `key` is a hash of the action type and payload.
40+ *
41+ * Why do we want this?  If we have an api endpoint to fetch a single app: `fetchApp({ id: 1 })`
42+ * if we don't set a timer per key then all calls to `fetchApp` will be on a timer.
43+ * So if we call `fetchApp({ id: 1 })` and then `fetchApp({ id: 2 })` if we use a normal
44+ * cache timer then the second call will not send an http request.
45+ */
46+export function timer(timer: number = 5 * MINUTES) {
47+  return function* onTimer(actionType: string, op: (action: Action) => OpFn) {
48+    const map: { [key: string]: Task<unknown> } = {};
49+
50+    function* activate(action: ActionWPayload<CreateActionPayload>) {
51+      yield* call(() => op(action));
52+      yield* sleep(timer);
53+      delete map[action.payload.key];
54+    }
55+
56+    while (true) {
57+      const action = yield* take<CreateActionPayload>(`${actionType}`);
58+      const key = action.payload.key;
59+      if (!map[key]) {
60+        const task = yield* spawn(function* () {
61+          yield* activate(action);
62+        });
63+        map[key] = task;
64+      }
65+    }
66+  };
67+}
A query/slice.ts
+51, -0
 1@@ -0,0 +1,51 @@
 2+import { createLoaderTable, createReducerMap, createTable } from "../deps.ts";
 3+import type { LoadingItemState } from "../deps.ts";
 4+
 5+import { createKey } from "./create-key.ts";
 6+
 7+export { defaultLoader, defaultLoadingItem } from "../deps.ts";
 8+
 9+export interface QueryState {
10+  "@@saga-query/loaders": { [key: string]: LoadingItemState };
11+  "@@saga-query/data": { [key: string]: any };
12+}
13+
14+export const LOADERS_NAME = `@@saga-query/loaders`;
15+export const loaders = createLoaderTable({ name: LOADERS_NAME });
16+export const {
17+  loading: setLoaderStart,
18+  error: setLoaderError,
19+  success: setLoaderSuccess,
20+  resetById: resetLoaderById,
21+} = loaders.actions;
22+export const { selectTable: selectLoaders, selectById: selectLoaderById } =
23+  loaders.getSelectors((state: any) => state[LOADERS_NAME] || {});
24+
25+export const DATA_NAME = `@@saga-query/data`;
26+export const data = createTable<any>({ name: DATA_NAME });
27+export const { add: addData, reset: resetData } = data.actions;
28+
29+export const { selectTable: selectData, selectById: selectDataById } = data
30+  .getSelectors((s: any) => s[DATA_NAME] || {});
31+
32+/**
33+ * Returns data from the saga-query slice of redux from an action.
34+ */
35+export const selectDataByName = (
36+  s: any,
37+  p: { name: string; payload?: any },
38+) => {
39+  const id = createKey(p.name, p.payload);
40+  const data = selectDataById(s, { id });
41+  return data;
42+};
43+
44+export const reducers = createReducerMap(loaders, data);
45+
46+export const createQueryState = (s: Partial<QueryState> = {}): QueryState => {
47+  return {
48+    [LOADERS_NAME]: {},
49+    [DATA_NAME]: {},
50+    ...s,
51+  };
52+};
A query/store.ts
+64, -0
 1@@ -0,0 +1,64 @@
 2+import type { Middleware, Reducer, Result, Task } from "../deps.ts";
 3+import { combineReducers, enableBatching } from "../deps.ts";
 4+import type { OpFn } from "../types.ts";
 5+import { createFxMiddleware } from "../redux/index.ts";
 6+import { keepAlive } from "../index.ts";
 7+
 8+import type { QueryState } from "./slice.ts";
 9+import { reducers as sagaQueryReducers } from "./slice.ts";
10+
11+export interface PrepareStore<
12+  S extends { [key: string]: any } = { [key: string]: any },
13+> {
14+  reducer: Reducer<S & QueryState>;
15+  middleware: Middleware<any, S, any>[];
16+  run: (...args: any[]) => Task<Result<unknown>>;
17+}
18+
19+interface Props<S extends { [key: string]: any } = { [key: string]: any }> {
20+  reducers: { [key in keyof S]: Reducer<S[key]> };
21+  fx: { [key: string]: OpFn };
22+}
23+
24+/**
25+ * This will setup `redux-batched-actions` to work with `redux-saga`.
26+ * It will also add some reducers to your `redux` store for decoupled loaders
27+ * and a simple data cache.
28+ *
29+ * @example
30+ * ```ts
31+ * import { prepareStore } from 'saga-query';
32+ * import { configureStore } from '@reduxjs/toolkit';
33+ *
34+ * const { middleware, reducer, run } = prepareStore({
35+ *  reducers: { users: (state, action) => state },
36+ *  fx: { api: api.saga() },
37+ * });
38+ *
39+ * const store = configureStore({
40+ *  reducer,
41+ *  middleware,
42+ * });
43+ *
44+ * // you must call `.run(...args: any[])` in order for the sagas to bootup.
45+ * run();
46+ * ```
47+ */
48+export function prepareStore<
49+  S extends { [key: string]: unknown } = { [key: string]: unknown },
50+>({ reducers, fx }: Props<S>): PrepareStore<S> {
51+  const middleware: Middleware<unknown, S>[] = [];
52+
53+  const fxMiddleware = createFxMiddleware();
54+  middleware.push(fxMiddleware.middleware);
55+
56+  const reducer = combineReducers({ ...sagaQueryReducers, ...reducers });
57+  const rootReducer = enableBatching(reducer as Reducer);
58+  const run = () => fxMiddleware.run(() => keepAlive(Object.values(fx)));
59+
60+  return {
61+    middleware,
62+    reducer: rootReducer as Reducer,
63+    run,
64+  };
65+}
A query/types.ts
+129, -0
  1@@ -0,0 +1,129 @@
  2+import type {
  3+  LoadingItemState,
  4+  LoadingMapPayload,
  5+  LoadingState,
  6+  Operation,
  7+} from "../deps.ts";
  8+
  9+export type { LoadingItemState, LoadingState };
 10+
 11+type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
 12+
 13+export interface Payload<P = any> {
 14+  payload: P;
 15+}
 16+
 17+export interface PipeCtx<P = any> extends Payload<P> {
 18+  name: string;
 19+  key: string;
 20+  action: ActionWithPayload<CreateActionPayload<P>>;
 21+  actionFn: IfAny<
 22+    P,
 23+    CreateAction<PipeCtx>,
 24+    CreateActionWithPayload<PipeCtx<P>, P>
 25+  >;
 26+}
 27+
 28+export interface LoaderCtx<P = any> extends PipeCtx<P> {
 29+  loader: LoadingMapPayload<Record<string, any>> | null;
 30+}
 31+
 32+export interface ApiFetchSuccess<ApiSuccess = any> {
 33+  ok: true;
 34+  data: ApiSuccess;
 35+}
 36+
 37+export interface ApiFetchError<ApiError = any> {
 38+  ok: false;
 39+  data: ApiError;
 40+}
 41+
 42+export type ApiFetchResponse<ApiSuccess = any, ApiError = any> =
 43+  | ApiFetchSuccess<ApiSuccess>
 44+  | ApiFetchError<ApiError>;
 45+
 46+export type ApiRequest = Partial<{ url: string } & RequestInit>;
 47+export type RequiredApiRequest = {
 48+  url: string;
 49+  headers: HeadersInit;
 50+} & Partial<RequestInit>;
 51+
 52+export interface FetchCtx<P = any> extends PipeCtx<P> {
 53+  request: ApiRequest | null;
 54+  req: (r?: ApiRequest) => RequiredApiRequest;
 55+  response: Response | null;
 56+  bodyType: "arrayBuffer" | "blob" | "formData" | "json" | "text";
 57+}
 58+
 59+export interface FetchJson<ApiSuccess = any, ApiError = any> {
 60+  json: ApiFetchResponse<ApiSuccess, ApiError>;
 61+}
 62+
 63+export interface FetchJsonCtx<P = any, ApiSuccess = any, ApiError = any>
 64+  extends FetchCtx<P>, FetchJson<ApiSuccess, ApiError> {}
 65+
 66+export interface ApiCtx<Payload = any, ApiSuccess = any, ApiError = any>
 67+  extends FetchJsonCtx<Payload, ApiSuccess, ApiError> {
 68+  actions: Action[];
 69+  loader: LoadingMapPayload<Record<string, any>> | null;
 70+  cache: boolean;
 71+  cacheData: any;
 72+}
 73+
 74+export type Middleware<Ctx extends PipeCtx = PipeCtx> = (
 75+  ctx: Ctx,
 76+  next: Next,
 77+) => any;
 78+export type MiddlewareCo<Ctx extends PipeCtx = PipeCtx> =
 79+  | Middleware<Ctx>
 80+  | Middleware<Ctx>[];
 81+
 82+export type MiddlewareApi<Ctx extends ApiCtx = ApiCtx> = (
 83+  ctx: Ctx,
 84+  next: Next,
 85+) => any;
 86+export type MiddlewareApiCo<Ctx extends ApiCtx = ApiCtx> =
 87+  | Middleware<Ctx>
 88+  | Middleware<Ctx>[];
 89+
 90+export type Next = () => any;
 91+
 92+export interface Action {
 93+  type: string;
 94+}
 95+
 96+export interface ActionWithPayload<P> extends Action {
 97+  payload: P;
 98+}
 99+
100+export interface CreateActionPayload<P = any> {
101+  name: string;
102+  key: string;
103+  options: P;
104+}
105+
106+export type CreateActionFn = () => ActionWithPayload<
107+  CreateActionPayload<Record<string | number | symbol, never>>
108+>;
109+
110+export interface CreateAction<Ctx> extends CreateActionFn {
111+  run: (
112+    p: ActionWithPayload<
113+      CreateActionPayload<Record<string | number | symbol, never>>
114+    >,
115+  ) => Operation<Ctx>;
116+}
117+
118+export type CreateActionFnWithPayload<P = any> = (
119+  p: P,
120+) => ActionWithPayload<CreateActionPayload<P>>;
121+
122+export interface CreateActionWithPayload<Ctx, P>
123+  extends CreateActionFnWithPayload<P> {
124+  run: (a: ActionWithPayload<CreateActionPayload<P>>) => Operation<Ctx>;
125+}
126+
127+export type Supervisor<T = unknown> = (
128+  pattern: string,
129+  op: (action: Action) => Operation<T>,
130+) => Operation<T>;
A query/util.test.ts
+16, -0
 1@@ -0,0 +1,16 @@
 2+import { describe, expect, it } from "../test.ts";
 3+
 4+import { createAction } from "./util.ts";
 5+import { API_ACTION_PREFIX } from "./constant.ts";
 6+
 7+const tests = describe("createAction()");
 8+
 9+it(tests, "should return action type when stringified", () => {
10+  const undo = createAction("UNDO");
11+  expect(`${API_ACTION_PREFIX}/UNDO`).toEqual(`${undo}`);
12+});
13+
14+it(tests, "return object with type", () => {
15+  const undo = createAction("UNDO");
16+  expect(undo()).toEqual({ type: `${API_ACTION_PREFIX}/UNDO` });
17+});
A query/util.ts
+70, -0
 1@@ -0,0 +1,70 @@
 2+import { configureStore } from "../deps.ts";
 3+import type { Reducer } from "../deps.ts";
 4+import type { OpFn } from "../types.ts";
 5+
 6+import type { ApiRequest, RequiredApiRequest } from "./types.ts";
 7+import { prepareStore } from "./store.ts";
 8+import { API_ACTION_PREFIX } from "./constant.ts";
 9+
10+export const noop = () => {};
11+// deno-lint-ignore no-explicit-any
12+export const isFn = (fn?: any) => fn && typeof fn === "function";
13+// deno-lint-ignore no-explicit-any
14+export const isObject = (obj?: any) => typeof obj === "object" && obj !== null;
15+export const createAction = (curType: string) => {
16+  if (!curType) throw new Error("createAction requires non-empty string");
17+  const type = `${API_ACTION_PREFIX}/${curType}`;
18+  const action = () => ({ type });
19+  action.toString = () => type;
20+  return action;
21+};
22+
23+export function setupStore(
24+  reducers: { [key: string]: Reducer } = {},
25+  fx: { [key: string]: OpFn },
26+) {
27+  const fxx = typeof fx === "function" ? { fx } : fx;
28+  const prepared = prepareStore({
29+    reducers,
30+    fx: fxx,
31+  });
32+  const store = configureStore({
33+    reducer: prepared.reducer,
34+    middleware: (getDefaultMiddleware) =>
35+      getDefaultMiddleware().concat(...prepared.middleware),
36+  });
37+  return { store, run: prepared.run };
38+}
39+
40+export const mergeHeaders = (
41+  cur?: { [key: string]: string },
42+  next?: { [key: string]: string },
43+): HeadersInit => {
44+  if (!cur && !next) return {};
45+  if (!cur && next) return next;
46+  if (cur && !next) return cur;
47+  return { ...cur, ...next };
48+};
49+
50+export const mergeRequest = (
51+  cur?: ApiRequest | null,
52+  next?: ApiRequest | null,
53+): RequiredApiRequest => {
54+  const defaultReq = { url: "", method: "GET", headers: mergeHeaders() };
55+  if (!cur && !next) return { ...defaultReq, headers: mergeHeaders() };
56+  if (!cur && next) return { ...defaultReq, ...next };
57+  if (cur && !next) return { ...defaultReq, ...cur };
58+  return {
59+    ...defaultReq,
60+    ...cur,
61+    ...next,
62+    headers: mergeHeaders((cur as any).headers, (next as any).headers),
63+  };
64+};
65+
66+export const sleep = (n: number) =>
67+  new Promise<void>((resolve) => {
68+    setTimeout(() => {
69+      resolve();
70+    }, n);
71+  });
M react.ts
+3, -1
 1@@ -3,7 +3,9 @@ const { createContext, createElement: h, useContext } = React;
 2 
 3 import type { Operation, Scope } from "./deps.ts";
 4 import type { Action } from "./types.ts";
 5-import { ActionContext } from "./redux.ts";
 6+import { ActionContext } from "./redux/index.ts";
 7+
 8+export * from "./query/react.ts";
 9 
10 const ScopeContext = createContext<Scope | null>(null);
11 
R redux.ts => redux/index.ts
+47, -16
  1@@ -1,6 +1,6 @@
  2-import type { Channel, Operation, Scope } from "./deps.ts";
  3-import type { Action, OpFn, StoreLike } from "./types.ts";
  4-import type { ActionPattern } from "./matcher.ts";
  5+import { BATCH, Channel, Instruction, Operation, Scope } from "../deps.ts";
  6+import type { Action, ActionWPayload, OpFn, StoreLike } from "../types.ts";
  7+import type { ActionPattern } from "../matcher.ts";
  8 
  9 import {
 10   configureStore,
 11@@ -8,10 +8,10 @@ import {
 12   createContext,
 13   createScope,
 14   spawn,
 15-} from "./deps.ts";
 16-import { contextualize } from "./context.ts";
 17-import { call, cancel, emit, parallel } from "./fx/mod.ts";
 18-import { once } from "./iter.ts";
 19+} from "../deps.ts";
 20+import { contextualize } from "../context.ts";
 21+import { call, cancel, emit, parallel } from "../fx/index.ts";
 22+import { once } from "../iter.ts";
 23 
 24 export const ActionContext = createContext<Channel<Action, void>>(
 25   "redux:action",
 26@@ -25,11 +25,18 @@ export function* select<S, R>(selectorFn: (s: S) => R) {
 27   return selectorFn(store.getState() as S);
 28 }
 29 
 30-export function* take(pattern: ActionPattern) {
 31-  return yield* once({
 32+// https://github.com/microsoft/TypeScript/issues/31751#issuecomment-498526919
 33+export function* take<P = never>(
 34+  pattern: ActionPattern,
 35+): Generator<
 36+  Instruction,
 37+  [P] extends [never] ? Action : ActionWPayload<P>
 38+> {
 39+  const action = yield* once({
 40     channel: ActionContext,
 41     pattern,
 42   });
 43+  return action as any;
 44 }
 45 
 46 export function* takeEvery<T>(
 47@@ -76,16 +83,44 @@ export function* takeLeading<T>(
 48 }
 49 
 50 export function* put(action: Action | Action[]) {
 51+  const store = yield* StoreContext;
 52+  if (Array.isArray(action)) {
 53+    action.map((act) => store.dispatch(act));
 54+  } else {
 55+    store.dispatch(action);
 56+  }
 57   yield* emit({
 58     channel: ActionContext,
 59     action,
 60   });
 61 }
 62 
 63+function* send(action: Action) {
 64+  if (action.type === BATCH) {
 65+    const actions: Action[] = action.payload;
 66+    yield* parallel(
 67+      actions.map(
 68+        (a) =>
 69+          function* () {
 70+            yield* emit({
 71+              channel: ActionContext,
 72+              action: a,
 73+            });
 74+          },
 75+      ),
 76+    );
 77+  } else {
 78+    yield* emit({
 79+      channel: ActionContext,
 80+      action,
 81+    });
 82+  }
 83+}
 84+
 85 export function createFxMiddleware(scope: Scope = createScope()) {
 86   function run<T>(op: OpFn<T>) {
 87     const task = scope.run(function* runner() {
 88-      yield* call(op);
 89+      return yield* call(op);
 90     });
 91 
 92     return task;
 93@@ -99,11 +134,7 @@ export function createFxMiddleware(scope: Scope = createScope()) {
 94     return (next: (a: Action) => T) => (action: Action) => {
 95       const result = next(action); // hit reducers
 96       scope.run(function* () {
 97-        if (Array.isArray(action)) {
 98-          yield* parallel(action.map((a) => () => put(a)));
 99-        } else {
100-          yield* put(action);
101-        }
102+        yield* send(action);
103       });
104       return result;
105     };
106@@ -112,7 +143,7 @@ export function createFxMiddleware(scope: Scope = createScope()) {
107   return { run, scope, middleware };
108 }
109 
110-interface SetupStoreProps<S = any> {
111+interface SetupStoreProps<S = unknown> {
112   reducer: (s: S, _: Action) => S;
113 }
114 
R test/middleware.test.ts => redux/middleware.test.ts
+3, -3
 1@@ -1,9 +1,9 @@
 2 import { describe, expect, it } from "../test.ts";
 3-
 4-import { call } from "../mod.ts";
 5-import { createFxMiddleware, select } from "../redux.ts";
 6+import { call } from "../fx/index.ts";
 7 import { Action } from "../types.ts";
 8 
 9+import { createFxMiddleware, select } from "./index.ts";
10+
11 const tests = describe("createMiddleware()");
12 
13 interface Store<S> {
R test/put.test.ts => redux/put.test.ts
+11, -7
 1@@ -1,7 +1,7 @@
 2-import { describe, expect, it } from "../test.ts";
 3+import { describe, expect, it, setupReduxScope } from "../test.ts";
 4+import { sleep, spawn } from "../deps.ts";
 5 
 6-import { run, sleep, spawn } from "../deps.ts";
 7-import { ActionContext, put, take } from "../redux.ts";
 8+import { ActionContext, put, take } from "./index.ts";
 9 
10 const putTests = describe("put()");
11 
12@@ -27,7 +27,8 @@ it(putTests, "should send actions through channel", async () => {
13     });
14   }
15 
16-  await run(() => genFn("arg"));
17+  const scope = setupReduxScope();
18+  await scope.run(() => genFn("arg"));
19 
20   const expected = ["arg", "2"];
21   expect(actual).toEqual(expected);
22@@ -56,7 +57,8 @@ it(putTests, "should handle nested puts", async () => {
23     yield* spawn(genA);
24   }
25 
26-  await run(root);
27+  const scope = setupReduxScope();
28+  await scope.run(() => root());
29 
30   const expected = ["put b", "put a"];
31   expect(actual).toEqual(expected);
32@@ -73,7 +75,8 @@ it(
33       yield* sleep(0);
34     }
35 
36-    await run(root);
37+    const scope = setupReduxScope();
38+    await scope.run(() => root());
39     expect(true).toBe(true);
40   },
41 );
42@@ -100,7 +103,8 @@ it(
43       yield* tsk;
44     }
45 
46-    await run(root);
47+    const scope = setupReduxScope();
48+    await scope.run(() => root());
49     const expected = ["didn't get missed"];
50     expect(actual).toEqual(expected);
51   },
R test/take-helper.test.ts => redux/take-helper.test.ts
+3, -3
 1@@ -1,9 +1,9 @@
 2 import { describe, expect, it } from "../test.ts";
 3-
 4-import { setupStore, take, takeEvery } from "../redux.ts";
 5-import { cancel } from "../fx/mod.ts";
 6+import { cancel } from "../fx/index.ts";
 7 import type { Action } from "../types.ts";
 8 
 9+import { setupStore, take, takeEvery } from "./index.ts";
10+
11 const testEvery = describe("takeEvery()");
12 
13 it(testEvery, "should work", async () => {
R test/take.test.ts => redux/take.test.ts
+14, -11
 1@@ -1,16 +1,16 @@
 2-import { describe, expect, it } from "../test.ts";
 3-
 4-import { run, sleep, spawn } from "../deps.ts";
 5-import { put, take } from "../redux.ts";
 6+import { describe, expect, it, setupReduxScope } from "../test.ts";
 7+import { sleep, spawn } from "../deps.ts";
 8 import type { Action } from "../types.ts";
 9 
10+import { put, take } from "./index.ts";
11+
12 const takeTests = describe("take()");
13 
14 it(
15   takeTests,
16   "a put should complete before more `take` are added and then consumed automatically",
17   async () => {
18-    const actual: any[] = [];
19+    const actual: Action[] = [];
20 
21     function* channelFn() {
22       yield* sleep(10);
23@@ -25,11 +25,13 @@ it(
24       actual.push(yield* take("action-1"));
25     }
26 
27-    await run(root);
28+    const scope = setupReduxScope();
29+    await scope.run(root);
30+
31     expect(actual).toEqual([
32       { type: "action-1", payload: 1 },
33       { type: "action-1", payload: 2 },
34-    ]); // actual: [ { type: "action-1", payload: 1 }, { type: "action-1", payload: 1 } ]
35+    ]);
36   },
37 );
38 
39@@ -57,7 +59,7 @@ it(takeTests, "take from default channel", async () => {
40     });
41   }
42 
43-  const actual: any[] = [];
44+  const actual: Action[] = [];
45   function* genFn() {
46     yield* spawn(channelFn);
47 
48@@ -79,11 +81,12 @@ it(takeTests, "take from default channel", async () => {
49         ]),
50       ); // take if match any from the mixed array
51     } finally {
52-      actual.push("auto ended");
53+      actual.push({ type: "auto ended" });
54     }
55   }
56 
57-  await run(genFn);
58+  const scope = setupReduxScope();
59+  await scope.run(genFn);
60 
61   const expected = [
62     {
63@@ -110,7 +113,7 @@ it(takeTests, "take from default channel", async () => {
64     {
65       type: "action-3",
66     },
67-    "auto ended",
68+    { type: "auto ended" },
69   ];
70   expect(actual).toEqual(expected);
71 });
M test.ts
+65, -1
 1@@ -1,2 +1,66 @@
 2-export { describe, it } from "https://deno.land/std@0.163.0/testing/bdd.ts";
 3+export { build, emptyDir } from "https://deno.land/x/dnt@0.35.0/mod.ts";
 4+export { assert } from "https://deno.land/std@0.187.0/testing/asserts.ts";
 5+export {
 6+  beforeEach,
 7+  describe,
 8+  it,
 9+} from "https://deno.land/std@0.163.0/testing/bdd.ts";
10+export * as asserts from "https://deno.land/std@0.185.0/testing/asserts.ts";
11 export { expect } from "https://deno.land/x/expect@v0.3.0/mod.ts";
12+export {
13+  install,
14+  mock,
15+  mockedFetch,
16+} from "https://deno.land/x/mock_fetch@0.3.0/mod.ts";
17+
18+import { contextualize } from "./context.ts";
19+import { configureStore, createScope } from "./deps.ts";
20+
21+export function isLikeSelector(selector: unknown) {
22+  return (
23+    selector !== null &&
24+    typeof selector === "object" &&
25+    Reflect.getPrototypeOf(selector) === Object.prototype &&
26+    Reflect.ownKeys(selector).length > 0
27+  );
28+}
29+
30+export const CIRCULAR_SELECTOR = new Error("Encountered a circular selector");
31+
32+export function assertLike(
33+  lhs: Record<any, any>,
34+  selector: Record<any, any>,
35+  circular = new Set(),
36+) {
37+  if (circular.has(selector)) {
38+    throw CIRCULAR_SELECTOR;
39+  }
40+
41+  circular.add(selector);
42+
43+  if (lhs === null || typeof lhs !== "object") {
44+    return lhs;
45+  }
46+
47+  const comparable: Record<any, any> = {};
48+  for (const [key, rhs] of Object.entries(selector)) {
49+    if (isLikeSelector(rhs)) {
50+      comparable[key] = assertLike(Reflect.get(lhs, key), rhs, circular);
51+    } else {
52+      comparable[key] = Reflect.get(lhs, key);
53+    }
54+  }
55+
56+  return comparable;
57+}
58+
59+export function setupReduxScope() {
60+  const scope = createScope();
61+  const store = configureStore({
62+    reducer: () => null,
63+  });
64+  scope.run(function* () {
65+    yield* contextualize("redux:store", store);
66+  });
67+  return scope;
68+}
M types.ts
+10, -2
 1@@ -5,10 +5,18 @@ export interface Computation<T = any> {
 2 }
 3 
 4 export type ActionType = string;
 5-export interface Action<P = any> {
 6+export interface Action {
 7   type: ActionType;
 8-  payload?: P;
 9+  payload?: any;
10+  meta?: any;
11+  error?: boolean;
12 }
13+export interface ActionWPayload<P> {
14+  type: ActionType;
15+  payload: P;
16+}
17+export type AnyAction = Action | ActionWPayload<unknown>;
18+
19 export type OpFn<T = unknown> =
20   | (() => Operation<T>)
21   | (() => PromiseLike<T>)