- commit
- 6ca0a85
- parent
- 2fe68a1
- author
- Eric Bower
- date
- 2024-07-30 19:08:39 +0000 UTC
refactor: enhanced thunk registry system (#44) The current registry system for thunks works like this: - User calls `const thunks = createThunks()` - User creates **all** thunks `const go = thunks.create("go")` - User registers thunks `store.run(thunks.bootup)` However, there's a caveat with this implementation: all thunks must be created before `store.run` is called. Further, since thunks are created at the module-level, if the module that exports those thunks isn't loaded before `thunk.bootup` is called then those thunks are silently ignored. This change will make it so it doesn't matter when a thunk is created, we will "lazy load" it. We still require `store.run(thunks.bootup)` to be called -- because we need access to the store and won't have it when creating a thunk. We are also sending an error whenever a thunk is dispatched without it being registered which should help ensure thunks get properly registered. We also changed the name of `thunks.bootup` to `thunks.register` to make it more clear that this is a registry system.
6 files changed,
+100,
-10
+3,
-3
1@@ -70,7 +70,7 @@ export function* take(pattern: ActionPattern): Operation<Action> {
2
3 export function* takeEvery<T>(
4 pattern: ActionPattern,
5- op: (action: Action) => Operation<T>,
6+ op: (action: AnyAction) => Operation<T>,
7 ) {
8 const fd = useActions(pattern);
9 for (const action of yield* each(fd)) {
10@@ -81,7 +81,7 @@ export function* takeEvery<T>(
11
12 export function* takeLatest<T>(
13 pattern: ActionPattern,
14- op: (action: Action) => Operation<T>,
15+ op: (action: AnyAction) => Operation<T>,
16 ) {
17 const fd = useActions(pattern);
18 let lastTask;
19@@ -97,7 +97,7 @@ export function* takeLatest<T>(
20
21 export function* takeLeading<T>(
22 pattern: ActionPattern,
23- op: (action: Action) => Operation<T>,
24+ op: (action: AnyAction) => Operation<T>,
25 ) {
26 while (true) {
27 const action = yield* take(pattern);
+5,
-1
1@@ -65,7 +65,11 @@ export function createApi<Ctx extends ApiCtx = ApiCtx>(
2
3 return {
4 use: thunks.use,
5- bootup: thunks.bootup,
6+ /**
7+ * @deprecated use `register()` instead
8+ */
9+ bootup: thunks.register,
10+ register: thunks.register,
11 create: thunks.create,
12 routes: thunks.routes,
13 reset: thunks.reset,
+49,
-5
1@@ -1,7 +1,6 @@
2 import { compose } from "../compose.ts";
3-import type { ActionWithPayload, Next, Payload } from "../types.ts";
4-import { keepAlive } from "../mod.ts";
5-import { takeEvery } from "../action.ts";
6+import type { ActionWithPayload, AnyAction, Next, Payload } from "../types.ts";
7+import { ActionContext, createAction, put, takeEvery } from "../action.ts";
8 import { isFn, isObject } from "./util.ts";
9 import { createKey } from "./create-key.ts";
10 import type {
11@@ -14,12 +13,18 @@ import type {
12 ThunkCtx,
13 } from "./types.ts";
14 import { API_ACTION_PREFIX } from "../action.ts";
15-import { Callable, Ok, Operation } from "../deps.ts";
16+import { Callable, Ok, Operation, Signal, spawn } from "../deps.ts";
17+import { supervise } from "../fx/mod.ts";
18+
19+const registerThunk = createAction<Callable<unknown>>(
20+ `${API_ACTION_PREFIX}REGISTER_THUNK`,
21+);
22
23 export interface ThunksApi<Ctx extends ThunkCtx> {
24 use: (fn: Middleware<Ctx>) => void;
25 routes: () => Middleware<Ctx>;
26 bootup: Callable<void>;
27+ register: Callable<void>;
28 reset: () => void;
29
30 /**
31@@ -122,6 +127,7 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
32 supervisor?: Supervisor;
33 } = { supervisor: takeEvery },
34 ): ThunksApi<Ctx> {
35+ let signal: Signal<AnyAction, void> | undefined = undefined;
36 const middleware: Middleware<Ctx>[] = [];
37 const visors: { [key: string]: Callable<unknown> } = {};
38 const middlewareMap: { [key: string]: Middleware<Ctx> } = {};
39@@ -198,13 +204,28 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
40 function* curVisor() {
41 yield* tt(type, onApi);
42 }
43+ // if we have a signal that means the `register()` function has already been called
44+ // so that means we can immediately register the thunk
45+ if (signal) {
46+ signal.send(registerThunk(curVisor));
47+ }
48 visors[name] = curVisor;
49
50+ const errMsg =
51+ `[${name}] is being called before its thunk has been registered. ` +
52+ "Run `store.run(thunks.register)` where `thunks` is the name of your `createThunks` or `createApi` variable.";
53+
54 const actionFn = (options?: Ctx["payload"]) => {
55+ if (!signal) {
56+ console.error(errMsg);
57+ }
58 const key = createKey(name, options);
59 return action({ name, key, options });
60 };
61 actionFn.run = (action?: unknown): Operation<Ctx> => {
62+ if (!signal) {
63+ console.error(errMsg);
64+ }
65 if (action && Object.hasOwn(action, "type")) {
66 return onApi(action as ActionWithPayload<CreateActionPayload>);
67 }
68@@ -226,8 +247,27 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
69 return actionFn;
70 }
71
72+ function* watcher(action: ActionWithPayload<Callable<unknown>>) {
73+ yield* supervise(action.payload)();
74+ }
75+
76+ function* register() {
77+ // cache the signal so we can use it when creating thunks after we
78+ // have already called `register()`
79+ signal = yield* ActionContext;
80+
81+ const task = yield* spawn(function* () {
82+ yield* takeEvery(`${registerThunk}`, watcher as any);
83+ });
84+
85+ // register any thunks already created
86+ yield* put(Object.values(visors).map(registerThunk));
87+
88+ yield* task;
89+ }
90+
91 function* bootup() {
92- yield* keepAlive(Object.values(visors));
93+ yield* register();
94 }
95
96 function routes() {
97@@ -255,7 +295,11 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
98 },
99 create,
100 routes,
101+ /**
102+ * @deprecated use `register()` instead
103+ */
104 bootup,
105 reset: resetMdw,
106+ register,
107 };
108 }
M
test.ts
+2,
-0
1@@ -1,5 +1,7 @@
2 export { assert } from "https://deno.land/std@0.187.0/testing/asserts.ts";
3 export {
4+ afterAll,
5+ beforeAll,
6 beforeEach,
7 describe,
8 it,
+11,
-1
1@@ -1,15 +1,25 @@
2-import { describe, expect, it } from "../test.ts";
3+import { afterAll, beforeAll, describe, expect, it } from "../test.ts";
4 import { type ActionWithPayload, createApi } from "../mod.ts";
5
6 const getKeyOf = (action: ActionWithPayload<{ key: string }>): string =>
7 action.payload.key;
8
9+const err = console.error;
10+beforeAll(() => {
11+ console.error = () => {};
12+});
13+
14+afterAll(() => {
15+ console.error = err;
16+});
17+
18 const tests = describe("create-key");
19
20 it(
21 tests,
22 "options object keys order for action key identity - 0: empty options",
23 () => {
24+ console.warn = () => {};
25 const api = createApi();
26 api.use(api.routes());
27 // no param
+30,
-0
1@@ -505,3 +505,33 @@ it(tests, "should only call thunk once", () => {
2 store.dispatch(action2());
3 asserts.assertEquals(acc, "a");
4 });
5+
6+it(tests, "should be able to create thunk after `register()`", () => {
7+ const api = createThunks<RoboCtx>();
8+ api.use(api.routes());
9+ const store = createStore({ initialState: {} });
10+ store.run(api.register);
11+
12+ let acc = "";
13+ const action = api.create("/users", function* () {
14+ acc += "a";
15+ });
16+ store.dispatch(action());
17+ asserts.assertEquals(acc, "a");
18+});
19+
20+it(tests, "should warn when calling thunk before registered", () => {
21+ const err = console.error;
22+ let called = false;
23+ console.error = () => {
24+ called = true;
25+ };
26+ const api = createThunks<RoboCtx>();
27+ api.use(api.routes());
28+ const store = createStore({ initialState: {} });
29+
30+ const action = api.create("/users");
31+ store.dispatch(action());
32+ asserts.assertEquals(called, true);
33+ console.error = err;
34+});