Extract store creation
This commit is contained in:
parent
279b859e7b
commit
871018684b
@ -15,95 +15,118 @@ describe('WaitMiddleware', () => {
|
|||||||
applyMiddleware(wm.handle),
|
applyMiddleware(wm.handle),
|
||||||
)
|
)
|
||||||
|
|
||||||
it('waits for certain async actions to be resolved', async () => {
|
describe('wait', () => {
|
||||||
const wm = new WaitMiddleware()
|
it('waits for certain async actions to be resolved', async () => {
|
||||||
const store = getStore(wm)
|
const wm = new WaitMiddleware()
|
||||||
const promise = wm.wait(['B', 'C'])
|
const store = getStore(wm)
|
||||||
store.dispatch({
|
const promise = wm.wait(['B', 'C'])
|
||||||
payload: undefined,
|
store.dispatch({
|
||||||
type: 'A',
|
payload: undefined,
|
||||||
status: 'resolved',
|
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,
|
it('times out when actions do not happen', async () => {
|
||||||
type: 'B',
|
const wm = new WaitMiddleware()
|
||||||
status: 'resolved',
|
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,
|
it('errors out when a promise is rejected', async () => {
|
||||||
type: 'C',
|
const wm = new WaitMiddleware()
|
||||||
status: 'resolved',
|
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 () => {
|
describe('record', () => {
|
||||||
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/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('errors out when a promise is rejected', async () => {
|
it('records pending actions', async () => {
|
||||||
const wm = new WaitMiddleware()
|
const wm = new WaitMiddleware()
|
||||||
const store = getStore(wm)
|
const store = getStore(wm)
|
||||||
const promise = wm.wait(['B', 'C'])
|
const recorder = wm.record()
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
payload: undefined,
|
payload: undefined,
|
||||||
type: 'A',
|
type: 'B',
|
||||||
status: 'resolved',
|
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 {
|
export class WaitMiddleware {
|
||||||
protected notify?: (action: TAsyncAction<any, string>) => void
|
protected notify?: (action: TAsyncAction<any, string>) => void
|
||||||
|
protected recorders: Recorder[] = []
|
||||||
|
|
||||||
handle: Middleware = store => next => (action: AnyAction) => {
|
handle: Middleware = store => next => (action: AnyAction) => {
|
||||||
next(action)
|
next(action)
|
||||||
|
this.recorders.forEach(recorder => recorder.record(action))
|
||||||
if (this.notify && 'status' in action) {
|
if (this.notify && 'status' in action) {
|
||||||
this.notify(action as TAsyncAction<any, string>)
|
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> {
|
async wait(actions: string[], timeout = 10000): Promise<void> {
|
||||||
if (this.notify) {
|
if (this.notify) {
|
||||||
throw new Error('WaitMiddleware.wait - already waiting!')
|
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 {IRenderer} from './IRenderer'
|
||||||
import {Provider} from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
import {Router} from 'react-router-dom'
|
import {Router} from 'react-router-dom'
|
||||||
import {TStoreFactory} from './TStoreFactory'
|
import {Store} from 'redux'
|
||||||
import {createBrowserHistory} from 'history'
|
import {createBrowserHistory} from 'history'
|
||||||
|
|
||||||
export interface IClientRendererParams<
|
export interface IClientRendererParams<A extends Action, D extends IAPIDef> {
|
||||||
State, A extends Action, D extends IAPIDef> {
|
|
||||||
readonly createStore: TStoreFactory<State, A | any>,
|
|
||||||
readonly RootComponent: React.ComponentType<{
|
readonly RootComponent: React.ComponentType<{
|
||||||
config: IClientConfig,
|
config: IClientConfig,
|
||||||
http: IHTTPClient<D>
|
http: IHTTPClient<D>
|
||||||
}>,
|
}>,
|
||||||
readonly target?: HTMLElement
|
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 {
|
implements IRenderer {
|
||||||
constructor(readonly params: IClientRendererParams<State, A, D>) {}
|
constructor(readonly params: IClientRendererParams<A, D>) {}
|
||||||
|
|
||||||
render(
|
render<State>(
|
||||||
url: string,
|
url: string,
|
||||||
|
store: Store<State>,
|
||||||
config = (window as any).__APP_CONFIG__ as IClientConfig,
|
config = (window as any).__APP_CONFIG__ as IClientConfig,
|
||||||
state = (window as any).__PRELOADED_STATE__,
|
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
RootComponent,
|
RootComponent,
|
||||||
createStore,
|
|
||||||
target = document.getElementById('container'),
|
target = document.getElementById('container'),
|
||||||
} = this.params
|
} = this.params
|
||||||
|
|
||||||
@ -41,8 +39,7 @@ export class ClientRenderer<State, A extends Action, D extends IAPIDef>
|
|||||||
basename: config.baseUrl,
|
basename: config.baseUrl,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (state) {
|
if (this.params.hydrate) {
|
||||||
const store = createStore(state)
|
|
||||||
ReactDOM.hydrate(
|
ReactDOM.hydrate(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
@ -52,7 +49,6 @@ export class ClientRenderer<State, A extends Action, D extends IAPIDef>
|
|||||||
target,
|
target,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const store = createStore()
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import {IAPIDef} from '@rondo/common'
|
import {IAPIDef} from '@rondo/common'
|
||||||
import {IClientConfig} from './IClientConfig'
|
import {IClientConfig} from './IClientConfig'
|
||||||
|
import {Store} from 'redux'
|
||||||
|
|
||||||
export interface IRenderer {
|
export interface IRenderer {
|
||||||
render(
|
render<State>(
|
||||||
url: string,
|
url: string,
|
||||||
|
store: Store<State>,
|
||||||
config: IClientConfig,
|
config: IClientConfig,
|
||||||
state?: any,
|
|
||||||
): any
|
): any
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,26 +7,24 @@ import {IRenderer} from './IRenderer'
|
|||||||
import {Provider} from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
import {StaticRouterContext} from 'react-router'
|
import {StaticRouterContext} from 'react-router'
|
||||||
import {StaticRouter} from 'react-router-dom'
|
import {StaticRouter} from 'react-router-dom'
|
||||||
import {TStoreFactory} from './TStoreFactory'
|
import {Store} from 'redux'
|
||||||
import {renderToNodeStream} from 'react-dom/server'
|
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 {
|
implements IRenderer {
|
||||||
constructor(
|
constructor(
|
||||||
readonly createStore: TStoreFactory<State, A | any>,
|
|
||||||
readonly RootComponent: React.ComponentType<{
|
readonly RootComponent: React.ComponentType<{
|
||||||
config: IClientConfig,
|
config: IClientConfig,
|
||||||
http: IHTTPClient<D>
|
http: IHTTPClient<D>
|
||||||
}>,
|
}>,
|
||||||
) {}
|
) {}
|
||||||
render(
|
render<State>(
|
||||||
url: string,
|
url: string,
|
||||||
|
store: Store<State>,
|
||||||
config: IClientConfig,
|
config: IClientConfig,
|
||||||
state?: any,
|
|
||||||
host: string = '',
|
host: string = '',
|
||||||
) {
|
) {
|
||||||
const {RootComponent} = this
|
const {RootComponent} = this
|
||||||
const store = this.createStore(state)
|
|
||||||
const http = new HTTPClient<D>(host + config.baseUrl + '/api')
|
const http = new HTTPClient<D>(host + config.baseUrl + '/api')
|
||||||
|
|
||||||
const context: StaticRouterContext = {}
|
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 './ClientRenderer'
|
||||||
export * from './IClientConfig'
|
export * from './IClientConfig'
|
||||||
export * from './IRenderer'
|
export * from './IRenderer'
|
||||||
export * from './TStoreFactory'
|
|
||||||
export * from './isClientSide'
|
export * from './isClientSide'
|
||||||
|
|||||||
@ -10,8 +10,9 @@ import {
|
|||||||
|
|
||||||
export interface ICreateStoreParams<State, A extends Action> {
|
export interface ICreateStoreParams<State, A extends Action> {
|
||||||
reducer: Reducer<State, A>
|
reducer: Reducer<State, A>
|
||||||
state?: DeepPartial<State>
|
state?: Partial<State>
|
||||||
middleware?: Middleware[]
|
middleware?: Middleware[]
|
||||||
|
extraMiddleware?: Middleware[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,8 +28,10 @@ export function createStore<State, A extends Action>(
|
|||||||
&& typeof window.localStorage.log !== 'undefined',
|
&& typeof window.localStorage.log !== 'undefined',
|
||||||
).handle,
|
).handle,
|
||||||
new PromiseMiddleware().handle,
|
new PromiseMiddleware().handle,
|
||||||
new WaitMiddleware().handle,
|
|
||||||
]
|
]
|
||||||
|
if (params.extraMiddleware) {
|
||||||
|
middleware.push(...params.extraMiddleware)
|
||||||
|
}
|
||||||
return (state?: DeepPartial<State>) => create(
|
return (state?: DeepPartial<State>) => create(
|
||||||
params.reducer,
|
params.reducer,
|
||||||
state,
|
state,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user