Skip to content

Commit 053d739

Browse files
committed
feat: use query
1 parent bca5a23 commit 053d739

File tree

9 files changed

+230
-30
lines changed

9 files changed

+230
-30
lines changed

.yarn/install-state.gz

4.83 KB
Binary file not shown.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@rollup/plugin-terser": "^0.4.4",
2020
"@rollup/plugin-typescript": "^12.1.1",
2121
"@testing-library/dom": "^10.4.0",
22+
"@testing-library/jest-dom": "^6.6.2",
2223
"@testing-library/react": "^16.0.1",
2324
"@types/babel__core": "^7",
2425
"@types/fs-extra": "^11.0.4",

packages/amos-core/src/action.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { AmosObject, createAmosObject, enhancerCollector } from 'amos-utils';
7+
import type { Box } from './box';
78
import { SelectorFactory } from './selector';
89
import { CacheOptions, Dispatch, Select } from './types';
910

@@ -31,9 +32,8 @@ export interface ActionOptions<A extends any[] = any, R = any> {
3132

3233
/**
3334
* How to handle conflicted action dispatch.
34-
* - always: will always dispatch.
35+
* - always: will always dispatch. (default)
3536
* - leading: only take first to dispatch.
36-
* The default value is always when no conflictKey is set.
3737
*
3838
* Note: we have no plan to implement `trailing` mode,
3939
* it should be controlled in user space.
@@ -43,9 +43,6 @@ export interface ActionOptions<A extends any[] = any, R = any> {
4343
/**
4444
* Use for checking if the action is equal to another one, if so,
4545
* dispatch will respect {@link conflictPolicy} strategy.
46-
*
47-
* If set this option, conflictPolicy's default value is 'first'.
48-
* This option is required for {@link import('amos-react').useQuery}.
4946
*/
5047
conflictKey?: CacheOptions<A>;
5148
}
@@ -71,8 +68,7 @@ export function action<A extends any[], R>(
7168
actor: Actor<A, R>,
7269
options: Partial<ActionOptions<A, R>> = {},
7370
): ActionFactory<A, R> {
74-
const finalOptions = { type: '', ...options } as ActionOptions;
75-
finalOptions.conflictPolicy ??= options.conflictKey ? 'leading' : 'always';
71+
const finalOptions = { type: '', conflictPolicy: 'always', ...options } as ActionOptions;
7672
return enhanceAction.apply([actor, finalOptions], (actor, options) => {
7773
const factory = createAmosObject<ActionFactory>(
7874
'action_factory',
@@ -99,15 +95,15 @@ export interface SelectableActionOptions<A extends any[] = any, S = any> {
9995
* Use for {@link import('amos-react').useQuery} derive the state even if
10096
* the action is running.
10197
*/
102-
selector: SelectorFactory<A, S>;
98+
selector: SelectorFactory<A, S> | Box<S>;
10399
}
104100

105101
export interface SelectableAction<A extends any[] = any, R = any, S = any>
106102
extends Action<A, R>,
107103
SelectableActionOptions<A, S> {}
108104

109105
export interface ActionFactoryStatic<A extends any[] = any, R = any> {
110-
select<S>(selector: SelectorFactory<A, S>): SelectableActionFactory<A, R, S>;
106+
select<S>(selector: SelectorFactory<A, S> | Box<S>): SelectableActionFactory<A, R, S>;
111107
}
112108

113109
export interface SelectableActionFactory<A extends any[] = any, R = any, S = any>

packages/amos-core/src/enhancers/withCache.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const withCache: () => StoreEnhancer = () => {
6767
stack.push([]);
6868
try {
6969
let v = select(s);
70-
if (cache && isSelectValueEqual(s, cache[1], v)) {
70+
if (cache && s.equal(cache[1], v)) {
7171
v = cache[1];
7272
}
7373
parent?.push([s, v]);

packages/amos-core/src/enhancers/withConcurrent.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const leadingFn = jest.fn(async (dispatch: Dispatch, select: Select, id: number)
2020
});
2121

2222
const leadingAsync = action(leadingFn, {
23+
conflictPolicy: 'leading',
2324
conflictKey: [countBox],
2425
});
2526

packages/amos-react/src/useQuery.spec.ts

+79-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,86 @@
33
* @author junbao <[email protected]>
44
*/
55

6+
import { act } from '@testing-library/react';
7+
import { action, type Dispatch, type Select } from 'amos-core';
8+
import { countBox, expectCalled, sleep } from 'amos-testing';
9+
import { clone } from 'amos-utils';
10+
import { QueryResult, useQuery } from './useQuery';
11+
import { renderDynamicHook } from './useSelector.spec';
12+
13+
const fn = async (dispatch: Dispatch, select: Select, multiply: number) => {
14+
dispatch(countBox.multiply(multiply));
15+
await sleep(10);
16+
dispatch(countBox.multiply(multiply));
17+
return multiply;
18+
};
19+
20+
const simpleFn = jest.fn(fn);
21+
const simple = action(simpleFn);
22+
23+
const stateFn = jest.fn(fn);
24+
const state = action(stateFn).select(countBox);
25+
626
describe('useQuery', () => {
7-
it('should useQuery', () => {
27+
it('should useQuery', async () => {
28+
const a1 = simple(1);
29+
const { result, mockFn, rerender } = renderDynamicHook(
30+
({ multiply }) => useQuery(simple(multiply)),
31+
{ count: 1 },
32+
{ multiply: 1 },
33+
);
34+
expectCalled(mockFn, 1);
35+
expect(result.current).toEqual([
36+
void 0,
37+
clone(new QueryResult(), {
38+
id: a1.id,
39+
_nextId: void 0,
40+
status: 'pending',
41+
promise: expect.any(Promise),
42+
value: void 0,
43+
error: void 0,
44+
}),
45+
]);
46+
await act(async () => {
47+
await result.current[1].promise;
48+
await sleep(1);
49+
});
50+
expectCalled(mockFn, 1);
51+
expectCalled(simpleFn, 1);
52+
rerender({ multiply: 1 });
53+
expectCalled(mockFn, 1);
54+
expect(result.current[0]).toBe(1);
55+
expectCalled(simpleFn, 0);
56+
});
857

58+
it('should useQuery with selector', async () => {
59+
const a1 = state(1);
60+
const { result, mockFn, rerender } = renderDynamicHook(
61+
({ multiply }) => useQuery(state(multiply)),
62+
{ count: 1 },
63+
{ multiply: 2 },
64+
);
65+
expectCalled(mockFn, 1);
66+
expect(result.current).toEqual([
67+
1,
68+
clone(new QueryResult(), {
69+
id: a1.id,
70+
_nextId: void 0,
71+
status: 'pending',
72+
promise: expect.any(Promise),
73+
value: void 0,
74+
error: void 0,
75+
}),
76+
]);
77+
await act(async () => {
78+
await result.current[1].promise;
79+
await sleep(1);
80+
});
81+
expectCalled(mockFn, 1);
82+
expectCalled(stateFn, 1);
83+
rerender({ multiply: 1 });
84+
expectCalled(mockFn, 1);
85+
expect(result.current[0]).toBe(4);
86+
expectCalled(stateFn, 0);
987
});
1088
});

packages/amos-react/src/useQuery.ts

+62-18
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @author junbao <[email protected]>
44
*/
55

6-
import { Action, box, SelectableAction } from 'amos';
6+
import { action, Action, type Box, box, isAmosObject, SelectableAction, selector } from 'amos';
77
import { resolveCacheKey } from 'amos-core';
88
import {
99
clone,
@@ -14,36 +14,36 @@ import {
1414
type WellPartial,
1515
} from 'amos-utils';
1616
import { useEffect } from 'react';
17+
import { useDispatch } from './context';
1718
import { useSelector } from './useSelector';
1819

19-
export interface QueryResultJSON<R> extends Pick<QueryResult<R>, 'status' | 'value' | 'error'> {}
20+
export interface QueryResultJSON<R> extends Pick<QueryResult<R>, 'status' | 'value'> {}
2021

2122
export class QueryResult<R> implements JSONSerializable<QueryResultJSON<R>> {
2223
/** @internal */
2324
id: string | undefined;
24-
status: 'pending' | 'fulfilled' | 'rejected' = 'pending';
25+
/** @internal */
26+
_nextId: string | undefined;
27+
status: 'initial' | 'pending' | 'fulfilled' | 'rejected' = 'initial';
2528
promise: Defer<void> = defer();
26-
value: Awaited<R> | undefined;
27-
error: any;
29+
value: Awaited<R> | undefined = void 0;
30+
error: any = void 0;
2831

2932
toJSON(): QueryResultJSON<R> {
3033
return {
3134
status: this.status,
3235
value: this.value,
33-
error: this.error,
3436
};
3537
}
3638

3739
fromJS(state: JSONState<QueryResultJSON<R>>): this {
38-
return clone(this, {
40+
const p = defer<void>();
41+
const r = clone(this, {
3942
...state,
40-
promise:
41-
state.status === 'pending'
42-
? defer()
43-
: state.status === 'rejected'
44-
? Promise.reject(state.error)
45-
: Promise.resolve(state.value),
43+
promise: p,
4644
} as WellPartial<this>);
45+
r.isRejected() ? p.reject(new Error('Server error')) : r.isFulfilled() ? p.resolve() : void 0;
46+
return r;
4747
}
4848

4949
isPending() {
@@ -80,8 +80,12 @@ export class QueryResultMap
8080
getItem(key: string, id: string): QueryResult<any> {
8181
let item = this.get(key);
8282
if (item) {
83-
if (!item.id) {
84-
item.id = id;
83+
if (item.id === void 0) {
84+
if (item._nextId === void 0) {
85+
item._nextId = id;
86+
} else if (item._nextId !== id) {
87+
item = void 0;
88+
}
8589
} else if (item.id !== id) {
8690
item = void 0;
8791
}
@@ -95,8 +99,21 @@ export class QueryResultMap
9599
}
96100
}
97101

102+
// Immutable state conflicts with react suspense use.
98103
export const queryMapBox = box('amos.queries', () => new QueryResultMap());
99104

105+
export const selectQuery = selector((select, key: string, id: string) => {
106+
return select(queryMapBox).getItem(key, id);
107+
});
108+
109+
export const updateQuery = action((dispatch, select, key: string, query: QueryResult<any>) => {
110+
const map = select(queryMapBox);
111+
if (map.get(key) === query) {
112+
map.set(key, clone(query, {}));
113+
}
114+
dispatch(queryMapBox.setState(map));
115+
});
116+
100117
export interface UseQuery {
101118
<A extends any[] = any, R = any, S = any>(
102119
action: SelectableAction<A, R, S>,
@@ -115,11 +132,38 @@ export interface UseQuery {
115132
*/
116133
export const useQuery: UseQuery = (action: Action | SelectableAction): [any, QueryResult<any>] => {
117134
const select = useSelector();
135+
const dispatch = useDispatch();
118136
const key = resolveCacheKey(select, action, action.conflictKey);
119-
const result = select(queryMapBox).getItem(key, action.id);
120-
useEffect(() => {}, [key]);
137+
const result = select(selectQuery(key, action.id));
138+
const shouldDispatch = result.status !== 'pending' && result.id !== void 0;
139+
if (shouldDispatch) {
140+
result.status = 'pending';
141+
}
142+
useEffect(() => {
143+
if (shouldDispatch) {
144+
(async () => {
145+
try {
146+
result.value = await dispatch(action);
147+
result.status = 'fulfilled';
148+
result.promise.resolve();
149+
} catch (e) {
150+
result.error = e;
151+
result.status = 'rejected';
152+
result.promise.reject(e);
153+
}
154+
dispatch(updateQuery(key, result));
155+
})();
156+
}
157+
}, [key, shouldDispatch]);
121158
if ('selector' in action) {
122-
return [select(action.selector(...action.args)), result];
159+
return [
160+
select(
161+
isAmosObject<Box>(action.selector, 'box')
162+
? action.selector
163+
: action.selector(...action.args),
164+
),
165+
result,
166+
];
123167
} else {
124168
return [result.value, result];
125169
}

packages/amos-react/src/useSelector.spec.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { arrayEqual } from 'amos-utils';
2222
import { Provider } from './context';
2323
import { useSelector } from './useSelector';
2424

25-
function renderDynamicHook<P, T>(
25+
export function renderDynamicHook<P, T>(
2626
fn: (props: P) => T,
2727
preloadedState?: Snapshot,
2828
initialProps?: P,

0 commit comments

Comments
 (0)