- commit
- 2e93eae
- parent
- a84cf61
- author
- Eric Bower
- date
- 2023-11-22 17:09:20 +0000 UTC
refactor(query): middleware naming (#25)
+4,
-3
1@@ -215,7 +215,7 @@ ${method}<P, ApiSuccess, ApiError = unknown>(
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 "./pipe.ts";
6+import type { SagaApi } from "./thunk.ts";
7 import type {
8 ApiCtx,
9 CreateAction,
10@@ -226,14 +226,15 @@ import type {
11 Supervisor,
12 } from "./types.ts";
13 import type { Payload } from "../types.ts";
14+import type { Operation } from "../deps.ts";
15
16 export type ApiName = string | string[];
17
18 export interface SagaQueryApi<Ctx extends ApiCtx = ApiCtx> extends SagaApi<Ctx> {
19 request: (
20 r: Partial<RequestInit>,
21- ) => (ctx: Ctx, next: Next) => Iterator<unknown>;
22- cache: () => (ctx: Ctx, next: Next) => Iterator<unknown>;
23+ ) => (ctx: Ctx, next: Next) => Operation<unknown>;
24+ cache: () => (ctx: Ctx, next: Next) => Operation<unknown>;
25
26 uri: (uri: string) => {
27 ${uriMethods}
+1,
-1
1@@ -2,5 +2,5 @@ export * from "./parallel.ts";
2 export * from "./call.ts";
3 export * from "./race.ts";
4 export * from "./request.ts";
5-export * from "./watch.ts";
6+export * from "./supervisor.ts";
7 export * from "./defer.ts";
+72,
-0
1@@ -0,0 +1,72 @@
2+import { describe, expect, it } from "../test.ts";
3+import { call, each, Operation, run, spawn } from "../deps.ts";
4+import { supervise, superviseBackoff } from "./supervisor.ts";
5+import { LogAction, LogContext } from "../log.ts";
6+
7+const test = describe("supervise()");
8+
9+describe("superviseBackoff", () => {
10+ it("should increase number exponentially", () => {
11+ const actual: number[] = [];
12+ for (let i = 1; i < 15; i += 1) {
13+ actual.push(superviseBackoff(i));
14+ }
15+ expect(actual).toEqual([
16+ 20,
17+ 40,
18+ 80,
19+ 160,
20+ 320,
21+ 640,
22+ 1280,
23+ 2560,
24+ 5120,
25+ 10240,
26+ -1,
27+ -1,
28+ -1,
29+ -1,
30+ ]);
31+ });
32+});
33+
34+it(test, "should recover with backoff pressure", async () => {
35+ const err = console.error;
36+ console.error = () => {};
37+
38+ const actions: LogAction[] = [];
39+ const backoff = (attempt: number) => {
40+ if (attempt === 4) return -1;
41+ return attempt;
42+ };
43+
44+ await run(function* () {
45+ function* op(): Operation<void> {
46+ throw new Error("boom!");
47+ }
48+ yield* spawn(function* () {
49+ const chan = yield* LogContext;
50+ for (const action of yield* each(chan)) {
51+ actions.push(action);
52+ yield* each.next();
53+ }
54+ });
55+ yield* call(supervise(op, backoff));
56+ });
57+
58+ expect(actions.length).toEqual(3);
59+ expect(actions[0].type).toEqual("fx:supervise");
60+ expect(actions[0].payload.message).toEqual(
61+ "Exception caught, waiting 1ms before restarting operation",
62+ );
63+ expect(actions[1].type).toEqual("fx:supervise");
64+ expect(actions[1].payload.message).toEqual(
65+ "Exception caught, waiting 2ms before restarting operation",
66+ );
67+ expect(actions[2].type).toEqual("fx:supervise");
68+ expect(actions[2].payload.message).toEqual(
69+ "Exception caught, waiting 3ms before restarting operation",
70+ );
71+
72+ console.error = err;
73+});
+61,
-0
1@@ -0,0 +1,61 @@
2+import { Operation, Result, sleep } from "../deps.ts";
3+import type { Operator } from "../types.ts";
4+import { safe } from "./call.ts";
5+import { parallel } from "./parallel.ts";
6+import { log } from "../log.ts";
7+
8+export function superviseBackoff(attempt: number, max = 10): number {
9+ if (attempt > max) return -1;
10+ // 20ms, 40ms, 80ms, 160ms, 320ms, 640ms, 1280ms, 2560ms, 5120ms, 10240ms
11+ return 2 ** attempt * 10;
12+}
13+
14+/**
15+ * {@link supvervise} will watch whatever {@link Operation} is provided
16+ * and it will automatically try to restart it when it exists. By
17+ * default it uses a backoff pressure mechanism so if there is an
18+ * error simply calling the {@link Operation} then it will exponentially
19+ * wait longer until attempting to restart and eventually give up.
20+ */
21+export function supervise<T>(
22+ op: Operator<T>,
23+ backoff: (attemp: number) => number = superviseBackoff,
24+) {
25+ return function* () {
26+ let attempt = 1;
27+ let waitFor = backoff(attempt);
28+
29+ while (waitFor >= 0) {
30+ const res = yield* safe(op);
31+
32+ if (res.ok) {
33+ attempt = 0;
34+ } else {
35+ yield* log({
36+ type: "fx:supervise",
37+ payload: {
38+ message:
39+ `Exception caught, waiting ${waitFor}ms before restarting operation`,
40+ op,
41+ error: res.error,
42+ },
43+ });
44+ yield* sleep(waitFor);
45+ }
46+
47+ attempt += 1;
48+ waitFor = backoff(attempt);
49+ }
50+ };
51+}
52+
53+export function* keepAlive(
54+ ops: Operator<unknown>[],
55+ backoff?: (attempt: number) => number,
56+): Operation<Result<void>[]> {
57+ const group = yield* parallel(
58+ ops.map((op) => supervise(op, backoff)),
59+ );
60+ const results = yield* group;
61+ return results;
62+}
+0,
-16
1@@ -1,16 +0,0 @@
2-import type { Operator } from "../types.ts";
3-import { safe } from "./call.ts";
4-import { parallel } from "./parallel.ts";
5-
6-export function supervise<T>(op: Operator<T>) {
7- return function* () {
8- while (true) {
9- yield* safe(op);
10- }
11- };
12-}
13-
14-export function* keepAlive(ops: Operator<unknown>[]) {
15- const results = yield* parallel(ops.map(supervise));
16- return yield* results;
17-}
A
log.ts
+26,
-0
1@@ -0,0 +1,26 @@
2+import { createChannel, createContext } from "./deps.ts";
3+import type { ActionWPayload } from "./types.ts";
4+
5+export interface LogMessage {
6+ message: string;
7+ [key: string]: any;
8+}
9+export type LogAction = ActionWPayload<LogMessage>;
10+
11+export function createLogger(type: string) {
12+ return function (payload: LogMessage) {
13+ return log({ type, payload });
14+ };
15+}
16+
17+export function* log(action: LogAction) {
18+ const chan = yield* LogContext;
19+ yield* chan.send(action);
20+ // TODO: only for dev mode?
21+ console.error(action);
22+}
23+
24+export const LogContext = createContext(
25+ "starfx:logger",
26+ createChannel<LogAction>(),
27+);
M
mod.ts
+1,
-0
1@@ -3,6 +3,7 @@ export * from "./query/mod.ts";
2 export * from "./types.ts";
3 export * from "./compose.ts";
4 export * from "./action.ts";
5+export * from "./log.ts";
6 export {
7 action,
8 createChannel,
+1,
-1
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 "./pipe.ts";
6+import type { SagaApi } from "./thunk.ts";
7 import type {
8 ApiCtx,
9 CreateAction,
+32,
-25
1@@ -1,5 +1,4 @@
2 import { describe, expect, it } from "../test.ts";
3-
4 import { call, keepAlive } from "../fx/mod.ts";
5 import {
6 configureStore,
7@@ -11,8 +10,7 @@ import {
8 } from "../store/mod.ts";
9 import { sleep } from "../test.ts";
10 import { safe } from "../mod.ts";
11-
12-import { queryCtx, requestMonitor, urlParser } from "./middleware.ts";
13+import * as mdw from "./mdw.ts";
14 import { createApi } from "./api.ts";
15 import { createKey } from "./create-key.ts";
16 import type { ApiCtx } from "./types.ts";
17@@ -45,8 +43,8 @@ const tests = describe("createApi()");
18 it(tests, "createApi - POST", async () => {
19 const query = createApi();
20
21- query.use(queryCtx);
22- query.use(urlParser);
23+ query.use(mdw.queryCtx);
24+ query.use(mdw.nameParser);
25 query.use(query.routes());
26 query.use(function* fetchApi(ctx, next) {
27 expect(ctx.req()).toEqual({
28@@ -108,8 +106,8 @@ it(tests, "createApi - POST", async () => {
29
30 it(tests, "POST with uri", () => {
31 const query = createApi();
32- query.use(queryCtx);
33- query.use(urlParser);
34+ query.use(mdw.queryCtx);
35+ query.use(mdw.nameParser);
36 query.use(query.routes());
37 query.use(function* fetchApi(ctx, next) {
38 expect(ctx.req()).toEqual({
39@@ -139,7 +137,7 @@ it(tests, "POST with uri", () => {
40
41 yield* next();
42 if (!ctx.json.ok) return;
43- const { users } = ctx.json.data;
44+ const { users } = ctx.json.value;
45 yield* updateStore<{ users: { [key: string]: User } }>((state) => {
46 users.forEach((u) => {
47 state.users[u.id] = u;
48@@ -155,8 +153,8 @@ it(tests, "POST with uri", () => {
49
50 it(tests, "middleware - with request fn", () => {
51 const query = createApi();
52- query.use(queryCtx);
53- query.use(urlParser);
54+ query.use(mdw.queryCtx);
55+ query.use(mdw.nameParser);
56 query.use(query.routes());
57 query.use(function* (ctx, next) {
58 expect(ctx.req().method).toEqual("POST");
59@@ -185,12 +183,16 @@ it(tests, "run() on endpoint action - should run the effect", () => {
60 acc += "a";
61 },
62 );
63- const action2 = api.get("/users2", function* (_, next) {
64- yield* next();
65- yield* call(() => action1.run(action1({ id: "1" })));
66- acc += "b";
67- expect(acc).toEqual("ab");
68- });
69+ const action2 = api.get(
70+ "/users2",
71+ { supervisor: takeEvery },
72+ function* (_, next) {
73+ yield* next();
74+ yield* call(() => action1.run(action1({ id: "1" })));
75+ acc += "b";
76+ expect(acc).toEqual("ab");
77+ },
78+ );
79
80 const store = configureStore({ initialState: { users: {} } });
81 store.run(api.bootup);
82@@ -235,7 +237,7 @@ it(tests, "run() from a normal saga", () => {
83 it(tests, "createApi with hash key on a large post", async () => {
84 const { store, schema } = testStore();
85 const query = createApi();
86- query.use(requestMonitor());
87+ query.use(mdw.api());
88 query.use(storeMdw(schema.db));
89 query.use(query.routes());
90 query.use(function* fetchApi(ctx, next) {
91@@ -275,6 +277,7 @@ it(tests, "createApi with hash key on a large post", async () => {
92 ctx.json = {
93 ok: true,
94 data: curUsers,
95+ value: curUsers,
96 };
97 },
98 );
99@@ -303,8 +306,9 @@ it(tests, "createApi - two identical endpoints", async () => {
100 const actual: string[] = [];
101 const { store, schema } = testStore();
102 const api = createApi();
103- api.use(requestMonitor());
104+ api.use(mdw.api());
105 api.use(storeMdw(schema.db));
106+ api.use(mdw.nameParser);
107 api.use(api.routes());
108
109 const first = api.get(
110@@ -344,7 +348,8 @@ it(tests, "ensure types for get() endpoint", () => {
111 api.use(api.routes());
112 api.use(function* (ctx, next) {
113 yield* next();
114- ctx.json = { ok: true, data: { result: "wow" } };
115+ const data = { result: "wow" };
116+ ctx.json = { ok: true, data, value: data };
117 });
118
119 const acc: string[] = [];
120@@ -358,7 +363,7 @@ it(tests, "ensure types for get() endpoint", () => {
121 yield* next();
122
123 if (ctx.json.ok) {
124- acc.push(ctx.json.data.result);
125+ acc.push(ctx.json.value.result);
126 }
127 },
128 );
129@@ -381,7 +386,8 @@ it(tests, "ensure ability to cast `ctx` in function definition", () => {
130 api.use(api.routes());
131 api.use(function* (ctx, next) {
132 yield* next();
133- ctx.json = { ok: true, data: { result: "wow" } };
134+ const data = { result: "wow" };
135+ ctx.json = { ok: true, data, value: data };
136 });
137
138 const acc: string[] = [];
139@@ -395,7 +401,7 @@ it(tests, "ensure ability to cast `ctx` in function definition", () => {
140 yield* next();
141
142 if (ctx.json.ok) {
143- acc.push(ctx.json.data.result);
144+ acc.push(ctx.json.value.result);
145 }
146 },
147 );
148@@ -417,7 +423,8 @@ it(
149 api.use(api.routes());
150 api.use(function* (ctx, next) {
151 yield* next();
152- ctx.json = { ok: true, data: { result: "wow" } };
153+ const data = { result: "wow" };
154+ ctx.json = { ok: true, data, value: data };
155 });
156
157 const acc: string[] = [];
158@@ -430,7 +437,7 @@ it(
159 yield* next();
160
161 if (ctx.json.ok) {
162- acc.push(ctx.json.data.result);
163+ acc.push(ctx.json.value.result);
164 }
165 },
166 );
167@@ -453,7 +460,7 @@ it(tests, "should bubble up error", () => {
168 error = err;
169 }
170 });
171- api.use(queryCtx);
172+ api.use(mdw.queryCtx);
173 api.use(storeMdw(schema.db));
174 api.use(api.routes());
175
+8,
-8
1@@ -1,23 +1,23 @@
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 { createThunks } from "./thunk.ts";
7+import type { SagaApi } from "./thunk.ts";
8 import type { ApiName, SagaQueryApi } from "./api-types.ts";
9
10 /**
11 * Creates a middleware pipeline for HTTP requests.
12 *
13 * @remarks
14- * It uses {@link createPipe} under the hood.
15+ * It uses {@link createThunks} under the hood.
16 *
17 * @example
18 * ```ts
19- * import { createApi, requestMonitor, fetcher } from 'starfx';
20+ * import { createApi, mdw } from 'starfx';
21 *
22 * const api = createApi();
23- * api.use(requestMonitor());
24+ * api.use(mdw.api());
25 * api.use(api.routes());
26- * api.use(fetcher({ baseUrl: 'https://api.com' }));
27+ * api.use(mdw.fetch({ baseUrl: 'https://api.com' }));
28 *
29 * const fetchUsers = api.get('/users', function*(ctx, next) {
30 * yield next();
31@@ -27,9 +27,9 @@ import type { ApiName, SagaQueryApi } from "./api-types.ts";
32 * ```
33 */
34 export function createApi<Ctx extends ApiCtx = ApiCtx>(
35- basePipe?: SagaApi<Ctx>,
36+ baseThunk?: SagaApi<Ctx>,
37 ): SagaQueryApi<Ctx> {
38- const pipe = basePipe || createPipe<Ctx>();
39+ const pipe = baseThunk || createThunks<Ctx>();
40 const uri = (prename: ApiName) => {
41 const create = pipe.create as any;
42
+52,
-47
1@@ -6,10 +6,9 @@ import {
2 storeMdw,
3 takeEvery,
4 } from "../store/mod.ts";
5-
6-import { fetcher, fetchRetry, headersMdw } from "./fetch.ts";
7+import * as fetchMdw from "./fetch.ts";
8 import { createApi } from "./api.ts";
9-import { requestMonitor } from "./middleware.ts";
10+import * as mdw from "./mdw.ts";
11
12 install();
13
14@@ -30,7 +29,7 @@ const testStore = () => {
15 return { schema, store };
16 };
17
18-const tests = describe("fetcher()");
19+const tests = describe("mdw.fetch()");
20
21 it(
22 tests,
23@@ -42,11 +41,11 @@ it(
24
25 const { store, schema } = testStore();
26 const api = createApi();
27- api.use(requestMonitor());
28+ api.use(mdw.api());
29 api.use(storeMdw(schema.db));
30 api.use(api.routes());
31- api.use(headersMdw);
32- api.use(fetcher({ baseUrl }));
33+ api.use(fetchMdw.headers);
34+ api.use(mdw.fetch({ baseUrl }));
35
36 const actual: any[] = [];
37 const fetchUsers = api.get(
38@@ -77,7 +76,7 @@ it(
39 headers: {
40 "Content-Type": "application/json",
41 },
42- }, { ok: true, data: mockUser }]);
43+ }, { ok: true, data: mockUser, value: mockUser }]);
44 },
45 );
46
47@@ -91,10 +90,10 @@ it(
48
49 const { store, schema } = testStore();
50 const api = createApi();
51- api.use(requestMonitor());
52+ api.use(mdw.api());
53 api.use(storeMdw(schema.db));
54 api.use(api.routes());
55- api.use(fetcher({ baseUrl }));
56+ api.use(mdw.fetch({ baseUrl }));
57
58 let actual = null;
59 const fetchUsers = api.get(
60@@ -114,7 +113,8 @@ it(
61 store.dispatch(action);
62
63 await delay();
64- expect(actual).toEqual({ ok: true, data: "this is some text" });
65+ const data = "this is some text";
66+ expect(actual).toEqual({ ok: true, data, value: data });
67 },
68 );
69
70@@ -126,15 +126,10 @@ it(tests, "fetch - error handling", async () => {
71
72 const { schema, store } = testStore();
73 const api = createApi();
74- api.use(requestMonitor());
75+ api.use(mdw.api());
76 api.use(storeMdw(schema.db));
77 api.use(api.routes());
78- api.use(function* (ctx, next) {
79- const url = ctx.req().url;
80- ctx.request = ctx.req({ url: `${baseUrl}${url}` });
81- yield* next();
82- });
83- api.use(fetcher());
84+ api.use(mdw.fetch({ baseUrl }));
85
86 let actual = null;
87 const fetchUsers = api.get(
88@@ -157,7 +152,7 @@ it(tests, "fetch - error handling", async () => {
89
90 const state = store.getState();
91 expect(state.data[action.payload.key]).toEqual(errMsg);
92- expect(actual).toEqual({ ok: false, data: errMsg });
93+ expect(actual).toEqual({ ok: false, data: errMsg, error: errMsg });
94 });
95
96 it(tests, "fetch - status 204", async () => {
97@@ -167,7 +162,7 @@ it(tests, "fetch - status 204", async () => {
98
99 const { schema, store } = testStore();
100 const api = createApi();
101- api.use(requestMonitor());
102+ api.use(mdw.api());
103 api.use(storeMdw(schema.db));
104 api.use(api.routes());
105 api.use(function* (ctx, next) {
106@@ -175,7 +170,7 @@ it(tests, "fetch - status 204", async () => {
107 ctx.request = ctx.req({ url: `${baseUrl}${url}` });
108 yield* next();
109 });
110- api.use(fetcher());
111+ api.use(mdw.fetch());
112
113 let actual = null;
114 const fetchUsers = api.get(
115@@ -197,7 +192,7 @@ it(tests, "fetch - status 204", async () => {
116
117 const state = store.getState();
118 expect(state.data[action.payload.key]).toEqual({});
119- expect(actual).toEqual({ ok: true, data: {} });
120+ expect(actual).toEqual({ ok: true, data: {}, value: {} });
121 });
122
123 it(tests, "fetch - malformed json", async () => {
124@@ -207,7 +202,7 @@ it(tests, "fetch - malformed json", async () => {
125
126 const { schema, store } = testStore();
127 const api = createApi();
128- api.use(requestMonitor());
129+ api.use(mdw.api());
130 api.use(storeMdw(schema.db));
131 api.use(api.routes());
132 api.use(function* (ctx, next) {
133@@ -215,7 +210,7 @@ it(tests, "fetch - malformed json", async () => {
134 ctx.request = ctx.req({ url: `${baseUrl}${url}` });
135 yield* next();
136 });
137- api.use(fetcher());
138+ api.use(mdw.fetch());
139
140 let actual = null;
141 const fetchUsers = api.get(
142@@ -235,11 +230,13 @@ it(tests, "fetch - malformed json", async () => {
143
144 await delay();
145
146+ const data = {
147+ message: "Unexpected token 'o', \"not json\" is not valid JSON",
148+ };
149 expect(actual).toEqual({
150 ok: false,
151- data: {
152- message: "Unexpected token 'o', \"not json\" is not valid JSON",
153- },
154+ data,
155+ error: data,
156 });
157 });
158
159@@ -250,17 +247,20 @@ it(tests, "fetch - POST", async () => {
160
161 const { schema, store } = testStore();
162 const api = createApi();
163- api.use(requestMonitor());
164+ api.use(mdw.api());
165 api.use(storeMdw(schema.db));
166 api.use(api.routes());
167- api.use(fetcher({ baseUrl }));
168+ api.use(fetchMdw.headers);
169+ api.use(mdw.fetch({ baseUrl }));
170
171 const fetchUsers = api.post(
172 "/users",
173 { supervisor: takeEvery },
174 function* (ctx, next) {
175 ctx.cache = true;
176- ctx.request = ctx.req({ body: JSON.stringify(mockUser) });
177+ ctx.request = ctx.req({
178+ body: JSON.stringify(mockUser),
179+ });
180 yield* next();
181
182 expect(ctx.req()).toEqual({
183@@ -272,7 +272,7 @@ it(tests, "fetch - POST", async () => {
184 body: JSON.stringify(mockUser),
185 });
186
187- expect(ctx.json).toEqual({ ok: true, data: mockUser });
188+ expect(ctx.json).toEqual({ ok: true, data: mockUser, value: mockUser });
189 },
190 );
191
192@@ -290,10 +290,11 @@ it(tests, "fetch - POST multiple endpoints with same uri", async () => {
193
194 const { store, schema } = testStore();
195 const api = createApi();
196- api.use(requestMonitor());
197+ api.use(mdw.api());
198 api.use(storeMdw(schema.db));
199 api.use(api.routes());
200- api.use(fetcher({ baseUrl }));
201+ api.use(fetchMdw.headers);
202+ api.use(mdw.fetch({ baseUrl }));
203
204 const fetchUsers = api.post<{ id: string }>(
205 "/users/:id/something",
206@@ -312,12 +313,13 @@ it(tests, "fetch - POST multiple endpoints with same uri", async () => {
207 body: JSON.stringify(mockUser),
208 });
209
210- expect(ctx.json).toEqual({ ok: true, data: mockUser });
211+ expect(ctx.json).toEqual({ ok: true, data: mockUser, value: mockUser });
212 },
213 );
214
215 const fetchUsersSecond = api.post<{ id: string }>(
216 ["/users/:id/something", "next"],
217+ { supervisor: takeEvery },
218 function* (ctx, next) {
219 ctx.cache = true;
220 ctx.request = ctx.req({ body: JSON.stringify(mockUser) });
221@@ -332,7 +334,7 @@ it(tests, "fetch - POST multiple endpoints with same uri", async () => {
222 body: JSON.stringify(mockUser),
223 });
224
225- expect(ctx.json).toEqual({ ok: true, data: mockUser });
226+ expect(ctx.json).toEqual({ ok: true, data: mockUser, value: mockUser });
227 },
228 );
229
230@@ -350,10 +352,10 @@ it(
231 async () => {
232 const { store, schema } = testStore();
233 const api = createApi();
234- api.use(requestMonitor());
235+ api.use(mdw.api());
236 api.use(storeMdw(schema.db));
237 api.use(api.routes());
238- api.use(fetcher({ baseUrl }));
239+ api.use(mdw.fetch({ baseUrl }));
240
241 const fetchUsers = api.post<{ id: string }>(
242 "/users/:id",
243@@ -364,10 +366,12 @@ it(
244
245 yield* next();
246
247+ const data =
248+ "found :id in endpoint name (/users/:id [POST]) but payload has falsy value ()";
249 expect(ctx.json).toEqual({
250 ok: false,
251- data:
252- "found :id in endpoint name (/users/:id [POST]) but payload has falsy value ()",
253+ data,
254+ error: data,
255 });
256 },
257 );
258@@ -397,10 +401,10 @@ it(
259
260 const { schema, store } = testStore();
261 const api = createApi();
262- api.use(requestMonitor());
263+ api.use(mdw.api());
264 api.use(storeMdw(schema.db));
265 api.use(api.routes());
266- api.use(fetcher({ baseUrl }));
267+ api.use(mdw.fetch({ baseUrl }));
268
269 let actual = null;
270 const fetchUsers = api.get("/users", { supervisor: takeEvery }, [
271@@ -414,7 +418,7 @@ it(
272
273 actual = ctx.json;
274 },
275- fetchRetry((n) => (n > 4 ? -1 : 10)),
276+ mdw.fetchRetry((n) => (n > 4 ? -1 : 10)),
277 ]);
278
279 store.run(api.bootup);
280@@ -426,7 +430,7 @@ it(
281
282 const state = store.getState();
283 expect(state.data[action.payload.key]).toEqual(mockUser);
284- expect(actual).toEqual({ ok: true, data: mockUser });
285+ expect(actual).toEqual({ ok: true, data: mockUser, value: mockUser });
286 },
287 );
288
289@@ -443,10 +447,10 @@ it(
290 const { schema, store } = testStore();
291 let actual = null;
292 const api = createApi();
293- api.use(requestMonitor());
294+ api.use(mdw.api());
295 api.use(storeMdw(schema.db));
296 api.use(api.routes());
297- api.use(fetcher({ baseUrl }));
298+ api.use(mdw.fetch({ baseUrl }));
299
300 const fetchUsers = api.get("/users", { supervisor: takeEvery }, [
301 function* (ctx, next) {
302@@ -454,7 +458,7 @@ it(
303 yield* next();
304 actual = ctx.json;
305 },
306- fetchRetry((n) => (n > 2 ? -1 : 10)),
307+ mdw.fetchRetry((n) => (n > 2 ? -1 : 10)),
308 ]);
309
310 store.run(api.bootup);
311@@ -462,6 +466,7 @@ it(
312 store.dispatch(action);
313
314 await delay();
315- expect(actual).toEqual({ ok: false, data: { message: "error" } });
316+ const data = { message: "error" };
317+ expect(actual).toEqual({ ok: false, data, error: data });
318 },
319 );
+82,
-109
1@@ -1,15 +1,63 @@
2-import { sleep } from "../deps.ts";
3-import { compose } from "../compose.ts";
4-import { safe } from "../mod.ts";
5-
6-import { noop } from "./util.ts";
7+import { safe } from "../fx/mod.ts";
8 import type { FetchCtx, FetchJsonCtx, Next } from "./types.ts";
9+import { isObject } from "./util.ts";
10+
11+/**
12+ * This middleware converts the name provided to {@link createApi}
13+ * into `url` and `method` for the fetch request.
14+ */
15+export function* nameParser<Ctx extends FetchJsonCtx = FetchJsonCtx>(
16+ ctx: Ctx,
17+ next: Next,
18+) {
19+ const httpMethods = [
20+ "get",
21+ "head",
22+ "post",
23+ "put",
24+ "delete",
25+ "connect",
26+ "options",
27+ "trace",
28+ "patch",
29+ ];
30+
31+ const options = ctx.payload || {};
32+ if (!isObject(options)) {
33+ yield* next();
34+ return;
35+ }
36+
37+ let url = Object.keys(options).reduce((acc, key) => {
38+ return acc.replace(`:${key}`, options[key]);
39+ }, ctx.name);
40+
41+ let method = "";
42+ httpMethods.forEach((curMethod) => {
43+ const pattern = new RegExp(`\\s*\\[` + curMethod + `\\]\\s*\\w*`, "i");
44+ const tmpUrl = url.replace(pattern, "");
45+ if (tmpUrl.length !== url.length) {
46+ method = curMethod.toLocaleUpperCase();
47+ }
48+ url = tmpUrl;
49+ }, url);
50+
51+ if (ctx.req().url === "") {
52+ ctx.request = ctx.req({ url });
53+ }
54+
55+ if (method) {
56+ ctx.request = ctx.req({ method });
57+ }
58+
59+ yield* next();
60+}
61
62 /**
63 * Automatically sets `content-type` to `application/json` when
64 * that header is not already present.
65 */
66-export function* headersMdw<CurCtx extends FetchCtx = FetchCtx>(
67+export function* headers<CurCtx extends FetchCtx = FetchCtx>(
68 ctx: CurCtx,
69 next: Next,
70 ) {
71@@ -41,7 +89,7 @@ export function* headersMdw<CurCtx extends FetchCtx = FetchCtx>(
72 * })
73 * ```
74 */
75-export function* jsonMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
76+export function* json<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
77 ctx: CurCtx,
78 next: Next,
79 ) {
80@@ -52,8 +100,9 @@ export function* jsonMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
81
82 if (ctx.response.status === 204) {
83 ctx.json = {
84- ok: ctx.response.ok,
85+ ok: true,
86 data: {},
87+ value: {},
88 };
89 yield* next();
90 return;
91@@ -66,14 +115,25 @@ export function* jsonMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
92 });
93
94 if (data.ok) {
95- ctx.json = {
96- ok: ctx.response.ok,
97- data: data.value,
98- };
99+ if (ctx.response.ok) {
100+ ctx.json = {
101+ ok: true,
102+ data: data.value,
103+ value: data.value,
104+ };
105+ } else {
106+ ctx.json = {
107+ ok: false,
108+ data: data.value,
109+ error: data.value,
110+ };
111+ }
112 } else {
113+ const dta = { message: data.error.message };
114 ctx.json = {
115 ok: false,
116- data: { message: data.error.message },
117+ data: dta,
118+ error: dta,
119 };
120 }
121
122@@ -81,10 +141,10 @@ export function* jsonMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
123 }
124
125 /*
126- * This middleware takes the `baseUrl` provided to `fetcher()` and combines it
127+ * This middleware takes the `baseUrl` provided to {@link mdw.fetch} and combines it
128 * with the url from `ctx.request.url`.
129 */
130-export function apiUrlMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
131+export function composeUrl<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
132 baseUrl = "",
133 ) {
134 return function* (ctx: CurCtx, next: Next) {
135@@ -105,7 +165,7 @@ export function apiUrlMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
136 * Ideally the action wouldn't have been dispatched at all but that is *not* a
137 * gaurantee we can make here.
138 */
139-export function* payloadMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
140+export function* payload<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
141 ctx: CurCtx,
142 next: Next,
143 ) {
144@@ -124,10 +184,12 @@ export function* payloadMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
145
146 const val = payload[key];
147 if (!val) {
148+ const data =
149+ `found :${key} in endpoint name (${ctx.name}) but payload has falsy value (${val})`;
150 ctx.json = {
151 ok: false,
152- data:
153- `found :${key} in endpoint name (${ctx.name}) but payload has falsy value (${val})`,
154+ data,
155+ error: data,
156 };
157 return;
158 }
159@@ -140,13 +202,13 @@ export function* payloadMdw<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
160 * This middleware makes the `fetch` http request using `ctx.request` and
161 * assigns the response to `ctx.response`.
162 */
163-export function* fetchMdw<CurCtx extends FetchCtx = FetchCtx>(
164+export function* request<CurCtx extends FetchCtx = FetchCtx>(
165 ctx: CurCtx,
166 next: Next,
167 ) {
168 const { url, ...req } = ctx.req();
169 const request = new Request(url, req);
170- const result = yield* safe(() => fetch(request));
171+ const result = yield* safe(fetch(request));
172 if (result.ok) {
173 ctx.response = result.value;
174 } else {
175@@ -154,92 +216,3 @@ export function* fetchMdw<CurCtx extends FetchCtx = FetchCtx>(
176 }
177 yield* next();
178 }
179-
180-function backoffExp(attempt: number): number {
181- if (attempt > 5) return -1;
182- // 1s, 1s, 1s, 2s, 4s
183- return Math.max(2 ** attempt * 125, 1000);
184-}
185-
186-/**
187- * This middleware will retry failed `Fetch` request if `response.ok` is `false`.
188- * It accepts a backoff function to determine how long to continue retrying.
189- * The default is an exponential backoff {@link backoffExp} where the minimum is
190- * 1sec between attempts and it'll reach 4s between attempts at the end with a
191- * max of 5 attempts.
192- *
193- * An example backoff:
194- * @example
195- * ```ts
196- * // Any value less than 0 will stop the retry middleware.
197- * // Each attempt will wait 1s
198- * const backoff = (attempt: number) => {
199- * if (attempt > 5) return -1;
200- * return 1000;
201- * }
202- *
203- * const api = createApi();
204- * api.use(requestMonitor());
205- * api.use(api.routes());
206- * api.use(fetcher());
207- *
208- * const fetchUsers = api.get('/users', [
209- * function*(ctx, next) {
210- * // ...
211- * yield next();
212- * },
213- * // fetchRetry should be after your endpoint function because
214- * // the retry middleware will update `ctx.json` before it reaches your middleware
215- * fetchRetry(backoff),
216- * ])
217- * ```
218- */
219-export function fetchRetry<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
220- backoff: (attempt: number) => number = backoffExp,
221-) {
222- return function* (ctx: CurCtx, next: Next) {
223- yield* next();
224-
225- if (!ctx.response) {
226- return;
227- }
228-
229- if (ctx.response.ok) {
230- return;
231- }
232-
233- let attempt = 1;
234- let waitFor = backoff(attempt);
235- while (waitFor >= 1) {
236- yield* sleep(waitFor);
237- yield* safe(() => fetchMdw(ctx, noop));
238- yield* safe(() => jsonMdw(ctx, noop));
239-
240- if (ctx.response.ok) {
241- return;
242- }
243-
244- attempt += 1;
245- waitFor = backoff(attempt);
246- }
247- };
248-}
249-
250-/**
251- * This middleware is a composition of other middleware required to use `window.fetch`
252- * {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API} with {@link createApi}
253- */
254-export function fetcher<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
255- {
256- baseUrl = "",
257- }: {
258- baseUrl?: string;
259- } = { baseUrl: "" },
260-) {
261- return compose<CurCtx>([
262- apiUrlMdw(baseUrl),
263- payloadMdw,
264- fetchMdw,
265- jsonMdw,
266- ]);
267-}
R query/middleware.test.ts =>
query/mdw.test.ts
+43,
-47
1@@ -1,12 +1,5 @@
2 import { assertLike, asserts, describe, expect, it } from "../test.ts";
3-import {
4- createApi,
5- createKey,
6- customKey,
7- queryCtx,
8- requestMonitor,
9- urlParser,
10-} from "../query/mod.ts";
11+import { createApi, createKey, mdw } from "../query/mod.ts";
12 import type { ApiCtx, Next, PipeCtx } from "../query/mod.ts";
13 import { createQueryState } from "../action.ts";
14 import { sleep } from "../test.ts";
15@@ -52,19 +45,19 @@ const tests = describe("middleware");
16 it(tests, "basic", () => {
17 const { store } = testStore();
18 const query = createApi<ApiCtx>();
19- query.use(queryCtx);
20- query.use(urlParser);
21+ query.use(mdw.queryCtx);
22+ query.use(mdw.api());
23 query.use(query.routes());
24 query.use(function* fetchApi(ctx, next) {
25 if (`${ctx.req().url}`.startsWith("/users/")) {
26- ctx.json = { ok: true, data: mockUser2 };
27+ ctx.json = { ok: true, data: mockUser2, value: mockUser2 };
28 yield* next();
29 return;
30 }
31 const data = {
32 users: [mockUser],
33 };
34- ctx.json = { ok: true, data };
35+ ctx.json = { ok: true, data, value: data };
36 yield* next();
37 });
38
39@@ -74,7 +67,7 @@ it(tests, "basic", () => {
40 function* processUsers(ctx: ApiCtx<unknown, { users: User[] }>, next) {
41 yield* next();
42 if (!ctx.json.ok) return;
43- const { users } = ctx.json.data;
44+ const { users } = ctx.json.value;
45
46 yield* updateStore((state) => {
47 users.forEach((u) => {
48@@ -93,7 +86,7 @@ it(tests, "basic", () => {
49 ctx.request = ctx.req({ method: "POST" });
50 yield* next();
51 if (!ctx.json.ok) return;
52- const curUser = ctx.json.data;
53+ const curUser = ctx.json.value;
54 yield* updateStore((state) => {
55 state.users[curUser.id] = curUser;
56 });
57@@ -116,12 +109,13 @@ it(tests, "basic", () => {
58 it(tests, "with loader", () => {
59 const { schema, store } = testStore();
60 const api = createApi<ApiCtx>();
61- api.use(requestMonitor());
62+ api.use(mdw.api());
63 api.use(storeMdw(schema.db));
64 api.use(api.routes());
65 api.use(function* fetchApi(ctx, next) {
66 ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
67- ctx.json = { ok: true, data: { users: [mockUser] } };
68+ const data = { users: [mockUser] };
69+ ctx.json = { ok: true, data, value: data };
70 yield* next();
71 });
72
73@@ -132,10 +126,10 @@ it(tests, "with loader", () => {
74 yield* next();
75 if (!ctx.json.ok) return;
76
77- const { data } = ctx.json;
78+ const { value } = ctx.json;
79
80 yield* updateStore((state) => {
81- data.users.forEach((u) => {
82+ value.users.forEach((u) => {
83 state.users[u.id] = u;
84 });
85 });
86@@ -158,12 +152,13 @@ it(tests, "with loader", () => {
87 it(tests, "with item loader", () => {
88 const { store, schema } = testStore();
89 const api = createApi<ApiCtx>();
90- api.use(requestMonitor());
91+ api.use(mdw.api());
92 api.use(storeMdw(schema.db));
93 api.use(api.routes());
94 api.use(function* fetchApi(ctx, next) {
95 ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
96- ctx.json = { ok: true, data: { users: [mockUser] } };
97+ const data = { users: [mockUser] };
98+ ctx.json = { ok: true, data, value: data };
99 yield* next();
100 });
101
102@@ -174,9 +169,9 @@ it(tests, "with item loader", () => {
103 yield* next();
104 if (!ctx.json.ok) return;
105
106- const { data } = ctx.json;
107+ const { value } = ctx.json;
108 yield* updateStore((state) => {
109- data.users.forEach((u) => {
110+ value.users.forEach((u) => {
111 state.users[u.id] = u;
112 });
113 });
114@@ -202,8 +197,8 @@ it(tests, "with item loader", () => {
115
116 it(tests, "with POST", () => {
117 const query = createApi();
118- query.use(queryCtx);
119- query.use(urlParser);
120+ query.use(mdw.queryCtx);
121+ query.use(mdw.api());
122 query.use(query.routes());
123 query.use(function* fetchApi(ctx, next) {
124 const request = ctx.req();
125@@ -237,7 +232,7 @@ it(tests, "with POST", () => {
126
127 if (!ctx.json.ok) return;
128
129- const { users } = ctx.json.data;
130+ const { users } = ctx.json.value;
131 yield* updateStore((state) => {
132 users.forEach((u) => {
133 state.users[u.id] = u;
134@@ -254,13 +249,13 @@ it(tests, "with POST", () => {
135 it(tests, "simpleCache", () => {
136 const { store, schema } = testStore();
137 const api = createApi<ApiCtx>();
138- api.use(requestMonitor());
139+ api.use(mdw.api());
140 api.use(storeMdw(schema.db));
141 api.use(api.routes());
142 api.use(function* fetchApi(ctx, next) {
143 const data = { users: [mockUser] };
144 ctx.response = new Response(jsonBlob(data));
145- ctx.json = { ok: true, data };
146+ ctx.json = { ok: true, data, value: data };
147 yield* next();
148 });
149
150@@ -284,13 +279,13 @@ it(tests, "simpleCache", () => {
151 it(tests, "overriding default loader behavior", () => {
152 const { store, schema } = testStore();
153 const api = createApi<ApiCtx>();
154- api.use(requestMonitor());
155+ api.use(mdw.api());
156 api.use(storeMdw(schema.db));
157 api.use(api.routes());
158 api.use(function* fetchApi(ctx, next) {
159 const data = { users: [mockUser] };
160 ctx.response = new Response(jsonBlob(data));
161- ctx.json = { ok: true, data };
162+ ctx.json = { ok: true, data, value: data };
163 yield* next();
164 });
165
166@@ -303,10 +298,10 @@ it(tests, "overriding default loader behavior", () => {
167 if (!ctx.json.ok) {
168 return;
169 }
170- const { data } = ctx.json;
171+ const { value } = ctx.json;
172 ctx.loader = { message: "yes", meta: { wow: true } };
173 yield* updateStore((state) => {
174- data.users.forEach((u) => {
175+ value.users.forEach((u) => {
176 state.users[u.id] = u;
177 });
178 });
179@@ -328,7 +323,7 @@ it(tests, "overriding default loader behavior", () => {
180 });
181 });
182
183-it(tests, "requestMonitor - error handler", () => {
184+it(tests, "mdw.api() - error handler", () => {
185 let err = false;
186 console.error = (msg: string) => {
187 if (err) return;
188@@ -341,7 +336,7 @@ it(tests, "requestMonitor - error handler", () => {
189
190 const { schema, store } = testStore();
191 const query = createApi<ApiCtx>();
192- query.use(requestMonitor());
193+ query.use(mdw.api());
194 query.use(storeMdw(schema.db));
195 query.use(query.routes());
196 query.use(function* () {
197@@ -357,10 +352,10 @@ it(tests, "requestMonitor - error handler", () => {
198 it(tests, "createApi with own key", async () => {
199 const { schema, store } = testStore();
200 const query = createApi();
201- query.use(requestMonitor());
202+ query.use(mdw.api());
203 query.use(storeMdw(schema.db));
204 query.use(query.routes());
205- query.use(customKey);
206+ query.use(mdw.customKey);
207 query.use(function* fetchApi(ctx, next) {
208 const data = {
209 users: [{ ...mockUser, ...ctx.action.payload.options }],
210@@ -400,6 +395,7 @@ it(tests, "createApi with own key", async () => {
211 ctx.json = {
212 ok: true,
213 data: curUsers,
214+ value: curUsers,
215 };
216 },
217 );
218@@ -427,10 +423,10 @@ it(tests, "createApi with own key", async () => {
219 it(tests, "createApi with custom key but no payload", async () => {
220 const { store, schema } = testStore();
221 const query = createApi();
222- query.use(requestMonitor());
223+ query.use(mdw.api());
224 query.use(storeMdw(schema.db));
225 query.use(query.routes());
226- query.use(customKey);
227+ query.use(mdw.customKey);
228 query.use(function* fetchApi(ctx, next) {
229 const data = {
230 users: [mockUser],
231@@ -470,6 +466,7 @@ it(tests, "createApi with custom key but no payload", async () => {
232 ctx.json = {
233 ok: true,
234 data: curUsers,
235+ value: curUsers,
236 };
237 },
238 );
239@@ -500,30 +497,29 @@ it(tests, "errorHandler", () => {
240 ctx: Ctx,
241 next: Next,
242 ) {
243- a = 1;
244- yield* next();
245- a = 2;
246- if (!ctx.result.ok) {
247+ try {
248+ a = 1;
249+ yield* next();
250+ a = 2;
251+ } catch (err) {
252 console.error(
253- `Error: ${ctx.result.error.message}. Check the endpoint [${ctx.name}]`,
254+ `Error: ${err.message}. Check the endpoint [${ctx.name}]`,
255 ctx,
256 );
257- console.error(ctx.result.error);
258 }
259 });
260- query.use(queryCtx);
261- query.use(urlParser);
262+ query.use(mdw.queryCtx);
263 query.use(query.routes());
264 query.use(function* fetchApi(ctx, next) {
265 if (`${ctx.req().url}`.startsWith("/users/")) {
266- ctx.json = { ok: true, data: mockUser2 };
267+ ctx.json = { ok: true, data: mockUser2, value: mockUser2 };
268 yield* next();
269 return;
270 }
271 const data = {
272 users: [mockUser],
273 };
274- ctx.json = { ok: true, data };
275+ ctx.json = { ok: true, data, value: data };
276 yield* next();
277 });
278
+214,
-0
1@@ -0,0 +1,214 @@
2+import { safe } from "../fx/mod.ts";
3+import { compose } from "../compose.ts";
4+import type {
5+ ApiCtx,
6+ ApiRequest,
7+ FetchJsonCtx,
8+ Next,
9+ PerfCtx,
10+ PipeCtx,
11+ RequiredApiRequest,
12+} from "./types.ts";
13+import { mergeRequest } from "./util.ts";
14+import { sleep } from "../deps.ts";
15+import { noop } from "./util.ts";
16+import * as fetchMdw from "./fetch.ts";
17+import { log } from "../log.ts";
18+export * from "./fetch.ts";
19+
20+/**
21+ * This middleware will catch any errors in the pipeline
22+ * and `console.error` the context object.
23+ *
24+ * You are highly encouraged to ditch this middleware if you need something
25+ * more custom.
26+ *
27+ * It also sets `ctx.result` which informs us whether the entire
28+ * middleware pipeline succeeded or not. Think the `.catch()` case for
29+ * `window.fetch`.
30+ */
31+export function* err<Ctx extends PipeCtx = PipeCtx>(
32+ ctx: Ctx,
33+ next: Next,
34+) {
35+ ctx.result = yield* safe(next);
36+ if (!ctx.result.ok) {
37+ yield* log({
38+ type: "query:err",
39+ payload: {
40+ message:
41+ `Error: ${ctx.result.error.message}. Check the endpoint [${ctx.name}]`,
42+ ctx,
43+ },
44+ });
45+ }
46+}
47+
48+/**
49+ * This middleware allows the user to override the default key provided
50+ * to every pipeline function and instead use whatever they want.
51+ *
52+ * @example
53+ * ```ts
54+ * import { createPipe } from 'starfx';
55+ *
56+ * const thunk = createPipe();
57+ * thunk.use(customKey);
58+ *
59+ * const doit = thunk.create('some-action', function*(ctx, next) {
60+ * ctx.key = 'something-i-want';
61+ * })
62+ * ```
63+ */
64+export function* customKey<Ctx extends PipeCtx = PipeCtx>(
65+ ctx: Ctx,
66+ next: Next,
67+) {
68+ if (
69+ ctx?.key &&
70+ ctx?.action?.payload?.key &&
71+ ctx.key !== ctx.action.payload.key
72+ ) {
73+ const newKey = ctx.name.split("|")[0] + "|" + ctx.key;
74+ ctx.key = newKey;
75+ ctx.action.payload.key = newKey;
76+ }
77+ yield* next();
78+}
79+
80+/**
81+ * This middleware sets up the context object with some values that are
82+ * necessary for {@link createApi} to work properly.
83+ */
84+export function* queryCtx<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
85+ if (!ctx.req) {
86+ ctx.req = (r?: ApiRequest): RequiredApiRequest =>
87+ mergeRequest(ctx.request, r);
88+ }
89+ if (!ctx.request) ctx.request = ctx.req();
90+ if (!ctx.response) ctx.response = null;
91+ if (!ctx.json) ctx.json = { ok: false, data: {}, error: {} };
92+ if (!ctx.actions) ctx.actions = [];
93+ if (!ctx.bodyType) ctx.bodyType = "json";
94+ yield* next();
95+}
96+
97+/**
98+ * This middleware is a composition of many middleware used to faciliate
99+ * the {@link createApi}.
100+ *
101+ * It is not required, however,
102+ */
103+export function api<Ctx extends ApiCtx = ApiCtx>() {
104+ return compose<Ctx>([
105+ err,
106+ queryCtx,
107+ customKey,
108+ fetchMdw.nameParser,
109+ ]);
110+}
111+
112+/**
113+ * This middleware will add `performance.now()` before and after your
114+ * middleware pipeline.
115+ */
116+export function* perf<Ctx extends PerfCtx = PerfCtx>(
117+ ctx: Ctx,
118+ next: Next,
119+) {
120+ const t0 = performance.now();
121+ yield* next();
122+ const t1 = performance.now();
123+ ctx.performance = t1 - t0;
124+}
125+
126+function backoffExp(attempt: number): number {
127+ if (attempt > 5) return -1;
128+ // 1s, 1s, 1s, 2s, 4s
129+ return Math.max(2 ** attempt * 125, 1000);
130+}
131+
132+/**
133+ * This middleware will retry failed `Fetch` request if `response.ok` is `false`.
134+ * It accepts a backoff function to determine how long to continue retrying.
135+ * The default is an exponential backoff {@link backoffExp} where the minimum is
136+ * 1sec between attempts and it'll reach 4s between attempts at the end with a
137+ * max of 5 attempts.
138+ *
139+ * An example backoff:
140+ * @example
141+ * ```ts
142+ * // Any value less than 0 will stop the retry middleware.
143+ * // Each attempt will wait 1s
144+ * const backoff = (attempt: number) => {
145+ * if (attempt > 5) return -1;
146+ * return 1000;
147+ * }
148+ *
149+ * const api = createApi();
150+ * api.use(mdw.api());
151+ * api.use(api.routes());
152+ * api.use(mdw.fetch());
153+ *
154+ * const fetchUsers = api.get('/users', [
155+ * function*(ctx, next) {
156+ * // ...
157+ * yield next();
158+ * },
159+ * // fetchRetry should be after your endpoint function because
160+ * // the retry middleware will update `ctx.json` before it reaches
161+ * // your middleware
162+ * fetchRetry(backoff),
163+ * ])
164+ * ```
165+ */
166+export function fetchRetry<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
167+ backoff: (attempt: number) => number = backoffExp,
168+) {
169+ return function* (ctx: CurCtx, next: Next) {
170+ yield* next();
171+
172+ if (!ctx.response) {
173+ return;
174+ }
175+
176+ if (ctx.response.ok) {
177+ return;
178+ }
179+
180+ let attempt = 1;
181+ let waitFor = backoff(attempt);
182+ while (waitFor >= 1) {
183+ yield* sleep(waitFor);
184+ yield* safe(() => fetchMdw.request(ctx, noop));
185+ yield* safe(() => fetchMdw.json(ctx, noop));
186+
187+ if (ctx.response.ok) {
188+ return;
189+ }
190+
191+ attempt += 1;
192+ waitFor = backoff(attempt);
193+ }
194+ };
195+}
196+
197+/**
198+ * This middleware is a composition of other middleware required to
199+ * use `window.fetch` {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API}
200+ * with {@link createApi}
201+ */
202+export function fetch<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
203+ {
204+ baseUrl = "",
205+ }: {
206+ baseUrl?: string;
207+ } = { baseUrl: "" },
208+) {
209+ return compose<CurCtx>([
210+ fetchMdw.composeUrl(baseUrl),
211+ fetchMdw.payload,
212+ fetchMdw.request,
213+ fetchMdw.json,
214+ ]);
215+}
+0,
-165
1@@ -1,165 +0,0 @@
2-import { call } from "../fx/mod.ts";
3-import { compose } from "../compose.ts";
4-import type { Operator } from "../types.ts";
5-
6-import type {
7- Action,
8- ApiCtx,
9- ApiRequest,
10- Next,
11- PipeCtx,
12- RequiredApiRequest,
13-} from "./types.ts";
14-import { isObject, mergeRequest } from "./util.ts";
15-
16-/**
17- * This middleware will catch any errors in the pipeline
18- * and return the context object.
19- */
20-export function* errorHandler<Ctx extends PipeCtx = PipeCtx>(
21- ctx: Ctx,
22- next: Next,
23-) {
24- yield* next();
25-
26- if (!ctx.result.ok) {
27- console.error(
28- `Error: ${ctx.result.error.message}. Check the endpoint [${ctx.name}]`,
29- ctx,
30- );
31- console.error(ctx.result.error);
32- }
33-}
34-
35-/**
36- * This middleware sets up the context object with some values that are
37- * necessary for {@link createApi} to work properly.
38- */
39-export function* queryCtx<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
40- if (!ctx.req) {
41- ctx.req = (r?: ApiRequest): RequiredApiRequest =>
42- mergeRequest(ctx.request, r);
43- }
44- if (!ctx.request) ctx.request = ctx.req();
45- if (!ctx.response) ctx.response = null;
46- if (!ctx.json) ctx.json = { ok: false, data: {} };
47- if (!ctx.actions) ctx.actions = [];
48- if (!ctx.bodyType) ctx.bodyType = "json";
49- yield* next();
50-}
51-
52-/**
53- * This middleware converts the name provided to {@link createApi} into data for the fetch request.
54- */
55-export function* urlParser<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
56- const httpMethods = [
57- "get",
58- "head",
59- "post",
60- "put",
61- "delete",
62- "connect",
63- "options",
64- "trace",
65- "patch",
66- ];
67-
68- const options = ctx.payload || {};
69- if (!isObject(options)) {
70- yield* next();
71- return;
72- }
73-
74- let url = Object.keys(options).reduce((acc, key) => {
75- return acc.replace(`:${key}`, options[key]);
76- }, ctx.name);
77-
78- let method = "";
79- httpMethods.forEach((curMethod) => {
80- const pattern = new RegExp(`\\s*\\[` + curMethod + `\\]\\s*\\w*`, "i");
81- const tmpUrl = url.replace(pattern, "");
82- if (tmpUrl.length !== url.length) {
83- method = curMethod.toLocaleUpperCase();
84- }
85- url = tmpUrl;
86- }, url);
87-
88- ctx.request = ctx.req({ url });
89- if (method) {
90- ctx.request = ctx.req({ method });
91- }
92-
93- yield* next();
94-}
95-
96-/**
97- * This middleware allows the user to override the default key provided to every pipeline function
98- * and instead use whatever they want.
99- *
100- * @example
101- * ```ts
102- * import { createPipe } from 'starfx';
103- *
104- * const thunk = createPipe();
105- * thunk.use(customKey);
106- *
107- * const doit = thunk.create('some-action', function*(ctx, next) {
108- * ctx.key = 'something-i-want';
109- * })
110- * ```
111- */
112-export function* customKey<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
113- if (
114- ctx?.key &&
115- ctx?.action?.payload?.key &&
116- ctx.key !== ctx.action.payload.key
117- ) {
118- const newKey = ctx.name.split("|")[0] + "|" + ctx.key;
119- ctx.key = newKey;
120- ctx.action.payload.key = newKey;
121- }
122- yield* next();
123-}
124-
125-/**
126- * This middleware is a composition of many middleware used to faciliate the {@link createApi}
127- */
128-export function requestMonitor<Ctx extends ApiCtx = ApiCtx>() {
129- return compose<Ctx>([
130- errorHandler,
131- queryCtx,
132- urlParser,
133- customKey,
134- ]);
135-}
136-
137-export interface PerfCtx<P = unknown> extends PipeCtx<P> {
138- performance: number;
139-}
140-
141-/**
142- * This middleware will add `performance.now()` before and after your middleware pipeline.
143- */
144-export function* performanceMonitor<Ctx extends PerfCtx = PerfCtx>(
145- ctx: Ctx,
146- next: Next,
147-) {
148- const t0 = performance.now();
149- yield* next();
150- const t1 = performance.now();
151- ctx.performance = t1 - t0;
152-}
153-
154-/**
155- * This middleware will call the `saga` provided with the action sent to the middleware pipeline.
156- */
157-export function wrap<Ctx extends PipeCtx = PipeCtx, T = any>(
158- op: (a: Action) => Operator<T>,
159-) {
160- return function* (ctx: Ctx, next: Next) {
161- yield* call(function* () {
162- return op(ctx.action);
163- });
164- yield* next();
165- };
166-}
+34,
-3
1@@ -1,6 +1,37 @@
2-export * from "./pipe.ts";
3+import { createThunks } from "./thunk.ts";
4+export * from "./thunk.ts";
5 export * from "./api.ts";
6 export * from "./types.ts";
7-export * from "./fetch.ts";
8-export * from "./middleware.ts";
9 export * from "./create-key.ts";
10+import * as mdw from "./mdw.ts";
11+
12+export { mdw };
13+
14+/**
15+ * @deprecated Use {@link createThunks} instead;
16+ */
17+export const createPipe = createThunks;
18+/**
19+ * @deprecated Use {@link mdw.err} instead;
20+ */
21+export const errorHandler = mdw.err;
22+/**
23+ * @deprecated Use {@link mdw.query} instead;
24+ */
25+export const queryCtx = mdw.queryCtx;
26+/**
27+ * @deprecated Use {@link fetchMdw.composeUrl} instead;
28+ */
29+export const urlParser = mdw.composeUrl;
30+/**
31+ * @deprecated Use {@link mdw.customKey} instead;
32+ */
33+export const customKey = mdw.customKey;
34+/**
35+ * @deprecated Use {@link mdw.api} instead;
36+ */
37+export const requestMonitor = mdw.api;
38+/**
39+ * @deprecated Use {@link mdw.fetch} instead;
40+ */
41+export const fetcher = mdw.fetch;
+2,
-2
1@@ -11,7 +11,7 @@ import { Provider, sleep as delay, useSelector } from "../deps.ts";
2 import { configureStore, updateStore } from "../store/mod.ts";
3
4 import { createApi } from "./api.ts";
5-import { requestMonitor } from "./middleware.ts";
6+import * as mdw from "./middleware.ts";
7 import { useApi } from "./react.ts";
8 import { selectDataById } from "./slice.ts";
9 import { createKey } from "./create-key.ts";
10@@ -32,7 +32,7 @@ interface User {
11
12 const setupTest = async () => {
13 const api = createApi();
14- api.use(requestMonitor());
15+ api.use(mdw.api());
16 api.use(api.routes());
17 api.use(function* (ctx, next) {
18 yield* delay(10);
R query/pipe.test.ts =>
query/thunk.test.ts
+13,
-14
1@@ -4,9 +4,8 @@ import { configureStore, put, takeEvery } from "../store/mod.ts";
2 import { sleep as delay } from "../deps.ts";
3 import type { QueryState } from "../types.ts";
4 import { createQueryState } from "../action.ts";
5-
6 import { sleep } from "../test.ts";
7-import { createPipe } from "./pipe.ts";
8+import { createThunks } from "./thunk.ts";
9 import type { Next, PipeCtx } from "./types.ts";
10 import { updateStore } from "../store/fx.ts";
11
12@@ -121,13 +120,13 @@ function* processTickets(
13 yield* next();
14 }
15
16-const tests = describe("createPipe()");
17+const tests = describe("createThunks()");
18
19 it(
20 tests,
21 "when create a query fetch pipeline - execute all middleware and save to redux",
22 () => {
23- const api = createPipe<RoboCtx>();
24+ const api = createThunks<RoboCtx>();
25 api.use(api.routes());
26 api.use(convertNameToUrl);
27 api.use(onFetchApi);
28@@ -154,7 +153,7 @@ it(
29 tests,
30 "when providing a generator the to api.create function - should call that generator before all other middleware",
31 () => {
32- const api = createPipe<RoboCtx>();
33+ const api = createThunks<RoboCtx>();
34 api.use(api.routes());
35 api.use(convertNameToUrl);
36 api.use(onFetchApi);
37@@ -189,7 +188,7 @@ it(
38
39 it(tests, "error handling", () => {
40 let called;
41- const api = createPipe<RoboCtx>();
42+ const api = createThunks<RoboCtx>();
43 api.use(api.routes());
44 api.use(function* upstream(_, next) {
45 try {
46@@ -212,7 +211,7 @@ it(tests, "error handling", () => {
47
48 it(tests, "error handling inside create", () => {
49 let called = false;
50- const api = createPipe<RoboCtx>();
51+ const api = createThunks<RoboCtx>();
52 api.use(api.routes());
53 api.use(function* fail() {
54 throw new Error("some error");
55@@ -237,7 +236,7 @@ it(tests, "error handling inside create", () => {
56
57 it(tests, "error inside endpoint mdw", () => {
58 let called = false;
59- const query = createPipe();
60+ const query = createThunks();
61 query.use(function* (_, next) {
62 try {
63 yield* next();
64@@ -268,7 +267,7 @@ it(tests, "error inside endpoint mdw", () => {
65 });
66
67 it(tests, "create fn is an array", () => {
68- const api = createPipe<RoboCtx>();
69+ const api = createThunks<RoboCtx>();
70 api.use(api.routes());
71 api.use(function* (ctx, next) {
72 asserts.assertEquals(ctx.request, {
73@@ -298,7 +297,7 @@ it(tests, "create fn is an array", () => {
74 });
75
76 it(tests, "run() on endpoint action - should run the effect", () => {
77- const api = createPipe<RoboCtx>();
78+ const api = createThunks<RoboCtx>();
79 api.use(api.routes());
80 let acc = "";
81 const action1 = api.create(
82@@ -338,7 +337,7 @@ it(tests, "run() on endpoint action - should run the effect", () => {
83
84 it(tests, "middleware order of execution", async () => {
85 let acc = "";
86- const api = createPipe();
87+ const api = createThunks();
88 api.use(api.routes());
89
90 api.use(function* (_, next) {
91@@ -379,7 +378,7 @@ it(tests, "retry with actionFn", async () => {
92 let acc = "";
93 let called = false;
94
95- const api = createPipe();
96+ const api = createThunks();
97 api.use(api.routes());
98
99 const action = api.create(
100@@ -407,7 +406,7 @@ it(tests, "retry with actionFn", async () => {
101
102 it(tests, "retry with actionFn with payload", async () => {
103 let acc = "";
104- const api = createPipe();
105+ const api = createThunks();
106 api.use(api.routes());
107
108 api.use(function* (ctx: PipeCtx<{ page: number }>, next) {
109@@ -436,7 +435,7 @@ it(tests, "retry with actionFn with payload", async () => {
110 });
111
112 it(tests, "should only call thunk once", () => {
113- const api = createPipe<RoboCtx>();
114+ const api = createThunks<RoboCtx>();
115 api.use(api.routes());
116 let acc = "";
117
R query/pipe.ts =>
query/thunk.ts
+8,
-9
1@@ -1,6 +1,6 @@
2 import { compose } from "../compose.ts";
3 import type { Operator, Payload } from "../types.ts";
4-import { parallel } from "../mod.ts";
5+import { keepAlive } from "../mod.ts";
6
7 // TODO: remove store deps
8 import { takeEvery } from "../redux/mod.ts";
9@@ -24,7 +24,7 @@ import { Ok } from "../deps.ts";
10 export interface SagaApi<Ctx extends PipeCtx> {
11 use: (fn: Middleware<Ctx>) => void;
12 routes: () => Middleware<Ctx>;
13- bootup: Operator<unknown>;
14+ bootup: Operator<void>;
15
16 /**
17 * Name only
18@@ -94,17 +94,17 @@ export interface SagaApi<Ctx extends PipeCtx> {
19 *
20 * @example
21 * ```ts
22- * import { createPipe } from 'starfx';
23+ * import { createThunks } from 'starfx';
24 *
25- * const thunk = createPipe();
26- * thunk.use(function* (ctx, next) {
27+ * const thunks = createThunks();
28+ * thunks.use(function* (ctx, next) {
29 * console.log('beginning');
30 * yield* next();
31 * console.log('end');
32 * });
33 * thunks.use(thunks.routes());
34 *
35- * const doit = thunk.create('do-something', function*(ctx, next) {
36+ * const doit = thunks.create('do-something', function*(ctx, next) {
37 * console.log('middle');
38 * yield* next();
39 * console.log('middle end');
40@@ -119,7 +119,7 @@ export interface SagaApi<Ctx extends PipeCtx> {
41 * // end
42 * ```
43 */
44-export function createPipe<Ctx extends PipeCtx = PipeCtx<any>>(
45+export function createThunks<Ctx extends PipeCtx = PipeCtx<any>>(
46 {
47 supervisor = takeEvery,
48 }: {
49@@ -215,8 +215,7 @@ export function createPipe<Ctx extends PipeCtx = PipeCtx<any>>(
50 }
51
52 function* bootup() {
53- const group = yield* parallel(Object.values(visors));
54- return yield* group;
55+ yield* keepAlive(Object.values(visors));
56 }
57
58 function routes() {
+23,
-15
1@@ -19,19 +19,23 @@ export interface LoaderCtx<P = unknown> extends PipeCtx<P> {
2 loader: Partial<LoaderItemState> | null;
3 }
4
5-export interface ApiFetchSuccess<ApiSuccess = any> {
6- ok: true;
7- data: ApiSuccess;
8-}
9-
10-export interface ApiFetchError<ApiError = any> {
11- ok: false;
12- data: ApiError;
13-}
14-
15-export type ApiFetchResponse<ApiSuccess = any, ApiError = any> =
16- | ApiFetchSuccess<ApiSuccess>
17- | ApiFetchError<ApiError>;
18+export type ApiFetchResult<ApiSuccess = any, ApiError = any> =
19+ | {
20+ ok: true;
21+ value: ApiSuccess;
22+ /**
23+ * @deprecated Use {@link ApiFetchResult.value} instead.
24+ */
25+ data: ApiSuccess;
26+ }
27+ | {
28+ ok: false;
29+ error: ApiError;
30+ /**
31+ * @deprecated Use {@link ApiFetchResult.error} instead.
32+ */
33+ data: ApiError;
34+ };
35
36 export type ApiRequest = Partial<{ url: string } & RequestInit>;
37 export type RequiredApiRequest = {
38@@ -47,7 +51,7 @@ export interface FetchCtx<P = any> extends PipeCtx<P> {
39 }
40
41 export interface FetchJson<ApiSuccess = any, ApiError = any> {
42- json: ApiFetchResponse<ApiSuccess, ApiError>;
43+ json: ApiFetchResult<ApiSuccess, ApiError>;
44 }
45
46 export interface FetchJsonCtx<P = any, ApiSuccess = any, ApiError = any>
47@@ -61,6 +65,10 @@ export interface ApiCtx<Payload = any, ApiSuccess = any, ApiError = any>
48 cacheData: any;
49 }
50
51+export interface PerfCtx<P = unknown> extends PipeCtx<P> {
52+ performance: number;
53+}
54+
55 export type Middleware<Ctx extends PipeCtx = PipeCtx> = (
56 ctx: Ctx,
57 next: Next,
58@@ -77,7 +85,7 @@ export type MiddlewareApiCo<Ctx extends ApiCtx = ApiCtx> =
59 | Middleware<Ctx>
60 | Middleware<Ctx>[];
61
62-export type Next = () => Operation<unknown>;
63+export type Next = () => Operation<void>;
64
65 export interface Action {
66 type: string;
+11,
-4
1@@ -65,7 +65,7 @@ export function* dispatchActions(ctx: { actions: AnyAction[] }, next: Next) {
2
3 /**
4 * This middleware will automatically cache any data found inside `ctx.json`
5- * which is where we store JSON data from the `fetcher` middleware.
6+ * which is where we store JSON data from the {@link mdw.fetch} middleware.
7 */
8 export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
9 ctx: Ctx,
10@@ -76,8 +76,11 @@ export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
11 );
12 yield* next();
13 if (!ctx.cache) return;
14- const { data } = ctx.json;
15- yield* put(addData({ [ctx.key]: data }));
16+ if (ctx.json.ok) {
17+ yield* put(addData({ [ctx.key]: ctx.json.value }));
18+ } else {
19+ yield* put(addData({ [ctx.key]: ctx.json.error }));
20+ }
21 ctx.cacheData = data;
22 }
23
24@@ -85,7 +88,11 @@ export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
25 * This middleware will track the status of a fetch request.
26 */
27 export function loadingMonitor<Ctx extends ApiCtx = ApiCtx>(
28- errorFn: (ctx: Ctx) => string = (ctx) => ctx.json?.data?.message || "",
29+ errorFn: (ctx: Ctx) => string = (ctx) => {
30+ const jso = ctx.json;
31+ if (jso.ok) return "";
32+ return jso.error?.message || "";
33+ },
34 ) {
35 return function* trackLoading(ctx: Ctx, next: Next) {
36 yield* put([
+12,
-3
1@@ -23,7 +23,7 @@ export function storeMdw<
2
3 /**
4 * This middleware will automatically cache any data found inside `ctx.json`
5- * which is where we store JSON data from the `fetcher` middleware.
6+ * which is where we store JSON data from the {@link mdw.fetch} middleware.
7 */
8 export function simpleCache<Ctx extends ApiCtx = ApiCtx>(
9 dataSchema: TableOutput<any, AnyState>,
10@@ -35,7 +35,12 @@ export function simpleCache<Ctx extends ApiCtx = ApiCtx>(
11 ctx.cacheData = yield* select(dataSchema.selectById, { id: ctx.key });
12 yield* next();
13 if (!ctx.cache) return;
14- const { data } = ctx.json;
15+ let data;
16+ if (ctx.json.ok) {
17+ data = ctx.json.value;
18+ } else {
19+ data = ctx.json.error;
20+ }
21 yield* updateStore(dataSchema.add({ [ctx.key]: data }));
22 ctx.cacheData = data;
23 };
24@@ -64,7 +69,11 @@ export function loadingMonitor<
25 M extends AnyState = AnyState,
26 >(
27 loaderSchema: LoaderOutput<M, AnyState>,
28- errorFn: (ctx: Ctx) => string = (ctx) => ctx.json?.data?.message || "",
29+ errorFn: (ctx: Ctx) => string = (ctx) => {
30+ const jso = ctx.json;
31+ if (jso.ok) return "";
32+ return jso.error?.message || "";
33+ },
34 ) {
35 return function* trackLoading(ctx: Ctx, next: Next) {
36 yield* updateStore([
+8,
-1
1@@ -15,6 +15,7 @@ import { Next } from "../query/types.ts";
2 import type { FxStore, Listener, StoreUpdater, UpdaterCtx } from "./types.ts";
3 import { ActionContext, StoreContext, StoreUpdateContext } from "./context.ts";
4 import { put } from "./fx.ts";
5+import { log } from "../log.ts";
6
7 const stubMsg = "This is merely a stub, not implemented";
8
9@@ -120,7 +121,13 @@ export function createStore<S extends AnyState>({
10 yield* mdw(ctx);
11 // TODO: dev mode only?
12 if (!ctx.result.ok) {
13- console.error(ctx.result);
14+ yield* log({
15+ type: "store:update",
16+ payload: {
17+ message: `Exception raised when calling store updaters`,
18+ error: ctx.result.error,
19+ },
20+ });
21 }
22 return ctx;
23 }