Skip to content

feat(arena): wire typed active flow and make mode UIs WIP stubs#14

Draft
muzzlol wants to merge 6 commits into
mainfrom
activate
Draft

feat(arena): wire typed active flow and make mode UIs WIP stubs#14
muzzlol wants to merge 6 commits into
mainfrom
activate

Conversation

@muzzlol

@muzzlol muzzlol commented Feb 9, 2026

Copy link
Copy Markdown
Owner
  • clean up to use the new typed arena client/socket interfaces
    • move file arounds for more readability
    • replace broken draw/code/typing implementations with read-only, concept-preserving stubs
    • keep typed boundaries intact while deferring action/event behavior

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced arena connection state handling with improved error messaging for missing configuration.
    • Added event bridge system for better event handling across arena components.
  • Bug Fixes

    • Improved JSON parsing robustness in arena socket communication.
    • Fixed cursor position tracking in code arena.
  • Refactoring

    • Restructured arena components (Code, Draw, Typing) to use unified data-driven props architecture.
    • Code pane editors set to read-only (Work in Progress).
    • Typing arena now displays preview mode (Work in Progress).

Greptile Overview

Greptile Summary

This PR updates the arena client/server websocket protocol to use a typed ClientMsg envelope (including {type:"action"}), refactors the Durable Object websocket handler to switch on msg.type, and restructures the arena route components (Active/Lobby/Ended) for clearer separation.

On the UI side, draw/code/typing modes are moved onto a unified ArenaComponentProps shape and most mode UIs are intentionally converted to read-only/WIP stubs while keeping typed boundaries (arena data, actions, and events) intact via useArenaSocket and a new event bridge.

Confidence Score: 4/5

  • This PR is largely safe to merge, with a couple of behavioral edge cases to address in the new event bridge and socket error handling.
  • Core protocol envelope changes appear consistently applied across client and server and the UI refactors are mostly mechanical. Remaining concerns are localized to the single-subscriber event bridge semantics and a minor error state persistence edge case in the socket hook.
  • src/routes/arena/-event-bridge.ts, src/routes/arena/-hooks/use-arena-socket.ts

Important Files Changed

Filename Overview
arenas/server.ts Refactors WS message handling to switch on msg.type and routes actions through a new handleAction helper.
shared/arena-protocol.ts Changes ClientMsg to require an explicit {type:'action', action} envelope and reorders type aliases.
src/routes/arena/-active.tsx Adds new ArenaActive layout and wiring to useArenaSocket, plus an event bridge ref; renders per-type arenas with WIP stubs.
src/routes/arena/-code/pane.tsx Switches cursor type to CursorPos, adds editable textarea path with cursor emission, and displays remote cursor line/col footer.
src/routes/arena/-event-bridge.ts Adds a single-listener event bridge that queues events until a listener subscribes.
src/routes/arena/-hooks/use-arena-socket.ts Adds enabled flag + env error handling, makes URL memoized, wraps actions in ClientMsg envelope, and hardens JSON parsing.

Sequence Diagram

sequenceDiagram
  participant UI as ArenaActive
  participant Hook as useArenaSocket
  participant WS as WebSocket
  participant DO as ArenaWSS (Durable Object)

  UI->>Hook: useArenaSocket({arenaId,userId,username,config,enabled})
  Hook->>WS: connect(socketUrl) if enabled && socketUrl!=null
  WS-->>Hook: onOpen
  Hook->>WS: send {type:"init", userId, username, config}
  WS->>DO: ClientMsg(init)
  DO-->>WS: ServerMsg(state)
  WS-->>Hook: onMessage(state)
  Hook-->>UI: participants/data/timeRemaining

  UI->>Hook: sendAction(action)
  Hook->>WS: send {type:"action", action}
  WS->>DO: ClientMsg(action)
  DO->>DO: handleAction(msg.action)
  DO-->>WS: ServerMsg(mechanic|tick|arena_over|error)
  WS-->>Hook: onMessage(...)
  Hook-->>UI: onArenaEvent / state updates

  UI->>Hook: leave()
  Hook->>WS: send {type:"leave"}
  WS->>DO: ClientMsg(leave)
  DO-->>WS: close(1000)
  WS-->>Hook: onClose
Loading

  - clean up  to use the new typed arena client/socket interfaces
  - move file arounds for more readability
  - replace broken draw/code/typing implementations with read-only, concept-preserving stubs
  - keep typed  boundaries intact while deferring action/event behavior
