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}