Skip to content

Commit

Permalink
test: selector
Browse files Browse the repository at this point in the history
  • Loading branch information
acrazing committed Oct 25, 2024
1 parent a28fe6f commit 10927de
Show file tree
Hide file tree
Showing 13 changed files with 114 additions and 31 deletions.
1 change: 1 addition & 0 deletions packages/amos-boxes/src/recordMapBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface RecordMapBox<RM extends RecordMap<any, any> = RecordMap<any, an
ShapeBox<RM, 'setAll' | 'mergeAll', never, RecordMap<Record<{}>, never>> {
setItem(key: MapKey<RM>, value: MapValue<RM>): Mutation<RM>;
setItem(value: MapValue<RM>): Mutation<RM>;
/** {@link RecordMap.mergeItem} */
mergeItem(props: PartialRequiredProps<MapValue<RM>, RecordMapKeyField<RM>>): Mutation<RM>;
mergeItem(key: MapKey<RM>, props: PartialProps<MapValue<RM>>): Mutation<RM>;
}
Expand Down
30 changes: 22 additions & 8 deletions packages/amos-core/src/box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export interface BoxFactory<B extends Box = Box> extends BoxFactoryStatic<B> {
new (key: string, initialState: BoxState<B>): B;
}

export interface BoxFactoryMutationOptions<B extends Box, A extends any[] = any> {
update: (box: B, state: BoxState<B>, ...args: A) => BoxState<B>;
}

