From 9f68b2250d283f451bc5a8fb6afd227fbeccd333 Mon Sep 17 00:00:00 2001 From: David Chin Date: Fri, 19 Jan 2018 12:30:46 +1100 Subject: [PATCH 1/3] chore(common): CHECKOUT-2507 Add `.ts` extension to files in `DataStore` module --- .../{combine-reducers.spec.js => combine-reducers.spec.ts} | 0 src/data-store/{combine-reducers.js => combine-reducers.ts} | 0 .../{compose-reducers.spec.js => compose-reducers.spec.ts} | 0 src/data-store/{compose-reducers.js => compose-reducers.ts} | 0 src/data-store/{create-action.spec.js => create-action.spec.ts} | 0 src/data-store/{create-action.js => create-action.ts} | 0 src/data-store/{create-data-store.js => create-data-store.ts} | 0 .../{create-error-action.spec.js => create-error-action.spec.ts} | 0 src/data-store/{create-error-action.js => create-error-action.ts} | 0 src/data-store/{data-store.spec.js => data-store.spec.ts} | 0 src/data-store/{data-store.js => data-store.ts} | 0 src/data-store/{deep-freeze.spec.js => deep-freeze.spec.ts} | 0 src/data-store/{deep-freeze.js => deep-freeze.ts} | 0 src/data-store/{index.js => index.ts} | 0 .../{noop-action-transformer.js => noop-action-transformer.ts} | 0 .../{noop-state-transformer.js => noop-state-transformer.ts} | 0 16 files changed, 0 insertions(+), 0 deletions(-) rename src/data-store/{combine-reducers.spec.js => combine-reducers.spec.ts} (100%) rename src/data-store/{combine-reducers.js => combine-reducers.ts} (100%) rename src/data-store/{compose-reducers.spec.js => compose-reducers.spec.ts} (100%) rename src/data-store/{compose-reducers.js => compose-reducers.ts} (100%) rename src/data-store/{create-action.spec.js => create-action.spec.ts} (100%) rename src/data-store/{create-action.js => create-action.ts} (100%) rename src/data-store/{create-data-store.js => create-data-store.ts} (100%) rename src/data-store/{create-error-action.spec.js => create-error-action.spec.ts} (100%) rename src/data-store/{create-error-action.js => create-error-action.ts} (100%) rename src/data-store/{data-store.spec.js => data-store.spec.ts} (100%) rename src/data-store/{data-store.js => data-store.ts} (100%) rename src/data-store/{deep-freeze.spec.js => deep-freeze.spec.ts} (100%) rename src/data-store/{deep-freeze.js => deep-freeze.ts} (100%) rename src/data-store/{index.js => index.ts} (100%) rename src/data-store/{noop-action-transformer.js => noop-action-transformer.ts} (100%) rename src/data-store/{noop-state-transformer.js => noop-state-transformer.ts} (100%) diff --git a/src/data-store/combine-reducers.spec.js b/src/data-store/combine-reducers.spec.ts similarity index 100% rename from src/data-store/combine-reducers.spec.js rename to src/data-store/combine-reducers.spec.ts diff --git a/src/data-store/combine-reducers.js b/src/data-store/combine-reducers.ts similarity index 100% rename from src/data-store/combine-reducers.js rename to src/data-store/combine-reducers.ts diff --git a/src/data-store/compose-reducers.spec.js b/src/data-store/compose-reducers.spec.ts similarity index 100% rename from src/data-store/compose-reducers.spec.js rename to src/data-store/compose-reducers.spec.ts diff --git a/src/data-store/compose-reducers.js b/src/data-store/compose-reducers.ts similarity index 100% rename from src/data-store/compose-reducers.js rename to src/data-store/compose-reducers.ts diff --git a/src/data-store/create-action.spec.js b/src/data-store/create-action.spec.ts similarity index 100% rename from src/data-store/create-action.spec.js rename to src/data-store/create-action.spec.ts diff --git a/src/data-store/create-action.js b/src/data-store/create-action.ts similarity index 100% rename from src/data-store/create-action.js rename to src/data-store/create-action.ts diff --git a/src/data-store/create-data-store.js b/src/data-store/create-data-store.ts similarity index 100% rename from src/data-store/create-data-store.js rename to src/data-store/create-data-store.ts diff --git a/src/data-store/create-error-action.spec.js b/src/data-store/create-error-action.spec.ts similarity index 100% rename from src/data-store/create-error-action.spec.js rename to src/data-store/create-error-action.spec.ts diff --git a/src/data-store/create-error-action.js b/src/data-store/create-error-action.ts similarity index 100% rename from src/data-store/create-error-action.js rename to src/data-store/create-error-action.ts diff --git a/src/data-store/data-store.spec.js b/src/data-store/data-store.spec.ts similarity index 100% rename from src/data-store/data-store.spec.js rename to src/data-store/data-store.spec.ts diff --git a/src/data-store/data-store.js b/src/data-store/data-store.ts similarity index 100% rename from src/data-store/data-store.js rename to src/data-store/data-store.ts diff --git a/src/data-store/deep-freeze.spec.js b/src/data-store/deep-freeze.spec.ts similarity index 100% rename from src/data-store/deep-freeze.spec.js rename to src/data-store/deep-freeze.spec.ts diff --git a/src/data-store/deep-freeze.js b/src/data-store/deep-freeze.ts similarity index 100% rename from src/data-store/deep-freeze.js rename to src/data-store/deep-freeze.ts diff --git a/src/data-store/index.js b/src/data-store/index.ts similarity index 100% rename from src/data-store/index.js rename to src/data-store/index.ts diff --git a/src/data-store/noop-action-transformer.js b/src/data-store/noop-action-transformer.ts similarity index 100% rename from src/data-store/noop-action-transformer.js rename to src/data-store/noop-action-transformer.ts diff --git a/src/data-store/noop-state-transformer.js b/src/data-store/noop-state-transformer.ts similarity index 100% rename from src/data-store/noop-state-transformer.js rename to src/data-store/noop-state-transformer.ts From 293e5e14eba220d9556e3ea931f2a2406af78bef Mon Sep 17 00:00:00 2001 From: David Chin Date: Thu, 18 Jan 2018 14:33:06 +1100 Subject: [PATCH 2/3] chore(common): CHECKOUT-2507 Add `tslint` to lint TypeScript files We are using the default configuration provided by `tslint`. We have added some overrides in order to match with the configuration we have for JavaScript files (enforced by `eslint`). We will need to revisit the rules some time in the future. --- package.json | 5 ++++- tslint.json | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tslint.json diff --git a/package.json b/package.json index 0b496b348d..cd36d632ed 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "precompile": "rm -rf lib", "compile": "tsc --outDir lib", "compile:watch": "yarn compile -- --watch", - "lint": "eslint src --config eslintrc.json", + "lint": "yarn lint:js && yarn lint:ts", + "lint:js": "eslint src --config eslintrc.json", + "lint:ts": "tslint src/**/*.ts --config tslint.json", "prerelease": "git fetch --tags && yarn validate-dependencies && yarn validate-commits && yarn build", "release": "git add dist lib && standard-version -a", "test": "jest --config jest-config.js", @@ -60,6 +62,7 @@ "standard-version": "^4.2.0", "ts-jest": "^21.2.3", "ts-loader": "^3.2.0", + "tslint": "^5.9.1", "typescript": "^2.6.2", "typescript-eslint-parser": "^9.0.1", "uglifyjs-webpack-plugin": "^1.1.1", diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000000..3822098852 --- /dev/null +++ b/tslint.json @@ -0,0 +1,27 @@ +{ + "extends": "tslint:recommended", + "rules": { + "arrow-parens": false, + "grouped-imports": false, + "interface-name": [true, "never-prefix"], + "max-line-length": false, + "member-access": [true, "no-public"], + "no-empty": false, + "no-shadowed-variable": false, + "object-literal-sort-keys": false, + "ordered-imports": false, + "quotemark": [true, "single"], + "trailing-comma": [ + true, + { + "multiline": { + "arrays": "always", + "functions": "never", + "objects": "always", + "typeLiterals": "always" + } + } + ], + "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"] + } +} From 036a2b86d6929e92b05b950385b62fce31340ab5 Mon Sep 17 00:00:00 2001 From: David Chin Date: Thu, 18 Jan 2018 14:42:49 +1100 Subject: [PATCH 3/3] refactor(common): CHECKOUT-2507 Convert `DataStore` module into TypeScript --- package.json | 2 + src/data-store/action.ts | 6 + src/data-store/action.typedef.js | 7 - src/data-store/combine-reducers.ts | 23 ++-- src/data-store/compose-reducers.spec.ts | 4 +- src/data-store/compose-reducers.ts | 17 +-- src/data-store/create-action.spec.ts | 4 +- src/data-store/create-action.ts | 21 ++- src/data-store/create-data-store.ts | 18 +-- src/data-store/create-error-action.spec.ts | 4 +- src/data-store/create-error-action.ts | 13 +- src/data-store/data-store.spec.ts | 15 ++- src/data-store/data-store.ts | 122 ++++++++---------- src/data-store/deep-freeze.spec.ts | 19 ++- src/data-store/deep-freeze.ts | 24 ++-- src/data-store/dispatchable-data-store.ts | 14 ++ .../dispatchable-data-store.typedef.js | 12 -- src/data-store/noop-action-transformer.ts | 14 +- src/data-store/noop-state-transformer.ts | 11 +- src/data-store/readable-data-store.ts | 15 +++ src/data-store/readable-data-store.typedef.js | 18 --- src/data-store/reducer.ts | 5 + src/data-store/reducer.typedef.js | 6 - tsconfig.json | 6 + yarn.lock | 45 ++++++- 25 files changed, 245 insertions(+), 200 deletions(-) create mode 100644 src/data-store/action.ts delete mode 100644 src/data-store/action.typedef.js create mode 100644 src/data-store/dispatchable-data-store.ts delete mode 100644 src/data-store/dispatchable-data-store.typedef.js create mode 100644 src/data-store/readable-data-store.ts delete mode 100644 src/data-store/readable-data-store.typedef.js create mode 100644 src/data-store/reducer.ts delete mode 100644 src/data-store/reducer.typedef.js diff --git a/package.json b/package.json index cd36d632ed..22afd1649c 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,8 @@ }, "dependencies": { "@bigcommerce/request-sender": "git+ssh://git@github.com/bigcommerce/request-sender-js.git#0.1.0", + "@types/jest": "^21.1.10", + "@types/lodash": "^4.14.92", "bigpay-client": "git+ssh://git@github.com/bigcommerce-labs/bigpay-client-js.git#2.9.1", "form-poster": "git+ssh://git@github.com/bigcommerce-labs/form-poster-js.git#1.1.1", "lodash": "^4.17.4", diff --git a/src/data-store/action.ts b/src/data-store/action.ts new file mode 100644 index 0000000000..3e4abf9632 --- /dev/null +++ b/src/data-store/action.ts @@ -0,0 +1,6 @@ +export default interface Action { + type: string; + error?: boolean; + meta?: TMeta; + payload?: TPayload; +} diff --git a/src/data-store/action.typedef.js b/src/data-store/action.typedef.js deleted file mode 100644 index 21fb0e80f8..0000000000 --- a/src/data-store/action.typedef.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @typedef {Object} Action - * @property {string} type - * @property {Object} payload - * @property {Object} meta - * @property {?boolean} error - */ diff --git a/src/data-store/combine-reducers.ts b/src/data-store/combine-reducers.ts index 692fe73189..a708acdcea 100644 --- a/src/data-store/combine-reducers.ts +++ b/src/data-store/combine-reducers.ts @@ -1,18 +1,23 @@ -/** - * @param {Object} reducers - * @return {Reducer} - */ -export default function combineReducers(reducers) { +import Action from './action'; +import Reducer from './reducer'; + +export default function combineReducers( + reducers: ReducerMap +): Reducer { return (state, action) => - Object.keys(reducers).reduce((result, key) => { - const reducer = reducers[key]; - const currentState = state ? state[key] : undefined; + Object.keys(reducers).reduce((result: TState, key) => { + const reducer = reducers[key as keyof TState]; + const currentState = state ? state[key as keyof TState] : undefined; const newState = reducer(currentState, action); if (currentState === newState && result) { return result; } - return { ...result, [key]: newState }; + return Object.assign({}, result, { [key]: newState }); }, state); } + +export type ReducerMap = { + [Key in keyof TState]: Reducer; +}; diff --git a/src/data-store/compose-reducers.spec.ts b/src/data-store/compose-reducers.spec.ts index fbbe085d31..a908a6143c 100644 --- a/src/data-store/compose-reducers.spec.ts +++ b/src/data-store/compose-reducers.spec.ts @@ -1,7 +1,7 @@ import composeReducers from './compose-reducers'; describe('composeReducers()', () => { - const fooReducer = (state, action) => { + const fooReducer = (state = '', action) => { switch (action.type) { case 'FOO': return 'foo'; @@ -14,7 +14,7 @@ describe('composeReducers()', () => { } }; - const barReducer = (state, action) => { + const barReducer = (state = '', action) => { switch (action.type) { case 'BAR': return 'bar'; diff --git a/src/data-store/compose-reducers.ts b/src/data-store/compose-reducers.ts index d5d1c6a144..1c717edcac 100644 --- a/src/data-store/compose-reducers.ts +++ b/src/data-store/compose-reducers.ts @@ -1,12 +1,13 @@ import { curryRight, flowRight } from 'lodash'; +import Action from './action'; +import Reducer from './reducer'; -/** - * @param {...Reducer} reducers - * @return {Reducer} - */ -export default function composeReducers(...reducers) { +export default function composeReducers( + ...reducers: Array, TAction>> +): Reducer { return (state, action) => - flowRight(...reducers.map(reducer => - curryRight(reducer)(action) - ))(state); + flowRight.apply( + null, + reducers.map(reducer => curryRight(reducer)(action)) + )(state); } diff --git a/src/data-store/create-action.spec.ts b/src/data-store/create-action.spec.ts index e16d2ad017..0a9c726481 100644 --- a/src/data-store/create-action.spec.ts +++ b/src/data-store/create-action.spec.ts @@ -15,7 +15,7 @@ describe('createAction()', () => { expect(action).toEqual({ type: 'ACTION' }); }); - it('throws an error if `type` is not provided', () => { - expect(() => createAction()).toThrow(); + it('throws an error if `type` is not provided or empty', () => { + expect(() => createAction('')).toThrow(); }); }); diff --git a/src/data-store/create-action.ts b/src/data-store/create-action.ts index d9f5fe9bd9..a82793564e 100644 --- a/src/data-store/create-action.ts +++ b/src/data-store/create-action.ts @@ -1,19 +1,14 @@ import { omitBy } from 'lodash'; +import Action from './action'; -/** - * @param {string} type - * @param {Object} [payload] - * @param {Object} [meta] - * @return {Action} - */ -export default function createAction(type, payload, meta) { - if (typeof type !== 'string') { +export default function createAction( + type: string, + payload?: TPayload, + meta?: TMeta +): Action { + if (typeof type !== 'string' || type === '') { throw new Error('`type` must be a string'); } - return omitBy({ - type, - payload, - meta, - }, value => value === undefined); + return { type, ...omitBy({ payload, meta }, value => value === undefined) }; } diff --git a/src/data-store/create-data-store.ts b/src/data-store/create-data-store.ts index 8e65715a21..6d16d73f5a 100644 --- a/src/data-store/create-data-store.ts +++ b/src/data-store/create-data-store.ts @@ -1,13 +1,13 @@ -import combineReducers from './combine-reducers'; -import DataStore from './data-store'; +import Action from './action'; +import combineReducers, { ReducerMap } from './combine-reducers'; +import DataStore, { DataStoreOptions } from './data-store'; +import Reducer from './reducer'; -/** - * @param {Reducer|Object} reducer - * @param {Object} [initialState] - * @param {Object} [options] - * @return {DataStore} - */ -export default function createDataStore(reducer, initialState, options) { +export default function createDataStore( + reducer: Reducer, TAction> | ReducerMap, TAction>, + initialState: Partial, + options: DataStoreOptions +): DataStore { if (typeof reducer === 'function') { return new DataStore(reducer, initialState, options); } diff --git a/src/data-store/create-error-action.spec.ts b/src/data-store/create-error-action.spec.ts index 7ac20ef412..c9515b51aa 100644 --- a/src/data-store/create-error-action.spec.ts +++ b/src/data-store/create-error-action.spec.ts @@ -15,7 +15,7 @@ describe('createErrorAction()', () => { expect(action).toEqual({ type: 'ACTION', error: true }); }); - it('throws an error if `type` is not provided', () => { - expect(() => createErrorAction()).toThrow(); + it('throws an error if `type` is not provided or empty', () => { + expect(() => createErrorAction('')).toThrow(); }); }); diff --git a/src/data-store/create-error-action.ts b/src/data-store/create-error-action.ts index 8c01bcafbf..3f65b366d6 100644 --- a/src/data-store/create-error-action.ts +++ b/src/data-store/create-error-action.ts @@ -1,12 +1,11 @@ +import Action from './action'; import createAction from './create-action'; -/** - * @param {string} type - * @param {Object} [payload] - * @param {Object} [meta] - * @return {Action} - */ -export default function createErrorAction(type, payload, meta) { +export default function createErrorAction( + type: string, + payload?: TPayload, + meta?: TMeta +): Action { return { ...createAction(type, payload, meta), error: true, diff --git a/src/data-store/data-store.spec.ts b/src/data-store/data-store.spec.ts index f6b3249422..37e68c7311 100644 --- a/src/data-store/data-store.spec.ts +++ b/src/data-store/data-store.spec.ts @@ -1,4 +1,5 @@ import { Observable } from 'rxjs'; +import Action from './action'; import DataStore from './data-store'; describe('DataStore', () => { @@ -31,7 +32,7 @@ describe('DataStore', () => { it('dispatches error actions and rejects with payload', async () => { const store = new DataStore(state => state); - const action = { error: true, payload: 'foobar' }; + const action = { type: 'FOOBAR', error: true, payload: 'foobar' }; try { await store.dispatch(action); @@ -52,7 +53,7 @@ describe('DataStore', () => { expect(await store.dispatch(Observable.of( { type: 'APPEND', payload: 'foo' }, { type: 'APPEND', payload: 'bar' }, - { type: 'APPEND', payload: '!!!' }, + { type: 'APPEND', payload: '!!!' } ))).toEqual({ message: 'foobar!!!' }); }); @@ -156,8 +157,8 @@ describe('DataStore', () => { const store = new DataStore(reducer); reducer.mockClear(); - store.dispatch({}); - store.dispatch({ payload: 'foobar' }); + store.dispatch({ type: '' }); + store.dispatch({ type: '', payload: 'foobar' }); expect(reducer).not.toHaveBeenCalled(); }); @@ -167,7 +168,7 @@ describe('DataStore', () => { const store = new DataStore(reducer); reducer.mockClear(); - store.dispatch(Observable.of({}, { payload: 'foobar' })); + store.dispatch(Observable.of({ type: '', payload: 'foo' }, { type: '', payload: 'bar' })); expect(reducer).not.toHaveBeenCalled(); }); @@ -290,7 +291,7 @@ describe('DataStore', () => { default: return state; } - }); + }, { foo: '', bar: '' }); const subscriber = jest.fn(); store.subscribe(subscriber, (state) => state.foo); @@ -323,7 +324,7 @@ describe('DataStore', () => { default: return state; } - }); + }, { foo: '', bar: '', foobar: '' }); const subscriber = jest.fn(); store.subscribe( diff --git a/src/data-store/data-store.ts b/src/data-store/data-store.ts index 6cf2aaf582..7f2f25d4e3 100644 --- a/src/data-store/data-store.ts +++ b/src/data-store/data-store.ts @@ -2,9 +2,13 @@ import { isEqual } from 'lodash'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; +import Action from './action'; import deepFreeze from './deep-freeze'; +import DispatchableDataStore, { DispatchOptions } from './dispatchable-data-store'; import noopActionTransformer from './noop-action-transformer'; import noopStateTransformer from './noop-state-transformer'; +import ReadableDataStore, { Filter, Subscriber, Unsubscriber } from './readable-data-store'; +import Reducer from './reducer'; import 'rxjs/add/observable/merge'; import 'rxjs/add/observable/of'; import 'rxjs/add/observable/throw'; @@ -17,52 +21,45 @@ import 'rxjs/add/operator/map'; import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/scan'; -/** - * @implements {ReadableDataStore} - * @implements {DispatchableDataStore} - */ -export default class DataStore { - /** - * @param {Reducer} reducer - * @param {State} [initialState={}] - * @param {Object} [options={}] - * @param {boolean} [options.shouldWarnMutation=true] - * @param {function(state: State): TransformedState} [options.stateTransformer=noopStateTransformer] - * @param {function(action: Observable>): Observable>} [options.actionTransformer=noopActionTransformer] - * @return {void} - * @template State, TransformedState - */ - constructor(reducer, initialState = {}, options = {}) { +export default class DataStore implements ReadableDataStore, DispatchableDataStore { + private _options: DataStoreOptions; + private _notification$: Subject; + private _dispatchers: { [key: string]: Dispatcher }; + private _dispatchQueue$: Subject>; + private _state$: BehaviorSubject; + + constructor( + reducer: Reducer, TAction>, + initialState: Partial = {}, + options?: DataStoreOptions + ) { this._options = { shouldWarnMutation: true, stateTransformer: noopStateTransformer, actionTransformer: noopActionTransformer, ...options, }; - this._state$ = new BehaviorSubject(initialState); + this._state$ = new BehaviorSubject(this._options.stateTransformer(initialState)); this._notification$ = new Subject(); this._dispatchers = {}; - this._dispatchQueue$ = new Subject() - .mergeMap((dispatcher$) => dispatcher$.concatMap((action$) => action$)) - .filter((action) => action.type); + this._dispatchQueue$ = new Subject(); this._dispatchQueue$ + .mergeMap((dispatcher$) => dispatcher$.concatMap((action$) => action$)) + .filter((action) => !!action.type) .scan((state, action) => reducer(state, action), initialState) .distinctUntilChanged(isEqual) .map((state) => this._options.shouldWarnMutation === false ? state : deepFreeze(state)) .map((state) => this._options.stateTransformer(state)) .subscribe(this._state$); - this.dispatch({ type: 'INIT' }); + this.dispatch({ type: 'INIT' } as TAction); } - /** - * @param {Action|Observable>} action - * @param {Object} [options] - * @return {Promise} - * @template T - */ - dispatch(action, options) { + dispatch( + action: TDispatchAction | Observable, + options?: DispatchOptions + ): Promise { if (action instanceof Observable) { return this._dispatchObservableAction(action, options); } @@ -70,27 +67,19 @@ export default class DataStore { return this._dispatchAction(action); } - /** - * @return {TransformedState} - */ - getState() { + getState(): TTransformedState { return this._state$.getValue(); } - /** - * @return {void} - */ - notifyState() { + notifyState(): void { this._notification$.next(this.getState()); } - /** - * @param {function(state: TransformedState): void} subscriber - * @param {...function(state: TransformedState): any} [filters] - * @return {function(): void} - */ - subscribe(subscriber, ...filters) { - let state$ = this._state$; + subscribe( + subscriber: Subscriber, + ...filters: Array> + ): Unsubscriber { + let state$: Observable = this._state$; if (filters.length > 0) { state$ = state$.distinctUntilChanged((stateA, stateB) => @@ -106,27 +95,23 @@ export default class DataStore { return () => subscriptions.forEach((subscription) => subscription.unsubscribe()); } - /** - * @private - * @param {Action} action - * @return {Promise} - * @template T - */ - _dispatchAction(action) { - return this._dispatchObservableAction(action.error ? Observable.throw(action) : Observable.of(action)); + private _dispatchAction( + action: TDispatchAction + ): Promise { + return this._dispatchObservableAction( + action.error ? + Observable.throw(action) : + Observable.of(action) + ); } - /** - * @private - * @param {Observable>} action$ - * @param {Object} [options] - * @return {Promise} - * @template T - */ - _dispatchObservableAction(action$, options = {}) { + private _dispatchObservableAction( + action$: Observable, + options: DispatchOptions = {} + ): Promise { return new Promise((resolve, reject) => { - let action; - let error; + let action: TDispatchAction; + let error: any; this._getDispatcher(options.queueId).next( this._options.actionTransformer(action$) @@ -153,12 +138,7 @@ export default class DataStore { }); } - /** - * @private - * @param {string} [queueId='default'] - * @return {Subject>} - */ - _getDispatcher(queueId = 'default') { + private _getDispatcher(queueId: string = 'default'): Dispatcher { if (!this._dispatchers[queueId]) { this._dispatchers[queueId] = new Subject(); @@ -168,3 +148,11 @@ export default class DataStore { return this._dispatchers[queueId]; } } + +export interface DataStoreOptions { + shouldWarnMutation?: boolean; + actionTransformer?: (action: Observable) => Observable; + stateTransformer?: (state: Partial) => TTransformedState; +} + +type Dispatcher = Subject>; diff --git a/src/data-store/deep-freeze.spec.ts b/src/data-store/deep-freeze.spec.ts index 0a28f7dc09..197561fe6e 100644 --- a/src/data-store/deep-freeze.spec.ts +++ b/src/data-store/deep-freeze.spec.ts @@ -2,7 +2,8 @@ import deepFreeze from './deep-freeze'; describe('deepFreeze()', () => { it('throws error if mutating object', () => { - const object = deepFreeze({ message: 'Foobar' }); + // Cast as `any` to bypass `Readonly` constraint. + const object = deepFreeze({ message: 'Foobar' }) as any; expect(() => { object.message = 'Hello'; }).toThrow(); expect(() => { object.newMessage = 'Hello'; }).toThrow(); @@ -16,7 +17,8 @@ describe('deepFreeze()', () => { }); it('throws error if mutating array', () => { - const array = deepFreeze(['Foobar']); + // Cast as `any` to bypass `ReadonlyArray` constraint. + const array = deepFreeze(['Foobar']) as any; expect(() => { array[0] = 'Hello'; }).toThrow(); expect(() => { array.push('Hello'); }).toThrow(); @@ -36,4 +38,17 @@ describe('deepFreeze()', () => { expect(() => { object.child.message = 'Hello'; }).toThrow(); expect(() => { collection[0].message = 'Hello'; }).toThrow(); }); + + it('does not freeze primitive values', () => { + const value = 'Foobar'; + + expect(deepFreeze(value)).toBe(value); + }); + + it('does not freeze complex types', () => { + class Foobar {} + const object = new Foobar(); + + expect(deepFreeze(object)).toBe(object); + }); }); diff --git a/src/data-store/deep-freeze.ts b/src/data-store/deep-freeze.ts index 92a43a6250..c5527e18ac 100644 --- a/src/data-store/deep-freeze.ts +++ b/src/data-store/deep-freeze.ts @@ -1,20 +1,20 @@ import { isPlainObject } from 'lodash'; -/** - * @param {any} object - * @return {any} - */ -export default function deepFreeze(object) { +export default function deepFreeze(object: T[]): ReadonlyArray; +export default function deepFreeze(object: T): Readonly; +export default function deepFreeze(object: T): T; +export default function deepFreeze(object: T[] | T): ReadonlyArray | Readonly | T { if (Object.isFrozen(object) || (!Array.isArray(object) && !isPlainObject(object))) { return object; } - return Object.freeze( - Object.getOwnPropertyNames(object) - .reduce((result, key) => { - result[key] = deepFreeze(object[key]); + if (Array.isArray(object)) { + return Object.freeze(object.map((value) => deepFreeze(value))); + } + + return Object.freeze(Object.getOwnPropertyNames(object).reduce((result, key) => { + result[key as keyof T] = deepFreeze(object[key as keyof T]); - return result; - }, Array.isArray(object) ? [] : {}) - ); + return result; + }, {} as T)); } diff --git a/src/data-store/dispatchable-data-store.ts b/src/data-store/dispatchable-data-store.ts new file mode 100644 index 0000000000..31239f5131 --- /dev/null +++ b/src/data-store/dispatchable-data-store.ts @@ -0,0 +1,14 @@ +import { Observable } from 'rxjs/Observable'; +import Action from './action'; +import ReadableDataStore from './readable-data-store'; + +export default interface DispatchableDataStore extends ReadableDataStore { + dispatch: ( + action: TDispatchAction | Observable, + options?: DispatchOptions + ) => Promise; +} + +export interface DispatchOptions { + queueId?: string; +} diff --git a/src/data-store/dispatchable-data-store.typedef.js b/src/data-store/dispatchable-data-store.typedef.js deleted file mode 100644 index 9ad4c92697..0000000000 --- a/src/data-store/dispatchable-data-store.typedef.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @interface DispatchableDataStore - * @template TransformedState - */ - -/** - * @function - * @name DispatchableDataStore#dispatch - * @param {Action} action - * @return {Promise} - * @template T - */ diff --git a/src/data-store/noop-action-transformer.ts b/src/data-store/noop-action-transformer.ts index c2605a68ff..aae2be4017 100644 --- a/src/data-store/noop-action-transformer.ts +++ b/src/data-store/noop-action-transformer.ts @@ -1,8 +1,8 @@ -/** - * @param {Observable>} action - * @return {Observable>} - * @template T - */ -export default function noopActionTransformer(action) { - return action; +import { Observable } from 'rxjs/Observable'; +import Action from './action'; + +export default function noopActionTransformer( + action: Observable +): Observable { + return action as any as Observable; } diff --git a/src/data-store/noop-state-transformer.ts b/src/data-store/noop-state-transformer.ts index 1f1847f8fc..474e12f8aa 100644 --- a/src/data-store/noop-state-transformer.ts +++ b/src/data-store/noop-state-transformer.ts @@ -1,8 +1,5 @@ -/** - * @param {T} state - * @return {T} - * @template T - */ -export default function noopStateTransformer(state) { - return state; +export default function noopStateTransformer( + state: TState +): TTransformedState { + return state as any as TTransformedState; } diff --git a/src/data-store/readable-data-store.ts b/src/data-store/readable-data-store.ts new file mode 100644 index 0000000000..16d585332f --- /dev/null +++ b/src/data-store/readable-data-store.ts @@ -0,0 +1,15 @@ +import Action from './action'; + +export default interface ReadableDataStore { + getState(): TTransformedState; + subscribe( + subscriber: Subscriber, + ...filters: Array> + ): Unsubscriber; +} + +export type Filter = (state: TState) => any; + +export type Subscriber = (state: TState) => void; + +export type Unsubscriber = () => void; diff --git a/src/data-store/readable-data-store.typedef.js b/src/data-store/readable-data-store.typedef.js deleted file mode 100644 index 072523f29b..0000000000 --- a/src/data-store/readable-data-store.typedef.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @interface ReadableDataStore - * @template TransformedState - */ - -/** - * @function - * @name ReadableDataStore#getState - * @returns {TransformedState} - */ - -/** - * @function - * @name ReadableDataStore#subscribe - * @param {function(state: TransformedState): void} subscriber - * @param {...function(state: TransformedState): any} [filters] - * @return {function(): void} - */ diff --git a/src/data-store/reducer.ts b/src/data-store/reducer.ts new file mode 100644 index 0000000000..36f6cfb418 --- /dev/null +++ b/src/data-store/reducer.ts @@ -0,0 +1,5 @@ +import Action from './action'; + +type Reducer = (state: TState, action: TAction) => TState; + +export default Reducer; diff --git a/src/data-store/reducer.typedef.js b/src/data-store/reducer.typedef.js deleted file mode 100644 index 1c260ef0b1..0000000000 --- a/src/data-store/reducer.typedef.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @callback Reducer - * @param {T} state - * @param {Action} action - * @returns {T} - */ diff --git a/tsconfig.json b/tsconfig.json index 96a1c1d240..135cb9a78b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,12 @@ "experimentalDecorators": true, "forceConsistentCasingInFileNames": true, "importHelpers": true, + "lib": [ + "dom", + "dom.iterable", + "es6", + "scripthost", + ], "moduleResolution": "node", "removeComments": true, "skipLibCheck": true, diff --git a/yarn.lock b/yarn.lock index e5b4cab0af..807ba00b50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,6 +11,14 @@ query-string "^5.0.0" tslib "^1.8.0" +"@types/jest@^21.1.10": + version "21.1.10" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-21.1.10.tgz#dcacb5217ddf997a090cc822bba219b4b2fd7984" + +"@types/lodash@^4.14.92": + version "4.14.92" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.92.tgz#6e3cb0b71a1e12180a47a42a744e856c3ae99a57" + JSONStream@^1.0.4: version "1.3.1" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" @@ -294,7 +302,7 @@ aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" -babel-code-frame@^6.26.0: +babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: @@ -626,7 +634,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -builtin-modules@^1.0.0: +builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -806,6 +814,10 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +commander@^2.12.1: + version "2.13.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" + commander@^2.9.0, commander@~2.12.1: version "2.12.2" resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" @@ -3839,7 +3851,7 @@ resolve@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.1.7: +resolve@^1.1.7, resolve@^1.3.2: version "1.5.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" dependencies: @@ -4438,6 +4450,33 @@ tslib@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.0.tgz#dc604ebad64bcbf696d613da6c954aa0e7ea1eb6" +tslib@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.1.tgz#6946af2d1d651a7b1863b531d6e5afa41aa44eac" + +tslint@^5.9.1: + version "5.9.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.9.1.tgz#1255f87a3ff57eb0b0e1f0e610a8b4748046c9ae" + dependencies: + babel-code-frame "^6.22.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^3.2.0" + glob "^7.1.1" + js-yaml "^3.7.0" + minimatch "^3.0.4" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.12.1" + +tsutils@^2.12.1: + version "2.19.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.19.0.tgz#170a267c1df5ae046e902568ca3444a1a4dfcb3a" + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"