Skip to main content

Keep objects synchronized over a persistent WebSocket session

Project description

ws-sync: WebSocket Sync

This library defines a very simple WebSocket and JSON & JSON Patch based protocol for keeping the python backend and the browser frontend in sync. There's a corresponding react library that implements the frontend side of the protocol.

Quickstart

Syncing simple states

Backend

Let's say you have the following object:

class Notes:
    def __init__(self):
        # my attributes, as usual
        self.title = "My Notes"
        self.notes = []
    
    @property
    def total_length(self):
        return sum(len(note) for note in self.notes)
    
    def rename(self, new_title):
        self.title = new_title
    
    def add(self, note):
        self.notes.append(note)

To sync it to the frontend, it is as simple as:

+from ws_sync import sync_all

class Notes:
+   @sync_all("NOTES")  # create the sync object and define the key
    def __init__(self):
        # my attributes, as usual
        self.title = "My Notes"
        self.notes = []

    @property
    def total_length(self):
        return sum(len(note) for note in self.notes)
    
+   async def rename(self, new_title):
        self.title = new_title
+       await self.sync()  # make sure the frontend knows about the change
    
+   async def add(self, note):
        self.notes.append(note)
+       await self.sync()  # make sure the frontend knows about the change

The Sync("Notes", self) call automatically detects the attributes to sync in self, which are all attributes or @properties that do not start with an underscore. You can also specify the attributes to sync manually:

@sync_only("NOTES",
    title = ...,
    notes = ...,
    total_length = "size",
)

The keyword argument is the local name of the attribute, the value is the name of the attribute in the frontend. If the value is ..., the local and frontend name are the same. This is useful if you want to rename an attribute in the frontend without changing the name in the backend (e.g. snake_case to camelCase).

For more info on the options and examples, see ws_sync.decorators docs.

Frontend

On the frontend, you can use the useSynced hook to sync the state to the backend:

const Notes = () => {
    const notes = useSynced("NOTES", {
        title: "",
        notes: [],
    })

    return (
        <div>
            <h1>{notes.title}</h1>
            <ul>{notes.notes.map(note => <li>{note}</li>)}</ul>
        </div>
    )
}

The second parameter of useSynced is the initial state.

The returned notes object not only contains the state, but also the setters and syncers:

const Notes = () => {
    const notes = useSynced("NOTES", {
        title: "",
        notes: [],
    })

    return (
        <div>
            <input value={notes.title} onChange={e => notes.syncTitle(e.target.value)} />
            <ul>{notes.notes.map(note => <li>{note}</li>)}</ul>
        </div>
    )
}

For more info on the react library, see ws-sync-react.

Actions

Actions are a way to call methods on the remote (action handlers), usually frontend -> backend.

TODO

Tasks

Tasks are like actions, but for long-running operations and can be cancelled.

TODO

Server

Of course, to actually connect the frontend and backend, you need a server. Here's an example using FastAPI:

from fastapi import FastAPI
from ws_sync import Session
from .notes import Notes

# create a new session, in this case only 1 global session
with Session() as session:
    my_notes = Notes()
    my_session = session

# FastAPI server
app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
    await ws.accept()

    try:
        await my_session.new_connection(ws)
        await my_session.handle_connection()
    finally:
        my_session.ws = None
        await ws.close()

Concepts and Implementation

High-Level

Session: A session is a connection between a frontend and a backend, and it persists across WebSocket reconnects. This means that any interruption of the connection will not affect the backend state in any way, and all the self.sync() calls will be ignored. On reconnect, the frontend will automatically restore the latest state from the backend.

Sync: A sync operation will generate a new snapshot of the object state, calculate the difference to the previous state snapshot, and send a JSON Patch object to the frontend. The frontend will then apply the patch to its local state. This is done automatically on every self.sync() call. This way, only the changes are sent over the network, and the frontend state is always in sync with the backend state.

Low-Level

Events: The primitive of the protocol are events. An event is a JSON object with a simple {"type": "event_type", "data": any} format. All the operations done by the Sync object uses different events, including actions and tasks.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ws_sync-0.6.4.tar.gz (12.2 kB view details)

Uploaded Source

Built Distribution

ws_sync-0.6.4-py3-none-any.whl (12.6 kB view details)

Uploaded Python 3

File details

Details for the file ws_sync-0.6.4.tar.gz.

File metadata

  • Download URL: ws_sync-0.6.4.tar.gz
  • Upload date:
  • Size: 12.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.7.1 CPython/3.12.3 Darwin/23.5.0

File hashes

Hashes for ws_sync-0.6.4.tar.gz
Algorithm Hash digest
SHA256 ff100c9b145fb1035d8742c01d841998717b71c16d053e0832176bc86908f6af
MD5 0c38696a4336a4b50c1ad0e7559a7122
BLAKE2b-256 15ffbc60091493f36f00d0ece678551ee662749d337c3b68e10d0551dbd89c6a

See more details on using hashes here.

File details

Details for the file ws_sync-0.6.4-py3-none-any.whl.

File metadata

  • Download URL: ws_sync-0.6.4-py3-none-any.whl
  • Upload date:
  • Size: 12.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.7.1 CPython/3.12.3 Darwin/23.5.0

File hashes

Hashes for ws_sync-0.6.4-py3-none-any.whl
Algorithm Hash digest
SHA256 0b49a3426a4c5726889845b19e8faec8e9993cbe4dfd7dd42efe384c17415e9a
MD5 4a53a2534efa4b36c20634425bb3c206
BLAKE2b-256 502271b7508910b46cb98f12c0e91d5e9f4a3bdd2dfbbe94f697ae280cd9d6e3

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page