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}