export interface BoxFactorySelectorOptions<S = any, A extends any[] = any, R = any>
extends Omit<Partial<SelectorOptions<A, R>>, 'type' | 'compute'> {
derive?: (state: S, ...args: A) => R;
Expand All @@ -102,7 +106,10 @@ export interface BoxFactoryOptions<TBox extends Box, TParentBox = {}> {
? never
: P
: never]: TBox[P] extends MutationFactory<infer A, BoxState<TBox>>
? null | ((state: BoxState<TBox>, ...args: A) => BoxState<TBox>)
?
| null
| ((state: BoxState<TBox>, ...args: A) => BoxState<TBox>)
| BoxFactoryMutationOptions<TBox, A>
: never;
};
selectors: {
Expand Down Expand Up @@ -152,12 +159,15 @@ function createBoxFactory<B extends Box, SB = {}>(
}
};
for (const k in mutations) {
const fn =
typeof mutations[k] === 'function'
? mutations[k]
: (state: any, ...args: any[]) => state[k](...args);
Object.defineProperty(Box.prototype, k, {
value: function (this: Box, ...args: any[]): Mutation {
value: function (this: B, ...args: any[]): Mutation {
const fn: (state: any, ...args: any[]) => any =
typeof mutations[k] === 'function'
? mutations[k]
: !mutations[k]
? (state: any, ...args: any[]) => state[k](...args)
: (state: any, ...args: any[]) =>
(mutations[k] as BoxFactoryMutationOptions<B>).update(this, state, ...args);
return createAmosObject<Mutation>('mutation', {
type: `${this.key}/${k as string}`,
mutator: (state: any) => fn(state, ...args),
Expand All @@ -178,7 +188,7 @@ function createBoxFactory<B extends Box, SB = {}>(
...resolvedOptions,
id: this.id,
type: `${this.key}/${k as string}`,
compute: (select: Select, ...args) => derive(select(this), ...args),
compute: (select: Select) => derive(select(this), ...args),
args: args,
});
},
Expand All @@ -194,7 +204,11 @@ function createBoxFactory<B extends Box, SB = {}>(
export const Box: BoxFactory = createBoxFactory<Box>({
name: 'Box',
mutations: {
setState: (state, next) => resolveFuncValue(next, state),
setState: {
update: (box, state, ...args) => {
return args.length ? resolveFuncValue(args[0], state) : box.initialState;
},
},
},
selectors: {},
});
Expand Down
20 changes: 14 additions & 6 deletions packages/amos-core/src/selector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@
* @author acrazing <[email protected]>
*/

import { selectUser } from 'amos-testing';
import { double, fourfold, select, selectUser } from 'amos-testing';
import { createAmosObject, is } from 'amos-utils';
import { Selector } from './selector';
import { Selector, SelectorFactory } from './selector';

describe('selector', () => {
it('should create selector factory', () => {
expect(selectUser).toBe(expect.any(Function));
expect(selectUser).toEqual(expect.any(Function));
expect({ ...selectUser }).toEqual(
createAmosObject<SelectorFactory>('selector_factory', {
id: expect.any(String),
}),
);
});
it('should create selector', () => {
expect(selectUser()).toEqual(
const s = double(3);
expect(s).toEqual(
createAmosObject<Selector>('selector', {
compute: expect.any(Function),
type: '',
type: 'amos/double',
equal: is,
args: [],
args: [3],
id: expect.any(String),
}),
);
expect(s.compute(select)).toEqual(6);
expect(fourfold(3).compute(select)).toEqual(12);
});
});
10 changes: 5 additions & 5 deletions packages/amos-core/src/selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export type Compute<A extends any[] = any, R = any> = (select: Select, ...args:

export interface SelectorOptions<A extends any[] = any, R = any> {
type: string;
compute: Compute<A, R>;

/**
* The equal fn, which is used for check the result is updated or not. If the fn
Expand Down Expand Up @@ -50,15 +49,16 @@ export interface SelectorOptions<A extends any[] = any, R = any> {
export interface Selector<A extends any[] = any, R = any>
extends AmosObject<'selector'>,
SelectorOptions<A, R> {
args: A;
args: readonly unknown[];
compute: (select: Select) => R;
}

export interface SelectorFactory<A extends any[] = any, R = any>
extends AmosObject<'selector_factory'> {
(...args: A): Selector<A, R>;
}

export const enhanceSelector = enhancerCollector<[SelectorOptions], SelectorFactory>();
export const enhanceSelector = enhancerCollector<[Compute, SelectorOptions], SelectorFactory>();

export function selector<A extends any[], R>(
compute: Compute<A, R>,
Expand All @@ -67,11 +67,11 @@ export function selector<A extends any[], R>(
const finalOptions = { ...options } as SelectorOptions;
finalOptions.type ??= '';
finalOptions.equal ??= is;
finalOptions.compute = compute;
return enhanceSelector.apply([finalOptions], (options) => {
return enhanceSelector.apply([compute, finalOptions], (compute, options) => {
const factory = createAmosObject<SelectorFactory>('selector_factory', ((...args: A) => {
return createAmosObject<Selector<A, R>>('selector', {
...options,
compute: (select) => compute(select, ...args),
id: factory.id,
args,
});
Expand Down
8 changes: 5 additions & 3 deletions packages/amos-core/src/signal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @author acrazing <[email protected]>
*/

import { LOGOUT } from 'amos-testing';
import { LOGOUT, LogoutEvent, select } from 'amos-testing';
import { isAmosObject } from 'amos-utils';
import { pick } from 'lodash';

Expand All @@ -14,13 +14,15 @@ describe('event', () => {
expect(LOGOUT.dispatch).toBeInstanceOf(Function);
});
it('should create signal', () => {
const s = LOGOUT({ userId: 1, sessionId: 1 });
const e: LogoutEvent = { userId: 1, sessionId: 1 };
const s = LOGOUT(e);
expect(isAmosObject(s, 'signal')).toBe(true);
expect(pick(s, 'args', 'creator', 'factory', 'type')).toEqual({
args: [{ userId: 1, sessionId: 1 }],
args: [e],
creator: expect.any(Function),
factory: LOGOUT,
type: 'session.logout',
});
expect(s.creator(select, ...s.args)).toBe(e);
});
});
4 changes: 2 additions & 2 deletions packages/amos-core/src/signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
createEventCenter,
enhancerCollector,
EventCenter,
identity,
second,
} from 'amos-utils';
import { Dispatch, Select } from './types';

Expand Down Expand Up @@ -64,7 +64,7 @@ export function signal(a: any, b?: any, c?: any): SignalFactory {
const bIsFunc = typeof b === 'function';
const options: SignalOptions = (bIsFunc ? c : b) || {};
options.type = a;
options.creator = (bIsFunc && b) || identity;
options.creator = (bIsFunc && b) || second;
const factory = enhanceSignal.apply([options as SignalOptions], (options) => {
return Object.assign((...args: any[]) => {
return createAmosObject<Signal>('signal', {
Expand Down
48 changes: 46 additions & 2 deletions packages/amos-core/src/store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,20 @@
* @author junbao <[email protected]>
*/

import { loginAsync, LOGOUT, LogoutEvent, sessionIdBox, sessionMapBox } from 'amos-testing';
import {
addTodo,
countBox,
LOGIN,
loginAsync,
loginSync,
LOGOUT,
LogoutEvent,
selectTodoList,
sessionIdBox,
sessionMapBox,
todoMapBox,
} from 'amos-testing';
import { pick } from 'lodash';
import { createStore, Store } from './store';

describe('store', () => {
Expand Down Expand Up @@ -37,6 +50,37 @@ describe('store', () => {
const s4 = store.dispatch(LOGOUT({ userId: 1, sessionId: s3 }));
expect(s4).toEqual<LogoutEvent>({ userId: 1, sessionId: s3 });
state[sessionMapBox.key] = sessionMapBox.initialState;
expect(store.snapshot()).toEqual(state);
expect(pick(store.snapshot(), Object.keys(state))).toMatchObject(state);
});

it('should select base selectable', async () => {
const { select, dispatch } = createStore();
expect(select(sessionMapBox)).toBe(sessionMapBox.initialState);
const id = await dispatch(addTodo({ title: 'Hello', description: 'World' }));
expect(pick(select(todoMapBox.getItem(id)).toJSON(), ['id', 'title', 'description'])).toEqual({
id: id,
title: 'Hello',
description: 'World',
});
expect(select(selectTodoList()).toJSON()).toEqual([id]);
});

it('should dispatch event', () => {
const { select, dispatch, subscribe } = createStore();
const f1 = jest.fn();
const u1 = subscribe(f1);
dispatch(loginSync(1));
expect(f1).toHaveBeenCalledTimes(1);
f1.mockReset();
select(countBox);
expect(f1).toHaveBeenCalledTimes(0);
dispatch(loginSync(1));
dispatch(countBox.setState());
dispatch(LOGIN({ userId: 1, sessionId: 1 }));
expect(f1).toHaveBeenCalledTimes(3);
f1.mockReset();
u1();
dispatch(LOGOUT({ userId: 1, sessionId: 1 }));
expect(f1).toHaveBeenCalledTimes(0);
});
});
2 changes: 1 addition & 1 deletion packages/amos-core/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export function createStore(options: StoreOptions = {}, ...enhancers: StoreEnhan
}
return store.state[selectable.key];
}
return selectable.compute(store.select, ...selectable.args);
return selectable.compute(store.select);
},
};
},
Expand Down
11 changes: 10 additions & 1 deletion packages/amos-shapes/src/RecordMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class RecordMap<R extends Record<any>, KF extends keyof RecordProps<R>> e
override mergeItem(a: any, b?: any) {
const props = b ?? a;
const key = b ? a : a[this.keyField];
props[this.keyField] = key;
return this.setItem(key, this.getItem(key).merge(props));
}

Expand Down Expand Up @@ -69,7 +70,15 @@ export class RecordMap<R extends Record<any>, KF extends keyof RecordProps<R>> e
| ReadonlyArray<PartialRequiredProps<R, KF> | Entry<IDOf<R[KF]>, PartialProps<R>>>,
): this {
if (Array.isArray(items)) {
return super.setAll(items.map((v): any => (Array.isArray(v) ? v : [v[this.keyField], v])));
return super.setAll(
items.map((v): any => {
if (Array.isArray(v)) {
v[1][this.keyField] = v[0];
return v;
}
return [v[this.keyField], v];
}),
);
} else {
return super.setAll(
Object.entries(items).map(([k, v]): any => [k, this.getItem(k as IDOf<R[KF]>).merge(v)]),
Expand Down
7 changes: 5 additions & 2 deletions packages/amos-testing/src/store/misc.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
* keep atomic boxes
*/

import { action } from 'amos-core';
import { countBox } from './misc.boxes';
import { action, selector } from 'amos-core';
import { sleep } from '../utils';
import { countBox } from './misc.boxes';

export const addTwiceAsync = action(async (dispatch, select, value: number) => {
await sleep(1);
dispatch(countBox.add(value));
dispatch(countBox.add(value));
});

export const double = selector((select, v: number) => v * 2);
export const fourfold = selector((select, v: number) => select(double(v)) * 2);
2 changes: 1 addition & 1 deletion packages/amos-testing/src/store/session.boxes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ export class SessionRecord extends Record<SessionModel>({
}

export const sessionMapBox = recordMapBox('sessions', SessionRecord, 'id');
sessionMapBox.subscribe(LOGOUT, (state, data) => state.removeItem(data.userId));
sessionMapBox.subscribe(LOGOUT, (state, data) => state.removeItem(data.sessionId));

export const sessionIdBox = box('sessions.currentId', 0);
1 change: 1 addition & 0 deletions packages/amos-testing/src/store/todo.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export const addTodo = action(
await sleep();
const id = Math.random();
dispatch([todoMapBox.mergeItem(id, input), userTodoListBox.unshiftIn(userId, id)]);
return id;
},
);
1 change: 1 addition & 0 deletions packages/amos-utils/src/equals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const is: (x: any, y: any) => boolean = Object.is || shimObjectIs;
* @param v
*/
export const identity = <T>(v: T) => v;
export const second = <T>(a: any, b: T) => b;
export const notNullable = <T>(v: T): v is Exclude<T, undefined | null> => v != null;
export const isNullable = (v: unknown): v is undefined | null => v == null;
export const isTruly = <T>(v: T): v is Exclude<T, undefined | null | '' | 0 | false> => !!v;
Expand Down

0 comments on commit 10927de

Please sign in to comment.