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