fluorine-orchestra
Version:
A data orchestration layer for Fluorine
257 lines (198 loc) • 7.97 kB
JavaScript
import invariant from 'invariant'
import { Observable } from 'rxjs'
import { Collection } from './Collection'
import { Store } from './Store'
import combineStores from './util/combineStores'
import isDispatcher from 'fluorine-lib/lib/util/isDispatcher'
import {
Iterable,
Set
} from 'immutable'
const resultCache = Symbol('resultCache')
export class Orchestra {
static isOrchestra(obj) {
return typeof obj === 'object' && obj instanceof Orchestra
}
constructor(...stores) {
invariant(
stores.length > 0 && stores.every(Store.isStore),
'Orchestra: constructor expects to receive Stores.')
const _stores = stores
.reduce((acc, store) => {
const { identifier } = store
if (acc[identifier]) {
throw new Error(`Orchestra: The identifier \`${identifier}\` is not unique.`)
}
acc[identifier] = store
return acc
}, {})
this[resultCache] = {}
this.stores = _stores
this.connections = {}
this.externals = {}
this.opts = {}
}
addDebounce(debounceTime) {
invariant(typeof debounceTime === 'number' && debounceTime > 0,
'Orchestra: Expected `debounceTime` to be a number and > 0.')
this.opts.debounceTime = debounceTime
}
addReducer(identifier, reducer) {
invariant(
typeof identifier === 'string',
'Orchestra: `identifier` is expected to be a string.')
invariant(
typeof reducer === 'function',
'Orchestra: `reducer` is expected to be a reducer function.')
const { externals, stores } = this
invariant(!stores.hasOwnProperty(identifier), `Orchesta: The identifier \`${identifier}\` is already taken by a Store.`)
invariant(!externals.hasOwnProperty(identifier), `Orchesta: The identifier \`${identifier}\` is not unique.`)
externals[identifier] = reducer
return this
}
reduce(dispatcher) {
invariant(isDispatcher(dispatcher), 'Orchestra: `dispatcher` is expected to be a Fluorine dispatcher.')
if (this[resultCache][dispatcher.identifier]) {
return this[resultCache][dispatcher.identifier]
}
const { stores, externals, connections } = this
const { debounceTime } = this.opts
const _externals = Object
.keys(externals)
.reduce((acc, key) => {
let external = dispatcher.reduce(externals[key])
if (debounceTime) {
external = external.debounceTime(debounceTime)
}
acc[key] = external
return acc
}, {})
const _stores = {}
const resolveStore = (store, visited = {}) => {
const { identifier } = store
if (visited[identifier]) {
throw new Error(`Orchestra: Failed to resolve circular dependency for identifier \`${identifier}\`.`)
}
visited[identifier] = true
if (_stores[identifier]) {
return _stores[identifier]
}
const reducer = store.getReducer()
const dependencies = store.getDependencies()
const dependencyIdentifiers = Object.keys(dependencies)
const post = store.getPost()
const _dependencies = dependencyIdentifiers.reduce((acc, dependency) => {
let _dependency
if (_externals[dependency]) {
_dependency = _externals[dependency]
} else if (_stores[dependency]) {
_dependency = _stores[dependency]
} else if (stores[dependency]) {
_dependency = resolveStore(stores[dependency], visited)
} else {
throw new Error(`Orchestra: Failed to resolve dependency for identifier \`${dependency}\`.`)
}
acc[dependency] = _dependency
return acc
}, {})
let _store = dispatcher.reduce(reducer)
if (debounceTime) {
_store = _store.debounceTime(debounceTime)
}
// The resulting observable store
let res
if (dependencies.length === 0) {
// Bail early if there are no dependencies for the store
res = _store
} else {
const applyDependency = (state, dependencyState, dependencyIdentifier) => {
const { getter, setter } = dependencies[dependencyIdentifier]
// If getter exists we need to resolve dependencies per ids
if (getter && typeof getter === 'function') {
let missingIds = []
const nextState = state.map(x => {
const ids = getter(x)
let result = undefined
if (typeof ids === 'string') {
result = dependencyState.get(ids)
if (result === undefined) {
missingIds = missingIds.concat(ids)
return x
}
} else if (Iterable.isIterable(ids) || Array.isArray(ids)) {
invariant(typeof ids.forEach === 'function', 'Orchestra: `ids` is expected to have a method `forEach`.')
const store = stores[dependencyIdentifier]
const collection = store ? store.createCollection() : new Collection()
result = collection.withMutations(map => {
ids.forEach(id => {
const item = dependencyState.get(id)
if (item === undefined) {
missingIds.push(id)
}
map.set(id, item)
})
})
}
return setter(x, result)
})
// Report missing items for ids
const dependencyStore = stores[dependencyIdentifier]
if (dependencyStore) {
dependencyStore._missing(new Set(missingIds), identifier)
}
return nextState
}
// If there is no getter we just map over the items with the setter only
return state.map(x => setter(x, dependencyState))
}
let lastDeps = null
res = combineStores({ ..._dependencies, [identifier]: _store })
.scan((pastResult, deps) => {
// Update lastDeps and copy it to _lastDeps
const _lastDeps = lastDeps
lastDeps = deps
const currentResult = deps[identifier]
if (_lastDeps && pastResult && pastResult === currentResult) {
// There can only be one changed dependency, since we're just combining their outputs.
// Therefore we can find the changed one and process it exclusively on the last resolved
// state that we had here.
let result = currentResult
for (const dependencyIdentifier of dependencyIdentifiers) {
const current = deps[dependencyIdentifier]
const last = _lastDeps[dependencyIdentifier]
if (current !== last) {
result = applyDependency(result, current, dependencyIdentifier)
}
}
return result
}
// Fallback to processing everything, since either this is the first iteration, or the original
// store changed.
return dependencyIdentifiers.reduce((acc, dependencyIdentifier) => {
const dependencyState = deps[dependencyIdentifier]
return applyDependency(acc, dependencyState, dependencyIdentifier)
}, currentResult)
}, null)
}
// Apply post-hook, if it's defined
if (post) {
res = res.map(x => x.map(post))
}
res = res.publishReplay(1)
_stores[identifier] = res
connections[identifier] = res.connect()
return res
}
for (const identifier in stores) {
if (stores.hasOwnProperty(identifier)) {
const store = stores[identifier]
_stores[identifier] = resolveStore(store)
}
}
this[resultCache][dispatcher.identifier] = _stores
return _stores
}
}
export default function createOrchestra(...stores) {
return new Orchestra(...stores)
}