Storing REST response to indexedDB with Cycle.js

211 Views Asked by At

I'm in the middle of learninig Cycle.JS and ran into a challenge. I have a component that will get a result from an HTTP call and I'd like to persist this response in indexDB. However, I feel that the request for persistence is the responsibility of another component.

The questions I have are:

  1. Is this a use case for a custom driver that persists HTTP responses to indexDB?
  2. How does another component access the response stream for a request it did not make?
  3. When I try to select the category from the HTTP source, nothing gets logged to the console. I'm using xstream, so the streams should be hot and I expect debug to output. What's going on here?

Below is my component that makes the HTTP call:

import { Feed } from './feed'

export function RssList ({HTTP, props}, feedAdapter = x => x) {
  const request$ = props.url$
    .map(url => ({
      url: url,
      method: 'GET',
      category: 'rss'
    }))

  const response$ = HTTP
    .select('rss')
    .flatten()
    .map(feedAdapter)

  const vDom$ = response$
    .map(Feed)
    .startWith('')

  return {
    DOM: vDom$,
    HTTP: request$
  }
}

Here is my attempt at accessing the response at the app level:

export function main (sources) {
  const urlSource = url$(sources)
  const rssSink = rss$(sources, urlSource.value)

  const vDom$ = xs.combine(urlSource.DOM, rssSink.DOM)
    .map(([urlInput, rssList]) =>
      <div>
        {urlInput}
        {rssList}
      </div>
    )

  sources.HTTP.select('rss').flatten().debug() // nothing happens here

  return {
    DOM: vDom$,
    HTTP: rssSink.HTTP
  }
}
2

There are 2 best solutions below

0
On BEST ANSWER

Selecting a category in the main (the parent) component is the correct approach, and is supported.

The only reason why sources.HTTP.select('rss').flatten().debug() doesn't log anything is because that's not how debug works. It doesn't "subscribe" to the stream and create side effects. debug is essentially like a map operator that uses an identity function (always takes x as input and outputs x), but with a logging operation as a side effect. So you either need to replace .debug() with .addListener({next: x => console.log(x)}) or use the stream that .debug() outputs and hook it with the operator pipeline that goes to sinks. In other words, debug is an in-between logging side effect, not a destination logging side effect.

0
On

Question #1: Custom HTTP->IDB Driver: It depends on the nature of the project, for a simple example I used a general CycleJS IDB Driver. See example below or codesandbox.io example.

Question #2: Components Sharing Streams: Since components and main share the same source/sink API you can link the output (sink) of one component to the input (source) of another. See example below or codesandbox.io example.

Question #3: debug and Logging: As the authoritative (literally) André Staltz pointed out debug needs to be inserted into a completed stream cycle, I.E., an already subscribed/listened stream.

In your example you can put debug in your RssList component:

const response$ = HTTP
  .select('rss')
  .flatten()
  .map(feedAdapter)
  .debug()

OR add a listener to your main example:

sources.HTTP.select('rss').flatten().debug()
  .addListener({next: x => console.log(x)})

OR, what I like to do, is include a log driver:

run(main, {
    DOM: makeDOMDriver('#app'),
    HTTP: makeHTTPDriver(),
    log: log$ => log$.addListener({next: log => console.log(log)}),
})

Then I'll just duplicate a stream and send it to the log sink:

const url$ = props.url
const http$ = url$.map(url => ({url: url, method: 'GET', category: 'rss'}))
const log$ = url$

return {
  DOM: vdom$,
  HTTP: http$,
  log: log$,
}

Here's some example code for sending HTTP response to IndexedDB storage, using two components that share the data and a general IndexedDB driver:

function main(sources) {
  const header$ = xs.of(div('RSS Feed:'))

  const rssSink = RssList(sources) // input HTTP select and props
                                   // output VDOM and data for IDB storage
  const vDom$ = xs.combine(header$, rssSink.DOM) // build VDOM
    .map(([header, rssList]) => div([header, rssList])
  )
  const idbSink = IdbSink(sources, rssSink.IDB) // output store and put HTTP response

  return {
    DOM: vDom$,
    HTTP: rssSink.HTTP, // send HTTP request
    IDB: idbSink.put, // send response to IDB store
    log: idbSink.get, // get and log data stored in IDB
  }
}

function RssList({ HTTP, props }, feedAdapter = x => x) {
  const request$ = props.url$
    .map(url => ({url: url, method: 'GET', category: 'rss'}))

  const response$ = HTTP.select('rss').flatten().map(feedAdapter)
  const idb$ = response$
  const vDom$ = response$
    .map(Feed)
    .startWith(div('','...loading'))

  return {
    DOM: vDom$,
    HTTP: request$,
    IDB: { response: idb$ },
  }
}
function Feed (feed) {
  return div('> ' + feed)
}

function IdbSink(sources, idb) {
  return {
    get: sources.IDB.store('rss').getAll()
      .map(obj => (obj['0'] && obj['0'].feed) || 'unknown'),
    put: idb.response
      .map(feedinfo => $put('rss', { feed: feedinfo }))
  }
}

run(main, {
  props: () => ({ url$: xs.of('http://lorem-rss.herokuapp.com/feed') }),
  DOM: makeDOMDriver('#root'),
  HTTP: makeHTTPDriver(),
  IDB: makeIdbDriver('rss-db', 1, upgradeDb => {
    upgradeDb.createObjectStore('rss', { keyPath: 'feed' })
  }),
  log: log$ => log$.addListener({next: log => console.log(log)}),
})

This is a contrived example, simply to explore the issues raised. Codesandbox.io example.