Skip to content

Instantly share code, notes, and snippets.

@searls
Last active June 17, 2018 12:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save searls/77f055ad93e24c2be7711003f805bb0f to your computer and use it in GitHub Desktop.
Save searls/77f055ad93e24c2be7711003f805bb0f to your computer and use it in GitHub Desktop.
Welp, I'm the proud owner of a hobbled-together framework built on top of ActionCable to simulate what had been an XHR request-response lifecycle. I'm just trying to use WebSockets to make a frequently-made request go faster, but boy did this feel like pulling teeth.
class StudyChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
def handle_answer(data)
self.class.broadcast_to(current_user, {
:request_id => data["requestId"],
data["mode"] => {
data["id"] => PresentsReviewResult.new.call(current_user, data["id"], data["answer"])
}
})
end
end
// app.ws - actioncable / websocket
app.ws = app.ws || {}
app.ws.cable = null
app.ws.init = () => {
app.ws.cable = app.ws.cable || ActionCable.createConsumer()
return app.ws.cable
}
app.ws.disconnect = () => {
if (app.ws.cable) {
app.ws.cable.disconnect()
}
}
app.ws.subscription = null
app.ws.subscriptionCallbacks = []
app.ws.subscriptionConnected = false
// I really hate this subscriptions API. Basically, I want the subscription to be
// a singleton but I don't want to leak details about how the cable API works,
// so I'm collecting all the callbacks I receive and then invoking them either
// when the subscription is connected or immediately if the subscription is
// already connected.
app.ws.subscribe = (connectionCb) => {
app.ws.subscriptionCallbacks.push(connectionCb)
const cable = app.ws.init()
if (!app.ws.subscription) {
app.ws.subscription = cable.subscriptions.create('StudyChannel', {
connected () {
app.ws.subscriptionConnected = true
app.ws.subscriptionCallbacks.forEach(cb =>
cb(app.ws.subscription)
)
app.ws.subscriptionCallbacks = []
},
received (data) {
app.ws.respond(humps.camelizeKeys(data))
}
})
} else if (app.ws.subscriptionConnected) {
app.ws.subscriptionCallbacks.forEach(cb =>
cb(app.ws.subscription)
)
app.ws.subscriptionCallbacks = []
} else {
cable.connect()
}
}
app.ws.unsubscribe = () => {
app.ws.disconnect()
app.ws.subscriptionConnected = false
}
app.ws.requestId = 0
app.ws.requestCallbacks = {}
app.ws.request = (payload, cb) => {
const requestId = ++app.ws.requestId
app.ws.requestCallbacks[requestId] = cb
app.ws.subscribe(subscription => {
subscription.perform('handle_answer', _.extend({requestId}, payload))
})
}
app.ws.respond = (data) => {
const requestId = data.requestId
delete data.requestId
if (app.ws.requestCallbacks[requestId]) {
app.ws.requestCallbacks[requestId](data)
delete app.ws.requestCallbacks[requestId]
}
}
// Actually using it looks something like this
app.ws.request({id: meaning.id, answer, mode}, (res) => {
const props = app.prop.merge(res)
app.store.save(props)
app.render(props)
app.route.redirect(`/app/${mode}/${meaning.id}/result`)
})
@javan
Copy link

javan commented Jun 17, 2018

subscription.perform() returns a "success" boolean that you can use instead of tracking the connected state manually. Simplified example:

cable.subscriptions.create('StudyChannel', {
  initialized () {
    this.queue = []
  },
  
  connected () {
    while (this.queue.length) {
      this.queue.shift()()
    }
  },

  received (data) {
    // …
  },

  request (payload) {
    const send = () => this.perform('handle_answer', payload)
    send() || this.queue.push(send)  
  }
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment