repos / starfx

a micro-mvc framework for react apps
git clone https://github.com/neurosnap/starfx.git

starfx / src / mdw
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}