repos / starfx

supercharged async flow control library.
git clone https://github.com/neurosnap/starfx.git

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