Skip to content

Commit

Permalink
refactor: enhanced thunk registry system (#44)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
neurosnap authored Jul 30, 2024
1 parent 2fe68a1 commit 6ca0a85
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 10 deletions.
6 changes: 3 additions & 3 deletions action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function* take(pattern: ActionPattern): Operation<Action> {

export function* takeEvery<T>(
pattern: ActionPattern,
op: (action: Action) => Operation<T>,
op: (action: AnyAction) => Operation<T>,
) {
const fd = useActions(pattern);
for (const action of yield* each(fd)) {
Expand All @@ -81,7 +81,7 @@ export function* takeEvery<T>(

export function* takeLatest<T>(
pattern: ActionPattern,
op: (action: Action) => Operation<T>,
op: (action: AnyAction) => Operation<T>,
) {
const fd = useActions(pattern);
let lastTask;
Expand All @@ -97,7 +97,7 @@ export function* takeLatest<T>(

export function* takeLeading<T>(
pattern: ActionPattern,
op: (action: Action) => Operation<T>,
op: (action: AnyAction) => Operation<T>,
) {
while (true) {
const action = yield* take(pattern);
Expand Down
6 changes: 5 additions & 1 deletion query/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ export function createApi<Ctx extends ApiCtx = ApiCtx>(

return {
use: thunks.use,
bootup: thunks.bootup,
/**
* @deprecated use `register()` instead
*/
bootup: thunks.register,
register: thunks.register,
create: thunks.create,
routes: thunks.routes,
reset: thunks.reset,
Expand Down
54 changes: 49 additions & 5 deletions query/thunk.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { compose } from "../compose.ts";
import type { ActionWithPayload, Next, Payload } from "../types.ts";
import { keepAlive } from "../mod.ts";
import { takeEvery } from "../action.ts";
import type { ActionWithPayload, AnyAction, Next, Payload } from "../types.ts";
import { ActionContext, createAction, put, takeEvery } from "../action.ts";
import { isFn, isObject } from "./util.ts";
import { createKey } from "./create-key.ts";
import type {
Expand All @@ -14,12 +13,18 @@ import type {
ThunkCtx,
} from "./types.ts";
import { API_ACTION_PREFIX } from "../action.ts";
import { Callable, Ok, Operation } from "../deps.ts";
import { Callable, Ok, Operation, Signal, spawn } from "../deps.ts";
import { supervise } from "../fx/mod.ts";

const registerThunk = createAction<Callable<unknown>>(
`${API_ACTION_PREFIX}REGISTER_THUNK`,
);

export interface ThunksApi<Ctx extends ThunkCtx> {
use: (fn: Middleware<Ctx>) => void;
routes: () => Middleware<Ctx>;
bootup: Callable<void>;
register: Callable<void>;
reset: () => void;

/**
Expand Down Expand Up @@ -122,6 +127,7 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
supervisor?: Supervisor;
} = { supervisor: takeEvery },
): ThunksApi<Ctx> {
let signal: Signal<AnyAction, void> | undefined = undefined;
const middleware: Middleware<Ctx>[] = [];
const visors: { [key: string]: Callable<unknown> } = {};
const middlewareMap: { [key: string]: Middleware<Ctx> } = {};
Expand Down Expand Up @@ -198,13 +204,28 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
function* curVisor() {
yield* tt(type, onApi);
}
// if we have a signal that means the `register()` function has already been called
// so that means we can immediately register the thunk
if (signal) {
signal.send(registerThunk(curVisor));
}
visors[name] = curVisor;

const errMsg =
`[${name}] is being called before its thunk has been registered. ` +
"Run `store.run(thunks.register)` where `thunks` is the name of your `createThunks` or `createApi` variable.";

const actionFn = (options?: Ctx["payload"]) => {
if (!signal) {
console.error(errMsg);
}
const key = createKey(name, options);
return action({ name, key, options });
};
actionFn.run = (action?: unknown): Operation<Ctx> => {
if (!signal) {
console.error(errMsg);
}
if (action && Object.hasOwn(action, "type")) {
return onApi(action as ActionWithPayload<CreateActionPayload>);
}
Expand All @@ -226,8 +247,27 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
return actionFn;
}

function* watcher(action: ActionWithPayload<Callable<unknown>>) {
yield* supervise(action.payload)();
}

function* register() {
// cache the signal so we can use it when creating thunks after we
// have already called `register()`
signal = yield* ActionContext;

const task = yield* spawn(function* () {
yield* takeEvery(`${registerThunk}`, watcher as any);
});

// register any thunks already created
yield* put(Object.values(visors).map(registerThunk));

yield* task;
}

function* bootup() {
yield* keepAlive(Object.values(visors));
yield* register();
}

function routes() {
Expand Down Expand Up @@ -255,7 +295,11 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
},
create,
routes,
/**
* @deprecated use `register()` instead
*/
bootup,
reset: resetMdw,
register,
};
}
2 changes: 2 additions & 0 deletions test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { assert } from "https://deno.land/[email protected]/testing/asserts.ts";
export {
afterAll,
beforeAll,
beforeEach,
describe,
it,
Expand Down
12 changes: 11 additions & 1 deletion test/create-key.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { describe, expect, it } from "../test.ts";
import { afterAll, beforeAll, describe, expect, it } from "../test.ts";
import { type ActionWithPayload, createApi } from "../mod.ts";

const getKeyOf = (action: ActionWithPayload<{ key: string }>): string =>
action.payload.key;

const err = console.error;
beforeAll(() => {
console.error = () => {};
});

afterAll(() => {
console.error = err;
});

const tests = describe("create-key");

it(
tests,
"options object keys order for action key identity - 0: empty options",
() => {
console.warn = () => {};
const api = createApi();
api.use(api.routes());
// no param
Expand Down
30 changes: 30 additions & 0 deletions test/thunk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,3 +505,33 @@ it(tests, "should only call thunk once", () => {
store.dispatch(action2());
asserts.assertEquals(acc, "a");
});

it(tests, "should be able to create thunk after `register()`", () => {
const api = createThunks<RoboCtx>();
api.use(api.routes());
const store = createStore({ initialState: {} });
store.run(api.register);

let acc = "";
const action = api.create("/users", function* () {
acc += "a";
});
store.dispatch(action());
asserts.assertEquals(acc, "a");
});

it(tests, "should warn when calling thunk before registered", () => {
const err = console.error;
let called = false;
console.error = () => {
called = true;
};
const api = createThunks<RoboCtx>();
api.use(api.routes());
const store = createStore({ initialState: {} });

const action = api.create("/users");
store.dispatch(action());
asserts.assertEquals(called, true);
console.error = err;
});

0 comments on commit 6ca0a85

Please sign in to comment.