Choo: dynamic subscriptions (adding & removing websocket channels)

Created on 5 Aug 2016  路  3Comments  路  Source: choojs/choo

Hello! I'm working on using choo for a simple go (the game) web client and running into either a limitation of my understanding or a limitation of the framework.

Each game is played via a named websocket channel that both players must subscribe to in order to receive each other's moves.

From what I've read it sounds like subscriptions are defined prior to app.start() and can't be modified thereafter. So what if I need to subscribe (or unsubscribe) to a data source after a route change, or an event/action/effect/etc? As far as I can tell it's not currently possible to use choo's subscriptions concept to accomplish something like that.

I looked through the choo-firebase example which unfortunately doesn't quite capture this issue as it's only subscribing to a single channel (src/models/quotes.js#L91).

I could just use effects and handle the websocket connection outside of the model altogether but I'm hoping I'm just looking at it the wrong way and some wise choo user can point me in the right direction.

Most helpful comment

Heya!

So a way I like to think about subscriptions (and effects) is just as the
normalized interface around APIs. Internally they can do pretty much whatever,
it's only the API that is locked down, in order to make it easier to reason
about data flow.

I think it makes sense to start / stop subs using effects.

So say you want to multiplex multiple subscriptions over a single websocket
handler, and close the socket if no active listeners exist, you could do
something like this:

// lib/socket.js
module.exports = socket

function socket (uri) {
  const listeners = {}
  var socket = null
  var count = 0

  return function listen (event, cb) {
    if (!count) createSocket()
    count++
    listeners[event] = cb

    return function stopListening () {
      delete listeners[event]
      count--
      if (!count) socket.close()
    }
  }

  function createSocket () {
    socket = new document.WebSocket(uri)
    socket.onmessage = function (e) {
      const name = e.name
      if (listeners[name]) listeners[name](e)
    }
  }
}
// index.js
const choo = require('choo')
const app = choo()

const Socket = require('./lib/socket')
const socket = Socket('ws://localhost:8081')

app.model(require('./models/foo')(socket))
app.model(require('./models/bar')(socket))

const tree = app.start()
document.body.appendChild(tree)
// models/foo.js
module.exports = fooModel

function fooModel (socket) {
  var _listener = null
  var _socket = null

  return {
    namespace: 'foo',
    subscriptions: {
      websocket: function (send, done) {
        _listener = function (e) {
          // call send() here with some data
        }
        send('foo:start', (err) {
          if (err) return done(err)
        })
      }
    },
    effects: {
      stop: function (data, state, send, done) {
        if (_socket) _socket()
        _socket = null
      },
      start: function (data, state, send, done) {
        if (_socket) return
        _socket = socket('bar_event', _listener)
      }
    }
  }
}
// models/bar.js
module.exports = fooModel

function fooModel (socket) {
  var _listener = null
  var _socket = null

  return {
    namespace: 'bar',
    subscriptions: {
      websocket: function (send, done) {
        _listener = function (e) {
          // call send() here with some data
        }
        send('bar:start', (err) {
          if (err) return done(err)
        })
      }
    },
    effects: {
      stop: function (data, state, send, done) {
        if (_socket) _socket()
        _socket = null
      },
      start: function (data, state, send, done) {
        if (_socket) return
        _socket = socket('foo_event', _listener)
      }
    }
  }
}

I'm quite sure the API could be more polished, removing the need to juggle
references inside the models, but I hope this gets the idea across. Cheers!

All 3 comments

Heya!

So a way I like to think about subscriptions (and effects) is just as the
normalized interface around APIs. Internally they can do pretty much whatever,
it's only the API that is locked down, in order to make it easier to reason
about data flow.

I think it makes sense to start / stop subs using effects.

So say you want to multiplex multiple subscriptions over a single websocket
handler, and close the socket if no active listeners exist, you could do
something like this:

// lib/socket.js
module.exports = socket

function socket (uri) {
  const listeners = {}
  var socket = null
  var count = 0

  return function listen (event, cb) {
    if (!count) createSocket()
    count++
    listeners[event] = cb

    return function stopListening () {
      delete listeners[event]
      count--
      if (!count) socket.close()
    }
  }

  function createSocket () {
    socket = new document.WebSocket(uri)
    socket.onmessage = function (e) {
      const name = e.name
      if (listeners[name]) listeners[name](e)
    }
  }
}
// index.js
const choo = require('choo')
const app = choo()

const Socket = require('./lib/socket')
const socket = Socket('ws://localhost:8081')

app.model(require('./models/foo')(socket))
app.model(require('./models/bar')(socket))

const tree = app.start()
document.body.appendChild(tree)
// models/foo.js
module.exports = fooModel

function fooModel (socket) {
  var _listener = null
  var _socket = null

  return {
    namespace: 'foo',
    subscriptions: {
      websocket: function (send, done) {
        _listener = function (e) {
          // call send() here with some data
        }
        send('foo:start', (err) {
          if (err) return done(err)
        })
      }
    },
    effects: {
      stop: function (data, state, send, done) {
        if (_socket) _socket()
        _socket = null
      },
      start: function (data, state, send, done) {
        if (_socket) return
        _socket = socket('bar_event', _listener)
      }
    }
  }
}
// models/bar.js
module.exports = fooModel

function fooModel (socket) {
  var _listener = null
  var _socket = null

  return {
    namespace: 'bar',
    subscriptions: {
      websocket: function (send, done) {
        _listener = function (e) {
          // call send() here with some data
        }
        send('bar:start', (err) {
          if (err) return done(err)
        })
      }
    },
    effects: {
      stop: function (data, state, send, done) {
        if (_socket) _socket()
        _socket = null
      },
      start: function (data, state, send, done) {
        if (_socket) return
        _socket = socket('foo_event', _listener)
      }
    }
  }
}

I'm quite sure the API could be more polished, removing the need to juggle
references inside the models, but I hope this gets the idea across. Cheers!

Thanks very much for the detailed answer @yoshuawuyts! I'll tinker with your suggestions and see where I get.

Closing because choo@5 will introduce a new API. See #425 for the merged PR. Thanks!

Was this page helpful?
0 / 5 - 0 ratings