repos / starfx

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

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
M action.ts
+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);
M query/api.ts
+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,
M query/thunk.ts
+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,
M test/create-key.test.ts
+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
M test/thunk.test.ts
+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+});