- 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" }); }); ```
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";
+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(
+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);
+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+});
+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 }
+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);
+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 }
+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";
+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 }
+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+});
+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+}
+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 };
+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+}
+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+}
+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 };
+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+}
+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+}
+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+}
+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+});
+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+}
+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 }
+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>;