django-channels icon indicating copy to clipboard operation
django-channels copied to clipboard

Handler objects

Open codingjoe opened this issue 10 months ago • 0 comments

Hi there 👋,

I don't know if this repo is still active. But I recently did a channels integration and wrote web components that could hook to a socket.

We implemented consumer object mixins, that mirrors the channels' consumer Python API, but in JavaScript. I figured I share it here:

/**
 * Mixin to consume a WebSocket connection.
 *
 * The class using this mixin must define the following methods:
 * - onOpen(event)
 * - onClose(event)
 * - onError(event)
 * - onMessage(event)
 * The class using this mixin must also define the following property:
 * - wsUrl: URL
 * @param {*} Base - The base class to extend.
 * @mixin
 * @returns {Base} The new class.
 */
export function WebSocketConsumerMixin(Base) {
  return class extends Base {
    wsUrl
    keepAlive = false
    retry = 0
    maxRetries = 10
    maxBackOff = 1000 * 60 // 1 minute
    minBackOff = 1000 // 1 second
    maxJitter = 1000 // 1 second
    /**
     * Connect to the WebSocket server.
     */
    open() {
      console.info("Opening WebSocket connection to:", this.wsUrl)
      this.ws = new WebSocket(this.wsUrl)
      this.ws.addEventListener("open", this.onOpen.bind(this))
      this.ws.addEventListener("close", this.onClose.bind(this))
      this.ws.addEventListener("message", this.onMessage.bind(this))
      // there is no need to bind `error` event, as `onClose` will be called right after
      //  and `Event` contains no useful information
    }
    /**
     * Close the WebSocket connection.
     */
    close() {
      console.debug("Closing WebSocket connection", this)
      this.keepAlive = false
      this.ws.close()
    }
    /**
     * Get the backoff time for the next retry.
     * @returns {number} The backoff time in milliseconds.
     */
    getBackOff() {
      const jitter = Math.floor(Math.random() * this.maxJitter * 2) - this.maxJitter
      return Math.min(2 ** this.retry * this.minBackOff, this.maxBackOff) + jitter
    }
    /**
     * Event handler for the WebSocket connection.
     * @param {Event} event - The event object.
     */
    onOpen(event) {
      console.debug("Socket opened:", event)
      this.retry = 0
    }
    /**
     * Event handler for the WebSocket connection.
     * @param {Event} event - The event object.
     *
     * If keepAlive is true, the connection will be re-established after a delay.
     */
    onClose(event) {
      console.warn("Socket closed:", event)
      if (this.keepAlive) {
        if (this.retry < this.maxRetries) {
          const backoff = this.getBackOff()
          console.info(`Try to reconnect after ${backoff}ms`)
          setTimeout(() => {
            this.retry++
            console.debug(`Reconnecting (retry ${this.retry})`)
            this.open()
          }, backoff)
        } else {
          throw new Error("Max retries reached")
        }
      }
    }
    /**
     * Event handler for the WebSocket connection.
     * @param {Event} event - The event object.
     */
    onMessage(event) {
      console.debug(event)
    }
    /**
     * Send a message to the WebSocket server.
     * @param {string} message - The message to send.
     */
    send(message) {
      this.ws.send(message)
    }
  }
}
/**
 * Mixin to consume a WebSocket connection and send/receive JSON messages.
 *
 * Counterpart of Django Channels' `channels.generic.websocket.AsyncJsonWebsocketConsumer`,
 * see also: https://channels.readthedocs.io/en/latest/topics/consumers.html#asyncjsonwebsocketconsumer
 *
 * The class using this mixin must define the following methods:
 * - receive(data: object)
 * - send(data: object)
 * The class using this mixin must also define the following property:
 * - wsUrl: URL
 * @param {*} Base - The base class to extend.
 * @mixin
 * @returns {Base} The new class.
 */
export function JSONWebSocketConsumerMixin(Base) {
  return class extends WebSocketConsumerMixin(Base) {
    onMessage(event) {
      this.receive(JSON.parse(event.data))
    }
    /**
     * Receive a JSON message from the WebSocket server.
     * @param {object} data - The JSON message received.
     */
    receive(data) {
      console.debug("Received:", data)
    }
    /**
     * Send a JSON message to the WebSocket server.
     * @param {object} data - The JSON message to send.
     */
    send(data) {
      console.debug("Sending:", data)
      this.ws.send(JSON.stringify(data))
    }
  }
}

You can use them by like so:

class Doodle extends JSONWebSocketConsumerMixin(HTMLCanvasElement) {
  connectedCallback() {
    this.wsUrl = this.getAttribute('ws-url')
    this.connect()
  }
}

I figured I share it, since someone might find this helpful.

Cheers! Joe

codingjoe avatar Jan 28 '25 10:01 codingjoe