- commit
- 8f0f0f0
- parent
- cde4792
- author
- Eric Bower
- date
- 2023-11-30 20:11:13 +0000 UTC
feat: thunks dynamic mdw api (#26)
+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);
+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";
+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
+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;
+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+}
+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>;
+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;
+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;
+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+});
+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+}
+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 ) {
+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}
+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;
+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 }));
+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 }
+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> = (
+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";
+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
+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
+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
+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";
+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) => {