repos / starfx

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

commit
98a9de7
parent
bcdad66
author
Eric Bower
date
2024-01-19 18:04:55 +0000 UTC
chore: folder/file org cleanup (#33)

16 files changed,  +311, -412
M api-type-template.ts
+1, -2
 1@@ -227,12 +227,11 @@ import type {
 2   ApiCtx,
 3   CreateAction,
 4   CreateActionWithPayload,
 5-  Next,
 6   FetchJson,
 7   MiddlewareApiCo,
 8   Supervisor,
 9 } from "./types.ts";
10-import type { Payload } from "../types.ts";
11+import type { Next, Payload } from "../types.ts";
12 import type { Operation } from "../deps.ts";
13 
14 export type ApiName = string | string[];
M compose.ts
+1, -1
1@@ -1,5 +1,5 @@
2-import type { Next } from "./query/mod.ts";
3 import { Instruction, Operation } from "./deps.ts";
4+import type { Next } from "./types.ts";
5 
6 export interface BaseCtx {
7   // deno-lint-ignore no-explicit-any
M query/api-types.ts
+1, -2
 1@@ -9,10 +9,9 @@ import type {
 2   CreateActionWithPayload,
 3   FetchJson,
 4   MiddlewareApiCo,
 5-  Next,
 6   Supervisor,
 7 } from "./types.ts";
 8-import type { Payload } from "../types.ts";
 9+import type { Next, Payload } from "../types.ts";
10 import type { Operation } from "../deps.ts";
11 
12 export type ApiName = string | string[];
M query/api.ts
+2, -1
1@@ -1,5 +1,6 @@
2 // deno-lint-ignore-file no-explicit-any
3-import type { ApiCtx, ApiRequest, Next } from "./types.ts";
4+import type { ApiCtx, ApiRequest } from "./types.ts";
5+import type { Next } from "../types.ts";
6 import { createThunks } from "./thunk.ts";
7 import type { ThunksApi } from "./thunk.ts";
8 import type { ApiName, QueryApi } from "./api-types.ts";
M query/fetch.ts
+2, -1
 1@@ -1,7 +1,8 @@
 2 import { sleep } from "../deps.ts";
 3 import { safe } from "../fx/mod.ts";
 4-import type { FetchCtx, FetchJsonCtx, Next } from "./types.ts";
 5+import type { FetchCtx, FetchJsonCtx } from "./types.ts";
 6 import { isObject, noop } from "./util.ts";
 7+import type { Next } from "../types.ts";
 8 
 9 /**
10  * This middleware converts the name provided to {@link createApi}
M query/mdw.ts
+1, -1
 1@@ -5,11 +5,11 @@ import type {
 2   ApiRequest,
 3   FetchJsonCtx,
 4   MiddlewareApi,
 5-  Next,
 6   PerfCtx,
 7   RequiredApiRequest,
 8   ThunkCtx,
 9 } from "./types.ts";
10+import type { Next } from "../types.ts";
11 import { mergeRequest } from "./util.ts";
12 import * as fetchMdw from "./fetch.ts";
13 import { log } from "../log.ts";
M query/thunk.ts
+1, -2
 1@@ -1,5 +1,5 @@
 2 import { compose } from "../compose.ts";
 3-import type { ActionWithPayload, Payload } from "../types.ts";
 4+import type { ActionWithPayload, Next, Payload } from "../types.ts";
 5 import { keepAlive } from "../mod.ts";
 6 import { takeEvery } from "../action.ts";
 7 import { isFn, isObject } from "./util.ts";
 8@@ -10,7 +10,6 @@ import type {
 9   CreateActionWithPayload,
10   Middleware,
11   MiddlewareCo,
12-  Next,
13   Supervisor,
14   ThunkCtx,
15 } from "./types.ts";
M query/types.ts
+1, -2
 1@@ -4,6 +4,7 @@ import type {
 2   ActionWithPayload,
 3   LoaderItemState,
 4   LoaderPayload,
 5+  Next,
 6   Payload,
 7 } from "../types.ts";
 8 
 9@@ -97,8 +98,6 @@ export type MiddlewareApiCo<Ctx extends ApiCtx = ApiCtx> =
10   | Middleware<Ctx>
11   | Middleware<Ctx>[];
12 
13-export type Next = () => Operation<void>;
14-
15 export interface CreateActionPayload<P = any, ApiSuccess = any> {
16   name: string;
17   key: string;
M react.ts
+275, -1
  1@@ -1,3 +1,277 @@
  2-export * from "./store/react.ts";
  3+import {
  4+  Provider as ReduxProvider,
  5+  React,
  6+  useDispatch,
  7+  useSelector,
  8+} from "./deps.ts";
  9+import type { AnyState, LoaderState } from "./types.ts";
 10+import type { ThunkAction } from "./query/mod.ts";
 11+import { type FxSchema, type FxStore, PERSIST_LOADER_ID } from "./store/mod.ts";
 12+
 13 export { useDispatch, useSelector } from "./deps.ts";
 14 export type { TypedUseSelectorHook } from "./deps.ts";
 15+
 16+const {
 17+  useContext,
 18+  useEffect,
 19+  useRef,
 20+  createContext,
 21+  createElement: h,
 22+} = React;
 23+
 24+type ActionFn<P = any> = (p: P) => { toString: () => string };
 25+type ActionFnSimple = () => { toString: () => string };
 26+
 27+export interface UseApiProps<P = any> extends LoaderState {
 28+  trigger: (p: P) => void;
 29+  action: ActionFn<P>;
 30+}
 31+export interface UseApiSimpleProps extends LoaderState {
 32+  trigger: () => void;
 33+  action: ActionFn;
 34+}
 35+export interface UseApiAction<A extends ThunkAction = ThunkAction>
 36+  extends LoaderState {
 37+  trigger: () => void;
 38+  action: A;
 39+}
 40+export type UseApiResult<P, A extends ThunkAction = ThunkAction> =
 41+  | UseApiProps<P>
 42+  | UseApiSimpleProps
 43+  | UseApiAction<A>;
 44+
 45+export interface UseCacheResult<D = any, A extends ThunkAction = ThunkAction>
 46+  extends UseApiAction<A> {
 47+  data: D | null;
 48+}
 49+
 50+const SchemaContext = createContext<FxSchema<any, any> | null>(null);
 51+
 52+export function Provider(
 53+  { store, schema, children }: {
 54+    store: FxStore<any>;
 55+    schema: FxSchema<any, any>;
 56+    children: React.ReactNode;
 57+  },
 58+) {
 59+  return (
 60+    h(ReduxProvider, {
 61+      store,
 62+      children: h(
 63+        SchemaContext.Provider,
 64+        { value: schema, children },
 65+      ) as any,
 66+    })
 67+  );
 68+}
 69+
 70+export function useSchema<S extends AnyState>() {
 71+  return useContext(SchemaContext) as FxSchema<S>;
 72+}
 73+
 74+/**
 75+ * useLoader will take an action creator or action itself and return the associated
 76+ * loader for it.
 77+ *
 78+ * @returns the loader object for an action creator or action
 79+ *
 80+ * @example
 81+ * ```ts
 82+ * import { useLoader } from 'starfx/react';
 83+ *
 84+ * import { api } from './api';
 85+ *
 86+ * const fetchUsers = api.get('/users', function*() {
 87+ *   // ...
 88+ * });
 89+ *
 90+ * const View = () => {
 91+ *   const loader = useLoader(fetchUsers);
 92+ *   // or: const loader = useLoader(fetchUsers());
 93+ *   return <div>{loader.isLoader ? 'Loading ...' : 'Done!'}</div>
 94+ * }
 95+ * ```
 96+ */
 97+export function useLoader<S extends AnyState>(
 98+  action: ThunkAction | ActionFn,
 99+) {
100+  const schema = useSchema();
101+  const id = typeof action === "function" ? `${action}` : action.payload.key;
102+  return useSelector((s: S) => schema.loaders.selectById(s, { id }));
103+}
104+
105+/**
106+ * useApi will take an action creator or action itself and fetch
107+ * the associated loader and create a `trigger` function that you can call
108+ * later in your react component.
109+ *
110+ * This hook will *not* fetch the data for you because it does not know how to fetch
111+ * data from your redux state.
112+ *
113+ * @example
114+ * ```ts
115+ * import { useApi } from 'starfx/react';
116+ *
117+ * import { api } from './api';
118+ *
119+ * const fetchUsers = api.get('/users', function*() {
120+ *   // ...
121+ * });
122+ *
123+ * const View = () => {
124+ *   const { isLoading, trigger } = useApi(fetchUsers);
125+ *   useEffect(() => {
126+ *     trigger();
127+ *   }, []);
128+ *   return <div>{isLoading ? : 'Loading' : 'Done!'}</div>
129+ * }
130+ * ```
131+ */
132+export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
133+  action: A,
134+): UseApiAction<A>;
135+export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
136+  action: ActionFn<P>,
137+): UseApiProps<P>;
138+export function useApi<A extends ThunkAction = ThunkAction>(
139+  action: ActionFnSimple,
140+): UseApiSimpleProps;
141+export function useApi(action: any): any {
142+  const dispatch = useDispatch();
143+  const loader = useLoader(action);
144+  const trigger = (p: any) => {
145+    if (typeof action === "function") {
146+      dispatch(action(p));
147+    } else {
148+      dispatch(action);
149+    }
150+  };
151+  return { ...loader, trigger, action };
152+}
153+
154+/**
155+ * useQuery uses {@link useApi} and automatically calls `useApi().trigger()`
156+ *
157+ * @example
158+ * ```ts
159+ * import { useQuery } from 'starfx/react';
160+ *
161+ * import { api } from './api';
162+ *
163+ * const fetchUsers = api.get('/users', function*() {
164+ *   // ...
165+ * });
166+ *
167+ * const View = () => {
168+ *   const { isLoading } = useQuery(fetchUsers);
169+ *   return <div>{isLoading ? : 'Loading' : 'Done!'}</div>
170+ * }
171+ * ```
172+ */
173+export function useQuery<P = any, A extends ThunkAction = ThunkAction<P>>(
174+  action: A,
175+): UseApiAction<A> {
176+  const api = useApi(action);
177+  useEffect(() => {
178+    api.trigger();
179+  }, [action.payload.key]);
180+  return api;
181+}
182+
183+/**
184+ * useCache uses {@link useQuery} and automatically selects the cached data associated
185+ * with the action creator or action provided.
186+ *
187+ * @example
188+ * ```ts
189+ * import { useCache } from 'starfx/react';
190+ *
191+ * import { api } from './api';
192+ *
193+ * const fetchUsers = api.get('/users', api.cache());
194+ *
195+ * const View = () => {
196+ *   const { isLoading, data } = useCache(fetchUsers());
197+ *   return <div>{isLoading ? : 'Loading' : data.length}</div>
198+ * }
199+ * ```
200+ */
201+export function useCache<P = any, ApiSuccess = any>(
202+  action: ThunkAction<P, ApiSuccess>,
203+): UseCacheResult<typeof action.payload._result, ThunkAction<P, ApiSuccess>> {
204+  const schema = useSchema();
205+  const id = action.payload.key;
206+  const data: any = useSelector((s: any) => schema.cache.selectById(s, { id }));
207+  const query = useQuery(action);
208+  return { ...query, data: data || null };
209+}
210+
211+/**
212+ * useLoaderSuccess will activate the callback provided when the loader transitions
213+ * from some state to success.
214+ *
215+ * @example
216+ * ```ts
217+ * import { useLoaderSuccess, useApi } from 'starfx/react';
218+ *
219+ * import { api } from './api';
220+ *
221+ * const createUser = api.post('/users', function*(ctx, next) {
222+ *   // ...
223+ * });
224+ *
225+ * const View = () => {
226+ *  const { loader, trigger } = useApi(createUser);
227+ *  const onSubmit = () => {
228+ *    trigger({ name: 'bob' });
229+ *  };
230+ *
231+ *  useLoaderSuccess(loader, () => {
232+ *    // success!
233+ *    // Use this callback to navigate to another view
234+ *  });
235+ *
236+ *  return <button onClick={onSubmit}>Create user!</button>
237+ * }
238+ * ```
239+ */
240+export function useLoaderSuccess(
241+  cur: Pick<LoaderState, "status">,
242+  success: () => any,
243+) {
244+  const prev = useRef(cur);
245+  useEffect(() => {
246+    if (prev.current.status !== "success" && cur.status === "success") {
247+      success();
248+    }
249+    prev.current = cur;
250+  }, [cur.status]);
251+}
252+
253+interface PersistGateProps {
254+  children: React.ReactNode;
255+  loading?: JSX.Element;
256+}
257+
258+function Loading({ text }: { text: string }) {
259+  return h("div", null, text);
260+}
261+
262+export function PersistGate(
263+  { children, loading = h(Loading) }: PersistGateProps,
264+) {
265+  const schema = useSchema();
266+  const ldr = useSelector((s: any) =>
267+    schema.loaders.selectById(s, { id: PERSIST_LOADER_ID })
268+  );
269+
270+  if (ldr.status === "error") {
271+    return h("div", null, ldr.message);
272+  }
273+
274+  if (ldr.status !== "success") {
275+    return loading;
276+  }
277+
278+  return children;
279+}
M store/batch.ts
+1, -2
1@@ -1,7 +1,6 @@
2 import { action } from "../deps.ts";
3-import { Next } from "../query/types.ts";
4 import { UpdaterCtx } from "./types.ts";
5-import { AnyState } from "../types.ts";
6+import { AnyState, Next } from "../types.ts";
7 
8 export function createBatchMdw<S extends AnyState>(
9   queue: (send: () => void) => void,
M store/persist.ts
+3, -4
 1@@ -1,8 +1,7 @@
 2-import { Err, Ok, Operation, Result } from "../deps.ts";
 3-import { Next } from "../query/types.ts";
 4-import { AnyState } from "../types.ts";
 5+import { Err, Ok, type Operation, type Result } from "../deps.ts";
 6+import type { AnyState, Next } from "../types.ts";
 7 import { select, updateStore } from "./fx.ts";
 8-import { UpdaterCtx } from "./types.ts";
 9+import type { UpdaterCtx } from "./types.ts";
10 
11 export const PERSIST_LOADER_ID = "@@starfx/persist";
12 
M store/query.ts
+2, -2
1@@ -1,6 +1,6 @@
2-import type { ApiCtx, Next, ThunkCtx } from "../query/mod.ts";
3+import type { ApiCtx, ThunkCtx } from "../query/mod.ts";
4 import { compose } from "../compose.ts";
5-import type { AnyAction, AnyState } from "../types.ts";
6+import type { AnyAction, AnyState, Next } from "../types.ts";
7 import { put } from "../action.ts";
8 import { select, updateStore } from "./fx.ts";
9 import { LoaderOutput } from "./slice/loader.ts";
D store/react.ts
+0, -270
  1@@ -1,270 +0,0 @@
  2-import { PERSIST_LOADER_ID } from "./persist.ts";
  3-import type { LoaderOutput } from "./slice/mod.ts";
  4-import type { AnyState, LoaderState } from "../types.ts";
  5-import {
  6-  Provider as ReduxProvider,
  7-  React,
  8-  useDispatch,
  9-  useSelector,
 10-} from "../deps.ts";
 11-import { ThunkAction } from "../query/types.ts";
 12-import type { FxSchema, FxStore } from "./types.ts";
 13-const { useEffect, useRef } = React;
 14-
 15-type ActionFn<P = any> = (p: P) => { toString: () => string };
 16-type ActionFnSimple = () => { toString: () => string };
 17-
 18-export interface UseApiProps<P = any> extends LoaderState {
 19-  trigger: (p: P) => void;
 20-  action: ActionFn<P>;
 21-}
 22-export interface UseApiSimpleProps extends LoaderState {
 23-  trigger: () => void;
 24-  action: ActionFn;
 25-}
 26-export interface UseApiAction<A extends ThunkAction = ThunkAction>
 27-  extends LoaderState {
 28-  trigger: () => void;
 29-  action: A;
 30-}
 31-export type UseApiResult<P, A extends ThunkAction = ThunkAction> =
 32-  | UseApiProps<P>
 33-  | UseApiSimpleProps
 34-  | UseApiAction<A>;
 35-
 36-export interface UseCacheResult<D = any, A extends ThunkAction = ThunkAction>
 37-  extends UseApiAction<A> {
 38-  data: D | null;
 39-}
 40-
 41-const SchemaContext = React.createContext<FxSchema<any> | null>(null);
 42-
 43-export function Provider<S extends AnyState>(
 44-  { store, schema, children }: {
 45-    store: FxStore<S>;
 46-    schema: FxSchema<S>;
 47-    children: React.ReactNode;
 48-  },
 49-) {
 50-  return (
 51-    React.createElement(ReduxProvider, {
 52-      store,
 53-      children: React.createElement(
 54-        SchemaContext.Provider,
 55-        { value: schema, children },
 56-      ) as any,
 57-    })
 58-  );
 59-}
 60-
 61-export function useSchema<S extends AnyState>() {
 62-  return React.useContext(SchemaContext) as FxSchema<S>;
 63-}
 64-
 65-/**
 66- * useLoader will take an action creator or action itself and return the associated
 67- * loader for it.
 68- *
 69- * @returns the loader object for an action creator or action
 70- *
 71- * @example
 72- * ```ts
 73- * import { useLoader } from 'starfx/react';
 74- *
 75- * import { api } from './api';
 76- *
 77- * const fetchUsers = api.get('/users', function*() {
 78- *   // ...
 79- * });
 80- *
 81- * const View = () => {
 82- *   const loader = useLoader(fetchUsers);
 83- *   // or: const loader = useLoader(fetchUsers());
 84- *   return <div>{loader.isLoader ? 'Loading ...' : 'Done!'}</div>
 85- * }
 86- * ```
 87- */
 88-export function useLoader<S extends AnyState>(
 89-  action: ThunkAction | ActionFn,
 90-) {
 91-  const schema = useSchema();
 92-  const id = typeof action === "function" ? `${action}` : action.payload.key;
 93-  return useSelector((s: S) => schema.loaders.selectById(s, { id }));
 94-}
 95-
 96-/**
 97- * useApi will take an action creator or action itself and fetch
 98- * the associated loader and create a `trigger` function that you can call
 99- * later in your react component.
100- *
101- * This hook will *not* fetch the data for you because it does not know how to fetch
102- * data from your redux state.
103- *
104- * @example
105- * ```ts
106- * import { useApi } from 'starfx/react';
107- *
108- * import { api } from './api';
109- *
110- * const fetchUsers = api.get('/users', function*() {
111- *   // ...
112- * });
113- *
114- * const View = () => {
115- *   const { isLoading, trigger } = useApi(fetchUsers);
116- *   useEffect(() => {
117- *     trigger();
118- *   }, []);
119- *   return <div>{isLoading ? : 'Loading' : 'Done!'}</div>
120- * }
121- * ```
122- */
123-export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
124-  action: A,
125-): UseApiAction<A>;
126-export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
127-  action: ActionFn<P>,
128-): UseApiProps<P>;
129-export function useApi<A extends ThunkAction = ThunkAction>(
130-  action: ActionFnSimple,
131-): UseApiSimpleProps;
132-export function useApi(action: any): any {
133-  const dispatch = useDispatch();
134-  const loader = useLoader(action);
135-  const trigger = (p: any) => {
136-    if (typeof action === "function") {
137-      dispatch(action(p));
138-    } else {
139-      dispatch(action);
140-    }
141-  };
142-  return { ...loader, trigger, action };
143-}
144-
145-/**
146- * useQuery uses {@link useApi} and automatically calls `useApi().trigger()`
147- *
148- * @example
149- * ```ts
150- * import { useQuery } from 'starfx/react';
151- *
152- * import { api } from './api';
153- *
154- * const fetchUsers = api.get('/users', function*() {
155- *   // ...
156- * });
157- *
158- * const View = () => {
159- *   const { isLoading } = useQuery(fetchUsers);
160- *   return <div>{isLoading ? : 'Loading' : 'Done!'}</div>
161- * }
162- * ```
163- */
164-export function useQuery<P = any, A extends ThunkAction = ThunkAction<P>>(
165-  action: A,
166-): UseApiAction<A> {
167-  const api = useApi(action);
168-  useEffect(() => {
169-    api.trigger();
170-  }, [action.payload.key]);
171-  return api;
172-}
173-
174-/**
175- * useCache uses {@link useQuery} and automatically selects the cached data associated
176- * with the action creator or action provided.
177- *
178- * @example
179- * ```ts
180- * import { useCache } from 'starfx/react';
181- *
182- * import { api } from './api';
183- *
184- * const fetchUsers = api.get('/users', api.cache());
185- *
186- * const View = () => {
187- *   const { isLoading, data } = useCache(fetchUsers());
188- *   return <div>{isLoading ? : 'Loading' : data.length}</div>
189- * }
190- * ```
191- */
192-export function useCache<P = any, ApiSuccess = any>(
193-  action: ThunkAction<P, ApiSuccess>,
194-): UseCacheResult<typeof action.payload._result, ThunkAction<P, ApiSuccess>> {
195-  const schema = useSchema();
196-  const id = action.payload.key;
197-  const data: any = useSelector((s: any) => schema.cache.selectById(s, { id }));
198-  const query = useQuery(action);
199-  return { ...query, data: data || null };
200-}
201-
202-/**
203- * useLoaderSuccess will activate the callback provided when the loader transitions
204- * from some state to success.
205- *
206- * @example
207- * ```ts
208- * import { useLoaderSuccess, useApi } from 'starfx/react';
209- *
210- * import { api } from './api';
211- *
212- * const createUser = api.post('/users', function*(ctx, next) {
213- *   // ...
214- * });
215- *
216- * const View = () => {
217- *  const { loader, trigger } = useApi(createUser);
218- *  const onSubmit = () => {
219- *    trigger({ name: 'bob' });
220- *  };
221- *
222- *  useLoaderSuccess(loader, () => {
223- *    // success!
224- *    // Use this callback to navigate to another view
225- *  });
226- *
227- *  return <button onClick={onSubmit}>Create user!</button>
228- * }
229- * ```
230- */
231-export function useLoaderSuccess(
232-  cur: Pick<LoaderState, "status">,
233-  success: () => any,
234-) {
235-  const prev = useRef(cur);
236-  useEffect(() => {
237-    if (prev.current.status !== "success" && cur.status === "success") {
238-      success();
239-    }
240-    prev.current = cur;
241-  }, [cur.status]);
242-}
243-
244-interface PersistGateProps {
245-  children: React.ReactNode;
246-  loading?: JSX.Element;
247-  loader: LoaderOutput<any, any>;
248-}
249-
250-function Loading({ text }: { text: string }) {
251-  return React.createElement("div", null, text);
252-}
253-
254-export function PersistGate(
255-  { children, loading = React.createElement(Loading) }: PersistGateProps,
256-) {
257-  const schema = useSchema();
258-  const ldr = useSelector((s: any) =>
259-    schema.loaders.selectById(s, { id: PERSIST_LOADER_ID })
260-  );
261-
262-  if (ldr.status === "error") {
263-    return React.createElement("div", null, ldr.message);
264-  }
265-
266-  if (ldr.status !== "success") {
267-    return loading;
268-  }
269-
270-  return children;
271-}
M store/store.ts
+1, -2
 1@@ -10,9 +10,8 @@ import {
 2   Task,
 3 } from "../deps.ts";
 4 import { BaseMiddleware, compose } from "../compose.ts";
 5-import type { AnyAction, AnyState } from "../types.ts";
 6+import type { AnyAction, AnyState, Next } from "../types.ts";
 7 import { safe } from "../fx/mod.ts";
 8-import { Next } from "../query/types.ts";
 9 import type { FxStore, Listener, StoreUpdater, UpdaterCtx } from "./types.ts";
