Skip to content

Instantly share code, notes, and snippets.

@jobelenus
Last active March 8, 2023 06:52
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jobelenus/387134f0910f8835edf266fe30d9cb8f to your computer and use it in GitHub Desktop.
Save jobelenus/387134f0910f8835edf266fe30d9cb8f to your computer and use it in GitHub Desktop.
Websockets with Django - Best Practices

John Obelenus, jobelenus@activefrequency.com

Websockets are basically the same sockets you used in your college programming courses.

Base websocket class

class BaseJsonWebsocketConsumer(JsonWebsocketConsumer):

    def send(self, content, close=False):
        """
        Encode the given content as JSON and send it to the client.
        """
        # NOTE: they dont have a class attribute for the encoder, so overriding here for Decimal support
        super(JsonWebsocketConsumer, self).send(text=json.dumps(content, cls=DjangoJSONEncoder), close=close)

The Basic Consumer

Each consumer should only do One Thing

class MyConsumer(BaseJsonWebsocketConsumer):
    GROUP_NAME = 'my_consumer'

    def receive(self, content, **kwargs):
      pass
      
    @staticmethod
    def send_to_websockets(data=None):
        data = {}
        Group(MyConsumer.GROUP_NAME).send(json.dumps(data, cls=DjangoJSONEncoder))
        //Group(MyConsumer.GROUP_NAME).send({'text': json.dumps(data, cls=DjangoJSONEncoder)})

The Javascript

A helper method so you never have to think about it again

function clean_websocket_data(evt) {  // evt is the onmessage event
    /* This is annoying to have to do but here is whats going on:
     * When a channels consumer replies with self.send, you can pass a dict
     * But when you use Group(name).send(...) you have to pass a dict with a key of `text`
     * that contains your data as a string (which is always json.dumps for us)
     *
     * This means that e.data may have a meaningless `text` key in the way
     * So if its there, grab the value and return that as the data you really want
     * If there is no `text` text you're good
     */
    var data = JSON.parse(evt.data);
    try {
        if (data.text) {
            return JSON.parse(data.text);
        } 
        return data;
    } catch(e) {
        return data;
    }
}

var protocol = (window.location.protocol === 'https:' ? 'wss://' : 'ws://');
var base_url = protocol + window.location.hostname + ':' + window.location.port;
var socket = new ReconnectingWebSocket(base_url + "/ws/my-url/", null, {automaticOpen: false});
socket.onmessage = function(e) {
    // whenever webhook recieves
    var data = clean_websocket_data(e);
};
socket.onopen = function() {
    // whenever webhook opens -- which can be At Any Time
    socket.send(JSON.stringify({text: ""}));
};
socket.open();

My most popular use case

Keeping track of who is working on what in a really big list of items (also, generating that list in the first place)

class ActivityConsumer(BaseJsonWebsocketConsumer):
    GROUP_NAME = 'my_activity'
    CACHE_KEY = 'my-activity-list'

    @staticmethod
    def update_cache(data):
        pass
        
    @staticmethod
    def update_cache(data):
        return cache.get(ActivityConsumer.CACHE_KEY, [])

    def receive(self, content, **kwargs):
        if 'action' in content.keys():  # if I have data echo it back to the group
            ActivityConsumer.update_cache(content)
            Group(ActivityConsumer.GROUP_NAME).send({'text': json.dumps([content], cls=DjangoJSONEncoder)}, immediately=True)
        else:
            self.send(ActivityConsumer.get_cache())  # send back the current master list of activity
            
    @staticmethod
    def send_to_websockets():
        Group(ActivityConsumer.GROUP_NAME).send({'text': json.dumps(ActivityConsumer.get_cache(), cls=DjangoJSONEncoder)})

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