repos / starfx

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

starfx / src / store / slice
Eric Bower  ·  2025-06-06

loaders.ts

  1import { createSelector } from "reselect";
  2import type {
  3  AnyState,
  4  LoaderItemState,
  5  LoaderPayload,
  6  LoaderState,
  7} from "../../types.js";
  8import type { BaseSchema } from "../types.js";
  9
 10interface PropId {
 11  id: string;
 12}
 13
 14interface PropIds {
 15  ids: string[];
 16}
 17
 18const excludesFalse = <T>(n?: T): n is T => Boolean(n);
 19
 20export function defaultLoaderItem<M extends AnyState = AnyState>(
 21  li: Partial<LoaderItemState<M>> = {},
 22): LoaderItemState<M> {
 23  return {
 24    id: "",
 25    status: "idle",
 26    message: "",
 27    lastRun: 0,
 28    lastSuccess: 0,
 29    meta: {} as M,
 30    ...li,
 31  };
 32}
 33
 34export function defaultLoader<M extends AnyState = AnyState>(
 35  l: Partial<LoaderItemState<M>> = {},
 36): LoaderState<M> {
 37  const loading = defaultLoaderItem(l);
 38  return {
 39    ...loading,
 40    isIdle: loading.status === "idle",
 41    isError: loading.status === "error",
 42    isSuccess: loading.status === "success",
 43    isLoading: loading.status === "loading",
 44    isInitialLoading:
 45      (loading.status === "idle" || loading.status === "loading") &&
 46      loading.lastSuccess === 0,
 47  };
 48}
 49
 50interface LoaderSelectors<
 51  M extends AnyState = AnyState,
 52  S extends AnyState = AnyState,
 53> {
 54  findById: (
 55    d: Record<string, LoaderItemState<M>>,
 56    { id }: PropId,
 57  ) => LoaderState<M>;
 58  findByIds: (
 59    d: Record<string, LoaderItemState<M>>,
 60    { ids }: PropIds,
 61  ) => LoaderState<M>[];
 62  selectTable: (s: S) => Record<string, LoaderItemState<M>>;
 63  selectTableAsList: (state: S) => LoaderItemState<M>[];
 64  selectById: (s: S, p: PropId) => LoaderState<M>;
 65  selectByIds: (s: S, p: PropIds) => LoaderState<M>[];
 66}
 67
 68function loaderSelectors<
 69  M extends AnyState = AnyState,
 70  S extends AnyState = AnyState,
 71>(
 72  selectTable: (s: S) => Record<string, LoaderItemState<M>>,
 73): LoaderSelectors<M, S> {
 74  const empty = defaultLoader();
 75  const tableAsList = (
 76    data: Record<string, LoaderItemState<M>>,
 77  ): LoaderItemState<M>[] => Object.values(data).filter(excludesFalse);
 78
 79  const findById = (data: Record<string, LoaderItemState<M>>, { id }: PropId) =>
 80    defaultLoader<M>(data[id]) || empty;
 81  const findByIds = (
 82    data: Record<string, LoaderItemState<M>>,
 83    { ids }: PropIds,
 84  ): LoaderState<M>[] =>
 85    ids.map((id) => defaultLoader<M>(data[id])).filter(excludesFalse);
 86  const selectById = createSelector(
 87    selectTable,
 88    (_: S, p: PropId) => p.id,
 89    (loaders, id): LoaderState<M> => findById(loaders, { id }),
 90  );
 91
 92  return {
 93    findById,
 94    findByIds,
 95    selectTable,
 96    selectTableAsList: createSelector(
 97      selectTable,
 98      (data): LoaderItemState<M>[] => tableAsList(data),
 99    ),
100    selectById,
101    selectByIds: createSelector(
102      selectTable,
103      (_: S, p: PropIds) => p.ids,
104      (loaders, ids) => findByIds(loaders, { ids }),
105    ),
106  };
107}
108
109export interface LoaderOutput<
110  M extends Record<string, unknown>,
111  S extends AnyState,
112> extends LoaderSelectors<M, S>,
113    BaseSchema<Record<string, LoaderItemState<M>>> {
114  schema: "loader";
115  initialState: Record<string, LoaderItemState<M>>;
116  start: (e: LoaderPayload<M>) => (s: S) => void;
117  success: (e: LoaderPayload<M>) => (s: S) => void;
118  error: (e: LoaderPayload<M>) => (s: S) => void;
119  reset: () => (s: S) => void;
120  resetByIds: (ids: string[]) => (s: S) => void;
121}
122
123const ts = () => new Date().getTime();
124
125export const createLoaders = <
126  M extends AnyState = AnyState,
127  S extends AnyState = AnyState,
128>({
129  name,
130  initialState = {},
131}: {
132  name: keyof S;
133  initialState?: Record<string, LoaderItemState<M>>;
134}): LoaderOutput<M, S> => {
135  const selectors = loaderSelectors<M, S>((s: S) => s[name]);
136
137  return {
138    schema: "loader",
139    name: name as string,
140    initialState,
141    start: (e) => (s) => {
142      const table = selectors.selectTable(s);
143      const loader = table[e.id];
144      table[e.id] = defaultLoaderItem({
145        ...loader,
146        ...e,
147        status: "loading",
148        lastRun: ts(),
149      });
150    },
151    success: (e) => (s) => {
152      const table = selectors.selectTable(s);
153      const loader = table[e.id];
154      table[e.id] = defaultLoaderItem({
155        ...loader,
156        ...e,
157        status: "success",
158        lastSuccess: ts(),
159      });
160    },
161    error: (e) => (s) => {
162      const table = selectors.selectTable(s);
163      const loader = table[e.id];
164      table[e.id] = defaultLoaderItem({
165        ...loader,
166        ...e,
167        status: "error",
168      });
169    },
170    reset: () => (s) => {
171      // deno-lint-ignore no-explicit-any
172      (s as any)[name] = initialState;
173    },
174    resetByIds: (ids: string[]) => (s) => {
175      const table = selectors.selectTable(s);
176      ids.forEach((id) => {
177        delete table[id];
178      });
179    },
180    ...selectors,
181  };
182};
183
184export function loaders<M extends AnyState = AnyState>(
185  initialState?: Record<string, LoaderItemState<M>>,
186) {
187  return (name: string) => createLoaders<M>({ name, initialState });
188}