Skip to content

Commit ea1fb02

Browse files
committed
feat(Feature): load a feature's dependencies when it is loaded
1 parent f641e66 commit ea1fb02

File tree

4 files changed

+124
-15
lines changed

4 files changed

+124
-15
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ initialization in the background.
8888

8989
### Feature API
9090

91-
The following optionkal properties on features are handled by `redux-features`. However, you may add any other
91+
The following optional properties on features are handled by `redux-features`. However, you may add any other
9292
properties you want.
9393
* `reducer: (state, action) => state`: a reducer to apply to the top-level redux state for each action
9494
* `middleware: store => next => action => any`: middleware to apply for each action
@@ -97,6 +97,8 @@ properties you want.
9797
return a promise that will resolve to the full feature after loading. The full feature **will replace
9898
the current feature in the redux state**, so initial properties of the feature will be blown away unless you merge
9999
them into the full feature yourself.
100+
* `dependencies: Array<string>`: an array of feature ids to load when this feature is loaded. Circular feature
101+
dependencies are not supported, and the behavior is undefined.
100102

101103
## Quick start
102104

src/index.js.flow

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export type FeatureStates = {[featureId: string]: FeatureState}
4242
export type Feature<S, A> = {
4343
init?: (store: MiddlewareAPI<S, A>) => any,
4444
load?: (store: MiddlewareAPI<S, A>) => Promise<Feature<S, A>>,
45+
dependencies?: Array<string>,
4546
middleware?: Middleware<S, A>,
4647
reducer?: Reducer<S, A>,
4748
}

src/loadFeatureMiddleware.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,20 @@ export default function loadFeatureMiddleware<S, A: {type: $Subtype<string>}>(
4141
return Promise.reject(error)
4242
}
4343

44-
return (
45-
feature.load(store)
46-
.catch((error: Error) => {
47-
store.dispatch(setFeatureState(id, error)) // https://github.com/facebook/flow/issues/2993
48-
throw error
49-
})
50-
.then((feature: Feature<S, A>): Feature<S, A> => {
51-
store.dispatch(installFeature(id, feature)) // https://github.com/facebook/flow/issues/2993
52-
return feature
53-
})
54-
)
44+
const featurePromise = feature.load(store)
45+
const {dependencies} = feature
46+
const promises = Array.isArray(dependencies)
47+
? [...dependencies.map(id => store.dispatch(loadFeature(id))), featurePromise]
48+
: [featurePromise]
49+
50+
return Promise.all(promises).then((features: Array<Feature<S, A>>): Feature<S, A> => {
51+
const feature = features[features.length - 1]
52+
store.dispatch(installFeature(id, feature))
53+
return feature
54+
}).catch((error: Error) => {
55+
store.dispatch(setFeatureState(id, error))
56+
throw error
57+
})
5558
}
5659
return Promise.reject(new Error('missing feature for id: ', +id))
5760
},

test/loadFeatureMiddlewareTest.js

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import featuresReducer from '../src/featuresReducer'
66
import featureStatesReducer from '../src/featureStatesReducer'
77
import loadFeatureMiddleware from '../src/loadFeatureMiddleware'
88
import {addFeature, loadFeature, setFeatureState, installFeature, loadInitialFeatures} from '../src/actions'
9-
import {expect} from 'chai'
9+
import {expect, assert} from 'chai'
1010
import sinon from 'sinon'
1111

1212
describe('loadFeatureMiddleware', () => {
@@ -16,6 +16,17 @@ describe('loadFeatureMiddleware', () => {
1616
return createStore(reducer, initialState, applyMiddleware(loadFeatureMiddleware(config)))
1717
}
1818

19+
function createFullStore(initialState, config) {
20+
return createStore(
21+
combineReducers({
22+
featureStates: featureStatesReducer(config),
23+
features: featuresReducer(config),
24+
}),
25+
initialState,
26+
applyMiddleware(loadFeatureMiddleware(config))
27+
)
28+
}
29+
1930
beforeEach(() => reducer.reset())
2031

2132
function tests(createTestStore) {
@@ -264,8 +275,100 @@ describe('loadFeatureMiddleware', () => {
264275
})
265276
})
266277
})
267-
it("returns a promise that rejects when any feature fails to load", () => {
268-
278+
it("returns a promise that rejects when any feature fails to load", async () => {
279+
const error = new Error("test!")
280+
const store = createStore(combineReducers({
281+
featureStates: featureStatesReducer(),
282+
features: featuresReducer(),
283+
}), {
284+
featureStates: {
285+
f1: 'LOADED',
286+
f2: 'LOADED',
287+
f3: 'NOT_LOADED',
288+
},
289+
features: {
290+
f1: {
291+
load: () => Promise.reject(error)
292+
},
293+
f2: {
294+
load: () => new Promise(resolve => setTimeout(() => resolve({b: 2}), 100))
295+
},
296+
f3: {},
297+
}
298+
}, applyMiddleware(loadFeatureMiddleware()))
299+
try {
300+
await store.dispatch(loadInitialFeatures())
301+
assert.fail('loadInitialFeatures should have rejected')
302+
} catch (e) {
303+
expect(e).to.equal(error)
304+
}
305+
expect(store.getState().featureStates).to.deep.equal({
306+
f1: error,
307+
f2: 'LOADING',
308+
f3: 'NOT_LOADED',
309+
})
310+
})
311+
})
312+
describe('on features with dependencies', () => {
313+
it("loads dependencies", async() => {
314+
const loadedDependency = {something: 'cool'}
315+
const loadedFeature = {hello: 'world'}
316+
const store = createFullStore({
317+
featureStates: {
318+
f1: 'NOT_LOADED',
319+
f2: 'NOT_LOADED',
320+
},
321+
features: {
322+
f1: {
323+
load: (store) => Promise.resolve(loadedDependency)
324+
},
325+
f2: {
326+
dependencies: ['f1'],
327+
load: (store) => Promise.resolve(loadedFeature)
328+
}
329+
}
330+
})
331+
const result = await store.dispatch(loadFeature('f2'))
332+
expect(result).to.equal(loadedFeature)
333+
expect(store.getState()).to.deep.equal({
334+
featureStates: {
335+
f1: 'LOADED',
336+
f2: 'LOADED',
337+
},
338+
features: {
339+
f1: loadedDependency,
340+
f2: loadedFeature,
341+
}
342+
})
343+
})
344+
it("rejects if any dependencies fail to load", async () => {
345+
const loadedFeature = {hello: 'world'}
346+
const error = new Error("test!")
347+
const store = createFullStore({
348+
featureStates: {
349+
f1: 'NOT_LOADED',
350+
f2: 'NOT_LOADED',
351+
},
352+
features: {
353+
f1: {
354+
load: (store) => Promise.reject(error)
355+
},
356+
f2: {
357+
dependencies: ['f1'],
358+
load: (store) => Promise.resolve(loadedFeature)
359+
}
360+
}
361+
})
362+
try {
363+
await store.dispatch(loadFeature('f2'))
364+
assert.fail('loadFeature should have rejected')
365+
} catch (e) {
366+
expect(e).to.equal(error)
367+
}
368+
expect(store.getState().featureStates).to.deep.equal({
369+
f1: error,
370+
f2: error,
371+
})
269372
})
270373
})
271374
}

0 commit comments

Comments
 (0)