logoalt Hacker News

riskablelast Monday at 5:21 PM0 repliesview on HN

After all my years of web development, my rules are thus:

    * If the browser has an optimal path for it, use HTTP (e.g. images where it caches them automatically or file uploads where you get a "free" progress API).
    * If I know my end users will be behind some shitty firewall that can't handle WebSockets (like we're still living in the early 2010s), use HTTP.
    * Requests will be rare (per client):  Use HTTP.
    * For all else, use WebSockets.
WebSockets are just too awesome! You can use a simple event dispatcher for both the frontend and the backend to handle any given request/response and it makes the code sooooo much simpler than REST. Example:

    WSDispatcher.on("pong", pongFunc);
...and `WSDispatcher` would be the (singleton) object that holds the WebSocket connection and has `on()`, `off()`, and `dispatch()` functions. When the server sends a message like `{"type": "pong", "payload": "<some timestamp>"}`, the client calls `WSDispatcher.dispatch("pong", "<some timestamp>")` which results in `pongFunc("<some timestamp>")` being called.

It makes reasoning about your API so simple and human-readable! It's also highly performant and fully async. With a bit of Promise wrapping, you can even make it behave like a synchronous call in your code which keeps the logic nice and concise.

In my latest pet project (collaborative editor) I've got the WebSocket API using a strict "call"/"call:ok" structure. Here's an example from my WEBSOCKET_API.md:

    ### Create Resource
    ```javascript
    // Create story
    send('resources:create', {
      resource_type: 'story',
      title: 'My New Story',
      content: '',
      tags: {},
      policy: {}
    });
    
    // Create chapter (child of story)
    send('resources:create', {
      resource_type: 'chapter',
      parent_id: 'story_abc123', // This would actually be a UUID
      title: 'Chapter 1'
    });
    
    // Response:
    {
      type: 'resources:create:ok', // <- Note the ":ok"
      resource: { id: '...', resource_type: '...', ... }
    }
    ```
I've got a `request()` helper that makes the async nature of the WebSocket feel more like a synchronous call. Here's what that looks like in action:

    const wsPromise = getWsService(); // Returns the WebSocket singleton
    
    // Create resource (story, chapter, or file)
    async function createResource(data: ResourcesCreateRequest) {
      loading.value = true;
      error.value = null;
      try {
        const ws = await wsPromise;
        const response = await ws.request<ResourcesCreateResponse>(
          "resources:create",
          data // <- The payload
        );
        // resources.value because it's a Vue 3 `ref()`:
        resources.value.push(response.resource); 
        return response.resource;
      } catch (err: any) {
        error.value = err?.message || "Failed to create resource";
        throw err;
      } finally {
        loading.value = false;
      }
    }
For reference, errors are returned in a different, more verbose format where "type" is "error" in the object that the `request()` function knows how to deal with. It used to be ":err" instead of ":ok" but I made it different for a good reason I can't remember right now (LOL).

Aside: There's still THREE firewalls that suck so bad they can't handle WebSockets: SophosXG Firewall, WatchGuard, and McAfee Web Gateway.