repos / starfx

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

commit
48ff5d6
parent
27b4af5
author
Vlad
date
2023-07-29 10:45:48 +0000 UTC
feat: schema concept

Schema is like a database schema: you specify what tables you want and their shape.  The goal of this feature is to help users build an immutable data store that resembles a database, with ways to change the data being store as well as query it.

New features:
- `createSchema` which will generate the `initialState` for `createStore`
- `table()` which will create a table slice
- `str()` which will create a string slice
- `num()` which will create a number slice
- `any()` which will create a generic slice with any type
- `obj()` which will create an object slice

```ts
import { createSchema, configureStore, slice, select } from 'starfx/store';
import { main } from 'starfx';

interface User {
  id: string;
  name: string;
}

const { db, initialState } = createSchema({
  users: slice.table<User>({ empty: { id: "", name: "" } }),
});

const store = configureStore({ initialState });
main(function*() {
  yield* schema.update(db.users.add({ "1": { id: "1", name: "bob" }  }));
  const user = yield* select(db.users.selectById, { id: "1" });
});
```
23 files changed,  +1032, -400
M deps.ts
+5, -0
 1@@ -61,8 +61,13 @@ export {
 2   batchActions,
 3   enableBatching,
 4 } from "https://esm.sh/redux-batched-actions@0.5.0?pin=v122";
 5+export type {
 6+  MapEntity,
 7+  PatchEntity,
 8+} from "https://esm.sh/robodux@15.0.1?pin=v122";
 9 export {
10   createLoaderTable,
11   createReducerMap,
12   createTable,
13+  mapReducers,
14 } from "https://esm.sh/robodux@15.0.1?pin=v122";
M query/api.test.ts
+20, -10
  1@@ -3,11 +3,12 @@ import { describe, expect, it } from "../test.ts";
  2 import { call, keepAlive } from "../fx/mod.ts";
  3 import {
  4   configureStore,
  5+  createSchema,
  6+  slice,
  7   storeMdw,
  8   takeEvery,
  9   updateStore,
 10 } from "../store/mod.ts";
 11-import { createQueryState } from "../action.ts";
 12 import { sleep } from "../test.ts";
 13 import { safe } from "../mod.ts";
 14 
 15@@ -22,8 +23,19 @@ interface User {
 16   email: string;
 17 }
 18 
 19+const emptyUser: User = { id: "", name: "", email: "" };
 20 const mockUser: User = { id: "1", name: "test", email: "test@test.com" };
 21 
 22+const testStore = () => {
 23+  const schema = createSchema({
 24+    users: slice.table<User>({ empty: emptyUser }),
 25+    loaders: slice.loader(),
 26+    data: slice.table({ empty: {} }),
 27+  });
 28+  const store = configureStore(schema);
 29+  return { schema, store };
 30+};
 31+
 32 const jsonBlob = (data: unknown) => {
 33   return JSON.stringify(data);
 34 };
 35@@ -221,9 +233,10 @@ it(tests, "run() from a normal saga", () => {
 36 });
 37 
 38 it(tests, "createApi with hash key on a large post", async () => {
 39+  const { store, schema } = testStore();
 40   const query = createApi();
 41   query.use(requestMonitor());
 42-  query.use(storeMdw());
 43+  query.use(storeMdw(schema.db));
 44   query.use(query.routes());
 45   query.use(function* fetchApi(ctx, next) {
 46     const data = {
 47@@ -269,9 +282,6 @@ it(tests, "createApi with hash key on a large post", async () => {
 48   const email = mockUser.email + "9";
 49   const largetext = "abc-def-ghi-jkl-mno-pqr".repeat(100);
 50 
 51-  const store = configureStore({
 52-    initialState: { ...createQueryState(), users: {} },
 53-  });
 54   store.run(query.bootup);
 55   store.dispatch(createUserDefaultKey({ email, largetext }));
 56 
 57@@ -284,16 +294,17 @@ it(tests, "createApi with hash key on a large post", async () => {
 58   });
 59 
 60   expect([8, 9].includes(expectedKey.split("|")[1].length)).toBeTruthy();
 61-  expect(s["@@starfx/data"][expectedKey]).toEqual({
 62+  expect(s.data[expectedKey]).toEqual({
 63     "1": { id: "1", name: "test", email: email, largetext: largetext },
 64   });
 65 });
 66 
 67 it(tests, "createApi - two identical endpoints", async () => {
 68   const actual: string[] = [];
 69+  const { store, schema } = testStore();
 70   const api = createApi();
 71   api.use(requestMonitor());
 72-  api.use(storeMdw());
 73+  api.use(storeMdw(schema.db));
 74   api.use(api.routes());
 75 
 76   const first = api.get(
 77@@ -314,7 +325,6 @@ it(tests, "createApi - two identical endpoints", async () => {
 78     },
 79   );
 80 
 81-  const store = configureStore({ initialState: { users: {} } });
 82   store.run(api.bootup);
 83   store.dispatch(first());
 84   store.dispatch(second());
 85@@ -434,6 +444,7 @@ it(
 86 
 87 it(tests, "should bubble up error", () => {
 88   let error: any = null;
 89+  const { store, schema } = testStore();
 90   const api = createApi();
 91   api.use(function* (_, next) {
 92     try {
 93@@ -443,7 +454,7 @@ it(tests, "should bubble up error", () => {
 94     }
 95   });
 96   api.use(queryCtx);
 97-  api.use(storeMdw());
 98+  api.use(storeMdw(schema.db));
 99   api.use(api.routes());
100 
101   const fetchUser = api.get(
102@@ -455,7 +466,6 @@ it(tests, "should bubble up error", () => {
103     },
104   );
105 
106-  const store = configureStore({ initialState: { users: {} } });
107   store.run(api.bootup);
108   store.dispatch(fetchUser());
109   expect(error.message).toBe(
M query/fetch.test.ts
+40, -47
  1@@ -1,7 +1,11 @@
  2 import { describe, expect, install, it, mock } from "../test.ts";
  3-import { configureStore, storeMdw, takeEvery } from "../store/mod.ts";
  4-import { createQueryState } from "../action.ts";
  5-import type { QueryState } from "../types.ts";
  6+import {
  7+  configureStore,
  8+  createSchema,
  9+  slice,
 10+  storeMdw,
 11+  takeEvery,
 12+} from "../store/mod.ts";
 13 
 14 import { fetcher, fetchRetry } from "./fetch.ts";
 15 import { createApi } from "./api.ts";
 16@@ -17,6 +21,15 @@ const delay = (n = 200) =>
 17     setTimeout(resolve, n);
 18   });
 19 
 20+const testStore = () => {
 21+  const schema = createSchema({
 22+    loaders: slice.loader(),
 23+    data: slice.table({ empty: {} }),
 24+  });
 25+  const store = configureStore(schema);
 26+  return { schema, store };
 27+};
 28+
 29 const tests = describe("fetcher()");
 30 
 31 it(
 32@@ -27,9 +40,10 @@ it(
 33       return new Response(JSON.stringify(mockUser));
 34     });
 35 
 36+    const { store, schema } = testStore();
 37     const api = createApi();
 38     api.use(requestMonitor());
 39-    api.use(storeMdw());
 40+    api.use(storeMdw(schema.db));
 41     api.use(api.routes());
 42     api.use(fetcher({ baseUrl }));
 43 
 44@@ -46,9 +60,6 @@ it(
 45       },
 46     );
 47 
 48-    const store = configureStore<QueryState>({
 49-      initialState: createQueryState(),
 50-    });
 51     store.run(api.bootup);
 52 
 53     const action = fetchUsers();
 54@@ -57,7 +68,7 @@ it(
 55     await delay();
 56 
 57     const state = store.getState();
 58-    expect(state["@@starfx/data"][action.payload.key]).toEqual(mockUser);
 59+    expect(state.data[action.payload.key]).toEqual(mockUser);
 60 
 61     expect(actual).toEqual([{
 62       url: `${baseUrl}/users`,
 63@@ -77,9 +88,10 @@ it(
 64       return new Response("this is some text");
 65     });
 66 
 67+    const { store, schema } = testStore();
 68     const api = createApi();
 69     api.use(requestMonitor());
 70-    api.use(storeMdw());
 71+    api.use(storeMdw(schema.db));
 72     api.use(api.routes());
 73     api.use(fetcher({ baseUrl }));
 74 
 75@@ -95,9 +107,6 @@ it(
 76       },
 77     );
 78 
 79-    const store = configureStore<QueryState>({
 80-      initialState: createQueryState(),
 81-    });
 82     store.run(api.bootup);
 83 
 84     const action = fetchUsers();
 85@@ -114,9 +123,10 @@ it(tests, "fetch - error handling", async () => {
 86     return new Response(JSON.stringify(errMsg), { status: 500 });
 87   });
 88 
 89+  const { schema, store } = testStore();
 90   const api = createApi();
 91   api.use(requestMonitor());
 92-  api.use(storeMdw());
 93+  api.use(storeMdw(schema.db));
 94   api.use(api.routes());
 95   api.use(function* (ctx, next) {
 96     const url = ctx.req().url;
 97@@ -137,9 +147,6 @@ it(tests, "fetch - error handling", async () => {
 98     },
 99   );
100 
101-  const store = configureStore<QueryState>({
102-    initialState: createQueryState(),
103-  });
104   store.run(api.bootup);
105 
106   const action = fetchUsers();
107@@ -148,7 +155,7 @@ it(tests, "fetch - error handling", async () => {
108   await delay();
109 
110   const state = store.getState();
111-  expect(state["@@starfx/data"][action.payload.key]).toEqual(errMsg);
112+  expect(state.data[action.payload.key]).toEqual(errMsg);
113   expect(actual).toEqual({ ok: false, data: errMsg });
114 });
115 
116@@ -157,9 +164,10 @@ it(tests, "fetch - status 204", async () => {
117     return new Response(null, { status: 204 });
118   });
119 
120+  const { schema, store } = testStore();
121   const api = createApi();
122   api.use(requestMonitor());
123-  api.use(storeMdw());
124+  api.use(storeMdw(schema.db));
125   api.use(api.routes());
126   api.use(function* (ctx, next) {
127     const url = ctx.req().url;
128@@ -179,9 +187,6 @@ it(tests, "fetch - status 204", async () => {
129     },
130   );
131 
132-  const store = configureStore<QueryState>({
133-    initialState: createQueryState(),
134-  });
135   store.run(api.bootup);
136 
137   const action = fetchUsers();
138@@ -190,7 +195,7 @@ it(tests, "fetch - status 204", async () => {
139   await delay();
140 
141   const state = store.getState();
142-  expect(state["@@starfx/data"][action.payload.key]).toEqual({});
143+  expect(state.data[action.payload.key]).toEqual({});
144   expect(actual).toEqual({ ok: true, data: {} });
145 });
146 
147@@ -199,9 +204,10 @@ it(tests, "fetch - malformed json", async () => {
148     return new Response("not json", { status: 200 });
149   });
150 
151+  const { schema, store } = testStore();
152   const api = createApi();
153   api.use(requestMonitor());
154-  api.use(storeMdw());
155+  api.use(storeMdw(schema.db));
156   api.use(api.routes());
157   api.use(function* (ctx, next) {
158     const url = ctx.req().url;
159@@ -222,9 +228,6 @@ it(tests, "fetch - malformed json", async () => {
160     },
161   );
162 
163-  const store = configureStore<QueryState>({
164-    initialState: createQueryState(),
165-  });
166   store.run(api.bootup);
167   const action = fetchUsers();
168   store.dispatch(action);
169@@ -244,9 +247,10 @@ it(tests, "fetch - POST", async () => {
170     return new Response(JSON.stringify(mockUser));
171   });
172 
173+  const { schema, store } = testStore();
174   const api = createApi();
175   api.use(requestMonitor());
176-  api.use(storeMdw());
177+  api.use(storeMdw(schema.db));
178   api.use(api.routes());
179   api.use(fetcher({ baseUrl }));
180 
181@@ -271,9 +275,6 @@ it(tests, "fetch - POST", async () => {
182     },
183   );
184 
185-  const store = configureStore<QueryState>({
186-    initialState: createQueryState(),
187-  });
188   store.run(api.bootup);
189   const action = fetchUsers();
190   store.dispatch(action);
191@@ -286,9 +287,10 @@ it(tests, "fetch - POST multiple endpoints with same uri", async () => {
192     return new Response(JSON.stringify(mockUser));
193   });
194 
195+  const { store, schema } = testStore();
196   const api = createApi();
197   api.use(requestMonitor());
198-  api.use(storeMdw());
199+  api.use(storeMdw(schema.db));
200   api.use(api.routes());
201   api.use(fetcher({ baseUrl }));
202 
203@@ -333,9 +335,6 @@ it(tests, "fetch - POST multiple endpoints with same uri", async () => {
204     },
205   );
206 
207-  const store = configureStore<QueryState>({
208-    initialState: createQueryState(),
209-  });
210   store.run(api.bootup);
211 
212   store.dispatch(fetchUsers({ id: "1" }));
213@@ -348,9 +347,10 @@ it(
214   tests,
215   "fetch - slug in url but payload has empty string for slug value",
216   async () => {
217+    const { store, schema } = testStore();
218     const api = createApi();
219     api.use(requestMonitor());
220-    api.use(storeMdw());
221+    api.use(storeMdw(schema.db));
222     api.use(api.routes());
223     api.use(fetcher({ baseUrl }));
224 
225@@ -371,9 +371,6 @@ it(
226       },
227     );
228 
229-    const store = configureStore<QueryState>({
230-      initialState: createQueryState(),
231-    });
232     store.run(api.bootup);
233     const action = fetchUsers({ id: "" });
234     store.dispatch(action);
235@@ -397,9 +394,10 @@ it(
236       });
237     });
238 
239+    const { schema, store } = testStore();
240     const api = createApi();
241     api.use(requestMonitor());
242-    api.use(storeMdw());
243+    api.use(storeMdw(schema.db));
244     api.use(api.routes());
245     api.use(fetcher({ baseUrl }));
246 
247@@ -418,9 +416,6 @@ it(
248       fetchRetry((n) => (n > 4 ? -1 : 10)),
249     ]);
250 
251-    const store = configureStore<QueryState>({
252-      initialState: createQueryState(),
253-    });
254     store.run(api.bootup);
255 
256     const action = fetchUsers();
257@@ -429,7 +424,7 @@ it(
258     await delay();
259 
260     const state = store.getState();
261-    expect(state["@@starfx/data"][action.payload.key]).toEqual(mockUser);
262+    expect(state.data[action.payload.key]).toEqual(mockUser);
263     expect(actual).toEqual({ ok: true, data: mockUser });
264   },
265 );
266@@ -444,10 +439,11 @@ it(
267       });
268     });
269 
270+    const { schema, store } = testStore();
271     let actual = null;
272     const api = createApi();
273     api.use(requestMonitor());
274-    api.use(storeMdw());
275+    api.use(storeMdw(schema.db));
276     api.use(api.routes());
277     api.use(fetcher({ baseUrl }));
278 
279@@ -460,9 +456,6 @@ it(
280       fetchRetry((n) => (n > 2 ? -1 : 10)),
281     ]);
282 
283-    const store = configureStore<QueryState>({
284-      initialState: createQueryState(),
285-    });
286     store.run(api.bootup);
287     const action = fetchUsers();
288     store.dispatch(action);
M query/middleware.test.ts
+109, -114
  1@@ -1,5 +1,4 @@
  2 import { assertLike, asserts, describe, expect, it } from "../test.ts";
  3-import { sleep as delay } from "../deps.ts";
  4 import {
  5   createApi,
  6   createKey,
  7@@ -8,21 +7,17 @@ import {
  8   requestMonitor,
  9   urlParser,
 10 } from "../query/mod.ts";
 11-import type { ApiCtx } from "../query/mod.ts";
 12+import type { ApiCtx, Next, PipeCtx } from "../query/mod.ts";
 13 import { createQueryState } from "../action.ts";
 14-import type { QueryState } from "../types.ts";
 15 import { sleep } from "../test.ts";
 16 
 17-import type { UndoCtx } from "../store/mod.ts";
 18 import {
 19   configureStore,
 20-  defaultLoader,
 21-  selectDataById,
 22+  createSchema,
 23+  slice,
 24   storeMdw,
 25   takeEvery,
 26   takeLatest,
 27-  undo,
 28-  undoer,
 29   updateStore,
 30 } from "../store/mod.ts";
 31 import { safe } from "../mod.ts";
 32@@ -33,10 +28,7 @@ interface User {
 33   email: string;
 34 }
 35 
 36-interface UserState extends QueryState {
 37-  users: { [key: string]: User };
 38-}
 39-
 40+const emptyUser: User = { id: "", name: "", email: "" };
 41 const mockUser: User = { id: "1", name: "test", email: "test@test.com" };
 42 const mockUser2: User = { id: "2", name: "two", email: "two@test.com" };
 43 
 44@@ -45,9 +37,20 @@ const jsonBlob = (data: any) => {
 45   return JSON.stringify(data);
 46 };
 47 
 48+const testStore = () => {
 49+  const schema = createSchema({
 50+    users: slice.table<User>({ empty: emptyUser }),
 51+    loaders: slice.loader(),
 52+    data: slice.table({ empty: {} }),
 53+  });
 54+  const store = configureStore(schema);
 55+  return { schema, store };
 56+};
 57+
 58 const tests = describe("middleware");
 59 
 60 it(tests, "basic", () => {
 61+  const { store } = testStore();
 62   const query = createApi<ApiCtx>();
 63   query.use(queryCtx);
 64   query.use(urlParser);
 65@@ -73,7 +76,7 @@ it(tests, "basic", () => {
 66       if (!ctx.json.ok) return;
 67       const { users } = ctx.json.data;
 68 
 69-      yield* updateStore<UserState>((state) => {
 70+      yield* updateStore((state) => {
 71         users.forEach((u) => {
 72           state.users[u.id] = u;
 73         });
 74@@ -91,36 +94,30 @@ it(tests, "basic", () => {
 75       yield* next();
 76       if (!ctx.json.ok) return;
 77       const curUser = ctx.json.data;
 78-      yield* updateStore<UserState>((state) => {
 79+      yield* updateStore((state) => {
 80         state.users[curUser.id] = curUser;
 81       });
 82     },
 83   );
 84 
 85-  const store = configureStore({
 86-    initialState: {
 87-      ...createQueryState(),
 88-      users: {},
 89-    },
 90-  });
 91   store.run(query.bootup);
 92 
 93   store.dispatch(fetchUsers());
 94-  expect(store.getState()).toEqual({
 95-    ...createQueryState(),
 96-    users: { [mockUser.id]: mockUser },
 97-  });
 98+  expect(store.getState().users).toEqual(
 99+    { [mockUser.id]: mockUser },
100+  );
101   store.dispatch(fetchUser({ id: "2" }));
102-  expect(store.getState()).toEqual({
103-    ...createQueryState(),
104-    users: { [mockUser.id]: mockUser, [mockUser2.id]: mockUser2 },
105+  expect(store.getState().users).toEqual({
106+    [mockUser.id]: mockUser,
107+    [mockUser2.id]: mockUser2,
108   });
109 });
110 
111 it(tests, "with loader", () => {
112+  const { schema, store } = testStore();
113   const api = createApi<ApiCtx>();
114   api.use(requestMonitor());
115-  api.use(storeMdw());
116+  api.use(storeMdw(schema.db));
117   api.use(api.routes());
118   api.use(function* fetchApi(ctx, next) {
119     ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
120@@ -137,7 +134,7 @@ it(tests, "with loader", () => {
121 
122       const { data } = ctx.json;
123 
124-      yield* updateStore<UserState>((state) => {
125+      yield* updateStore((state) => {
126         data.users.forEach((u) => {
127           state.users[u.id] = u;
128         });
129@@ -145,15 +142,12 @@ it(tests, "with loader", () => {
130     },
131   );
132 
133-  const store = configureStore<UserState>({
134-    initialState: { ...createQueryState(), users: {} },
135-  });
136   store.run(api.bootup);
137 
138   store.dispatch(fetchUsers());
139   assertLike(store.getState(), {
140     users: { [mockUser.id]: mockUser },
141-    "@@starfx/loaders": {
142+    loaders: {
143       "/users": {
144         status: "success",
145       },
146@@ -162,9 +156,10 @@ it(tests, "with loader", () => {
147 });
148 
149 it(tests, "with item loader", () => {
150+  const { store, schema } = testStore();
151   const api = createApi<ApiCtx>();
152   api.use(requestMonitor());
153-  api.use(storeMdw());
154+  api.use(storeMdw(schema.db));
155   api.use(api.routes());
156   api.use(function* fetchApi(ctx, next) {
157     ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
158@@ -180,7 +175,7 @@ it(tests, "with item loader", () => {
159       if (!ctx.json.ok) return;
160 
161       const { data } = ctx.json;
162-      yield* updateStore<UserState>((state) => {
163+      yield* updateStore((state) => {
164         data.users.forEach((u) => {
165           state.users[u.id] = u;
166         });
167@@ -188,16 +183,13 @@ it(tests, "with item loader", () => {
168     },
169   );
170 
171-  const store = configureStore<UserState>({
172-    initialState: { ...createQueryState(), users: {} },
173-  });
174   store.run(api.bootup);
175 
176   const action = fetchUser({ id: mockUser.id });
177   store.dispatch(action);
178   assertLike(store.getState(), {
179     users: { [mockUser.id]: mockUser },
180-    "@@starfx/loaders": {
181+    loaders: {
182       "/users/:id": {
183         status: "success",
184       },
185@@ -246,7 +238,7 @@ it(tests, "with POST", () => {
186       if (!ctx.json.ok) return;
187 
188       const { users } = ctx.json.data;
189-      yield* updateStore<UserState>((state) => {
190+      yield* updateStore((state) => {
191         users.forEach((u) => {
192           state.users[u.id] = u;
193         });
194@@ -254,18 +246,16 @@ it(tests, "with POST", () => {
195     },
196   );
197 
198-  const store = configureStore<UserState>({
199-    initialState: { ...createQueryState(), users: {} },
200-  });
201+  const { store } = testStore();
202   store.run(query.bootup);
203-
204   store.dispatch(createUser({ email: mockUser.email }));
205 });
206 
207 it(tests, "simpleCache", () => {
208+  const { store, schema } = testStore();
209   const api = createApi<ApiCtx>();
210   api.use(requestMonitor());
211-  api.use(storeMdw());
212+  api.use(storeMdw(schema.db));
213   api.use(api.routes());
214   api.use(function* fetchApi(ctx, next) {
215     const data = { users: [mockUser] };
216@@ -275,18 +265,15 @@ it(tests, "simpleCache", () => {
217   });
218 
219   const fetchUsers = api.get("/users", { supervisor: takeEvery }, api.cache());
220-  const store = configureStore<UserState>({
221-    initialState: { ...createQueryState(), users: {} },
222-  });
223   store.run(api.bootup);
224 
225   const action = fetchUsers();
226   store.dispatch(action);
227   assertLike(store.getState(), {
228-    "@@starfx/data": {
229+    data: {
230       [action.payload.key]: { users: [mockUser] },
231     },
232-    "@@starfx/loaders": {
233+    loaders: {
234       [`${fetchUsers}`]: {
235         status: "success",
236       },
237@@ -295,9 +282,10 @@ it(tests, "simpleCache", () => {
238 });
239 
240 it(tests, "overriding default loader behavior", () => {
241+  const { store, schema } = testStore();
242   const api = createApi<ApiCtx>();
243   api.use(requestMonitor());
244-  api.use(storeMdw());
245+  api.use(storeMdw(schema.db));
246   api.use(api.routes());
247   api.use(function* fetchApi(ctx, next) {
248     const data = { users: [mockUser] };
249@@ -318,7 +306,7 @@ it(tests, "overriding default loader behavior", () => {
250       }
251       const { data } = ctx.json;
252       ctx.loader = { id, message: "yes", meta: { wow: true } };
253-      yield* updateStore<UserState>((state) => {
254+      yield* updateStore((state) => {
255         data.users.forEach((u) => {
256           state.users[u.id] = u;
257         });
258@@ -326,15 +314,12 @@ it(tests, "overriding default loader behavior", () => {
259     },
260   );
261 
262-  const store = configureStore<UserState>({
263-    initialState: { ...createQueryState(), users: {} },
264-  });
265   store.run(api.bootup);
266 
267   store.dispatch(fetchUsers());
268   assertLike(store.getState(), {
269     users: { [mockUser.id]: mockUser },
270-    "@@starfx/loaders": {
271+    loaders: {
272       [`${fetchUsers}`]: {
273         status: "success",
274         message: "yes",
275@@ -344,48 +329,6 @@ it(tests, "overriding default loader behavior", () => {
276   });
277 });
278 
279-it(tests, "undo", () => {
280-  const api = createApi<UndoCtx>();
281-  api.use(requestMonitor());
282-  api.use(storeMdw());
283-  api.use(api.routes());
284-  api.use(undoer());
285-
286-  api.use(function* fetchApi(ctx, next) {
287-    yield* delay(500);
288-    ctx.response = new Response(jsonBlob({ users: [mockUser] }), {
289-      status: 200,
290-    });
291-    yield* next();
292-  });
293-
294-  const createUser = api.post(
295-    "/users",
296-    { supervisor: takeEvery },
297-    function* (ctx, next) {
298-      ctx.undoable = true;
299-      yield* next();
300-    },
301-  );
302-
303-  const store = configureStore<UserState>({
304-    initialState: { ...createQueryState(), users: {} },
305-  });
306-  store.run(api.bootup);
307-
308-  const action = createUser();
309-  store.dispatch(action);
310-  store.dispatch(undo());
311-  assertLike(store.getState(), {
312-    ...createQueryState({
313-      "@@starfx/loaders": {
314-        [`${createUser}`]: defaultLoader(),
315-        [action.payload.name]: defaultLoader(),
316-      },
317-    }),
318-  });
319-});
320-
321 it(tests, "requestMonitor - error handler", () => {
322   let err = false;
323   console.error = (msg: string) => {
324@@ -396,10 +339,11 @@ it(tests, "requestMonitor - error handler", () => {
325     );
326     err = true;
327   };
328-  const query = createApi<ApiCtx>();
329 
330+  const { schema, store } = testStore();
331+  const query = createApi<ApiCtx>();
332   query.use(requestMonitor());
333-  query.use(storeMdw());
334+  query.use(storeMdw(schema.db));
335   query.use(query.routes());
336   query.use(function* () {
337     throw new Error("something happened");
338@@ -407,18 +351,15 @@ it(tests, "requestMonitor - error handler", () => {
339 
340   const fetchUsers = query.create(`/users`, { supervisor: takeEvery });
341 
342-  const store = configureStore<UserState>({
343-    initialState: { ...createQueryState(), users: {} },
344-  });
345   store.run(query.bootup);
346-
347   store.dispatch(fetchUsers());
348 });
349 
350 it(tests, "createApi with own key", async () => {
351+  const { schema, store } = testStore();
352   const query = createApi();
353   query.use(requestMonitor());
354-  query.use(storeMdw());
355+  query.use(storeMdw(schema.db));
356   query.use(query.routes());
357   query.use(customKey);
358   query.use(function* fetchApi(ctx, next) {
359@@ -464,9 +405,7 @@ it(tests, "createApi with own key", async () => {
360     },
361   );
362   const newUEmail = mockUser.email + ".org";
363-  const store = configureStore<UserState>({
364-    initialState: { ...createQueryState(), users: {} },
365-  });
366+
367   store.run(query.bootup);
368 
369   store.dispatch(createUserCustomKey({ email: newUEmail }));
370@@ -476,7 +415,7 @@ it(tests, "createApi with own key", async () => {
371     : createKey("/users [POST]", { email: newUEmail });
372 
373   const s = store.getState();
374-  asserts.assertEquals(selectDataById(s, { id: expectedKey }), {
375+  asserts.assertEquals(schema.db.data.selectById(s, { id: expectedKey }), {
376     "1": { id: "1", name: "test", email: newUEmail },
377   });
378 
379@@ -487,9 +426,10 @@ it(tests, "createApi with own key", async () => {
380 });
381 
382 it(tests, "createApi with custom key but no payload", async () => {
383+  const { store, schema } = testStore();
384   const query = createApi();
385   query.use(requestMonitor());
386-  query.use(storeMdw());
387+  query.use(storeMdw(schema.db));
388   query.use(query.routes());
389   query.use(customKey);
390   query.use(function* fetchApi(ctx, next) {
391@@ -535,9 +475,6 @@ it(tests, "createApi with custom key but no payload", async () => {
392     },
393   );
394 
395-  const store = configureStore<UserState>({
396-    initialState: { ...createQueryState(), users: {} },
397-  });
398   store.run(query.bootup);
399 
400   store.dispatch(getUsers());
401@@ -547,7 +484,7 @@ it(tests, "createApi with custom key but no payload", async () => {
402     : createKey("/users [GET]", null);
403 
404   const s = store.getState();
405-  asserts.assertEquals(selectDataById(s, { id: expectedKey }), {
406+  asserts.assertEquals(schema.db.data.selectById(s, { id: expectedKey }), {
407     "1": mockUser,
408   });
409 
410@@ -556,3 +493,61 @@ it(tests, "createApi with custom key but no payload", async () => {
411     "the keypart should match the input",
412   );
413 });
414+
415+it(tests, "errorHandler", () => {
416+  let a = 0;
417+  const query = createApi<ApiCtx>();
418+  query.use(function* errorHandler<Ctx extends PipeCtx = PipeCtx>(
419+    ctx: Ctx,
420+    next: Next,
421+  ) {
422+    a = 1;
423+    yield* next();
424+    a = 2;
425+    if (!ctx.result.ok) {
426+      console.error(
427+        `Error: ${ctx.result.error.message}.  Check the endpoint [${ctx.name}]`,
428+        ctx,
429+      );
430+      console.error(ctx.result.error);
431+    }
432+  });
433+  query.use(queryCtx);
434+  query.use(urlParser);
435+  query.use(query.routes());
436+  query.use(function* fetchApi(ctx, next) {
437+    if (`${ctx.req().url}`.startsWith("/users/")) {
438+      ctx.json = { ok: true, data: mockUser2 };
439+      yield* next();
440+      return;
441+    }
442+    const data = {
443+      users: [mockUser],
444+    };
445+    ctx.json = { ok: true, data };
446+    yield* next();
447+  });
448+
449+  const fetchUsers = query.create(
450+    `/users`,
451+    { supervisor: takeEvery },
452+    function* processUsers(_: ApiCtx<unknown, { users: User[] }>, next) {
453+      // throw new Error("some error");
454+      yield* next();
455+    },
456+  );
457+
458+  const store = configureStore({
459+    initialState: {
460+      ...createQueryState(),
461+      users: {},
462+    },
463+  });
464+  store.run(query.bootup);
465+  store.dispatch(fetchUsers());
466+  expect(store.getState()).toEqual({
467+    ...createQueryState(),
468+    users: {},
469+  });
470+  expect(a).toEqual(2);
471+});
M query/types.ts
+3, -3
 1@@ -1,5 +1,5 @@
 2 import type { Operation, Result } from "../deps.ts";
 3-import type { LoadingItemState, LoadingPayload, Payload } from "../types.ts";
 4+import type { LoaderItemState, LoaderPayload, Payload } from "../types.ts";
 5 
 6 type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
 7 
 8@@ -16,7 +16,7 @@ export interface PipeCtx<P = any> extends Payload<P> {
 9 }
10 
11 export interface LoaderCtx<P = unknown> extends PipeCtx<P> {
12-  loader: Partial<LoadingItemState> | null;
13+  loader: Partial<LoaderItemState> | null;
14 }
15 
16 export interface ApiFetchSuccess<ApiSuccess = any> {
17@@ -56,7 +56,7 @@ export interface FetchJsonCtx<P = any, ApiSuccess = any, ApiError = any>
18 export interface ApiCtx<Payload = any, ApiSuccess = any, ApiError = any>
19   extends FetchJsonCtx<Payload, ApiSuccess, ApiError> {
20   actions: Action[];
21-  loader: LoadingPayload | null;
22+  loader: LoaderPayload<any> | null;
23   cache: boolean;
24   cacheData: any;
25 }
M redux/put.test.ts
+1, -1
1@@ -70,7 +70,7 @@ it(
2   "should not cause stack overflow when puts are emitted while dispatching saga",
3   async () => {
4     function* root() {
5-      for (let i = 0; i < 10_000; i += 1) {
6+      for (let i = 0; i < 5_000; i += 1) {
7         yield* put({ type: "test" });
8       }
9       yield* sleep(0);
M store/fx.ts
+9, -1
 1@@ -37,7 +37,15 @@ export function* emit({
 2   }
 3 }
 4 
 5-export function* select<S, R, P>(selectorFn: (s: S, p?: P) => R, p?: P) {
 6+export function select<S, R>(selectorFn: (s: S) => R): Operation<R>;
 7+export function select<S, R, P>(
 8+  selectorFn: (s: S, p: P) => R,
 9+  p: P,
10+): Operation<R>;
11+export function* select<S, R, P>(
12+  selectorFn: (s: S, p?: P) => R,
13+  p?: P,
14+): Operation<R> {
15   const store = yield* StoreContext;
16   return selectorFn(store.getState() as S, p);
17 }
M store/mod.ts
+2, -0
1@@ -6,3 +6,5 @@ export * from "./slice.ts";
2 export * from "./query.ts";
3 export * from "./supervisor.ts";
4 export { createSelector } from "../deps.ts";
5+export * from "./slice/mod.ts";
6+export * from "./schema.ts";
M store/query.ts
+55, -139
  1@@ -1,42 +1,44 @@
  2-import { race } from "../fx/mod.ts";
  3-import { sleep } from "../deps.ts";
  4-import type { ApiCtx, LoaderCtx, Next } from "../query/mod.ts";
  5+import type { ApiCtx, Next } from "../query/mod.ts";
  6 import { compose } from "../compose.ts";
  7-import type { AnyAction, QueryState } from "../types.ts";
  8-import { createAction } from "../action.ts";
  9-
 10-import { put, select, take, updateStore } from "./fx.ts";
 11-import {
 12-  addData,
 13-  resetLoaderById,
 14-  selectDataById,
 15-  setLoaderError,
 16-  setLoaderStart,
 17-  setLoaderSuccess,
 18-} from "./slice.ts";
 19-
 20-export function storeMdw<Ctx extends ApiCtx = ApiCtx>(
 21-  errorFn?: (ctx: Ctx) => string,
 22-) {
 23-  return compose<Ctx>([dispatchActions, loadingMonitor(errorFn), simpleCache]);
 24+import type { AnyAction, AnyState } from "../types.ts";
 25+
 26+import { put, select, updateStore } from "./fx.ts";
 27+import { LoaderOutput } from "./slice/loader.ts";
 28+import { TableOutput } from "./slice/table.ts";
 29+
 30+export function storeMdw<
 31+  Ctx extends ApiCtx = ApiCtx,
 32+  M extends AnyState = AnyState,
 33+>({ data, loaders, errorFn }: {
 34+  loaders: LoaderOutput<M, AnyState>;
 35+  data: TableOutput<any, AnyState>;
 36+  errorFn?: (ctx: Ctx) => string;
 37+}) {
 38+  return compose<Ctx>([
 39+    dispatchActions,
 40+    loadingMonitor(loaders, errorFn),
 41+    simpleCache(data),
 42+  ]);
 43 }
 44 
 45 /**
 46  * This middleware will automatically cache any data found inside `ctx.json`
 47  * which is where we store JSON data from the `fetcher` middleware.
 48  */
 49-export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
 50-  ctx: Ctx,
 51-  next: Next,
 52+export function simpleCache<Ctx extends ApiCtx = ApiCtx>(
 53+  dataSchema: TableOutput<any, AnyState>,
 54 ) {
 55-  ctx.cacheData = yield* select((state: QueryState) =>
 56-    selectDataById(state, { id: ctx.key })
 57-  );
 58-  yield* next();
 59-  if (!ctx.cache) return;
 60-  const { data } = ctx.json;
 61-  yield* updateStore(addData({ [ctx.key]: data }));
 62-  ctx.cacheData = data;
 63+  return function* (
 64+    ctx: Ctx,
 65+    next: Next,
 66+  ) {
 67+    ctx.cacheData = yield* select(dataSchema.selectById, { id: ctx.key });
 68+    yield* next();
 69+    if (!ctx.cache) return;
 70+    const { data } = ctx.json;
 71+    yield* updateStore(dataSchema.add({ [ctx.key]: data }));
 72+    ctx.cacheData = data;
 73+  };
 74 }
 75 
 76 /**
 77@@ -54,123 +56,29 @@ export function* dispatchActions(ctx: { actions: AnyAction[] }, next: Next) {
 78   yield* put(ctx.actions);
 79 }
 80 
 81-export interface OptimisticCtx<
 82-  A extends AnyAction = AnyAction,
 83-  R extends AnyAction = AnyAction,
 84-> extends ApiCtx {
 85-  optimistic: {
 86-    apply: A;
 87-    revert: R;
 88-  };
 89-}
 90-
 91-/**
 92- * This middleware performs an optimistic update for a middleware pipeline.
 93- * It accepts an `apply` and `revert` action.
 94- *
 95- * @remarks This means that we will first `apply` and then if the request is successful we
 96- * keep the change or we `revert` if there's an error.
 97- */
 98-export function* optimistic<Ctx extends OptimisticCtx = OptimisticCtx>(
 99-  ctx: Ctx,
100-  next: Next,
101-) {
102-  if (!ctx.optimistic) {
103-    yield* next();
104-    return;
105-  }
106-
107-  const { apply, revert } = ctx.optimistic;
108-  // optimistically update user
109-  yield* put(apply);
110-
111-  yield* next();
112-
113-  if (!ctx.response || !ctx.response.ok) {
114-    yield* put(revert);
115-  }
116-}
117-
118-export interface UndoCtx<P = any, S = any, E = any> extends ApiCtx<P, S, E> {
119-  undoable: boolean;
120-}
121-
122-export const doIt = createAction("DO_IT");
123-export const undo = createAction("UNDO");
124-/**
125- * This middleware will allow pipeline functions to be undoable which means before they are activated
126- * we have a timeout that allows the function to be cancelled.
127- */
128-export function undoer<Ctx extends UndoCtx = UndoCtx>(
129-  doItType = `${doIt}`,
130-  undoType = `${undo}`,
131-  timeout = 30 * 1000,
132-) {
133-  return function* onUndo(ctx: Ctx, next: Next) {
134-    if (!ctx.undoable) {
135-      yield* next();
136-      return;
137-    }
138-
139-    const winner = yield* race({
140-      doIt: () => take(`${doItType}`),
141-      undo: () => take(`${undoType}`),
142-      timeout: () => sleep(timeout),
143-    });
144-
145-    if (winner.undo || winner.timeout) {
146-      return;
147-    }
148-
149-    yield* next();
150-  };
151-}
152-
153-/**
154- * This middleware creates a loader for a generator function which allows us to track
155- * the status of a pipeline function.
156- */
157-export function* loadingMonitorSimple<Ctx extends LoaderCtx = LoaderCtx>(
158-  ctx: Ctx,
159-  next: Next,
160-) {
161-  yield* updateStore([
162-    setLoaderStart({ id: ctx.name }),
163-    setLoaderStart({ id: ctx.key }),
164-  ]);
165-
166-  if (!ctx.loader) {
167-    ctx.loader = {};
168-  }
169-
170-  yield* next();
171-
172-  yield* updateStore([
173-    setLoaderSuccess({ ...ctx.loader, id: ctx.name }),
174-    setLoaderSuccess({ ...ctx.loader, id: ctx.key }),
175-  ]);
176-}
177-
178 /**
179  * This middleware will track the status of a fetch request.
180  */
181-export function loadingMonitor<Ctx extends ApiCtx = ApiCtx>(
182+export function loadingMonitor<
183+  Ctx extends ApiCtx = ApiCtx,
184+  M extends AnyState = AnyState,
185+>(
186+  loaderSchema: LoaderOutput<M, AnyState>,
187   errorFn: (ctx: Ctx) => string = (ctx) => ctx.json?.data?.message || "",
188 ) {
189   return function* trackLoading(ctx: Ctx, next: Next) {
190     yield* updateStore([
191-      setLoaderStart({ id: ctx.name }),
192-      setLoaderStart({ id: ctx.key }),
193+      loaderSchema.start({ id: ctx.name }),
194+      loaderSchema.start({ id: ctx.key }),
195     ]);
196     if (!ctx.loader) ctx.loader = {} as any;
197 
198     yield* next();
199 
200     if (!ctx.response) {
201-      yield* updateStore([
202-        resetLoaderById({ id: ctx.name }),
203-        resetLoaderById({ id: ctx.key }),
204-      ]);
205+      yield* updateStore(
206+        loaderSchema.resetByIds([ctx.name, ctx.key]),
207+      );
208       return;
209     }
210 
211@@ -180,15 +88,23 @@ export function loadingMonitor<Ctx extends ApiCtx = ApiCtx>(
212 
213     if (!ctx.response.ok) {
214       yield* updateStore([
215-        setLoaderError({ id: ctx.name, message: errorFn(ctx), ...ctx.loader }),
216-        setLoaderError({ id: ctx.key, message: errorFn(ctx), ...ctx.loader }),
217+        loaderSchema.error({
218+          id: ctx.name,
219+          message: errorFn(ctx),
220+          ...ctx.loader,
221+        }),
222+        loaderSchema.error({
223+          id: ctx.key,
224+          message: errorFn(ctx),
225+          ...ctx.loader,
226+        }),
227       ]);
228       return;
229     }
230 
231     yield* updateStore([
232-      setLoaderSuccess({ id: ctx.name, ...ctx.loader }),
233-      setLoaderSuccess({ id: ctx.key, ...ctx.loader }),
234+      loaderSchema.success({ id: ctx.name, ...ctx.loader }),
235+      loaderSchema.success({ id: ctx.key, ...ctx.loader }),
236     ]);
237   };
238 }
A store/schema.test.ts
+69, -0
 1@@ -0,0 +1,69 @@
 2+import { asserts, describe, it } from "../test.ts";
 3+import { select } from "./fx.ts";
 4+import { configureStore } from "./store.ts";
 5+import { slice } from "./slice/mod.ts";
 6+import { createSchema } from "./schema.ts";
 7+
 8+const tests = describe("createSchema()");
 9+
10+interface User {
11+  id: string;
12+  name: string;
13+}
14+
15+const emptyUser = { id: "", name: "" };
16+it(tests, "general types and functionality", async () => {
17+  const schema = createSchema({
18+    users: slice.table<User>({
19+      initialState: { "1": { id: "1", name: "wow" } },
20+      empty: emptyUser,
21+    }),
22+    token: slice.str(),
23+    counter: slice.num(),
24+    dev: slice.any<boolean>(false),
25+    currentUser: slice.obj<User>(emptyUser),
26+    loaders: slice.loader(),
27+  });
28+  const db = schema.db;
29+  const store = configureStore(schema);
30+
31+  asserts.assertEquals(store.getState(), {
32+    users: { "1": { id: "1", name: "wow" } },
33+    token: "",
34+    counter: 0,
35+    dev: false,
36+    currentUser: { id: "", name: "" },
37+    loaders: {},
38+  });
39+  const userMap = schema.db.users.selectTable(store.getState());
40+  asserts.assertEquals(userMap, { "1": { id: "1", name: "wow" } });
41+
42+  await store.run(function* () {
43+    yield* schema.update([
44+      db.users.add({ "2": { id: "2", name: "bob" } }),
45+      db.users.patch({ "1": { name: "zzz" } }),
46+    ]);
47+
48+    const users = yield* select(db.users.selectTable);
49+    asserts.assertEquals(users, {
50+      "1": { id: "1", name: "zzz" },
51+      "2": { id: "2", name: "bob" },
52+    });
53+
54+    yield* schema.update(db.counter.increment());
55+    const counter = yield* select(db.counter.select);
56+    asserts.assertEquals(counter, 1);
57+
58+    yield* schema.update(db.currentUser.patch({ key: "name", value: "vvv" }));
59+    const curUser = yield* select(db.currentUser.select);
60+    asserts.assertEquals(curUser, { id: "", name: "vvv" });
61+
62+    yield* schema.update(db.loaders.start({ id: "fetch-users" }));
63+    const fetchLoader = yield* select(db.loaders.selectById, {
64+      id: "fetch-users",
65+    });
66+    asserts.assertEquals(fetchLoader.id, "fetch-users");
67+    asserts.assertEquals(fetchLoader.status, "loading");
68+    asserts.assertNotEquals(fetchLoader.lastRun, 0);
69+  });
70+});
A store/schema.ts
+35, -0
 1@@ -0,0 +1,35 @@
 2+import { updateStore } from "./fx.ts";
 3+import { BaseSchema, FxStore, StoreUpdater } from "./types.ts";
 4+
 5+export function createSchema<
 6+  O extends { [key: string]: (name: string) => BaseSchema<unknown> },
 7+  S extends { [key in keyof O]: ReturnType<O[key]>["initialState"] },
 8+>(
 9+  slices: O,
10+): {
11+  db: { [key in keyof O]: ReturnType<O[key]> };
12+  initialState: S;
13+  update: FxStore<S>["update"];
14+} {
15+  const db = Object.keys(slices).reduce((acc, key) => {
16+    // deno-lint-ignore no-explicit-any
17+    (acc as any)[key] = slices[key](key);
18+    return acc;
19+  }, {} as { [key in keyof O]: ReturnType<O[key]> });
20+
21+  const initialState = Object.keys(db).reduce((acc, key) => {
22+    // deno-lint-ignore no-explicit-any
23+    (acc as any)[key] = db[key].initialState;
24+    return acc;
25+  }, {}) as S;
26+
27+  function* update(
28+    ups:
29+      | StoreUpdater<S>
30+      | StoreUpdater<S>[],
31+  ) {
32+    return yield* updateStore(ups);
33+  }
34+
35+  return { db, initialState, update };
36+}
M store/slice.ts
+1, -75
 1@@ -1,80 +1,6 @@
 2-import type {
 3-  IdProp,
 4-  LoadingItemState,
 5-  LoadingPayload,
 6-  LoadingState,
 7-  LoadingStatus,
 8-} from "../types.ts";
 9+import type { IdProp } from "../types.ts";
10 import type { QueryState } from "../types.ts";
11 
12-export const defaultLoader = (
13-  p: Partial<LoadingItemState> = {},
14-): LoadingItemState => {
15-  return {
16-    id: "",
17-    status: "idle",
18-    message: "",
19-    lastRun: 0,
20-    lastSuccess: 0,
21-    meta: {},
22-    ...p,
23-  };
24-};
25-
26-export const selectLoaderTable = (s: QueryState) => {
27-  return s["@@starfx/loaders"] || {};
28-};
29-
30-const initLoader = defaultLoader();
31-export const selectLoaderById = (
32-  s: QueryState,
33-  { id }: { id: IdProp },
34-): LoadingState => {
35-  const base = selectLoaderTable(s)[id] || initLoader;
36-  return {
37-    ...base,
38-    isIdle: base.status === "idle",
39-    isError: base.status === "error",
40-    isSuccess: base.status === "success",
41-    isLoading: base.status === "loading",
42-    isInitialLoading: (base.status === "idle" || base.status === "loading") &&
43-      base.lastSuccess === 0,
44-  };
45-};
46-
47-const setLoaderState = (status: LoadingStatus) => {
48-  return (props: LoadingPayload) => {
49-    function updateLoadingState(s: QueryState) {
50-      if (!props.id) return;
51-      const loaders = selectLoaderTable(s);
52-      if (!loaders[props.id]) {
53-        loaders[props.id] = defaultLoader({ ...props });
54-        return;
55-      }
56-
57-      const loader = loaders[props.id];
58-      loader.status = status;
59-      if (props.meta) {
60-        loader.meta = props.meta;
61-      }
62-      if (props.message) {
63-        loader.message = props.message;
64-      }
65-    }
66-    return updateLoadingState;
67-  };
68-};
69-export const setLoaderStart = setLoaderState("loading");
70-export const setLoaderSuccess = setLoaderState("success");
71-export const setLoaderError = setLoaderState("error");
72-export const resetLoaderById = ({ id }: { id: string }) => {
73-  function resetLoader(s: QueryState) {
74-    const loaders = selectLoaderTable(s);
75-    delete loaders[id];
76-  }
77-  return resetLoader;
78-};
79-
80 export const selectDataTable = (s: QueryState) => {
81   return s["@@starfx/data"] || {};
82 };
A store/slice/any.ts
+37, -0
 1@@ -0,0 +1,37 @@
 2+import type { AnyState } from "../../types.ts";
 3+
 4+import type { BaseSchema } from "../types.ts";
 5+
 6+export interface AnyOutput<V, S extends AnyState> extends BaseSchema<V> {
 7+  schema: "any";
 8+  initialState: V;
 9+  set: (v: string) => (s: S) => void;
10+  reset: () => (s: S) => void;
11+  select: (s: S) => V;
12+}
13+
14+export function createAny<V, S extends AnyState = AnyState>(
15+  { name, initialState }: { name: keyof S; initialState: V },
16+): AnyOutput<V, S> {
17+  return {
18+    schema: "any",
19+    name: name as string,
20+    initialState,
21+    set: (value) => (state) => {
22+      // deno-lint-ignore no-explicit-any
23+      (state as any)[name] = value;
24+    },
25+    reset: () => (state) => {
26+      // deno-lint-ignore no-explicit-any
27+      (state as any)[name] = initialState;
28+    },
29+    select: (state) => {
30+      // deno-lint-ignore no-explicit-any
31+      return (state as any)[name];
32+    },
33+  };
34+}
35+
36+export function any<V>(initialState: V) {
37+  return (name: string) => createAny<V, AnyState>({ name, initialState });
38+}
A store/slice/loader.ts
+190, -0
  1@@ -0,0 +1,190 @@
  2+import { createSelector } from "../../deps.ts";
  3+import type {
  4+  AnyState,
  5+  LoaderItemState,
  6+  LoaderPayload,
  7+  LoaderState,
  8+} from "../../types.ts";
  9+import { BaseSchema } from "../types.ts";
 10+
 11+interface PropId {
 12+  id: string;
 13+}
 14+
 15+interface PropIds {
 16+  ids: string[];
 17+}
 18+
 19+const excludesFalse = <T>(n?: T): n is T => Boolean(n);
 20+
 21+export function defaultLoaderItem<
 22+  M extends AnyState = AnyState,
 23+>(li: Partial<LoaderItemState<M>> = {}): LoaderItemState<M> {
 24+  return {
 25+    id: "",
 26+    status: "idle",
 27+    message: "",
 28+    lastRun: 0,
 29+    lastSuccess: 0,
 30+    meta: {} as M,
 31+    ...li,
 32+  };
 33+}
 34+
 35+export function defaultLoader<M extends AnyState = AnyState>(
 36+  l: Partial<LoaderItemState<M>> = {},
 37+): LoaderState<M> {
 38+  const loading = defaultLoaderItem(l);
 39+  return {
 40+    ...loading,
 41+    isIdle: loading.status === "idle",
 42+    isError: loading.status === "error",
 43+    isSuccess: loading.status === "success",
 44+    isLoading: loading.status === "loading",
 45+    isInitialLoading:
 46+      (loading.status === "idle" || loading.status === "loading") &&
 47+      loading.lastSuccess === 0,
 48+  };
 49+}
 50+
 51+interface LoaderSelectors<
 52+  M extends AnyState = AnyState,
 53+  S extends AnyState = AnyState,
 54+> {
 55+  findById: (
 56+    d: Record<string, LoaderItemState<M>>,
 57+    { id }: PropId,
 58+  ) => LoaderState<M>;
 59+  findByIds: (
 60+    d: Record<string, LoaderItemState<M>>,
 61+    { ids }: PropIds,
 62+  ) => LoaderState<M>[];
 63+  selectTable: (s: S) => Record<string, LoaderItemState<M>>;
 64+  selectTableAsList: (state: S) => LoaderItemState<M>[];
 65+  selectById: (s: S, p: PropId) => LoaderState<M>;
 66+  selectByIds: (s: S, p: PropIds) => LoaderState<M>[];
 67+}
 68+
 69+function loaderSelectors<
 70+  M extends AnyState = AnyState,
 71+  S extends AnyState = AnyState,
 72+>(
 73+  selectTable: (s: S) => Record<string, LoaderItemState<M>>,
 74+): LoaderSelectors<M, S> {
 75+  const empty = defaultLoader();
 76+  const tableAsList = (
 77+    data: Record<string, LoaderItemState<M>>,
 78+  ): LoaderItemState<M>[] => Object.values(data).filter(excludesFalse);
 79+
 80+  const findById = (
 81+    data: Record<string, LoaderItemState<M>>,
 82+    { id }: PropId,
 83+  ) => (defaultLoader<M>(data[id]) || empty);
 84+  const findByIds = (
 85+    data: Record<string, LoaderItemState<M>>,
 86+    { ids }: PropIds,
 87+  ): LoaderState<M>[] =>
 88+    ids.map((id) => defaultLoader<M>(data[id])).filter(excludesFalse);
 89+  const selectById = (state: S, { id }: PropId): LoaderState<M> => {
 90+    const data = selectTable(state);
 91+    return defaultLoader<M>(findById(data, { id })) || empty;
 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,
106+      findByIds,
107+    ),
108+  };
109+}
110+
111+export 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+
126+const ts = () => new Date().getTime();
127+
128+export const createLoader = <
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 = selectors.selectById(s, { id: e.id });
147+      table[e.id] = {
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 = selectors.selectById(s, { id: e.id });
157+      table[e.id] = {
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 = selectors.selectById(s, { id: e.id });
167+      table[e.id] = {
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+
187+export function loader<
188+  M extends AnyState = AnyState,
189+>(initialState?: Record<string, LoaderItemState<M>>) {
190+  return (name: string) => createLoader<M>({ name, initialState });
191+}
A store/slice/mod.ts
+15, -0
 1@@ -0,0 +1,15 @@
 2+import { str } from "./str.ts";
 3+import { num } from "./num.ts";
 4+import { table } from "./table.ts";
 5+import { any } from "./any.ts";
 6+import { obj } from "./obj.ts";
 7+import { defaultLoader, defaultLoaderItem, loader } from "./loader.ts";
 8+export const slice = {
 9+  str,
10+  num,
11+  table,
12+  any,
13+  obj,
14+  loader,
15+};
16+export { defaultLoader, defaultLoaderItem };
A store/slice/num.ts
+47, -0
 1@@ -0,0 +1,47 @@
 2+import type { AnyState } from "../../types.ts";
 3+
 4+import type { BaseSchema } from "../types.ts";
 5+
 6+export interface NumOutput<S extends AnyState> extends BaseSchema<number> {
 7+  schema: "num";
 8+  initialState: number;
 9+  set: (v: number) => (s: S) => void;
10+  increment: (by?: number) => (s: S) => void;
11+  decrement: (by?: number) => (s: S) => void;
12+  reset: () => (s: S) => void;
13+  select: (s: S) => number;
14+}
15+
16+export function createNum<S extends AnyState = AnyState>(
17+  { name, initialState = 0 }: { name: keyof S; initialState?: number },
18+): NumOutput<S> {
19+  return {
20+    name: name as string,
21+    schema: "num",
22+    initialState,
23+    set: (value) => (state) => {
24+      // deno-lint-ignore no-explicit-any
25+      (state as any)[name] = value;
26+    },
27+    increment: (by = 1) => (state) => {
28+      // deno-lint-ignore no-explicit-any
29+      (state as any)[name] += by;
30+    },
31+    decrement: (by = 1) => (state) => {
32+      // deno-lint-ignore no-explicit-any
33+      (state as any)[name] -= by;
34+    },
35+    reset: () => (state) => {
36+      // deno-lint-ignore no-explicit-any
37+      (state as any)[name] = initialState;
38+    },
39+    select: (state) => {
40+      // deno-lint-ignore no-explicit-any
41+      return (state as any)[name];
42+    },
43+  };
44+}
45+
46+export function num(initialState?: number) {
47+  return (name: string) => createNum<AnyState>({ name, initialState });
48+}
A store/slice/obj.ts
+43, -0
 1@@ -0,0 +1,43 @@
 2+import type { AnyState } from "../../types.ts";
 3+
 4+import type { BaseSchema } from "../types.ts";
 5+
 6+export interface ObjOutput<V extends AnyState, S extends AnyState>
 7+  extends BaseSchema<V> {
 8+  schema: "obj";
 9+  initialState: V;
10+  set: (v: V) => (s: S) => void;
11+  reset: () => (s: S) => void;
12+  patch: <P extends keyof V>(prop: { key: P; value: V[P] }) => (s: S) => void;
13+  select: (s: S) => V;
14+}
15+
16+export function createObj<V extends AnyState, S extends AnyState = AnyState>(
17+  { name, initialState }: { name: keyof S; initialState: V },
18+): ObjOutput<V, S> {
19+  return {
20+    schema: "obj",
21+    name: name as string,
22+    initialState,
23+    set: (value) => (state) => {
24+      // deno-lint-ignore no-explicit-any
25+      (state as any)[name] = value;
26+    },
27+    reset: () => (state) => {
28+      // deno-lint-ignore no-explicit-any
29+      (state as any)[name] = initialState;
30+    },
31+    patch: <P extends keyof V>(prop: { key: P; value: V[P] }) => (state) => {
32+      // deno-lint-ignore no-explicit-any
33+      (state as any)[name][prop.key] = prop.value;
34+    },
35+    select: (state) => {
36+      // deno-lint-ignore no-explicit-any
37+      return (state as any)[name];
38+    },
39+  };
40+}
41+
42+export function obj<V extends AnyState>(initialState: V) {
43+  return (name: string) => createObj<V, AnyState>({ name, initialState });
44+}
A store/slice/str.ts
+38, -0
 1@@ -0,0 +1,38 @@
 2+import type { AnyState } from "../../types.ts";
 3+
 4+import type { BaseSchema } from "../types.ts";
 5+
 6+export interface StrOutput<S extends AnyState = AnyState>
 7+  extends BaseSchema<string> {
 8+  schema: "str";
 9+  initialState: string;
10+  set: (v: string) => (s: S) => void;
11+  reset: () => (s: S) => void;
12+  select: (s: S) => string;
13+}
14+
15+export function createStr<S extends AnyState = AnyState>(
16+  { name, initialState = "" }: { name: keyof S; initialState?: string },
17+): StrOutput<S> {
18+  return {
19+    schema: "str",
20+    name: name as string,
21+    initialState,
22+    set: (value) => (state) => {
23+      // deno-lint-ignore no-explicit-any
24+      (state as any)[name] = value;
25+    },
26+    reset: () => (state) => {
27+      // deno-lint-ignore no-explicit-any
28+      (state as any)[name] = initialState;
29+    },
30+    select: (state) => {
31+      // deno-lint-ignore no-explicit-any
32+      return (state as any)[name];
33+    },
34+  };
35+}
36+
37+export function str(initialState?: string) {
38+  return (name: string) => createStr<AnyState>({ name, initialState });
39+}
A store/slice/table.test.ts
+114, -0
  1@@ -0,0 +1,114 @@
  2+import { asserts, describe, it } from "../../test.ts";
  3+import { configureStore, updateStore } from "../../store/mod.ts";
  4+import { createQueryState } from "../../action.ts";
  5+
  6+import { createTable } from "./table.ts";
  7+
  8+const tests = describe("createTable()");
  9+
 10+type TUser = {
 11+  id: number;
 12+  user: string;
 13+};
 14+
 15+const NAME = "table";
 16+const empty = { id: 0, user: "" };
 17+const slice = createTable<TUser>({
 18+  name: NAME,
 19+  empty,
 20+});
 21+
 22+const initialState = {
 23+  ...createQueryState(),
 24+  [NAME]: slice.initialState,
 25+};
 26+
 27+const first = { id: 1, user: "A" };
 28+const second = { id: 2, user: "B" };
 29+const third = { id: 3, user: "C" };
 30+
 31+it(tests, "sets up a table", async () => {
 32+  const store = configureStore({
 33+    initialState,
 34+  });
 35+
 36+  await store.run(function* () {
 37+    yield* updateStore(slice.set({ [first.id]: first }));
 38+  });
 39+  asserts.assertEquals(store.getState()[NAME], { [first.id]: first });
 40+});
 41+
 42+it(tests, "adds a row", async () => {
 43+  const store = configureStore({
 44+    initialState,
 45+  });
 46+
 47+  await store.run(function* () {
 48+    yield* updateStore(slice.set({ [second.id]: second }));
 49+  });
 50+  asserts.assertEquals(store.getState()[NAME], { 2: second });
 51+});
 52+
 53+it(tests, "removes a row", async () => {
 54+  const store = configureStore({
 55+    initialState: {
 56+      ...initialState,
 57+      [NAME]: { [first.id]: first, [second.id]: second } as Record<
 58+        string,
 59+        TUser
 60+      >,
 61+    },
 62+  });
 63+
 64+  await store.run(function* () {
 65+    yield* updateStore(slice.remove(["1"]));
 66+  });
 67+  asserts.assertEquals(store.getState()[NAME], { [second.id]: second });
 68+});
 69+
 70+it(tests, "updates a row", async () => {
 71+  const store = configureStore({
 72+    initialState,
 73+  });
 74+  await store.run(function* () {
 75+    const updated = { id: second.id, user: "BB" };
 76+    yield* updateStore(slice.patch({ [updated.id]: updated }));
 77+  });
 78+  asserts.assertEquals(store.getState()[NAME], {
 79+    [second.id]: { ...second, user: "BB" },
 80+  });
 81+});
 82+
 83+it(tests, "gets a row", async () => {
 84+  const store = configureStore({
 85+    initialState,
 86+  });
 87+  await store.run(function* () {
 88+    yield* updateStore(
 89+      slice.add({ [first.id]: first, [second.id]: second, [third.id]: third }),
 90+    );
 91+  });
 92+
 93+  const row = slice.selectById(store.getState(), { id: "2" });
 94+  asserts.assertEquals(row, second);
 95+});
 96+
 97+it(tests, "when the record doesnt exist, it returns empty record", () => {
 98+  const store = configureStore({
 99+    initialState,
100+  });
101+
102+  const row = slice.selectById(store.getState(), { id: "2" });
103+  asserts.assertEquals(row, empty);
104+});
105+
106+it(tests, "gets all rows", async () => {
107+  const store = configureStore({
108+    initialState,
109+  });
110+  const data = { [first.id]: first, [second.id]: second, [third.id]: third };
111+  await store.run(function* () {
112+    yield* updateStore(slice.add(data));
113+  });
114+  asserts.assertEquals(store.getState()[NAME], data);
115+});
A store/slice/table.ts
+174, -0
  1@@ -0,0 +1,174 @@
  2+import { createSelector } from "../../deps.ts";
  3+import type { AnyState, IdProp } from "../../types.ts";
  4+import { BaseSchema } from "../types.ts";
  5+
  6+interface PropId {
  7+  id: IdProp;
  8+}
  9+
 10+interface PropIds {
 11+  ids: IdProp[];
 12+}
 13+
 14+interface PatchEntity<T> {
 15+  [key: string]: Partial<T[keyof T]>;
 16+}
 17+
 18+const excludesFalse = <T>(n?: T): n is T => Boolean(n);
 19+
 20+function mustSelectEntity<Entity extends AnyState = AnyState>(
 21+  defaultEntity: Entity | (() => Entity),
 22+) {
 23+  const isFn = typeof defaultEntity === "function";
 24+
 25+  return function selectEntity<S extends AnyState = AnyState>(
 26+    selectById: (s: S, p: PropId) => Entity | undefined,
 27+  ) {
 28+    return (state: S, { id }: PropId): Entity => {
 29+      if (isFn) {
 30+        const entity = defaultEntity as () => Entity;
 31+        return selectById(state, { id }) || entity();
 32+      }
 33+
 34+      return selectById(state, { id }) || (defaultEntity as Entity);
 35+    };
 36+  };
 37+}
 38+
 39+interface TableSelectors<
 40+  Entity extends AnyState = AnyState,
 41+  S extends AnyState = AnyState,
 42+> {
 43+  findById: (d: Record<IdProp, Entity>, { id }: PropId) => Entity;
 44+  findByIds: (d: Record<IdProp, Entity>, { ids }: PropIds) => Entity[];
 45+  tableAsList: (d: Record<IdProp, Entity>) => Entity[];
 46+  selectTable: (s: S) => Record<IdProp, Entity>;
 47+  selectTableAsList: (state: S) => Entity[];
 48+  selectById: (s: S, p: PropId) => Entity;
 49+  selectByIds: (s: S, p: PropIds) => Entity[];
 50+}
 51+
 52+function tableSelectors<
 53+  Entity extends AnyState = AnyState,
 54+  S extends AnyState = AnyState,
 55+>(
 56+  selectTable: (s: S) => Record<IdProp, Entity>,
 57+  empty: Entity | (() => Entity),
 58+): TableSelectors<Entity, S> {
 59+  const must = mustSelectEntity(empty);
 60+  const tableAsList = (data: Record<IdProp, Entity>): Entity[] =>
 61+    Object.values(data).filter(excludesFalse);
 62+  const findById = (data: Record<IdProp, Entity>, { id }: PropId) => data[id];
 63+  const findByIds = (
 64+    data: Record<IdProp, Entity>,
 65+    { ids }: PropIds,
 66+  ): Entity[] => ids.map((id) => data[id]).filter(excludesFalse);
 67+  const selectById = (state: S, { id }: PropId): Entity | undefined => {
 68+    const data = selectTable(state);
 69+    return findById(data, { id });
 70+  };
 71+
 72+  return {
 73+    findById: must(findById),
 74+    findByIds,
 75+    tableAsList,
 76+    selectTable,
 77+    selectTableAsList: createSelector(
 78+      selectTable,
 79+      (data): Entity[] => tableAsList(data),
 80+    ),
 81+    selectById: must(selectById),
 82+    selectByIds: createSelector(
 83+      selectTable,
 84+      (_: S, p: PropIds) => p,
 85+      findByIds,
 86+    ),
 87+  };
 88+}
 89+
 90+export interface TableOutput<Entity extends AnyState, S extends AnyState>
 91+  extends TableSelectors<Entity, S>, BaseSchema<Record<IdProp, Entity>> {
 92+  schema: "table";
 93+  initialState: Record<IdProp, Entity>;
 94+  add: (e: Record<IdProp, Entity>) => (s: S) => void;
 95+  set: (e: Record<IdProp, Entity>) => (s: S) => void;
 96+  remove: (ids: IdProp[]) => (s: S) => void;
 97+  patch: (e: PatchEntity<Record<IdProp, Entity>>) => (s: S) => void;
 98+  merge: (e: PatchEntity<Record<IdProp, Entity>>) => (s: S) => void;
 99+  reset: () => (s: S) => void;
100+}
101+
102+export const createTable = <
103+  Entity extends AnyState = AnyState,
104+  S extends AnyState = AnyState,
105+>({
106+  name,
107+  empty,
108+  initialState = {},
109+}: {
110+  name: keyof S;
111+  empty: Entity | (() => Entity);
112+  initialState?: Record<IdProp, Entity>;
113+}): TableOutput<Entity, S> => {
114+  const selectors = tableSelectors<Entity, S>((s: S) => s[name], empty);
115+
116+  return {
117+    schema: "table",
118+    name: name as string,
119+    initialState,
120+    add: (entities) => (s) => {
121+      const state = selectors.selectTable(s);
122+      Object.keys(entities).forEach((id) => {
123+        state[id] = entities[id];
124+      });
125+    },
126+    set: (entities) => (s) => {
127+      // deno-lint-ignore no-explicit-any
128+      (s as any)[name] = entities;
129+    },
130+    remove: (ids) => (s) => {
131+      const state = selectors.selectTable(s);
132+      ids.forEach((id) => {
133+        delete state[id];
134+      });
135+    },
136+    patch: (entities) => (s) => {
137+      const state = selectors.selectTable(s);
138+      Object.keys(entities).forEach((id) => {
139+        state[id] = { ...state[id], ...entities[id] };
140+      });
141+    },
142+    merge: (entities) => (s) => {
143+      const state = selectors.selectTable(s);
144+      Object.keys(entities).forEach((id) => {
145+        const entity = entities[id];
146+        Object.keys(entity).forEach((prop) => {
147+          const val = entity[prop];
148+          if (Array.isArray(val)) {
149+            // deno-lint-ignore no-explicit-any
150+            const list = val as any[];
151+            // deno-lint-ignore no-explicit-any
152+            (state as any)[id][prop].push(...list);
153+          } else {
154+            // deno-lint-ignore no-explicit-any
155+            (state as any)[id][prop] = entities[id][prop];
156+          }
157+        });
158+      });
159+    },
160+    reset: () => (s) => {
161+      // deno-lint-ignore no-explicit-any
162+      (s as any)[name] = initialState;
163+    },
164+    ...selectors,
165+  };
166+};
167+
168+export function table<
169+  V extends AnyState = AnyState,
170+>({ initialState, empty }: {
171+  initialState?: Record<IdProp, V>;
172+  empty: V | (() => V);
173+}) {
174+  return (name: string) => createTable<V>({ name, empty, initialState });
175+}
M store/store.ts
+4, -0
 1@@ -138,6 +138,9 @@ export function createStore<S extends AnyState>({
 2     });
 3   }
 4 
 5+  function getInitialState() {
 6+    return initialState;
 7+  }
 8   return {
 9     getScope,
10     getState,
11@@ -154,6 +157,7 @@ export function createStore<S extends AnyState>({
12     ): void {
13       throw new Error(stubMsg);
14     },
15+    getInitialState,
16     [Symbol.observable]: observable,
17   };
18 }
M store/types.ts
+11, -0
 1@@ -17,6 +17,16 @@ declare global {
 2   }
 3 }
 4 
 5+export interface BaseSchema<TOutput> {
 6+  initialState: TOutput;
 7+  schema: string;
 8+  name: string;
 9+}
10+
11+export type Output<O extends { [key: string]: BaseSchema<unknown> }> = {
12+  [key in keyof O]: O[key]["initialState"];
13+};
14+
15 export interface FxStore<S extends AnyState> {
16   getScope: () => Scope;
17   getState: () => S;
18@@ -26,6 +36,7 @@ export interface FxStore<S extends AnyState> {
19   // deno-lint-ignore no-explicit-any
20   dispatch: (a: AnyAction) => any;
21   replaceReducer: (r: (s: S, a: AnyAction) => S) => void;
22+  getInitialState: () => S;
23   // deno-lint-ignore no-explicit-any
24   [Symbol.observable]: () => any;
25 }
M types.ts
+10, -10
 1@@ -11,16 +11,16 @@ export type OpFn<T = unknown> =
 2   | (() => T);
 3 
 4 export interface QueryState {
 5-  "@@starfx/loaders": Record<IdProp, LoadingItemState>;
 6+  "@@starfx/loaders": Record<string, LoaderItemState>;
 7   "@@starfx/data": Record<string, unknown>;
 8 }
 9 
10 export type IdProp = string | number;
11 export type LoadingStatus = "loading" | "success" | "error" | "idle";
12-export interface LoadingItemState<
13-  M extends Record<IdProp, unknown> = Record<IdProp, unknown>,
14+export interface LoaderItemState<
15+  M extends Record<string, unknown> = Record<IdProp, unknown>,
16 > {
17-  id: IdProp;
18+  id: string;
19   status: LoadingStatus;
20   message: string;
21   lastRun: number;
22@@ -28,9 +28,9 @@ export interface LoadingItemState<
23   meta: M;
24 }
25 
26-export interface LoadingState<
27-  M extends Record<IdProp, unknown> = Record<IdProp, unknown>,
28-> extends LoadingItemState<M> {
29+export interface LoaderState<
30+  M extends AnyState = AnyState,
31+> extends LoaderItemState<M> {
32   isIdle: boolean;
33   isLoading: boolean;
34   isError: boolean;
35@@ -38,9 +38,9 @@ export interface LoadingState<
36   isInitialLoading: boolean;
37 }
38 
39-export type LoadingPayload =
40-  & Pick<LoadingItemState, "id">
41-  & Partial<Pick<LoadingItemState, "message" | "meta">>;
42+export type LoaderPayload<M extends AnyState> =
43+  & Pick<LoaderItemState<M>, "id">
44+  & Partial<Pick<LoaderItemState<M>, "message" | "meta">>;
45 
46 // deno-lint-ignore no-explicit-any
47 export type AnyState = Record<string, any>;