@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request refactors the arena feature's component architecture from prop-drilling to a data-driven pattern using generic props. It relocates ArenaActive, introduces EventBridge for event management, enhances useArenaSocket with enabled flags and robust error handling, and updates Draw/Code/Typing arena components to receive data through a unified ArenaComponentProps<T> interface.

Changes

Cohort / File(s) Summary
Route and Main Orchestration
src/routes/arena/$arenaId.tsx, src/routes/arena/-active.tsx
Removed currentPlayerCount from hook destructuring and component props. ArenaActive moved from -components/ to root arena directory and now fully orchestrates arena context, socket connection, event bridging, and type-specific subcomponent rendering with new generic prop pattern.
Arena Type Components (Refactored to Data-Driven)
src/routes/arena/-draw/arena.tsx, src/routes/arena/-draw/canvas.tsx, src/routes/arena/-code/arena.tsx, src/routes/arena/-code/pane.tsx, src/routes/arena/-typing/arena.tsx, src/routes/arena/-typing/wpm-display.tsx
All arena-specific components refactored to accept ArenaComponentProps<T> with a single data prop instead of individual prop drilling. Replaced CursorData with CursorPos, made TestResult.name optional with output field, added early renders for missing participants, and set editors/canvases to read-only mode. TypingProgress moved to shared arena-protocol.
New Infrastructure and Abstractions
src/routes/arena/-props.tsx, src/routes/arena/-event-bridge.ts
Introduced ArenaComponentProps<T> generic type defining unified props for all arena components and EventBridge<T> utility for pub/sub event management with queuing support.
Socket Hook Enhancement
src/routes/arena/-hooks/use-arena-socket.ts
Added enabled flag to control WebSocket connection, improved error handling with env error detection and JSON parse safety, refactored message routing with explicit typed handling for all message types, and renamed public types to follow PascalCase (UseArenaSocketOptions, UseArenaSocketReturn).
Layout and Navigation
src/routes/arena/-ended.tsx, src/routes/arena/-lobby.tsx
Updated ParticipantList import path from ./participant-list to ./-components/participant-list.
Removed Component
src/routes/arena/-components/arena-active.tsx
Entire ArenaActive component, props interface, imports, and rendering logic removed; functionality relocated to new -active.tsx file.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: refactoring the arena active flow with typed interfaces and converting mode UIs to WIP stubs.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch activate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@muzzlol

muzzlol commented Feb 9, 2026

Copy link
Copy Markdown
Owner Author

@greptile review

@muzzlol

muzzlol commented Feb 9, 2026

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

15 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/routes/arena/-hooks/use-arena-socket.ts
Comment thread src/routes/arena/-hooks/use-arena-socket.ts
@greptile-apps

greptile-apps Bot commented Feb 9, 2026

Copy link
Copy Markdown
Additional Comments (1)

src/routes/arena/-lobby.tsx
Always toasts success

handleCopyLink shows toast.success("Link copied to clipboard!") even when navigator.clipboard.writeText(...) fails (it toasts error then immediately toasts success). This is user-visible and will mislead users on browsers/contexts where clipboard write is denied.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/routes/arena/-lobby.tsx
Line: 82:93

Comment:
**Always toasts success**

`handleCopyLink` shows `toast.success("Link copied to clipboard!")` even when `navigator.clipboard.writeText(...)` fails (it toasts error then immediately toasts success). This is user-visible and will mislead users on browsers/contexts where clipboard write is denied.

How can I resolve this? If you propose a fix, please make it concise.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/routes/arena/-lobby.tsx (1)

82-93: ⚠️ Potential issue | 🟡 Minor

Bug: success toast fires even when clipboard write fails.

When navigator.clipboard.writeText rejects, success is false, the error toast fires at Line 89, but execution continues to Line 92, which unconditionally shows the success toast.

🐛 Proposed fix
   const handleCopyLink = async () => {
     const success = await navigator.clipboard
       .writeText(window.location.href)
       .then(() => true)
       .catch(() => false)
 
     if (!success) {
       toast.error("Failed to copy link to clipboard")
+      return
     }
 
     toast.success("Link copied to clipboard!")
   }
src/routes/arena/$arenaId.tsx (1)

93-93: 🧹 Nitpick | 🔵 Trivial

Non-null assertion is unnecessary — ArenaEndedProps.endedAt already accepts undefined.

Since endedAt is typed as number | undefined in ArenaEndedProps, you can pass arena.endedAt directly without the ! assertion.

