Eric Bower
·
2025-06-06
store.ts
1import { compose } from "../compose.js";
2import type { ApiCtx, ThunkCtxWLoader } from "../query/index.js";
3import {
4 type LoaderOutput,
5 type TableOutput,
6 select,
7 updateStore,
8} from "../store/index.js";
9import type { AnyState, Next } from "../types.js";
10import { nameParser } from "./fetch.js";
11import { actions, customKey, err, queryCtx } from "./query.js";
12
13export interface ApiMdwProps<
14 Ctx extends ApiCtx = ApiCtx,
15 M extends AnyState = AnyState,
16> {
17 schema: {
18 loaders: LoaderOutput<M, AnyState>;
19 cache: TableOutput<any, AnyState>;
20 };
21 errorFn?: (ctx: Ctx) => string;
22}
23
24interface ErrorLike {
25 message: string;
26}
27
28function isErrorLike(err: unknown): err is ErrorLike {
29 return typeof err === "object" && err !== null && "message" in err;
30}
31
32/**
33 * This middleware is a composition of many middleware used to faciliate
34 * the {@link createApi}.
35 *
36 * It is not required, however, it is battle-tested and highly recommended.
37 *
38 * List of mdw:
39 * - {@link mdw.err}
40 * - {@link mdw.actions}
41 * - {@link mdw.queryCtx}
42 * - {@link mdw.customKey}
43 * - {@link mdw.nameParser}
44 * - {@link mdw.loaderApi}
45 * - {@link mdw.cache}
46 */
47export function api<Ctx extends ApiCtx = ApiCtx, S extends AnyState = AnyState>(
48 props: ApiMdwProps<Ctx, S>,
49) {
50 return compose<Ctx>([
51 err,
52 actions,
53 queryCtx,
54 customKey,
55 nameParser,
56 loaderApi(props),
57 cache(props.schema),
58 ]);
59}
60
61/**
62 * This middleware will automatically cache any data found inside `ctx.json`
63 * which is where we store JSON data from the {@link mdw.fetch} middleware.
64 */
65export function cache<Ctx extends ApiCtx = ApiCtx>(schema: {
66 cache: TableOutput<any, AnyState>;
67}) {
68 return function* cache(ctx: Ctx, next: Next) {
69 ctx.cacheData = yield* select(schema.cache.selectById, { id: ctx.key });
70 yield* next();
71 if (!ctx.cache) return;
72 let data;
73 if (ctx.json.ok) {
74 data = ctx.json.value;
75 } else {
76 data = ctx.json.error;
77 }
78 yield* updateStore(schema.cache.add({ [ctx.key]: data }));
79 ctx.cacheData = data;
80 };
81}
82
83/**
84 * This middleware will track the status of a middleware fn
85 */
86export function loader<M extends AnyState = AnyState>(schema: {
87 loaders: LoaderOutput<M, AnyState>;
88}) {
89 return function* <Ctx extends ThunkCtxWLoader = ThunkCtxWLoader>(
90 ctx: Ctx,
91 next: Next,
92 ) {
93 yield* updateStore([
94 schema.loaders.start({ id: ctx.name }),
95 schema.loaders.start({ id: ctx.key }),
96 ]);
97
98 if (!ctx.loader) ctx.loader = {} as any;
99
100 try {
101 yield* next();
102
103 if (!ctx.loader) {
104 ctx.loader = {};
105 }
106
107 yield* updateStore([
108 schema.loaders.success({ id: ctx.name, ...ctx.loader }),
109 schema.loaders.success({ id: ctx.key, ...ctx.loader }),
110 ]);
111 } catch (err) {
112 if (!ctx.loader) {
113 ctx.loader = {};
114 }
115
116 const message = isErrorLike(err) ? err.message : "unknown exception";
117 yield* updateStore([
118 schema.loaders.error({
119 id: ctx.name,
120 message,
121 ...ctx.loader,
122 }),
123 schema.loaders.error({
124 id: ctx.key,
125 message,
126 ...ctx.loader,
127 }),
128 ]);
129 } finally {
130 const loaders = yield* select((s: any) =>
131 schema.loaders.selectByIds(s, { ids: [ctx.name, ctx.key] }),
132 );
133 const ids = loaders
134 .filter((loader) => loader.status === "loading")
135 .map((loader) => loader.id);
136
137 if (ids.length > 0) {
138 yield* updateStore(schema.loaders.resetByIds(ids));
139 }
140 }
141 };
142}
143
144function defaultErrorFn<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx) {
145 const jso = ctx.json;
146 if (jso.ok) return "";
147 return jso.error?.message || "";
148}
149
150/**
151 * This middleware will track the status of a fetch request.
152 */
153export function loaderApi<
154 Ctx extends ApiCtx = ApiCtx,
155 S extends AnyState = AnyState,
156>({ schema, errorFn = defaultErrorFn }: ApiMdwProps<Ctx, S>) {
157 return function* trackLoading(ctx: Ctx, next: Next) {
158 try {
159 yield* updateStore([
160 schema.loaders.start({ id: ctx.name }),
161 schema.loaders.start({ id: ctx.key }),
162 ]);
163 if (!ctx.loader) ctx.loader = {} as any;
164
165 yield* next();
166
167 if (!ctx.response) {
168 yield* updateStore(schema.loaders.resetByIds([ctx.name, ctx.key]));
169 return;
170 }
171
172 if (!ctx.loader) {
173 ctx.loader = {};
174 }
175
176 if (!ctx.response.ok) {
177 yield* updateStore([
178 schema.loaders.error({
179 id: ctx.name,
180 message: errorFn(ctx),
181 ...ctx.loader,
182 }),
183 schema.loaders.error({
184 id: ctx.key,
185 message: errorFn(ctx),
186 ...ctx.loader,
187 }),
188 ]);
189 return;
190 }
191
192 yield* updateStore([
193 schema.loaders.success({ id: ctx.name, ...ctx.loader }),
194 schema.loaders.success({ id: ctx.key, ...ctx.loader }),
195 ]);
196 } catch (err) {
197 const message = isErrorLike(err) ? err.message : "unknown exception";
198 yield* updateStore([
199 schema.loaders.error({
200 id: ctx.name,
201 message,
202 ...ctx.loader,
203 }),
204 schema.loaders.error({
205 id: ctx.key,
206 message,
207 ...ctx.loader,
208 }),
209 ]);
210 } finally {
211 const loaders = yield* select((s: any) =>
212 schema.loaders.selectByIds(s, { ids: [ctx.name, ctx.key] }),
213 );
214 const ids = loaders
215 .filter((loader) => loader.status === "loading")
216 .map((loader) => loader.id);
217 yield* updateStore(schema.loaders.resetByIds(ids));
218 }
219 };
220}