repos / starfx

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

starfx / store / slice
Eric Bower · 03 Jan 24

table.ts

  1import { createSelector } from "../../deps.ts";
  2import type { AnyState, IdProp } from "../../types.ts";
  3import { BaseSchema } from "../types.ts";
  4
  5interface PropId {
  6  id: IdProp;
  7}
  8
  9interface PropIds {
 10  ids: IdProp[];
 11}
 12
 13interface PatchEntity<T> {
 14  [key: string]: Partial<T[keyof T]>;
 15}
 16
 17const excludesFalse = <T>(n?: T): n is T => Boolean(n);
 18
 19function mustSelectEntity<Entity extends AnyState = AnyState>(
 20  defaultEntity: Entity | (() => Entity),
 21) {
 22  const isFn = typeof defaultEntity === "function";
 23
 24  return function selectEntity<S extends AnyState = AnyState>(
 25    selectById: (s: S, p: PropId) => Entity | undefined,
 26  ) {
 27    return (state: S, { id }: PropId): Entity => {
 28      if (isFn) {
 29        const entity = defaultEntity as () => Entity;
 30        return selectById(state, { id }) || entity();
 31      }
 32
 33      return selectById(state, { id }) || (defaultEntity as Entity);
 34    };
 35  };
 36}
 37
 38function tableSelectors<
 39  Entity extends AnyState = AnyState,
 40  S extends AnyState = AnyState,
 41>(
 42  selectTable: (s: S) => Record<IdProp, Entity>,
 43  empty?: Entity | (() => Entity) | undefined,
 44) {
 45  const must = empty ? mustSelectEntity(empty) : null;
 46  const tableAsList = (data: Record<IdProp, Entity>): Entity[] =>
 47    Object.values(data).filter(excludesFalse);
 48  const findById = (data: Record<IdProp, Entity>, { id }: PropId) => data[id];
 49  const findByIds = (
 50    data: Record<IdProp, Entity>,
 51    { ids }: PropIds,
 52  ): Entity[] => ids.map((id) => data[id]).filter(excludesFalse);
 53  const selectById = (
 54    state: S,
 55    { id }: PropId,
 56  ): typeof empty extends undefined ? (Entity | undefined) : Entity => {
 57    const data = selectTable(state);
 58    return findById(data, { id });
 59  };
 60
 61  const sbi = must ? must(selectById) : selectById;
 62
 63  return {
 64    findById: must ? must(findById) : findById,
 65    findByIds,
 66    tableAsList,
 67    selectTable,
 68    selectTableAsList: createSelector(
 69      selectTable,
 70      (data): Entity[] => tableAsList(data),
 71    ),
 72    selectById: sbi,
 73    selectByIds: createSelector(
 74      selectTable,
 75      (_: S, p: PropIds) => p.ids,
 76      (data, ids) => findByIds(data, { ids }),
 77    ),
 78  };
 79}
 80
 81export interface TableOutput<
 82  Entity extends AnyState,
 83  S extends AnyState,
 84  Empty extends Entity | undefined = Entity | undefined,
 85> extends BaseSchema<Record<IdProp, Entity>> {
 86  schema: "table";
 87  initialState: Record<IdProp, Entity>;
 88  empty: Empty;
 89  add: (e: Record<IdProp, Entity>) => (s: S) => void;
 90  set: (e: Record<IdProp, Entity>) => (s: S) => void;
 91  remove: (ids: IdProp[]) => (s: S) => void;
 92  patch: (e: PatchEntity<Record<IdProp, Entity>>) => (s: S) => void;
 93  merge: (e: PatchEntity<Record<IdProp, Entity>>) => (s: S) => void;
 94  reset: () => (s: S) => void;
 95  findById: (
 96    d: Record<IdProp, Entity>,
 97    { id }: PropId,
 98  ) => Empty;
 99  findByIds: (d: Record<IdProp, Entity>, { ids }: PropIds) => Entity[];
100  tableAsList: (d: Record<IdProp, Entity>) => Entity[];
101  selectTable: (s: S) => Record<IdProp, Entity>;
102  selectTableAsList: (state: S) => Entity[];
103  selectById: (
104    s: S,
105    p: PropId,
106  ) => Empty;
107  selectByIds: (s: S, p: PropIds) => Entity[];
108}
109
110export function createTable<
111  Entity extends AnyState = AnyState,
112  S extends AnyState = AnyState,
113>(p: {
114  name: keyof S;
115  initialState?: Record<IdProp, Entity>;
116  empty: Entity | (() => Entity);
117}): TableOutput<Entity, S, Entity>;
118export function createTable<
119  Entity extends AnyState = AnyState,
120  S extends AnyState = AnyState,
121>(p: {
122  name: keyof S;
123  initialState?: Record<IdProp, Entity>;
124  empty?: Entity | (() => Entity);
125}): TableOutput<Entity, S, Entity | undefined>;
126export function createTable<
127  Entity extends AnyState = AnyState,
128  S extends AnyState = AnyState,
129>({
130  name,
131  empty,
132  initialState = {},
133}: {
134  name: keyof S;
135  initialState?: Record<IdProp, Entity>;
136  empty?: Entity | (() => Entity);
137}): TableOutput<Entity, S, Entity | undefined> {
138  const selectors = tableSelectors<Entity, S>((s: S) => s[name], empty);
139
140  return {
141    schema: "table",
142    name: name as string,
143    initialState,
144    empty: typeof empty === "function" ? empty() : empty,
145    add: (entities) => (s) => {
146      const state = selectors.selectTable(s);
147      Object.keys(entities).forEach((id) => {
148        state[id] = entities[id];
149      });
150    },
151    set: (entities) => (s) => {
152      // deno-lint-ignore no-explicit-any
153      (s as any)[name] = entities;
154    },
155    remove: (ids) => (s) => {
156      const state = selectors.selectTable(s);
157      ids.forEach((id) => {
158        delete state[id];
159      });
160    },
161    patch: (entities) => (s) => {
162      const state = selectors.selectTable(s);
163      Object.keys(entities).forEach((id) => {
164        state[id] = { ...state[id], ...entities[id] };
165      });
166    },
167    merge: (entities) => (s) => {
168      const state = selectors.selectTable(s);
169      Object.keys(entities).forEach((id) => {
170        const entity = entities[id];
171        Object.keys(entity).forEach((prop) => {
172          const val = entity[prop];
173          if (Array.isArray(val)) {
174            // deno-lint-ignore no-explicit-any
175            const list = val as any[];
176            // deno-lint-ignore no-explicit-any
177            (state as any)[id][prop].push(...list);
178          } else {
179            // deno-lint-ignore no-explicit-any
180            (state as any)[id][prop] = entities[id][prop];
181          }
182        });
183      });
184    },
185    reset: () => (s) => {
186      // deno-lint-ignore no-explicit-any
187      (s as any)[name] = initialState;
188    },
189    ...selectors,
190  };
191}
192
193export function table<
194  Entity extends AnyState = AnyState,
195  S extends AnyState = AnyState,
196>(p: {
197  initialState?: Record<IdProp, Entity>;
198  empty: Entity | (() => Entity);
199}): (n: string) => TableOutput<Entity, S, Entity>;
200export function table<
201  Entity extends AnyState = AnyState,
202  S extends AnyState = AnyState,
203>(p?: {
204  initialState?: Record<IdProp, Entity>;
205  empty?: Entity | (() => Entity);
206}): (n: string) => TableOutput<Entity, S, Entity | undefined>;
207export function table<
208  Entity extends AnyState = AnyState,
209  S extends AnyState = AnyState,
210>(
211  { initialState, empty }: {
212    initialState?: Record<IdProp, Entity>;
213    empty?: Entity | (() => Entity);
214  } = {},
215): (n: string) => TableOutput<Entity, S, Entity | undefined> {
216  return (name: string) => createTable<Entity>({ name, empty, initialState });
217}