Suggested fix
-          endedAt={arena.endedAt!}
+          endedAt={arena.endedAt}
🤖 Fix all issues with AI agents
In `@src/routes/arena/-active.tsx`:
- Around line 184-232: Replace the mutable let arenaContent with a const by
moving the switch into a small resolver (either an IIFE or a helper function
like resolveArenaContent) that returns the JSX for DrawArena, CodeArena or
TypingArena and assign that result to const arenaContent; keep using the
existing props (user._id, data, participants, isSpectator, prompt, sendAction,
and bridgeRef.current as EventBridge<...>) when constructing each component. In
the switch default branch keep the exhaustive never check (const _exhaustive:
never = type) but ensure the resolver returns a concrete fallback (e.g., null)
or throws after logging so TypeScript understands arenaContent is always
assigned. This removes the let and fixes the unassigned-default problem.

In `@src/routes/arena/-code/arena.tsx`:
- Around line 24-32: Extract the duplicated participant-sorting logic into a new
custom hook named useSortedParticipants(participants, userId) that returns the
memoized sorted array; replace the inline useMemo in both DrawArena and
CodeArena (the sortedParticipants definition) with calls to
useSortedParticipants(participants, userId) so the current user is always first
while preserving memoization and the same dependency list.
- Around line 38-56: Extract the repeated "Problem" header block into a small
reusable component (e.g., PromptHeader) that accepts the prompt string (prop
name: prompt) and renders the same structure currently duplicated; then replace
both inline blocks in the empty-state branch and the non-empty-state branch with
<PromptHeader prompt={prompt} /> (or equivalent) to remove duplication while
keeping existing classNames and layout; ensure the new component is defined in
the same module (or imported) and used in place of the duplicate JSX so
sortedParticipants logic and prompt variable usage remain unchanged.

In `@src/routes/arena/-code/pane.tsx`:
- Around line 128-132: The rendered cursor display in pane.tsx incorrectly
treats CursorPos.x as line and CursorPos.y as column; locate the render block
that checks {!isEditable && cursor && ...} and fix by mapping spatial CursorPos
to text coordinates (either introduce a dedicated TextCursorPos with
{line,column} and convert CursorPos → TextCursorPos, or swap the fields when
rendering so line = Math.round(cursor.y) and column = Math.round(cursor.x));
update any helper or prop types you add and document the mapping so future
maintainers know CursorPos is spatial while the code pane uses line/column.

In `@src/routes/arena/-draw/arena.tsx`:
- Around line 39-53: Extract the duplicated header JSX (the block that displays
"Challenge" and the prompt using prompt) into a shared component (e.g.,
PromptHeader) and replace both occurrences in this file and in CodeArena with
that component; specifically locate the duplicated JSX around the early return
when sortedParticipants.length === 0 and the identical header in the main
render, move the markup into PromptHeader (accepting prompt as a prop) and
import/use it in place of the inline markup to avoid duplication.
- Around line 109-118: The FocusOverlay is currently given all entries from
cursorsByParticipant which includes the focused participant's own cursor; update
the cursors prop construction in the FocusOverlay invocation to exclude the
focused participant (i.e., filter out entries where participantId ===
focusedParticipant.id) similar to how paneCursors is created; keep the same
mapping to { participantId, ...cursor } so the shape stays identical and still
use userId to determine whether to pass an empty array when
focusedParticipant.id === userId.

In `@src/routes/arena/-draw/canvas.tsx`:
- Around line 7-25: DrawCanvasProps declares ownerId, onElementsChange, and
onCursorMove but the DrawCanvas function no longer destructures or uses them;
either remove these fields from the DrawCanvasProps interface or mark them as
intentionally unused by renaming to _ownerId, _onElementsChange, and
_onCursorMove (or prefix with underscore in the parameter destructure) so the
contract matches implementation and linter warnings go away; update the
DrawCanvas function signature/props destructuring (and any references inside
DrawCanvas) to match the chosen approach (remove unused props or rename them)
while keeping ownerUsername, isEditable, elements, cursors, and onFocus
unchanged.

In `@src/routes/arena/-event-bridge.ts`:
- Around line 6-27: The createEventBridge function currently allows silent
replacement of an existing subscriber and clears the entire queue even if the
subscriber throws during the flush; update sub, emit, and unsub to be safer:
either enforce the single-listener policy by throwing if listener is already set
in sub (or explicitly document behavior) and, when flushing queued events in
sub, drain the queue one-by-one calling the listener and stop on the first error
so that unprocessed events remain in q (do not zero q unconditionally), leaving
unsub to still clear listener and queue when explicitly called; reference
createEventBridge, sub, emit, unsub, listener, and q when making these changes.

