Skip to content

Commit

Permalink
Wait for loader (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
neurosnap authored Feb 10, 2024
1 parent 797ba54 commit 95bee2f
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 110 deletions.
25 changes: 25 additions & 0 deletions action.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
call,
Callable,
createContext,
createSignal,
each,
Expand All @@ -12,6 +13,7 @@ import {
import { ActionPattern, matcher } from "./matcher.ts";
import type { Action, ActionWithPayload, AnyAction } from "./types.ts";
import { createFilterQueue } from "./queue.ts";
import { ActionFnWithPayload } from "./types.ts";

export const ActionContext = createContext(
"starfx:action",
Expand Down Expand Up @@ -103,6 +105,29 @@ export function* takeLeading<T>(
}
}

export function* waitFor(
predicate: Callable<boolean>,
) {
const init = yield* call(predicate as any);
if (init) {
return;
}

while (true) {
yield* take("*");
const result = yield* call(() => predicate as any);
if (result) {
return;
}
}
}

export function getIdFromAction(
action: ActionWithPayload<{ key: string }> | ActionFnWithPayload,
): string {
return typeof action === "function" ? action.toString() : action.payload.key;
}

export const API_ACTION_PREFIX = "";
export const createAction = (type: string) => {
if (!type) throw new Error("createAction requires non-empty string");
Expand Down
2 changes: 1 addition & 1 deletion fx/supervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function superviseBackoff(attempt: number, max = 10): number {
}

/**
* {@link supvervise} will watch whatever {@link Operation} is provided
* supvervise will watch whatever {@link Operation} is provided
* and it will automatically try to restart it when it exists. By
* default it uses a backoff pressure mechanism so if there is an
* error simply calling the {@link Operation} then it will exponentially
Expand Down
3 changes: 2 additions & 1 deletion query/mod.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { createThunks, type ThunksApi } from "./thunk.ts";
import * as mdw from "./mdw.ts";

export * from "./api.ts";
export * from "./types.ts";
export * from "./create-key.ts";
import * as mdw from "./mdw.ts";

export { createThunks, mdw, ThunksApi };

Expand Down
17 changes: 8 additions & 9 deletions react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
import type { AnyState, LoaderState } from "./types.ts";
import type { ThunkAction } from "./query/mod.ts";
import { type FxSchema, type FxStore, PERSIST_LOADER_ID } from "./store/mod.ts";
import { ActionFn, ActionFnWithPayload } from "./types.ts";
import { getIdFromAction } from "./action.ts";

export { useDispatch, useSelector } from "./deps.ts";
export type { TypedUseSelectorHook } from "./deps.ts";
Expand All @@ -19,16 +21,13 @@ const {
createElement: h,
} = React;

type ActionFn<P = any> = (p: P) => { toString: () => string };
type ActionFnSimple = () => { toString: () => string };

export interface UseApiProps<P = any> extends LoaderState {
trigger: (p: P) => void;
action: ActionFn<P>;
action: ActionFnWithPayload<P>;
}
export interface UseApiSimpleProps extends LoaderState {
trigger: () => void;
action: ActionFn;
action: ActionFnWithPayload;
}
export interface UseApiAction<A extends ThunkAction = ThunkAction>
extends LoaderState {
Expand Down Expand Up @@ -93,10 +92,10 @@ export function useSchema<S extends AnyState>() {
* ```
*/
export function useLoader<S extends AnyState>(
action: ThunkAction | ActionFn,
action: ThunkAction | ActionFnWithPayload,
) {
const schema = useSchema();
const id = typeof action === "function" ? `${action}` : action.payload.key;
const id = getIdFromAction(action);
return useSelector((s: S) => schema.loaders.selectById(s, { id }));
}

Expand Down Expand Up @@ -131,10 +130,10 @@ export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
action: A,
): UseApiAction<A>;
export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
action: ActionFn<P>,
action: ActionFnWithPayload<P>,
): UseApiProps<P>;
export function useApi<A extends ThunkAction = ThunkAction>(
action: ActionFnSimple,
action: ActionFn,
): UseApiSimpleProps;
export function useApi(action: any): any {
const dispatch = useDispatch();
Expand Down
38 changes: 36 additions & 2 deletions store/fx.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Operation, Result } from "../deps.ts";
import type { AnyState } from "../types.ts";
import type { ActionFnWithPayload, AnyState } from "../types.ts";
import type { FxStore, StoreUpdater, UpdaterCtx } from "./types.ts";
import { StoreContext } from "./context.ts";
import { LoaderOutput } from "./slice/loader.ts";
import { safe } from "../fx/mod.ts";
import { parallel, safe } from "../fx/mod.ts";
import { ThunkAction } from "../query/mod.ts";
import { getIdFromAction, take } from "../action.ts";

export function* updateStore<S extends AnyState>(
updater: StoreUpdater<S> | StoreUpdater<S>[],
Expand All @@ -28,6 +30,38 @@ export function* select<S, R, P>(
return selectorFn(store.getState() as S, p);
}

export function* waitForLoader<M extends AnyState>(
loaders: LoaderOutput<M, AnyState>,
action: ThunkAction | ActionFnWithPayload,
) {
const id = getIdFromAction(action);
const selector = (s: AnyState) => loaders.selectById(s, { id });

// check for done state on init
let loader = yield* select(selector);
if (loader.isSuccess || loader.isError) {
return loader;
}

while (true) {
yield* take("*");
loader = yield* select(selector);
if (loader.isSuccess || loader.isError) {
return loader;
}
}
}

export function* waitForLoaders<M extends AnyState>(
loaders: LoaderOutput<M, AnyState>,
actions: (ThunkAction | ActionFnWithPayload)[],
) {
const group = yield* parallel(
actions.map((action) => waitForLoader(loaders, action)),
);
return yield* group;
}

export function createTracker<T, M extends Record<string, unknown>>(
loader: LoaderOutput<M, AnyState>,
) {
Expand Down
2 changes: 1 addition & 1 deletion store/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export function loaderApi<
}

if (!ctx.loader) {
ctx.loader || {};
ctx.loader = {};
}

if (!ctx.response.ok) {
Expand Down
7 changes: 0 additions & 7 deletions test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@ export * as asserts from "https://deno.land/[email protected]/testing/asserts.ts";
export { expect } from "https://deno.land/x/[email protected]/mod.ts";
export { install, mock } from "https://deno.land/x/[email protected]/mod.ts";

export const sleep = (n: number) =>
new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, n);
});

