repos / starfx

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

starfx / store / slice
Eric Bower · 23 Feb 24

loaders.ts

  1import { createSelector } from "../../deps.ts";
  2import type {
  3  AnyState,
  4  LoaderItemState,
  5  LoaderPayload,
  6  LoaderState,
  7} from "../../types.ts";
  8import { BaseSchema } from "../types.ts";
  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<
 21  M extends AnyState = AnyState,
 22>(li: Partial<LoaderItemState<M>> = {}): 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 = (
 80    data: Record<string, LoaderItemState<M>>,
 81    { id }: PropId,
 82  ) => (defaultLoader<M>(data[id]) || empty);
 83  const findByIds = (
 84    data: Record<string, LoaderItemState<M>>,
 85    { ids }: PropIds,
 86  ): LoaderState<M>[] =>
 87    ids.map((id) => defaultLoader<M>(data[id])).filter(excludesFalse);
 88  const selectById = createSelector(
 89    selectTable,
 90    (_: S, p: PropId) => p.id,
 91    (loaders, id): LoaderState<M> => findById(loaders, { id }),
 92  );
 93
 94  return {
 95    findById,
 96    findByIds,
 97    selectTable,
 98    selectTableAsList: createSelector(
 99      selectTable,
100      (data): LoaderItemState<M>[] => tableAsList(data),
101    ),
102    selectById,
103    selectByIds: createSelector(
104      selectTable,
105      (_: S, p: PropIds) => p.ids,
106      (loaders, ids) => findByIds(loaders, { ids }),
107    ),
108  };
109}
110
111export interface LoaderOutput<
112  M extends Record<string, unknown>,
113  S extends AnyState,
114> extends
115  LoaderSelectors<M, S>,
116  BaseSchema<Record<string, LoaderItemState<M>>> {
117  schema: "loader";
118  initialState: Record<string, LoaderItemState<M>>;
119  start: (e: LoaderPayload<M>) => (s: S) => void;
120  success: (e: LoaderPayload<M>) => (s: S) => void;
121  error: (e: LoaderPayload<M>) => (s: S) => void;
122  reset: () => (s: S) => void;
123  resetByIds: (ids: string[]) => (s: S) => void;
124}
125
126const ts = () => new Date().getTime();
127
128export const createLoaders = <
129  M extends AnyState = AnyState,
130  S extends AnyState = AnyState,
131>({
132  name,
133  initialState = {},
134}: {
135  name: keyof S;
136  initialState?: Record<string, LoaderItemState<M>>;
137}): LoaderOutput<M, S> => {
138  const selectors = loaderSelectors<M, S>((s: S) => s[name]);
139
140  return {
141    schema: "loader",
142    name: name as string,
143    initialState,
144    start: (e) => (s) => {
145      const table = selectors.selectTable(s);
146      const loader = table[e.id];
147      table[e.id] = defaultLoaderItem({
148        ...loader,
149        ...e,
150        status: "loading",
151        lastRun: ts(),
152      });
153    },
154    success: (e) => (s) => {
155      const table = selectors.selectTable(s);
156      const loader = table[e.id];
157      table[e.id] = defaultLoaderItem({
158        ...loader,
159        ...e,
160        status: "success",
161        lastSuccess: ts(),
162      });
163    },
164    error: (e) => (s) => {
165      const table = selectors.selectTable(s);
166      const loader = table[e.id];
167      table[e.id] = defaultLoaderItem({
168        ...loader,
169        ...e,
170        status: "error",
171      });
172    },
173    reset: () => (s) => {
174      // deno-lint-ignore no-explicit-any
175      (s as any)[name] = initialState;
176    },
177    resetByIds: (ids: string[]) => (s) => {
178      const table = selectors.selectTable(s);
179      ids.forEach((id) => {
180        delete table[id];
181      });
182    },
183    ...selectors,
184  };
185};
186
187export function loaders<
188  M extends AnyState = AnyState,
189>(initialState?: Record<string, LoaderItemState<M>>) {
190  return (name: string) => createLoaders<M>({ name, initialState });
191}