- commit
- 0215e8a
- parent
- 531fbae
- author
- Eric Bower
- date
- 2024-01-18 15:22:43 +0000 UTC
refactor: rm redux (#32)
+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: {
+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;) {
+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-}
+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,
+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";
+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;
+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-});
+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-}
+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";
+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-);
+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-}
+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-}
+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-});
+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-});
+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-}
+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-};
+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-});
+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");
+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 ) {
+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";
+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
+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") {
+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 }
+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
+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(
+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 }