From 594043ec042dde713c09d31888760df1fdbfbdd0 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Tue, 13 Jun 2023 16:08:29 +0200 Subject: [PATCH] Add diagram explaining architecture --- docs/index.md | 24 +++++++++++++++++++++++- docs/usage/client.md | 2 ++ docs/usage/server.md | 2 ++ mkdocs.yml | 6 +++++- ypy_websocket/websocket_provider.py | 4 +++- ypy_websocket/websocket_server.py | 4 +++- ypy_websocket/yroom.py | 4 +++- ypy_websocket/ystore.py | 4 +++- 8 files changed, 44 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index c894327..68ee816 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,25 @@ Ypy-websocket is a Python library for building WebSocket servers and clients that connect and synchronize shared documents. - It can be used to create collaborative web applications. + +The following diagram illustrates a typical architecture. The goal is to share a document among several clients. + +Each client has an instance of a [YDoc](https://ypy.readthedocs.io/en/latest/autoapi/y_py/index.html#y_py.YDoc), representing their view of a document. A shared document also lives in a [room](./reference/Room.md) on the server side. Conceptually, a room can be seen as the place where clients collaborate on a document. The WebSocket to which a client connects points to the corresponding room through the endpoint path. In the example below, clients A and B connect to a WebSocket at path `room-1`, and thus both clients find themselves in a room called `room-1`. All the `YDoc` synchronization logic is taken care of by the [WebsocketProvider](./reference/WebSocket_provider.md). + +Each update to a shared document can be persisted to disk using a [store](./reference/Store.md), which can be a file or a database. +```mermaid +flowchart TD + classDef room1 fill:#f96 + classDef room2 fill:#bbf + A[Client A
room-1]:::room1 <-->|WebSocket
Provider| server(WebSocket Server) + B[Client B
room-1]:::room1 <-->|WebSocket
Provider| server + C[Client C
room-2]:::room2 <-->|WebSocket
Provider| server + D[Client D
room-2]:::room2 <-->|WebSocket
Provider| server + server <--> room1((room-1
clients: A, B)):::room1 + server <--> room2((room-2
clients: C, D)):::room2 + A <-..-> room1 + B <-..-> room1 + C <-..-> room2 + D <-..-> room2 + room1 ---> store1[(Store)] + room2 ---> store2[(Store)] +``` diff --git a/docs/usage/client.md b/docs/usage/client.md index b737c40..58a0200 100644 --- a/docs/usage/client.md +++ b/docs/usage/client.md @@ -1,3 +1,5 @@ +A client connects their `YDoc` through a [WebsocketProvider](../reference/WebSocket_provider.md). + Here is a code example using the [websockets](https://websockets.readthedocs.io) library: ```py import asyncio diff --git a/docs/usage/server.md b/docs/usage/server.md index f8bf954..5b1d5e0 100644 --- a/docs/usage/server.md +++ b/docs/usage/server.md @@ -1,3 +1,5 @@ +A server connects multiple `YDoc` through a [WebsocketServer](../reference/WebSocket_server.md). + Here is a code example using the [websockets](https://websockets.readthedocs.io) library: ```py import asyncio diff --git a/mkdocs.yml b/mkdocs.yml index bafc53b..b4bdbe6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,7 +44,11 @@ nav: markdown_extensions: - admonition - pymdownx.details - - pymdownx.superfences + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format plugins: - mkdocstrings: diff --git a/ypy_websocket/websocket_provider.py b/ypy_websocket/websocket_provider.py index 06726e9..076d10a 100644 --- a/ypy_websocket/websocket_provider.py +++ b/ypy_websocket/websocket_provider.py @@ -66,7 +66,7 @@ def started(self) -> Event: self._started = Event() return self._started - async def __aenter__(self): + async def __aenter__(self) -> WebsocketProvider: if self._task_group is not None: raise RuntimeError("WebsocketProvider already running") @@ -77,6 +77,8 @@ async def __aenter__(self): tg.start_soon(self._run) self.started.set() + return self + async def __aexit__(self, exc_type, exc_value, exc_tb): if self._task_group is None: raise RuntimeError("WebsocketProvider not running") diff --git a/ypy_websocket/websocket_server.py b/ypy_websocket/websocket_server.py index 343a09c..baf2253 100644 --- a/ypy_websocket/websocket_server.py +++ b/ypy_websocket/websocket_server.py @@ -177,7 +177,7 @@ async def _serve(self, websocket, *, task_status: TaskStatus[None] = TASK_STATUS tg.cancel_scope.cancel() task_status.started() - async def __aenter__(self): + async def __aenter__(self) -> WebsocketServer: if self._task_group is not None: raise RuntimeError("WebsocketServer already running") @@ -187,6 +187,8 @@ async def __aenter__(self): self._exit_stack = exit_stack.pop_all() self.started.set() + return self + async def __aexit__(self, exc_type, exc_value, exc_tb): if self._task_group is None: raise RuntimeError("WebsocketServer not running") diff --git a/ypy_websocket/yroom.py b/ypy_websocket/yroom.py index 282eb8c..06bf5de 100644 --- a/ypy_websocket/yroom.py +++ b/ypy_websocket/yroom.py @@ -81,7 +81,7 @@ async def _broadcast_updates(self): self.log.debug("Writing Y update to YStore") self._task_group.start_soon(self.ystore.write, update) - async def __aenter__(self): + async def __aenter__(self) -> YRoom: if self._task_group is not None: raise RuntimeError("YRoom already running") @@ -92,6 +92,8 @@ async def __aenter__(self): tg.start_soon(self._broadcast_updates) self.started.set() + return self + async def __aexit__(self, exc_type, exc_value, exc_tb): if self._task_group is None: raise RuntimeError("YRoom not running") diff --git a/ypy_websocket/ystore.py b/ypy_websocket/ystore.py index f567909..1b5c6c5 100644 --- a/ypy_websocket/ystore.py +++ b/ypy_websocket/ystore.py @@ -47,7 +47,7 @@ def started(self) -> Event: self._started = Event() return self._started - async def __aenter__(self): + async def __aenter__(self) -> BaseYStore: if self._task_group is not None: raise RuntimeError("YStore already running") @@ -57,6 +57,8 @@ async def __aenter__(self): self._exit_stack = exit_stack.pop_all() tg.start_soon(self.start) + return self + async def __aexit__(self, exc_type, exc_value, exc_tb): if self._task_group is None: raise RuntimeError("YStore not running")