export function isLikeSelector(selector: unknown) {
return (
selector !== null &&
Expand Down
25 changes: 14 additions & 11 deletions test/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { describe, expect, it } from "../test.ts";
import { sleep } from "../test.ts";
import {
configureStore,
createSchema,
select,
slice,
storeMdw,
updateStore,
waitForLoader,
} from "../store/mod.ts";
import {
AnyState,
type ApiCtx,
call,
createApi,
createKey,
keepAlive,
mdw,
Operation,
safe,
takeEvery,
waitFor,
} from "../mod.ts";
import { useCache } from "../react.ts";

Expand Down Expand Up @@ -44,9 +48,8 @@ const jsonBlob = (data: unknown) => {

const tests = describe("createApi()");

it(tests, "createApi - POST", async () => {
it(tests, "POST", async () => {
const query = createApi();

query.use(mdw.queryCtx);
query.use(mdw.nameParser);
query.use(query.routes());
Expand Down Expand Up @@ -102,7 +105,12 @@ it(tests, "createApi - POST", async () => {
store.run(query.bootup);

store.dispatch(createUser({ email: mockUser.email }));
await sleep(150);

await store.run(waitFor(function* (): Operation<boolean> {
const res = yield* select((state: AnyState) => state.users["1"].id);
return res !== "";
}));

expect(store.getState().users).toEqual({
"1": { id: "1", name: "test", email: "[email protected]" },
});
Expand Down Expand Up @@ -292,7 +300,7 @@ it(tests, "with hash key on a large post", async () => {
const action = createUserDefaultKey({ email, largetext });
store.dispatch(action);

await sleep(150);
await store.run(waitForLoader(schema.loaders, action));

const s = store.getState();
const expectedKey = createKey(action.payload.name, {
Expand All @@ -306,18 +314,16 @@ it(tests, "with hash key on a large post", async () => {
});
});

it(tests, "createApi - two identical endpoints", async () => {
it(tests, "two identical endpoints", () => {
const actual: string[] = [];
const { store, schema } = testStore();
const api = createApi();
api.use(mdw.api());
api.use(storeMdw.store(schema));
api.use(mdw.nameParser);
api.use(api.routes());

const first = api.get(
"/health",
{ supervisor: takeEvery },
function* (ctx, next) {
actual.push(ctx.req().url);
yield* next();
Expand All @@ -326,7 +332,6 @@ it(tests, "createApi - two identical endpoints", async () => {

const second = api.get(
["/health", "poll"],
{ supervisor: takeEvery },
function* (ctx, next) {
actual.push(ctx.req().url);
yield* next();
Expand All @@ -337,8 +342,6 @@ it(tests, "createApi - two identical endpoints", async () => {
store.dispatch(first());
store.dispatch(second());

await sleep(150);

expect(actual).toEqual(["/health", "/health"]);
});

Expand Down
Loading

0 comments on commit 95bee2f

Please sign in to comment.