room SCROLL applet: live async refresh over WebSocket (not just on page reload)
The room's scroll-of-events feed only updated on refresh — a gamer watching the SCROLL view never saw a co-player's deposit / role pick / sig appear. Now every recorded GameEvent nudges all open room sockets to re-fetch the feed. - drama.models.record() broadcasts a `scroll_update` to the `room_<id>` group via transaction.on_commit — so the live re-fetch sees the committed row, and a rolled-back TestCase never fires it (zero overhead / channel-layer traffic for the plain IT suite). _broadcast_scroll_update is fully guarded: a missing/unreachable channel layer must NEVER break event recording (falls back to refresh-to-update). One central hook covers every event writer, current + future. - RoomConsumer gains a `scroll_update` relay handler (same one-liner shape as gate_update / turn_changed). - New `scroll_status` view + url (epic:scroll_status, room/<id>/scroll/status) renders JUST core/_partials/_scroll.html with the same events/viewer/scroll_position context as room_view's inline paint, so the swapped feed is identical. - room-scroll.js listens for `room:scroll_update`, fetches the partial, swaps #id_drama_scroll, then re-applies the saved Frame/Redact filter + restarts the buffer dots on the fresh nodes. URL comes from .room-page[data-scroll-status-url]. Refactored the dots + filter into re-runnable helpers; existing behavior (title reel IO, filter form, localStorage) preserved. TDD: - drama RecordBroadcast ITs: record() schedules the broadcast on commit (captureOnCommitCallbacks execute=True) and NOT before commit. - RoomConsumer relays scroll_update (InMemory layer, WebsocketCommunicator). - ScrollStatusViewTest: endpoint renders the feed section, reflects the latest events, is the bare partial (no navbar/aperture chrome). 544 drama+epic ITs green — the on_commit hook is inert under TestCase, so no existing event-writer test regressed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -214,9 +214,34 @@ class ScrollPosition(models.Model):
|
||||
return f"{self.user.email} @ {self.room.name}: {self.position}px"
|
||||
|
||||
|
||||
def _broadcast_scroll_update(room_id):
|
||||
"""Nudge every open room socket to re-fetch the scroll-of-events feed so the
|
||||
SCROLL applet updates live (not just on page refresh). Guarded — a missing
|
||||
or unreachable channel layer must NEVER break event recording, so any error
|
||||
is swallowed (the feed simply falls back to refresh-to-update)."""
|
||||
try:
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
layer = get_channel_layer()
|
||||
if layer is None:
|
||||
return
|
||||
async_to_sync(layer.group_send)(
|
||||
f"room_{room_id}", {"type": "scroll_update"})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def record(room, verb, actor=None, **data):
|
||||
"""Record a game event in the drama log."""
|
||||
return GameEvent.objects.create(room=room, actor=actor, verb=verb, data=data)
|
||||
"""Record a game event in the drama log.
|
||||
|
||||
Broadcasts a `scroll_update` to the room group AFTER the surrounding
|
||||
transaction commits (`on_commit`) so the live re-fetch sees the new row,
|
||||
and so a rolled-back TestCase never fires it (zero overhead/risk for the
|
||||
plain IT suite). RoomConsumer relays it → room-scroll.js swaps the feed."""
|
||||
from django.db import transaction
|
||||
event = GameEvent.objects.create(room=room, actor=actor, verb=verb, data=data)
|
||||
transaction.on_commit(lambda: _broadcast_scroll_update(room.id))
|
||||
return event
|
||||
|
||||
|
||||
_NOTE_DISPLAY = {
|
||||
|
||||
Reference in New Issue
Block a user