A Cycle.js Driver for Netflix Falcor data platform. The model uses HttpDataSource with fixed settings for now: model.json route with 60 seconds timeout.
npm install --save cycle-falcor
- method (String) - required - model method (get, getValue, set, setValue, call)
- path (Array) - required - array representing model's path (for example ['items', 'latest'])
- resKey (String) - key with which the response will be wrapped
- invalidatePath (Array) - array with model path to invalidate upon received HttpDataSource response
- eager (Boolean) - by default, response$ observables are lazy - setting this option to true would make them eager with replay. Example use case: the request$ is a sequence of merged observables where some of those observables emit request object with path property that must contain response values generated by one or more other request$ observables.
A simple example of fetching and displaying data from ['items', 'latest'] path that gives a JSON graph response in the following format:
{
"json": {
"items": {
"$__path": ["items"],
"latest": {
"$__path": ["items", "latest"],
"0": {
"name": "Item 1",
"description": "Description of item 1",
"price": 450,
"$__path": ["item", "latest", 0]
},
"1": {
"name": "Item 2",
"description": "Description of item 2",
"price": 220,
"$__path": ["item", "latest", 1]
}
}
}
}
}
import Cycle from '@cycle/core';
import {Observable} from 'rx';
import {makeDOMDriver, div} from '@cycle/dom';
import {makeFalcorDriver} from 'cycle-falcor';
function getItems(res) {
const items = res.json.items.latest;
return Object.keys(items)
.filter(key => key !== '$__path')
.map(key => items[key]);
}
function main(sources) {
const request$ = Observable.just({
method: 'get',
path: ['items', 'latest']
});
const state$ = sources.Falcor
.mergeAll()
.map(res => getItems(res));
const vtree$ = state$.map(items => {
return div('.items', [
items.map(item => {
return div('.row', [
div('.column', item.name),
div('.column', item.description),
div('.column', item.price)
]);
})
]);
});
return {
DOM: vtree$,
Falcor: request$
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#app'),
Falcor: makeFalcorDriver()
});
More complex example where the request objects of two request observables are dependent on responses generated by other request observables. To be more precise, lengthRequest$ depends on the item emitted by intent$ (search query), and listRequest$ depends on search query and the actual length value - response generated by lengthRequest$ request observable. This is where the eager request option comes in handy, as multiple observables get subscribed to each response observable.
import Cycle from '@cycle/core';
import {Observable} from 'rx';
import {makeDOMDriver, div, input} from '@cycle/dom';
import {makeFalcorDriver} from 'cycle-falcor';
function makeRequest(intent$, Falcor) {
const makeLengthRequest = searchQuery => {
return Observable.just({
method: 'get',
path: ['items', 'length', searchQuery],
resKey: 'lengthRes',
eager: true
});
};
const makeListRequest = (searchQuery, lengthRes) => {
if ((searchQuery || searchQuery === '') && lengthRes) {
let length = lengthRes.json.items.length[searchQuery];
let range = { from: 0, to: length };
return Observable.just({
method: 'get',
path: ['items', 'list', searchQuery, range],
resKey: 'listRes',
eager: true
});
}
return Observable.empty();
};
const lengthRequest$ = Observable.merge(Falcor.mergeAll(), intent$)
.filter(item => typeof item === 'string')
.flatMapLatest(searchQuery => makeLengthRequest(searchQuery));
const listRequest$ = Observable.merge(Falcor.mergeAll(), intent$)
.filter(item => typeof item === 'string' || item.hasOwnProperty('lengthRes'))
.scan((acc, item) => {
if (item.lengthRes) {
acc.lengthRes = item.lengthRes;
} else {
acc.searchQuery = item;
}
return acc;
}, {})
.flatMapLatest(item => makeListRequest(item.searchQuery, item.lengthRes));
return Observable.merge(lengthRequest$, listRequest$);
}
function getItems(listRes, searchQuery) {
const items = listRes.json.items.list[searchQuery];
return Object.keys(items)
.filter(key => key !== '$__path')
.map(key => items[key]);
}
function main(sources) {
const intent$ = sources.DOM.select('.search')
.events('input')
.debounce(200)
.map(ev => ev.target.value)
.startWith('')
.replay(null, 1)
.refCount();
const request$ = makeRequest(intent$, sources.Falcor);
const state$ = Observable.merge(sources.Falcor.mergeAll(), intent$)
.scan((acc, item) => {
if (item.listRes) {
acc.items = getItems(item.listRes, acc.searchQuery);
}
if (typeof item === 'string') {
acc.searchQuery = item;
}
return acc;
}, { items: [], searchQuery: '' })
.map(data => data.items);
const vtree$ = state$.map(items => {
return div('.container', [
div('.search-box', [
input('.search', {
type: 'text'
})
]),
div('.items', [
items.map(item => {
return div('.row', [
div('.column', item.name),
div('.column', item.description),
div('.column', item.price)
]);
})
])
]);
});
return {
DOM: vtree$,
Falcor: request$
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#app'),
Falcor: makeFalcorDriver()
});