-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(middleware): Rewrote library to include testing utilities and to…
… dynamically run effects
- Loading branch information
1 parent
b2c1a04
commit 489a896
Showing
12 changed files
with
627 additions
and
120 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
# SagaRunner | ||
The `SagaRunner` service allows you to run, pause, and stop saga effects dynamically: | ||
|
||
```ts | ||
const authEffect = createSaga(...); | ||
|
||
@Component({ | ||
selector: 'profile-route', | ||
template: '...' | ||
}) | ||
class ProfileRoute{ | ||
constructor(private runner: SagaRunner){ } | ||
|
||
routerOnActivate(){ | ||
this.runner.run(incrementEffect); | ||
} | ||
|
||
routerOnDeactivate(){ | ||
this.runner.stop(incrementEffect); | ||
} | ||
} | ||
``` | ||
|
||
## Important Note About Injection | ||
Sagas that are started during bootstrap use the root injector. However, a dynamically started saga may want to use a different injector to resolve the saga effect. This is especially the case when you are working on code splitting. | ||
|
||
The `SagaRunner` service is designed to work in tandem with Angular 2's hierarchical injector. To use a different injector, simply re-provide the `SagaRunner` service in your component's providers array: | ||
|
||
```ts | ||
@Component({ | ||
selector: 'admin-route', | ||
template: '...', | ||
providers: [ SagaRunner ] | ||
}) | ||
class AdminRoute{ | ||
constructor(runner: SagaRunner){ | ||
// runner uses this component's injector to resolve | ||
// any saga effects, storing the resolved effect | ||
// and subscription with the root SagaRunner | ||
} | ||
} | ||
``` | ||
|
||
## SagaRunner API | ||
|
||
### Methods | ||
#### run(saga) | ||
Resolves and starts a new saga, or resumes a previously paused saga. | ||
|
||
__Params__ | ||
* `saga` __SagaProvider__ Saga to begin running | ||
|
||
```ts | ||
sagaRunner.run(saga); | ||
``` | ||
|
||
#### stop(saga) | ||
Stops a saga from running and discards the resolved saga. | ||
|
||
__Params__ | ||
* `saga` __SagaProvider__ Saga to stop | ||
|
||
```ts | ||
sagaRunner.stop(saga) | ||
``` | ||
|
||
#### pause(saga) | ||
Pauses a saga but retains the resolved saga. In _most_ cases you should `stop` sagas. Only pause a saga if you are certain you will be restarting it. | ||
|
||
__Params__ | ||
* `saga` __SagaProvider__ Saga to pause | ||
|
||
```ts | ||
sagaRunner.pause(saga) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# Testing Sagas | ||
store-saga ships with a testing helper called `SagaTester` that extends `SagaRunner`. It exposes some helpful methods to interact with a saga effect and make assertions about the effect's resultant stream. | ||
|
||
Extending from our `authEffect` example written in the _Getting Started_ guide, let's write a test that ensures the effect is responding correctly to dispatched actions: | ||
|
||
_Note: for this example I am using respond-ng, a simple Http mocking library I wrote to make unit testing Http calls easier_ | ||
|
||
```ts | ||
import {Injector} from 'angular2/core'; | ||
import RESPOND_PROVIDERS, {Respond} from 'respond-ng'; | ||
import {SagaTester} from 'store-saga/testing'; | ||
|
||
import {authEffect} from './auth-effect'; | ||
|
||
describe('Authentication Effect', () => { | ||
let sagaTester: SagaTester; | ||
let respond: Respond; | ||
|
||
beforeEach(() => { | ||
const injector = Injector.resolveAndCreate([ | ||
SagaTester, | ||
RESPOND_PROVIDERS | ||
]); | ||
|
||
sagaTester = injector.get(SagaTester); | ||
respond = injector.get(Respond); | ||
|
||
sagaTester.run(authEffect); | ||
}); | ||
|
||
it('should auth a user when an auth action is dispatched', () => { | ||
const payload = { username: 'Mike', password: 'test' }; | ||
respond.ok().when.post('/auth', payload) | ||
|
||
sagaTester.sendAction({ type: 'AUTH', payload }); | ||
|
||
expect(sagaTester.last).toEqual({ type: 'AUTH_SUCCESS' }); | ||
}); | ||
}); | ||
``` | ||
|
||
## SagaTester API | ||
### Parameters | ||
#### output | ||
Output stream of all running sagas. | ||
|
||
_Type_ __BehaviorSubject__ | ||
|
||
```ts | ||
sagaTester.output.subscribe(action => { }); | ||
``` | ||
|
||
#### last | ||
Most recent action dispatched from a saga | ||
|
||
_Type_ __Action__ | ||
|
||
```ts | ||
expect(sagaTester.last).toEqual({ type: 'INCREMENT' }); | ||
``` | ||
|
||
### Methods | ||
#### sendAction(action) | ||
Sends an action and an empty state object to all running sagas. | ||
|
||
__Params__ | ||
* `action` __Action__ Action object to dispatch to sagas | ||
|
||
```ts | ||
sagaTester.sendAction({ type: 'INCREMENT' }); | ||
``` | ||
|
||
#### sendState(state) | ||
Sends state and an empty action object to all running sagas. | ||
|
||
__Params__ | ||
* `state` __any__ State object to dispatch to sagas | ||
|
||
```ts | ||
sagaTester.sendState({ counter: 3 }); | ||
``` | ||
|
||
#### send(state, action) | ||
Sends both state and action objects to all running sagas. | ||
|
||
__Params__ | ||
* `state` __any__ State object to dispatch to sagas | ||
* `action` __Action__ Action object to dispatch to sagas | ||
|
||
```ts | ||
sagaTester.send({ counter: 3 }, { type: 'INCREMENT' }); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# Utilities API | ||
|
||
#### createSaga(factory: SagaFactory, dependencies: any[]) | ||
Creates a saga in the context of the injector. | ||
|
||
__Params__ | ||
* `factory` __SagaFactory__ Factory function called in the context of the injector. Must return a function that implements the `Saga` interface. | ||
* `dependencies` __any[]__ Array of dependencies the factory function needs | ||
|
||
_Returns_ `Provider` | ||
|
||
```ts | ||
const authEffect = createSaga(function(http: Http) { | ||
return saga$ => saga$ | ||
.filter(saga => saga.action.type === 'AUTH') | ||
.map(saga => saga.action.payload) | ||
.flatMap(payload => { | ||
return http.post('/auth', JSON.stringify(payload)) | ||
.map(res => { | ||
return { | ||
type: 'AUTH_SUCESS', | ||
payload: res.json() | ||
} | ||
}) | ||
.catch(error => Observable.of({ | ||
type: 'AUTH_FAILED', | ||
payload: error.json() | ||
})); | ||
}); | ||
}, [ Http ]); | ||
``` | ||
|
||
#### installSagaMiddleware(...sagas: Provider[]) | ||
Installs the saga middleware and initializes it to immediately begin running the provided sagas. | ||
|
||
__Params__ | ||
* `...sagas` __Provider[]__ Sagas you want to begin running immediately. | ||
|
||
_Returns_ `Provider[]` | ||
|
||
```ts | ||
boostrap(App, [ | ||
provideStore(reducer), | ||
installSagaMiddleware(authEffect, signoutEffect) | ||
]); | ||
``` | ||
|
||
#### whenAction(actionType: string) | ||
Filters a stream of `SagaIteration`s to only include iterations with an action of the provided type. | ||
|
||
__Params__ | ||
* `actionType` __string__ Action type to filter for | ||
|
||
_Returns_ `(iteration: SagaIteration) => boolean` | ||
|
||
```ts | ||
return saga$ => saga$ | ||
.filter(whenAction('AUTH')) | ||
``` | ||
|
||
#### toPayload | ||
Function you can pass in to map a saga iteration to the payload of that iteration's action | ||
|
||
```ts | ||
return saga$ => saga$ | ||
.map(toPayload) | ||
.do(payload => { ... }); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,3 @@ | ||
import 'rxjs/add/operator/map'; | ||
import 'rxjs/add/operator/withLatestFrom'; | ||
import 'rxjs/add/observable/merge'; | ||
import {Observable} from 'rxjs/Observable'; | ||
import {Subject} from 'rxjs/Subject'; | ||
import {OpaqueToken, provide, Provider} from 'angular2/core'; | ||
import {Action, Store, Dispatcher, POST_MIDDLEWARE} from '@ngrx/store' | ||
|
||
export const SAGA_FUNCTIONS = new OpaqueToken('store-saga/saga-functions'); | ||
|
||
export interface Saga<State>{ | ||
(iterable: Observable<{ state: State, action: Action }>): Observable<any>; | ||
} | ||
|
||
export function useSaga(saga: Saga<any>){ | ||
return provide(SAGA_FUNCTIONS, { useValue: saga, multi: true }); | ||
} | ||
|
||
export function useSagaFactory(useFactory: (...deps: any[]) => Saga<any>, deps: any[]){ | ||
return provide(SAGA_FUNCTIONS, { deps, useFactory, multi: true }); | ||
} | ||
|
||
export default provide(POST_MIDDLEWARE, { | ||
multi: true, | ||
deps: [ Dispatcher, SAGA_FUNCTIONS ], | ||
useFactory(dispatcher: Dispatcher<any>, sagas: Saga<any>[]){ | ||
return function(state$: Observable<any>){ | ||
const iterable$ = new Subject(); | ||
const resolvedSagas = sagas.map(saga => saga(iterable$)); | ||
|
||
Observable.merge(...resolvedSagas).subscribe(dispatcher); | ||
|
||
return state$ | ||
.withLatestFrom(dispatcher) | ||
.map(([ state, action ]) => { | ||
iterable$.next({ state, action }); | ||
|
||
return state; | ||
}); | ||
} | ||
} | ||
}); | ||
export * from './interfaces'; | ||
export * from './runner'; | ||
export * from './util'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import {Observable} from 'rxjs/Observable'; | ||
import {Action} from '@ngrx/store'; | ||
|
||
export interface SagaIteration<State>{ | ||
state: State; | ||
action: Action; | ||
} | ||
|
||
export interface Saga<State>{ | ||
(iterable: Observable<SagaIteration<State>>): Observable<any>; | ||
} | ||
|
||
export interface SagaFactory<State>{ | ||
(...deps: any[]): Saga<State> | ||
} |
Oops, something went wrong.