Skip to content

Commit

Permalink
feat(store): add observable proposal interop to store
Browse files Browse the repository at this point in the history
- Adds `Symbol.observable` method to the store that returns a generic observable
- Adds tests to ensure interoperability. (rxjs 5 was used for a simple integration test, and
  is a dev only dependency)

closes #1631
  • Loading branch information
benlesh committed Apr 18, 2016
1 parent f02e825 commit 5271b87
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"isparta": "^4.0.0",
"mocha": "^2.2.5",
"rimraf": "^2.3.4",
"rxjs": "^5.0.0-beta.6",
"typescript": "^1.8.0",
"typescript-definition-tester": "0.0.4",
"webpack": "^1.9.6"
Expand Down
24 changes: 23 additions & 1 deletion src/createStore.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import isPlainObject from 'lodash/isPlainObject'
import $$observable from './utils/Symbol_observable'

/**
* These are private action types reserved by Redux.
Expand Down Expand Up @@ -198,6 +199,26 @@ export default function createStore(reducer, initialState, enhancer) {
dispatch({ type: ActionTypes.INIT })
}

/**
* Interop point for observable libraries
* @returns minimal observable of state changes. This was added for Interop
* For more information, see the
* [observable proposal](https://github.com/zenparsing/es-observable)
*/
function observable() {
let outerSubscribe = subscribe
return {
subscribe(observer) {
observer.next(getState());
const unsubscribe = outerSubscribe(() => observer.next(getState()))
return { unsubscribe }
},
[$$observable]() {
return this
}
}
}

// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
Expand All @@ -207,6 +228,7 @@ export default function createStore(reducer, initialState, enhancer) {
dispatch,
subscribe,
getState,
replaceReducer
replaceReducer,
[$$observable]: observable
}
}
18 changes: 18 additions & 0 deletions src/utils/Symbol_observable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
let $$observable

if (typeof Symbol === 'function') {
if (!Symbol.observable) {
if (typeof Symbol.for === 'function') {
$$observable = Symbol.for('observable')
} else {
$$observable = Symbol()
}
Symbol.observable = $$observable
} else {
$$observable = Symbol.observable
}
} else {
$$observable = '@@observable'
}

export default $$observable
62 changes: 62 additions & 0 deletions test/createStore.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import expect from 'expect'
import { createStore, combineReducers } from '../src/index'
import { addTodo, dispatchInMiddle, throwError, unknownAction } from './helpers/actionCreators'
import * as reducers from './helpers/reducers'
import * as Rx from 'rxjs'

describe('createStore', () => {
it('exposes the public API', () => {
Expand Down Expand Up @@ -610,4 +611,65 @@ describe('createStore', () => {
store.subscribe(undefined)
).toThrow()
})

it('should implement Symbol.observable interop point', () => {
function foo(state = 0, action) {
return action.type === 'foo' ? 1 : state
}

function bar(state = 0, action) {
return action.type === 'bar' ? 2 : state
}

const store = createStore(combineReducers({ foo, bar }))

let key
if (typeof Symbol === 'function') {
if (Symbol.observable) {
key = Symbol.observable
} else if (typeof Symbol.for === 'function') {
key = Symbol.for('observable')
}
} else {
key = '@@observable'
}

expect(typeof store[key]).toBe('function')

const obs = store[key]()

expect(typeof obs.subscribe).toBe('function')

const results = []

const sub = obs.subscribe({
next(x) { results.push(x) }
})

expect(typeof sub.unsubscribe).toBe('function')

store.dispatch({ type: 'foo' })

expect(results).toEqual([ { foo: 0, bar: 0 }, { foo: 1, bar: 0 } ])
})

it('should pass an integration test with a Symbol.observable interop consumer', () => {
function foo(state = 0, action) {
return action.type === 'foo' ? 1 : state
}

function bar(state = 0, action) {
return action.type === 'bar' ? 2 : state
}

const store = createStore(combineReducers({ foo, bar }))
const observable = Rx.Observable.from(store)
const results = []

observable.subscribe(x => results.push(x))

store.dispatch({ type: 'foo' })

expect(results).toEqual([ { foo: 0, bar: 0 }, { foo: 1, bar: 0 } ])
})
})

0 comments on commit 5271b87

Please sign in to comment.