In `@src/routes/arena/-hooks/use-arena-socket.ts`:
- Around line 73-81: The options object passed to the websocket hook is
recreated every render so onOpen/onMessage can capture stale
userId/username/config; memoize the options (or memoize the handlers) using
useMemo/useCallback so that options is recreated only when its dependencies
change: include userId, username, config, onArenaEvent, onGameOver, enabled (and
any functions like sendJsonMessage or setState if not stable) in the dependency
list; ensure the onOpen handler inside options sends the init ClientMsg<T> using
the current values and onMessage uses the current onArenaEvent/onGameOver by
referencing the memoized handlers instead of inline closures.

In `@src/routes/arena/-props.tsx`:
- Around line 1-18: This file only exports types (ArenaComponentProps, ArenaType
import aliases, and EventBridge usage) and contains no JSX, so rename the module
from -props.tsx to -props.ts and update any imports that reference "./-props" in
the codebase; ensure the exported types (ArenaComponentProps and referenced
types like ArenaData, ArenaEvent, ClientAction, Participant, and EventBridge)
remain unchanged and that your build/tsconfig allows .ts imports.

In `@src/routes/arena/-typing/arena.tsx`:
- Line 21: The useMemo around previewText is unnecessary for a cheap string
slice; replace the memoized value with a plain constant by computing previewText
directly from prompt (e.g., const previewText = prompt.slice(0, 320)) and remove
the useMemo import/usage so the component no longer creates a memo for this
trivial operation.

Comment thread src/routes/arena/-active.tsx Outdated
Comment thread src/routes/arena/-code/arena.tsx
Comment thread src/routes/arena/-code/arena.tsx
Comment thread src/routes/arena/-code/pane.tsx
Comment thread src/routes/arena/-draw/arena.tsx
Comment thread src/routes/arena/-draw/canvas.tsx
Comment thread src/routes/arena/-event-bridge.ts
Comment thread src/routes/arena/-hooks/use-arena-socket.ts
Comment thread src/routes/arena/-props.ts
Comment thread src/routes/arena/-typing/arena.tsx Outdated
@muzzlol

muzzlol commented Feb 10, 2026

Copy link
Copy Markdown
Owner Author

@greptile re-review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/routes/arena/-hooks/use-arena-socket.ts
Comment thread src/routes/arena/-code/pane.tsx
@greptile-apps

greptile-apps Bot commented Feb 10, 2026

Copy link
Copy Markdown
Additional Comments (1)

shared/arena-protocol.ts
Type alias uses ActionMap
ClientAction<T> is defined as ActionMap[T] before ActionMap is declared. TypeScript generally requires referenced type aliases to be declared before use, so this can break typechecking in shared/arena-protocol.ts after this reordering.

export type ActionMap = {
  draw: CursorUpdate | CanvasUpdate
  code: CursorUpdate | CodeUpdate | CodeRun
  typing: TypingProgressUpdate
}

export type ClientAction<T extends ArenaType> = ActionMap[T]

export type DrawAction = ActionMap["draw"]
export type CodeAction = ActionMap["code"]
export type TypingAction = ActionMap["typing"]
Prompt To Fix With AI
This is a comment left during a code review.
Path: shared/arena-protocol.ts
Line: 90:106

Comment:
**Type alias uses ActionMap**
`ClientAction<T>` is defined as `ActionMap[T]` before `ActionMap` is declared. TypeScript generally requires referenced type aliases to be declared before use, so this can break typechecking in `shared/arena-protocol.ts` after this reordering.

```suggestion
export type ActionMap = {
  draw: CursorUpdate | CanvasUpdate
  code: CursorUpdate | CodeUpdate | CodeRun
  typing: TypingProgressUpdate
}

export type ClientAction<T extends ArenaType> = ActionMap[T]

export type DrawAction = ActionMap["draw"]
export type CodeAction = ActionMap["code"]
export type TypingAction = ActionMap["typing"]
```

How can I resolve this? If you propose a fix, please make it concise.

muzzlol and others added 2 commits February 11, 2026 00:37
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@muzzlol

muzzlol commented Feb 10, 2026

Copy link
Copy Markdown
Owner Author

