Liveblocks Client SDK

This writeup goes into trying to understand how liveblocks client sdk works by diving into codebase & devtools. I would be using the Open source liveblocks repo & nextjs-logo-builder as a example to help me setup & understand the sdk with ease.

If you want to use you will need 3 packages:

I wanted to link the packages so that I can use them locally in my example. My first approach was to use yarn link but then I found the handy link-liveblocks.sh script to do it.

../../scripts/link-liveblocks.sh
==> Rebuilding (in /Users/deepankarbhade/Desktop/liveblocks/packages/liveblocks-client)

> @liveblocks/client@0.17.5-dev build
> rollup -c && cp ./package.json ./README.md ./lib

...

Now my example app uses local liveblocks sdk builds instead of pointing to npm ones.

The first thing all react/nextjs devs would do is to wrap the root component with <RoomProvider />. This magically handles join/leave but how does it do that?

import { createRoomContext } from '@liveblocks/react';

const client = createClient({
  authEndpoint: '/api/auth',
});

export const { RoomProvider } = createRoomContex(client);

const App = () => {
  return <RoomProvider>{/* Components */}</RoomProvider>;
};

Let's peep into the code of createRoomContext.

Inside liveblocks-react/src/factory.tsx file we can check the implementation of createRoomContext

On mounting enter & unmounting leave actions are called.

    React.useEffect(() => {
      setRoom(
        _client.enter(roomId, {
          initialPresence: frozen.initialPresence,
          initialStorage: frozen.initialStorage,
          defaultPresence: frozen.defaultPresence, // Will get removed in 0.18
          defaultStorageRoot: frozen.defaultStorageRoot, // Will get removed in 0.18
          DO_NOT_USE_withoutConnecting: typeof window === "undefined",
        } as RoomInitializers<TPresence, TStorage>)
      );

      return () => {
        _client.leave(roomId);
      };
    }, [_client, roomId, frozen]);
Link to Code

If we look at the implementation of createClient we can observe that it creates a new Map of rooms and implements enter and leave actions.

Let's dive into enter as it's more interesting.

enter creates an internalRoom via createRoom function which returns connect, and disconnect which is called later.

createRoom implementation

Every room associates 3 data components machine, room, and state.

For understanding and play around with it quickly I mounted all 3 of them in the window.

if (typeof window !== 'undefined') {
  window.__room = room;
  window.__state = state;
  window.__machine = machine;
}

connect is one of many actions of a machine that sets the state.

prepareAuthEndpoint makes a POST request to our endpoint and gets the token.

If we reload the page and inspect in the network tab we can see the API call.

Now it passes the auth object to the authenticate function which is a "listener" (not sure). It ultimately sets the token to the state

Setting token to state

Setting token to state

Now that we have our WebSocket connection open let's see how presence and storage are sent across. I focused on the input and then changed the theme here's what I see in my WebSocket network tab.

Websocket Panel

Websocket Panel

We can see that each message has a type and its value is associated with the kind of message it is. Focusing/Unfocusing input sends a message of 100 (Presence update) & Changing theme sends 201 (Storage update).

Here's the message codes for Client & Server

export enum ClientMsgCode {
  // For Presence
  UPDATE_PRESENCE = 100,
  BROADCAST_EVENT = 103,

  // For Storage
  FETCH_STORAGE = 200,
  UPDATE_STORAGE = 201,
}

export enum ServerMsgCode {
  // For Presence
  UPDATE_PRESENCE = 100,
  USER_JOINED = 101,
  USER_LEFT = 102,
  BROADCASTED_EVENT = 103,
  ROOM_STATE = 104,

  // For Storage
  INITIAL_STORAGE_STATE = 200,
  UPDATE_STORAGE = 201,
}

I love when companies provide react hooks for their SDKs this is something I did at 100ms and I am glad liveblocks has great support for it.

This made me curious about how they work under the hood. Let's see the most common hook useOthers which gives us info on other users in the room. Let's try to get the value from the window object that we set.

Use Others

Use Others

Now if we see its actual implementation.

function useOthers(): Others<TPresence, TUserMeta> {
  const room = useRoom();
  const rerender = useRerender();

  React.useEffect(() => {
    const unsubscribe = room.subscribe('others', rerender);
    return () => {
      unsubscribe();
    };
  }, [room, rerender]);

  return room.getOthers();
}

room.getOthers() under the hood calls machine.selectors.getOthers() See the implementation

Bonus:

This is a part dedicated to my love for liveblock's DX

Handling polyfills

I love how polyfills for fetch, WebSocket & atob are handled.

Handling polyfills

Handling polyfills

Missing any param? These helpful error messages would help.

Doc links in Error

Doc links in Error

API call responses

Handling edge cases of not passing keys.

API error response

API error response

Motivation

I have known liveblocks for over a year & have been dwelling in its codebase, blogs, and examples.

Ever since then I had the interest to join Liveblocks but I waited for the "Right moment". The truth is there is no such thing as the "right" time. If you wait for the perfect moment, you may end up waiting for a while. Instead, take a moment and start.

Also beautifully said on the Liveblocks career page:

Sometimes all it takes is a simple message

Motivation behind this write-up

Motivation behind this write-up

Thank you & have a great day!