10 import { StoreContext, StoreUpdateContext } from "./context.ts";
11 import { log } from "../log.ts";
M test/react.test.ts
+16, -118
  1@@ -1,122 +1,20 @@
  2-/* import {
  3-  cleanup,
  4-  fireEvent,
  5-  render,
  6-  screen,
  7-} from "https://esm.sh/@testing-library/react@14.0.0?pin=v122";
  8-
  9+import { asserts, describe, it } from "../test.ts";
 10+import { Provider } from "../react.ts";
 11+import { createSchema, createStore, slice } from "../store/mod.ts";
 12 import { React } from "../deps.ts";
 13-import { asserts, beforeEach, describe, it } from "../test.ts";
 14-import { Provider, sleep as delay, useSelector } from "../deps.ts";
 15-import { configureStore, updateStore } from "../store/mod.ts";
 16-
 17-import { createApi } from "./api.ts";
 18-import * as mdw from "./middleware.ts";
 19-import { useApi } from "./react.ts";
 20-import { selectDataById } from "./slice.ts";
 21-import { createKey } from "./create-key.ts";
 22-
 23-const h = React.createElement;
 24-
 25-const mockUser = { id: "1", email: "test@starfx.com" };
 26-
 27-const jsonBlob = (data: any) => {
 28-  return JSON.stringify(data);
 29-};
 30-
 31-interface User {
 32-  id: string;
 33-  name: string;
 34-  email: string;
 35-}
 36-
 37-const setupTest = async () => {
 38-  const api = createApi();
 39-  api.use(mdw.api());
 40-  api.use(api.routes());
 41-  api.use(function* (ctx, next) {
 42-    yield* delay(10);
 43-    ctx.json = { ok: true, data: mockUser };
 44-    ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
 45-    yield* next();
 46-  });
 47-
 48-  const fetchUser = api.get<{ id: string }>("/user/:id", function* (ctx, next) {
 49-    ctx.cache = true;
 50-    yield* next();
 51-    if (!ctx.json.ok) return;
 52-    yield* updateStore<{ user: User }>((state) => {
 53-      state.user = ctx.json.data;
 54-    });
 55-  });
 56-
 57-  const store = configureStore<{ user?: User }>({
 58-    initialState: {},
 59-  });
 60-  store.run(api.bootup);
 61-
 62-  return { store, fetchUser, api };
 63-};
 64-
 65-describe.ignore("useApi()", () => {
 66-  beforeEach(() => cleanup());
 67-  it("with action", async () => {
 68-    const { fetchUser, store } = await setupTest();
 69-    const App = () => {
 70-      const action = fetchUser({ id: "1" });
 71-      const query = useApi(action);
 72-      const user = useSelector((s: any) =>
 73-        selectDataById(s, { id: action.payload.key })
 74-      ) as User;
 75-
 76-      return h("div", null, [
 77-        h("div", { key: "1" }, user?.email || ""),
 78-        h(
 79-          "button",
 80-          { key: "2", onClick: () => query.trigger() },
 81-          query.isLoading ? "loading" : "fetch",
 82-        ),
 83-        h("div", { key: "3" }, query.isSuccess ? "success" : ""),
 84-      ]);
 85-    };
 86-    // render(h(Provider, { store, children: h(App) }));
 87-
 88-    const button = screen.getByText("fetch");
 89-    fireEvent.click(button);
 90-
 91-    await screen.findByText("loading");
 92-    await screen.findByText(mockUser.email);
 93-    await screen.findByText("success");
 94-    asserts.assert(true);
 95-  });
 96-
 97-  it("with action creator", async () => {
 98-    const { fetchUser, store } = await setupTest();
 99-    const App = () => {
100-      const query = useApi(fetchUser);
101-      const user = useSelector((s: any) => {
102-        const id = createKey(`${fetchUser}`, { id: "1" });
103-        return selectDataById(s, { id });
104-      }) as User;
105-
106-      return h("div", null, [
107-        h("div", { key: "1" }, user?.email || "no user"),
108-        h(
109-          "button",
110-          { key: "2", onClick: () => query.trigger({ id: "1" }) },
111-          query.isLoading ? "loading" : "fetch",
112-        ),
113-        h("div", { key: "3" }, query.isSuccess ? "success" : ""),
114-      ]);
115-    };
116-    // render(h(Provider, { store, children: h(App) }));
117 
118-    const button = screen.getByText("fetch");
119-    fireEvent.click(button);
120+const tests = describe("react");
121 
122-    await screen.findByText("loading");
123-    await screen.findByText(mockUser.email);
124-    await screen.findByText("success");
125-    asserts.assert(true);
126+// typing test
127+it(tests, () => {
128+  const [schema, initialState] = createSchema({
129+    cache: slice.table(),
130+    loaders: slice.loader(),
131   });
132-}); */
133+  const store = createStore({ initialState });
134+  React.createElement(
135+    Provider,
136+    { schema, store, children: React.createElement("div") },
137+  );
138+  asserts.equal(true, true);
139+});
M types.ts
+3, -1
 1@@ -1,10 +1,12 @@
 2-import type { Instruction } from "./deps.ts";
 3+import type { Instruction, Operation } from "./deps.ts";
 4 
 5 export interface Computation<T = unknown> {
 6   // deno-lint-ignore no-explicit-any
 7   [Symbol.iterator](): Iterator<Instruction, T, any>;
 8 }
 9 
10+export type Next = () => Operation<void>;
11+
12 export type IdProp = string | number;
13 export type LoadingStatus = "loading" | "success" | "error" | "idle";
14 export interface LoaderItemState<