@greptile review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +1 to +3
export type EventBridge<T> = {
emit: (event: T) => void
sub: (listener: (event: T) => void) => void

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single subscriber overwrites
sub() overwrites the previous listener without notifying/unsubscribing it, and unsub() clears all queued events. If multiple components subscribe (or a component re-subscribes), earlier subscribers will silently stop receiving events and queued events can be dropped unexpectedly.

This is especially easy to hit if each arena pane/child uses the bridge. Consider supporting multiple listeners (Set) or making sub() return an unsubscribe function scoped to that listener, without clearing global queue on unrelated unsubs.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/routes/arena/-event-bridge.ts
Line: 1:3

Comment:
**Single subscriber overwrites**
`sub()` overwrites the previous `listener` without notifying/unsubscribing it, and `unsub()` clears *all* queued events. If multiple components subscribe (or a component re-subscribes), earlier subscribers will silently stop receiving events and queued events can be dropped unexpectedly.

This is especially easy to hit if each arena pane/child uses the bridge. Consider supporting multiple listeners (Set) or making `sub()` return an unsubscribe function scoped to that listener, without clearing global queue on unrelated unsubs.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only one component subscribes at a time - this is intended behavior, unsub will run in cleanup function - sub in the useEffect of component mount

Comment thread src/routes/arena/-hooks/use-arena-socket.ts
@muzzlol

muzzlol commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

@rgtile

@rgtile

rgtile Bot commented Jul 20, 2026

Copy link
Copy Markdown

Greptile Summary

This PR wires typed arena client/socket interfaces across the active arena, cleans up component file structure (moving ArenaActive, ArenaLobby, ArenaEnded up a level), replaces broken draw/code/typing implementations with read-only WIP stubs, and wraps ClientAction in an { type: "action" } envelope on both client and server for cleaner protocol dispatch. The refactoring is well-structured — typed boundaries are intact, exhaustiveness checks are in place, and the enabled flag properly gates the WebSocket connection until the user is loaded.

Confidence Score: 4/5

Safe to merge; all findings are P2 style suggestions with no runtime impact.

Only P2 findings: a divergent local TestResult type, silent listener replacement in EventBridge, and an unguarded leave callback. No logic bugs or broken contracts.

src/routes/arena/-code/pane.tsx (TestResult/RunResult type drift) and src/routes/arena/-event-bridge.ts (single-listener semantics).

Important Files Changed

Filename Overview
arenas/server.ts Refactored message dispatch into a clean switch on msg.type with a new handleAction helper; exhaustiveness checks added on both switches; correct async handling.
shared/arena-protocol.ts Wrapped ClientAction inside a { type: "action"; action: ... } envelope and reorganised type aliases; protocol is now consistent with the server's new switch.
src/routes/arena/-active.tsx New top-level orchestration component; introduces ArenaLayout, EventBridge ref, and enabled guard; config.hostId is safely empty-string until user loads because enabled gates the socket.
src/routes/arena/-event-bridge.ts New simple pub-sub bridge with queue-then-deliver pattern; supports only one listener — silent replacement on second sub call is a minor concern.
src/routes/arena/-hooks/use-arena-socket.ts Refactored to support enabled flag, lazy socketUrl memoization, robust JSON parsing, and leave helper; leave lacks the same guard as sendAction.
src/routes/arena/-code/pane.tsx Added emitCursor helper and textarea for editable mode; local TestResult type diverges from protocol's RunResult (extra name? field, optional output).
src/routes/arena/-props.ts New shared ArenaComponentProps<T> type that unifies all arena component prop shapes; clean addition.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User/Browser
    participant AA as ArenaActive
    participant HAS as useArenaSocket
    participant EB as EventBridge
    participant Arena as Draw/Code/TypingArena
    participant WS as ArenaWSS (DO)

    U->>AA: mount (arenaId, type, mode)
    AA->>EB: createEventBridge()
    AA->>HAS: "useArenaSocket({ enabled: Boolean(user) })"
    Note over HAS: socketUrl=null until user loads
    HAS-->>AA: "readyState=UNINSTANTIATED"

    U->>AA: user loaded
    AA->>HAS: "enabled=true, socketUrl computed"
    HAS->>WS: WebSocket connect
    WS-->>HAS: "onOpen → send { type:init, userId, config }"

    WS-->>HAS: "{ type:state, participants, data }"
    HAS-->>AA: state update (participants, data)
    AA->>Arena: render with data prop

    WS-->>HAS: "{ type:mechanic, event }"
    HAS-->>AA: onArenaEvent(event)
    AA->>EB: bridge.emit(event)
    EB-->>Arena: listener(event) [when wired]

    Arena->>HAS: sendAction(action)
    HAS->>WS: "{ type:action, action }"
    WS->>WS: handleAction → dispatch by arenaType

    U->>AA: leave
    HAS->>WS: "{ type:leave }"
    WS-->>HAS: ws.close(1000)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User/Browser
    participant AA as ArenaActive
    participant HAS as useArenaSocket
    participant EB as EventBridge
    participant Arena as Draw/Code/TypingArena
    participant WS as ArenaWSS (DO)

    U->>AA: mount (arenaId, type, mode)
    AA->>EB: createEventBridge()
    AA->>HAS: "useArenaSocket({ enabled: Boolean(user) })"
    Note over HAS: socketUrl=null until user loads
    HAS-->>AA: "readyState=UNINSTANTIATED"

    U->>AA: user loaded
    AA->>HAS: "enabled=true, socketUrl computed"
    HAS->>WS: WebSocket connect
    WS-->>HAS: "onOpen → send { type:init, userId, config }"

    WS-->>HAS: "{ type:state, participants, data }"
    HAS-->>AA: state update (participants, data)
    AA->>Arena: render with data prop

    WS-->>HAS: "{ type:mechanic, event }"
    HAS-->>AA: onArenaEvent(event)
    AA->>EB: bridge.emit(event)
    EB-->>Arena: listener(event) [when wired]

    Arena->>HAS: sendAction(action)
    HAS->>WS: "{ type:action, action }"
    WS->>WS: handleAction → dispatch by arenaType

    U->>AA: leave
    HAS->>WS: "{ type:leave }"
    WS-->>HAS: ws.close(1000)
Loading

Reviews (1): Last reviewed commit: "address bugs" | Re-trigger Greptile

Comment on lines 5 to 10
export type TestResult = {
passed: boolean
name: string
name?: string
output?: string
time?: number
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Local TestResult type diverges from protocol's RunResult

TestResult adds name?: string and makes output optional, but CodeData.testResults in arena-protocol.ts is typed as Record<string, RunResult[]> where RunResult has no name field. In code/arena.tsx the RunResult[] values are passed directly to CodePane as testResults, which TypeScript accepts only because TestResult is a looser supertype. When the real code arena is wired up, callers building TestResult objects will get no compile error if they omit output (expecting it to be required as in RunResult), or incorrectly populate name (absent from RunResult). Consider removing the local type and importing RunResult from ~/shared/arena-protocol instead.

Suggested change
export type TestResult = {
passed: boolean
name: string
name?: string
output?: string
time?: number
}
export type { RunResult as TestResult } from "~/shared/arena-protocol"

Or simply replace TestResult references in this file with the imported RunResult.

Comment on lines +18 to +22
sub: (fn: (event: T) => void) => {
listener = fn
q.forEach(fn)
q.length = 0
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 sub silently replaces an existing listener

Calling sub a second time (e.g. component remount in React Strict Mode, or a bug where two consumers try to subscribe) silently discards the previous listener without any warning. The first subscriber stops receiving events with no error. Consider guarding or at minimum asserting:

Suggested change
sub: (fn: (event: T) => void) => {
listener = fn
q.forEach(fn)
q.length = 0
},
sub: (fn: (event: T) => void) => {
if (listener) {
console.warn("EventBridge: replacing existing listener. Only one subscriber is supported.")
}
listener = fn
q.forEach(fn)
q.length = 0
},

Comment on lines +162 to +164
const leave = useCallback(() => {
sendJsonMessage({ type: "leave" } satisfies ClientMsg<T>)
}, [sendJsonMessage])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 leave is not guarded by enabled or socketUrl

sendAction checks if (!enabled || !socketUrl) return before sending, but leave calls sendJsonMessage unconditionally. If leave() is called when the hook is disabled or the host env-var is absent, react-use-websocket will silently no-op, which is harmless — but the inconsistency could be surprising when debugging. For symmetry:

Suggested change
const leave = useCallback(() => {
sendJsonMessage({ type: "leave" } satisfies ClientMsg<T>)
}, [sendJsonMessage])
const leave = useCallback(() => {
if (!enabled || !socketUrl) return
sendJsonMessage({ type: "leave" } satisfies ClientMsg<T>)
}, [enabled, sendJsonMessage, socketUrl])

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant