repos / starfx

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

commit
0215e8a
parent
531fbae
author
Eric Bower
date
2024-01-18 15:22:43 +0000 UTC
refactor: rm redux (#32)

51 files changed,  +575, -1418
M log.ts
M mod.ts
M npm.ts
M action.ts
+110, -9
  1@@ -1,4 +1,113 @@
  2-import { QueryState } from "./types.ts";
  3+import {
  4+  call,
  5+  createContext,
  6+  each,
  7+  Operation,
  8+  Signal,
  9+  SignalQueueFactory,
 10+  spawn,
 11+  Stream,
 12+} from "./deps.ts";
 13+import { ActionPattern, matcher } from "./matcher.ts";
 14+import type { Action, ActionWithPayload, AnyAction } from "./types.ts";
 15+import { createFilterQueue } from "./queue.ts";
 16+
 17+export const ActionContext = createContext<Signal<AnyAction, void>>(
 18+  "starfx:action",
 19+);
 20+
 21+export function useActions(pattern: ActionPattern): Stream<AnyAction, void> {
 22+  return {
 23+    *subscribe() {
 24+      const actions = yield* ActionContext;
 25+      const match = matcher(pattern);
 26+      yield* SignalQueueFactory.set(() => createFilterQueue(match) as any);
 27+      return yield* actions.subscribe();
 28+    },
 29+  };
 30+}
 31+
 32+export function emit({
 33+  signal,
 34+  action,
 35+}: {
 36+  signal: Signal<AnyAction, void>;
 37+  action: AnyAction | AnyAction[];
 38+}) {
 39+  if (Array.isArray(action)) {
 40+    if (action.length === 0) {
 41+      return;
 42+    }
 43+    action.map((a) => signal.send(a));
 44+  } else {
 45+    signal.send(action);
 46+  }
 47+}
 48+
 49+export function* put(action: AnyAction | AnyAction[]) {
 50+  const signal = yield* ActionContext;
 51+  return emit({
 52+    signal,
 53+    action,
 54+  });
 55+}
 56+
 57+export function take<P>(
 58+  pattern: ActionPattern,
 59+): Operation<ActionWithPayload<P>>;
 60+export function* take(pattern: ActionPattern): Operation<Action> {
 61+  const fd = useActions(pattern);
 62+  for (const action of yield* each(fd)) {
 63+    return action;
 64+  }
 65+
 66+  return { type: "take failed, this should not be possible" };
 67+}
 68+
 69+export function* takeEvery<T>(
 70+  pattern: ActionPattern,
 71+  op: (action: Action) => Operation<T>,
 72+) {
 73+  return yield* spawn(function* (): Operation<void> {
 74+    const fd = useActions(pattern);
 75+    for (const action of yield* each(fd)) {
 76+      yield* spawn(() => op(action));
 77+      yield* each.next();
 78+    }
 79+  });
 80+}
 81+
 82+export function* takeLatest<T>(
 83+  pattern: ActionPattern,
 84+  op: (action: Action) => Operation<T>,
 85+) {
 86+  return yield* spawn(function* (): Operation<void> {
 87+    const fd = useActions(pattern);
 88+    let lastTask;
 89+
 90+    for (const action of yield* each(fd)) {
 91+      if (lastTask) {
 92+        yield* lastTask.halt();
 93+      }
 94+      lastTask = yield* spawn(() => op(action));
 95+      yield* each.next();
 96+    }
 97+  });
 98+}
 99+export const latest = takeLatest;
100+
101+export function* takeLeading<T>(
102+  pattern: ActionPattern,
103+  op: (action: Action) => Operation<T>,
104+) {
105+  return yield* spawn(function* (): Operation<void> {
106+    while (true) {
107+      const action = yield* take(pattern);
108+      yield* call(() => op(action));
109+    }
110+  });
111+}
112+export const leading = takeLeading;
113 
114 export const API_ACTION_PREFIX = "@@starfx";
115 export const createAction = (curType: string) => {
116@@ -8,11 +117,3 @@ export const createAction = (curType: string) => {
117   action.toString = () => type;
118   return action;
119 };
120-
121-export const createQueryState = (s: Partial<QueryState> = {}): QueryState => {
122-  return {
123-    "@@starfx/loaders": {},
124-    "@@starfx/data": {},
125-    ...s,
126-  };
127-};
M deps.ts
+0, -23
 1@@ -53,26 +53,3 @@ export {
 2   produceWithPatches,
 3 } from "https://esm.sh/immer@10.0.2?pin=v122";
 4 export type { Patch } from "https://esm.sh/immer@10.0.2?pin=v122";
 5-
 6-// redux
 7-export type {
 8-  Action,
 9-  Reducer,
10-  ReducersMapObject,
11-} from "https://esm.sh/redux@4.2.1?pin=v122";
12-export {
13-  applyMiddleware,
14-  combineReducers,
15-  legacy_createStore as createStore,
16-} from "https://esm.sh/redux@4.2.1?pin=v122";
17-export type { BatchAction } from "https://esm.sh/redux-batched-actions@0.5.0?pin=v122";
18-export {
19-  BATCH,
20-  batchActions,
21-  enableBatching,
22-} from "https://esm.sh/redux-batched-actions@0.5.0?pin=v122";
23-export {
24-  createLoaderTable,
25-  createReducerMap,
26-  createTable,
27-} from "https://esm.sh/robodux@15.0.2?pin=v122";
M log.ts
+2, -2
 1@@ -1,10 +1,10 @@
 2 import { createChannel, createContext } from "./deps.ts";
 3-import type { ActionWPayload } from "./types.ts";
 4+import type { ActionWithPayload } from "./types.ts";
 5 
 6 export interface LogMessage {
 7   [key: string]: any;
 8 }
 9-export type LogAction = ActionWPayload<LogMessage>;
10+export type LogAction = ActionWithPayload<LogMessage>;
11 
12 export function createLogger(type: string) {
13   return function (payload: LogMessage) {
M mod.ts
+1, -0
1@@ -4,6 +4,7 @@ export * from "./types.ts";
2 export * from "./compose.ts";
3 export * from "./action.ts";
4 export * from "./log.ts";
5+export * from "./supervisor.ts";
6 export {
7   action,
8   call,
M npm.ts
+0, -16
 1@@ -25,10 +25,6 @@ async function init() {
 2         name: "./store",
 3         path: "./store/mod.ts",
 4       },
 5-      {
 6-        name: "./redux",
 7-        path: "./redux/mod.ts",
 8-      },
 9     ],
10     mappings: {
11       "https://deno.land/x/effection@3.0.0-beta.3/mod.ts": {
12@@ -52,18 +48,6 @@ async function init() {
13         name: "reselect",
14         version: "^4.1.8",
15       },
16-      "https://esm.sh/robodux@15.0.2?pin=v122": {
17-        name: "robodux",
18-        version: "^15.0.2",
19-      },
20-      "https://esm.sh/redux@4.2.1?pin=v122": {
21-        name: "redux",
22-        version: "^4.2.1",
23-      },
24-      "https://esm.sh/redux-batched-actions@0.5.0?pin=v122": {
25-        name: "redux-batched-actions",
26-        version: "^0.5.0",
27-      },
28     },
29     outDir: "./npm",
30     shims: {
M query/create-key.ts
+2, -1
 1@@ -20,7 +20,8 @@ function padStart(hash: string, len: number) {
 2   }
 3   return hash;
 4 }
 5-//credit to Ivan Perelivskiy: https://gist.github.com/iperelivskiy/4110988
 6+
 7+// https://gist.github.com/iperelivskiy/4110988
 8 const tinySimpleHash = (s: string) => {
 9   let h = 9;
10   for (let i = 0; i < s.length;) {
D query/react.ts
+0, -210
  1@@ -1,210 +0,0 @@
  2-import type { LoaderState, QueryState } from "../types.ts";
  3-import { React, useDispatch, useSelector } from "../deps.ts";
  4-import { ThunkAction } from "./types.ts";
  5-const { useEffect, useRef } = React;
  6-
  7-// TODO: remove store deps
  8-import { selectDataById, selectLoaderById } from "../redux/mod.ts";
  9-
 10-type ActionFn<P = any> = (p: P) => { toString: () => string };
 11-type ActionFnSimple = () => { toString: () => string };
 12-
 13-export interface UseApiProps<P = any> extends LoaderState {
 14-  trigger: (p: P) => void;
 15-  action: ActionFn<P>;
 16-}
 17-export interface UseApiSimpleProps extends LoaderState {
 18-  trigger: () => void;
 19-  action: ActionFn;
 20-}
 21-export interface UseApiAction<A extends ThunkAction = ThunkAction>
 22-  extends LoaderState {
 23-  trigger: () => void;
 24-  action: A;
 25-}
 26-export type UseApiResult<P, A extends ThunkAction = ThunkAction> =
 27-  | UseApiProps<P>
 28-  | UseApiSimpleProps
 29-  | UseApiAction<A>;
 30-
 31-export interface UseCacheResult<D = any, A extends ThunkAction = ThunkAction>
 32-  extends UseApiAction<A> {
 33-  data: D | null;
 34-}
 35-
 36-/**
 37- * useLoader will take an action creator or action itself and return the associated
 38- * loader for it.
 39- *
 40- * @returns the loader object for an action creator or action
 41- *
 42- * @example
 43- * ```ts
 44- * import { useLoader } from 'starfx/react';
 45- *
 46- * import { api } from './api';
 47- *
 48- * const fetchUsers = api.get('/users', function*() {
 49- *   // ...
 50- * });
 51- *
 52- * const View = () => {
 53- *   const loader = useLoader(fetchUsers);
 54- *   // or: const loader = useLoader(fetchUsers());
 55- *   return <div>{loader.isLoader ? 'Loading ...' : 'Done!'}</div>
 56- * }
 57- * ```
 58- */
 59-export function useLoader<S extends QueryState = QueryState>(
 60-  action: ThunkAction | ActionFn,
 61-) {
 62-  const id = typeof action === "function" ? `${action}` : action.payload.key;
 63-  return useSelector((s: S) => selectLoaderById(s, { id }));
 64-}
 65-
 66-/**
 67- * useApi will take an action creator or action itself and fetch
 68- * the associated loader and create a `trigger` function that you can call
 69- * later in your react component.
 70- *
 71- * This hook will *not* fetch the data for you because it does not know how to fetch
 72- * data from your redux state.
 73- *
 74- * @example
 75- * ```ts
 76- * import { useApi } from 'starfx/react';
 77- *
 78- * import { api } from './api';
 79- *
 80- * const fetchUsers = api.get('/users', function*() {
 81- *   // ...
 82- * });
 83- *
 84- * const View = () => {
 85- *   const { isLoading, trigger } = useApi(fetchUsers);
 86- *   useEffect(() => {
 87- *     trigger();
 88- *   }, []);
 89- *   return <div>{isLoading ? : 'Loading' : 'Done!'}</div>
 90- * }
 91- * ```
 92- */
 93-export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
 94-  action: A,
 95-): UseApiAction<A>;
 96-export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
 97-  action: ActionFn<P>,
 98-): UseApiProps<P>;
 99-export function useApi<A extends ThunkAction = ThunkAction>(
100-  action: ActionFnSimple,
101-): UseApiSimpleProps;
102-export function useApi(action: any): any {
103-  const dispatch = useDispatch();
104-  const loader = useLoader(action);
105-  const trigger = (p: any) => {
106-    if (typeof action === "function") {
107-      dispatch(action(p));
108-    } else {
109-      dispatch(action);
110-    }
111-  };
112-  return { ...loader, trigger, action };
113-}
114-
115-/**
116- * useQuery uses {@link useApi} and automatically calls `useApi().trigger()`
117- *
118- * @example
119- * ```ts
120- * import { useQuery } from 'starfx/react';
121- *
122- * import { api } from './api';
123- *
124- * const fetchUsers = api.get('/users', function*() {
125- *   // ...
126- * });
127- *
128- * const View = () => {
129- *   const { isLoading } = useQuery(fetchUsers);
130- *   return <div>{isLoading ? : 'Loading' : 'Done!'}</div>
131- * }
132- * ```
133- */
134-export function useQuery<P = any, A extends ThunkAction = ThunkAction<P>>(
135-  action: A,
136-): UseApiAction<A> {
137-  const api = useApi(action);
138-  useEffect(() => {
139-    api.trigger();
140-  }, [action.payload.key]);
141-  return api;
142-}
143-
144-/**
145- * useCache uses {@link useQuery} and automatically selects the cached data associated
146- * with the action creator or action provided.
147- *
148- * @example
149- * ```ts
150- * import { useCache } from 'starfx/react';
151- *
152- * import { api } from './api';
153- *
154- * const fetchUsers = api.get('/users', api.cache());
155- *
156- * const View = () => {
157- *   const { isLoading, data } = useCache(fetchUsers());
158- *   return <div>{isLoading ? : 'Loading' : data.length}</div>
159- * }
160- * ```
161- */
162-export function useCache<P = any, ApiSuccess = any>(
163-  action: ThunkAction<P, ApiSuccess>,
164-): UseCacheResult<typeof action.payload._result, ThunkAction<P, ApiSuccess>> {
165-  const id = action.payload.key;
166-  const data: any = useSelector((s: any) => selectDataById(s, { id }));
167-  const query = useQuery(action);
168-  return { ...query, data: data || null };
169-}
170-
171-/**
172- * useLoaderSuccess will activate the callback provided when the loader transitions
173- * from some state to success.
174- *
175- * @example
176- * ```ts
177- * import { useLoaderSuccess, useApi } from 'starfx/react';
178- *
179- * import { api } from './api';
180- *
181- * const createUser = api.post('/users', function*(ctx, next) {
182- *   // ...
183- * });
184- *
185- * const View = () => {
186- *  const { loader, trigger } = useApi(createUser);
187- *  const onSubmit = () => {
188- *    trigger({ name: 'bob' });
189- *  };
190- *
191- *  useLoaderSuccess(loader, () => {
192- *    // success!
193- *    // Use this callback to navigate to another view
194- *  });
195- *
196- *  return <button onClick={onSubmit}>Create user!</button>
197- * }
198- * ```
199- */
200-export function useLoaderSuccess(
201-  cur: Pick<LoaderState, "status">,
202-  success: () => any,
203-) {
204-  const prev = useRef(cur);
205-  useEffect(() => {
206-    if (prev.current.status !== "success" && cur.status === "success") {
207-      success();
208-    }
209-    prev.current = cur;
210-  }, [cur.status]);
211-}
M query/thunk.ts
+2, -4
 1@@ -1,12 +1,10 @@
 2 import { compose } from "../compose.ts";
 3-import type { Payload } from "../types.ts";
 4+import type { ActionWithPayload, Payload } from "../types.ts";
 5 import { keepAlive } from "../mod.ts";
 6-// TODO: remove store deps
 7-import { takeEvery } from "../redux/mod.ts";
 8+import { takeEvery } from "../action.ts";
 9 import { isFn, isObject } from "./util.ts";
10 import { createKey } from "./create-key.ts";
11 import type {
12-  ActionWithPayload,
13   CreateAction,
14   CreateActionPayload,
15   CreateActionWithPayload,
M query/types.ts
+7, -9
 1@@ -1,5 +1,11 @@
 2 import type { Operation, Result } from "../deps.ts";
 3-import type { LoaderItemState, LoaderPayload, Payload } from "../types.ts";
 4+import type {
 5+  Action,
 6+  ActionWithPayload,
 7+  LoaderItemState,
 8+  LoaderPayload,
 9+  Payload,
10+} from "../types.ts";
11 
12 type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
13 
14@@ -93,14 +99,6 @@ export type MiddlewareApiCo<Ctx extends ApiCtx = ApiCtx> =
15 
16 export type Next = () => Operation<void>;
17 
18-export interface Action {
19-  type: string;
20-}
21-
22-export interface ActionWithPayload<P> extends Action {
23-  payload: P;
24-}
25-
26 export interface CreateActionPayload<P = any, ApiSuccess = any> {
27   name: string;
28   key: string;
M react.ts
+1, -2
1@@ -1,4 +1,3 @@
2-export * from "./query/react.ts";
3 export * from "./store/react.ts";
4-export { Provider, useDispatch, useSelector } from "./deps.ts";
5+export { useDispatch, useSelector } from "./deps.ts";
6 export type { TypedUseSelectorHook } from "./deps.ts";
D redux/fx.ts
+0, -131
  1@@ -1,131 +0,0 @@
  2-import type { Action, Operation, Queue, Signal, Stream } from "../deps.ts";
  3-import {
  4-  call,
  5-  createContext,
  6-  createQueue,
  7-  each,
  8-  SignalQueueFactory,
  9-  spawn,
 10-} from "../deps.ts";
 11-import { ActionPattern, matcher } from "../matcher.ts";
 12-import type { ActionWPayload, AnyAction } from "../types.ts";
 13-import type { StoreLike } from "./types.ts";
 14-
 15-export const ActionContext = createContext<Signal<Action, void>>(
 16-  "redux:action",
 17-);
 18-export const StoreContext = createContext<StoreLike>("redux:store");
 19-
 20-function createFilterQueue<T, TClose>(
 21-  predicate: (a: T) => boolean,
 22-): Queue<T, TClose> {
 23-  const queue = createQueue<T, TClose>();
 24-
 25-  return {
 26-    ...queue,
 27-    add(value: T) {
 28-      if (predicate(value)) {
 29-        queue.add(value);
 30-      }
 31-    },
 32-  };
 33-}
 34-
 35-export function* put(action: AnyAction | AnyAction[]) {
 36-  const store = yield* StoreContext;
 37-  if (Array.isArray(action)) {
 38-    action.map((act) => store.dispatch(act));
 39-  } else {
 40-    store.dispatch(action);
 41-  }
 42-}
 43-
 44-export function emit({
 45-  signal,
 46-  action,
 47-}: {
 48-  signal: Signal<AnyAction, void>;
 49-  action: AnyAction | AnyAction[];
 50-}) {
 51-  if (Array.isArray(action)) {
 52-    if (action.length === 0) {
 53-      return;
 54-    }
 55-    action.map((a) => signal.send(a));
 56-  } else {
 57-    signal.send(action);
 58-  }
 59-}
 60-
 61-export function* select<S, R>(selectorFn: (s: S) => R) {
 62-  const store = yield* StoreContext;
 63-  return selectorFn(store.getState() as S);
 64-}
 65-
 66-function useActions(pattern: ActionPattern): Stream<AnyAction, void> {
 67-  return {
 68-    *subscribe() {
 69-      const actions = yield* ActionContext;
 70-      const match = matcher(pattern);
 71-      yield* SignalQueueFactory.set(() =>
 72-        createFilterQueue<AnyAction, void>(match) as any
 73-      );
 74-      return yield* actions.subscribe();
 75-    },
 76-  };
 77-}
 78-
 79-export function take<P>(pattern: ActionPattern): Operation<ActionWPayload<P>>;
 80-export function* take(pattern: ActionPattern): Operation<Action> {
 81-  const fd = useActions(pattern);
 82-  for (const action of yield* each(fd)) {
 83-    return action;
 84-  }
 85-
 86-  return { type: "take failed, this should not be possible" };
 87-}
 88-
 89-export function* takeEvery<T>(
 90-  pattern: ActionPattern,
 91-  op: (action: Action) => Operation<T>,
 92-) {
 93-  return yield* spawn(function* (): Operation<void> {
 94-    const fd = useActions(pattern);
 95-    for (const action of yield* each(fd)) {
 96-      yield* spawn(() => op(action));
 97-      yield* each.next();
 98-    }
 99-  });
100-}
101-
102-export function* takeLatest<T>(
103-  pattern: ActionPattern,
104-  op: (action: Action) => Operation<T>,
105-) {
106-  return yield* spawn(function* (): Operation<void> {
107-    const fd = useActions(pattern);
108-    let lastTask;
109-
110-    for (const action of yield* each(fd)) {
111-      if (lastTask) {
112-        yield* lastTask.halt();
113-      }
114-      lastTask = yield* spawn(() => op(action));
115-      yield* each.next();
116-    }
117-  });
118-}
119-export const latest = takeLatest;
120-
121-export function* takeLeading<T>(
122-  pattern: ActionPattern,
123-  op: (action: Action) => Operation<T>,
124-) {
125-  return yield* spawn(function* (): Operation<void> {
126-    while (true) {
127-      const action = yield* take(pattern);
128-      yield* call(() => op(action));
129-    }
130-  });
131-}
132-export const leading = takeLeading;
D redux/middleware.test.ts
+0, -51
 1@@ -1,51 +0,0 @@
 2-import { describe, expect, it } from "../test.ts";
 3-import { Action, call, sleep } from "../deps.ts";
 4-
 5-import { createFxMiddleware, select } from "./mod.ts";
 6-
 7-const tests = describe("createMiddleware()");
 8-
 9-interface Store<S> {
10-  getState(): S;
11-  dispatch(a: Action): void;
12-}
13-
14-interface TestState {
15-  user: { id: string };
16-}
17-
18-function createStore<S>(state: S): Store<S> {
19-  const store = {
20-    getState(): S {
21-      return state;
22-    },
23-    dispatch(_: Action) {},
24-  };
25-
26-  return store;
27-}
28-
29-it.only(tests, "should be able to grab values from store", async () => {
30-  const store = createStore({ user: { id: "1" } });
31-  const { scope, middleware } = createFxMiddleware();
32-  middleware(store);
33-
34-  let actual;
35-  await scope.run(function* () {
36-    yield* sleep(100);
37-    actual = yield* select((s: TestState) => s.user);
38-  });
39-  expect(actual).toEqual({ id: "1" });
40-});
41-
42-it(tests, "should be able to grab store from a nested call", async () => {
43-  const store = createStore({ user: { id: "2" } });
44-  const { scope, middleware } = createFxMiddleware();
45-  middleware(store);
46-  await scope.run(function* () {
47-    const actual = yield* call(function* () {
48-      return yield* select((s: TestState) => s.user);
49-    });
50-    expect(actual).toEqual({ id: "2" });
51-  });
52-});
D redux/middleware.ts
+0, -93
 1@@ -1,93 +0,0 @@
 2-import {
 3-  BATCH,
 4-  BatchAction,
 5-  combineReducers,
 6-  createScope,
 7-  createSignal,
 8-  enableBatching,
 9-  ReducersMapObject,
10-  Scope,
11-  Signal,
12-} from "../deps.ts";
13-import type { AnyAction } from "../types.ts";
14-import { ActionContext, emit, StoreContext } from "./fx.ts";
15-import { reducers as queryReducers } from "./query.ts";
16-import type { StoreLike } from "./types.ts";
17-
18-export function send(signal: Signal<AnyAction, void>, act: AnyAction) {
19-  let action: AnyAction | AnyAction[] = act;
20-  if (act.type === BATCH) {
21-    action = act.payload as BatchAction[];
22-  }
23-  emit({
24-    signal,
25-    action,
26-  });
27-}
28-
29-export function createFxMiddleware(initScope?: Scope) {
30-  let scope: Scope;
31-  if (initScope) {
32-    scope = initScope;
33-  } else {
34-    const tuple = createScope();
35-    scope = tuple[0];
36-  }
37-  const signal = createSignal<AnyAction, void>();
38-
39-  function middleware<S = unknown>(store: StoreLike<S>) {
40-    scope.set(StoreContext, store);
41-    scope.set(ActionContext, signal);
42-
43-    return (next: (a: unknown) => unknown) => (action: unknown) => {
44-      const result = next(action); // hit reducers
45-      send(signal, action as AnyAction);
46-      return result;
47-    };
48-  }
49-
50-  return { scope, middleware, run: scope.run };
51-}
52-
53-// deno-lint-ignore no-explicit-any
54-interface SetupStoreProps<S = any> {
55-  reducers: ReducersMapObject<S>;
56-}
57-
58-/**
59- * This function will integrate `starfx` and `redux`.
60- *
61- * In order to enable `starfx/query`, it will add some reducers to your `redux`
62- * store for decoupled loaders and a simple data cache.
63- *
64- * It also adds `redux-batched-actions` which is critical for `starfx`.
65- *
66- * @example
67- * ```ts
68- * import { prepareStore } from 'starfx/redux';
69- *
70- * const { reducer, fx } = prepareStore({
71- *  reducers: { users: (state, action) => state },
72- * });
73- *
74- * const store = configureStore({
75- *  reducer,
76- *  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(fx),
77- * });
78- *
79- * fx.run(function*() {
80- *  yield* put({ type: 'LOADING' });
81- *  yield* fetch('https://bower.sh');
82- *  yield* put({ type: 'LOADING_COMPLETE' });
83- * });
84- * ```
85- */
86-export function prepareStore(
87-  { reducers }: SetupStoreProps,
88-) {
89-  const fx = createFxMiddleware();
90-  const reducer = enableBatching(
91-    combineReducers({ ...queryReducers, ...reducers }),
92-  );
93-  return { reducer, fx };
94-}
D redux/mod.ts
+0, -7
1@@ -1,7 +0,0 @@
2-export * from "./fx.ts";
3-export * from "./query.ts";
4-export * from "./middleware.ts";
5-export * from "./types.ts";
6-export type { ActionWPayload, AnyAction, AnyState } from "../types.ts";
7-export { BATCH, batchActions, createSelector } from "../deps.ts";
8-export * from "./supervisor.ts";
D redux/put.test.ts
+0, -110
  1@@ -1,110 +0,0 @@
  2-import { describe, expect, it } from "../test.ts";
  3-import { each, sleep, spawn } from "../deps.ts";
  4-
  5-import { ActionContext, put, take } from "./mod.ts";
  6-import { createTestStore } from "./util.ts";
  7-
  8-const putTests = describe("put()");
  9-
 10-it(putTests, "should send actions through channel", async () => {
 11-  const actual: string[] = [];
 12-
 13-  function* genFn(arg: string) {
 14-    yield* spawn(function* () {
 15-      const actions = yield* ActionContext;
 16-      for (const action of yield* each(actions)) {
 17-        actual.push(action.type);
 18-        yield* each.next();
 19-      }
 20-    });
 21-
 22-    yield* put({
 23-      type: arg,
 24-    });
 25-    yield* put({
 26-      type: "2",
 27-    });
 28-  }
 29-
 30-  const { fx } = createTestStore();
 31-  await fx.run(() => genFn("arg"));
 32-
 33-  const expected = ["arg", "2"];
 34-  expect(actual).toEqual(expected);
 35-});
 36-
 37-it(putTests, "should handle nested puts", async () => {
 38-  const actual: string[] = [];
 39-
 40-  function* genA() {
 41-    yield* put({
 42-      type: "a",
 43-    });
 44-    actual.push("put a");
 45-  }
 46-
 47-  function* genB() {
 48-    yield* take(["a"]);
 49-    yield* put({
 50-      type: "b",
 51-    });
 52-    actual.push("put b");
 53-  }
 54-
 55-  function* root() {
 56-    yield* spawn(genB);
 57-    yield* spawn(genA);
 58-  }
 59-
 60-  const { fx } = createTestStore();
 61-  await fx.run(root);
 62-
 63-  const expected = ["put b", "put a"];
 64-  expect(actual).toEqual(expected);
 65-});
 66-
 67-it(
 68-  putTests,
 69-  "should not cause stack overflow when puts are emitted while dispatching saga",
 70-  async () => {
 71-    function* root() {
 72-      for (let i = 0; i < 5_000; i += 1) {
 73-        yield* put({ type: "test" });
 74-      }
 75-      yield* sleep(0);
 76-    }
 77-
 78-    const { fx } = createTestStore();
 79-    await fx.run(root);
 80-    expect(true).toBe(true);
 81-  },
 82-);
 83-
 84-it(
 85-  putTests,
 86-  "should not miss `put` that was emitted directly after creating a task (caused by another `put`)",
 87-  async () => {
 88-    const actual: string[] = [];
 89-
 90-    function* root() {
 91-      yield* spawn(function* firstspawn() {
 92-        yield* sleep(1000);
 93-        yield* put({ type: "c" });
 94-        yield* put({ type: "do not miss" });
 95-      });
 96-
 97-      yield* take("c");
 98-
 99-      const tsk = yield* spawn(function* () {
100-        yield* take("do not miss");
101-        actual.push("didn't get missed");
102-      });
103-      yield* tsk;
104-    }
105-
106-    const { fx } = createTestStore();
107-    await fx.run(root);
108-    const expected = ["didn't get missed"];
109-    expect(actual).toEqual(expected);
110-  },
111-);
D redux/query.ts
+0, -139
  1@@ -1,139 +0,0 @@
  2-import {
  3-  batchActions,
  4-  createLoaderTable,
  5-  createReducerMap,
  6-  createTable,
  7-} from "../deps.ts";
  8-import { compose } from "../compose.ts";
  9-export { defaultLoader, defaultLoaderItem } from "../store/mod.ts";
 10-import { ApiCtx, createKey, Next } from "../query/mod.ts";
 11-import { put, select } from "./mod.ts";
 12-import type { AnyAction, QueryState } from "../types.ts";
 13-
 14-export function reduxMdw<Ctx extends ApiCtx = ApiCtx>(
 15-  errorFn?: (ctx: Ctx) => string,
 16-) {
 17-  return compose<Ctx>([dispatchActions, loadingMonitor(errorFn), simpleCache]);
 18-}
 19-
 20-export const LOADERS_NAME = "@@starfx/loaders";
 21-export const loaders = createLoaderTable({ name: LOADERS_NAME });
 22-export const {
 23-  loading: setLoaderStart,
 24-  error: setLoaderError,
 25-  success: setLoaderSuccess,
 26-  resetById: resetLoaderById,
 27-} = loaders.actions;
 28-export const { selectTable: selectLoaders, selectById: selectLoaderById } =
 29-  loaders.getSelectors((state: any) => state[LOADERS_NAME] || {});
 30-
 31-export const DATA_NAME = "@@starfx/data";
 32-export const data = createTable<any>({ name: DATA_NAME });
 33-export const { add: addData, reset: resetData } = data.actions;
 34-
 35-export const { selectTable: selectData, selectById: selectDataById } = data
 36-  .getSelectors((s: any) => s[DATA_NAME] || {});
 37-
 38-/**
 39- * Returns data from the starfx slice of redux from an action.
 40- */
 41-export const selectDataByName = (
 42-  s: any,
 43-  p: { name: string; payload?: any },
 44-) => {
 45-  const id = createKey(p.name, p.payload);
 46-  const data = selectDataById(s, { id });
 47-  return data;
 48-};
 49-
 50-export const reducers = createReducerMap(loaders, data);
 51-
 52-/**
 53- * This middleware will take the result of `ctx.actions` and dispatch them
 54- * as a single batch.
 55- *
 56- * @remarks This is useful because sometimes there are a lot of actions that need dispatched
 57- * within the pipeline of the middleware and instead of dispatching them serially this
 58- * improves performance by only hitting the reducers once.
 59- */
 60-export function* dispatchActions(ctx: { actions: AnyAction[] }, next: Next) {
 61-  if (!ctx.actions) ctx.actions = [];
 62-  yield* next();
 63-  if (ctx.actions.length === 0) return;
 64-  yield* put(batchActions(ctx.actions));
 65-}
 66-
 67-/**
 68- * This middleware will automatically cache any data found inside `ctx.json`
 69- * which is where we store JSON data from the {@link mdw.fetch} middleware.
 70- */
 71-export function* simpleCache<Ctx extends ApiCtx = ApiCtx>(
 72-  ctx: Ctx,
 73-  next: Next,
 74-) {
 75-  ctx.cacheData = yield* select((state: QueryState) =>
 76-    selectDataById(state, { id: ctx.key })
 77-  );
 78-  yield* next();
 79-  if (!ctx.cache) return;
 80-  if (ctx.json.ok) {
 81-    yield* put(addData({ [ctx.key]: ctx.json.value }));
 82-  } else {
 83-    yield* put(addData({ [ctx.key]: ctx.json.error }));
 84-  }
 85-  ctx.cacheData = data;
 86-}
 87-
 88-/**
 89- * This middleware will track the status of a fetch request.
 90- */
 91-export function loadingMonitor<Ctx extends ApiCtx = ApiCtx>(
 92-  errorFn: (ctx: Ctx) => string = (ctx) => {
 93-    const jso = ctx.json;
 94-    if (jso.ok) return "";
 95-    return jso.error?.message || "";
 96-  },
 97-) {
 98-  return function* trackLoading(ctx: Ctx, next: Next) {
 99-    yield* put([
100-      setLoaderStart({ id: ctx.name }),
101-      setLoaderStart({ id: ctx.key }),
102-    ]);
103-    if (!ctx.loader) ctx.loader = {} as any;
104-
105-    yield* next();
106-
107-    if (!ctx.response) {
108-      yield* put([
109-        resetLoaderById(ctx.name),
110-        resetLoaderById(ctx.key),
111-      ]);
112-      return;
113-    }
114-
115-    if (!ctx.loader) {
116-      ctx.loader || {};
117-    }
118-
119-    if (!ctx.response.ok) {
120-      yield* put([
121-        setLoaderError({
122-          id: ctx.name as any,
123-          message: errorFn(ctx),
124-          ...ctx.loader,
125-        }),
126-        setLoaderError({
127-          id: ctx.key as any,
128-          message: errorFn(ctx),
129-          ...ctx.loader,
130-        }),
131-      ]);
132-      return;
133-    }
134-
135-    yield* put([
136-      setLoaderSuccess({ id: ctx.name as any, ...ctx.loader }),
137-      setLoaderSuccess({ id: ctx.key as any, ...ctx.loader }),
138-    ]);
139-  };
140-}
D redux/supervisor.ts
+0, -71
 1@@ -1,71 +0,0 @@
 2-import { race } from "../fx/mod.ts";
 3-import { take } from "./fx.ts";
 4-import { call, Operation, sleep, spawn, Task } from "../deps.ts";
 5-import type { ActionWPayload, AnyAction } from "../types.ts";
 6-import type { CreateActionPayload, Supervisor } from "../query/mod.ts";
 7-
 8-const MS = 1000;
 9-const SECONDS = 1 * MS;
10-const MINUTES = 60 * SECONDS;
11-
12-export function poll(
13-  parentTimer: number = 5 * 1000,
14-  cancelType?: string,
15-): Supervisor {
16-  return function* poller<T>(
17-    actionType: string,
18-    op: (action: AnyAction) => Operation<T>,
19-  ): Operation<T> {
20-    const cancel = cancelType || actionType;
21-    function* fire(action: { type: string }, timer: number) {
22-      while (true) {
23-        yield* call(() => op(action));
24-        yield* sleep(timer);
25-      }
26-    }
27-
28-    while (true) {
29-      const action = yield* take<{ timer?: number }>(actionType);
30-      const timer = action.payload?.timer || parentTimer;
31-      yield* race({
32-        fire: () => fire(action, timer),
33-        cancel: () => take(`${cancel}`),
34-      });
35-    }
36-  };
37-}
38-
39-/**
40- * timer() will create a cache timer for each `key` inside
41- * of a starfx api endpoint.  `key` is a hash of the action type and payload.
42- *
43- * Why do we want this?  If we have an api endpoint to fetch a single app: `fetchApp({ id: 1 })`
44- * if we don't set a timer per key then all calls to `fetchApp` will be on a timer.
45- * So if we call `fetchApp({ id: 1 })` and then `fetchApp({ id: 2 })` if we use a normal
46- * cache timer then the second call will not send an http request.
47- */
48-export function timer(timer: number = 5 * MINUTES): Supervisor {
49-  return function* onTimer<T>(
50-    actionType: string,
51-    op: (action: AnyAction) => Operation<T>,
52-  ) {
53-    const map: { [key: string]: Task<unknown> } = {};
54-
55-    function* activate(action: ActionWPayload<CreateActionPayload>) {
56-      yield* call(() => op(action));
57-      yield* sleep(timer);
58-      delete map[action.payload.key];
59-    }
60-
61-    while (true) {
62-      const action = yield* take<CreateActionPayload>(`${actionType}`);
63-      const key = action.payload.key;
64-      if (!map[key]) {
65-        const task = yield* spawn(function* () {
66-          yield* activate(action);
67-        });
68-        map[key] = task;
69-      }
70-    }
71-  };
72-}
D redux/take-helper.test.ts
+0, -56
 1@@ -1,56 +0,0 @@
 2-import { describe, expect, it } from "../test.ts";
 3-import type { AnyAction } from "../types.ts";
 4-
 5-import { take, takeEvery } from "./mod.ts";
 6-import { createTestStore } from "./util.ts";
 7-
 8-const testEvery = describe("takeEvery()");
 9-
10-it(testEvery, "should work", async () => {
11-  const loop = 10;
12-  const actual: string[][] = [];
13-
14-  function* root() {
15-    const task = yield* takeEvery(
16-      "ACTION",
17-      (action) => worker("a1", "a2", action),
18-    );
19-    yield* take("CANCEL_WATCHER");
20-    yield* task.halt();
21-  }
22-
23-  function* worker(arg1: string, arg2: string, action: AnyAction) {
24-    actual.push([arg1, arg2, action.payload]);
25-  }
26-
27-  const { store, fx } = createTestStore();
28-  const task = fx.run(root);
29-
30-  for (let i = 1; i <= loop / 2; i += 1) {
31-    store.dispatch({
32-      type: "ACTION",
33-      payload: i,
34-    });
35-  }
36-
37-  // no further task should be forked after this
38-  store.dispatch({
39-    type: "CANCEL_WATCHER",
40-  });
41-
42-  for (let i = loop / 2 + 1; i <= loop; i += 1) {
43-    store.dispatch({
44-      type: "ACTION",
45-      payload: i,
46-    });
47-  }
48-  await task;
49-
50-  expect(actual).toEqual([
51-    ["a1", "a2", 1],
52-    ["a1", "a2", 2],
53-    ["a1", "a2", 3],
54-    ["a1", "a2", 4],
55-    ["a1", "a2", 5],
56-  ]);
57-});
D redux/take.test.ts
+0, -120
  1@@ -1,120 +0,0 @@
  2-import { describe, expect, it } from "../test.ts";
  3-import { sleep, spawn } from "../deps.ts";
  4-import type { AnyAction } from "../types.ts";
  5-
  6-import { put, take } from "./mod.ts";
  7-import { createTestStore } from "./util.ts";
  8-
  9-const takeTests = describe("take()");
 10-
 11-it(
 12-  takeTests,
 13-  "a put should complete before more `take` are added and then consumed automatically",
 14-  async () => {
 15-    const actual: AnyAction[] = [];
 16-
 17-    function* channelFn() {
 18-      yield* sleep(10);
 19-      yield* put({ type: "action-1", payload: 1 });
 20-      yield* put({ type: "action-1", payload: 2 });
 21-    }
 22-
 23-    function* root() {
 24-      yield* spawn(channelFn);
 25-
 26-      actual.push(yield* take("action-1"));
 27-      actual.push(yield* take("action-1"));
 28-    }
 29-
 30-    const { fx } = createTestStore();
 31-    await fx.run(root);
 32-
 33-    expect(actual).toEqual([
 34-      { type: "action-1", payload: 1 },
 35-      { type: "action-1", payload: 2 },
 36-    ]);
 37-  },
 38-);
 39-
 40-it(takeTests, "take from default channel", async () => {
 41-  function* channelFn() {
 42-    yield* sleep(10);
 43-    yield* put({ type: "action-*" });
 44-    yield* put({ type: "action-1" });
 45-    yield* put({ type: "action-2" });
 46-    yield* put({ type: "unnoticeable-action" });
 47-    yield* put({
 48-      type: "",
 49-      payload: {
 50-        isAction: true,
 51-      },
 52-    });
 53-    yield* put({
 54-      type: "",
 55-      payload: {
 56-        isMixedWithPredicate: true,
 57-      },
 58-    });
 59-    yield* put({
 60-      type: "action-3",
 61-    });
 62-  }
 63-
 64-  const actual: AnyAction[] = [];
 65-  function* genFn() {
 66-    yield* spawn(channelFn);
 67-
 68-    try {
 69-      actual.push(yield* take("*")); // take all actions
 70-      actual.push(yield* take("action-1")); // take only actions of type 'action-1'
 71-      actual.push(yield* take(["action-2", "action-2222"])); // take either type
 72-      actual.push(yield* take((a: AnyAction) => a.payload?.isAction)); // take if match predicate
 73-      actual.push(
 74-        yield* take([
 75-          "action-3",
 76-          (a: AnyAction) => a.payload?.isMixedWithPredicate,
 77-        ]),
 78-      ); // take if match any from the mixed array
 79-      actual.push(
 80-        yield* take([
 81-          "action-3",
 82-          (a: AnyAction) => a.payload?.isMixedWithPredicate,
 83-        ]),
 84-      ); // take if match any from the mixed array
 85-    } finally {
 86-      actual.push({ type: "auto ended" });
 87-    }
 88-  }
 89-
 90-  const { fx } = createTestStore();
 91-  await fx.run(genFn);
 92-
 93-  const expected = [
 94-    {
 95-      type: "action-*",
 96-    },
 97-    {
 98-      type: "action-1",
 99-    },
100-    {
101-      type: "action-2",
102-    },
103-    {
104-      type: "",
105-      payload: {
106-        isAction: true,
107-      },
108-    },
109-    {
110-      type: "",
111-      payload: {
112-        isMixedWithPredicate: true,
113-      },
114-    },
115-    {
116-      type: "action-3",
117-    },
118-    { type: "auto ended" },
119-  ];
120-  expect(actual).toEqual(expected);
121-});
D redux/types.ts
+0, -6
1@@ -1,6 +0,0 @@
2-import type { AnyAction } from "../types.ts";
3-
4-export interface StoreLike<S = unknown> {
5-  getState: () => S;
6-  dispatch: (action: AnyAction) => void;
7-}
D redux/util.ts
+0, -12
 1@@ -1,12 +0,0 @@
 2-import { applyMiddleware, createStore } from "../deps.ts";
 3-import { prepareStore } from "./middleware.ts";
 4-
 5-export const createTestStore = () => {
 6-  const { reducer, fx } = prepareStore({
 7-    reducers: {
 8-      def: (s = null, _) => s,
 9-    },
10-  });
11-  const store = createStore(reducer, {}, applyMiddleware(fx.middleware as any));
12-  return { store, fx };
13-};
D store/batch.test.ts
+0, -37
 1@@ -1,37 +0,0 @@
 2-import { createBatchMdw } from "./batch.ts";
 3-import { configureStore } from "./store.ts";
 4-import { describe, expect, it } from "../test.ts";
 5-import { createSchema } from "./schema.ts";
 6-import { slice } from "./slice/mod.ts";
 7-import { parallel } from "../fx/mod.ts";
 8-
 9-const batch = describe("batch mdw");
10-
11-it(batch, "should batch notify subscribers based on mdw", async () => {
12-  const schema = createSchema({
13-    cache: slice.table({ empty: {} }),
14-    loaders: slice.loader(),
15-  });
16-  const store = configureStore({
17-    initialState: schema.initialState,
18-    middleware: [createBatchMdw(queueMicrotask)],
19-  });
20-  let counter = 0;
21-  store.subscribe(() => {
22-    counter += 1;
23-  });
24-  await store.run(function* () {
25-    const group: any = yield* parallel([
26-      schema.update(schema.db.cache.add({ "1": "one" })),
27-      schema.update(schema.db.cache.add({ "2": "two" })),
28-      schema.update(schema.db.cache.add({ "3": "three" })),
29-      schema.update(schema.db.cache.add({ "4": "four" })),
30-      schema.update(schema.db.cache.add({ "5": "five" })),
31-      schema.update(schema.db.cache.add({ "6": "six" })),
32-    ]);
33-    yield* group;
34-    // make sure it will still notify subscribers after batched round
35-    yield* schema.update(schema.db.cache.add({ "7": "seven" }));
36-  });
37-  expect(counter).toBe(2);
38-});
M store/context.ts
+4, -9
 1@@ -1,15 +1,10 @@
 2-import { Channel, createChannel, createContext, Signal } from "../deps.ts";
 3-import type { AnyAction, AnyState } from "../types.ts";
 4-
 5+import { Channel, createChannel, createContext } from "../deps.ts";
 6+import type { AnyState } from "../types.ts";
 7 import type { FxStore } from "./types.ts";
 8 
 9-export const ActionContext = createContext<Signal<AnyAction, void>>(
10-  "store:action",
11-);
12-
13 export const StoreUpdateContext = createContext<Channel<void, void>>(
14-  "store:update",
15+  "starfx:store:update",
16   createChannel<void, void>(),
17 );
18 
19-export const StoreContext = createContext<FxStore<AnyState>>("store");
20+export const StoreContext = createContext<FxStore<AnyState>>("starfx:store");
M store/fx.ts
+3, -106
  1@@ -1,19 +1,7 @@
  2-import {
  3-  Action,
  4-  call,
  5-  each,
  6-  Operation,
  7-  Result,
  8-  Signal,
  9-  SignalQueueFactory,
 10-  spawn,
 11-  Stream,
 12-} from "../deps.ts";
 13-import { ActionPattern, matcher } from "../matcher.ts";
 14-import type { ActionWPayload, AnyAction, AnyState } from "../types.ts";
 15+import { Operation, Result } from "../deps.ts";
 16+import type { AnyState } from "../types.ts";
 17 import type { FxStore, StoreUpdater, UpdaterCtx } from "./types.ts";
 18-import { ActionContext, StoreContext } from "./context.ts";
 19-import { createFilterQueue } from "../queue.ts";
 20+import { StoreContext } from "./context.ts";
 21 import { LoaderOutput } from "./slice/loader.ts";
 22 import { safe } from "../fx/mod.ts";
 23 
 24@@ -27,23 +15,6 @@ export function* updateStore<S extends AnyState>(
 25   return ctx;
 26 }
 27 
 28-export function emit({
 29-  signal,
 30-  action,
 31-}: {
 32-  signal: Signal<AnyAction, void>;
 33-  action: AnyAction | AnyAction[];
 34-}) {
 35-  if (Array.isArray(action)) {
 36-    if (action.length === 0) {
 37-      return;
 38-    }
 39-    action.map((a) => signal.send(a));
 40-  } else {
 41-    signal.send(action);
 42-  }
 43-}
 44-
 45 export function select<S, R>(selectorFn: (s: S) => R): Operation<R>;
 46 export function select<S, R, P>(
 47   selectorFn: (s: S, p: P) => R,
 48@@ -57,80 +28,6 @@ export function* select<S, R, P>(
 49   return selectorFn(store.getState() as S, p);
 50 }
 51 
 52-export function* put(action: AnyAction | AnyAction[]) {
 53-  const signal = yield* ActionContext;
 54-  return emit({
 55-    signal,
 56-    action,
 57-  });
 58-}
 59-
 60-function useActions(pattern: ActionPattern): Stream<AnyAction, void> {
 61-  return {
 62-    *subscribe() {
 63-      const actions = yield* ActionContext;
 64-      const match = matcher(pattern);
 65-      yield* SignalQueueFactory.set(() => createFilterQueue(match) as any);
 66-      return yield* actions.subscribe();
 67-    },
 68-  };
 69-}
 70-
 71-export function take<P>(pattern: ActionPattern): Operation<ActionWPayload<P>>;
 72-export function* take(pattern: ActionPattern): Operation<Action> {
 73-  const fd = useActions(pattern);
 74-  for (const action of yield* each(fd)) {
 75-    return action;
 76-  }
 77-
 78-  return { type: "take failed, this should not be possible" };
 79-}
 80-
 81-export function* takeEvery<T>(
 82-  pattern: ActionPattern,
 83-  op: (action: Action) => Operation<T>,
 84-) {
 85-  return yield* spawn(function* (): Operation<void> {
 86-    const fd = useActions(pattern);
 87-    for (const action of yield* each(fd)) {
 88-      yield* spawn(() => op(action));
 89-      yield* each.next();
 90-    }
 91-  });
 92-}
 93-
 94-export function* takeLatest<T>(
 95-  pattern: ActionPattern,
 96-  op: (action: Action) => Operation<T>,
 97-) {
 98-  return yield* spawn(function* (): Operation<void> {
 99-    const fd = useActions(pattern);
100-    let lastTask;
101-
102-    for (const action of yield* each(fd)) {
103-      if (lastTask) {
104-        yield* lastTask.halt();
105-      }
106-      lastTask = yield* spawn(() => op(action));
107-      yield* each.next();
108-    }
109-  });
110-}
111-export const latest = takeLatest;
112-
113-export function* takeLeading<T>(
114-  pattern: ActionPattern,
115-  op: (action: Action) => Operation<T>,
116-) {
117-  return yield* spawn(function* (): Operation<void> {
118-    while (true) {
119-      const action = yield* take(pattern);
120-      yield* call(() => op(action));
121-    }
122-  });
123-}
124-export const leading = takeLeading;
125-
126 export function createTracker<T, M extends Record<string, unknown>>(
127   loader: LoaderOutput<M, AnyState>,
128 ) {
M store/mod.ts
+0, -1
1@@ -2,7 +2,6 @@ export * from "./context.ts";
2 export * from "./fx.ts";
3 export * from "./store.ts";
4 export * from "./types.ts";
5-export * from "./supervisor.ts";
6 export { createSelector } from "../deps.ts";
7 export * from "./slice/mod.ts";
8 export * from "./schema.ts";
M store/query.ts
+2, -2
 1@@ -1,8 +1,8 @@
 2 import type { ApiCtx, Next, ThunkCtx } from "../query/mod.ts";
 3 import { compose } from "../compose.ts";
 4 import type { AnyAction, AnyState } from "../types.ts";
 5-
 6-import { put, select, updateStore } from "./fx.ts";
 7+import { put } from "../action.ts";
 8+import { select, updateStore } from "./fx.ts";
 9 import { LoaderOutput } from "./slice/loader.ts";
10 import { TableOutput } from "./slice/table.ts";
11 
M store/react.ts
+243, -5
  1@@ -1,6 +1,244 @@
  2-import { React, useSelector } from "../deps.ts";
  3 import { PERSIST_LOADER_ID } from "./persist.ts";
  4 import type { LoaderOutput } from "./slice/mod.ts";
  5+import type { AnyState, LoaderState } from "../types.ts";
  6+import {
  7+  Provider as ReduxProvider,
  8+  React,
  9+  useDispatch,
 10+  useSelector,
 11+} from "../deps.ts";
 12+import { ThunkAction } from "../query/types.ts";
 13+import type { FxSchema, FxStore } from "./types.ts";
 14+const { useEffect, useRef } = React;
 15+
 16+type ActionFn<P = any> = (p: P) => { toString: () => string };
 17+type ActionFnSimple = () => { toString: () => string };
 18+
 19+export interface UseApiProps<P = any> extends LoaderState {
 20+  trigger: (p: P) => void;
 21+  action: ActionFn<P>;
 22+}
 23+export interface UseApiSimpleProps extends LoaderState {
 24+  trigger: () => void;
 25+  action: ActionFn;
 26+}
 27+export interface UseApiAction<A extends ThunkAction = ThunkAction>
 28+  extends LoaderState {
 29+  trigger: () => void;
 30+  action: A;
 31+}
 32+export type UseApiResult<P, A extends ThunkAction = ThunkAction> =
 33+  | UseApiProps<P>
 34+  | UseApiSimpleProps
 35+  | UseApiAction<A>;
 36+
 37+export interface UseCacheResult<D = any, A extends ThunkAction = ThunkAction>
 38+  extends UseApiAction<A> {
 39+  data: D | null;
 40+}
 41+
 42+const SchemaContext = React.createContext<FxSchema<any> | null>(null);
 43+
 44+export function Provider<S extends AnyState>(
 45+  { store, schema, children }: {
 46+    store: FxStore<S>;
 47+    schema: FxSchema<S>;
 48+    children: React.ReactNode;
 49+  },
 50+) {
 51+  return (
 52+    React.createElement(ReduxProvider, {
 53+      store,
 54+      children: React.createElement(
 55+        SchemaContext.Provider,
 56+        { value: schema, children },
 57+      ) as any,
 58+    })
 59+  );
 60+}
 61+
 62+export function useSchema<S extends AnyState>() {
 63+  return React.useContext(SchemaContext) as FxSchema<S>;
 64+}
 65+
 66+/**
 67+ * useLoader will take an action creator or action itself and return the associated
 68+ * loader for it.
 69+ *
 70+ * @returns the loader object for an action creator or action
 71+ *
 72+ * @example
 73+ * ```ts
 74+ * import { useLoader } from 'starfx/react';
 75+ *
 76+ * import { api } from './api';
 77+ *
 78+ * const fetchUsers = api.get('/users', function*() {
 79+ *   // ...
 80+ * });
 81+ *
 82+ * const View = () => {
 83+ *   const loader = useLoader(fetchUsers);
 84+ *   // or: const loader = useLoader(fetchUsers());
 85+ *   return <div>{loader.isLoader ? 'Loading ...' : 'Done!'}</div>
 86+ * }
 87+ * ```
 88+ */
 89+export function useLoader<S extends AnyState>(
 90+  action: ThunkAction | ActionFn,
 91+) {
 92+  const schema = useSchema();
 93+  const id = typeof action === "function" ? `${action}` : action.payload.key;
 94+  return useSelector((s: S) => schema.loaders.selectById(s, { id }));
 95+}
 96+
 97+/**
 98+ * useApi will take an action creator or action itself and fetch
 99+ * the associated loader and create a `trigger` function that you can call
100+ * later in your react component.
101+ *
102+ * This hook will *not* fetch the data for you because it does not know how to fetch
103+ * data from your redux state.
104+ *
105+ * @example
106+ * ```ts
107+ * import { useApi } from 'starfx/react';
108+ *
109+ * import { api } from './api';
110+ *
111+ * const fetchUsers = api.get('/users', function*() {
112+ *   // ...
113+ * });
114+ *
115+ * const View = () => {
116+ *   const { isLoading, trigger } = useApi(fetchUsers);
117+ *   useEffect(() => {
118+ *     trigger();
119+ *   }, []);
120+ *   return <div>{isLoading ? : 'Loading' : 'Done!'}</div>
121+ * }
122+ * ```
123+ */
124+export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
125+  action: A,
126+): UseApiAction<A>;
127+export function useApi<P = any, A extends ThunkAction = ThunkAction<P>>(
128+  action: ActionFn<P>,
129+): UseApiProps<P>;
130+export function useApi<A extends ThunkAction = ThunkAction>(
131+  action: ActionFnSimple,
132+): UseApiSimpleProps;
133+export function useApi(action: any): any {
134+  const dispatch = useDispatch();
135+  const loader = useLoader(action);
136+  const trigger = (p: any) => {
137+    if (typeof action === "function") {
138+      dispatch(action(p));
139+    } else {
140+      dispatch(action);
141+    }
142+  };
143+  return { ...loader, trigger, action };
144+}
145+
146+/**
147+ * useQuery uses {@link useApi} and automatically calls `useApi().trigger()`
148+ *
149+ * @example
150+ * ```ts
151+ * import { useQuery } from 'starfx/react';
152+ *
153+ * import { api } from './api';
154+ *
155+ * const fetchUsers = api.get('/users', function*() {
156+ *   // ...
157+ * });
158+ *
159+ * const View = () => {
160+ *   const { isLoading } = useQuery(fetchUsers);
161+ *   return <div>{isLoading ? : 'Loading' : 'Done!'}</div>
162+ * }
163+ * ```
164+ */
165+export function useQuery<P = any, A extends ThunkAction = ThunkAction<P>>(
166+  action: A,
167+): UseApiAction<A> {
168+  const api = useApi(action);
169+  useEffect(() => {
170+    api.trigger();
171+  }, [action.payload.key]);
172+  return api;
173+}
174+
175+/**
176+ * useCache uses {@link useQuery} and automatically selects the cached data associated
177+ * with the action creator or action provided.
178+ *
179+ * @example
180+ * ```ts
181+ * import { useCache } from 'starfx/react';
182+ *
183+ * import { api } from './api';
184+ *
185+ * const fetchUsers = api.get('/users', api.cache());
186+ *
187+ * const View = () => {
188+ *   const { isLoading, data } = useCache(fetchUsers());
189+ *   return <div>{isLoading ? : 'Loading' : data.length}</div>
190+ * }
191+ * ```
192+ */
193+export function useCache<P = any, ApiSuccess = any>(
194+  action: ThunkAction<P, ApiSuccess>,
195+): UseCacheResult<typeof action.payload._result, ThunkAction<P, ApiSuccess>> {
196+  const schema = useSchema();
197+  const id = action.payload.key;
198+  const data: any = useSelector((s: any) => schema.cache.selectById(s, { id }));
199+  const query = useQuery(action);
200+  return { ...query, data: data || null };
201+}
202+
203+/**
204+ * useLoaderSuccess will activate the callback provided when the loader transitions
205+ * from some state to success.
206+ *
207+ * @example
208+ * ```ts
209+ * import { useLoaderSuccess, useApi } from 'starfx/react';
210+ *
211+ * import { api } from './api';
212+ *
213+ * const createUser = api.post('/users', function*(ctx, next) {
214+ *   // ...
215+ * });
216+ *
217+ * const View = () => {
218+ *  const { loader, trigger } = useApi(createUser);
219+ *  const onSubmit = () => {
220+ *    trigger({ name: 'bob' });
221+ *  };
222+ *
223+ *  useLoaderSuccess(loader, () => {
224+ *    // success!
225+ *    // Use this callback to navigate to another view
226+ *  });
227+ *
228+ *  return <button onClick={onSubmit}>Create user!</button>
229+ * }
230+ * ```
231+ */
232+export function useLoaderSuccess(
233+  cur: Pick<LoaderState, "status">,
234+  success: () => any,
235+) {
236+  const prev = useRef(cur);
237+  useEffect(() => {
238+    if (prev.current.status !== "success" && cur.status === "success") {
239+      success();
240+    }
241+    prev.current = cur;
242+  }, [cur.status]);
243+}
244 
245 interface PersistGateProps {
246   children: React.ReactNode;
247@@ -13,11 +251,11 @@ function Loading({ text }: { text: string }) {
248 }
249 
250 export function PersistGate(
251-  { loader, children, loading = React.createElement(Loading) }:
252-    PersistGateProps,
253+  { children, loading = React.createElement(Loading) }: PersistGateProps,
254 ) {
255-  const ldr = useSelector((s) =>
256-    loader.selectById(s, { id: PERSIST_LOADER_ID })
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") {
M store/schema.ts
+8, -17
 1@@ -1,28 +1,17 @@
 2-import { AnyState } from "../types.ts";
 3 import { updateStore } from "./fx.ts";
 4-import { LoaderOutput } from "./slice/loader.ts";
 5-import { TableOutput } from "./slice/table.ts";
 6-import { BaseSchema, FxStore, StoreUpdater } from "./types.ts";
 7+import { FxMap, FxSchema, StoreUpdater } from "./types.ts";
 8 
 9 export function createSchema<
10-  O extends {
11-    loaders: <M extends AnyState>(s: string) => LoaderOutput<M, AnyState>;
12-    cache: (s: string) => TableOutput<any, AnyState>;
13-    [key: string]: (name: string) => BaseSchema<unknown>;
14-  },
15+  O extends FxMap,
16   S extends { [key in keyof O]: ReturnType<O[key]>["initialState"] },
17 >(
18   slices: O,
19-): {
20-  db: { [key in keyof O]: ReturnType<O[key]> };
21-  initialState: S;
22-  update: FxStore<S>["update"];
23-} {
24-  const db = Object.keys(slices).reduce((acc, key) => {
25+): [FxSchema<S, O>, S] {
26+  const db = Object.keys(slices).reduce<FxSchema<S, O>>((acc, key) => {
27     // deno-lint-ignore no-explicit-any
28     (acc as any)[key] = slices[key](key);
29     return acc;
30-  }, {} as { [key in keyof O]: ReturnType<O[key]> });
31+  }, {} as FxSchema<S, O>);
32 
33   const initialState = Object.keys(db).reduce((acc, key) => {
34     // deno-lint-ignore no-explicit-any
35@@ -38,5 +27,7 @@ export function createSchema<
36     return yield* updateStore(ups);
37   }
38 
39-  return { db, initialState, update };
40+  db.update = update;
41+
42+  return [db, initialState];
43 }
M store/store.ts
+2, -2
 1@@ -14,9 +14,9 @@ import type { AnyAction, AnyState } from "../types.ts";
 2 import { safe } from "../fx/mod.ts";
 3 import { Next } from "../query/types.ts";
 4 import type { FxStore, Listener, StoreUpdater, UpdaterCtx } from "./types.ts";
 5-import { ActionContext, StoreContext, StoreUpdateContext } from "./context.ts";
 6-import { emit } from "./fx.ts";
 7+import { StoreContext, StoreUpdateContext } from "./context.ts";
 8 import { log } from "../log.ts";
 9+import { ActionContext, emit } from "../action.ts";
10 
11 const stubMsg = "This is merely a stub, not implemented";
12 
M store/types.ts
+12, -0
 1@@ -36,6 +36,18 @@ export type Output<O extends { [key: string]: BaseSchema<unknown> }> = {
 2   [key in keyof O]: O[key]["initialState"];
 3 };
 4 
 5+export interface FxMap {
 6+  loaders: <M extends AnyState>(s: string) => LoaderOutput<M, AnyState>;
 7+  cache: (s: string) => TableOutput<any, AnyState>;
 8+  [key: string]: (name: string) => BaseSchema<unknown>;
 9+}
10+
11+export type FxSchema<S extends AnyState, O extends FxMap = FxMap> =
12+  & {
13+    [key in keyof O]: ReturnType<O[key]>;
14+  }
15+  & { update: FxStore<S>["update"] };
16+
17 export interface FxStore<S extends AnyState> {
18   getScope: () => Scope;
19   getState: () => S;
R store/supervisor.ts => supervisor.ts
+6, -6
 1@@ -1,8 +1,8 @@
 2-import { race } from "../fx/mod.ts";
 3-import { take } from "./fx.ts";
 4-import { call, Callable, Operation, sleep, spawn, Task } from "../deps.ts";
 5-import type { ActionWPayload, AnyAction } from "../types.ts";
 6-import type { CreateActionPayload } from "../query/mod.ts";
 7+import { race } from "./fx/mod.ts";
 8+import { take } from "./action.ts";
 9+import { call, Callable, Operation, sleep, spawn, Task } from "./deps.ts";
10+import type { ActionWithPayload, AnyAction } from "./types.ts";
11+import type { CreateActionPayload } from "./query/mod.ts";
12 
13 const MS = 1000;
14 const SECONDS = 1 * MS;
15@@ -48,7 +48,7 @@ export function timer(timer: number = 5 * MINUTES) {
16   ) {
17     const map: { [key: string]: Task<unknown> } = {};
18 
19-    function* activate(action: ActionWPayload<CreateActionPayload>) {
20+    function* activate(action: ActionWithPayload<CreateActionPayload>) {
21       yield* call(() => op(action));
22       yield* sleep(timer);
23       delete map[action.payload.key];
R query/util.test.ts => test/action.test.ts
+1, -1
1@@ -1,5 +1,5 @@
2 import { describe, expect, it } from "../test.ts";
3-import { API_ACTION_PREFIX, createAction } from "../action.ts";
4+import { API_ACTION_PREFIX, createAction } from "../mod.ts";
5 
6 const tests = describe("createAction()");
7 
R query/api.test.ts => test/api.test.ts
+17, -15
 1@@ -1,21 +1,23 @@
 2 import { describe, expect, it } from "../test.ts";
 3-import { keepAlive } from "../fx/mod.ts";
 4+import { sleep } from "../test.ts";
 5 import {
 6   configureStore,
 7   createSchema,
 8   slice,
 9   storeMdw,
10-  takeEvery,
11   updateStore,
12 } from "../store/mod.ts";
13-import { sleep } from "../test.ts";
14-import { safe } from "../mod.ts";
15-import * as mdw from "./mdw.ts";
16-import { createApi } from "./api.ts";
17-import { createKey } from "./create-key.ts";
18-import type { ApiCtx } from "./types.ts";
19-import { call } from "../deps.ts";
20-import { useCache } from "./react.ts";
21+import {
22+  type ApiCtx,
23+  call,
24+  createApi,
25+  createKey,
26+  keepAlive,
27+  mdw,
28+  safe,
29+  takeEvery,
30+} from "../mod.ts";
31+import { useCache } from "../react.ts";
32 
33 interface User {
34   id: string;
35@@ -27,12 +29,12 @@ const emptyUser: User = { id: "", name: "", email: "" };
36 const mockUser: User = { id: "1", name: "test", email: "test@test.com" };
37 
38 const testStore = () => {
39-  const schema = createSchema({
40+  const [schema, initialState] = createSchema({
41     users: slice.table<User>({ empty: emptyUser }),
42     loaders: slice.loader(),
43     cache: slice.table({ empty: {} }),
44   });
45-  const store = configureStore(schema);
46+  const store = configureStore({ initialState });
47   return { schema, store };
48 };
49 
50@@ -240,7 +242,7 @@ it(tests, "createApi with hash key on a large post", async () => {
51   const { store, schema } = testStore();
52   const query = createApi();
53   query.use(mdw.api());
54-  query.use(storeMdw.store(schema.db));
55+  query.use(storeMdw.store(schema));
56   query.use(query.routes());
57   query.use(function* fetchApi(ctx, next) {
58     const data = {
59@@ -309,7 +311,7 @@ it(tests, "createApi - two identical endpoints", async () => {
60   const { store, schema } = testStore();
61   const api = createApi();
62   api.use(mdw.api());
63-  api.use(storeMdw.store(schema.db));
64+  api.use(storeMdw.store(schema));
65   api.use(mdw.nameParser);
66   api.use(api.routes());
67 
68@@ -463,7 +465,7 @@ it(tests, "should bubble up error", () => {
69     }
70   });
71   api.use(mdw.queryCtx);
72-  api.use(storeMdw.store(schema.db));
73+  api.use(storeMdw.store(schema));
74   api.use(api.routes());
75 
76   const fetchUser = api.get(
A test/batch.test.ts
+39, -0
 1@@ -0,0 +1,39 @@
 2+import { describe, expect, it } from "../test.ts";
 3+import {
 4+  configureStore,
 5+  createBatchMdw,
 6+  createSchema,
 7+  slice,
 8+} from "../store/mod.ts";
 9+import { parallel } from "../mod.ts";
10+
11+const batch = describe("batch mdw");
12+
13+it(batch, "should batch notify subscribers based on mdw", async () => {
14+  const [schema, initialState] = createSchema({
15+    cache: slice.table({ empty: {} }),
16+    loaders: slice.loader(),
17+  });
18+  const store = configureStore({
19+    initialState,
20+    middleware: [createBatchMdw(queueMicrotask)],
21+  });
22+  let counter = 0;
23+  store.subscribe(() => {
24+    counter += 1;
25+  });
26+  await store.run(function* () {
27+    const group: any = yield* parallel([
28+      schema.update(schema.cache.add({ "1": "one" })),
29+      schema.update(schema.cache.add({ "2": "two" })),
30+      schema.update(schema.cache.add({ "3": "three" })),
31+      schema.update(schema.cache.add({ "4": "four" })),
32+      schema.update(schema.cache.add({ "5": "five" })),
33+      schema.update(schema.cache.add({ "6": "six" })),
34+    ]);
35+    yield* group;
36+    // make sure it will still notify subscribers after batched round
37+    yield* schema.update(schema.cache.add({ "7": "seven" }));
38+  });
39+  expect(counter).toBe(2);
40+});
R compose.test.ts => test/compose.test.ts
+2, -5
 1@@ -1,8 +1,5 @@
 2-import { asserts, describe, expect, it } from "./test.ts";
 3-
 4-import { Err, Ok, Result, run, sleep } from "./deps.ts";
 5-import { compose } from "./compose.ts";
 6-import { safe } from "./mod.ts";
 7+import { asserts, describe, expect, it } from "../test.ts";
 8+import { compose, Err, Ok, Result, run, safe, sleep } from "../mod.ts";
 9 
10 const tests = describe("compose()");
11 
R store/configureStore.test.ts => test/configureStore.test.ts
+2, -3
 1@@ -1,7 +1,6 @@
 2 import { describe, expect, it } from "../test.ts";
 3-import { select } from "./mod.ts";
 4-import { configureStore } from "./store.ts";
 5-import { call } from "../deps.ts";
 6+import { configureStore, select } from "../store/mod.ts";
 7+import { call } from "../mod.ts";
 8 
 9 const tests = describe("configureStore()");
10 
R query/create-key.test.ts => test/create-key.test.ts
+1, -3
1@@ -1,7 +1,5 @@
2 import { describe, expect, it } from "../test.ts";
3-
4-import type { ActionWithPayload } from "./types.ts";
5-import { createApi } from "./api.ts";
6+import { type ActionWithPayload, createApi } from "../mod.ts";
7 
8 const getKeyOf = (action: ActionWithPayload<{ key: string }>): string =>
9   action.payload.key;
R query/fetch.test.ts => test/fetch.test.ts
+19, -27
  1@@ -1,14 +1,6 @@
  2 import { describe, expect, install, it, mock } from "../test.ts";
  3-import {
  4-  configureStore,
  5-  createSchema,
  6-  slice,
  7-  storeMdw,
  8-  takeEvery,
  9-} from "../store/mod.ts";
 10-import * as fetchMdw from "./fetch.ts";
 11-import { createApi } from "./api.ts";
 12-import * as mdw from "./mdw.ts";
 13+import { configureStore, createSchema, slice, storeMdw } from "../store/mod.ts";
 14+import { createApi, mdw, takeEvery } from "../mod.ts";
 15 
 16 install();
 17 
 18@@ -21,11 +13,11 @@ const delay = (n = 200) =>
 19   });
 20 
 21 const testStore = () => {
 22-  const schema = createSchema({
 23+  const [schema, initialState] = createSchema({
 24     loaders: slice.loader(),
 25     cache: slice.table({ empty: {} }),
 26   });
 27-  const store = configureStore(schema);
 28+  const store = configureStore({ initialState });
 29   return { schema, store };
 30 };
 31 
 32@@ -42,9 +34,9 @@ it(
 33     const { store, schema } = testStore();
 34     const api = createApi();
 35     api.use(mdw.api());
 36-    api.use(storeMdw.store(schema.db));
 37+    api.use(storeMdw.store(schema));
 38     api.use(api.routes());
 39-    api.use(fetchMdw.headers);
 40+    api.use(mdw.headers);
 41     api.use(mdw.fetch({ baseUrl }));
 42 
 43     const actual: any[] = [];
 44@@ -91,7 +83,7 @@ it(
 45     const { store, schema } = testStore();
 46     const api = createApi();
 47     api.use(mdw.api());
 48-    api.use(storeMdw.store(schema.db));
 49+    api.use(storeMdw.store(schema));
 50     api.use(api.routes());
 51     api.use(mdw.fetch({ baseUrl }));
 52 
 53@@ -127,7 +119,7 @@ it(tests, "error handling", async () => {
 54   const { schema, store } = testStore();
 55   const api = createApi();
 56   api.use(mdw.api());
 57-  api.use(storeMdw.store(schema.db));
 58+  api.use(storeMdw.store(schema));
 59   api.use(api.routes());
 60   api.use(mdw.fetch({ baseUrl }));
 61 
 62@@ -163,7 +155,7 @@ it(tests, "status 204", async () => {
 63   const { schema, store } = testStore();
 64   const api = createApi();
 65   api.use(mdw.api());
 66-  api.use(storeMdw.store(schema.db));
 67+  api.use(storeMdw.store(schema));
 68   api.use(api.routes());
 69   api.use(function* (ctx, next) {
 70     const url = ctx.req().url;
 71@@ -203,7 +195,7 @@ it(tests, "malformed json", async () => {
 72   const { schema, store } = testStore();
 73   const api = createApi();
 74   api.use(mdw.api());
 75-  api.use(storeMdw.store(schema.db));
 76+  api.use(storeMdw.store(schema));
 77   api.use(api.routes());
 78   api.use(function* (ctx, next) {
 79     const url = ctx.req().url;
 80@@ -248,9 +240,9 @@ it(tests, "POST", async () => {
 81   const { schema, store } = testStore();
 82   const api = createApi();
 83   api.use(mdw.api());
 84-  api.use(storeMdw.store(schema.db));
 85+  api.use(storeMdw.store(schema));
 86   api.use(api.routes());
 87-  api.use(fetchMdw.headers);
 88+  api.use(mdw.headers);
 89   api.use(mdw.fetch({ baseUrl }));
 90 
 91   const fetchUsers = api.post(
 92@@ -291,9 +283,9 @@ it(tests, "POST multiple endpoints with same uri", async () => {
 93   const { store, schema } = testStore();
 94   const api = createApi();
 95   api.use(mdw.api());
 96-  api.use(storeMdw.store(schema.db));
 97+  api.use(storeMdw.store(schema));
 98   api.use(api.routes());
 99-  api.use(fetchMdw.headers);
100+  api.use(mdw.headers);
101   api.use(mdw.fetch({ baseUrl }));
102 
103   const fetchUsers = api.post<{ id: string }>(
104@@ -353,7 +345,7 @@ it(
105     const { store, schema } = testStore();
106     const api = createApi();
107     api.use(mdw.api());
108-    api.use(storeMdw.store(schema.db));
109+    api.use(storeMdw.store(schema));
110     api.use(api.routes());
111     api.use(mdw.fetch({ baseUrl }));
112 
113@@ -402,7 +394,7 @@ it(
114     const { schema, store } = testStore();
115     const api = createApi();
116     api.use(mdw.api());
117-    api.use(storeMdw.store(schema.db));
118+    api.use(storeMdw.store(schema));
119     api.use(api.routes());
120     api.use(mdw.fetch({ baseUrl }));
121 
122@@ -448,7 +440,7 @@ it(
123     let actual = null;
124     const api = createApi();
125     api.use(mdw.api());
126-    api.use(storeMdw.store(schema.db));
127+    api.use(storeMdw.store(schema));
128     api.use(api.routes());
129     api.use(mdw.fetch({ baseUrl }));
130 
131@@ -479,7 +471,7 @@ it(
132     let actual = null;
133     const api = createApi();
134     api.use(mdw.api());
135-    api.use(storeMdw.store(schema.db));
136+    api.use(storeMdw.store(schema));
137     api.use(api.routes());
138     api.use(mdw.fetch({ baseUrl }));
139 
140@@ -504,7 +496,7 @@ it(tests, "should use dynamic mdw to mock response", async () => {
141   let actual = null;
142   const api = createApi();
143   api.use(mdw.api());
144-  api.use(storeMdw.store(schema.db));
145+  api.use(storeMdw.store(schema));
146   api.use(api.routes());
147   api.use(mdw.fetch({ baseUrl }));
148 
R query/mdw.test.ts => test/mdw.test.ts
+22, -24
  1@@ -1,18 +1,20 @@
  2-import { assertLike, asserts, describe, expect, it } from "../test.ts";
  3-import { createApi, createKey, mdw } from "../query/mod.ts";
  4-import type { ApiCtx, Next, ThunkCtx } from "../query/mod.ts";
  5-import { createQueryState } from "../action.ts";
  6-import { sleep } from "../test.ts";
  7+import { assertLike, asserts, describe, expect, it, sleep } from "../test.ts";
  8 import {
  9   configureStore,
 10   createSchema,
 11   slice,
 12   storeMdw,
 13-  takeEvery,
 14-  takeLatest,
 15   updateStore,
 16 } from "../store/mod.ts";
 17-import { safe } from "../mod.ts";
 18+import {
 19+  createApi,
 20+  createKey,
 21+  mdw,
 22+  safe,
 23+  takeEvery,
 24+  takeLatest,
 25+} from "../mod.ts";
 26+import type { ApiCtx, Next, ThunkCtx } from "../mod.ts";
 27 
 28 interface User {
 29   id: string;
 30@@ -30,12 +32,12 @@ const jsonBlob = (data: any) => {
 31 };
 32 
 33 const testStore = () => {
 34-  const schema = createSchema({
 35+  const [schema, initialState] = createSchema({
 36     users: slice.table<User>({ empty: emptyUser }),
 37     loaders: slice.loader(),
 38     cache: slice.table({ empty: {} }),
 39   });
 40-  const store = configureStore(schema);
 41+  const store = configureStore({ initialState });
 42   return { schema, store };
 43 };
 44 
 45@@ -109,7 +111,7 @@ it(tests, "with loader", () => {
 46   const { schema, store } = testStore();
 47   const api = createApi<ApiCtx>();
 48   api.use(mdw.api());
 49-  api.use(storeMdw.store(schema.db));
 50+  api.use(storeMdw.store(schema));
 51   api.use(api.routes());
 52   api.use(function* fetchApi(ctx, next) {
 53     ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
 54@@ -152,7 +154,7 @@ it(tests, "with item loader", () => {
 55   const { store, schema } = testStore();
 56   const api = createApi<ApiCtx>();
 57   api.use(mdw.api());
 58-  api.use(storeMdw.store(schema.db));
 59+  api.use(storeMdw.store(schema));
 60   api.use(api.routes());
 61   api.use(function* fetchApi(ctx, next) {
 62     ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
 63@@ -249,7 +251,7 @@ it(tests, "simpleCache", () => {
 64   const { store, schema } = testStore();
 65   const api = createApi<ApiCtx>();
 66   api.use(mdw.api());
 67-  api.use(storeMdw.store(schema.db));
 68+  api.use(storeMdw.store(schema));
 69   api.use(api.routes());
 70   api.use(function* fetchApi(ctx, next) {
 71     const data = { users: [mockUser] };
 72@@ -279,7 +281,7 @@ it(tests, "overriding default loader behavior", () => {
 73   const { store, schema } = testStore();
 74   const api = createApi<ApiCtx>();
 75   api.use(mdw.api());
 76-  api.use(storeMdw.store(schema.db));
 77+  api.use(storeMdw.store(schema));
 78   api.use(api.routes());
 79   api.use(function* fetchApi(ctx, next) {
 80     const data = { users: [mockUser] };
 81@@ -336,7 +338,7 @@ it(tests, "mdw.api() - error handler", () => {
 82   const { schema, store } = testStore();
 83   const query = createApi<ApiCtx>();
 84   query.use(mdw.api());
 85-  query.use(storeMdw.store(schema.db));
 86+  query.use(storeMdw.store(schema));
 87   query.use(query.routes());
 88   query.use(function* () {
 89     throw new Error("something happened");
 90@@ -352,7 +354,7 @@ it(tests, "createApi with own key", async () => {
 91   const { schema, store } = testStore();
 92   const query = createApi();
 93   query.use(mdw.api());
 94-  query.use(storeMdw.store(schema.db));
 95+  query.use(storeMdw.store(schema));
 96   query.use(query.routes());
 97   query.use(mdw.customKey);
 98   query.use(function* fetchApi(ctx, next) {
 99@@ -409,7 +411,7 @@ it(tests, "createApi with own key", async () => {
100     : createKey("/users [POST]", { email: newUEmail });
101 
102   const s = store.getState();
103-  asserts.assertEquals(schema.db.cache.selectById(s, { id: expectedKey }), {
104+  asserts.assertEquals(schema.cache.selectById(s, { id: expectedKey }), {
105     "1": { id: "1", name: "test", email: newUEmail },
106   });
107 
108@@ -423,7 +425,7 @@ it(tests, "createApi with custom key but no payload", async () => {
109   const { store, schema } = testStore();
110   const query = createApi();
111   query.use(mdw.api());
112-  query.use(storeMdw.store(schema.db));
113+  query.use(storeMdw.store(schema));
114   query.use(query.routes());
115   query.use(mdw.customKey);
116   query.use(function* fetchApi(ctx, next) {
117@@ -479,7 +481,7 @@ it(tests, "createApi with custom key but no payload", async () => {
118     : createKey("/users [GET]", null);
119 
120   const s = store.getState();
121-  asserts.assertEquals(schema.db.cache.selectById(s, { id: expectedKey }), {
122+  asserts.assertEquals(schema.cache.selectById(s, { id: expectedKey }), {
123     "1": mockUser,
124   });
125 
126@@ -533,14 +535,12 @@ it(tests, "errorHandler", () => {
127 
128   const store = configureStore({
129     initialState: {
130-      ...createQueryState(),
131       users: {},
132     },
133   });
134   store.run(query.bootup);
135   store.dispatch(fetchUsers());
136   expect(store.getState()).toEqual({
137-    ...createQueryState(),
138     users: {},
139   });
140   expect(a).toEqual(2);
141@@ -571,9 +571,7 @@ it(tests, "stub predicate", async () => {
142   ]);
143 
144   const store = configureStore({
145-    initialState: {
146-      ...createQueryState(),
147-    },
148+    initialState: {},
149   });
150   store.run(api.bootup);
151   store.dispatch(fetchUsers());
R fx/parallel.test.ts => test/parallel.test.ts
+2, -4
 1@@ -1,8 +1,6 @@
 2 import { describe, expect, it } from "../test.ts";
 3-import type { Operation, Result } from "../deps.ts";
 4-import { each, Err, Ok, run, sleep, spawn } from "../deps.ts";
 5-
 6-import { parallel } from "./parallel.ts";
 7+import type { Operation, Result } from "../mod.ts";
 8+import { each, Err, Ok, parallel, run, sleep, spawn } from "../mod.ts";
 9 
10 const test = describe("parallel()");
11 
R store/persist.test.ts => test/persist.test.ts
+13, -17
 1@@ -1,27 +1,24 @@
 2-import { Ok, Operation } from "../deps.ts";
 3-import { parallel } from "../fx/mod.ts";
 4 import { asserts, describe, it } from "../test.ts";
 5-import { put, take } from "./fx.ts";
 6-import { configureStore } from "./store.ts";
 7 import {
 8+  configureStore,
 9   createPersistor,
10+  createSchema,
11   PERSIST_LOADER_ID,
12   PersistAdapter,
13   persistStoreMdw,
14-} from "./persist.ts";
15-import { createSchema } from "./schema.ts";
16-import { slice } from "./slice/mod.ts";
17+  slice,
18+} from "../store/mod.ts";
19+import { Ok, Operation, parallel, put, take } from "../mod.ts";
20 
21 const tests = describe("store");
22 
23 it(tests, "can persist to storage adapters", async () => {
24-  const schema = createSchema({
25+  const [schema, initialState] = createSchema({
26     token: slice.str(),
27     loaders: slice.loader(),
28     cache: slice.table({ empty: {} }),
29   });
30-  const db = schema.db;
31-  type State = typeof schema.initialState;
32+  type State = typeof initialState;
33   let ls = "{}";
34   const adapter: PersistAdapter<State> = {
35     getItem: function* (_: string) {
36@@ -38,7 +35,7 @@ it(tests, "can persist to storage adapters", async () => {
37   const persistor = createPersistor<State>({ adapter, allowlist: ["token"] });
38   const mdw = persistStoreMdw(persistor);
39   const store = configureStore({
40-    initialState: schema.initialState,
41+    initialState,
42     middleware: [mdw],
43   });
44 
45@@ -48,7 +45,7 @@ it(tests, "can persist to storage adapters", async () => {
46     const group = yield* parallel([
47       function* (): Operation<void> {
48         const action = yield* take<string>("SET_TOKEN");
49-        yield* schema.update(db.token.set(action.payload));
50+        yield* schema.update(schema.token.set(action.payload));
51       },
52       function* () {
53         yield* put({ type: "SET_TOKEN", payload: "1234" });
54@@ -64,13 +61,12 @@ it(tests, "can persist to storage adapters", async () => {
55 });
56 
57 it(tests, "rehydrates state", async () => {
58-  const schema = createSchema({
59+  const [schema, initialState] = createSchema({
60     token: slice.str(),
61     loaders: slice.loader(),
62     cache: slice.table({ empty: {} }),
63   });
64-  const db = schema.db;
65-  type State = typeof schema.initialState;
66+  type State = typeof initialState;
67   let ls = JSON.stringify({ token: "123" });
68   const adapter: PersistAdapter<State> = {
69     getItem: function* (_: string) {
70@@ -87,13 +83,13 @@ it(tests, "rehydrates state", async () => {
71   const persistor = createPersistor<State>({ adapter, allowlist: ["token"] });
72   const mdw = persistStoreMdw(persistor);
73   const store = configureStore({
74-    initialState: schema.initialState,
75+    initialState,
76     middleware: [mdw],
77   });
78 
79   await store.run(function* (): Operation<void> {
80     yield* persistor.rehydrate();
81-    yield* schema.update(db.loaders.success({ id: PERSIST_LOADER_ID }));
82+    yield* schema.update(schema.loaders.success({ id: PERSIST_LOADER_ID }));
83   });
84 
85   asserts.assertEquals(
R store/put.test.ts => test/put.test.ts
+2, -4
 1@@ -1,8 +1,6 @@
 2 import { describe, expect, it } from "../test.ts";
 3-import { each, sleep, spawn } from "../deps.ts";
 4-
 5-import { ActionContext, put, take } from "./mod.ts";
 6-import { configureStore } from "./store.ts";
 7+import { ActionContext, each, put, sleep, spawn, take } from "../mod.ts";
 8+import { configureStore } from "../store/mod.ts";
 9 
10 const putTests = describe("put()");
11 
R query/react.test.ts => test/react.test.ts
+0, -0
R fx/safe.test.ts => test/safe.test.ts
+1, -1
1@@ -1,5 +1,5 @@
2 import { describe, expect, it } from "../test.ts";
3-import { call, run } from "../deps.ts";
4+import { call, run } from "../mod.ts";
5 
6 const tests = describe("call()");
7 
R store/schema.test.ts => test/schema.test.ts
+13, -18
 1@@ -1,8 +1,5 @@
 2 import { asserts, describe, it } from "../test.ts";
 3-import { select } from "./fx.ts";
 4-import { configureStore } from "./store.ts";
 5-import { slice } from "./slice/mod.ts";
 6-import { createSchema } from "./schema.ts";
 7+import { configureStore, createSchema, select, slice } from "../store/mod.ts";
 8 
 9 const tests = describe("createSchema()");
10 
11@@ -16,7 +13,7 @@ interface UserWithRoles extends User {
12 
13 const emptyUser = { id: "", name: "" };
14 it(tests, "general types and functionality", async () => {
15-  const schema = createSchema({
16+  const [db, initialState] = createSchema({
17     users: slice.table<User>({
18       initialState: { "1": { id: "1", name: "wow" } },
19       empty: emptyUser,
20@@ -28,8 +25,7 @@ it(tests, "general types and functionality", async () => {
21     cache: slice.table({ empty: {} }),
22     loaders: slice.loader(),
23   });
24-  const db = schema.db;
25-  const store = configureStore(schema);
26+  const store = configureStore({ initialState });
27 
28   asserts.assertEquals(store.getState(), {
29     users: { "1": { id: "1", name: "wow" } },
30@@ -40,11 +36,11 @@ it(tests, "general types and functionality", async () => {
31     cache: {},
32     loaders: {},
33   });
34-  const userMap = schema.db.users.selectTable(store.getState());
35+  const userMap = db.users.selectTable(store.getState());
36   asserts.assertEquals(userMap, { "1": { id: "1", name: "wow" } });
37 
38   await store.run(function* () {
39-    yield* schema.update([
40+    yield* db.update([
41       db.users.add({ "2": { id: "2", name: "bob" } }),
42       db.users.patch({ "1": { name: "zzz" } }),
43     ]);
44@@ -55,15 +51,15 @@ it(tests, "general types and functionality", async () => {
45       "2": { id: "2", name: "bob" },
46     });
47 
48-    yield* schema.update(db.counter.increment());
49+    yield* db.update(db.counter.increment());
50     const counter = yield* select(db.counter.select);
51     asserts.assertEquals(counter, 1);
52 
53-    yield* schema.update(db.currentUser.update({ key: "name", value: "vvv" }));
54+    yield* db.update(db.currentUser.update({ key: "name", value: "vvv" }));
55     const curUser = yield* select(db.currentUser.select);
56     asserts.assertEquals(curUser, { id: "", name: "vvv" });
57 
58-    yield* schema.update(db.loaders.start({ id: "fetch-users" }));
59+    yield* db.update(db.loaders.start({ id: "fetch-users" }));
60     const fetchLoader = yield* select(db.loaders.selectById, {
61       id: "fetch-users",
62     });
63@@ -74,25 +70,24 @@ it(tests, "general types and functionality", async () => {
64 });
65 
66 it(tests, "can work with a nested object", async () => {
67-  const schema = createSchema({
68+  const [db, initialState] = createSchema({
69     currentUser: slice.obj<UserWithRoles>({ id: "", name: "", roles: [] }),
70     cache: slice.table({ empty: {} }),
71     loaders: slice.loader(),
72   });
73-  const db = schema.db;
74-  const store = configureStore(schema);
75+  const store = configureStore({ initialState });
76   await store.run(function* () {
77-    yield* schema.update(db.currentUser.update({ key: "name", value: "vvv" }));
78+    yield* db.update(db.currentUser.update({ key: "name", value: "vvv" }));
79     const curUser = yield* select(db.currentUser.select);
80     asserts.assertEquals(curUser, { id: "", name: "vvv", roles: [] });
81 
82-    yield* schema.update(
83+    yield* db.update(
84       db.currentUser.update({ key: "roles", value: ["admin"] }),
85     );
86     const curUser2 = yield* select(db.currentUser.select);
87     asserts.assertEquals(curUser2, { id: "", name: "vvv", roles: ["admin"] });
88 
89-    yield* schema.update(
90+    yield* db.update(
91       db.currentUser.update({ key: "roles", value: ["admin", "users"] }),
92     );
93     const curUser3 = yield* select(db.currentUser.select);
R store/store.test.ts => test/store.test.ts
+8, -5
 1@@ -1,9 +1,12 @@
 2-import { createScope, Operation, Result } from "../deps.ts";
 3-import { parallel } from "../fx/mod.ts";
 4 import { asserts, describe, it } from "../test.ts";
 5-import { StoreContext, StoreUpdateContext } from "./context.ts";
 6-import { put, take, updateStore } from "./fx.ts";
 7-import { configureStore, createStore } from "./store.ts";
 8+import {
 9+  configureStore,
10+  createStore,
11+  StoreContext,
12+  StoreUpdateContext,
13+  updateStore,
14+} from "../store/mod.ts";
15+import { createScope, Operation, parallel, put, Result, take } from "../mod.ts";
16 
17 const tests = describe("store");
18 
R fx/supervisor.test.ts => test/supervisor.test.ts
+11, -3
 1@@ -1,7 +1,15 @@
 2 import { describe, expect, it } from "../test.ts";
 3-import { call, each, Operation, run, spawn } from "../deps.ts";
 4-import { supervise, superviseBackoff } from "./supervisor.ts";
 5-import { LogAction, LogContext } from "../log.ts";
 6+import {
 7+  call,
 8+  each,
 9+  LogAction,
10+  LogContext,
11+  Operation,
12+  run,
13+  spawn,
14+  supervise,
15+  superviseBackoff,
16+} from "../mod.ts";
17 
18 const test = describe("supervise()");
19 
R store/take-helper.test.ts => test/take-helper.test.ts
+3, -5
 1@@ -1,9 +1,7 @@
 2 import { describe, expect, it } from "../test.ts";
 3-import { sleep } from "../deps.ts";
 4-import type { AnyAction } from "../types.ts";
 5-
 6-import { configureStore } from "./mod.ts";
 7-import { take, takeEvery, takeLatest, takeLeading } from "./fx.ts";
 8+import { configureStore } from "../store/mod.ts";
 9+import type { AnyAction } from "../mod.ts";
10+import { sleep, take, takeEvery, takeLatest, takeLeading } from "../mod.ts";
11 
12 const testEvery = describe("takeEvery()");
13 const testLatest = describe("takeLatest()");
R store/take.test.ts => test/take.test.ts
+3, -5
 1@@ -1,9 +1,7 @@
 2 import { describe, expect, it } from "../test.ts";
 3-import { sleep, spawn } from "../deps.ts";
 4-import type { AnyAction } from "../types.ts";
 5-
 6-import { put, take } from "./mod.ts";
 7-import { configureStore } from "./store.ts";
 8+import type { AnyAction } from "../mod.ts";
 9+import { put, sleep, spawn, take } from "../mod.ts";
10+import { configureStore } from "../store/mod.ts";
11 
12 const takeTests = describe("take()");
13 
R query/thunk.test.ts => test/thunk.test.ts
+7, -15
 1@@ -1,12 +1,7 @@
 2-import { assertLike, asserts, describe, it } from "../test.ts";
 3-import { configureStore, put, takeEvery } from "../store/mod.ts";
 4-import { call, sleep as delay } from "../deps.ts";
 5-import type { QueryState } from "../types.ts";
 6-import { createQueryState } from "../action.ts";
 7-import { sleep } from "../test.ts";
 8-import { createThunks } from "./thunk.ts";
 9-import type { Next, ThunkCtx } from "./types.ts";
10-import { updateStore } from "../store/fx.ts";
11+import { assertLike, asserts, describe, it, sleep } from "../test.ts";
12+import { configureStore, updateStore } from "../store/mod.ts";
13+import { call, createThunks, put, sleep as delay, takeEvery } from "../mod.ts";
14+import type { Next, ThunkCtx } from "../mod.ts";
15 
16 // deno-lint-ignore no-explicit-any
17 interface RoboCtx<D = Record<string, unknown>, P = any> extends ThunkCtx<P> {
18@@ -52,7 +47,7 @@ const deserializeTicket = (u: TicketResponse): Ticket => {
19   };
20 };
21 
22-interface TestState extends QueryState {
23+interface TestState {
24   users: { [key: string]: User };
25   tickets: { [key: string]: Ticket };
26 }
27@@ -134,14 +129,13 @@ it(
28     const fetchUsers = api.create(`/users`, { supervisor: takeEvery });
29 
30     const store = configureStore<TestState>({
31-      initialState: { ...createQueryState(), users: {}, tickets: {} },
32+      initialState: { users: {}, tickets: {} },
33     });
34     store.run(api.bootup);
35 
36     store.dispatch(fetchUsers());
37 
38     asserts.assertEquals(store.getState(), {
39-      ...createQueryState(),
40       users: { [mockUser.id]: deserializeUser(mockUser) },
41       tickets: {},
42     });
43@@ -172,13 +166,12 @@ it(
44     });
45 
46     const store = configureStore<TestState>({
47-      initialState: { ...createQueryState(), users: {}, tickets: {} },
48+      initialState: { users: {}, tickets: {} },
49     });
50     store.run(api.bootup);
51 
52     store.dispatch(fetchTickets());
53     asserts.assertEquals(store.getState(), {
54-      ...createQueryState(),
55       users: { [mockUser.id]: deserializeUser(mockUser) },
56       tickets: { [mockTicket.id]: deserializeTicket(mockTicket) },
57     });
58@@ -256,7 +249,6 @@ it(tests, "error inside endpoint mdw", () => {
59 
60   const store = configureStore({
61     initialState: {
62-      ...createQueryState(),
63       users: {},
64     },
65   });
M types.ts
+4, -6
 1@@ -5,11 +5,6 @@ export interface Computation<T = unknown> {
 2   [Symbol.iterator](): Iterator<Instruction, T, any>;
 3 }
 4 
 5-export interface QueryState {
 6-  "@@starfx/loaders": Record<string, LoaderItemState>;
 7-  "@@starfx/data": Record<string, unknown>;
 8-}
 9-
10 export type IdProp = string | number;
11 export type LoadingStatus = "loading" | "success" | "error" | "idle";
12 export interface LoaderItemState<
13@@ -51,7 +46,10 @@ export interface AnyAction {
14   [key: string]: any;
15 }
16 
17-export interface ActionWPayload<P> {
18+export interface Action {
19   type: string;
20+}
21+
22+export interface ActionWithPayload<P> extends Action {
23   payload: P;
24 }