Projects live at URLs, have owners, and are edited together live
A project is only ever at /p/<id>/<slug>; / is the index of the projects
you own or edit. Every project has an owner, who can name editors;
anyone with the link can view. Every edit saves itself, one request in
flight at a time, as a patch of the leaves that changed, and a websocket
(channels + daphne) carries presence and each committed write to
everyone else in the project. The first write to a leaf wins, and the
loser is told.
Undo is per person: a step undoes only if the leaves it touched still
hold what it left, so it never takes a collaborator's work with it.
Named snapshots replace saving, and restore as an ordinary write.
An empty symbol now survives the leaf round trip with `:nodes {}`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c17ee138f2
commit
6a53adb5e0
31 changed files with 1918 additions and 158 deletions
|
|
@ -1,12 +1,25 @@
|
|||
"""ASGI entry point.
|
||||
|
||||
ASGI and not only WSGI because the collaboration design in docs/architecture.md
|
||||
puts presence and document deltas on a websocket. Those consumers are out of step
|
||||
9's scope; this is the half of their setup that costs nothing now.
|
||||
HTTP is Django as usual. The websocket carries presence and the document deltas
|
||||
the server broadcasts after a write — never writes, which stay on HTTP. See
|
||||
docs/architecture.md, Collaboration.
|
||||
"""
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings")
|
||||
application = get_asgi_application()
|
||||
django_asgi_app = get_asgi_application()
|
||||
|
||||
from channels.auth import AuthMiddlewareStack # noqa: E402
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
|
||||
from channels.security.websocket import AllowedHostsOriginValidator # noqa: E402
|
||||
|
||||
from clips.routing import websocket_urlpatterns # noqa: E402
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": django_asgi_app,
|
||||
"websocket": AllowedHostsOriginValidator(
|
||||
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
|
||||
),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ CSRF_TRUSTED_ORIGINS = [
|
|||
]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# First, so `runserver` is daphne's and serves the websocket too.
|
||||
"daphne",
|
||||
"channels",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
|
|
@ -51,6 +54,10 @@ MIDDLEWARE = [
|
|||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
]
|
||||
|
||||
# Process-local, like the consumer's ROOMS: one worker. docs/architecture.md
|
||||
# names the move — Redis — for the day there is a second.
|
||||
CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
|
||||
|
||||
ROOT_URLCONF = "server.urls"
|
||||
WSGI_APPLICATION = "server.wsgi.application"
|
||||
ASGI_APPLICATION = "server.asgi.application"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ urlpatterns = [
|
|||
# ARTHUR_URL can stay pointed at the same place and only the port moves.
|
||||
path("", views.page, name="page"),
|
||||
path("index.html", views.page),
|
||||
# A project's own address, with its name as a slug for people to read. The
|
||||
# same page; the client reads the id out of the path and opens it.
|
||||
path("p/<uuid:project_id>", views.page),
|
||||
path("p/<uuid:project_id>/<slug:slug>", views.page),
|
||||
path("api/", include("clips.urls")),
|
||||
path("blob/<str:digest>", views.blob, name="blob"),
|
||||
path("admin/", admin.site.urls),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue