- commit
- 442f5d7
- parent
- b8d1fd5
- author
- Eric Bower
- date
- 2023-07-14 13:25:01 +0000 UTC
feat: immutable store (#2)
+18,
-0
1@@ -0,0 +1,18 @@
2+import { QueryState } from "./types.ts";
3+
4+export const API_ACTION_PREFIX = "@@starfx";
5+export const createAction = (curType: string) => {
6+ if (!curType) throw new Error("createAction requires non-empty string");
7+ const type = `${API_ACTION_PREFIX}/${curType}`;
8+ const action = () => ({ type });
9+ action.toString = () => type;
10+ return action;
11+};
12+
13+export const createQueryState = (s: Partial<QueryState> = {}): QueryState => {
14+ return {
15+ "@@starfx/loaders": {},
16+ "@@starfx/data": {},
17+ ...s,
18+ };
19+};
+1,
-1
1@@ -223,9 +223,9 @@ import type {
2 Next,
3 FetchJson,
4 MiddlewareApiCo,
5- Payload,
6 Supervisor,
7 } from "./types.ts";
8+import type { Payload } from "../types.ts";
9
10 export type ApiName = string | string[];
11
+4,
-4
1@@ -1,6 +1,6 @@
2 import { describe, expect, it } from "./test.ts";
3
4-import { run, sleep } from "./deps.ts";
5+import { Ok, run, sleep } from "./deps.ts";
6 import { compose } from "./compose.ts";
7
8 const tests = describe("compose()");
9@@ -20,11 +20,11 @@ it(tests, "should compose middleware", async () => {
10 return yield* mdw({ one: "", three: "" });
11 });
12
13- const expected = {
14+ const expected = Ok({
15 // we should see the mutation
16 one: "two",
17 three: "four",
18- };
19+ });
20 expect(actual).toEqual(expected);
21 });
22
23@@ -54,6 +54,6 @@ it(tests, "order of execution", async () => {
24 const actual = await run(function* () {
25 return yield* mdw({ actual: "" });
26 });
27- const expected = { actual: "abcdefg" };
28+ const expected = Ok({ actual: "abcdefg" });
29 expect(actual).toEqual(expected);
30 });
+16,
-10
1@@ -1,7 +1,7 @@
2 import { call } from "./fx/mod.ts";
3 import type { Next } from "./query/mod.ts";
4-import type { Instruction, Operation, Result } from "./deps.ts";
5-import { Err, Ok } from "./deps.ts";
6+import type { Instruction, Operation } from "./deps.ts";
7+import { Ok } from "./deps.ts";
8
9 // deno-lint-ignore no-explicit-any
10 export type BaseCtx = Record<string, any>;
11@@ -23,26 +23,32 @@ export function compose<Ctx extends BaseCtx = BaseCtx, T = unknown>(
12 }
13 }
14
15- return function* composeFn(context: Ctx, next?: BaseMiddleware<Ctx, T>) {
16+ return function* composeFn(context: Ctx, mdw?: BaseMiddleware<Ctx, T>) {
17 // last called middleware #
18 let index = -1;
19
20- function* dispatch(i: number): Generator<Instruction, Result<T>, void> {
21+ function* dispatch(i: number): Generator<Instruction, T | undefined, void> {
22 if (i <= index) {
23- return Err(new Error("next() called multiple times"));
24+ throw new Error("next() called multiple times");
25 }
26 index = i;
27 let fn: BaseMiddleware<Ctx, T> | undefined = middleware[i];
28 if (i === middleware.length) {
29- fn = next;
30+ fn = mdw;
31+ }
32+ if (!fn) {
33+ return;
34 }
35- if (!fn) return Err(new Error("fn is falsy"));
36 const nxt = dispatch.bind(null, i + 1);
37 const result = yield* fn(context, nxt);
38- return Ok(result);
39+ return result;
40 }
41
42- yield* call(() => dispatch(0));
43- return context;
44+ const result = yield* call(() => dispatch(0));
45+ if (result.ok) {
46+ return Ok(context);
47+ } else {
48+ return result;
49+ }
50 };
51 }
+0,
-6
1@@ -1,6 +0,0 @@
2-import { getframe } from "./deps.ts";
3-
4-export function* contextualize(context: string, value: unknown) {
5- const frame = yield* getframe();
6- frame.context[context] = value;
7-}
+4,
-33
1@@ -148,11 +148,11 @@
2 "https://deno.land/x/ts_morph@18.0.0/common/typescript.d.ts": "21c0786dddf52537611499340166278507eb9784628d321c2cb6acc696cba0f6",
3 "https://deno.land/x/ts_morph@18.0.0/common/typescript.js": "d5c598b6a2db2202d0428fca5fd79fc9a301a71880831a805d778797d2413c59",
4 "https://esm.sh/@reduxjs/toolkit@1.9.5?pin=v122": "90302cd219f485943878c9f023338f069f3141b8118438fa70eb5759bac6e4b4",
5- "https://esm.sh/@testing-library/react@14.0.0?pin=v122": "dd4b49857ea73e0471e4fd9839cc142d48b4df0b53c6e5f21f6ca8861bd97989",
6+ "https://esm.sh/immer@10.0.2?pin=v122": "7ac87b9c76176de8384a67f8cd93d44f75be1a7496c92707252acb669595c393",
7 "https://esm.sh/react-redux@8.0.5?pin=v122": "fa98e94dc8803fb84bee9eb08a13f11833f634d381003247207682823887dc51",
8- "https://esm.sh/react@18.2.0": "8950a34a030620fce8349d6bd3913b3bdb186c5ec7968fa5ba4d054e22d78e6c",
9 "https://esm.sh/react@18.2.0?pin=v122": "8950a34a030620fce8349d6bd3913b3bdb186c5ec7968fa5ba4d054e22d78e6c",
10 "https://esm.sh/redux-batched-actions@0.5.0?pin=v122": "bb9ba7abde4cbba4352e5d25cf8407795f962e6f7c47b59657ee91834fd6744c",
11+ "https://esm.sh/reselect@4.1.8?pin=v122": "486407fec8db8f0c87ba540ff6457dbec3c8ec8fa93a4845383bc8cdb33c6008",
12 "https://esm.sh/robodux@15.0.1?pin=v122": "51ac2aa6f6fbaac2795f3d34117ba7e77c37e9e3e48bf667a994f6851399bf76",
13 "https://esm.sh/stable/react@18.2.0/denonext/react.mjs": "3c4f23bcfc53b256fcfaf6f834fa9f584c3bb7be667b2682c6cb6ba8ef88f8e6",
14 "https://esm.sh/v118/@types/prop-types@15.7.5/index.d.ts": "6a386ff939f180ae8ef064699d8b7b6e62bc2731a62d7fbf5e02589383838dea",
15@@ -187,35 +187,13 @@
16 "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/serializableStateInvariantMiddleware.d.ts": "f9ff1b947da0074054d30826aaa38e876d1b7027b92324a346ef27c8e0865267",
17 "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/tsHelpers.d.ts": "1d7bf5f58c4a24f4a654a2af61207662b910fa429fafa1d32518c4c6cc925c99",
18 "https://esm.sh/v122/@reduxjs/toolkit@1.9.5/dist/utils.d.ts": "9f14472b238598bdf200a0de9f05dd6568e1171fd753fbd72c01a46bf336145d",
19- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/config.d.ts": "2a90177ebaef25de89351de964c2c601ab54d6e3a157cba60d9cd3eaf5a5ee1a",
20- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/events.d.ts": "1d2699a343a347a830be26eb17ab340d7875c6f549c8d7477efb1773060cc7e5",
21- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/get-node-text.d.ts": "a0a6f0095f25f08a7129bc4d7cb8438039ec422dc341218d274e1e5131115988",
22- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/get-queries-for-element.d.ts": "61b0bd9a20e0738fd87e67017a69df89106f12e516fdd15ce0a889f7c60d479f",
23- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/index.d.ts": "561aeabb2e1fa95bc9d1f9153ccb5e8cd8fb7ffd5a412616c3cfb24dfa613d79",
24- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/matches.d.ts": "0f4c3516347edf0800b6cbb38c84addc96de419019ea3e440d8866cc7b87733d",
25- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/pretty-dom.d.ts": "2d0c0bee7f35288ccb19c673ad8ffdf06b5f6052be8ddc4df0782b1412b5f22a",
26- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/queries.d.ts": "a2a6a00d5bd1e4a1c4782a291309c0269112492641c7167904af156a0bc7ece7",
27- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/query-helpers.d.ts": "56f39bce2cd0e3f3cdcb4bea7175eddba13ee18b91aa3ecc5d42dadc5fb64ccc",
28- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/role-helpers.d.ts": "a3ce619711ff1bcdaaf4b5187d1e3f84e76064909a7c7ecb2e2f404f145b7b5c",
29- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/screen.d.ts": "678c9811a30c4d5d94e185c4e2526bb0194b852eef79b10722920048a10b8edb",
30- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/suggestions.d.ts": "82200e963d3c767976a5a9f41ecf8c65eca14a6b33dcbe00214fcbe959698c46",
31- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/wait-for-element-to-be-removed.d.ts": "278ba90329ef4874f485dbd9f4e2ede0a71f0b10dbca0a6b0562d013f343d247",
32- "https://esm.sh/v122/@testing-library/dom@9.2.0/types/wait-for.d.ts": "8387ec1601cf6b8948672537cf8d430431ba0d87b1f9537b4597c1ab8d3ade5b",
33- "https://esm.sh/v122/@testing-library/dom@9.3.0/denonext/dom.mjs": "bc1b9ef199c3d07f513942175d5f563cbc9b392f5be608d263abd06aa1950985",
34- "https://esm.sh/v122/@testing-library/react@14.0.0/denonext/react.mjs": "60b2b640e50332a27d7bbb8a1ee75e397821726eacd3d0ff3850cf8f6b2559b2",
35- "https://esm.sh/v122/@testing-library/react@14.0.0/types/index.d.ts": "73edecc15b451ea14df5b698d53ce96549ae0ac1911cdfcd40e61581ac695f73",
36- "https://esm.sh/v122/@types/aria-query@5.0.1/index.d.ts": "21522c0f405e58c8dd89cd97eb3d1aa9865ba017fde102d01f86ab50b44e5610",
37 "https://esm.sh/v122/@types/hoist-non-react-statics@3.3.1/index.d.ts": "a84059e8ce2394008282b5a395b28820e4c2fd2da0cd4a15d0d50631b9993854",
38 "https://esm.sh/v122/@types/react-dom@18.2.4/index.d.ts": "7f1ca21337a49885088a365081b652451342e45d8c29dfbf97243c66a3f60196",
39- "https://esm.sh/v122/@types/react-dom@18.2.4/test-utils/index.d.ts": "00f67209791dc2295fce29aa9c7632621dd7605b83a94346275c232b33fd2927",
40 "https://esm.sh/v122/@types/use-sync-external-store@0.0.3/index.d.ts": "61f41da9aaa809e5142b1d849d4e70f3e09913a5cb32c629bf6e61ef27967ff7",
41 "https://esm.sh/v122/@types/use-sync-external-store@0.0.3/with-selector.d.ts": "da0195f35a277ff34bb5577062514ce75b7a1b12f476d6be3d4489e26fcf00d8",
42- "https://esm.sh/v122/ansi-regex@5.0.1/denonext/ansi-regex.mjs": "738d194ad7037256cfcd3f87d3716d28096261492d163395badfbdab3f72ce15",
43- "https://esm.sh/v122/ansi-styles@5.2.0/denonext/ansi-styles.mjs": "8a10e68165432fcfd0929bdeb4e33a194af8b790702f60b2ea0477749990964c",
44- "https://esm.sh/v122/aria-query@5.3.0/denonext/aria-query.mjs": "403e8892efc7c54a89afe84852e7c932a72afe78891eb67eef67bb1e233e5cda",
45- "https://esm.sh/v122/dequal@2.0.3/denonext/lite.js": "6a3dedd0ac89564a613bd65c79813852e4f234d4d3122b4f9cfb153622256b82",
46- "https://esm.sh/v122/dom-accessibility-api@0.5.16/denonext/dom-accessibility-api.mjs": "3d546b0f6750cf4c72ad9d049eeb454226e76b6a56d03f6018aa7bad8798871b",
47 "https://esm.sh/v122/hoist-non-react-statics@3.3.2/denonext/hoist-non-react-statics.mjs": "41018d0142e45a133637f9a3e4da6b8babc22cab2b3ec05cfb202a727da5a0cb",
48+ "https://esm.sh/v122/immer@10.0.2/denonext/immer.mjs": "694ebf85b769db4d026ec4b9655a8caaa6ce51776d857df7cedf3fa4774d5297",
49+ "https://esm.sh/v122/immer@10.0.2/dist/immer.d.ts": "058561900fef84704cbc45a29355da6487be9380b8ed0072f3a03693769a5dd0",
50 "https://esm.sh/v122/immer@9.0.21/denonext/immer.mjs": "3819c7f2cc0f19de974517bd2421b80f800a9bc8bcdb87e3b3aaf022640bd7d6",
51 "https://esm.sh/v122/immer@9.0.21/dist/core/current.d.ts": "99cf6d12499eac02b32c6fa20a8d361a43575ff2fbb774091921c1fa948d046a",
52 "https://esm.sh/v122/immer@9.0.21/dist/core/finalize.d.ts": "4e39c4f3c1a84861990fdf88c2ae5fd629e2445f6a8f24bff38fcfd95992b3b6",
53@@ -234,15 +212,8 @@
54 "https://esm.sh/v122/immer@9.0.21/dist/utils/env.d.ts": "f5a35aab17a4fb73b9bca92b3db33cc10649f2af281862a01231ce8d0d500d40",
55 "https://esm.sh/v122/immer@9.0.21/dist/utils/errors.d.ts": "7a43554d2b957482977f76623246b78fab4bdfee55daae1531300f37602d4a88",
56 "https://esm.sh/v122/immer@9.0.21/dist/utils/plugins.d.ts": "282ca31db57b351ef685ea90d97dea434755390329b1297f3d67ee06dae144d0",
57- "https://esm.sh/v122/lz-string@1.5.0/denonext/lz-string.mjs": "d7c5e1cdf048a845f4da9563ecc9fca66d2caddf625d6632512bc8a477ce75d7",
58- "https://esm.sh/v122/pretty-format@27.5.1/build/index.d.ts": "56a50e283257c7fc16958f9e31d8260262505551782a6afc31030970ff48775a",
59- "https://esm.sh/v122/pretty-format@27.5.1/build/types.d.ts": "462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094",
60- "https://esm.sh/v122/pretty-format@27.5.1/denonext/pretty-format.mjs": "720565b4d3cfdf52b0fdcb2042fef473341db961d34bfa8658ba5b7212af6f69",
61- "https://esm.sh/v122/react-dom@18.2.0/denonext/client.js": "19e10c27f3a6e07c66bd37e59bf77de53dc62e0956c04eb97ffe84805cb05832",
62 "https://esm.sh/v122/react-dom@18.2.0/denonext/react-dom.mjs": "6efaa0beeb36e4c6567824ef5b24ed7856b64e30ec245fbaedf8bed748e6e121",
63- "https://esm.sh/v122/react-dom@18.2.0/denonext/test-utils.js": "fff128c551af42604a3efbb593e0d6f37e33644ad3e2d8cd4207a19f603084cf",
64 "https://esm.sh/v122/react-is@16.13.1/denonext/react-is.mjs": "4fc2bb5cfecb53c0f28cd7b730101ff83c05ca06fdd373bc2d6a9bdf662f96d4",
65- "https://esm.sh/v122/react-is@17.0.2/denonext/react-is.mjs": "b56e6628b118f3621e68402d70b71e691e69f83de02d092b1997070e1070b799",
66 "https://esm.sh/v122/react-is@18.2.0/denonext/react-is.mjs": "07ca6a7473ccbb06466e805c8253ade80bd664a0f486cf020864c22c27a60479",
67 "https://esm.sh/v122/react-redux@8.0.5/denonext/react-redux.mjs": "eeba4c3485907ed356f889ce071f63fde42e8397f14dede20d9732e5be188294",
68 "https://esm.sh/v122/react-redux@8.0.5/es/components/Context.d.ts": "7c66a0e01cd2d48baf1572a2f697565765140cc5cdff8e5a04b06a4295591845",
M
deps.ts
+8,
-10
1@@ -36,6 +36,14 @@ export {
2 useDispatch,
3 useSelector,
4 } from "https://esm.sh/react-redux@8.0.5?pin=v122";
5+export { createSelector } from "https://esm.sh/reselect@4.1.8?pin=v122";
6+
7+export {
8+ enablePatches,
9+ produce,
10+ produceWithPatches,
11+} from "https://esm.sh/immer@10.0.2?pin=v122";
12+export type { Patch } from "https://esm.sh/immer@10.0.2?pin=v122";
13
14 export type {
15 Action,
16@@ -58,18 +66,8 @@ export {
17 batchActions,
18 enableBatching,
19 } from "https://esm.sh/redux-batched-actions@0.5.0?pin=v122";
20-export type {
21- LoadingItemState,
22- LoadingMapPayload,
23- LoadingState,
24- MapEntity,
25-} from "https://esm.sh/robodux@15.0.1?pin=v122";
26 export {
27- createAction,
28- createAssign,
29 createLoaderTable,
30 createReducerMap,
31 createTable,
32- defaultLoader,
33- defaultLoadingItem,
34 } from "https://esm.sh/robodux@15.0.1?pin=v122";
+0,
-7
1@@ -1,7 +0,0 @@
2-import { contextualize } from "../context.ts";
3-import type { Task } from "../deps.ts";
4-
5-export function* cancel<T>(task: Task<T>) {
6- yield* task.halt();
7- yield* contextualize("cancelled", true);
8-}
+0,
-21
1@@ -1,21 +0,0 @@
2-import type { Action, Channel, Operation } from "../deps.ts";
3-
4-import { parallel } from "./parallel.ts";
5-
6-export function* emit({
7- channel,
8- action,
9-}: {
10- channel: Operation<Channel<Action, void>>;
11- action: Action | Action[];
12-}) {
13- const { input } = yield* channel;
14- if (Array.isArray(action)) {
15- if (action.length === 0) {
16- return;
17- }
18- yield* parallel(action.map((a) => () => input.send(a)));
19- } else {
20- yield* input.send(action);
21- }
22-}
+0,
-2
1@@ -1,9 +1,7 @@
2 export * from "./parallel.ts";
3 export * from "./call.ts";
4-export * from "./cancel.ts";
5 export * from "./cancelled.ts";
6 export * from "./race.ts";
7-export * from "./emit.ts";
8 export * from "./request.ts";
9 export * from "./watch.ts";
10 export * from "./defer.ts";
+43,
-0
1@@ -0,0 +1,43 @@
2+import type { AnyAction } from "./store/types.ts";
3+
4+type ActionType = string;
5+type GuardPredicate<G extends T, T = unknown> = (arg: T) => arg is G;
6+type Predicate = (action: AnyAction) => boolean;
7+type StringableActionCreator<A extends AnyAction = AnyAction> = {
8+ (...args: unknown[]): A;
9+ toString(): string;
10+};
11+type SubPattern = Predicate | StringableActionCreator | ActionType;
12+export type Pattern = SubPattern | SubPattern[];
13+type ActionSubPattern<Guard extends AnyAction = AnyAction> =
14+ | GuardPredicate<Guard, AnyAction>
15+ | StringableActionCreator<Guard>
16+ | Predicate
17+ | ActionType;
18+export type ActionPattern<Guard extends AnyAction = AnyAction> =
19+ | ActionSubPattern<Guard>
20+ | ActionSubPattern<Guard>[];
21+
22+export function matcher(pattern: ActionPattern): (input: AnyAction) => boolean {
23+ if (pattern === "*") {
24+ return (input: AnyAction) => !!input;
25+ }
26+
27+ if (typeof pattern === "string") {
28+ return (input: AnyAction) => pattern === input.type;
29+ }
30+
31+ if (Array.isArray(pattern)) {
32+ return (input: AnyAction) => pattern.some((p) => matcher(p)(input));
33+ }
34+
35+ if (typeof pattern === "function" && Object.hasOwn(pattern, "toString")) {
36+ return (input: AnyAction) => pattern.toString() === input.type;
37+ }
38+
39+ if (typeof pattern === "function") {
40+ return (input: AnyAction) => pattern(input) as boolean;
41+ }
42+
43+ throw new Error("invalid pattern");
44+}
M
mod.ts
+2,
-1
1@@ -1,8 +1,9 @@
2 export * from "./fx/mod.ts";
3+export * from "./query/mod.ts";
4 export * from "./types.ts";
5 export * from "./iter.ts";
6-export * from "./context.ts";
7 export * from "./compose.ts";
8+export * from "./action.ts";
9 export {
10 action,
11 createChannel,
M
npm.ts
+20,
-11
1@@ -1,4 +1,4 @@
2-import { assert, build, emptyDir } from "./test.ts";
3+import { build, emptyDir } from "./test.ts";
4
5 main().then(console.log).catch(console.error);
6
7@@ -6,7 +6,9 @@ async function main() {
8 await emptyDir("./npm");
9
10 const version = Deno.env.get("NPM_VERSION");
11- assert(version, "NPM_VERSION is required to build npm package");
12+ if (!version) {
13+ throw new Error("NPM_VERSION is required to build npm package");
14+ }
15
16 await build({
17 entryPoints: [
18@@ -19,12 +21,12 @@ async function main() {
19 path: "react.ts",
20 },
21 {
22- name: "./redux",
23- path: "redux/mod.ts",
24+ name: "./store",
25+ path: "./store/mod.ts",
26 },
27 {
28- name: "./query",
29- path: "./query/mod.ts",
30+ name: "./redux",
31+ path: "./redux/mod.ts",
32 },
33 ],
34 mappings: {
35@@ -37,6 +39,18 @@ async function main() {
36 version: "^18.2.0",
37 peerDependency: true,
38 },
39+ "https://esm.sh/react-redux@8.0.5?pin=v122": {
40+ name: "react-redux",
41+ version: "^8.0.5",
42+ },
43+ "https://esm.sh/immer@10.0.2?pin=v122": {
44+ name: "immer",
45+ version: "^10.0.2",
46+ },
47+ "https://esm.sh/reselect@4.1.8?pin=v122": {
48+ name: "reselect",
49+ version: "^4.1.8",
50+ },
51 "https://esm.sh/robodux@15.0.1?pin=v122": {
52 name: "robodux",
53 version: "^15.0.1",
54@@ -49,11 +63,6 @@ async function main() {
55 name: "redux-batched-actions",
56 version: "^0.5.0",
57 },
58- "https://esm.sh/react-redux@8.0.5?pin=v122": {
59- name: "react-redux",
60- version: "^8.0.5",
61- peerDependency: true,
62- },
63 },
64 outDir: "./npm",
65 shims: {
+1,
-1
1@@ -10,9 +10,9 @@ import type {
2 FetchJson,
3 MiddlewareApiCo,
4 Next,
5- Payload,
6 Supervisor,
7 } from "./types.ts";
8+import type { Payload } from "../types.ts";
9
10 export type ApiName = string | string[];
11
+61,
-71
1@@ -1,16 +1,19 @@
2 import { describe, expect, it } from "../test.ts";
3
4 import { call, keepAlive } from "../fx/mod.ts";
5-import { configureStore, put, takeEvery } from "../redux/mod.ts";
6-import { createAction, createReducerMap, createTable } from "../deps.ts";
7-import type { MapEntity } from "../deps.ts";
8+import {
9+ configureStore,
10+ storeMdw,
11+ takeEvery,
12+ updateStore,
13+} from "../store/mod.ts";
14+import { createQueryState } from "../action.ts";
15
16 import { queryCtx, requestMonitor, urlParser } from "./middleware.ts";
17 import { createApi } from "./api.ts";
18-import { sleep } from "./util.ts";
19+import { sleep } from "../test.ts";
20 import { createKey } from "./create-key.ts";
21 import type { ApiCtx } from "./types.ts";
22-import { poll } from "./supervisor.ts";
23
24 interface User {
25 id: string;
26@@ -24,13 +27,9 @@ const jsonBlob = (data: unknown) => {
27 return JSON.stringify(data);
28 };
29
30-const reducers = { init: () => null };
31-
32 const tests = describe("createApi()");
33
34 it(tests, "createApi - POST", async () => {
35- const name = "users";
36- const cache = createTable<User>({ name });
37 const query = createApi();
38
39 query.use(queryCtx);
40@@ -74,17 +73,17 @@ it(tests, "createApi - POST", async () => {
41 const result = new TextDecoder("utf-8").decode(buff.value);
42 const { users } = JSON.parse(result);
43 if (!users) return;
44- const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
45- acc[u.id] = u;
46- return acc;
47- }, {});
48- yield* put(cache.actions.add(curUsers));
49+
50+ yield* updateStore<{ users: { [key: string]: User } }>((state) => {
51+ (users as User[]).forEach((u) => {
52+ state.users[u.id] = u;
53+ });
54+ });
55 },
56 );
57
58- const reducers = createReducerMap(cache);
59- const { store, fx } = configureStore({ reducers });
60- fx.run(query.bootup);
61+ const store = await configureStore({ initialState: { users: {} } });
62+ store.run(query.bootup);
63
64 store.dispatch(createUser({ email: mockUser.email }));
65 await sleep(150);
66@@ -93,11 +92,8 @@ it(tests, "createApi - POST", async () => {
67 });
68 });
69
70-it(tests, "POST with uri", () => {
71- const name = "users";
72- const cache = createTable<User>({ name });
73+it(tests, "POST with uri", async () => {
74 const query = createApi();
75-
76 query.use(queryCtx);
77 query.use(urlParser);
78 query.use(query.routes());
79@@ -128,21 +124,19 @@ it(tests, "POST with uri", () => {
80 yield* next();
81 if (!ctx.json.ok) return;
82 const { users } = ctx.json.data;
83- const curUsers = users.reduce<MapEntity<User>>((acc, u) => {
84- acc[u.id] = u;
85- return acc;
86- }, {});
87- yield* put(cache.actions.add(curUsers));
88+ yield* updateStore<{ users: { [key: string]: User } }>((state) => {
89+ users.forEach((u) => {
90+ state.users[u.id] = u;
91+ });
92+ });
93 });
94
95- const reducers = createReducerMap(cache);
96- const { store, fx } = configureStore({ reducers });
97- fx.run(query.bootup);
98-
99+ const store = await configureStore({ initialState: { users: {} } });
100+ store.run(query.bootup);
101 store.dispatch(createUser({ email: mockUser.email }));
102 });
103
104-it(tests, "middleware - with request fn", () => {
105+it(tests, "middleware - with request fn", async () => {
106 const query = createApi();
107 query.use(queryCtx);
108 query.use(urlParser);
109@@ -153,13 +147,12 @@ it(tests, "middleware - with request fn", () => {
110 yield* next();
111 });
112 const createUser = query.create("/users", query.request({ method: "POST" }));
113- const { store, fx } = configureStore({ reducers });
114- fx.run(query.bootup);
115-
116+ const store = await configureStore({ initialState: { users: {} } });
117+ store.run(query.bootup);
118 store.dispatch(createUser());
119 });
120
121-it(tests, "run() on endpoint action - should run the effect", () => {
122+it(tests, "run() on endpoint action - should run the effect", async () => {
123 const api = createApi<TestCtx>();
124 api.use(api.routes());
125 let acc = "";
126@@ -177,13 +170,12 @@ it(tests, "run() on endpoint action - should run the effect", () => {
127 expect(acc).toEqual("ab");
128 });
129
130- const { store, fx } = configureStore({ reducers });
131- fx.run(api.bootup);
132-
133+ const store = await configureStore({ initialState: { users: {} } });
134+ store.run(api.bootup);
135 store.dispatch(action2());
136 });
137
138-it(tests, "run() from a normal saga", () => {
139+it(tests, "run() from a normal saga", async () => {
140 const api = createApi();
141 api.use(api.routes());
142 let acc = "";
143@@ -191,7 +183,7 @@ it(tests, "run() from a normal saga", () => {
144 yield* next();
145 acc += "a";
146 });
147- const action2 = createAction("ACTION");
148+ const action2 = () => ({ type: "ACTION" });
149 function* onAction() {
150 const ctx = yield* call(() => action1.run(action1({ id: "1" })));
151 if (!ctx.ok) {
152@@ -211,15 +203,15 @@ it(tests, "run() from a normal saga", () => {
153 yield* task;
154 }
155
156- const { store, fx } = configureStore({ reducers });
157- fx.run(() => keepAlive([api.bootup, watchAction]));
158-
159+ const store = await configureStore({ initialState: { users: {} } });
160+ store.run(() => keepAlive([api.bootup, watchAction]));
161 store.dispatch(action2());
162 });
163
164 it(tests, "createApi with hash key on a large post", async () => {
165 const query = createApi();
166 query.use(requestMonitor());
167+ query.use(storeMdw());
168 query.use(query.routes());
169 query.use(function* fetchApi(ctx, next) {
170 const data = {
171@@ -246,10 +238,13 @@ it(tests, "createApi with hash key on a large post", async () => {
172 const result = new TextDecoder("utf-8").decode(buff.value);
173 const { users } = JSON.parse(result);
174 if (!users) return;
175- const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
176- acc[u.id] = u;
177- return acc;
178- }, {});
179+ const curUsers = (users as User[]).reduce<Record<string, User>>(
180+ (acc, u) => {
181+ acc[u.id] = u;
182+ return acc;
183+ },
184+ {},
185+ );
186 ctx.response = new Response();
187 ctx.json = {
188 ok: true,
189@@ -260,13 +255,15 @@ it(tests, "createApi with hash key on a large post", async () => {
190
191 const email = mockUser.email + "9";
192 const largetext = "abc-def-ghi-jkl-mno-pqr".repeat(100);
193- const reducers = createReducerMap();
194-
195- const { store, fx } = configureStore({ reducers });
196- fx.run(query.bootup);
197
198+ const store = await configureStore({
199+ initialState: { ...createQueryState(), users: {} },
200+ });
201+ store.run(query.bootup);
202 store.dispatch(createUserDefaultKey({ email, largetext }));
203+
204 await sleep(150);
205+
206 const s = store.getState();
207 const expectedKey = createKey(`${createUserDefaultKey}`, {
208 email,
209@@ -274,8 +271,7 @@ it(tests, "createApi with hash key on a large post", async () => {
210 });
211
212 expect([8, 9].includes(expectedKey.split("|")[1].length)).toBeTruthy();
213-
214- expect(s["@@saga-query/data"][expectedKey]).toEqual({
215+ expect(s["@@starfx/data"][expectedKey]).toEqual({
216 "1": { id: "1", name: "test", email: email, largetext: largetext },
217 });
218 });
219@@ -284,6 +280,7 @@ it(tests, "createApi - two identical endpoints", async () => {
220 const actual: string[] = [];
221 const api = createApi();
222 api.use(requestMonitor());
223+ api.use(storeMdw());
224 api.use(api.routes());
225
226 const first = api.get("/health", function* (ctx, next) {
227@@ -293,24 +290,19 @@ it(tests, "createApi - two identical endpoints", async () => {
228
229 const second = api.get(
230 ["/health", "poll"],
231- { supervisor: poll(1 * 1000) },
232 function* (ctx, next) {
233 actual.push(ctx.req().url);
234 yield* next();
235 },
236 );
237
238- const { store, fx } = configureStore({ reducers });
239- fx.run(api.bootup);
240-
241+ const store = await configureStore({ initialState: { users: {} } });
242+ store.run(api.bootup);
243 store.dispatch(first());
244 store.dispatch(second());
245
246 await sleep(150);
247
248- // stop poll
249- store.dispatch(second());
250-
251 expect(actual).toEqual(["/health", "/health"]);
252 });
253
254@@ -319,7 +311,7 @@ interface TestCtx<P = any, S = any, E = any> extends ApiCtx<P, S, E> {
255 }
256
257 // this is strictly for testing types
258-it(tests, "ensure types for get() endpoint", () => {
259+it(tests, "ensure types for get() endpoint", async () => {
260 const api = createApi<TestCtx>();
261 api.use(api.routes());
262 api.use(function* (ctx, next) {
263@@ -342,8 +334,8 @@ it(tests, "ensure types for get() endpoint", () => {
264 },
265 );
266
267- const { store, fx } = configureStore({ reducers });
268- fx.run(api.bootup);
269+ const store = await configureStore({ initialState: { users: {} } });
270+ store.run(api.bootup);
271
272 store.dispatch(action1({ id: "1" }));
273 expect(acc).toEqual(["1", "wow"]);
274@@ -355,7 +347,7 @@ interface FetchUserProps {
275 type FetchUserCtx = TestCtx<FetchUserProps>;
276
277 // this is strictly for testing types
278-it(tests, "ensure ability to cast `ctx` in function definition", () => {
279+it(tests, "ensure ability to cast `ctx` in function definition", async () => {
280 const api = createApi<TestCtx>();
281 api.use(api.routes());
282 api.use(function* (ctx, next) {
283@@ -378,9 +370,8 @@ it(tests, "ensure ability to cast `ctx` in function definition", () => {
284 },
285 );
286
287- const { store, fx } = configureStore({ reducers });
288- fx.run(api.bootup);
289-
290+ const store = await configureStore({ initialState: { users: {} } });
291+ store.run(api.bootup);
292 store.dispatch(action1({ id: "1" }));
293 expect(acc).toEqual(["1", "wow"]);
294 });
295@@ -391,7 +382,7 @@ type FetchUserSecondCtx = TestCtx<any, { result: string }>;
296 it(
297 tests,
298 "ensure ability to cast `ctx` in function definition with no props",
299- () => {
300+ async () => {
301 const api = createApi<TestCtx>();
302 api.use(api.routes());
303 api.use(function* (ctx, next) {
304@@ -413,9 +404,8 @@ it(
305 },
306 );
307
308- const { store, fx } = configureStore({ reducers });
309- fx.run(api.bootup);
310-
311+ const store = await configureStore({ initialState: { users: {} } });
312+ store.run(api.bootup);
313 store.dispatch(action1());
314 expect(acc).toEqual(["wow"]);
315 },
+0,
-1
1@@ -1 +0,0 @@
2-export const API_ACTION_PREFIX = "@@starfx";
+0,
-3
1@@ -2,7 +2,6 @@ import { describe, expect, it } from "../test.ts";
2
3 import type { ActionWithPayload } from "./types.ts";
4 import { createApi } from "./api.ts";
5-import { poll } from "./supervisor.ts";
6
7 const getKeyOf = (action: ActionWithPayload<{ key: string }>): string =>
8 action.payload.key;
9@@ -18,7 +17,6 @@ it(
10 // no param
11 const action0 = api.get(
12 "/users",
13- { supervisor: poll(5 * 1000) }, // with poll middleware
14 function* (ctx, next) {
15 ctx.request = {
16 method: "GET",
17@@ -43,7 +41,6 @@ it(
18 [key: string]: string | boolean | number | null | undefined;
19 }>(
20 "/users",
21- { supervisor: poll(5 * 1000) }, // with poll middleware
22 function* (ctx, next) {
23 ctx.request = {
24 method: "GET",
+57,
-30
1@@ -1,5 +1,7 @@
2 import { describe, expect, install, it, mock } from "../test.ts";
3-import { configureStore } from "../redux/mod.ts";
4+import { configureStore, storeMdw } from "../store/mod.ts";
5+import { createQueryState } from "../action.ts";
6+import type { QueryState } from "../types.ts";
7
8 import { fetcher, fetchRetry } from "./fetch.ts";
9 import { createApi } from "./api.ts";
10@@ -9,7 +11,6 @@ install();
11
12 const baseUrl = "https://saga-query.com";
13 const mockUser = { id: "1", email: "test@saga-query.com" };
14-const reducers = { init: () => null };
15
16 const delay = (n = 200) =>
17 new Promise((resolve) => {
18@@ -28,6 +29,7 @@ it(
19
20 const api = createApi();
21 api.use(requestMonitor());
22+ api.use(storeMdw());
23 api.use(api.routes());
24 api.use(fetcher({ baseUrl }));
25
26@@ -46,8 +48,10 @@ it(
27 expect(ctx.json).toEqual({ ok: true, data: mockUser });
28 });
29
30- const { store, fx } = configureStore({ reducers });
31- fx.run(api.bootup);
32+ const store = await configureStore<QueryState>({
33+ initialState: createQueryState(),
34+ });
35+ store.run(api.bootup);
36
37 const action = fetchUsers();
38 store.dispatch(action);
39@@ -55,7 +59,7 @@ it(
40 await delay();
41
42 const state = store.getState();
43- expect(state["@@saga-query/data"][action.payload.key]).toEqual(mockUser);
44+ expect(state["@@starfx/data"][action.payload.key]).toEqual(mockUser);
45 },
46 );
47
48@@ -69,6 +73,7 @@ it(
49
50 const api = createApi();
51 api.use(requestMonitor());
52+ api.use(storeMdw());
53 api.use(api.routes());
54 api.use(fetcher({ baseUrl }));
55
56@@ -79,8 +84,10 @@ it(
57 expect(ctx.json).toEqual({ ok: true, data: "this is some text" });
58 });
59
60- const { store, fx } = configureStore({ reducers });
61- fx.run(api.bootup);
62+ const store = await configureStore<QueryState>({
63+ initialState: createQueryState(),
64+ });
65+ store.run(api.bootup);
66
67 const action = fetchUsers();
68 store.dispatch(action);
69@@ -97,6 +104,7 @@ it(tests, "fetch - error handling", async () => {
70
71 const api = createApi();
72 api.use(requestMonitor());
73+ api.use(storeMdw());
74 api.use(api.routes());
75 api.use(function* (ctx, next) {
76 const url = ctx.req().url;
77@@ -112,8 +120,10 @@ it(tests, "fetch - error handling", async () => {
78 expect(ctx.json).toEqual({ ok: false, data: errMsg });
79 });
80
81- const { store, fx } = configureStore({ reducers });
82- fx.run(api.bootup);
83+ const store = await configureStore<QueryState>({
84+ initialState: createQueryState(),
85+ });
86+ store.run(api.bootup);
87
88 const action = fetchUsers();
89 store.dispatch(action);
90@@ -121,7 +131,7 @@ it(tests, "fetch - error handling", async () => {
91 await delay();
92
93 const state = store.getState();
94- expect(state["@@saga-query/data"][action.payload.key]).toEqual(errMsg);
95+ expect(state["@@starfx/data"][action.payload.key]).toEqual(errMsg);
96 });
97
98 it(tests, "fetch - status 204", async () => {
99@@ -131,6 +141,7 @@ it(tests, "fetch - status 204", async () => {
100
101 const api = createApi();
102 api.use(requestMonitor());
103+ api.use(storeMdw());
104 api.use(api.routes());
105 api.use(function* (ctx, next) {
106 const url = ctx.req().url;
107@@ -146,8 +157,10 @@ it(tests, "fetch - status 204", async () => {
108 expect(ctx.json).toEqual({ ok: true, data: {} });
109 });
110
111- const { store, fx } = configureStore({ reducers });
112- fx.run(api.bootup);
113+ const store = await configureStore<QueryState>({
114+ initialState: createQueryState(),
115+ });
116+ store.run(api.bootup);
117
118 const action = fetchUsers();
119 store.dispatch(action);
120@@ -155,7 +168,7 @@ it(tests, "fetch - status 204", async () => {
121 await delay();
122
123 const state = store.getState();
124- expect(state["@@saga-query/data"][action.payload.key]).toEqual({});
125+ expect(state["@@starfx/data"][action.payload.key]).toEqual({});
126 });
127
128 it(tests, "fetch - malformed json", async () => {
129@@ -165,6 +178,7 @@ it(tests, "fetch - malformed json", async () => {
130
131 const api = createApi();
132 api.use(requestMonitor());
133+ api.use(storeMdw());
134 api.use(api.routes());
135 api.use(function* (ctx, next) {
136 const url = ctx.req().url;
137@@ -186,9 +200,10 @@ it(tests, "fetch - malformed json", async () => {
138 });
139 });
140
141- const { store, fx } = configureStore({ reducers });
142- fx.run(api.bootup);
143-
144+ const store = await configureStore<QueryState>({
145+ initialState: createQueryState(),
146+ });
147+ store.run(api.bootup);
148 const action = fetchUsers();
149 store.dispatch(action);
150
151@@ -202,6 +217,7 @@ it(tests, "fetch - POST", async () => {
152
153 const api = createApi();
154 api.use(requestMonitor());
155+ api.use(storeMdw());
156 api.use(api.routes());
157 api.use(fetcher({ baseUrl }));
158
159@@ -222,9 +238,10 @@ it(tests, "fetch - POST", async () => {
160 expect(ctx.json).toEqual({ ok: true, data: mockUser });
161 });
162
163- const { store, fx } = configureStore({ reducers });
164- fx.run(api.bootup);
165-
166+ const store = await configureStore<QueryState>({
167+ initialState: createQueryState(),
168+ });
169+ store.run(api.bootup);
170 const action = fetchUsers();
171 store.dispatch(action);
172
173@@ -238,6 +255,7 @@ it(tests, "fetch - POST multiple endpoints with same uri", async () => {
174
175 const api = createApi();
176 api.use(requestMonitor());
177+ api.use(storeMdw());
178 api.use(api.routes());
179 api.use(fetcher({ baseUrl }));
180
181@@ -281,8 +299,10 @@ it(tests, "fetch - POST multiple endpoints with same uri", async () => {
182 },
183 );
184
185- const { store, fx } = configureStore({ reducers });
186- fx.run(api.bootup);
187+ const store = await configureStore<QueryState>({
188+ initialState: createQueryState(),
189+ });
190+ store.run(api.bootup);
191
192 store.dispatch(fetchUsers({ id: "1" }));
193 store.dispatch(fetchUsersSecond({ id: "1" }));
194@@ -296,6 +316,7 @@ it(
195 async () => {
196 const api = createApi();
197 api.use(requestMonitor());
198+ api.use(storeMdw());
199 api.use(api.routes());
200 api.use(fetcher({ baseUrl }));
201
202@@ -315,9 +336,10 @@ it(
203 },
204 );
205
206- const { store, fx } = configureStore({ reducers });
207- fx.run(api.bootup);
208-
209+ const store = await configureStore<QueryState>({
210+ initialState: createQueryState(),
211+ });
212+ store.run(api.bootup);
213 const action = fetchUsers({ id: "" });
214 store.dispatch(action);
215
216@@ -342,6 +364,7 @@ it(
217
218 const api = createApi();
219 api.use(requestMonitor());
220+ api.use(storeMdw());
221 api.use(api.routes());
222 api.use(fetcher({ baseUrl }));
223
224@@ -359,8 +382,10 @@ it(
225 fetchRetry((n) => (n > 4 ? -1 : 10)),
226 ]);
227
228- const { store, fx } = configureStore({ reducers });
229- fx.run(api.bootup);
230+ const store = await configureStore<QueryState>({
231+ initialState: createQueryState(),
232+ });
233+ store.run(api.bootup);
234
235 const action = fetchUsers();
236 store.dispatch(action);
237@@ -368,7 +393,7 @@ it(
238 await delay();
239
240 const state = store.getState();
241- expect(state["@@saga-query/data"][action.payload.key]).toEqual(mockUser);
242+ expect(state["@@starfx/data"][action.payload.key]).toEqual(mockUser);
243 },
244 );
245
246@@ -384,6 +409,7 @@ it.ignore(
247
248 const api = createApi();
249 api.use(requestMonitor());
250+ api.use(storeMdw());
251 api.use(api.routes());
252 api.use(fetcher({ baseUrl }));
253
254@@ -396,9 +422,10 @@ it.ignore(
255 fetchRetry((n) => (n > 2 ? -1 : 10)),
256 ]);
257
258- const { store, fx } = configureStore({ reducers });
259- fx.run(api.bootup);
260-
261+ const store = await configureStore<QueryState>({
262+ initialState: createQueryState(),
263+ });
264+ store.run(api.bootup);
265 const action = fetchUsers();
266 store.dispatch(action);
267
+135,
-119
1@@ -1,33 +1,30 @@
2 import { assertLike, asserts, describe, expect, it } from "../test.ts";
3-import { configureStore, put, takeLatest } from "../redux/mod.ts";
4-import {
5- createReducerMap,
6- createTable,
7- defaultLoadingItem,
8- sleep as delay,
9-} from "../deps.ts";
10-import type { MapEntity } from "../deps.ts";
11+import { sleep as delay } from "../deps.ts";
12 import { call } from "../fx/mod.ts";
13-
14-import { createApi } from "./api.ts";
15 import {
16+ createApi,
17+ createKey,
18 customKey,
19 queryCtx,
20 requestMonitor,
21- undo,
22- undoer,
23 urlParser,
24-} from "./middleware.ts";
25-import type { UndoCtx } from "./middleware.ts";
26-import type { ApiCtx } from "./types.ts";
27-import { sleep } from "./util.ts";
28-import { createKey } from "./create-key.ts";
29+} from "../query/mod.ts";
30+import type { ApiCtx } from "../query/mod.ts";
31+import { createQueryState } from "../action.ts";
32+import type { QueryState } from "../types.ts";
33+import { sleep } from "../test.ts";
34+
35+import type { UndoCtx } from "../store/mod.ts";
36 import {
37- createQueryState,
38- DATA_NAME,
39- LOADERS_NAME,
40+ configureStore,
41+ defaultLoader,
42 selectDataById,
43-} from "./slice.ts";
44+ storeMdw,
45+ takeLatest,
46+ undo,
47+ undoer,
48+ updateStore,
49+} from "../store/mod.ts";
50
51 interface User {
52 id: string;
53@@ -35,6 +32,10 @@ interface User {
54 email: string;
55 }
56
57+interface UserState extends QueryState {
58+ users: { [key: string]: User };
59+}
60+
61 const mockUser: User = { id: "1", name: "test", email: "test@test.com" };
62 const mockUser2: User = { id: "2", name: "two", email: "two@test.com" };
63
64@@ -45,11 +46,8 @@ const jsonBlob = (data: any) => {
65
66 const tests = describe("middleware");
67
68-it(tests, "basic", () => {
69- const name = "users";
70- const cache = createTable<User>({ name });
71+it(tests, "basic", async () => {
72 const query = createApi<ApiCtx>();
73-
74 query.use(queryCtx);
75 query.use(urlParser);
76 query.use(query.routes());
77@@ -72,11 +70,12 @@ it(tests, "basic", () => {
78 yield* next();
79 if (!ctx.json.ok) return;
80 const { users } = ctx.json.data;
81- const curUsers = users.reduce<MapEntity<User>>((acc, u) => {
82- acc[u.id] = u;
83- return acc;
84- }, {});
85- yield* put(cache.actions.add(curUsers));
86+
87+ yield* updateStore<UserState>((state) => {
88+ users.forEach((u) => {
89+ state.users[u.id] = u;
90+ });
91+ });
92 },
93 );
94
95@@ -90,14 +89,19 @@ it(tests, "basic", () => {
96 yield* next();
97 if (!ctx.json.ok) return;
98 const curUser = ctx.json.data;
99- const curUsers = { [curUser.id]: curUser };
100- yield* put(cache.actions.add(curUsers));
101+ yield* updateStore<UserState>((state) => {
102+ state.users[curUser.id] = curUser;
103+ });
104 },
105 );
106
107- const reducers = createReducerMap(cache);
108- const { store, fx } = configureStore({ reducers });
109- fx.run(query.bootup);
110+ const store = await configureStore({
111+ initialState: {
112+ ...createQueryState(),
113+ users: {},
114+ },
115+ });
116+ store.run(query.bootup);
117
118 store.dispatch(fetchUsers());
119 expect(store.getState()).toEqual({
120@@ -111,11 +115,10 @@ it(tests, "basic", () => {
121 });
122 });
123
124-it(tests, "with loader", () => {
125- const users = createTable<User>({ name: "users" });
126-
127+it(tests, "with loader", async () => {
128 const api = createApi<ApiCtx>();
129 api.use(requestMonitor());
130+ api.use(storeMdw());
131 api.use(api.routes());
132 api.use(function* fetchApi(ctx, next) {
133 ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
134@@ -130,23 +133,24 @@ it(tests, "with loader", () => {
135 if (!ctx.json.ok) return;
136
137 const { data } = ctx.json;
138- const curUsers = data.users.reduce<MapEntity<User>>((acc, u) => {
139- acc[u.id] = u;
140- return acc;
141- }, {});
142
143- ctx.actions.push(users.actions.add(curUsers));
144+ yield* updateStore<UserState>((state) => {
145+ data.users.forEach((u) => {
146+ state.users[u.id] = u;
147+ });
148+ });
149 },
150 );
151
152- const reducers = createReducerMap(users);
153- const { store, fx } = configureStore({ reducers });
154- fx.run(api.bootup);
155+ const store = await configureStore<UserState>({
156+ initialState: { ...createQueryState(), users: {} },
157+ });
158+ store.run(api.bootup);
159
160 store.dispatch(fetchUsers());
161 assertLike(store.getState(), {
162- [users.name]: { [mockUser.id]: mockUser },
163- [LOADERS_NAME]: {
164+ users: { [mockUser.id]: mockUser },
165+ "@@starfx/loaders": {
166 "/users": {
167 status: "success",
168 },
169@@ -154,11 +158,10 @@ it(tests, "with loader", () => {
170 });
171 });
172
173-it(tests, "with item loader", () => {
174- const users = createTable<User>({ name: "users" });
175-
176+it(tests, "with item loader", async () => {
177 const api = createApi<ApiCtx>();
178 api.use(requestMonitor());
179+ api.use(storeMdw());
180 api.use(api.routes());
181 api.use(function* fetchApi(ctx, next) {
182 ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
183@@ -173,24 +176,24 @@ it(tests, "with item loader", () => {
184 if (!ctx.json.ok) return;
185
186 const { data } = ctx.json;
187- const curUsers = data.users.reduce<MapEntity<User>>((acc, u) => {
188- acc[u.id] = u;
189- return acc;
190- }, {});
191-
192- ctx.actions.push(users.actions.add(curUsers));
193+ yield* updateStore<UserState>((state) => {
194+ data.users.forEach((u) => {
195+ state.users[u.id] = u;
196+ });
197+ });
198 },
199 );
200
201- const reducers = createReducerMap(users);
202- const { store, fx } = configureStore({ reducers });
203- fx.run(api.bootup);
204+ const store = await configureStore<UserState>({
205+ initialState: { ...createQueryState(), users: {} },
206+ });
207+ store.run(api.bootup);
208
209 const action = fetchUser({ id: mockUser.id });
210 store.dispatch(action);
211 assertLike(store.getState(), {
212- [users.name]: { [mockUser.id]: mockUser },
213- [LOADERS_NAME]: {
214+ users: { [mockUser.id]: mockUser },
215+ "@@starfx/loaders": {
216 "/users/:id": {
217 status: "success",
218 },
219@@ -201,11 +204,8 @@ it(tests, "with item loader", () => {
220 });
221 });
222
223-it(tests, "with POST", () => {
224- const name = "users";
225- const cache = createTable<User>({ name });
226+it(tests, "with POST", async () => {
227 const query = createApi();
228-
229 query.use(queryCtx);
230 query.use(urlParser);
231 query.use(query.routes());
232@@ -241,24 +241,26 @@ it(tests, "with POST", () => {
233 if (!ctx.json.ok) return;
234
235 const { users } = ctx.json.data;
236- const curUsers = users.reduce<MapEntity<User>>((acc, u) => {
237- acc[u.id] = u;
238- return acc;
239- }, {});
240- yield* put(cache.actions.add(curUsers));
241+ yield* updateStore<UserState>((state) => {
242+ users.forEach((u) => {
243+ state.users[u.id] = u;
244+ });
245+ });
246 },
247 );
248
249- const reducers = createReducerMap(cache);
250- const { store, fx } = configureStore({ reducers });
251- fx.run(query.bootup);
252+ const store = await configureStore<UserState>({
253+ initialState: { ...createQueryState(), users: {} },
254+ });
255+ store.run(query.bootup);
256
257 store.dispatch(createUser({ email: mockUser.email }));
258 });
259
260-it(tests, "simpleCache", () => {
261+it(tests, "simpleCache", async () => {
262 const api = createApi<ApiCtx>();
263 api.use(requestMonitor());
264+ api.use(storeMdw());
265 api.use(api.routes());
266 api.use(function* fetchApi(ctx, next) {
267 const data = { users: [mockUser] };
268@@ -268,16 +270,18 @@ it(tests, "simpleCache", () => {
269 });
270
271 const fetchUsers = api.get("/users", api.cache());
272- const { store, fx } = configureStore({ reducers: { init: () => null } });
273- fx.run(api.bootup);
274+ const store = await configureStore<UserState>({
275+ initialState: { ...createQueryState(), users: {} },
276+ });
277+ store.run(api.bootup);
278
279 const action = fetchUsers();
280 store.dispatch(action);
281 assertLike(store.getState(), {
282- [DATA_NAME]: {
283+ "@@starfx/data": {
284 [action.payload.key]: { users: [mockUser] },
285 },
286- [LOADERS_NAME]: {
287+ "@@starfx/loaders": {
288 [`${fetchUsers}`]: {
289 status: "success",
290 },
291@@ -285,11 +289,10 @@ it(tests, "simpleCache", () => {
292 });
293 });
294
295-it(tests, "overriding default loader behavior", () => {
296- const users = createTable<User>({ name: "users" });
297-
298+it(tests, "overriding default loader behavior", async () => {
299 const api = createApi<ApiCtx>();
300 api.use(requestMonitor());
301+ api.use(storeMdw());
302 api.use(api.routes());
303 api.use(function* fetchApi(ctx, next) {
304 const data = { users: [mockUser] };
305@@ -308,24 +311,24 @@ it(tests, "overriding default loader behavior", () => {
306 return;
307 }
308 const { data } = ctx.json;
309- const curUsers = data.users.reduce<MapEntity<User>>((acc, u) => {
310- acc[u.id] = u;
311- return acc;
312- }, {});
313-
314 ctx.loader = { id, message: "yes", meta: { wow: true } };
315- ctx.actions.push(users.actions.add(curUsers));
316+ yield* updateStore<UserState>((state) => {
317+ data.users.forEach((u) => {
318+ state.users[u.id] = u;
319+ });
320+ });
321 },
322 );
323
324- const reducers = createReducerMap(users);
325- const { store, fx } = configureStore({ reducers });
326- fx.run(api.bootup);
327+ const store = await configureStore<UserState>({
328+ initialState: { ...createQueryState(), users: {} },
329+ });
330+ store.run(api.bootup);
331
332 store.dispatch(fetchUsers());
333 assertLike(store.getState(), {
334- [users.name]: { [mockUser.id]: mockUser },
335- [LOADERS_NAME]: {
336+ users: { [mockUser.id]: mockUser },
337+ "@@starfx/loaders": {
338 [`${fetchUsers}`]: {
339 status: "success",
340 message: "yes",
341@@ -335,9 +338,10 @@ it(tests, "overriding default loader behavior", () => {
342 });
343 });
344
345-it(tests, "undo", () => {
346+it(tests, "undo", async () => {
347 const api = createApi<UndoCtx>();
348 api.use(requestMonitor());
349+ api.use(storeMdw());
350 api.use(api.routes());
351 api.use(undoer());
352
353@@ -354,23 +358,25 @@ it(tests, "undo", () => {
354 yield* next();
355 });
356
357- const { store, fx } = configureStore({ reducers: { init: () => null } });
358- fx.run(api.bootup);
359+ const store = await configureStore<UserState>({
360+ initialState: { ...createQueryState(), users: {} },
361+ });
362+ store.run(api.bootup);
363
364 const action = createUser();
365 store.dispatch(action);
366 store.dispatch(undo());
367 assertLike(store.getState(), {
368 ...createQueryState({
369- [LOADERS_NAME]: {
370- [`${createUser}`]: defaultLoadingItem(),
371- [action.payload.name]: defaultLoadingItem(),
372+ "@@starfx/loaders": {
373+ [`${createUser}`]: defaultLoader(),
374+ [action.payload.name]: defaultLoader(),
375 },
376 }),
377 });
378 });
379
380-it(tests, "requestMonitor - error handler", () => {
381+it(tests, "requestMonitor - error handler", async () => {
382 let err = false;
383 console.error = (msg: string) => {
384 if (err) return;
385@@ -380,11 +386,10 @@ it(tests, "requestMonitor - error handler", () => {
386 );
387 err = true;
388 };
389- const name = "users";
390- const cache = createTable<User>({ name });
391 const query = createApi<ApiCtx>();
392
393 query.use(requestMonitor());
394+ query.use(storeMdw());
395 query.use(query.routes());
396 query.use(function* () {
397 throw new Error("something happened");
398@@ -392,9 +397,10 @@ it(tests, "requestMonitor - error handler", () => {
399
400 const fetchUsers = query.create(`/users`);
401
402- const reducers = createReducerMap(cache);
403- const { store, fx } = configureStore({ reducers });
404- fx.run(query.bootup);
405+ const store = await configureStore<UserState>({
406+ initialState: { ...createQueryState(), users: {} },
407+ });
408+ store.run(query.bootup);
409
410 store.dispatch(fetchUsers());
411 });
412@@ -402,6 +408,7 @@ it(tests, "requestMonitor - error handler", () => {
413 it(tests, "createApi with own key", async () => {
414 const query = createApi();
415 query.use(requestMonitor());
416+ query.use(storeMdw());
417 query.use(query.routes());
418 query.use(customKey);
419 query.use(function* fetchApi(ctx, next) {
420@@ -431,10 +438,13 @@ it(tests, "createApi with own key", async () => {
421 const result = new TextDecoder("utf-8").decode(buff.value);
422 const { users } = JSON.parse(result);
423 if (!users) return;
424- const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
425- acc[u.id] = u;
426- return acc;
427- }, {});
428+ const curUsers = (users as User[]).reduce<Record<string, User>>(
429+ (acc, u) => {
430+ acc[u.id] = u;
431+ return acc;
432+ },
433+ {},
434+ );
435 ctx.response = new Response();
436 ctx.json = {
437 ok: true,
438@@ -443,9 +453,10 @@ it(tests, "createApi with own key", async () => {
439 },
440 );
441 const newUEmail = mockUser.email + ".org";
442- const reducers = createReducerMap();
443- const { store, fx } = configureStore({ reducers });
444- fx.run(query.bootup);
445+ const store = await configureStore<UserState>({
446+ initialState: { ...createQueryState(), users: {} },
447+ });
448+ store.run(query.bootup);
449
450 store.dispatch(createUserCustomKey({ email: newUEmail }));
451 await sleep(150);
452@@ -467,6 +478,7 @@ it(tests, "createApi with own key", async () => {
453 it(tests, "createApi with custom key but no payload", async () => {
454 const query = createApi();
455 query.use(requestMonitor());
456+ query.use(storeMdw());
457 query.use(query.routes());
458 query.use(customKey);
459 query.use(function* fetchApi(ctx, next) {
460@@ -496,10 +508,13 @@ it(tests, "createApi with custom key but no payload", async () => {
461 const result = new TextDecoder("utf-8").decode(buff.value);
462 const { users } = JSON.parse(result);
463 if (!users) return;
464- const curUsers = (users as User[]).reduce<MapEntity<User>>((acc, u) => {
465- acc[u.id] = u;
466- return acc;
467- }, {});
468+ const curUsers = (users as User[]).reduce<Record<string, User>>(
469+ (acc, u) => {
470+ acc[u.id] = u;
471+ return acc;
472+ },
473+ {},
474+ );
475 ctx.response = new Response();
476 ctx.json = {
477 ok: true,
478@@ -508,9 +523,10 @@ it(tests, "createApi with custom key but no payload", async () => {
479 },
480 );
481
482- const reducers = createReducerMap();
483- const { store, fx } = configureStore({ reducers });
484- fx.run(query.bootup);
485+ const store = await configureStore<UserState>({
486+ initialState: { ...createQueryState(), users: {} },
487+ });
488+ store.run(query.bootup);
489
490 store.dispatch(getUsers());
491 await sleep(150);
+4,
-192
1@@ -1,6 +1,4 @@
2-import { call, race } from "../fx/mod.ts";
3-import { put, select, take } from "../redux/mod.ts";
4-import { batchActions, sleep } from "../deps.ts";
5+import { call } from "../fx/mod.ts";
6 import { compose } from "../compose.ts";
7 import type { OpFn } from "../types.ts";
8
9@@ -8,20 +6,11 @@ import type {
10 Action,
11 ApiCtx,
12 ApiRequest,
13- LoaderCtx,
14 Next,
15 PipeCtx,
16 RequiredApiRequest,
17 } from "./types.ts";
18-import { createAction, isObject, mergeRequest } from "./util.ts";
19-import {
20- addData,
21- resetLoaderById,
22- selectDataById,
23- setLoaderError,
24- setLoaderStart,
25- setLoaderSuccess,
26-} from "./slice.ts";
27+import { isObject, mergeRequest } from "./util.ts";
28
29 /**
30 * This middleware will catch any errors in the pipeline
31@@ -103,176 +92,6 @@ export function* urlParser<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
32 yield* next();
33 }
34
35-/**
36- * This middleware will take the result of `ctx.actions` and dispatch them
37- * as a single batch.
38- *
39- * @remarks This is useful because sometimes there are a lot of actions that need dispatched
40- * within the pipeline of the middleware and instead of dispatching them serially this
41- * improves performance by only hitting the reducers once.
42- */
43-export function* dispatchActions(ctx: { actions: Action[] }, next: Next) {
44- if (!ctx.actions) ctx.actions = [];
45- yield* next();
46- if (ctx.actions.length === 0) return;
47- yield* put(batchActions(ctx.actions));
48-}
49-
50-/**
51- * This middleware creates a loader for a generator function which allows us to track
52- * the status of a pipeline function.
53- */
54-export function* loadingMonitorSimple<Ctx extends LoaderCtx = LoaderCtx>(
55- ctx: Ctx,
56- next: Next,
57-) {
58- yield* put(
59- batchActions([
60- setLoaderStart({ id: ctx.name }),
61- setLoaderStart({ id: ctx.key }),
62- ]),
63- );
64- if (!ctx.loader) {
65- ctx.loader = {};
66- }
67-
68- yield* next();
69-
70- yield* put(
71- batchActions([
72- setLoaderSuccess({ ...ctx.loader, id: ctx.name }),
73- setLoaderSuccess({ ...ctx.loader, id: ctx.key }),
74- ]),
75- );
76-}
77-
78-/**
79- * This middleware will track the status of a fetch request.
80- */
81-export function loadingMonitor<Ctx extends ApiCtx = ApiCtx>(
82- errorFn: (ctx: Ctx) => string = (ctx) => ctx.json?.data?.message || "",
83-) {
84- return function* trackLoading(ctx: Ctx, next: Next) {
85- yield* put(
86- batchActions([
87- setLoaderStart({ id: ctx.name }),
88- setLoaderStart({ id: ctx.key }),
89- ]),
90- );
91- if (!ctx.loader) ctx.loader = {} as any;
92-
93- yield* next();
94-
95- if (!ctx.response) {
96- ctx.actions.push(resetLoaderById(ctx.name), resetLoaderById(ctx.key));
97- return;
98- }
99-
100- const payload = ctx.loader || {};
101- if (!ctx.response.ok) {
102- ctx.actions.push(
103- setLoaderError({ id: ctx.name, message: errorFn(ctx), ...payload }),
104- setLoaderError({ id: ctx.key, message: errorFn(ctx), ...payload }),
105- );
106- return;
107- }
108-
109- ctx.actions.push(
110- setLoaderSuccess({ id: ctx.name, ...payload }),
111- setLoaderSuccess({ id: ctx.key, ...payload }),
112- );
113- };
114-}
115-
116-export interface UndoCtx<P = any, S = any, E = any> extends ApiCtx<P, S, E> {
117- undoable: boolean;
118-}
119-
120-export const doIt = createAction("DO_IT");
121-export const undo = createAction("UNDO");
122-/**
123- * This middleware will allow pipeline functions to be undoable which means before they are activated
124- * we have a timeout that allows the function to be cancelled.
125- */
126-export function undoer<Ctx extends UndoCtx = UndoCtx>(
127- doItType = `${doIt}`,
128- undoType = `${undo}`,
129- timeout = 30 * 1000,
130-) {
131- return function* onUndo(ctx: Ctx, next: Next) {
132- if (!ctx.undoable) {
133- yield* next();
134- return;
135- }
136-
137- const winner = yield* race({
138- doIt: () => take(`${doItType}`),
139- undo: () => take(`${undoType}`),
140- timeout: () => sleep(timeout),
141- });
142-
143- if (winner.undo || winner.timeout) {
144- return;
145- }
146-
147- yield* next();
148- };
149-}
150-
151-export interface OptimisticCtx<
152- A extends Action = Action,
153- R extends Action = Action,
154-> extends ApiCtx {
155- optimistic: {
156- apply: A;
157- revert: R;
158- };
159-}
160-
161-/**
162- * This middleware performs an optimistic update for a middleware pipeline.
163- * It accepts an `apply` and `revert` action.
164- *
165- * @remarks This means that we will first `apply` and then if the request is successful we
166- * keep the change or we `revert` if there's an error.
167- */
168-export function* optimistic<
169- Ctx extends OptimisticCtx = OptimisticCtx,
170->(ctx: Ctx, next: Next) {
171- if (!ctx.optimistic) {
172- yield* next();
173- return;
174- }
175-
176- const { apply, revert } = ctx.optimistic;
177- // optimistically update user
178- yield* put(apply);
179-
180- yield* next();
181-
182- if (!ctx.response || !ctx.response.ok) {
183- yield* put(revert);
184- }
185-}
186-
187-/**
188- * This middleware will automatically cache any data found inside `ctx.json`
189- * which is where we store JSON data from the `fetcher` middleware.
190- */
191-export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
192- ctx: Ctx,
193- next: Next,
194-) {
195- ctx.cacheData = yield* select((state) =>
196- selectDataById(state, { id: ctx.key })
197- );
198- yield* next();
199- if (!ctx.cache) return;
200- const { data } = ctx.json;
201- ctx.actions.push(addData({ [ctx.key]: data }));
202- ctx.cacheData = data;
203-}
204-
205 /**
206 * This middleware allows the user to override the default key provided to every pipeline function
207 * and instead use whatever they want.
208@@ -305,16 +124,11 @@ export function* customKey<Ctx extends ApiCtx = ApiCtx>(ctx: Ctx, next: Next) {
209 /**
210 * This middleware is a composition of many middleware used to faciliate the {@link createApi}
211 */
212-export function requestMonitor<Ctx extends ApiCtx = ApiCtx>(
213- errorFn?: (ctx: Ctx) => string,
214-) {
215+export function requestMonitor<Ctx extends ApiCtx = ApiCtx>() {
216 return compose<Ctx>([
217 errorHandler,
218 queryCtx,
219 urlParser,
220- dispatchActions,
221- loadingMonitor(errorFn),
222- simpleCache,
223 customKey,
224 ]);
225 }
226@@ -339,9 +153,7 @@ export function* performanceMonitor<Ctx extends PerfCtx = PerfCtx>(
227 /**
228 * This middleware will call the `saga` provided with the action sent to the middleware pipeline.
229 */
230-export function wrap<Ctx extends PipeCtx = PipeCtx>(
231- op: (a: Action) => OpFn,
232-) {
233+export function wrap<Ctx extends PipeCtx = PipeCtx>(op: (a: Action) => OpFn) {
234 return function* (ctx: Ctx, next: Next) {
235 yield* call(() => op(ctx.action));
236 yield* next();
+0,
-5
1@@ -1,11 +1,6 @@
2-export { BATCH, batchActions } from "../deps.ts";
3-
4 export * from "./pipe.ts";
5 export * from "./api.ts";
6 export * from "./types.ts";
7 export * from "./fetch.ts";
8 export * from "./middleware.ts";
9-export * from "./constant.ts";
10-export * from "./slice.ts";
11 export * from "./create-key.ts";
12-export * from "./supervisor.ts";
+64,
-85
1@@ -1,21 +1,20 @@
2 import { assertLike, asserts, describe, it } from "../test.ts";
3 import { call } from "../fx/mod.ts";
4-import { configureStore, put } from "../redux/mod.ts";
5-import { createReducerMap, createTable, sleep as delay } from "../deps.ts";
6-import type { Action, MapEntity } from "../deps.ts";
7+import { configureStore, put } from "../store/mod.ts";
8+import { sleep as delay } from "../deps.ts";
9+import type { QueryState } from "../types.ts";
10+import { createQueryState } from "../action.ts";
11
12+import { sleep } from "../test.ts";
13 import { createPipe } from "./pipe.ts";
14 import type { Next, PipeCtx } from "./types.ts";
15-import { sleep } from "./util.ts";
16-import { createQueryState } from "./slice.ts";
17-import { OpFn } from "../types.ts";
18+import { updateStore } from "../store/fx.ts";
19
20 // deno-lint-ignore no-explicit-any
21 interface RoboCtx<D = Record<string, unknown>, P = any> extends PipeCtx<P> {
22 url: string;
23 request: { method: string; body?: Record<string, unknown> };
24 response: D;
25- actions: Action[];
26 }
27
28 interface User {
29@@ -55,9 +54,10 @@ const deserializeTicket = (u: TicketResponse): Ticket => {
30 };
31 };
32
33-const users = createTable<User>({ name: "USER" });
34-const tickets = createTable<Ticket>({ name: "TICKET" });
35-const reducers = createReducerMap(users, tickets);
36+interface TestState extends QueryState {
37+ users: { [key: string]: User };
38+ tickets: { [key: string]: Ticket };
39+}
40
41 const mockUser = { id: "1", name: "test", email_address: "test@test.com" };
42 const mockTicket = { id: "2", name: "test-ticket" };
43@@ -88,21 +88,18 @@ function* onFetchApi(ctx: RoboCtx, next: Next) {
44 yield* next();
45 }
46
47-function* setupActionState(ctx: RoboCtx, next: Next) {
48- ctx.actions = [];
49- yield* next();
50-}
51-
52 function* processUsers(ctx: RoboCtx<{ users?: UserResponse[] }>, next: Next) {
53 if (!ctx.response.users) {
54 yield* next();
55 return;
56 }
57- const curUsers = ctx.response.users.reduce<MapEntity<User>>((acc, u) => {
58- acc[u.id] = deserializeUser(u);
59- return acc;
60- }, {});
61- ctx.actions.push(users.actions.add(curUsers));
62+ yield* updateStore<TestState>((state) => {
63+ if (!ctx.response.users) return;
64+ ctx.response.users.forEach((u) => {
65+ state.users[u.id] = deserializeUser(u);
66+ });
67+ });
68+
69 yield* next();
70 }
71
72@@ -114,71 +111,55 @@ function* processTickets(
73 yield* next();
74 return;
75 }
76- const curTickets = ctx.response.tickets.reduce<MapEntity<Ticket>>(
77- (acc, u) => {
78- acc[u.id] = deserializeTicket(u);
79- return acc;
80- },
81- {},
82- );
83- ctx.actions.push(tickets.actions.add(curTickets));
84- yield* next();
85-}
86+ yield* updateStore<TestState>((state) => {
87+ if (!ctx.response.tickets) return;
88+ ctx.response.tickets.forEach((u) => {
89+ state.tickets[u.id] = deserializeTicket(u);
90+ });
91+ });
92
93-function* saveToRedux(ctx: RoboCtx, next: Next) {
94- for (let i = 0; i < ctx.actions.length; i += 1) {
95- const action = ctx.actions[i];
96- yield* put(action);
97- }
98 yield* next();
99 }
100
101-function setupStore(op: OpFn) {
102- const { store, fx } = configureStore({ reducers });
103- return { store, run: () => fx.run(op) };
104-}
105-
106 const tests = describe("createPipe()");
107
108 it(
109 tests,
110 "when create a query fetch pipeline - execute all middleware and save to redux",
111- () => {
112+ async () => {
113 const api = createPipe<RoboCtx>();
114 api.use(api.routes());
115 api.use(convertNameToUrl);
116 api.use(onFetchApi);
117- api.use(setupActionState);
118 api.use(processUsers);
119 api.use(processTickets);
120- api.use(saveToRedux);
121 const fetchUsers = api.create(`/users`);
122
123- const { store, run } = setupStore(api.bootup);
124- run();
125+ const store = await configureStore<TestState>({
126+ initialState: { ...createQueryState(), users: {}, tickets: {} },
127+ });
128+ store.run(api.bootup);
129
130 store.dispatch(fetchUsers());
131
132 asserts.assertEquals(store.getState(), {
133 ...createQueryState(),
134- [users.name]: { [mockUser.id]: deserializeUser(mockUser) },
135- [tickets.name]: {},
136+ users: { [mockUser.id]: deserializeUser(mockUser) },
137+ tickets: {},
138 });
139 },
140 );
141
142-it(
143+it.only(
144 tests,
145 "when providing a generator the to api.create function - should call that generator before all other middleware",
146- () => {
147+ async () => {
148 const api = createPipe<RoboCtx>();
149 api.use(api.routes());
150 api.use(convertNameToUrl);
151 api.use(onFetchApi);
152- api.use(setupActionState);
153 api.use(processUsers);
154 api.use(processTickets);
155- api.use(saveToRedux);
156 const fetchUsers = api.create(`/users`);
157 const fetchTickets = api.create(`/ticket-wrong-url`, function* (ctx, next) {
158 // before middleware has been triggered
159@@ -187,28 +168,24 @@ it(
160 // triggers all middleware
161 yield* next();
162
163- // after middleware has been triggered
164- asserts.assertEquals(ctx.actions, [
165- tickets.actions.add({
166- [mockTicket.id]: deserializeTicket(mockTicket),
167- }),
168- ]);
169 yield* put(fetchUsers());
170 });
171
172- const { store, run } = setupStore(api.bootup);
173- run();
174+ const store = await configureStore<TestState>({
175+ initialState: { ...createQueryState(), users: {}, tickets: {} },
176+ });
177+ store.run(api.bootup);
178
179 store.dispatch(fetchTickets());
180 asserts.assertEquals(store.getState(), {
181 ...createQueryState(),
182- [users.name]: { [mockUser.id]: deserializeUser(mockUser) },
183- [tickets.name]: { [mockTicket.id]: deserializeTicket(mockTicket) },
184+ users: { [mockUser.id]: deserializeUser(mockUser) },
185+ tickets: { [mockTicket.id]: deserializeTicket(mockTicket) },
186 });
187 },
188 );
189
190-it(tests, "error handling", () => {
191+it(tests, "error handling", async () => {
192 const api = createPipe<RoboCtx>();
193 api.use(api.routes());
194 api.use(function* upstream(_, next) {
195@@ -223,12 +200,13 @@ it(tests, "error handling", () => {
196 });
197
198 const action = api.create(`/error`);
199- const { store, run } = setupStore(api.bootup);
200- run();
201+
202+ const store = await configureStore({ initialState: {} });
203+ store.run(api.bootup);
204 store.dispatch(action());
205 });
206
207-it(tests, "error handling inside create", () => {
208+it(tests, "error handling inside create", async () => {
209 const api = createPipe<RoboCtx>();
210 api.use(api.routes());
211 api.use(function* fail() {
212@@ -242,12 +220,12 @@ it(tests, "error handling inside create", () => {
213 asserts.assert(true);
214 }
215 });
216- const { store, run } = setupStore(api.bootup);
217- run();
218+ const store = await configureStore({ initialState: {} });
219+ store.run(api.bootup);
220 store.dispatch(action());
221 });
222
223-it(tests, "error handling - error handler", () => {
224+it(tests, "error handling - error handler", async () => {
225 const api = createPipe<RoboCtx>();
226 api.use(api.routes());
227 api.use(function* upstream() {
228@@ -255,12 +233,13 @@ it(tests, "error handling - error handler", () => {
229 });
230
231 const action = api.create(`/error`);
232- const { store, run } = setupStore(api.bootup);
233- run();
234+ const store = await configureStore({ initialState: {} });
235+ store.run(api.bootup);
236+
237 store.dispatch(action());
238 });
239
240-it(tests, "create fn is an array", () => {
241+it(tests, "create fn is an array", async () => {
242 const api = createPipe<RoboCtx>();
243 api.use(api.routes());
244 api.use(function* (ctx, next) {
245@@ -285,12 +264,12 @@ it(tests, "create fn is an array", () => {
246 },
247 ]);
248
249- const { store, run } = setupStore(api.bootup);
250- run();
251+ const store = await configureStore({ initialState: {} });
252+ store.run(api.bootup);
253 store.dispatch(action());
254 });
255
256-it(tests, "run() on endpoint action - should run the effect", () => {
257+it(tests, "run() on endpoint action - should run the effect", async () => {
258 const api = createPipe<RoboCtx>();
259 api.use(api.routes());
260 let acc = "";
261@@ -316,8 +295,8 @@ it(tests, "run() on endpoint action - should run the effect", () => {
262 });
263 });
264
265- const { store, run } = setupStore(api.bootup);
266- run();
267+ const store = await configureStore({ initialState: {} });
268+ store.run(api.bootup);
269 store.dispatch(action2());
270 });
271
272@@ -348,8 +327,8 @@ it(tests, "middleware order of execution", async () => {
273 acc += "g";
274 });
275
276- const { store, run } = setupStore(api.bootup);
277- run();
278+ const store = await configureStore({ initialState: {} });
279+ store.run(api.bootup);
280 store.dispatch(action());
281
282 await sleep(150);
283@@ -374,8 +353,8 @@ it.ignore(tests, "retry with actionFn", async () => {
284 }
285 });
286
287- const { store, run } = setupStore(api.bootup);
288- run();
289+ const store = await configureStore({ initialState: {} });
290+ store.run(api.bootup);
291 store.dispatch(action());
292
293 await sleep(150);
294@@ -400,8 +379,8 @@ it.ignore(tests, "retry with actionFn with payload", async () => {
295 acc += "g";
296 });
297
298- const { store, run } = setupStore(api.bootup);
299- const task = run();
300+ const store = await configureStore({ initialState: {} });
301+ const task = store.run(api.bootup);
302 store.dispatch(action({ page: 1 }));
303
304 await sleep(150);
305@@ -409,7 +388,7 @@ it.ignore(tests, "retry with actionFn with payload", async () => {
306 await task;
307 });
308
309-it.only(tests, "should only call thunk once", () => {
310+it(tests, "should only call thunk once", async () => {
311 const api = createPipe<RoboCtx>();
312 api.use(api.routes());
313 let acc = "";
314@@ -423,8 +402,8 @@ it.only(tests, "should only call thunk once", () => {
315 yield* put(action1(1));
316 });
317
318- const { store, run } = setupStore(api.bootup);
319- run();
320+ const store = await configureStore({ initialState: {} });
321+ store.run(api.bootup);
322 store.dispatch(action2());
323 asserts.assertEquals(acc, "a");
324 });
+7,
-6
1@@ -1,9 +1,11 @@
2 import { call } from "../fx/mod.ts";
3-import { takeEvery } from "../redux/mod.ts";
4 import { compose } from "../compose.ts";
5-import type { OpFn } from "../types.ts";
6+import type { OpFn, Payload } from "../types.ts";
7 import { parallel } from "../mod.ts";
8
9+// TODO: remove store deps
10+import { takeEvery } from "../store/mod.ts";
11+
12 import { isFn, isObject } from "./util.ts";
13 import { createKey } from "./create-key.ts";
14 import type {
15@@ -14,11 +16,10 @@ import type {
16 Middleware,
17 MiddlewareCo,
18 Next,
19- Payload,
20 PipeCtx,
21 Supervisor,
22 } from "./types.ts";
23-import { API_ACTION_PREFIX } from "./constant.ts";
24+import { API_ACTION_PREFIX } from "../action.ts";
25
26 export interface SagaApi<Ctx extends PipeCtx> {
27 use: (fn: Middleware<Ctx>) => void;
28@@ -213,8 +214,8 @@ export function createPipe<Ctx extends PipeCtx = PipeCtx<any>>(
29 }
30
31 function* bootup() {
32- const results = yield* parallel(Object.values(visors));
33- yield* results;
34+ const group = yield* parallel(Object.values(visors));
35+ return yield* group;
36 }
37
38 function routes() {
+20,
-19
1@@ -7,13 +7,8 @@
2
3 import { React } from "../deps.ts";
4 import { asserts, beforeEach, describe, it } from "../test.ts";
5-import {
6- createAssign,
7- Provider,
8- sleep as delay,
9- useSelector,
10-} from "../deps.ts";
11-import { configureStore } from "../redux/mod.ts";
12+import { Provider, sleep as delay, useSelector } from "../deps.ts";
13+import { configureStore, updateStore } from "../store/mod.ts";
14
15 import { createApi } from "./api.ts";
16 import { requestMonitor } from "./middleware.ts";
17@@ -29,12 +24,13 @@ const jsonBlob = (data: any) => {
18 return JSON.stringify(data);
19 };
20
21-const setupTest = () => {
22- const slice = createAssign({
23- name: "user",
24- initialState: { id: "", email: "" },
25- });
26+interface User {
27+ id: string;
28+ name: string;
29+ email: string;
30+}
31
32+const setupTest = async () => {
33 const api = createApi();
34 api.use(requestMonitor());
35 api.use(api.routes());
36@@ -49,11 +45,15 @@ const setupTest = () => {
37 ctx.cache = true;
38 yield next();
39 if (!ctx.json.ok) return;
40- slice.actions.set(ctx.json.data);
41+ yield* updateStore<{ user: User }>((state) => {
42+ state.user = ctx.json.data;
43+ });
44 });
45
46- const { store, fx } = configureStore({ reducers: { user: slice.reducer } });
47- fx.run(api.bootup);
48+ const store = await configureStore<{ user?: User }>({
49+ initialState: {},
50+ });
51+ store.run(api.bootup);
52
53 return { store, fetchUser, api };
54 };
55@@ -61,13 +61,13 @@ const setupTest = () => {
56 describe.ignore("useApi()", () => {
57 beforeEach(() => cleanup());
58 it("with action", async () => {
59- const { fetchUser, store } = setupTest();
60+ const { fetchUser, store } = await setupTest();
61 const App = () => {
62 const action = fetchUser({ id: "1" });
63 const query = useApi(action);
64 const user = useSelector((s: any) =>
65 selectDataById(s, { id: action.payload.key })
66- );
67+ ) as User;
68
69 return h("div", null, [
70 h("div", { key: "1" }, user?.email || ""),
71@@ -91,13 +91,14 @@ describe.ignore("useApi()", () => {
72 });
73
74 it("with action creator", async () => {
75- const { fetchUser, store } = setupTest();
76+ const { fetchUser, store } = await setupTest();
77 const App = () => {
78 const query = useApi(fetchUser);
79 const user = useSelector((s: any) => {
80 const id = createKey(`${fetchUser}`, { id: "1" });
81 return selectDataById(s, { id });
82- });
83+ }) as User;
84+
85 return h("div", null, [
86 h("div", { key: "1" }, user?.email || "no user"),
87 h(
+5,
-5
1@@ -1,9 +1,9 @@
2-import type { LoadingState } from "../deps.ts";
3+import type { LoadingState, QueryState } from "../types.ts";
4 import { React, useDispatch, useSelector } from "../deps.ts";
5 const { useState, useEffect } = React;
6
7-import type { QueryState } from "./slice.ts";
8-import { selectDataById, selectLoaderById } from "./slice.ts";
9+// TODO: remove store deps
10+import { selectDataById, selectLoaderById } from "../store/mod.ts";
11
12 type ActionFn<P = any> = (p: P) => { toString: () => string };
13 type ActionFnSimple = () => { toString: () => string };
14@@ -102,7 +102,7 @@ export function useApi<P = any, A extends SagaAction = SagaAction<P>>(
15 export function useApi<A extends SagaAction = SagaAction>(
16 action: ActionFnSimple,
17 ): UseApiSimpleProps;
18-export function useApi(action: any) {
19+export function useApi(action: any): any {
20 const dispatch = useDispatch();
21 const loader = useLoader(action);
22 const trigger = (p: any) => {
23@@ -166,7 +166,7 @@ export function useCache<D = any, A extends SagaAction = SagaAction>(
24 action: A,
25 ): UseCacheResult<D, A> {
26 const id = action.payload.key;
27- const data = useSelector((s: any) => selectDataById(s, { id }));
28+ const data: any = useSelector((s: any) => selectDataById(s, { id }));
29 const query = useQuery(action);
30 return { ...query, data: data || null };
31 }
+0,
-50
1@@ -1,50 +0,0 @@
2-import { createLoaderTable, createReducerMap, createTable } from "../deps.ts";
3-import type { LoadingItemState } from "../deps.ts";
4-export { defaultLoader, defaultLoadingItem } from "../deps.ts";
5-
6-import { createKey } from "./create-key.ts";
7-
8-export interface QueryState {
9- "@@saga-query/loaders": { [key: string]: LoadingItemState };
10- "@@saga-query/data": { [key: string]: any };
11-}
12-
13-export const LOADERS_NAME = `@@saga-query/loaders`;
14-export const loaders = createLoaderTable({ name: LOADERS_NAME });
15-export const {
16- loading: setLoaderStart,
17- error: setLoaderError,
18- success: setLoaderSuccess,
19- resetById: resetLoaderById,
20-} = loaders.actions;
21-export const { selectTable: selectLoaders, selectById: selectLoaderById } =
22- loaders.getSelectors((state: any) => state[LOADERS_NAME] || {});
23-
24-export const DATA_NAME = `@@saga-query/data`;
25-export const data = createTable<any>({ name: DATA_NAME });
26-export const { add: addData, reset: resetData } = data.actions;
27-
28-export const { selectTable: selectData, selectById: selectDataById } = data
29- .getSelectors((s: any) => s[DATA_NAME] || {});
30-
31-/**
32- * Returns data from the saga-query slice of redux from an action.
33- */
34-export const selectDataByName = (
35- s: any,
36- p: { name: string; payload?: any },
37-) => {
38- const id = createKey(p.name, p.payload);
39- const data = selectDataById(s, { id });
40- return data;
41-};
42-
43-export const reducers = createReducerMap(loaders, data);
44-
45-export const createQueryState = (s: Partial<QueryState> = {}): QueryState => {
46- return {
47- [LOADERS_NAME]: {},
48- [DATA_NAME]: {},
49- ...s,
50- };
51-};
+3,
-13
1@@ -1,18 +1,8 @@
2-import type {
3- LoadingItemState,
4- LoadingMapPayload,
5- LoadingState,
6- Operation,
7-} from "../deps.ts";
8-
9-export type { LoadingItemState, LoadingState };
10+import type { Operation } from "../deps.ts";
11+import type { LoadingItemState, LoadingPayload, Payload } from "../types.ts";
12
13 type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
14
15-export interface Payload<P = any> {
16- payload: P;
17-}
18-
19 export interface PipeCtx<P = any> extends Payload<P> {
20 name: string;
21 key: string;
22@@ -65,7 +55,7 @@ export interface FetchJsonCtx<P = any, ApiSuccess = any, ApiError = any>
23 export interface ApiCtx<Payload = any, ApiSuccess = any, ApiError = any>
24 extends FetchJsonCtx<Payload, ApiSuccess, ApiError> {
25 actions: Action[];
26- loader: LoadingMapPayload<Record<string, any>> | null;
27+ loader: LoadingPayload | null;
28 cache: boolean;
29 cacheData: any;
30 }
+1,
-3
1@@ -1,7 +1,5 @@
2 import { describe, expect, it } from "../test.ts";
3-
4-import { createAction } from "./util.ts";
5-import { API_ACTION_PREFIX } from "./constant.ts";
6+import { API_ACTION_PREFIX, createAction } from "../action.ts";
7
8 const tests = describe("createAction()");
9
+0,
-15
1@@ -1,18 +1,10 @@
2 import type { ApiRequest, RequiredApiRequest } from "./types.ts";
3-import { API_ACTION_PREFIX } from "./constant.ts";
4
5 export const noop = () => {};
6 // deno-lint-ignore no-explicit-any
7 export const isFn = (fn?: any) => fn && typeof fn === "function";
8 // deno-lint-ignore no-explicit-any
9 export const isObject = (obj?: any) => typeof obj === "object" && obj !== null;
10-export const createAction = (curType: string) => {
11- if (!curType) throw new Error("createAction requires non-empty string");
12- const type = `${API_ACTION_PREFIX}/${curType}`;
13- const action = () => ({ type });
14- action.toString = () => type;
15- return action;
16-};
17
18 export const mergeHeaders = (
19 cur?: HeadersInit,
20@@ -39,10 +31,3 @@ export const mergeRequest = (
21 headers: mergeHeaders(cur?.headers, next?.headers),
22 };
23 };
24-
25-export const sleep = (n: number) =>
26- new Promise<void>((resolve) => {
27- setTimeout(() => {
28- resolve();
29- }, n);
30- });
M
react.ts
+1,
-44
1@@ -1,45 +1,2 @@
2-import { React } from "./deps.ts";
3-const { createContext, createElement: h, useContext } = React;
4-import type { Action, Operation, Scope } from "./deps.ts";
5-import { ActionContext } from "./redux/mod.ts";
6-
7 export * from "./query/react.ts";
8-
9-const ScopeContext = createContext<Scope | null>(null);
10-
11-export function Provider({
12- scope,
13- children,
14-}: {
15- scope: Scope;
16- children: React.ReactNode;
17-}) {
18- return h(ScopeContext.Provider, { value: scope }, children);
19-}
20-
21-export function useScope(): Scope {
22- const scope = useContext(ScopeContext);
23- if (!scope) {
24- throw new Error("scope is null");
25- }
26- return scope;
27-}
28-
29-/**
30- * This hook dispatches actions directly to the Action channel we use
31- * for redux. This makes it so you don't have to dispatch a redux action
32- * in order to trigger an fx.
33- */
34-export function useDispatchFx() {
35- const scope = useScope();
36- return (action: Action) =>
37- scope.run(function* () {
38- const { input } = yield* ActionContext;
39- yield* input.send(action);
40- });
41-}
42-
43-export function useFx<T>(op: () => Operation<T>) {
44- const scope = useScope();
45- return scope.run(op);
46-}
47+export * from "./store/react.ts";
+124,
-0
1@@ -0,0 +1,124 @@
2+import { Action, AnyAction, Channel, Operation } from "../deps.ts";
3+import { createChannel, createContext, spawn } from "../deps.ts";
4+import { call, parallel } from "../fx/mod.ts";
5+import { ActionPattern, matcher } from "../matcher.ts";
6+
7+export interface ActionWPayload<P> {
8+ type: string;
9+ payload: P;
10+}
11+
12+export interface StoreLike<S = unknown> {
13+ getState: () => S;
14+ dispatch: (action: Action) => void;
15+}
16+
17+export const ActionContext = createContext<Channel<Action, void>>(
18+ "redux:action",
19+ createChannel<Action, void>(),
20+);
21+
22+export const StoreContext = createContext<StoreLike>("redux:store");
23+
24+export function* emit({
25+ channel,
26+ action,
27+}: {
28+ channel: Operation<Channel<AnyAction, void>>;
29+ action: AnyAction | AnyAction[];
30+}) {
31+ const { input } = yield* channel;
32+ if (Array.isArray(action)) {
33+ if (action.length === 0) {
34+ return;
35+ }
36+ yield* parallel(action.map((a) => () => input.send(a)));
37+ } else {
38+ yield* input.send(action);
39+ }
40+}
41+
42+export function* once({
43+ channel,
44+ pattern,
45+}: {
46+ channel: Operation<Channel<Action, void>>;
47+ pattern: ActionPattern;
48+}) {
49+ const { output } = yield* channel;
50+ const msgList = yield* output;
51+ let next = yield* msgList.next();
52+ while (!next.done) {
53+ const match = matcher(pattern);
54+ if (match(next.value)) {
55+ return next.value;
56+ }
57+ next = yield* msgList.next();
58+ }
59+}
60+
61+export function* select<S, R>(selectorFn: (s: S) => R) {
62+ const store = yield* StoreContext;
63+ return selectorFn(store.getState() as S);
64+}
65+
66+export function take<P>(pattern: ActionPattern): Operation<ActionWPayload<P>>;
67+export function* take(pattern: ActionPattern): Operation<Action> {
68+ const action = yield* once({
69+ channel: ActionContext,
70+ pattern,
71+ });
72+ return action as Action;
73+}
74+
75+export function* takeEvery<T>(
76+ pattern: ActionPattern,
77+ op: (action: Action) => Operation<T>,
78+) {
79+ return yield* spawn(function* () {
80+ while (true) {
81+ const action = yield* take(pattern);
82+ if (!action) continue;
83+ yield* spawn(() => op(action));
84+ }
85+ });
86+}
87+
88+export function* takeLatest<T>(
89+ pattern: ActionPattern,
90+ op: (action: Action) => Operation<T>,
91+) {
92+ return yield* spawn(function* () {
93+ let lastTask;
94+ while (true) {
95+ const action = yield* take(pattern);
96+ if (lastTask) {
97+ yield* lastTask.halt();
98+ }
99+ if (!action) continue;
100+ lastTask = yield* spawn(() => op(action));
101+ }
102+ });
103+}
104+
105+export function* takeLeading<T>(
106+ pattern: ActionPattern,
107+ op: (action: Action) => Operation<T>,
108+) {
109+ return yield* spawn(function* () {
110+ while (true) {
111+ const action = yield* take(pattern);
112+ if (!action) continue;
113+ yield* call(() => op(action));
114+ }
115+ });
116+}
117+
118+export function* put(action: AnyAction | AnyAction[]) {
119+ const store = yield* StoreContext;
120+ if (Array.isArray(action)) {
121+ action.map((act) => store.dispatch(act));
122+ } else {
123+ store.dispatch(action);
124+ }
125+}
+0,
-43
1@@ -1,43 +0,0 @@
2-import type { Action } from "../deps.ts";
3-
4-type ActionType = string;
5-type GuardPredicate<G extends T, T = unknown> = (arg: T) => arg is G;
6-type Predicate = (action: Action) => boolean;
7-type StringableActionCreator<A extends Action = Action> = {
8- (...args: unknown[]): A;
9- toString(): string;
10-};
11-type SubPattern = Predicate | StringableActionCreator | ActionType;
12-export type Pattern = SubPattern | SubPattern[];
13-type ActionSubPattern<Guard extends Action = Action> =
14- | GuardPredicate<Guard, Action>
15- | StringableActionCreator<Guard>
16- | Predicate
17- | ActionType;
18-export type ActionPattern<Guard extends Action = Action> =
19- | ActionSubPattern<Guard>
20- | ActionSubPattern<Guard>[];
21-
22-export function matcher(pattern: ActionPattern): (input: Action) => boolean {
23- if (pattern === "*") {
24- return (input: Action) => !!input;
25- }
26-
27- if (typeof pattern === "string") {
28- return (input: Action) => pattern === input.type;
29- }
30-
31- if (Array.isArray(pattern)) {
32- return (input: Action) => pattern.some((p) => matcher(p)(input));
33- }
34-
35- if (typeof pattern === "function" && Object.hasOwn(pattern, "toString")) {
36- return (input: Action) => pattern.toString() === input.type;
37- }
38-
39- if (typeof pattern === "function") {
40- return (input: Action) => pattern(input) as boolean;
41- }
42-
43- throw new Error("invalid pattern");
44-}
+117,
-0
1@@ -0,0 +1,117 @@
2+import {
3+ Action,
4+ AnyAction,
5+ BATCH,
6+ ConfigureEnhancersCallback,
7+ Middleware,
8+ ReducersMapObject,
9+ Scope,
10+ StoreEnhancer,
11+} from "../deps.ts";
12+import {
13+ combineReducers,
14+ configureStore as reduxStore,
15+ createScope,
16+ enableBatching,
17+} from "../deps.ts";
18+import type { OpFn } from "../types.ts";
19+import { call, parallel } from "../fx/mod.ts";
20+
21+import { ActionContext, emit, StoreContext, StoreLike } from "./fx.ts";
22+import { reducers as queryReducers } from "./query.ts";
23+
24+function* send(action: AnyAction) {
25+ if (action.type === BATCH) {
26+ const actions: Action[] = action.payload;
27+ const group = yield* parallel(
28+ actions.map(
29+ (a) =>
30+ function* () {
31+ yield* emit({
32+ channel: ActionContext,
33+ action: a,
34+ });
35+ },
36+ ),
37+ );
38+ yield* group;
39+ } else {
40+ yield* emit({
41+ channel: ActionContext,
42+ action,
43+ });
44+ }
45+}
46+
47+export function createFxMiddleware(scope: Scope = createScope()) {
48+ function run<T>(op: OpFn<T>) {
49+ const task = scope.run(function* runner() {
50+ return yield* call(op);
51+ });
52+
53+ return task;
54+ }
55+
56+ function middleware<S = unknown, T = unknown>(store: StoreLike<S>) {
57+ scope.run(function* () {
58+ yield* StoreContext.set(store);
59+ });
60+
61+ return (next: (a: Action) => T) => (action: Action) => {
62+ const result = next(action); // hit reducers
63+ scope.run(function* () {
64+ yield* send(action);
65+ });
66+ return result;
67+ };
68+ }
69+
70+ return { run, scope, middleware };
71+}
72+
73+// deno-lint-ignore no-explicit-any
74+interface SetupStoreProps<S = any> {
75+ reducers: ReducersMapObject<S>;
76+ middleware?: Middleware[];
77+ enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback;
78+ initialState?: S;
79+}
80+
81+/**
82+ * This function will integrate `starfx` and `redux`.
83+ *
84+ * In order to enable `starfx/query`, it will add some reducers to your `redux`
85+ * store for decoupled loaders and a simple data cache.
86+ *
87+ * It also adds `redux-batched-actions` which is critical for `starfx`.
88+ *
89+ * @example
90+ * ```ts
91+ * import { configureStore } from 'starfx/redux';
92+ *
93+ * const { store, fx } = prepareStore({
94+ * reducers: { users: (state, action) => state },
95+ * });
96+ *
97+ * fx.run(function*() {
98+ * yield* put({ type: 'LOADING' });
99+ * yield* fetch('https://bower.sh');
100+ * yield* put({ type: 'LOADING_COMPLETE' });
101+ * });
102+ * ```
103+ */
104+export function configureStore(
105+ { reducers, middleware = [], enhancers = [], initialState }: SetupStoreProps,
106+) {
107+ const fx = createFxMiddleware();
108+ const rootReducer = combineReducers({ ...queryReducers, ...reducers });
109+ const store = reduxStore({
110+ reducer: enableBatching(rootReducer),
111+ middleware: (getDefaultMiddleware) =>
112+ getDefaultMiddleware().concat([fx.middleware, ...middleware]),
113+ enhancers,
114+ preloadedState: initialState,
115+ });
116+
117+ return { store, fx };
118+}
+5,
-222
1@@ -1,222 +1,5 @@
2-import {
3- Action,
4- AnyAction,
5- BATCH,
6- Channel,
7- ConfigureEnhancersCallback,
8- Middleware,
9- Operation,
10- ReducersMapObject,
11- Scope,
12- StoreEnhancer,
13-} from "../deps.ts";
14-import type { OpFn } from "../types.ts";
15-import {
16- combineReducers,
17- configureStore as reduxStore,
18- createChannel,
19- createContext,
20- createScope,
21- enableBatching,
22- spawn,
23-} from "../deps.ts";
24-import { contextualize } from "../context.ts";
25-import { call, cancel, emit, parallel } from "../fx/mod.ts";
26-import { reducers as queryReducers } from "../query/mod.ts";
27-
28-import { ActionPattern, matcher } from "./matcher.ts";
29-
30-export interface ActionWPayload<P> {
31- type: string;
32- payload: P;
33-}
34-
35-export interface StoreLike<S = unknown> {
36- getState: () => S;
37- dispatch: (action: Action) => void;
38-}
39-
40-export const ActionContext = createContext<Channel<Action, void>>(
41- "redux:action",
42- createChannel<Action, void>(),
43-);
44-
45-export const StoreContext = createContext<StoreLike>("redux:store");
46-
47-export function* once({
48- channel,
49- pattern,
50-}: {
51- channel: Operation<Channel<Action, void>>;
52- pattern: ActionPattern;
53-}) {
54- const { output } = yield* channel;
55- const msgList = yield* output;
56- let next = yield* msgList.next();
57- while (!next.done) {
58- const match = matcher(pattern);
59- if (match(next.value)) {
60- return next.value;
61- }
62- next = yield* msgList.next();
63- }
64-}
65-
66-export function* select<S, R>(selectorFn: (s: S) => R) {
67- const store = yield* StoreContext;
68- return selectorFn(store.getState() as S);
69-}
70-
71-export function take<P>(pattern: ActionPattern): Operation<ActionWPayload<P>>;
72-export function* take(pattern: ActionPattern): Operation<Action> {
73- const action = yield* once({
74- channel: ActionContext,
75- pattern,
76- });
77- return action as Action;
78-}
79-
80-export function* takeEvery<T>(
81- pattern: ActionPattern,
82- op: (action: Action) => Operation<T>,
83-) {
84- return yield* spawn(function* () {
85- while (true) {
86- const action = yield* take(pattern);
87- if (!action) continue;
88- yield* spawn(() => op(action));
89- }
90- });
91-}
92-
93-export function* takeLatest<T>(
94- pattern: ActionPattern,
95- op: (action: Action) => Operation<T>,
96-) {
97- return yield* spawn(function* () {
98- let lastTask;
99- while (true) {
100- const action = yield* take(pattern);
101- if (lastTask) {
102- yield* cancel(lastTask);
103- }
104- if (!action) continue;
105- lastTask = yield* spawn(() => op(action));
106- }
107- });
108-}
109-
110-export function* takeLeading<T>(
111- pattern: ActionPattern,
112- op: (action: Action) => Operation<T>,
113-) {
114- return yield* spawn(function* () {
115- while (true) {
116- const action = yield* take(pattern);
117- if (!action) continue;
118- yield* call(() => op(action));
119- }
120- });
121-}
122-
123-export function* put(action: AnyAction | AnyAction[]) {
124- const store = yield* StoreContext;
125- if (Array.isArray(action)) {
126- action.map((act) => store.dispatch(act));
127- } else {
128- store.dispatch(action);
129- }
130-}
131-
132-function* send(action: AnyAction) {
133- if (action.type === BATCH) {
134- const actions: Action[] = action.payload;
135- yield* parallel(
136- actions.map(
137- (a) =>
138- function* () {
139- yield* emit({
140- channel: ActionContext,
141- action: a,
142- });
143- },
144- ),
145- );
146- } else {
147- yield* emit({
148- channel: ActionContext,
149- action,
150- });
151- }
152-}
153-
154-export function createFxMiddleware(scope: Scope = createScope()) {
155- function run<T>(op: OpFn<T>) {
156- const task = scope.run(function* runner() {
157- return yield* call(op);
158- });
159-
160- return task;
161- }
162-
163- function middleware<S = unknown, T = unknown>(store: StoreLike<S>) {
164- scope.run(function* () {
165- yield* contextualize("redux:store", store);
166- });
167-
168- return (next: (a: Action) => T) => (action: Action) => {
169- const result = next(action); // hit reducers
170- scope.run(function* () {
171- yield* send(action);
172- });
173- return result;
174- };
175- }
176-
177- return { run, scope, middleware };
178-}
179-
180-// deno-lint-ignore no-explicit-any
181-interface SetupStoreProps<S = any> {
182- reducers: ReducersMapObject<S>;
183- middleware?: Middleware[];
184- enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback;
185-}
186-
187-/**
188- * This function will integrate `starfx` and `redux`.
189- *
190- * In order to enable `starfx/query`, it will add some reducers to your `redux`
191- * store for decoupled loaders and a simple data cache.
192- *
193- * It also adds `redux-batched-actions` which is critical for `starfx`.
194- *
195- * @example
196- * ```ts
197- * import { configureStore } from 'starfx/redux';
198- *
199- * const { store, fx } = prepareStore({
200- * reducers: { users: (state, action) => state },
201- * });
202- *
203- * fx.run(function*() {
204- * yield* put({ type: 'LOADING' });
205- * yield* fetch('https://bower.sh');
206- * yield* put({ type: 'LOADING_COMPLETE' });
207- * });
208- * ```
209- */
210-export function configureStore(
211- { reducers, middleware = [], enhancers = [] }: SetupStoreProps,
212-) {
213- const fx = createFxMiddleware();
214- const rootReducer = combineReducers({ ...queryReducers, ...reducers });
215- const store = reduxStore({
216- reducer: enableBatching(rootReducer),
217- middleware: (getDefaultMiddleware) =>
218- getDefaultMiddleware().concat([fx.middleware, ...middleware]),
219- enhancers: enhancers,
220- });
221-
222- return { store, fx };
223-}
224+export * from "./fx.ts";
225+export * from "./query.ts";
226+export * from "./middleware.ts";
227+export type { ActionWPayload, AnyAction, AnyState } from "../store/types.ts";
228+export { createSelector } from "../deps.ts";
+18,
-10
1@@ -1,7 +1,7 @@
2-import { describe, expect, it, setupReduxScope } from "../test.ts";
3+import { describe, expect, it } from "../test.ts";
4 import { sleep, spawn } from "../deps.ts";
5
6-import { ActionContext, put, take } from "./mod.ts";
7+import { ActionContext, configureStore, put, take } from "./mod.ts";
8
9 const putTests = describe("put()");
10
11@@ -27,8 +27,10 @@ it(putTests, "should send actions through channel", async () => {
12 });
13 }
14
15- const scope = setupReduxScope();
16- await scope.run(() => genFn("arg"));
17+ const store = configureStore({
18+ reducers: { def: (s = null, _) => s },
19+ });
20+ await store.fx.run(() => genFn("arg"));
21
22 const expected = ["arg", "2"];
23 expect(actual).toEqual(expected);
24@@ -57,8 +59,10 @@ it(putTests, "should handle nested puts", async () => {
25 yield* spawn(genA);
26 }
27
28- const scope = setupReduxScope();
29- await scope.run(() => root());
30+ const store = configureStore({
31+ reducers: { def: (s = null, _) => s },
32+ });
33+ await store.fx.run(root);
34
35 const expected = ["put b", "put a"];
36 expect(actual).toEqual(expected);
37@@ -75,8 +79,10 @@ it(
38 yield* sleep(0);
39 }
40
41- const scope = setupReduxScope();
42- await scope.run(() => root());
43+ const store = configureStore({
44+ reducers: { def: (s = null, _) => s },
45+ });
46+ await store.fx.run(root);
47 expect(true).toBe(true);
48 },
49 );
50@@ -103,8 +109,10 @@ it(
51 yield* tsk;
52 }
53
54- const scope = setupReduxScope();
55- await scope.run(() => root());
56+ const store = configureStore({
57+ reducers: { def: (s = null, _) => s },
58+ });
59+ await store.fx.run(root);
60 const expected = ["didn't get missed"];
61 expect(actual).toEqual(expected);
62 },
+113,
-0
1@@ -0,0 +1,113 @@
2+import { createLoaderTable, createReducerMap, createTable } from "../deps.ts";
3+import { compose } from "../compose.ts";
4+export { defaultLoader } from "../store/mod.ts";
5+import { dispatchActions } from "../store/mod.ts";
6+import { ApiCtx, createKey, Next } from "../query/mod.ts";
7+import { put, select } from "./mod.ts";
8+import type { QueryState } from "../types.ts";
9+
10+export function reduxMdw<Ctx extends ApiCtx = ApiCtx>(
11+ errorFn?: (ctx: Ctx) => string,
12+) {
13+ return compose<Ctx>([dispatchActions, loadingMonitor(errorFn), simpleCache]);
14+}
15+
16+export const LOADERS_NAME = "@@starfx/loaders";
17+export const loaders = createLoaderTable({ name: LOADERS_NAME });
18+export const {
19+ loading: setLoaderStart,
20+ error: setLoaderError,
21+ success: setLoaderSuccess,
22+ resetById: resetLoaderById,
23+} = loaders.actions;
24+export const { selectTable: selectLoaders, selectById: selectLoaderById } =
25+ loaders.getSelectors((state: any) => state[LOADERS_NAME] || {});
26+
27+export const DATA_NAME = "@@starfx/data";
28+export const data = createTable<any>({ name: DATA_NAME });
29+export const { add: addData, reset: resetData } = data.actions;
30+
31+export const { selectTable: selectData, selectById: selectDataById } = data
32+ .getSelectors((s: any) => s[DATA_NAME] || {});
33+
34+/**
35+ * Returns data from the saga-query slice of redux from an action.
36+ */
37+export const selectDataByName = (
38+ s: any,
39+ p: { name: string; payload?: any },
40+) => {
41+ const id = createKey(p.name, p.payload);
42+ const data = selectDataById(s, { id });
43+ return data;
44+};
45+
46+export const reducers = createReducerMap(loaders, data);
47+
48+/**
49+ * This middleware will automatically cache any data found inside `ctx.json`
50+ * which is where we store JSON data from the `fetcher` middleware.
51+ */
52+export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
53+ ctx: Ctx,
54+ next: Next,
55+) {
56+ ctx.cacheData = yield* select((state: QueryState) =>
57+ selectDataById(state, { id: ctx.key })
58+ );
59+ yield* next();
60+ if (!ctx.cache) return;
61+ const { data } = ctx.json;
62+ yield* put(addData({ [ctx.key]: data }));
63+ ctx.cacheData = data;
64+}
65+
66+/**
67+ * This middleware will track the status of a fetch request.
68+ */
69+export function loadingMonitor<Ctx extends ApiCtx = ApiCtx>(
70+ errorFn: (ctx: Ctx) => string = (ctx) => ctx.json?.data?.message || "",
71+) {
72+ return function* trackLoading(ctx: Ctx, next: Next) {
73+ yield* put([
74+ setLoaderStart({ id: ctx.name }),
75+ setLoaderStart({ id: ctx.key }),
76+ ]);
77+ if (!ctx.loader) ctx.loader = {} as any;
78+
79+ yield* next();
80+
81+ if (!ctx.response) {
82+ yield* put([
83+ resetLoaderById(ctx.name),
84+ resetLoaderById(ctx.key),
85+ ]);
86+ return;
87+ }
88+
89+ if (!ctx.loader) {
90+ ctx.loader || {};
91+ }
92+
93+ if (!ctx.response.ok) {
94+ yield* put([
95+ setLoaderError({
96+ id: ctx.name as any,
97+ message: errorFn(ctx),
98+ ...ctx.loader,
99+ }),
100+ setLoaderError({
101+ id: ctx.key as any,
102+ message: errorFn(ctx),
103+ ...ctx.loader,
104+ }),
105+ ]);
106+ return;
107+ }
108+
109+ yield* put([
110+ setLoaderSuccess({ id: ctx.name as any, ...ctx.loader }),
111+ setLoaderSuccess({ id: ctx.key as any, ...ctx.loader }),
112+ ]);
113+ };
114+}
+1,
-2
1@@ -1,5 +1,4 @@
2 import { describe, expect, it } from "../test.ts";
3-import { cancel } from "../fx/mod.ts";
4 import type { AnyAction } from "../deps.ts";
5
6 import { configureStore, take, takeEvery } from "./mod.ts";
7@@ -17,7 +16,7 @@ it(testEvery, "should work", async () => {
8 (action) => worker("a1", "a2", action),
9 );
10 yield* take("CANCEL_WATCHER");
11- yield* cancel(task);
12+ yield* task.halt();
13 }
14
15 function* worker(arg1: string, arg2: string, action: AnyAction) {
+10,
-6
1@@ -1,7 +1,7 @@
2-import { describe, expect, it, setupReduxScope } from "../test.ts";
3+import { describe, expect, it } from "../test.ts";
4 import { AnyAction, sleep, spawn } from "../deps.ts";
5
6-import { put, take } from "./mod.ts";
7+import { configureStore, put, take } from "./mod.ts";
8
9 const takeTests = describe("take()");
10
11@@ -24,8 +24,10 @@ it(
12 actual.push(yield* take("action-1"));
13 }
14
15- const scope = setupReduxScope();
16- await scope.run(root);
17+ const store = configureStore({
18+ reducers: { def: (s = null, _) => s },
19+ });
20+ await store.fx.run(root);
21
22 expect(actual).toEqual([
23 { type: "action-1", payload: 1 },
24@@ -84,8 +86,10 @@ it(takeTests, "take from default channel", async () => {
25 }
26 }
27
28- const scope = setupReduxScope();
29- await scope.run(genFn);
30+ const store = configureStore({
31+ reducers: { def: (s = null, _) => s },
32+ });
33+ await store.fx.run(genFn);
34
35 const expected = [
36 {
+29,
-0
1@@ -0,0 +1,29 @@
2+import { describe, expect, it } from "../test.ts";
3+import { call } from "../fx/mod.ts";
4+
5+import { select } from "./mod.ts";
6+import { configureStore } from "./store.ts";
7+
8+const tests = describe("configureStore()");
9+
10+interface TestState {
11+ user: { id: string };
12+}
13+
14+it(tests, "should be able to grab values from store", async () => {
15+ const store = await configureStore({ initialState: { user: { id: "1" } } });
16+ await store.run(function* () {
17+ const actual = yield* select((s: TestState) => s.user);
18+ expect(actual).toEqual({ id: "1" });
19+ });
20+});
21+
22+it(tests, "should be able to grab store from a nested call", async () => {
23+ const store = await configureStore({ initialState: { user: { id: "2" } } });
24+ await store.run(function* () {
25+ const actual = yield* call(function* () {
26+ return yield* select((s: TestState) => s.user);
27+ });
28+ expect(actual).toEqual({ id: "2" });
29+ });
30+});
+15,
-0
1@@ -0,0 +1,15 @@
2+import { Channel, createChannel, createContext } from "../deps.ts";
3+
4+import type { AnyAction, AnyState, FxStore } from "./types.ts";
5+
6+export const ActionContext = createContext<Channel<AnyAction, void>>(
7+ "store:action",
8+ createChannel<AnyAction, void>(),
9+);
10+
11+export const StoreUpdateContext = createContext<Channel<void, void>>(
12+ "store:update",
13+ createChannel<void, void>(),
14+);
15+
16+export const StoreContext = createContext<FxStore<AnyState>>("store");
+122,
-0
1@@ -0,0 +1,122 @@
2+import { Channel, Operation, spawn, Task } from "../deps.ts";
3+import { call, parallel } from "../fx/mod.ts";
4+import { ActionPattern, matcher } from "../matcher.ts";
5+
6+import type {
7+ ActionWPayload,
8+ AnyAction,
9+ AnyState,
10+ StoreUpdater,
11+ UpdaterCtx,
12+} from "./types.ts";
13+import { ActionContext, StoreContext } from "./context.ts";
14+
15+export function* updateStore<S extends AnyState>(
16+ updater: StoreUpdater<S> | StoreUpdater<S>[],
17+): Operation<UpdaterCtx<S>> {
18+ const store = yield* StoreContext;
19+ const ctx = yield* store.update(updater as any);
20+ // TODO: fix type
21+ return ctx as any;
22+}
23+
24+export function* emit({
25+ channel,
26+ action,
27+}: {
28+ channel: Operation<Channel<AnyAction, void>>;
29+ action: AnyAction | AnyAction[];
30+}) {
31+ const { input } = yield* channel;
32+ if (Array.isArray(action)) {
33+ if (action.length === 0) {
34+ return;
35+ }
36+ yield* parallel(action.map((a) => () => input.send(a)));
37+ } else {
38+ yield* input.send(action);
39+ }
40+}
41+
42+export function* once({
43+ channel,
44+ pattern,
45+}: {
46+ channel: Operation<Channel<AnyAction, void>>;
47+ pattern: ActionPattern;
48+}) {
49+ const { output } = yield* channel;
50+ const msgList = yield* output;
51+ let next = yield* msgList.next();
52+ while (!next.done) {
53+ const match = matcher(pattern);
54+ if (match(next.value)) {
55+ return next.value;
56+ }
57+ next = yield* msgList.next();
58+ }
59+}
60+
61+export function* select<S, R, P>(selectorFn: (s: S, p?: P) => R, p?: P) {
62+ const store = yield* StoreContext;
63+ return selectorFn(store.getState() as S, p);
64+}
65+
66+export function* put(action: AnyAction | AnyAction[]) {
67+ return yield* emit({
68+ channel: ActionContext,
69+ action,
70+ });
71+}
72+
73+export function take<P>(pattern: ActionPattern): Operation<ActionWPayload<P>>;
74+export function* take(pattern: ActionPattern): Operation<AnyAction> {
75+ const action = yield* once({
76+ channel: ActionContext,
77+ pattern,
78+ });
79+ return action as AnyAction;
80+}
81+
82+export function* takeEvery<T>(
83+ pattern: ActionPattern,
84+ op: (action: AnyAction) => Operation<T>,
85+): Operation<Task<void>> {
86+ return yield* spawn(function* () {
87+ while (true) {
88+ const action = yield* take(pattern);
89+ if (!action) continue;
90+ yield* spawn(() => op(action));
91+ }
92+ });
93+}
94+
95+export function* takeLatest<T>(
96+ pattern: ActionPattern,
97+ op: (action: AnyAction) => Operation<T>,
98+): Operation<Task<void>> {
99+ return yield* spawn(function* () {
100+ let lastTask;
101+ while (true) {
102+ const action = yield* take(pattern);
103+ if (lastTask) {
104+ yield* lastTask.halt();
105+ }
106+ if (!action) continue;
107+ lastTask = yield* spawn(() => op(action));
108+ }
109+ });
110+}
111+
112+export function* takeLeading<T>(
113+ pattern: ActionPattern,
114+ op: (action: AnyAction) => Operation<T>,
115+): Operation<Task<void>> {
116+ return yield* spawn(function* () {
117+ while (true) {
118+ const action = yield* take(pattern);
119+ if (!action) continue;
120+ yield* call(() => op(action));
121+ }
122+ });
123+}
+8,
-0
1@@ -0,0 +1,8 @@
2+export * from "./context.ts";
3+export * from "./fx.ts";
4+export * from "./store.ts";
5+export * from "./types.ts";
6+export * from "./slice.ts";
7+export * from "./query.ts";
8+export * from "./supervisor.ts";
9+export { createSelector } from "../deps.ts";
+112,
-0
1@@ -0,0 +1,112 @@
2+import { describe, expect, it } from "../test.ts";
3+import { sleep, spawn } from "../deps.ts";
4+
5+import { ActionContext, put, take } from "./mod.ts";
6+import { configureStore } from "./store.ts";
7+
8+const putTests = describe("put()");
9+
10+it(putTests, "should send actions through channel", async () => {
11+ const actual: string[] = [];
12+
13+ function* genFn(arg: string) {
14+ yield* spawn(function* () {
15+ const actions = yield* ActionContext;
16+ const msgs = yield* actions.output;
17+ let action = yield* msgs.next();
18+ while (!action.done) {
19+ actual.push(action.value.type);
20+ action = yield* msgs.next();
21+ }
22+ });
23+
24+ yield* put({
25+ type: arg,
26+ });
27+ yield* put({
28+ type: "2",
29+ });
30+ }
31+
32+ const store = await configureStore({ initialState: {} });
33+ await store.run(() => genFn("arg"));
34+
35+ const expected = ["arg", "2"];
36+ expect(actual).toEqual(expected);
37+});
38+
39+it(putTests, "should handle nested puts", async () => {
40+ const actual: string[] = [];
41+
42+ function* genA() {
43+ yield* put({
44+ type: "a",
45+ });
46+ actual.push("put a");
47+ }
48+
49+ function* genB() {
50+ yield* take(["a"]);
51+ yield* put({
52+ type: "b",
53+ });
54+ actual.push("put b");
55+ }
56+
57+ function* root() {
58+ yield* spawn(genB);
59+ yield* spawn(genA);
60+ }
61+
62+ const store = await configureStore({ initialState: {} });
63+ await store.run(() => root());
64+
65+ const expected = ["put b", "put a"];
66+ expect(actual).toEqual(expected);
67+});
68+
69+it(
70+ putTests,
71+ "should not cause stack overflow when puts are emitted while dispatching saga",
72+ async () => {
73+ function* root() {
74+ for (let i = 0; i < 40_000; i += 1) {
75+ yield* put({ type: "test" });
76+ }
77+ yield* sleep(0);
78+ }
79+
80+ const store = await configureStore({ initialState: {} });
81+ await store.run(() => root());
82+ expect(true).toBe(true);
83+ },
84+);
85+
86+it(
87+ putTests,
88+ "should not miss `put` that was emitted directly after creating a task (caused by another `put`)",
89+ async () => {
90+ const actual: string[] = [];
91+
92+ function* root() {
93+ yield* spawn(function* firstspawn() {
94+ yield* sleep(1000);
95+ yield* put({ type: "c" });
96+ yield* put({ type: "do not miss" });
97+ });
98+
99+ yield* take("c");
100+
101+ const tsk = yield* spawn(function* () {
102+ yield* take("do not miss");
103+ actual.push("didn't get missed");
104+ });
105+ yield* tsk;
106+ }
107+
108+ const store = await configureStore({ initialState: {} });
109+ await store.run(() => root());
110+ const expected = ["didn't get missed"];
111+ expect(actual).toEqual(expected);
112+ },
113+);
+195,
-0
1@@ -0,0 +1,195 @@
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 { compose } from "../compose.ts";
6+import type { QueryState } from "../types.ts";
7+import { createAction } from "../action.ts";
8+
9+import { put, select, take, updateStore } from "./fx.ts";
10+import type { AnyAction } from "./types.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+}
25+
26+/**
27+ * This middleware will automatically cache any data found inside `ctx.json`
28+ * which is where we store JSON data from the `fetcher` middleware.
29+ */
30+export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
31+ ctx: Ctx,
32+ next: Next,
33+) {
34+ ctx.cacheData = yield* select((state: QueryState) =>
35+ selectDataById(state, { id: ctx.key })
36+ );
37+ yield* next();
38+ if (!ctx.cache) return;
39+ const { data } = ctx.json;
40+ yield* updateStore(addData({ [ctx.key]: data }));
41+ ctx.cacheData = data;
42+}
43+
44+/**
45+ * This middleware will take the result of `ctx.actions` and dispatch them
46+ * as a single batch.
47+ *
48+ * @remarks This is useful because sometimes there are a lot of actions that need dispatched
49+ * within the pipeline of the middleware and instead of dispatching them serially this
50+ * improves performance by only hitting the reducers once.
51+ */
52+export function* dispatchActions(ctx: { actions: AnyAction[] }, next: Next) {
53+ if (!ctx.actions) ctx.actions = [];
54+ yield* next();
55+ if (ctx.actions.length === 0) return;
56+ yield* put(ctx.actions);
57+}
58+
59+export interface OptimisticCtx<
60+ A extends AnyAction = AnyAction,
61+ R extends AnyAction = AnyAction,
62+> extends ApiCtx {
63+ optimistic: {
64+ apply: A;
65+ revert: R;
66+ };
67+}
68+
69+/**
70+ * This middleware performs an optimistic update for a middleware pipeline.
71+ * It accepts an `apply` and `revert` action.
72+ *
73+ * @remarks This means that we will first `apply` and then if the request is successful we
74+ * keep the change or we `revert` if there's an error.
75+ */
76+export function* optimistic<Ctx extends OptimisticCtx = OptimisticCtx>(
77+ ctx: Ctx,
78+ next: Next,
79+) {
80+ if (!ctx.optimistic) {
81+ yield* next();
82+ return;
83+ }
84+
85+ const { apply, revert } = ctx.optimistic;
86+ // optimistically update user
87+ yield* put(apply);
88+
89+ yield* next();
90+
91+ if (!ctx.response || !ctx.response.ok) {
92+ yield* put(revert);
93+ }
94+}
95+
96+export interface UndoCtx<P = any, S = any, E = any> extends ApiCtx<P, S, E> {
97+ undoable: boolean;
98+}
99+
100+export const doIt = createAction("DO_IT");
101+export const undo = createAction("UNDO");
102+/**
103+ * This middleware will allow pipeline functions to be undoable which means before they are activated
104+ * we have a timeout that allows the function to be cancelled.
105+ */
106+export function undoer<Ctx extends UndoCtx = UndoCtx>(
107+ doItType = `${doIt}`,
108+ undoType = `${undo}`,
109+ timeout = 30 * 1000,
110+) {
111+ return function* onUndo(ctx: Ctx, next: Next) {
112+ if (!ctx.undoable) {
113+ yield* next();
114+ return;
115+ }
116+
117+ const winner = yield* race({
118+ doIt: () => take(`${doItType}`),
119+ undo: () => take(`${undoType}`),
120+ timeout: () => sleep(timeout),
121+ });
122+
123+ if (winner.undo || winner.timeout) {
124+ return;
125+ }
126+
127+ yield* next();
128+ };
129+}
130+
131+/**
132+ * This middleware creates a loader for a generator function which allows us to track
133+ * the status of a pipeline function.
134+ */
135+export function* loadingMonitorSimple<Ctx extends LoaderCtx = LoaderCtx>(
136+ ctx: Ctx,
137+ next: Next,
138+) {
139+ yield* updateStore([
140+ setLoaderStart({ id: ctx.name }),
141+ setLoaderStart({ id: ctx.key }),
142+ ]);
143+
144+ if (!ctx.loader) {
145+ ctx.loader = {};
146+ }
147+
148+ yield* next();
149+
150+ yield* updateStore([
151+ setLoaderSuccess({ ...ctx.loader, id: ctx.name }),
152+ setLoaderSuccess({ ...ctx.loader, id: ctx.key }),
153+ ]);
154+}
155+
156+/**
157+ * This middleware will track the status of a fetch request.
158+ */
159+export function loadingMonitor<Ctx extends ApiCtx = ApiCtx>(
160+ errorFn: (ctx: Ctx) => string = (ctx) => ctx.json?.data?.message || "",
161+) {
162+ return function* trackLoading(ctx: Ctx, next: Next) {
163+ yield* updateStore([
164+ setLoaderStart({ id: ctx.name }),
165+ setLoaderStart({ id: ctx.key }),
166+ ]);
167+ if (!ctx.loader) ctx.loader = {} as any;
168+
169+ yield* next();
170+
171+ if (!ctx.response) {
172+ yield* updateStore([
173+ resetLoaderById({ id: ctx.name }),
174+ resetLoaderById({ id: ctx.key }),
175+ ]);
176+ return;
177+ }
178+
179+ if (!ctx.loader) {
180+ ctx.loader || {};
181+ }
182+
183+ if (!ctx.response.ok) {
184+ yield* updateStore([
185+ setLoaderError({ id: ctx.name, message: errorFn(ctx), ...ctx.loader }),
186+ setLoaderError({ id: ctx.key, message: errorFn(ctx), ...ctx.loader }),
187+ ]);
188+ return;
189+ }
190+
191+ yield* updateStore([
192+ setLoaderSuccess({ id: ctx.name, ...ctx.loader }),
193+ setLoaderSuccess({ id: ctx.key, ...ctx.loader }),
194+ ]);
195+ };
196+}
+1,
-0
1@@ -0,0 +1 @@
2+export { Provider, useDispatch, useSelector } from "../deps.ts";
+91,
-0
1@@ -0,0 +1,91 @@
2+import type {
3+ IdProp,
4+ LoadingItemState,
5+ LoadingPayload,
6+ LoadingState,
7+ LoadingStatus,
8+} from "../types.ts";
9+import type { QueryState } from "../types.ts";
10+
11+export const defaultLoader = (
12+ p: Partial<LoadingItemState> = {},
13+): LoadingItemState => {
14+ return {
15+ id: "",
16+ status: "idle",
17+ message: "",
18+ lastRun: 0,
19+ lastSuccess: 0,
20+ meta: {},
21+ ...p,
22+ };
23+};
24+
25+export const selectLoaderTable = (s: QueryState) => {
26+ return s["@@starfx/loaders"] || {};
27+};
28+
29+const initLoader = defaultLoader();
30+export const selectLoaderById = (
31+ s: QueryState,
32+ { id }: { id: IdProp },
33+): LoadingState => {
34+ const base = selectLoaderTable(s)[id] || initLoader;
35+ return {
36+ ...base,
37+ isIdle: base.status === "idle",
38+ isError: base.status === "error",
39+ isSuccess: base.status === "success",
40+ isLoading: base.status === "loading",
41+ isInitialLoading: (base.status === "idle" || base.status === "loading") &&
42+ base.lastSuccess === 0,
43+ };
44+};
45+
46+const setLoaderState = (status: LoadingStatus) => {
47+ return (props: LoadingPayload) => {
48+ function updateLoadingState(s: QueryState) {
49+ if (!props.id) return;
50+ const loaders = selectLoaderTable(s);
51+ if (!loaders[props.id]) {
52+ loaders[props.id] = defaultLoader({ ...props });
53+ return;
54+ }
55+
56+ const loader = loaders[props.id];
57+ loader.status = status;
58+ if (props.meta) {
59+ loader.meta = props.meta;
60+ }
61+ if (props.message) {
62+ loader.message = props.message;
63+ }
64+ }
65+ return updateLoadingState;
66+ };
67+};
68+export const setLoaderStart = setLoaderState("loading");
69+export const setLoaderSuccess = setLoaderState("success");
70+export const setLoaderError = setLoaderState("error");
71+export const resetLoaderById = ({ id }: { id: string }) => {
72+ function resetLoader(s: QueryState) {
73+ const loaders = selectLoaderTable(s);
74+ delete loaders[id];
75+ }
76+ return resetLoader;
77+};
78+
79+export const selectDataTable = (s: QueryState) => {
80+ return s["@@starfx/data"] || {};
81+};
82+
83+export const selectDataById = (s: QueryState, { id }: { id: IdProp }) => {
84+ return selectDataTable(s)[id];
85+};
86+
87+export const addData = (props: { [key: string]: unknown }) => {
88+ function addDataState(s: QueryState) {
89+ s["@@starfx/data"] = { ...s["@@starfx/data"], ...props };
90+ }
91+ return addDataState;
92+};
+144,
-0
1@@ -0,0 +1,144 @@
2+import { createScope } from "../deps.ts";
3+import { parallel } from "../fx/mod.ts";
4+import { asserts, describe, it } from "../test.ts";
5+
6+import { StoreContext, StoreUpdateContext } from "./context.ts";
7+import { put, take, updateStore } from "./fx.ts";
8+import { createStore, register } from "./store.ts";
9+
10+const tests = describe("store");
11+
12+interface User {
13+ id: string;
14+ name: string;
15+}
16+
17+interface State {
18+ users: { [key: string]: User };
19+ theme: string;
20+ token: string;
21+ dev: boolean;
22+}
23+
24+function findUserById(state: State, { id }: { id: string }) {
25+ return state.users[id];
26+}
27+
28+function findUsers(state: State) {
29+ return state.users;
30+}
31+
32+interface UpdateUserProps {
33+ id: string;
34+ name: string;
35+}
36+
37+const updateUser = ({ id, name }: UpdateUserProps) => (state: State) => {
38+ // use selectors to find the data you want to mutate
39+ const user = findUserById(state, { id });
40+ user.name = name;
41+
42+ // different ways to update a `zod` record
43+ const users = findUsers(state);
44+ users[id].name = name;
45+
46+ delete users[2];
47+ users[3] = { id: "", name: "" };
48+
49+ // or mutate state directly without selectors
50+ state.dev = true;
51+};
52+
53+it(
54+ tests,
55+ "update store and receives update from channel `StoreUpdateContext`",
56+ async () => {
57+ const scope = createScope();
58+ const initialState: Partial<State> = {
59+ users: { 1: { id: "1", name: "testing" }, 2: { id: "2", name: "wow" } },
60+ dev: false,
61+ };
62+ const store = createStore({ scope, initialState });
63+ await register(store);
64+
65+ await scope.run(function* (): any {
66+ const result = yield* parallel([
67+ function* () {
68+ const store = yield* StoreContext;
69+ const chan = yield* StoreUpdateContext;
70+ const msgList = yield* chan.output;
71+ yield* msgList.next();
72+ asserts.assertEquals(store.getState(), {
73+ users: { 1: { id: "1", name: "eric" }, 3: { id: "", name: "" } },
74+ theme: "",
75+ token: null,
76+ dev: true,
77+ });
78+ },
79+
80+ function* () {
81+ yield* updateStore(updateUser({ id: "1", name: "eric" }));
82+ },
83+ ]);
84+
85+ return yield* result;
86+ });
87+ },
88+);
89+
90+it(tests, "update store and receives update from `subscribe()`", async () => {
91+ const scope = createScope();
92+ const initialState: Partial<State> = {
93+ users: { 1: { id: "1", name: "testing" }, 2: { id: "2", name: "wow" } },
94+ dev: false,
95+ theme: "",
96+ token: "",
97+ };
98+ const store = createStore({ scope, initialState });
99+ await register(store);
100+
101+ store.subscribe(() => {
102+ asserts.assertEquals(store.getState(), {
103+ users: { 1: { id: "1", name: "eric" }, 3: { id: "", name: "" } },
104+ theme: "",
105+ token: "",
106+ dev: true,
107+ });
108+ });
109+
110+ await scope.run(function* () {
111+ yield* updateStore(updateUser({ id: "1", name: "eric" }));
112+ });
113+});
114+
115+it(tests, "emit Action and update store", async () => {
116+ const scope = createScope();
117+ const initialState: Partial<State> = {
118+ users: { 1: { id: "1", name: "testing" }, 2: { id: "2", name: "wow" } },
119+ dev: false,
120+ theme: "",
121+ token: "",
122+ };
123+ const store = createStore({ scope, initialState });
124+ await register(store);
125+
126+ await scope.run(function* (): any {
127+ const result = yield* parallel([
128+ function* (): any {
129+ const action = yield* take<UpdateUserProps>("UPDATE_USER");
130+ yield* updateStore(updateUser(action.payload));
131+ },
132+ function* () {
133+ yield* put({ type: "UPDATE_USER", payload: { id: "1", name: "eric" } });
134+ },
135+ ]);
136+ yield* result;
137+ });
138+
139+ asserts.assertEquals(store.getState(), {
140+ users: { 1: { id: "1", name: "eric" }, 3: { id: "", name: "" } },
141+ theme: "",
142+ token: "",
143+ dev: true,
144+ });
145+});
+171,
-0
1@@ -0,0 +1,171 @@
2+import {
3+ createScope,
4+ enablePatches,
5+ produceWithPatches,
6+ Result,
7+ Scope,
8+ Task,
9+} from "../deps.ts";
10+import { BaseMiddleware, compose } from "../compose.ts";
11+import type { OpFn } from "../types.ts";
12+import { call } from "../fx/mod.ts";
13+
14+import type {
15+ AnyAction,
16+ AnyState,
17+ FxStore,
18+ Listener,
19+ StoreUpdater,
20+ UpdaterCtx,
21+} from "./types.ts";
22+import { StoreContext, StoreUpdateContext } from "./context.ts";
23+import { put } from "./fx.ts";
24+import { Next } from "../query/types.ts";
25+
26+const stubMsg = "This is merely a stub, not implemented";
27+
28+// https://github.com/reduxjs/redux/blob/4a6d2fb227ba119d3498a43fab8f53fe008be64c/src/createStore.ts#L344
29+function observable() {
30+ return {
31+ subscribe: (_observer: unknown) => {
32+ throw new Error(stubMsg);
33+ },
34+ [Symbol.observable]() {
35+ return this;
36+ },
37+ };
38+}
39+
40+export interface CreateStore<S extends AnyState> {
41+ scope?: Scope;
42+ initialState: S;
43+ middleware?: BaseMiddleware[];
44+}
45+
46+export function createStore<S extends AnyState>({
47+ initialState,
48+ scope = createScope(),
49+ middleware = [],
50+}: CreateStore<S>): FxStore<S> {
51+ let state = initialState;
52+ const listeners = new Set<Listener>();
53+ enablePatches();
54+
55+ function getScope() {
56+ return scope;
57+ }
58+
59+ function getState() {
60+ return state;
61+ }
62+
63+ function subscribe(fn: Listener) {
64+ listeners.add(fn);
65+ return () => listeners.delete(fn);
66+ }
67+
68+ function* updateMdw(ctx: UpdaterCtx<S>, next: Next) {
69+ const upds: StoreUpdater<S>[] = [];
70+
71+ if (Array.isArray(ctx.updater)) {
72+ upds.push(...ctx.updater);
73+ } else {
74+ upds.push(ctx.updater);
75+ }
76+
77+ const [nextState, patches, _] = produceWithPatches(getState(), (draft) => {
78+ // TODO: check for return value inside updater
79+ upds.forEach((updater) => updater(draft as any));
80+ });
81+ ctx.patches = patches;
82+
83+ // set the state!
84+ state = nextState;
85+
86+ yield* next();
87+ }
88+
89+ function* notifyChannelMdw(_: UpdaterCtx<S>, next: Next) {
90+ const chan = yield* StoreUpdateContext;
91+ yield* chan.input.send();
92+ yield* next();
93+ }
94+
95+ function* notifyListenersMdw(_: UpdaterCtx<S>, next: Next) {
96+ listeners.forEach((f) => f());
97+ yield* next();
98+ }
99+
100+ function createUpdater() {
101+ const fn = compose<UpdaterCtx<S>>([
102+ ...middleware,
103+ updateMdw,
104+ notifyChannelMdw,
105+ notifyListenersMdw,
106+ ]);
107+
108+ return fn;
109+ }
110+
111+ const mdw = createUpdater();
112+ function* update(updater: StoreUpdater<S> | StoreUpdater<S>[]) {
113+ const ctx = {
114+ updater,
115+ patches: [],
116+ };
117+ const result = yield* mdw(ctx);
118+ // TODO: dev mode only?
119+ if (!result.ok) {
120+ console.error(result.error);
121+ }
122+ return result;
123+ }
124+
125+ function dispatch(action: AnyAction | AnyAction[]): Task<any> {
126+ return scope.run(function* () {
127+ yield* put(action);
128+ });
129+ }
130+
131+ function run<T>(op: OpFn<T>): Task<Result<T>> {
132+ return scope.run(function* () {
133+ return yield* call(op);
134+ });
135+ }
136+
137+ return {
138+ getScope,
139+ getState,
140+ subscribe,
141+ update,
142+ run,
143+ // instead of actions relating to store mutation, they
144+ // refer to pieces of business logic -- that can also mutate state
145+ dispatch,
146+ // stubs so `react-redux` is happy
147+ replaceReducer<S = any>(
148+ _nextReducer: (_s: S, _a: AnyAction) => void,
149+ ): void {
150+ throw new Error(stubMsg);
151+ },
152+ [Symbol.observable]: observable,
153+ };
154+}
155+
156+export function register<S extends AnyState>(store: FxStore<S>) {
157+ const scope = store.getScope();
158+ return scope.run(function* () {
159+ // TODO: fix type
160+ yield* StoreContext.set(store as any);
161+ });
162+}
163+
164+const defaultScope = createScope();
165+export async function configureStore<S extends AnyState>({
166+ scope = defaultScope,
167+ ...props
168+}: CreateStore<S>): Promise<FxStore<S>> {
169+ const store = createStore<S>({ scope, ...props });
170+ await register(store);
171+ return store;
172+}
R query/supervisor.ts =>
store/supervisor.ts
+9,
-6
1@@ -1,9 +1,9 @@
2 import { call, race } from "../fx/mod.ts";
3-import { ActionWPayload, take } from "../redux/mod.ts";
4-import { Action, Operation, sleep, spawn, Task } from "../deps.ts";
5+import { ActionWPayload, take } from "../store/mod.ts";
6+import type { AnyAction } from "../store/types.ts";
7+import { Operation, sleep, spawn, Task } from "../deps.ts";
8 import type { OpFn } from "../types.ts";
9-
10-import type { CreateActionPayload } from "./types.ts";
11+import type { CreateActionPayload } from "../query/types.ts";
12
13 const MS = 1000;
14 const SECONDS = 1 * MS;
15@@ -12,7 +12,7 @@ const MINUTES = 60 * SECONDS;
16 export function poll(parentTimer: number = 5 * 1000, cancelType?: string) {
17 return function* poller<T>(
18 actionType: string,
19- op: (action: Action) => Operation<T>,
20+ op: (action: AnyAction) => Operation<T>,
21 ): Operation<T> {
22 const cancel = cancelType || actionType;
23 function* fire(action: { type: string }, timer: number) {
24@@ -43,7 +43,10 @@ export function poll(parentTimer: number = 5 * 1000, cancelType?: string) {
25 * cache timer then the second call will not send an http request.
26 */
27 export function timer(timer: number = 5 * MINUTES) {
28- return function* onTimer(actionType: string, op: (action: Action) => OpFn) {
29+ return function* onTimer(
30+ actionType: string,
31+ op: (action: AnyAction) => OpFn,
32+ ) {
33 const map: { [key: string]: Task<unknown> } = {};
34
35 function* activate(action: ActionWPayload<CreateActionPayload>) {
+55,
-0
1@@ -0,0 +1,55 @@
2+import { describe, expect, it } from "../test.ts";
3+
4+import type { AnyAction } from "./types.ts";
5+import { configureStore, take, takeEvery } from "./mod.ts";
6+
7+const testEvery = describe("takeEvery()");
8+
9+it(testEvery, "should work", async () => {
10+ const loop = 10;
11+ const actual: string[][] = [];
12+
13+ function* root() {
14+ const task = yield* takeEvery(
15+ "ACTION",
16+ (action) => worker("a1", "a2", action),
17+ );
18+ yield* take("CANCEL_WATCHER");
19+ yield* task.halt();
20+ }
21+
22+ function* worker(arg1: string, arg2: string, action: AnyAction) {
23+ actual.push([arg1, arg2, action.payload]);
24+ }
25+
26+ const store = await configureStore({ initialState: {} });
27+ const task = store.run(root);
28+
29+ for (let i = 1; i <= loop / 2; i += 1) {
30+ store.dispatch({
31+ type: "ACTION",
32+ payload: i,
33+ });
34+ }
35+
36+ // no further task should be forked after this
37+ store.dispatch({
38+ type: "CANCEL_WATCHER",
39+ });
40+
41+ for (let i = loop / 2 + 1; i <= loop; i += 1) {
42+ store.dispatch({
43+ type: "ACTION",
44+ payload: i,
45+ });
46+ }
47+ await task;
48+
49+ expect(actual).toEqual([
50+ ["a1", "a2", 1],
51+ ["a1", "a2", 2],
52+ ["a1", "a2", 3],
53+ ["a1", "a2", 4],
54+ ["a1", "a2", 5],
55+ ]);
56+});
+120,
-0
1@@ -0,0 +1,120 @@
2+import { describe, expect, it } from "../test.ts";
3+import { sleep, spawn } from "../deps.ts";
4+
5+import type { AnyAction } from "./types.ts";
6+import { put, take } from "./mod.ts";
7+import { configureStore } from "./store.ts";
8+
9+const takeTests = describe("take()");
10+
11+it(
12+ takeTests,
13+ "a put should complete before more `take` are added and then consumed automatically",
14+ async () => {
15+ const actual: AnyAction[] = [];
16+
17+ function* channelFn() {
18+ yield* sleep(10);
19+ yield* put({ type: "action-1", payload: 1 });
20+ yield* put({ type: "action-1", payload: 2 });
21+ }
22+
23+ function* root() {
24+ yield* spawn(channelFn);
25+
26+ actual.push(yield* take("action-1"));
27+ actual.push(yield* take("action-1"));
28+ }
29+
30+ const store = await configureStore({ initialState: {} });
31+ await store.run(root);
32+
33+ expect(actual).toEqual([
34+ { type: "action-1", payload: 1 },
35+ { type: "action-1", payload: 2 },
36+ ]);
37+ },
38+);
39+
40+it(takeTests, "take from default channel", async () => {
41+ function* channelFn() {
42+ yield* sleep(10);
43+ yield* put({ type: "action-*" });
44+ yield* put({ type: "action-1" });
45+ yield* put({ type: "action-2" });
46+ yield* put({ type: "unnoticeable-action" });
47+ yield* put({
48+ type: "",
49+ payload: {
50+ isAction: true,
51+ },
52+ });
53+ yield* put({
54+ type: "",
55+ payload: {
56+ isMixedWithPredicate: true,
57+ },
58+ });
59+ yield* put({
60+ type: "action-3",
61+ });
62+ }
63+
64+ const actual: AnyAction[] = [];
65+ function* genFn() {
66+ yield* spawn(channelFn);
67+
68+ try {
69+ actual.push(yield* take("*")); // take all actions
70+ actual.push(yield* take("action-1")); // take only actions of type 'action-1'
71+ actual.push(yield* take(["action-2", "action-2222"])); // take either type
72+ actual.push(yield* take((a: AnyAction) => a.payload?.isAction)); // take if match predicate
73+ actual.push(
74+ yield* take([
75+ "action-3",
76+ (a: AnyAction) => a.payload?.isMixedWithPredicate,
77+ ]),
78+ ); // take if match any from the mixed array
79+ actual.push(
80+ yield* take([
81+ "action-3",
82+ (a: AnyAction) => a.payload?.isMixedWithPredicate,
83+ ]),
84+ ); // take if match any from the mixed array
85+ } finally {
86+ actual.push({ type: "auto ended" });
87+ }
88+ }
89+
90+ const store = await configureStore({ initialState: {} });
91+ await store.run(genFn);
92+
93+ const expected = [
94+ {
95+ type: "action-*",
96+ },
97+ {
98+ type: "action-1",
99+ },
100+ {
101+ type: "action-2",
102+ },
103+ {
104+ type: "",
105+ payload: {
106+ isAction: true,
107+ },
108+ },
109+ {
110+ type: "",
111+ payload: {
112+ isMixedWithPredicate: true,
113+ },
114+ },
115+ {
116+ type: "action-3",
117+ },
118+ { type: "auto ended" },
119+ ];
120+ expect(actual).toEqual(expected);
121+});
+40,
-0
1@@ -0,0 +1,40 @@
2+import type { Operation, Patch, Result, Scope, Task } from "../deps.ts";
3+import type { OpFn } from "../types.ts";
4+
5+export type StoreUpdater<S extends AnyState> = (s: S) => S | void;
6+
7+export type Listener = () => void;
8+
9+export interface UpdaterCtx<S extends AnyState> {
10+ updater: StoreUpdater<S> | StoreUpdater<S>[];
11+ patches: Patch[];
12+}
13+
14+export interface AnyAction {
15+ type: string;
16+ [key: string]: any;
17+}
18+
19+export interface ActionWPayload<P> {
20+ type: string;
21+ payload: P;
22+}
23+
24+export type AnyState = Record<string, any>;
25+
26+declare global {
27+ interface SymbolConstructor {
28+ readonly observable: symbol;
29+ }
30+}
31+
32+export interface FxStore<S extends AnyState> {
33+ getScope: () => Scope;
34+ getState: () => S;
35+ subscribe: (fn: Listener) => () => void;
36+ update: (u: StoreUpdater<S>) => Operation<Result<UpdaterCtx<S>>>;
37+ run: <T>(op: OpFn<T>) => Task<Result<T>>;
38+ dispatch: (a: AnyAction) => Task<any>;
39+ replaceReducer: (r: (s: S, a: AnyAction) => S) => void;
40+ [Symbol.observable]: () => any;
41+}
M
test.ts
+6,
-16
1@@ -13,9 +13,12 @@ export {
2 mockedFetch,
3 } from "https://deno.land/x/mock_fetch@0.3.0/mod.ts";
4
5-import { contextualize } from "./context.ts";
6-import { configureStore, createScope } from "./deps.ts";
7-import { createFxMiddleware } from "./redux/mod.ts";
8+export const sleep = (n: number) =>
9+ new Promise<void>((resolve) => {
10+ setTimeout(() => {
11+ resolve();
12+ }, n);
13+ });
14
15 export function isLikeSelector(selector: unknown) {
16 return (
17@@ -54,16 +57,3 @@ export function assertLike(
18
19 return comparable;
20 }
21-
22-export function setupReduxScope() {
23- const scope = createScope();
24- const mdw = createFxMiddleware(scope);
25- const store = configureStore({
26- reducer: () => null,
27- middleware: [mdw.middleware],
28- });
29- scope.run(function* () {
30- yield* contextualize("redux:store", store);
31- });
32- return { run: mdw.run };
33-}
M
types.ts
+36,
-0
1@@ -9,3 +9,39 @@ export type OpFn<T = unknown> =
2 | (() => Operation<T>)
3 | (() => PromiseLike<T>)
4 | (() => T);
5+
6+export interface QueryState {
7+ "@@starfx/loaders": Record<IdProp, LoadingItemState>;
8+ "@@starfx/data": Record<string, unknown>;
9+}
10+
11+export type IdProp = string | number;
12+export type LoadingStatus = "loading" | "success" | "error" | "idle";
13+export interface LoadingItemState<
14+ M extends Record<IdProp, unknown> = Record<IdProp, unknown>,
15+> {
16+ id: IdProp;
17+ status: LoadingStatus;
18+ message: string;
19+ lastRun: number;
20+ lastSuccess: number;
21+ meta: M;
22+}
23+
24+export interface LoadingState<
25+ M extends Record<IdProp, unknown> = Record<IdProp, unknown>,
26+> extends LoadingItemState<M> {
27+ isIdle: boolean;
28+ isLoading: boolean;
29+ isError: boolean;
30+ isSuccess: boolean;
31+ isInitialLoading: boolean;
32+}
33+
34+export type LoadingPayload =
35+ & Pick<LoadingItemState, "id">
36+ & Partial<Pick<LoadingItemState, "message" | "meta">>;
37+
38+export interface Payload<P = any> {
39+ payload: P;
40+}