Skip to content

Commit

Permalink
wip(sync): websocket server with ASGI and PostgreSQL LISTEN/NOTIFY
Browse files Browse the repository at this point in the history
  • Loading branch information
yohanboniface committed Jan 2, 2025
1 parent c6c965a commit f78e16d
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 101 deletions.
22 changes: 10 additions & 12 deletions umap/asgi.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import os

from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "umap.settings")

from django.core.asgi import get_asgi_application
from django.urls import re_path

from .sync import consumers
from .sync.app import application as ws_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "umap.settings")
# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()

urlpatterns = (re_path(r"ws/sync/(?P<map_id>\w+)/$", consumers.SyncConsumer.as_asgi()),)

application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": AllowedHostsOriginValidator(URLRouter(urlpatterns)),
}
)
async def application(scope, receive, send):
if scope["type"] == "http":
await django_asgi_app(scope, receive, send)
elif scope["type"] == "websocket":
await ws_application(scope, receive, send)
else:
raise NotImplementedError(f"Unknown scope type {scope['type']}")
2 changes: 1 addition & 1 deletion umap/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"django.contrib.gis",
"django_probes",
"umap",
"umap.sync",
"social_django",
# See https://github.com/peopledoc/django-agnocomplete/commit/26eda2dfa4a2f8a805ca2ea19a0c504b9d773a1c
# Django does not find the app config in the default place, so the app is not loaded
Expand Down Expand Up @@ -343,4 +344,3 @@
WEBSOCKET_BACK_HOST = env("WEBSOCKET_BACK_HOST", default="localhost")
WEBSOCKET_BACK_PORT = env.int("WEBSOCKET_BACK_PORT", default=8001)
WEBSOCKET_FRONT_URI = env("WEBSOCKET_FRONT_URI", default="ws://localhost:8001")
CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
9 changes: 7 additions & 2 deletions umap/static/umap/js/modules/sync/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class WebSocketTransport {
constructor(webSocketURI, authToken, messagesReceiver) {
this.receiver = messagesReceiver

this.websocket = new WebSocket(`${webSocketURI}`)
this.websocket = new WebSocket(webSocketURI)

this.websocket.onopen = () => {
this.send('JoinRequest', { token: authToken })
Expand All @@ -21,6 +21,10 @@ export class WebSocketTransport {
}
}

this.websocket.onerror = (error) => {
console.log('WS ERROR', error)
}

this.ensureOpen = setInterval(() => {
if (this.websocket.readyState !== WebSocket.OPEN) {
this.websocket.close()
Expand All @@ -34,6 +38,7 @@ export class WebSocketTransport {
// See https://making.close.com/posts/reliable-websockets/ for more details.
this.pingInterval = setInterval(() => {
if (this.websocket.readyState === WebSocket.OPEN) {
console.log('sending ping')
this.websocket.send('ping')
this.pongReceived = false
setTimeout(() => {
Expand All @@ -48,7 +53,6 @@ export class WebSocketTransport {
}

onMessage(wsMessage) {
console.log(wsMessage)
if (wsMessage.data === 'pong') {
this.pongReceived = true
} else {
Expand All @@ -64,6 +68,7 @@ export class WebSocketTransport {
}

close() {
console.log('Closing')
this.receiver.closeRequested = true
this.websocket.close()
}
Expand Down
45 changes: 45 additions & 0 deletions umap/sync/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import uuid

from django.urls.resolvers import RoutePattern

ws_pattern = RoutePattern("/ws/sync/<str:map_id>")


async def application(scope, receive, send):
from .models import Peer

matched = ws_pattern.match(scope["path"])
print(matched)
if not matched:
print("Wrong path")
return
_, _, kwargs = matched

map_id = kwargs["map_id"]
room_id = f"room{map_id}"
peer = await Peer.objects.acreate(uuid=uuid.uuid4(), name="FooBar", room_id=room_id)
print(peer)
peer._send = send
while True:
event = await receive()
print("EVENT", event)

if event["type"] == "websocket.connect":
try:
print("Let's accept")
await send({"type": "websocket.accept"})
await peer.connect()
except ValueError:
await send({"type": "websocket.close"})

if event["type"] == "websocket.disconnect":
print("Closing", event)
await peer.disconnect()
print("Closed")
break

if event["type"] == "websocket.receive":
if event["text"] == "ping":
await send({"type": "websocket.send", "text": "pong"})
else:
await peer.receive(event["text"])
6 changes: 6 additions & 0 deletions umap/sync/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class UmapConfig(AppConfig):
name = "umap.sync"
verbose_name = "uMap Sync"
86 changes: 0 additions & 86 deletions umap/sync/consumers.py

This file was deleted.

23 changes: 23 additions & 0 deletions umap/sync/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.1.4 on 2024-12-27 16:14

from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="Peer",
fields=[
(
"uuid",
models.UUIDField(primary_key=True, serialize=False, unique=True),
),
("name", models.CharField(max_length=200)),
("room_id", models.CharField(max_length=200)),
],
),
]
Empty file.
Loading

0 comments on commit f78e16d

Please sign in to comment.