Eric Bower
·
2025-06-06
query.ts
1import { type Callable, call } from "effection";
2import { put } from "../action.js";
3import { compose } from "../compose.js";
4import { safe } from "../fx/index.js";
5import type {
6 ApiCtx,
7 ApiRequest,
8 FetchJsonCtx,
9 MiddlewareApi,
10 PerfCtx,
11 RequiredApiRequest,
12 ThunkCtx,
13} from "../query/types.js";
14import { mergeRequest } from "../query/util.js";
15import type { AnyAction, Next } from "../types.js";
16import * as fetchMdw from "./fetch.js";
17export * from "./fetch.js";
18
19/**
20 * This middleware will catch any errors in the pipeline
21 * and `console.error` the context object.
22 *
23 * You are highly encouraged to ditch this middleware if you need something
24 * more custom.
25 *
26 * It also sets `ctx.result` which informs us whether the entire
27 * middleware pipeline succeeded or not. Think the `.catch()` case for
28 * `window.fetch`.
29 */
30export function* err<Ctx extends ThunkCtx = ThunkCtx>(ctx: Ctx, next: Next) {
31 ctx.result = yield* safe(next);
32 if (!ctx.result.ok) {
33 const message = `Error: ${ctx.result.error.message}. Check the endpoint [${ctx.name}]`;
34 console.error(message, ctx);
35 yield* put({
36 type: "error:query",
37 payload: {
38 message,
39 ctx,
40 },
41 });
42 }
43}
44
45/**
46 * This middleware allows the user to override the default key provided
47 * to every pipeline function and instead use whatever they want.
48 *
49 * @example
50 * ```ts
51 * import { createPipe } from 'starfx';
52 *
53 * const thunk = createPipe();
54 * thunk.use(customKey);
55 *
56 * const doit = thunk.create('some-action', function*(ctx, next) {
57 * ctx.key = 'something-i-want';
58 * })
59 * ```
60 */
61export function* customKey<Ctx extends ThunkCtx = ThunkCtx>(
62 ctx: Ctx,
63 next: Next,
64) {
65 if (
66 ctx?.key &&
67 ctx?.action?.payload?.key &&
68 ctx.key !== ctx.action.payload.key
69 ) {
70 const newKey = `${ctx.name.split("|")[0]}|${ctx.key}`;
71 ctx.key = newKey;
72 ctx.action.payload.key = newKey;
73 }
74 yield* next();
75}
76
77/**
78 * This middleware sets up the context object with some values that are
79 * necessary for {@link createApi} to work properly.
80 */
81export function* queryCtx<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
82 if (!ctx.req) {
83 ctx.req = (r?: ApiRequest): RequiredApiRequest =>
84 mergeRequest(ctx.request, r);
85 }
86 if (!ctx.request) ctx.request = ctx.req();
87 if (!ctx.response) ctx.response = null;
88 if (!ctx.json) ctx.json = { ok: false, error: {} };
89 if (!ctx.actions) ctx.actions = [];
90 if (!ctx.bodyType) ctx.bodyType = "json";
91 yield* next();
92}
93
94/**
95 * This middleware will take the result of `ctx.actions` and dispatch them
96 * as a single batch.
97 *
98 * @remarks This is useful because sometimes there are a lot of actions that need dispatched
99 * within the pipeline of the middleware and instead of dispatching them serially this
100 * improves performance by only hitting the reducers once.
101 */
102export function* actions(ctx: { actions: AnyAction[] }, next: Next) {
103 if (!ctx.actions) ctx.actions = [];
104 yield* next();
105 if (ctx.actions.length === 0) return;
106 yield* put(ctx.actions);
107}
108
109/**
110 * This middleware will add `performance.now()` before and after your
111 * middleware pipeline.
112 */
113export function* perf<Ctx extends PerfCtx = PerfCtx>(ctx: Ctx, next: Next) {
114 const t0 = performance.now();
115 yield* next();
116 const t1 = performance.now();
117 ctx.performance = t1 - t0;
118}
119
120/**
121 * This middleware is a composition of other middleware required to
122 * use `window.fetch` {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API}
123 * with {@link createApi}
124 */
125export function fetch<CurCtx extends FetchJsonCtx = FetchJsonCtx>(
126 {
127 baseUrl = "",
128 }: {
129 baseUrl?: string;
130 } = { baseUrl: "" },
131) {
132 return compose<CurCtx>([
133 fetchMdw.composeUrl(baseUrl),
134 fetchMdw.payload,
135 fetchMdw.request,
136 fetchMdw.json,
137 ]);
138}
139
140/**
141 * This middleware will only be activated if predicate is true.
142 */
143export function predicate<Ctx extends ApiCtx = ApiCtx>(
144 predicate: ((ctx: Ctx) => boolean) | ((ctx: Ctx) => Callable<boolean>),
145) {
146 return (mdw: MiddlewareApi) => {
147 return function* (ctx: Ctx, next: Next) {
148 const valid = yield* call(() => predicate(ctx));
149 if (!valid) {
150 yield* next();
151 return;
152 }
153
154 yield* mdw(ctx, next);
155 };
156 };
157}