diff --git a/action.ts b/action.ts index 4043bd9..52f1450 100644 --- a/action.ts +++ b/action.ts @@ -70,7 +70,7 @@ export function* take(pattern: ActionPattern): Operation { export function* takeEvery( pattern: ActionPattern, - op: (action: Action) => Operation, + op: (action: AnyAction) => Operation, ) { const fd = useActions(pattern); for (const action of yield* each(fd)) { @@ -81,7 +81,7 @@ export function* takeEvery( export function* takeLatest( pattern: ActionPattern, - op: (action: Action) => Operation, + op: (action: AnyAction) => Operation, ) { const fd = useActions(pattern); let lastTask; @@ -97,7 +97,7 @@ export function* takeLatest( export function* takeLeading( pattern: ActionPattern, - op: (action: Action) => Operation, + op: (action: AnyAction) => Operation, ) { while (true) { const action = yield* take(pattern); diff --git a/query/api.ts b/query/api.ts index 0678dbb..e6e2cd2 100644 --- a/query/api.ts +++ b/query/api.ts @@ -65,7 +65,11 @@ export function createApi( 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, diff --git a/query/thunk.ts b/query/thunk.ts index 65c36cc..fc29520 100644 --- a/query/thunk.ts +++ b/query/thunk.ts @@ -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 { @@ -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>( + `${API_ACTION_PREFIX}REGISTER_THUNK`, +); export interface ThunksApi { use: (fn: Middleware) => void; routes: () => Middleware; bootup: Callable; + register: Callable; reset: () => void; /** @@ -122,6 +127,7 @@ export function createThunks>( supervisor?: Supervisor; } = { supervisor: takeEvery }, ): ThunksApi { + let signal: Signal | undefined = undefined; const middleware: Middleware[] = []; const visors: { [key: string]: Callable } = {}; const middlewareMap: { [key: string]: Middleware } = {}; @@ -198,13 +204,28 @@ export function createThunks>( 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 => { + if (!signal) { + console.error(errMsg); + } if (action && Object.hasOwn(action, "type")) { return onApi(action as ActionWithPayload); } @@ -226,8 +247,27 @@ export function createThunks>( return actionFn; } + function* watcher(action: ActionWithPayload>) { + 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() { @@ -255,7 +295,11 @@ export function createThunks>( }, create, routes, + /** + * @deprecated use `register()` instead + */ bootup, reset: resetMdw, + register, }; } diff --git a/test.ts b/test.ts index 88fa907..915a3a6 100644 --- a/test.ts +++ b/test.ts @@ -1,5 +1,7 @@ export { assert } from "https://deno.land/std@0.187.0/testing/asserts.ts"; export { + afterAll, + beforeAll, beforeEach, describe, it, diff --git a/test/create-key.test.ts b/test/create-key.test.ts index 741b251..e9f929c 100644 --- a/test/create-key.test.ts +++ b/test/create-key.test.ts @@ -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 diff --git a/test/thunk.test.ts b/test/thunk.test.ts index 3e0705f..92bae05 100644 --- a/test/thunk.test.ts +++ b/test/thunk.test.ts @@ -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(); + 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(); + api.use(api.routes()); + const store = createStore({ initialState: {} }); + + const action = api.create("/users"); + store.dispatch(action()); + asserts.assertEquals(called, true); + console.error = err; +});