feat(nx): infer state type, add optional action type parameter

This commit is contained in:
Manduro 2017-10-18 14:56:34 +02:00 committed by Victor Savkin
parent 7219c8af71
commit 6fc7145602
2 changed files with 71 additions and 62 deletions

View File

@ -92,7 +92,7 @@ describe('DataPersistence', () => {
class TodoEffects { class TodoEffects {
@Effect() @Effect()
loadTodo = this.s.navigation(TodoComponent, { loadTodo = this.s.navigation(TodoComponent, {
run: (a: ActivatedRouteSnapshot, state: TodosState) => { run: (a, state) => {
return { return {
type: 'TODO_LOADED', type: 'TODO_LOADED',
payload: { id: a.params['id'], user: state.user } payload: { id: a.params['id'], user: state.user }
@ -100,7 +100,7 @@ describe('DataPersistence', () => {
}, },
onError: () => null onError: () => null
}); });
constructor(private s: DataPersistence<any>) {} constructor(private s: DataPersistence<TodosState>) {}
} }
beforeEach(() => { beforeEach(() => {
@ -131,7 +131,7 @@ describe('DataPersistence', () => {
class TodoEffects { class TodoEffects {
@Effect() @Effect()
loadTodo = this.s.navigation(TodoComponent, { loadTodo = this.s.navigation(TodoComponent, {
run: (a: ActivatedRouteSnapshot, state: TodosState) => { run: (a, state) => {
if (a.params['id'] === '123') { if (a.params['id'] === '123') {
throw new Error('boom'); throw new Error('boom');
} else { } else {
@ -143,7 +143,7 @@ describe('DataPersistence', () => {
}, },
onError: (a, e) => ({ type: 'ERROR', payload: { error: e } }) onError: (a, e) => ({ type: 'ERROR', payload: { error: e } })
}); });
constructor(private s: DataPersistence<any>) {} constructor(private s: DataPersistence<TodosState>) {}
} }
beforeEach(() => { beforeEach(() => {
@ -183,7 +183,7 @@ describe('DataPersistence', () => {
class TodoEffects { class TodoEffects {
@Effect() @Effect()
loadTodo = this.s.navigation(TodoComponent, { loadTodo = this.s.navigation(TodoComponent, {
run: (a: ActivatedRouteSnapshot, state: TodosState) => { run: (a, state) => {
if (a.params['id'] === '123') { if (a.params['id'] === '123') {
return _throw('boom'); return _throw('boom');
} else { } else {
@ -195,7 +195,7 @@ describe('DataPersistence', () => {
}, },
onError: (a, e) => ({ type: 'ERROR', payload: { error: e } }) onError: (a, e) => ({ type: 'ERROR', payload: { error: e } })
}); });
constructor(private s: DataPersistence<any>) {} constructor(private s: DataPersistence<TodosState>) {}
} }
beforeEach(() => { beforeEach(() => {
@ -236,11 +236,15 @@ describe('DataPersistence', () => {
}); });
describe('no id', () => { describe('no id', () => {
type GetTodos = {
type: 'GET_TODOS';
};
@Injectable() @Injectable()
class TodoEffects { class TodoEffects {
@Effect() @Effect()
loadTodos = this.s.fetch('GET_TODOS', { loadTodos = this.s.fetch<GetTodos>('GET_TODOS', {
run: (a: any, state: TodosState) => { run: (a, state) => {
// we need to introduce the delay to "enable" switchMap // we need to introduce the delay to "enable" switchMap
return of({ return of({
type: 'TODOS', type: 'TODOS',
@ -248,10 +252,10 @@ describe('DataPersistence', () => {
}).delay(1); }).delay(1);
}, },
onError: (a: UpdateTodo, e: any) => null onError: (a, e: any) => null
}); });
constructor(private s: DataPersistence<any>) {} constructor(private s: DataPersistence<TodosState>) {}
} }
function userReducer() { function userReducer() {
@ -281,16 +285,21 @@ describe('DataPersistence', () => {
}); });
describe('id', () => { describe('id', () => {
type GetTodo = {
type: 'GET_TODO';
payload: { id: string };
};
@Injectable() @Injectable()
class TodoEffects { class TodoEffects {
@Effect() @Effect()
loadTodo = this.s.fetch('GET_TODO', { loadTodo = this.s.fetch<GetTodo>('GET_TODO', {
id: (a: any, state: TodosState) => a.payload.id, id: (a, state) => a.payload.id,
run: (a: any, state: TodosState) => of({ type: 'TODO', payload: a.payload }).delay(1), run: (a, state) => of({ type: 'TODO', payload: a.payload }).delay(1),
onError: (a: UpdateTodo, e: any) => null onError: (a, e: any) => null
}); });
constructor(private s: DataPersistence<any>) {} constructor(private s: DataPersistence<TodosState>) {}
} }
function userReducer() { function userReducer() {
@ -333,15 +342,15 @@ describe('DataPersistence', () => {
@Injectable() @Injectable()
class TodoEffects { class TodoEffects {
@Effect() @Effect()
loadTodo = this.s.pessimisticUpdate('UPDATE_TODO', { loadTodo = this.s.pessimisticUpdate<UpdateTodo>('UPDATE_TODO', {
run: (a: UpdateTodo, state: TodosState) => ({ run: (a, state) => ({
type: 'TODO_UPDATED', type: 'TODO_UPDATED',
payload: { user: state.user, newTitle: a.payload.newTitle } payload: { user: state.user, newTitle: a.payload.newTitle }
}), }),
onError: (a: UpdateTodo, e: any) => null onError: (a, e: any) => null
}); });
constructor(private s: DataPersistence<any>) {} constructor(private s: DataPersistence<TodosState>) {}
} }
function userReducer() { function userReducer() {
@ -379,18 +388,18 @@ describe('DataPersistence', () => {
@Injectable() @Injectable()
class TodoEffects { class TodoEffects {
@Effect() @Effect()
loadTodo = this.s.pessimisticUpdate('UPDATE_TODO', { loadTodo = this.s.pessimisticUpdate<UpdateTodo>('UPDATE_TODO', {
run: (a: UpdateTodo, state: TodosState) => { run: (a, state) => {
throw new Error('boom'); throw new Error('boom');
}, },
onError: (a: UpdateTodo, e: any) => ({ onError: (a, e: any) => ({
type: 'ERROR', type: 'ERROR',
payload: { error: e } payload: { error: e }
}) })
}); });
constructor(private s: DataPersistence<any>) {} constructor(private s: DataPersistence<TodosState>) {}
} }
function userReducer() { function userReducer() {
@ -426,18 +435,18 @@ describe('DataPersistence', () => {
@Injectable() @Injectable()
class TodoEffects { class TodoEffects {
@Effect() @Effect()
loadTodo = this.s.pessimisticUpdate('UPDATE_TODO', { loadTodo = this.s.pessimisticUpdate<UpdateTodo>('UPDATE_TODO', {
run: (a: UpdateTodo, state: TodosState) => { run: (a, state) => {
return _throw('boom'); return _throw('boom');
}, },
onError: (a: UpdateTodo, e: any) => ({ onError: (a, e: any) => ({
type: 'ERROR', type: 'ERROR',
payload: { error: e } payload: { error: e }
}) })
}); });
constructor(private s: DataPersistence<any>) {} constructor(private s: DataPersistence<TodosState>) {}
} }
function userReducer() { function userReducer() {
@ -479,18 +488,18 @@ describe('DataPersistence', () => {
@Injectable() @Injectable()
class TodoEffects { class TodoEffects {
@Effect() @Effect()
loadTodo = this.s.optimisticUpdate('UPDATE_TODO', { loadTodo = this.s.optimisticUpdate<UpdateTodo>('UPDATE_TODO', {
run: (a: UpdateTodo, state: TodosState) => { run: (a, state) => {
throw new Error('boom'); throw new Error('boom');
}, },
undoAction: (a: UpdateTodo, e: any) => ({ undoAction: (a, e: any) => ({
type: 'UNDO_UPDATE_TODO', type: 'UNDO_UPDATE_TODO',
payload: a.payload payload: a.payload
}) })
}); });
constructor(private s: DataPersistence<any>) {} constructor(private s: DataPersistence<TodosState>) {}
} }
function userReducer() { function userReducer() {

View File

@ -17,32 +17,32 @@ import { withLatestFrom } from 'rxjs/operator/withLatestFrom';
/** /**
* See {@link DataPersistence.pessimisticUpdate} for more information. * See {@link DataPersistence.pessimisticUpdate} for more information.
*/ */
export interface PessimisticUpdateOpts { export interface PessimisticUpdateOpts<T, A> {
run(a: Action, state?: any): Observable<Action> | Action | void; run(a: A, state?: T): Observable<Action> | Action | void;
onError(a: Action, e: any): Observable<any> | any; onError(a: A, e: any): Observable<any> | any;
} }
/** /**
* See {@link DataPersistence.pessimisticUpdate} for more information. * See {@link DataPersistence.pessimisticUpdate} for more information.
*/ */
export interface OptimisticUpdateOpts { export interface OptimisticUpdateOpts<T, A> {
run(a: Action, state?: any): Observable<any> | any; run(a: A, state?: T): Observable<Action> | Action | void;
undoAction(a: Action, e: any): Observable<Action> | Action; undoAction(a: A, e: any): Observable<Action> | Action;
} }
/** /**
* See {@link DataPersistence.navigation} for more information. * See {@link DataPersistence.navigation} for more information.
*/ */
export interface FetchOpts { export interface FetchOpts<T, A> {
id?(a: Action, state?: any): any; id?(a: A, state?: T): any;
run(a: Action, state?: any): Observable<Action> | Action | void; run(a: A, state?: T): Observable<Action> | Action | void;
onError?(a: Action, e: any): Observable<any> | any; onError?(a: A, e: any): Observable<any> | any;
} }
/** /**
* See {@link DataPersistence.navigation} for more information. * See {@link DataPersistence.navigation} for more information.
*/ */
export interface HandleNavigationOpts { export interface HandleNavigationOpts<T> {
run(a: ActivatedRouteSnapshot, state?: any): Observable<Action> | Action | void; run(a: ActivatedRouteSnapshot, state?: T): Observable<Action> | Action | void;
onError?(a: ActivatedRouteSnapshot, e: any): Observable<any> | any; onError?(a: ActivatedRouteSnapshot, e: any): Observable<any> | any;
} }
@ -69,9 +69,9 @@ export class DataPersistence<T> {
* ```typescript * ```typescript
* @Injectable() * @Injectable()
* class TodoEffects { * class TodoEffects {
* @Effect() updateTodo = this.s.pessimisticUpdate('UPDATE_TODO', { * @Effect() updateTodo = this.s.pessimisticUpdate<UpdateTodo>('UPDATE_TODO', {
* // provides an action and the current state of the store * // provides an action and the current state of the store
* run(a: UpdateTodo, state: TodosState) { * run(a, state) {
* // update the backend first, and then dispatch an action that will * // update the backend first, and then dispatch an action that will
* // update the client side * // update the client side
* return this.backend(state.user, a.payload).map(updated => ({ * return this.backend(state.user, a.payload).map(updated => ({
@ -80,7 +80,7 @@ export class DataPersistence<T> {
* })); * }));
* }, * },
* *
* onError(a: UpdateTodo, e: any) { * onError(a, e: any) {
* // we don't need to undo the changes on the client side. * // we don't need to undo the changes on the client side.
* // we can dispatch an error, or simply log the error here and return `null` * // we can dispatch an error, or simply log the error here and return `null`
* return null; * return null;
@ -91,7 +91,7 @@ export class DataPersistence<T> {
* } * }
* ``` * ```
*/ */
pessimisticUpdate(actionType: string, opts: PessimisticUpdateOpts): Observable<any> { pessimisticUpdate<A = Action>(actionType: string, opts: PessimisticUpdateOpts<T, A>): Observable<any> {
const nav = this.actions.ofType(actionType); const nav = this.actions.ofType(actionType);
const pairs = withLatestFrom.call(nav, this.store); const pairs = withLatestFrom.call(nav, this.store);
return concatMap.call(pairs, this.runWithErrorHandling(opts.run, opts.onError)); return concatMap.call(pairs, this.runWithErrorHandling(opts.run, opts.onError));
@ -114,13 +114,13 @@ export class DataPersistence<T> {
* ```typescript * ```typescript
* @Injectable() * @Injectable()
* class TodoEffects { * class TodoEffects {
* @Effect() updateTodo = this.s.optimisticUpdate('UPDATE_TODO', { * @Effect() updateTodo = this.s.optimisticUpdate<UpdateTodo>('UPDATE_TODO', {
* // provides an action and the current state of the store * // provides an action and the current state of the store
* run: (a: UpdateTodo, state: TodosState) => { * run: (a, state) => {
* return this.backend(state.user, a.payload); * return this.backend(state.user, a.payload);
* }, * },
* *
* undoAction: (a: UpdateTodo, e: any) => { * undoAction: (a, e: any) => {
* // dispatch an undo action to undo the changes in the client state * // dispatch an undo action to undo the changes in the client state
* return ({ * return ({
* type: 'UNDO_UPDATE_TODO', * type: 'UNDO_UPDATE_TODO',
@ -133,7 +133,7 @@ export class DataPersistence<T> {
* } * }
* ``` * ```
*/ */
optimisticUpdate(actionType: string, opts: OptimisticUpdateOpts): Observable<any> { optimisticUpdate<A = Action>(actionType: string, opts: OptimisticUpdateOpts<T, A>): Observable<any> {
const nav = this.actions.ofType(actionType); const nav = this.actions.ofType(actionType);
const pairs = withLatestFrom.call(nav, this.store); const pairs = withLatestFrom.call(nav, this.store);
return concatMap.call(pairs, this.runWithErrorHandling(opts.run, opts.undoAction)); return concatMap.call(pairs, this.runWithErrorHandling(opts.run, opts.undoAction));
@ -153,16 +153,16 @@ export class DataPersistence<T> {
* ```typescript * ```typescript
* @Injectable() * @Injectable()
* class TodoEffects { * class TodoEffects {
* @Effect() loadTodos = this.s.fetch('GET_TODOS', { * @Effect() loadTodos = this.s.fetch<GetTodos>('GET_TODOS', {
* // provides an action and the current state of the store * // provides an action and the current state of the store
* run: (a: GetTodos, state: TodosState) => { * run: (a, state) => {
* return this.backend(state.user, a.payload).map(r => ({ * return this.backend(state.user, a.payload).map(r => ({
* type: 'TODOS', * type: 'TODOS',
* payload: r * payload: r
* }); * });
* }, * },
* *
* onError: (a: GetTodos, e: any) => { * onError: (a, e: any) => {
* // dispatch an undo action to undo the changes in the client state * // dispatch an undo action to undo the changes in the client state
* } * }
* }); * });
@ -178,20 +178,20 @@ export class DataPersistence<T> {
* ```typescript * ```typescript
* @Injectable() * @Injectable()
* class TodoEffects { * class TodoEffects {
* @Effect() loadTodo = this.s.fetch('GET_TODO', { * @Effect() loadTodo = this.s.fetch<GetTodo>('GET_TODO', {
* id: (a: GetTodo, state: TodosState) => { * id: (a, state) => {
* return a.payload.id; * return a.payload.id;
* } * }
* *
* // provides an action and the current state of the store * // provides an action and the current state of the store
* run: (a: GetTodo, state: TodosState) => { * run: (a, state) => {
* return this.backend(state.user, a.payload).map(r => ({ * return this.backend(state.user, a.payload).map(r => ({
* type: 'TODO', * type: 'TODO',
* payload: r * payload: r
* }); * });
* }, * },
* *
* onError: (a: GetTodo, e: any) => { * onError: (a, e: any) => {
* // dispatch an undo action to undo the changes in the client state * // dispatch an undo action to undo the changes in the client state
* return null; * return null;
* } * }
@ -206,7 +206,7 @@ export class DataPersistence<T> {
* In addition, if DataPersistence notices that there are multiple requests for Todo 1 scheduled, * In addition, if DataPersistence notices that there are multiple requests for Todo 1 scheduled,
* it will only run the last one. * it will only run the last one.
*/ */
fetch(actionType: string, opts: FetchOpts): Observable<any> { fetch<A = Action>(actionType: string, opts: FetchOpts<T, A>): Observable<any> {
const nav = this.actions.ofType(actionType); const nav = this.actions.ofType(actionType);
const allPairs = withLatestFrom.call(nav, this.store); const allPairs = withLatestFrom.call(nav, this.store);
@ -237,13 +237,13 @@ export class DataPersistence<T> {
* @Injectable() * @Injectable()
* class TodoEffects { * class TodoEffects {
* @Effect() loadTodo = this.s.navigation(TodoComponent, { * @Effect() loadTodo = this.s.navigation(TodoComponent, {
* run: (a: ActivatedRouteSnapshot, state: TodosState) => { * run: (a, state) => {
* return this.backend.fetchTodo(a.params['id']).map(todo => ({ * return this.backend.fetchTodo(a.params['id']).map(todo => ({
* type: 'TODO_LOADED', * type: 'TODO_LOADED',
* payload: todo * payload: todo
* })); * }));
* }, * },
* onError: (a: ActivatedRouteSnapshot, e: any) => { * onError: (a, e: any) => {
* // we can log and error here and return null * // we can log and error here and return null
* // we can also navigate back * // we can also navigate back
* return null; * return null;
@ -254,7 +254,7 @@ export class DataPersistence<T> {
* ``` * ```
* *
*/ */
navigation(component: Type<any>, opts: HandleNavigationOpts): Observable<any> { navigation(component: Type<any>, opts: HandleNavigationOpts<T>): Observable<any> {
const nav = filter.call( const nav = filter.call(
map.call(this.actions.ofType(ROUTER_NAVIGATION), (a: RouterNavigationAction<RouterStateSnapshot>) => map.call(this.actions.ofType(ROUTER_NAVIGATION), (a: RouterNavigationAction<RouterStateSnapshot>) =>
findSnapshot(component, a.payload.routerState.root) findSnapshot(component, a.payload.routerState.root)