repos / starfx

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

commit
8f0f0f0
parent
cde4792
author
Eric Bower
date
2023-11-30 20:11:13 +0000 UTC
feat: thunks dynamic mdw api (#26)

26 files changed,  +362, -268
M mod.ts
M npm.ts
M api-type-template.ts
+4, -4
 1@@ -1,4 +1,4 @@
 2-function createSagaQueryApi() {
 3+function createQueryApi() {
 4   const methods = [
 5     "get",
 6     "post",
 7@@ -215,7 +215,7 @@ ${method}<P, ApiSuccess, ApiError = unknown>(
 8 * This is an auto-generated file, do not edit directly!
 9 * Run "yarn template" to generate this file.
10 */
11-import type { SagaApi } from "./thunk.ts";
12+import type { ThunksApi } from "./thunk.ts";
13 import type {
14   ApiCtx,
15   CreateAction,
16@@ -230,7 +230,7 @@ import type { Operation } from "../deps.ts";
17 
18 export type ApiName = string | string[];
19 
20-export interface SagaQueryApi<Ctx extends ApiCtx = ApiCtx> extends SagaApi<Ctx> {
21+export interface QueryApi<Ctx extends ApiCtx = ApiCtx> extends ThunksApi<Ctx> {
22   request: (
23     r: Partial<RequestInit>,
24   ) => (ctx: Ctx, next: Next) => Operation<unknown>;
25@@ -254,4 +254,4 @@ async function createTemplateFile(tmpl: string) {
26   }
27 }
28 
29-createTemplateFile(createSagaQueryApi()).then(console.log).catch(console.error);
30+createTemplateFile(createQueryApi()).then(console.log).catch(console.error);
M deno.lock
+14, -7
 1@@ -2,7 +2,7 @@
 2   "version": "3",
 3   "redirects": {
 4     "https://crux.land/router@0.0.5": "https://crux.land/api/get/2KNRVU.ts",
 5-    "https://esm.sh/v128/@types/react@~18.2/index.d.ts": "https://esm.sh/v128/@types/react@18.2.20/index.d.ts"
 6+    "https://esm.sh/v128/@types/react@~18.2/index.d.ts": "https://esm.sh/v128/@types/react@18.2.38/index.d.ts"
 7   },
 8   "remote": {
 9     "https://crux.land/api/get/2KNRVU.ts": "6a77d55844aba78d01520c5ff0b2f0af7f24cc1716a0de8b3bb6bd918c47b5ba",
10@@ -157,17 +157,24 @@
11     "https://deno.land/x/ts_morph@18.0.0/common/typescript.js": "d5c598b6a2db2202d0428fca5fd79fc9a301a71880831a805d778797d2413c59",
12     "https://deno.land/x/wasmbuild@0.15.0/cache.ts": "89eea5f3ce6035a1164b3e655c95f21300498920575ade23161421f5b01967f4",
13     "https://deno.land/x/wasmbuild@0.15.0/loader.ts": "d98d195a715f823151cbc8baa3f32127337628379a02d9eb2a3c5902dbccfc02",
14-    "https://esm.sh/immer@10.0.2?pin=v122": "7ac87b9c76176de8384a67f8cd93d44f75be1a7496c92707252acb669595c393",
15-    "https://esm.sh/react-redux@8.0.5?pin=v122": "fa98e94dc8803fb84bee9eb08a13f11833f634d381003247207682823887dc51",
16-    "https://esm.sh/react@18.2.0?pin=v122": "8950a34a030620fce8349d6bd3913b3bdb186c5ec7968fa5ba4d054e22d78e6c",
17-    "https://esm.sh/redux-batched-actions@0.5.0?pin=v122": "bb9ba7abde4cbba4352e5d25cf8407795f962e6f7c47b59657ee91834fd6744c",
18-    "https://esm.sh/redux@4.2.1?pin=v122": "94f9684721d9f8f48f86778e03f237c00d771283a8533a1e63000e7539f3a303",
19-    "https://esm.sh/reselect@4.1.8?pin=v122": "486407fec8db8f0c87ba540ff6457dbec3c8ec8fa93a4845383bc8cdb33c6008",
20+    "https://esm.sh/immer@10.0.2?pin=v122": "f5441852ba9150f72eb995fc27d5d62c74634ff9db3443690ba9813bb41bf3a6",
21+    "https://esm.sh/react-redux@8.0.5?pin=v122": "3932bbe7b7af1e47962844c5cb3a258d3084bcc5d16235915b16ddd12d99ac1c",
22+    "https://esm.sh/react@18.2.0?pin=v122": "04ad7dc6d11fa27b24c136686a334ecdd19e972fae627cd98cbdc13cdac238c2",
23+    "https://esm.sh/redux-batched-actions@0.5.0?pin=v122": "b04d108cb890d2b128eee6874bd87ca61c07d4e76b89cfd4259e538c6d71f0ff",
24+    "https://esm.sh/redux@4.2.1?pin=v122": "cb93b33d683299991e95af6663dd5c452ddf3c3b98d77a32827c8a7e8b29e86b",
25+    "https://esm.sh/reselect@4.1.8?pin=v122": "8deb34bf285957ed9897171abab8c3e9c332ffc7fb02b2eba7114bf96afe1596",
26     "https://esm.sh/robodux@15.0.2?pin=v122": "0311191f385c627ec97a15dc147cbad4367e09169eb1b591a6992b5d707f1446",
27     "https://esm.sh/stable/react@18.2.0/denonext/react.mjs": "3c4f23bcfc53b256fcfaf6f834fa9f584c3bb7be667b2682c6cb6ba8ef88f8e6",
28     "https://esm.sh/v122/@babel/runtime@7.22.5/denonext/helpers/esm/extends.js": "3955a0ef35db01cd4efff831a9027924f90fa7d55621cd2b6b8519283e573c21",
29     "https://esm.sh/v122/@babel/runtime@7.22.5/denonext/helpers/esm/objectSpread2.js": "39b25571151291cf2778cd3bb118c5336f7682f878f59fbcf79e2684ae55b194",
30     "https://esm.sh/v122/@babel/runtime@7.22.5/denonext/helpers/esm/objectWithoutPropertiesLoose.js": "fae2539db9a813ad1aab88d10a58d81d9403d242fb3f405e9070fc01c2d8808b",
31+    "https://esm.sh/v122/@babel/runtime@7.23.5/denonext/helpers/esm/defineProperty.js": "13f1ca9874644d87caf4f1c7c6029600aef3b2282e5be76fc72965d0ec10d8a8",
32+    "https://esm.sh/v122/@babel/runtime@7.23.5/denonext/helpers/esm/extends.js": "a255beed3049780b7227a4ab04033d3fcdc0f5b16e2f40d50868db351d6a4713",
33+    "https://esm.sh/v122/@babel/runtime@7.23.5/denonext/helpers/esm/objectSpread2.js": "4efd3940e87a8f83f150e6a2f8a794888b39b93f89c30a070ad071785be0ada3",
34+    "https://esm.sh/v122/@babel/runtime@7.23.5/denonext/helpers/esm/objectWithoutPropertiesLoose.js": "4c002759b2e90c80ec29dcf7e7c2e3fd2c11efee184891ec3589649f4cb88c5f",
35+    "https://esm.sh/v122/@babel/runtime@7.23.5/denonext/helpers/esm/toPrimitive.js": "3cd6dc027623adf0a661e719d4ac66d207625e1aadbd74ecc3fde6520e92b83c",
36+    "https://esm.sh/v122/@babel/runtime@7.23.5/denonext/helpers/esm/toPropertyKey.js": "9dbde1a53dc209af48a2b456a7607fc93487e429e3902ee93a02e8bafc59c125",
37+    "https://esm.sh/v122/@babel/runtime@7.23.5/denonext/helpers/esm/typeof.js": "63e81169470f8b02123455bebcb637867e3acf10c687712b535e9bc700199b28",
38     "https://esm.sh/v122/hoist-non-react-statics@3.3.2/denonext/hoist-non-react-statics.mjs": "41018d0142e45a133637f9a3e4da6b8babc22cab2b3ec05cfb202a727da5a0cb",
39     "https://esm.sh/v122/immer@10.0.2/denonext/immer.mjs": "694ebf85b769db4d026ec4b9655a8caaa6ce51776d857df7cedf3fa4774d5297",
40     "https://esm.sh/v122/immer@9.0.21/denonext/immer.mjs": "3819c7f2cc0f19de974517bd2421b80f800a9bc8bcdb87e3b3aaf022640bd7d6",
M deps.ts
+1, -7
 1@@ -23,8 +23,6 @@ export {
 2   createSignal,
 3   each,
 4   Err,
 5-  expect,
 6-  filter,
 7   getframe,
 8   Ok,
 9   resource,
10@@ -52,6 +50,7 @@ export {
11 } from "https://esm.sh/immer@10.0.2?pin=v122";
12 export type { Patch } from "https://esm.sh/immer@10.0.2?pin=v122";
13 
14+// redux
15 export type {
16   Action,
17   Reducer,
18@@ -68,13 +67,8 @@ export {
19   batchActions,
20   enableBatching,
21 } from "https://esm.sh/redux-batched-actions@0.5.0?pin=v122";
22-export type {
23-  MapEntity,
24-  PatchEntity,
25-} from "https://esm.sh/robodux@15.0.2?pin=v122";
26 export {
27   createLoaderTable,
28   createReducerMap,
29   createTable,
30-  mapReducers,
31 } from "https://esm.sh/robodux@15.0.2?pin=v122";
M fx/call.test.ts
+1, -2
1@@ -1,6 +1,5 @@
2 import { describe, expect, it } from "../test.ts";
3-import { run } from "../deps.ts";
4-import { call } from "./call.ts";
5+import { call, run } from "../deps.ts";
6 
7 const tests = describe("call()");
8 
M fx/call.ts
+0, -2
1@@ -2,8 +2,6 @@ import type { Operation, Result } from "../deps.ts";
2 import { call, Err, Ok } from "../deps.ts";
3 import type { Operator } from "../types.ts";
4 
5-export { call };
6-
7 export function* safe<T>(operator: Operator<T>): Operation<Result<T>> {
8   try {
9     const value: T = yield* call(operator as any) as any;
M fx/race.ts
+1, -2
1@@ -1,7 +1,6 @@
2 import type { Operation, Task } from "../deps.ts";
3-import { action, resource, spawn } from "../deps.ts";
4+import { action, call, resource, spawn } from "../deps.ts";
5 import type { Operator } from "../types.ts";
6-import { call } from "./call.ts";
7 
8 interface OpMap<T = unknown> {
9   [key: string]: Operator<T>;
M mod.ts
+1, -0
1@@ -6,6 +6,7 @@ export * from "./action.ts";
2 export * from "./log.ts";
3 export {
4   action,
5+  call,
6   createChannel,
7   createContext,
8   createQueue,
M npm.ts
+100, -97
  1@@ -1,103 +1,106 @@
  2-import { build, emptyDir } from "./test.ts";
  3-
  4-await emptyDir("./npm");
  5+import { build, emptyDir } from "https://deno.land/x/dnt@0.38.1/mod.ts";
  6 
  7 const [version] = Deno.args;
  8 if (!version) {
  9   throw new Error("a version argument is required to build the npm package");
 10 }
 11 
 12-await build({
 13-  declaration: "inline",
 14-  scriptModule: "cjs",
 15-  entryPoints: [
 16-    {
 17-      name: ".",
 18-      path: "mod.ts",
 19-    },
 20-    {
 21-      name: "./react",
 22-      path: "react.ts",
 23-    },
 24-    {
 25-      name: "./store",
 26-      path: "./store/mod.ts",
 27-    },
 28-    {
 29-      name: "./redux",
 30-      path: "./redux/mod.ts",
 31-    },
 32-  ],
 33-  mappings: {
 34-    "https://deno.land/x/effection@3.0.0-beta.2/mod.ts": {
 35-      name: "effection",
 36-      version: "3.0.0-beta.2",
 37-    },
 38-    "https://esm.sh/react@18.2.0?pin=v122": {
 39-      name: "react",
 40-      version: "^18.2.0",
 41-      peerDependency: true,
 42-    },
 43-    "https://esm.sh/react-redux@8.0.5?pin=v122": {
 44-      name: "react-redux",
 45-      version: "^8.0.5",
 46-    },
 47-    "https://esm.sh/immer@10.0.2?pin=v122": {
 48-      name: "immer",
 49-      version: "^10.0.2",
 50-    },
 51-    "https://esm.sh/reselect@4.1.8?pin=v122": {
 52-      name: "reselect",
 53-      version: "^4.1.8",
 54-    },
 55-    "https://esm.sh/robodux@15.0.2?pin=v122": {
 56-      name: "robodux",
 57-      version: "^15.0.2",
 58-    },
 59-    "https://esm.sh/redux@4.2.1?pin=v122": {
 60-      name: "redux",
 61-      version: "^4.2.1",
 62-    },
 63-    "https://esm.sh/redux-batched-actions@0.5.0?pin=v122": {
 64-      name: "redux-batched-actions",
 65-      version: "^0.5.0",
 66-    },
 67-  },
 68-  outDir: "./npm",
 69-  shims: {
 70-    deno: false,
 71-  },
 72-  test: false,
 73-  typeCheck: "both",
 74-  compilerOptions: {
 75-    target: "ES2020",
 76-    sourceMap: true,
 77-    lib: ["DOM", "DOM.Iterable", "ESNext"],
 78-  },
 79-  package: {
 80-    name: "starfx",
 81-    version,
 82-    description:
 83-      "Async flow control and state management system for deno, node, and browser",
 84-    license: "MIT",
 85-    author: {
 86-      name: "Eric Bower",
 87-      email: "me@erock.io",
 88-    },
 89-    repository: {
 90-      type: "git",
 91-      url: "git+https://github.com/neurosnap/starfx.git",
 92-    },
 93-    bugs: {
 94-      url: "https://github.com/neurosnap/starfx/issues",
 95-    },
 96-    engines: {
 97-      node: ">= 18",
 98-    },
 99-    sideEffects: false,
100-  },
101-  postBuild() {
102-    Deno.copyFileSync("LICENSE.md", "npm/LICENSE.md");
103-    Deno.copyFileSync("README.md", "npm/README.md");
104-  },
105-});
106+init().then(console.log).catch(console.error);
107+
108+async function init() {
109+  await emptyDir("./npm");
110+  await build({
111+    declaration: "inline",
112+    scriptModule: "cjs",
113+    entryPoints: [
114+      {
115+        name: ".",
116+        path: "mod.ts",
117+      },
118+      {
119+        name: "./react",
120+        path: "react.ts",
121+      },
122+      {
123+        name: "./store",
124+        path: "./store/mod.ts",
125+      },
126+      {
127+        name: "./redux",
128+        path: "./redux/mod.ts",
129+      },
130+    ],
131+    mappings: {
132+      "https://deno.land/x/effection@3.0.0-beta.2/mod.ts": {
133+        name: "effection",
134+        version: "3.0.0-beta.2",
135+      },
136+      "https://esm.sh/react@18.2.0?pin=v122": {
137+        name: "react",
138+        version: "^18.2.0",
139+        peerDependency: true,
140+      },
141+      "https://esm.sh/react-redux@8.0.5?pin=v122": {
142+        name: "react-redux",
143+        version: "^8.0.5",
144+      },
145+      "https://esm.sh/immer@10.0.2?pin=v122": {
146+        name: "immer",
147+        version: "^10.0.2",
148+      },
149+      "https://esm.sh/reselect@4.1.8?pin=v122": {
150+        name: "reselect",
151+        version: "^4.1.8",
152+      },
153+      "https://esm.sh/robodux@15.0.2?pin=v122": {
154+        name: "robodux",
155+        version: "^15.0.2",
156+      },
157+      "https://esm.sh/redux@4.2.1?pin=v122": {
158+        name: "redux",
159+        version: "^4.2.1",
160+      },
161+      "https://esm.sh/redux-batched-actions@0.5.0?pin=v122": {
162+        name: "redux-batched-actions",
163+        version: "^0.5.0",
164+      },
165+    },
166+    outDir: "./npm",
167+    shims: {
168+      deno: false,
169+    },
170+    test: false,
171+    typeCheck: "both",
172+    compilerOptions: {
173+      target: "ES2020",
174+      sourceMap: true,
175+      lib: ["DOM", "DOM.Iterable", "ESNext"],
176+    },
177+    package: {
178+      name: "starfx",
179+      version,
180+      description:
181+        "Async flow control and state management system for deno, node, and browser",
182+      license: "MIT",
183+      author: {
184+        name: "Eric Bower",
185+        email: "me@erock.io",
186+      },
187+      repository: {
188+        type: "git",
189+        url: "git+https://github.com/neurosnap/starfx.git",
190+      },
191+      bugs: {
192+        url: "https://github.com/neurosnap/starfx/issues",
193+      },
194+      engines: {
195+        node: ">= 18",
196+      },
197+      sideEffects: false,
198+    },
199+    postBuild() {
200+      Deno.copyFileSync("LICENSE.md", "npm/LICENSE.md");
201+      Deno.copyFileSync("README.md", "npm/README.md");
202+    },
203+  });
204+}
M query/api-types.ts
+2, -3
 1@@ -2,7 +2,7 @@
 2  * This is an auto-generated file, do not edit directly!
 3  * Run "yarn template" to generate this file.
 4  */
 5-import type { SagaApi } from "./thunk.ts";
 6+import type { ThunksApi } from "./thunk.ts";
 7 import type {
 8   ApiCtx,
 9   CreateAction,
10@@ -17,8 +17,7 @@ import type { Operation } from "../deps.ts";
11 
12 export type ApiName = string | string[];
13 
14-export interface SagaQueryApi<Ctx extends ApiCtx = ApiCtx>
15-  extends SagaApi<Ctx> {
16+export interface QueryApi<Ctx extends ApiCtx = ApiCtx> extends ThunksApi<Ctx> {
17   request: (
18     r: Partial<RequestInit>,
19   ) => (ctx: Ctx, next: Next) => Operation<unknown>;
M query/api.test.ts
+2, -1
 1@@ -1,5 +1,5 @@
 2 import { describe, expect, it } from "../test.ts";
 3-import { call, keepAlive } from "../fx/mod.ts";
 4+import { keepAlive } from "../fx/mod.ts";
 5 import {
 6   configureStore,
 7   createSchema,
 8@@ -14,6 +14,7 @@ import * as mdw from "./mdw.ts";
 9 import { createApi } from "./api.ts";
10 import { createKey } from "./create-key.ts";
11 import type { ApiCtx } from "./types.ts";
12+import { call } from "../deps.ts";
13 
14 interface User {
15   id: string;
M query/api.ts
+12, -11
 1@@ -1,11 +1,11 @@
 2 // deno-lint-ignore-file no-explicit-any
 3 import type { ApiCtx, ApiRequest, Next } from "./types.ts";
 4 import { createThunks } from "./thunk.ts";
 5-import type { SagaApi } from "./thunk.ts";
 6-import type { ApiName, SagaQueryApi } from "./api-types.ts";
 7+import type { ThunksApi } from "./thunk.ts";
 8+import type { ApiName, QueryApi } from "./api-types.ts";
 9 
10 /**
11- * Creates a middleware pipeline for HTTP requests.
12+ * Creates a middleware thunksline for HTTP requests.
13  *
14  * @remarks
15  * It uses {@link createThunks} under the hood.
16@@ -27,11 +27,11 @@ import type { ApiName, SagaQueryApi } from "./api-types.ts";
17  * ```
18  */
19 export function createApi<Ctx extends ApiCtx = ApiCtx>(
20-  baseThunk?: SagaApi<Ctx>,
21-): SagaQueryApi<Ctx> {
22-  const pipe = baseThunk || createThunks<Ctx>();
23+  baseThunk?: ThunksApi<Ctx>,
24+): QueryApi<Ctx> {
25+  const thunks = baseThunk || createThunks<Ctx>();
26   const uri = (prename: ApiName) => {
27-    const create = pipe.create as any;
28+    const create = thunks.create as any;
29 
30     let name = prename;
31     let remainder = "";
32@@ -63,10 +63,11 @@ export function createApi<Ctx extends ApiCtx = ApiCtx>(
33   };
34 
35   return {
36-    use: pipe.use,
37-    bootup: pipe.bootup,
38-    create: pipe.create,
39-    routes: pipe.routes,
40+    use: thunks.use,
41+    bootup: thunks.bootup,
42+    create: thunks.create,
43+    routes: thunks.routes,
44+    reset: thunks.reset,
45     cache: () => {
46       return function* onCache(ctx: Ctx, next: Next) {
47         ctx.cache = true;
M query/fetch.test.ts
+69, -8
  1@@ -82,7 +82,7 @@ it(
  2 
  3 it(
  4   tests,
  5-  "fetch - should be able to fetch a resource and parse as text instead of json",
  6+  "should be able to fetch a resource and parse as text instead of json",
  7   async () => {
  8     mock(`GET@/users`, () => {
  9       return new Response("this is some text");
 10@@ -118,7 +118,7 @@ it(
 11   },
 12 );
 13 
 14-it(tests, "fetch - error handling", async () => {
 15+it(tests, "error handling", async () => {
 16   const errMsg = { message: "something happened" };
 17   mock(`GET@/users`, () => {
 18     return new Response(JSON.stringify(errMsg), { status: 500 });
 19@@ -155,7 +155,7 @@ it(tests, "fetch - error handling", async () => {
 20   expect(actual).toEqual({ ok: false, data: errMsg, error: errMsg });
 21 });
 22 
 23-it(tests, "fetch - status 204", async () => {
 24+it(tests, "status 204", async () => {
 25   mock(`GET@/users`, () => {
 26     return new Response(null, { status: 204 });
 27   });
 28@@ -195,7 +195,7 @@ it(tests, "fetch - status 204", async () => {
 29   expect(actual).toEqual({ ok: true, data: {}, value: {} });
 30 });
 31 
 32-it(tests, "fetch - malformed json", async () => {
 33+it(tests, "malformed json", async () => {
 34   mock(`GET@/users`, () => {
 35     return new Response("not json", { status: 200 });
 36   });
 37@@ -240,7 +240,7 @@ it(tests, "fetch - malformed json", async () => {
 38   });
 39 });
 40 
 41-it(tests, "fetch - POST", async () => {
 42+it(tests, "POST", async () => {
 43   mock(`POST@/users`, () => {
 44     return new Response(JSON.stringify(mockUser));
 45   });
 46@@ -283,7 +283,7 @@ it(tests, "fetch - POST", async () => {
 47   await delay();
 48 });
 49 
 50-it(tests, "fetch - POST multiple endpoints with same uri", async () => {
 51+it(tests, "POST multiple endpoints with same uri", async () => {
 52   mock(`POST@/users/1/something`, () => {
 53     return new Response(JSON.stringify(mockUser));
 54   });
 55@@ -348,7 +348,7 @@ it(tests, "fetch - POST multiple endpoints with same uri", async () => {
 56 
 57 it(
 58   tests,
 59-  "fetch - slug in url but payload has empty string for slug value",
 60+  "slug in url but payload has empty string for slug value",
 61   async () => {
 62     const { store, schema } = testStore();
 63     const api = createApi();
 64@@ -386,7 +386,7 @@ it(
 65 
 66 it(
 67   tests,
 68-  "fetch retry - with success - should keep retrying fetch request",
 69+  "with success - should keep retrying fetch request",
 70   async () => {
 71     let counter = 0;
 72     mock(`GET@/users`, () => {
 73@@ -470,3 +470,64 @@ it(
 74     expect(actual).toEqual({ ok: false, data, error: data });
 75   },
 76 );
 77+
 78+it(
 79+  tests,
 80+  "should *not* make http request and instead simply mock response",
 81+  async () => {
 82+    const { schema, store } = testStore();
 83+    let actual = null;
 84+    const api = createApi();
 85+    api.use(mdw.api());
 86+    api.use(storeMdw(schema.db));
 87+    api.use(api.routes());
 88+    api.use(mdw.fetch({ baseUrl }));
 89+
 90+    const fetchUsers = api.get("/users", { supervisor: takeEvery }, [
 91+      function* (ctx, next) {
 92+        yield* next();
 93+        actual = ctx.json;
 94+      },
 95+      mdw.response(new Response(JSON.stringify(mockUser))),
 96+    ]);
 97+
 98+    store.run(api.bootup);
 99+    store.dispatch(fetchUsers());
100+
101+    await delay();
102+    expect(actual).toEqual({ ok: true, data: mockUser, value: mockUser });
103+  },
104+);
105+
106+it(tests, "should use dynamic mdw to mock response", async () => {
107+  const { schema, store } = testStore();
108+  let actual = null;
109+  const api = createApi();
110+  api.use(mdw.api());
111+  api.use(storeMdw(schema.db));
112+  api.use(api.routes());
113+  api.use(mdw.fetch({ baseUrl }));
114+
115+  const fetchUsers = api.get("/users", { supervisor: takeEvery }, [
116+    function* (ctx, next) {
117+      yield* next();
118+      actual = ctx.json;
119+    },
120+    mdw.response(new Response(JSON.stringify(mockUser))),
121+  ]);
122+
123+  store.run(api.bootup);
124+
125+  // override default response with dynamic mdw
126+  const dynamicUser = { id: "2", email: "dynamic@starfx.com" };
127+  fetchUsers.use(mdw.response(new Response(JSON.stringify(dynamicUser))));
128+  store.dispatch(fetchUsers());
129+  await delay();
130+  expect(actual).toEqual({ ok: true, data: dynamicUser, value: dynamicUser });
131+
132+  // reset dynamic mdw and try again
133+  api.reset();
134+  store.dispatch(fetchUsers());
135+  await delay();
136+  expect(actual).toEqual({ ok: true, data: mockUser, value: mockUser });
137+});
M query/fetch.ts
+96, -1
  1@@ -1,6 +1,7 @@
  2+import { sleep } from "../deps.ts";
  3 import { safe } from "../fx/mod.ts";
  4 import type { FetchCtx, FetchJsonCtx, Next } from "./types.ts";
  5-import { isObject } from "./util.ts";
  6+import { isObject, noop } from "./util.ts";
  7 
  8 /**
  9  * This middleware converts the name provided to {@link createApi}
 10@@ -198,6 +199,20 @@ export function* payload<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
 11   yield* next();
 12 }
 13 
 14+export function response<CurCtx extends FetchCtx = FetchCtx>(
 15+  response?: Response,
 16+) {
 17+  return function* responseMdw(
 18+    ctx: CurCtx,
 19+    next: Next,
 20+  ) {
 21+    if (response) {
 22+      ctx.response = response;
 23+    }
 24+    yield* next();
 25+  };
 26+}
 27+
 28 /*
 29  * This middleware makes the `fetch` http request using `ctx.request` and
 30  * assigns the response to `ctx.response`.
 31@@ -206,6 +221,13 @@ export function* request<CurCtx extends FetchCtx = FetchCtx>(
 32   ctx: CurCtx,
 33   next: Next,
 34 ) {
 35+  // if there is already a response then we want to bail so we don't
 36+  // override it.
 37+  if (ctx.response) {
 38+    yield* next();
 39+    return;
 40+  }
 41+
 42   const { url, ...req } = ctx.req();
 43   const request = new Request(url, req);
 44   const result = yield* safe(fetch(request));
 45@@ -216,3 +238,76 @@ export function* request<CurCtx extends FetchCtx = FetchCtx>(
 46   }
 47   yield* next();
 48 }
 49+
 50+function backoffExp(attempt: number): number {
 51+  if (attempt > 5) return -1;
 52+  // 1s, 1s, 1s, 2s, 4s
 53+  return Math.max(2 ** attempt * 125, 1000);
 54+}
 55+
 56+/**
 57+ * This middleware will retry failed `Fetch` request if `response.ok` is `false`.
 58+ * It accepts a backoff function to determine how long to continue retrying.
 59+ * The default is an exponential backoff {@link backoffExp} where the minimum is
 60+ * 1sec between attempts and it'll reach 4s between attempts at the end with a
 61+ * max of 5 attempts.
 62+ *
 63+ * An example backoff:
 64+ * @example
 65+ * ```ts
 66+ *  // Any value less than 0 will stop the retry middleware.
 67+ *  // Each attempt will wait 1s
 68+ *  const backoff = (attempt: number) => {
 69+ *    if (attempt > 5) return -1;
 70+ *    return 1000;
 71+ *  }
 72+ *
 73+ * const api = createApi();
 74+ * api.use(mdw.api());
 75+ * api.use(api.routes());
 76+ * api.use(mdw.fetch());
 77+ *
 78+ * const fetchUsers = api.get('/users', [
 79+ *  function*(ctx, next) {
 80+ *    // ...
 81+ *    yield next();
 82+ *  },
 83+ *  // fetchRetry should be after your endpoint function because
 84+ *  // the retry middleware will update `ctx.json` before it reaches
 85+ *  // your middleware
 86+ *  fetchRetry(backoff),
 87+ * ])
 88+ * ```
 89+ */
 90+export function fetchRetry<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
 91+  backoff: (attempt: number) => number = backoffExp,
 92+) {
 93+  return function* (ctx: CurCtx, next: Next) {
 94+    yield* next();
 95+
 96+    if (!ctx.response) {
 97+      return;
 98+    }
 99+
100+    if (ctx.response.ok) {
101+      return;
102+    }
103+
104+    let attempt = 1;
105+    let waitFor = backoff(attempt);
106+    while (waitFor >= 1) {
107+      yield* sleep(waitFor);
108+      // reset response so `request` mdw actually runs
109+      ctx.response = null;
110+      yield* safe(() => request(ctx, noop));
111+      yield* safe(() => json(ctx, noop));
112+
113+      if (ctx.response && (ctx.response as Response).ok) {
114+        return;
115+      }
116+
117+      attempt += 1;
118+      waitFor = backoff(attempt);
119+    }
120+  };
121+}
M query/mdw.test.ts
+2, -2
 1@@ -1,6 +1,6 @@
 2 import { assertLike, asserts, describe, expect, it } from "../test.ts";
 3 import { createApi, createKey, mdw } from "../query/mod.ts";
 4-import type { ApiCtx, Next, PipeCtx } from "../query/mod.ts";
 5+import type { ApiCtx, Next, ThunkCtx } from "../query/mod.ts";
 6 import { createQueryState } from "../action.ts";
 7 import { sleep } from "../test.ts";
 8 
 9@@ -493,7 +493,7 @@ it(tests, "createApi with custom key but no payload", async () => {
10 it(tests, "errorHandler", () => {
11   let a = 0;
12   const query = createApi<ApiCtx>();
13-  query.use(function* errorHandler<Ctx extends PipeCtx = PipeCtx>(
14+  query.use(function* errorHandler<Ctx extends ThunkCtx = ThunkCtx>(
15     ctx: Ctx,
16     next: Next,
17   ) {
M query/mdw.ts
+3, -76
  1@@ -6,12 +6,10 @@ import type {
  2   FetchJsonCtx,
  3   Next,
  4   PerfCtx,
  5-  PipeCtx,
  6   RequiredApiRequest,
  7+  ThunkCtx,
  8 } from "./types.ts";
  9 import { mergeRequest } from "./util.ts";
 10-import { sleep } from "../deps.ts";
 11-import { noop } from "./util.ts";
 12 import * as fetchMdw from "./fetch.ts";
 13 import { log } from "../log.ts";
 14 export * from "./fetch.ts";
 15@@ -27,7 +25,7 @@ export * from "./fetch.ts";
 16  * middleware pipeline succeeded or not. Think the `.catch()` case for
 17  * `window.fetch`.
 18  */
 19-export function* err<Ctx extends PipeCtx = PipeCtx>(
 20+export function* err<Ctx extends ThunkCtx = ThunkCtx>(
 21   ctx: Ctx,
 22   next: Next,
 23 ) {
 24@@ -60,7 +58,7 @@ export function* err<Ctx extends PipeCtx = PipeCtx>(
 25  * })
 26  * ```
 27  */
 28-export function* customKey<Ctx extends PipeCtx = PipeCtx>(
 29+export function* customKey<Ctx extends ThunkCtx = ThunkCtx>(
 30   ctx: Ctx,
 31   next: Next,
 32 ) {
 33@@ -122,77 +120,6 @@ export function* perf<Ctx extends PerfCtx = PerfCtx>(
 34   ctx.performance = t1 - t0;
 35 }
 36 
 37-function backoffExp(attempt: number): number {
 38-  if (attempt > 5) return -1;
 39-  // 1s, 1s, 1s, 2s, 4s
 40-  return Math.max(2 ** attempt * 125, 1000);
 41-}
 42-
 43-/**
 44- * This middleware will retry failed `Fetch` request if `response.ok` is `false`.
 45- * It accepts a backoff function to determine how long to continue retrying.
 46- * The default is an exponential backoff {@link backoffExp} where the minimum is
 47- * 1sec between attempts and it'll reach 4s between attempts at the end with a
 48- * max of 5 attempts.
 49- *
 50- * An example backoff:
 51- * @example
 52- * ```ts
 53- *  // Any value less than 0 will stop the retry middleware.
 54- *  // Each attempt will wait 1s
 55- *  const backoff = (attempt: number) => {
 56- *    if (attempt > 5) return -1;
 57- *    return 1000;
 58- *  }
 59- *
 60- * const api = createApi();
 61- * api.use(mdw.api());
 62- * api.use(api.routes());
 63- * api.use(mdw.fetch());
 64- *
 65- * const fetchUsers = api.get('/users', [
 66- *  function*(ctx, next) {
 67- *    // ...
 68- *    yield next();
 69- *  },
 70- *  // fetchRetry should be after your endpoint function because
 71- *  // the retry middleware will update `ctx.json` before it reaches
 72- *  // your middleware
 73- *  fetchRetry(backoff),
 74- * ])
 75- * ```
 76- */
 77-export function fetchRetry<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
 78-  backoff: (attempt: number) => number = backoffExp,
 79-) {
 80-  return function* (ctx: CurCtx, next: Next) {
 81-    yield* next();
 82-
 83-    if (!ctx.response) {
 84-      return;
 85-    }
 86-
 87-    if (ctx.response.ok) {
 88-      return;
 89-    }
 90-
 91-    let attempt = 1;
 92-    let waitFor = backoff(attempt);
 93-    while (waitFor >= 1) {
 94-      yield* sleep(waitFor);
 95-      yield* safe(() => fetchMdw.request(ctx, noop));
 96-      yield* safe(() => fetchMdw.json(ctx, noop));
 97-
 98-      if (ctx.response.ok) {
 99-        return;
100-      }
101-
102-      attempt += 1;
103-      waitFor = backoff(attempt);
104-    }
105-  };
106-}
107-
108 /**
109  * This middleware is a composition of other middleware required to
110  * use `window.fetch` {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API}
M query/react.ts
+10, -10
 1@@ -8,7 +8,7 @@ import { selectDataById, selectLoaderById } from "../redux/mod.ts";
 2 type ActionFn<P = any> = (p: P) => { toString: () => string };
 3 type ActionFnSimple = () => { toString: () => string };
 4 
 5-interface SagaAction<P = any> {
 6+interface ThunkAction<P = any> {
 7   type: string;
 8   payload: { key: string; options: P };
 9 }
10@@ -21,17 +21,17 @@ export interface UseApiSimpleProps extends LoaderState {
11   trigger: () => void;
12   action: ActionFn;
13 }
14-export interface UseApiAction<A extends SagaAction = SagaAction>
15+export interface UseApiAction<A extends ThunkAction = ThunkAction>
16   extends LoaderState {
17   trigger: () => void;
18   action: A;
19 }
20-export type UseApiResult<P, A extends SagaAction = SagaAction> =
21+export type UseApiResult<P, A extends ThunkAction = ThunkAction> =
22   | UseApiProps<P>
23   | UseApiSimpleProps
24   | UseApiAction<A>;
25 
26-interface UseCacheResult<D = any, A extends SagaAction = SagaAction>
27+interface UseCacheResult<D = any, A extends ThunkAction = ThunkAction>
28   extends UseApiAction<A> {
29   data: D | null;
30 }
31@@ -60,7 +60,7 @@ interface UseCacheResult<D = any, A extends SagaAction = SagaAction>
32  * ```
33  */
34 export function useLoader<S extends QueryState = QueryState>(
35-  action: SagaAction | ActionFn,
36+  action: ThunkAction | ActionFn,
37 ) {
38   const id = typeof action === "function" ? `${action}` : action.payload.key;
39   return useSelector((s: S) => selectLoaderById(s, { id }));
40@@ -93,13 +93,13 @@ export function useLoader<S extends QueryState = QueryState>(
41  * }
42  * ```
43  */
44-export function useApi<P = any, A extends SagaAction = SagaAction<P>>(
45+export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
46   action: A,
47 ): UseApiAction<A>;
48-export function useApi<P = any, A extends SagaAction = SagaAction<P>>(
49+export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
50   action: ActionFn<P>,
51 ): UseApiProps<P>;
52-export function useApi<A extends SagaAction = SagaAction>(
53+export function useApi<A extends ThunkAction = ThunkAction>(
54   action: ActionFnSimple,
55 ): UseApiSimpleProps;
56 export function useApi(action: any): any {
57@@ -134,7 +134,7 @@ export function useApi(action: any): any {
58  * }
59  * ```
60  */
61-export function useQuery<P = any, A extends SagaAction = SagaAction<P>>(
62+export function useQuery<P = any, A extends ThunkAction = ThunkAction<P>>(
63   action: A,
64 ): UseApiAction<A> {
65   const api = useApi(action);
66@@ -162,7 +162,7 @@ export function useQuery<P = any, A extends SagaAction = SagaAction<P>>(
67  * }
68  * ```
69  */
70-export function useCache<D = any, A extends SagaAction = SagaAction>(
71+export function useCache<D = any, A extends ThunkAction = ThunkAction>(
72   action: A,
73 ): UseCacheResult<D, A> {
74   const id = action.payload.key;
M query/thunk.test.ts
+4, -5
 1@@ -1,16 +1,15 @@
 2 import { assertLike, asserts, describe, it } from "../test.ts";
 3-import { call } from "../fx/mod.ts";
 4 import { configureStore, put, takeEvery } from "../store/mod.ts";
 5-import { sleep as delay } from "../deps.ts";
 6+import { call, sleep as delay } from "../deps.ts";
 7 import type { QueryState } from "../types.ts";
 8 import { createQueryState } from "../action.ts";
 9 import { sleep } from "../test.ts";
10 import { createThunks } from "./thunk.ts";
11-import type { Next, PipeCtx } from "./types.ts";
12+import type { Next, ThunkCtx } from "./types.ts";
13 import { updateStore } from "../store/fx.ts";
14 
15 // deno-lint-ignore no-explicit-any
16-interface RoboCtx<D = Record<string, unknown>, P = any> extends PipeCtx<P> {
17+interface RoboCtx<D = Record<string, unknown>, P = any> extends ThunkCtx<P> {
18   url: string;
19   request: { method: string; body?: Record<string, unknown> };
20   response: D;
21@@ -409,7 +408,7 @@ it(tests, "retry with actionFn with payload", async () => {
22   const api = createThunks();
23   api.use(api.routes());
24 
25-  api.use(function* (ctx: PipeCtx<{ page: number }>, next) {
26+  api.use(function* (ctx: ThunkCtx<{ page: number }>, next) {
27     yield* next();
28     if (ctx.payload.page == 1) {
29       yield* put(ctx.actionFn({ page: 2 }));
M query/thunk.ts
+20, -5
 1@@ -15,16 +15,17 @@ import type {
 2   Middleware,
 3   MiddlewareCo,
 4   Next,
 5-  PipeCtx,
 6   Supervisor,
 7+  ThunkCtx,
 8 } from "./types.ts";
 9 import { API_ACTION_PREFIX } from "../action.ts";
10 import { Ok } from "../deps.ts";
11 
12-export interface SagaApi<Ctx extends PipeCtx> {
13+export interface ThunksApi<Ctx extends ThunkCtx> {
14   use: (fn: Middleware<Ctx>) => void;
15   routes: () => Middleware<Ctx>;
16   bootup: Operator<void>;
17+  reset: () => void;
18 
19   /**
20    * Name only
21@@ -119,16 +120,17 @@ export interface SagaApi<Ctx extends PipeCtx> {
22  * // end
23  * ```
24  */
25-export function createThunks<Ctx extends PipeCtx = PipeCtx<any>>(
26+export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
27   {
28     supervisor = takeEvery,
29   }: {
30     supervisor?: Supervisor;
31   } = { supervisor: takeEvery },
32-): SagaApi<Ctx> {
33+): ThunksApi<Ctx> {
34   const middleware: Middleware<Ctx>[] = [];
35   const visors: { [key: string]: Operator<unknown> } = {};
36   const middlewareMap: { [key: string]: Middleware<Ctx> } = {};
37+  let dynamicMiddlewareMap: { [key: string]: Middleware<Ctx> } = {};
38   const actionMap: {
39     [key: string]: CreateActionWithPayload<Ctx, any>;
40   } = {};
41@@ -208,6 +210,14 @@ export function createThunks<Ctx extends PipeCtx = PipeCtx<any>>(
42       return action({ name, key, options });
43     };
44     actionFn.run = onApi;
45+    actionFn.use = (fn: Middleware<Ctx>) => {
46+      const cur = middlewareMap[name];
47+      if (cur) {
48+        dynamicMiddlewareMap[name] = compose([cur, fn]);
49+      } else {
50+        dynamicMiddlewareMap[name] = fn;
51+      }
52+    };
53     actionFn.toString = () => name;
54     actionMap[name] = actionFn;
55 
56@@ -220,7 +230,7 @@ export function createThunks<Ctx extends PipeCtx = PipeCtx<any>>(
57 
58   function routes() {
59     function* router(ctx: Ctx, next: Next) {
60-      const match = middlewareMap[ctx.name];
61+      const match = dynamicMiddlewareMap[ctx.name] || middlewareMap[ctx.name];
62       if (!match) {
63         yield* next();
64         return;
65@@ -233,6 +243,10 @@ export function createThunks<Ctx extends PipeCtx = PipeCtx<any>>(
66     return router;
67   }
68 
69+  function resetMdw() {
70+    dynamicMiddlewareMap = {};
71+  }
72+
73   return {
74     use: (fn: Middleware<Ctx>) => {
75       middleware.push(fn);
76@@ -240,5 +254,6 @@ export function createThunks<Ctx extends PipeCtx = PipeCtx<any>>(
77     create,
78     routes,
79     bootup,
80+    reset: resetMdw,
81   };
82 }
M query/types.ts
+11, -9
 1@@ -3,19 +3,19 @@ import type { LoaderItemState, LoaderPayload, Payload } from "../types.ts";
 2 
 3 type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
 4 
 5-export interface PipeCtx<P = any> extends Payload<P> {
 6+export interface ThunkCtx<P = any> extends Payload<P> {
 7   name: string;
 8   key: string;
 9   action: ActionWithPayload<CreateActionPayload<P>>;
10   actionFn: IfAny<
11     P,
12-    CreateAction<PipeCtx>,
13-    CreateActionWithPayload<PipeCtx<P>, P>
14+    CreateAction<ThunkCtx>,
15+    CreateActionWithPayload<ThunkCtx<P>, P>
16   >;
17   result: Result<void>;
18 }
19 
20-export interface LoaderCtx<P = unknown> extends PipeCtx<P> {
21+export interface LoaderCtx<P = unknown> extends ThunkCtx<P> {
22   loader: Partial<LoaderItemState> | null;
23 }
24 
25@@ -43,7 +43,7 @@ export type RequiredApiRequest = {
26   headers: HeadersInit;
27 } & Partial<RequestInit>;
28 
29-export interface FetchCtx<P = any> extends PipeCtx<P> {
30+export interface FetchCtx<P = any> extends ThunkCtx<P> {
31   request: ApiRequest | null;
32   req: (r?: ApiRequest) => RequiredApiRequest;
33   response: Response | null;
34@@ -65,15 +65,15 @@ export interface ApiCtx<Payload = any, ApiSuccess = any, ApiError = any>
35   cacheData: any;
36 }
37 
38-export interface PerfCtx<P = unknown> extends PipeCtx<P> {
39+export interface PerfCtx<P = unknown> extends ThunkCtx<P> {
40   performance: number;
41 }
42 
43-export type Middleware<Ctx extends PipeCtx = PipeCtx> = (
44+export type Middleware<Ctx extends ThunkCtx = ThunkCtx> = (
45   ctx: Ctx,
46   next: Next,
47 ) => Operation<any>;
48-export type MiddlewareCo<Ctx extends PipeCtx = PipeCtx> =
49+export type MiddlewareCo<Ctx extends ThunkCtx = ThunkCtx> =
50   | Middleware<Ctx>
51   | Middleware<Ctx>[];
52 
53@@ -105,12 +105,14 @@ export type CreateActionFn = () => ActionWithPayload<
54   CreateActionPayload<Record<string | number | symbol, never>>
55 >;
56 
57-export interface CreateAction<Ctx> extends CreateActionFn {
58+export interface CreateAction<Ctx extends ThunkCtx = ThunkCtx>
59+  extends CreateActionFn {
60   run: (
61     p: ActionWithPayload<
62       CreateActionPayload<Record<string | number | symbol, never>>
63     >,
64   ) => Operation<Ctx>;
65+  use: (mdw: Middleware<Ctx>) => void;
66 }
67 
68 export type CreateActionFnWithPayload<P = any> = (
M redux/fx.ts
+1, -1
 1@@ -1,12 +1,12 @@
 2 import type { Action, Operation, Queue, Signal, Stream } from "../deps.ts";
 3 import {
 4+  call,
 5   createContext,
 6   createQueue,
 7   each,
 8   SignalQueueFactory,
 9   spawn,
10 } from "../deps.ts";
11-import { call } from "../fx/mod.ts";
12 import { ActionPattern, matcher } from "../matcher.ts";
13 import type { ActionWPayload, AnyAction } from "../types.ts";
14 import type { StoreLike } from "./types.ts";
M redux/middleware.test.ts
+1, -2
1@@ -1,6 +1,5 @@
2 import { describe, expect, it } from "../test.ts";
3-import { call } from "../fx/mod.ts";
4-import { Action, sleep } from "../deps.ts";
5+import { Action, call, sleep } from "../deps.ts";
6 
7 import { createFxMiddleware, select } from "./mod.ts";
8 
M redux/supervisor.ts
+2, -2
1@@ -1,6 +1,6 @@
2-import { call, race } from "../fx/mod.ts";
3+import { race } from "../fx/mod.ts";
4 import { take } from "./fx.ts";
5-import { Operation, sleep, spawn, Task } from "../deps.ts";
6+import { call, Operation, sleep, spawn, Task } from "../deps.ts";
7 import type { ActionWPayload, AnyAction } from "../types.ts";
8 import type { CreateActionPayload, Supervisor } from "../query/mod.ts";
9 
M store/configureStore.test.ts
+1, -2
 1@@ -1,8 +1,7 @@
 2 import { describe, expect, it } from "../test.ts";
 3-import { call } from "../fx/mod.ts";
 4-
 5 import { select } from "./mod.ts";
 6 import { configureStore } from "./store.ts";
 7+import { call } from "../deps.ts";
 8 
 9 const tests = describe("configureStore()");
10 
M store/fx.ts
+1, -1
 1@@ -1,5 +1,6 @@
 2 import {
 3   Action,
 4+  call,
 5   each,
 6   Operation,
 7   Signal,
 8@@ -7,7 +8,6 @@ import {
 9   spawn,
10   Stream,
11 } from "../deps.ts";
12-import { call } from "../fx/mod.ts";
13 import { ActionPattern, matcher } from "../matcher.ts";
14 import type { ActionWPayload, AnyAction, AnyState } from "../types.ts";
15 import type { FxStore, StoreUpdater, UpdaterCtx } from "./types.ts";
M store/supervisor.ts
+2, -2
1@@ -1,6 +1,6 @@
2-import { call, race } from "../fx/mod.ts";
3+import { race } from "../fx/mod.ts";
4 import { take } from "./fx.ts";
5-import { Operation, sleep, spawn, Task } from "../deps.ts";
6+import { call, Operation, sleep, spawn, Task } from "../deps.ts";
7 import type { ActionWPayload, AnyAction, Operator } from "../types.ts";
8 import type { CreateActionPayload } from "../query/mod.ts";
9 
M test.ts
+1, -6
 1@@ -1,4 +1,3 @@
 2-export { build, emptyDir } from "https://deno.land/x/dnt@0.38.1/mod.ts";
 3 export { assert } from "https://deno.land/std@0.187.0/testing/asserts.ts";
 4 export {
 5   beforeEach,
 6@@ -7,11 +6,7 @@ export {
 7 } from "https://deno.land/std@0.163.0/testing/bdd.ts";
 8 export * as asserts from "https://deno.land/std@0.185.0/testing/asserts.ts";
 9 export { expect } from "https://deno.land/x/expect@v0.3.0/mod.ts";
10-export {
11-  install,
12-  mock,
13-  mockedFetch,
14-} from "https://deno.land/x/mock_fetch@0.3.0/mod.ts";
15+export { install, mock } from "https://deno.land/x/mock_fetch@0.3.0/mod.ts";
16 
17 export const sleep = (n: number) =>
18   new Promise<void>((resolve) => {