Extract store creation
This commit is contained in:
parent
279b859e7b
commit
871018684b
@ -15,95 +15,118 @@ describe('WaitMiddleware', () => {
|
||||
applyMiddleware(wm.handle),
|
||||
)
|
||||
|
||||
it('waits for certain async actions to be resolved', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B', 'C'])
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'A',
|
||||
status: 'resolved',
|
||||
describe('wait', () => {
|
||||
it('waits for certain async actions to be resolved', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B', 'C'])
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'A',
|
||||
status: 'resolved',
|
||||
})
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
status: 'resolved',
|
||||
})
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'C',
|
||||
status: 'resolved',
|
||||
})
|
||||
await promise
|
||||
expect(store.getState().slice(1)).toEqual(['A', 'B', 'C'])
|
||||
})
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
status: 'resolved',
|
||||
|
||||
it('times out when actions do not happen', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B', 'C'], 5)
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'A',
|
||||
status: 'resolved',
|
||||
})
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
status: 'resolved',
|
||||
})
|
||||
const error = await getError(promise)
|
||||
expect(error.message).toMatch(/timed/)
|
||||
})
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'C',
|
||||
status: 'resolved',
|
||||
|
||||
it('errors out when a promise is rejected', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B', 'C'])
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'A',
|
||||
status: 'resolved',
|
||||
})
|
||||
store.dispatch({
|
||||
payload: new Error('test'),
|
||||
type: 'B',
|
||||
status: 'rejected',
|
||||
})
|
||||
const error = await getError(promise)
|
||||
expect(error.message).toMatch(/test/)
|
||||
})
|
||||
|
||||
it('errors out when wait is called twice', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B'])
|
||||
const error = await getError(wm.wait(['B']))
|
||||
expect(error.message).toMatch(/already waiting/)
|
||||
store.dispatch({
|
||||
payload: new Error('test'),
|
||||
type: 'B',
|
||||
status: 'resolved',
|
||||
})
|
||||
await promise
|
||||
})
|
||||
|
||||
it('does nothing when pending is dispatched', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B'], 1)
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
status: 'pending',
|
||||
})
|
||||
const error = await getError(promise)
|
||||
expect(error.message).toMatch(/timed/)
|
||||
})
|
||||
|
||||
it('resolved immediately when no actions are defined', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
await wm.wait([])
|
||||
})
|
||||
await promise
|
||||
expect(store.getState().slice(1)).toEqual(['A', 'B', 'C'])
|
||||
})
|
||||
|
||||
it('times out when actions do not happen', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B', 'C'], 5)
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'A',
|
||||
status: 'resolved',
|
||||
})
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
status: 'resolved',
|
||||
})
|
||||
const error = await getError(promise)
|
||||
expect(error.message).toMatch(/timed/)
|
||||
})
|
||||
describe('record', () => {
|
||||
|
||||
it('errors out when a promise is rejected', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B', 'C'])
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'A',
|
||||
status: 'resolved',
|
||||
it('records pending actions', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const recorder = wm.record()
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
status: 'pending',
|
||||
})
|
||||
const promise = wm.waitForRecorded(recorder)
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
status: 'resolved',
|
||||
})
|
||||
await promise
|
||||
})
|
||||
store.dispatch({
|
||||
payload: new Error('test'),
|
||||
type: 'B',
|
||||
status: 'rejected',
|
||||
})
|
||||
const error = await getError(promise)
|
||||
expect(error.message).toMatch(/test/)
|
||||
})
|
||||
|
||||
it('errors out when wait is called twice', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B'])
|
||||
const error = await getError(wm.wait(['B']))
|
||||
expect(error.message).toMatch(/already waiting/)
|
||||
store.dispatch({
|
||||
payload: new Error('test'),
|
||||
type: 'B',
|
||||
status: 'resolved',
|
||||
})
|
||||
await promise
|
||||
})
|
||||
|
||||
it('does nothing when pending is dispatched', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B'], 1)
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
status: 'pending',
|
||||
})
|
||||
const error = await getError(promise)
|
||||
expect(error.message).toMatch(/timed/)
|
||||
})
|
||||
|
||||
it('resolved immediately when no actions are defined', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
await wm.wait([])
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -3,14 +3,46 @@ import {AnyAction, Middleware} from 'redux'
|
||||
|
||||
export class WaitMiddleware {
|
||||
protected notify?: (action: TAsyncAction<any, string>) => void
|
||||
protected recorders: Recorder[] = []
|
||||
|
||||
handle: Middleware = store => next => (action: AnyAction) => {
|
||||
next(action)
|
||||
this.recorders.forEach(recorder => recorder.record(action))
|
||||
if (this.notify && 'status' in action) {
|
||||
this.notify(action as TAsyncAction<any, string>)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts recording pending actions and returns the recorder.
|
||||
*/
|
||||
record(): Recorder {
|
||||
const recorder = new Recorder()
|
||||
this.recorders.push(recorder)
|
||||
return recorder
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops recording pending actions
|
||||
*/
|
||||
stopRecording(recorder: Recorder): void {
|
||||
const index = this.recorders.indexOf(recorder)
|
||||
this.recorders.splice(index, index + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops recording, Waits for recorded pending actions to be resolved or
|
||||
* rejected. Throws an error if any actions is rejected.
|
||||
*/
|
||||
async waitForRecorded(recorder: Recorder, timeout?: number): Promise<void> {
|
||||
this.stopRecording(recorder)
|
||||
await this.wait(recorder.getActionTypes(), timeout)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for actions to be resolved or rejected. Times out after 10 seconds
|
||||
* by default.
|
||||
*/
|
||||
async wait(actions: string[], timeout = 10000): Promise<void> {
|
||||
if (this.notify) {
|
||||
throw new Error('WaitMiddleware.wait - already waiting!')
|
||||
@ -59,3 +91,17 @@ export class WaitMiddleware {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Recorder {
|
||||
protected actionTypes: string[] = []
|
||||
|
||||
getActionTypes(): string[] {
|
||||
return this.actionTypes.slice()
|
||||
}
|
||||
|
||||
record(action: AnyAction) {
|
||||
if ('status' in action && action.status === 'pending') {
|
||||
this.actionTypes.push(action.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,31 +7,29 @@ import {IHTTPClient, HTTPClient} from '../http'
|
||||
import {IRenderer} from './IRenderer'
|
||||
import {Provider} from 'react-redux'
|
||||
import {Router} from 'react-router-dom'
|
||||
import {TStoreFactory} from './TStoreFactory'
|
||||
import {Store} from 'redux'
|
||||
import {createBrowserHistory} from 'history'
|
||||
|
||||
export interface IClientRendererParams<
|
||||
State, A extends Action, D extends IAPIDef> {
|
||||
readonly createStore: TStoreFactory<State, A | any>,
|
||||
export interface IClientRendererParams<A extends Action, D extends IAPIDef> {
|
||||
readonly RootComponent: React.ComponentType<{
|
||||
config: IClientConfig,
|
||||
http: IHTTPClient<D>
|
||||
}>,
|
||||
readonly target?: HTMLElement
|
||||
readonly hydrate: boolean // TODO make this better
|
||||
}
|
||||
|
||||
export class ClientRenderer<State, A extends Action, D extends IAPIDef>
|
||||
export class ClientRenderer<A extends Action, D extends IAPIDef>
|
||||
implements IRenderer {
|
||||
constructor(readonly params: IClientRendererParams<State, A, D>) {}
|
||||
constructor(readonly params: IClientRendererParams<A, D>) {}
|
||||
|
||||
render(
|
||||
render<State>(
|
||||
url: string,
|
||||
store: Store<State>,
|
||||
config = (window as any).__APP_CONFIG__ as IClientConfig,
|
||||
state = (window as any).__PRELOADED_STATE__,
|
||||
) {
|
||||
const {
|
||||
RootComponent,
|
||||
createStore,
|
||||
target = document.getElementById('container'),
|
||||
} = this.params
|
||||
|
||||
@ -41,8 +39,7 @@ export class ClientRenderer<State, A extends Action, D extends IAPIDef>
|
||||
basename: config.baseUrl,
|
||||
})
|
||||
|
||||
if (state) {
|
||||
const store = createStore(state)
|
||||
if (this.params.hydrate) {
|
||||
ReactDOM.hydrate(
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
@ -52,7 +49,6 @@ export class ClientRenderer<State, A extends Action, D extends IAPIDef>
|
||||
target,
|
||||
)
|
||||
} else {
|
||||
const store = createStore()
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import {IAPIDef} from '@rondo/common'
|
||||
import {IClientConfig} from './IClientConfig'
|
||||
import {Store} from 'redux'
|
||||
|
||||
export interface IRenderer {
|
||||
render(
|
||||
render<State>(
|
||||
url: string,
|
||||
store: Store<State>,
|
||||
config: IClientConfig,
|
||||
state?: any,
|
||||
): any
|
||||
}
|
||||
|
||||
@ -7,26 +7,24 @@ import {IRenderer} from './IRenderer'
|
||||
import {Provider} from 'react-redux'
|
||||
import {StaticRouterContext} from 'react-router'
|
||||
import {StaticRouter} from 'react-router-dom'
|
||||
import {TStoreFactory} from './TStoreFactory'
|
||||
import {Store} from 'redux'
|
||||
import {renderToNodeStream} from 'react-dom/server'
|
||||
|
||||
export class ServerRenderer<State, A extends Action, D extends IAPIDef>
|
||||
export class ServerRenderer<A extends Action, D extends IAPIDef>
|
||||
implements IRenderer {
|
||||
constructor(
|
||||
readonly createStore: TStoreFactory<State, A | any>,
|
||||
readonly RootComponent: React.ComponentType<{
|
||||
config: IClientConfig,
|
||||
http: IHTTPClient<D>
|
||||
}>,
|
||||
) {}
|
||||
render(
|
||||
render<State>(
|
||||
url: string,
|
||||
store: Store<State>,
|
||||
config: IClientConfig,
|
||||
state?: any,
|
||||
host: string = '',
|
||||
) {
|
||||
const {RootComponent} = this
|
||||
const store = this.createStore(state)
|
||||
const http = new HTTPClient<D>(host + config.baseUrl + '/api')
|
||||
|
||||
const context: StaticRouterContext = {}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import {Action, Store} from 'redux'
|
||||
|
||||
// TODO maybe Store should also be typed
|
||||
export type TStoreFactory<State, A extends Action> =
|
||||
(state?: State) => Store<State, A | any>
|
||||
@ -1,5 +1,4 @@
|
||||
export * from './ClientRenderer'
|
||||
export * from './IClientConfig'
|
||||
export * from './IRenderer'
|
||||
export * from './TStoreFactory'
|
||||
export * from './isClientSide'
|
||||
|
||||
@ -10,8 +10,9 @@ import {
|
||||
|
||||
export interface ICreateStoreParams<State, A extends Action> {
|
||||
reducer: Reducer<State, A>
|
||||
state?: DeepPartial<State>
|
||||
state?: Partial<State>
|
||||
middleware?: Middleware[]
|
||||
extraMiddleware?: Middleware[]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -27,8 +28,10 @@ export function createStore<State, A extends Action>(
|
||||
&& typeof window.localStorage.log !== 'undefined',
|
||||
).handle,
|
||||
new PromiseMiddleware().handle,
|
||||
new WaitMiddleware().handle,
|
||||
]
|
||||
if (params.extraMiddleware) {
|
||||
middleware.push(...params.extraMiddleware)
|
||||
}
|
||||
return (state?: DeepPartial<State>) => create(
|
||||
params.reducer,
|
||||
state,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user