Skip to content

feat(apps/playground): WebSocket sync for Lexical × EG-walker - #831

Merged
zhyd1997 merged 8 commits into
nextfrom
cursor/lexical-eg-walker-ws-4f5f
Aug 5, 2026
Merged

zhyd1997 merged 8 commits into
nextfrom
cursor/lexical-eg-walker-ws-4f5f

Conversation

@zhyd1997

@zhyd1997 zhyd1997 commented Aug 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds real WebSocket transport for Lexical + EG-walker collaboration (document batches + presence), with a playground demo you can verify across browsers.

What changed

  • Nitro WebSocket servers — /api/collab-doc, /api/presence, /api/collab-sync
  • Reconnect — doc channel repairs on reopen; presence emits reconnecting / error; StatusRail shows offline UI
  • WS lifecycle — connectionTimeoutMs for hung connecting sockets; close prior socket before connect() replaces it; camelCase modules under 200 lines
  • Homepage — SoftMaple demo grid led by Lexical × EG-walker

How to verify

pnpm --filter @softmaple/playground typecheck
pnpm --filter @softmaple/playground test
pnpm --filter @softmaple/awareness typecheck
pnpm --filter @softmaple/awareness test:coverage
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • Added WebSocket collaboration for the Lexical EG-walker demo, with BroadcastChannel available for local, multi-tab collaboration.
    • Added room synchronization, presence, durable persistence, repair support, and automatic reconnection.
    • Added transport-aware status indicators and reconnect warnings.
    • Redesigned the playground homepage with collaboration demo cards and direct navigation.
  • Bug Fixes
    • Improved room joining, leaving, URL handling, and connection lifecycle behavior.
  • Documentation
    • Added local collaboration server setup and testing guidance.
  • Tests
    • Expanded synchronization, persistence, navigation, and reconnection coverage.

Enable Nitro WebSocket relays for document batches and presence, wire the
Lexical demo to same-origin WS by default (BroadcastChannel fallback via
?transport=broadcast), and cover cross-browser sync in e2e.
@cursor

cursor Bot commented Aug 5, 2026

Copy link
Copy Markdown

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@cursor

cursor Bot commented Aug 5, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 27ebc64c-5e91-4849-ae58-d08b9c2b3876

📥 Commits

Reviewing files that changed from the base of the PR and between 94adc3d and 2f6a6b3.

📒 Files selected for processing (1)
  • apps/playground/src/modules/collab-transport/urls.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/playground/src/modules/collab-transport/urls.test.ts

📝 Walkthrough

Walkthrough

The playground adds selectable WebSocket and BroadcastChannel collaboration transports. It introduces Nitro WebSocket relays, reconnecting document channels, Lexical room integration, connection-state UI, updated navigation, and browser tests for synchronization and persistence.

Changes

Collaboration transport

Layer / File(s) Summary
WebSocket relay servers
apps/playground/server/**, apps/playground/vite.config.ts, apps/playground/server/README.md
Nitro handlers validate room messages and relay document, sync, and presence traffic with bounded room storage and disconnect cleanup.
Transport resolution and WebSocket channel
apps/playground/src/env.ts, apps/playground/src/modules/collab-transport/*
Transport selection resolves query, environment, same-origin, and override URLs. The WebSocket channel queues messages, parses frames, reports state, and reconnects with bounded retries.
Connection lifecycle and persistence state
packages/awareness/src/adapters/websocket/*, apps/playground/src/modules/lexical-eg-walker/persistence/*
Awareness reconnects through explicit lifecycle states. Persistence channels and coordinators expose synchronization connection state and request repair after reconnects.
Lexical room transport integration
apps/playground/src/modules/lexical-eg-walker/{transport.ts,presence.ts,useLexicalRoom.ts,StatusRail.tsx,LexicalEgWalkerDemo.tsx}, apps/playground/src/modules/lexical-eg-walker/presence.test.ts
Lexical rooms configure document and presence transports, expose the selected mode, merge connection states, and render transport-specific status information.
Demo endpoint and navigation wiring
apps/playground/src/routes/demo/online-collab-editor.tsx, apps/playground/src/routes/index.tsx, docs/design/surface-bindings.md
The online editor uses resolved sync endpoints and centralized room URL updates. The homepage and binding documentation link to collaboration demos.
Configurable playground and E2E coverage
apps/playground/playground-port.ts, apps/playground/playwright.config.ts, apps/playground/e2e/**
Playwright uses a configurable port and shared room helpers. E2E tests cover BroadcastChannel tabs, WebSocket browser contexts, persistence, and homepage navigation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BrowserA
  participant LexicalRoom
  participant WebSocketRelays
  participant BrowserB
  BrowserA->>LexicalRoom: Resolve room transport
  LexicalRoom->>WebSocketRelays: Connect document and presence sockets
  BrowserA->>WebSocketRelays: Send room event and presence update
  WebSocketRelays-->>BrowserB: Relay synchronized data
  BrowserB-->>LexicalRoom: Update document and connection state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: adding WebSocket synchronization for Lexical × EG-walker in the playground.
Description check ✅ Passed The description explains the WebSocket changes and verification commands, but it does not include the required issue-linking line or template headings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/lexical-eg-walker-ws-4f5f

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.

@zhyd1997

zhyd1997 commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

@cursor make sure the type check passes

@cursor

cursor Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Fixed. CI failed with TS2742 because defineWebSocketHandler’s inferred default export referenced a non-portable nested h3 path.

Annotated all three Nitro WS handlers as EventHandler from nitro/h3. pnpm --filter @softmaple/playground typecheck now passes; fix is on cursor/lexical-eg-walker-ws-4f5f (#831).

Open in Web Open in Cursor 

TS2742 failed typecheck because defineWebSocketHandler's inferred
default export referenced a non-portable h3 path. Annotate handlers as
EventHandler from nitro/h3.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
apps/playground/src/routes/demo/online-collab-editor.tsx (1)

66-70: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the selected transport in room URLs.

If a user starts with ?transport=broadcast, both history updates remove that parameter. A reload or shared room URL then resolves the default WebSocket transport instead of BroadcastChannel.

Update the existing URL and set only its room parameter.

Proposed fix
- window.history.replaceState(
-   null,
-   "",
-   `${window.location.pathname}?room=${roomId}`,
- );
+ const url = new URL(window.location.href);
+ url.searchParams.set("room", roomId);
+ window.history.replaceState(null, "", url);

Apply the same change in handleJoinRoom.

Also applies to: 83-87

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/routes/demo/online-collab-editor.tsx` around lines 66 -
70, Update both history URL writes in the room-joining flow, including
handleJoinRoom, to modify the existing URL’s room parameter while preserving any
existing transport parameter. Avoid reconstructing the query string from only
pathname and roomId so broadcast transport remains encoded across reloads and
shared URLs.

Source: Coding guidelines

🧹 Nitpick comments (7)
apps/playground/e2e/lexical-eg-walker.spec.ts (1)

283-324: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Split this E2E specification.

This file reaches 324 lines. Extract the WebSocket and BroadcastChannel cases into dedicated specification files. Keep shared room helpers in a small utility module.

As per coding guidelines, “Keep modules under 200 lines; split larger modules into smaller ones.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/e2e/lexical-eg-walker.spec.ts` around lines 283 - 324, Split
the tests in the “Lexical EG-walker WebSocket cross-browser” describe block into
dedicated WebSocket and BroadcastChannel specification files, keeping each under
200 lines. Move shared helpers such as room setup and document assertions into a
small utility module, and update both specifications to reuse those helpers
without changing test behavior.

Source: Coding guidelines

apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts (2)

95-98: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Compare socket identity in the close listener.

The listener sets the shared socket to null without checking which socket fired the event. The current control flow prevents overlapping connections, so this is correct today. If a future change ever calls connect while a socket is still open, a late close event from the old socket clears the new socket and schedules a spurious reconnect.

Capture the socket in a local and compare identity.

♻️ Sketch
-    socket.addEventListener("close", () => {
-      socket = null;
-      scheduleReconnect();
-    });
+    const current = socket;
+    current.addEventListener("close", () => {
+      if (socket !== current) return;
+      socket = null;
+      scheduleReconnect();
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts`
around lines 95 - 98, Update the close listener in the socket connection flow to
capture the connected socket in a local variable and only clear the shared
socket and call scheduleReconnect when the closed socket is still the current
socket. Preserve the existing reconnect behavior for the active socket while
ignoring late close events from replaced sockets.

46-68: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add reconnect backoff and bound the outbound queue.

Two resilience gaps:

  1. scheduleReconnect always waits the same reconnectDelayMs, which defaults to 1000 ms. There is no backoff, no jitter, and no attempt cap. If the relay is down, every open tab retries once per second indefinitely.
  2. outboundQueue has no size limit. Line 120 pushes every message sent while the socket is down. A long disconnection in an active editing session grows the array without bound, and flushQueue then sends the whole backlog in one synchronous loop on reconnect.

Apply exponential backoff with jitter, and cap the queue length.

♻️ Sketch
 const DEFAULT_RECONNECT_DELAY_MS = 1_000;
+const MAX_RECONNECT_DELAY_MS = 30_000;
+const MAX_QUEUED_MESSAGES = 500;
@@
+  let attempt = 0;
   const scheduleReconnect = (): void => {
     if (closed || reconnectTimer !== null) return;
+    const base = Math.min(
+      reconnectDelayMs * 2 ** attempt,
+      MAX_RECONNECT_DELAY_MS,
+    );
+    attempt += 1;
     reconnectTimer = setTimeout(() => {
       reconnectTimer = null;
       connect();
-    }, reconnectDelayMs);
+    }, base * (0.5 + Math.random() * 0.5));
   };

Reset attempt = 0 inside the open listener.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts`
around lines 46 - 68, Update scheduleReconnect to track reconnect attempts,
apply capped exponential backoff with jitter, and stop scheduling after a
defined maximum attempt count; reset the attempt counter in the socket open
listener. Bound outboundQueue when enqueueing messages in the send path,
retaining only the permitted number of pending messages, and ensure reconnect
behavior remains unchanged after a successful connection.
apps/playground/src/modules/collab-transport/urls.test.ts (2)

61-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the reconnect and inbound-message paths.

The suite tests the outbound queue but not the other asynchronous behaviour of createWebSocketBroadcastChannel:

  • Reconnect after a close event, which needs vi.useFakeTimers to advance reconnectDelayMs.
  • The message listener, including that onmessage receives parsed JSON and that a malformed frame is dropped without throwing.
  • That channel.close() stops further reconnect attempts.

The existing fake socket already collects listeners, so each case needs only a dispatch and an assertion.

As per path instructions "Test async operations thoroughly using Vitest or Playwright".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/modules/collab-transport/urls.test.ts` around lines 61 -
104, Extend the websocket broadcast channel tests around
createWebSocketBroadcastChannel to cover reconnect scheduling after a close
event using vi.useFakeTimers and reconnectDelayMs, inbound message handling that
passes parsed JSON to onmessage, and malformed frames being ignored without
throwing. Also verify channel.close() prevents subsequent reconnect attempts,
reusing the existing fake socket listener registry and dispatching events
directly; restore fake timers after each test.

Source: Coding guidelines


15-22: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a wss: input case to the origin test.

toWebSocketOrigin is tested only with http: and https: inputs. It downgrades a wss: input to ws:, as noted in the comment on urls.ts lines 20-27. Add the case so the fix is locked in.

💚 Proposed addition
     expect(toWebSocketOrigin("https://playground.example/")).toBe(
       "wss://playground.example",
     );
+    expect(toWebSocketOrigin("wss://playground.example")).toBe(
+      "wss://playground.example",
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/modules/collab-transport/urls.test.ts` around lines 15 -
22, Add a `wss:` input assertion to the existing “maps http(s) origins to ws(s)”
test for `toWebSocketOrigin`, verifying that a secure WebSocket origin remains
`wss:` with its host and path preserved.
apps/playground/src/modules/collab-transport/urls.ts (1)

87-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify the null override semantics and the broadcast placeholder origin.

Three points:

  1. Line 100-102 uses ??, so an explicit null for docWsUrl, presenceWsUrl, or syncWsUrl falls back to the same-origin default. The option types declare string | null, which suggests null means "no endpoint". Narrow the option types to string | undefined, or handle null as an explicit disable.
  2. Line 88 passes the literal "http://localhost" into buildSameOriginEndpoints for broadcast mode. The broadcast branch returns all-null endpoints and never reads the origin. The argument is dead and misleading. Return the broadcast endpoints directly.
  3. Line 94 hardcodes the SSR fallback port 3000. The PR makes the Playwright port configurable. Keep the two in sync, or derive this value from the same source.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/modules/collab-transport/urls.ts` around lines 87 - 103,
Update the URL option types and fallback handling for docWsUrl, presenceWsUrl,
and syncWsUrl so null is not silently converted to a default—either narrow them
to string | undefined or preserve null as an explicit disabled endpoint. In the
broadcast branch, return the broadcast endpoints directly without passing the
unused localhost origin to buildSameOriginEndpoints. Replace the SSR
localhost:3000 fallback with the shared configurable Playwright port source.
apps/playground/src/env.ts (1)

19-24: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Validate the WebSocket override values as URLs.

z.string().min(1) accepts invalid input such as "not-a=url". These values are passed to new URL(baseUrl, ...), which throws at WebSocket construction time. Use z.url() so malformed WebSocket overrides fail during environment validation, and keep them limited to WebSocket schemes if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/env.ts` around lines 19 - 24, Update the
VITE_COLLAB_DOC_WS_URL, VITE_COLLAB_PRESENCE_WS_URL, and VITE_COLLAB_SYNC_WS_URL
schemas in the environment validation to use z.url() instead of only
z.string().min(1). Restrict accepted values to WebSocket schemes (ws or wss) if
supported by the existing validation conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/playground/playwright.config.ts`:
- Around line 45-46: Ensure the Playwright web server and Vite use a strict E2E
port by updating the Vite server configuration in vite.config.ts to enable
strictPort, or by adding the equivalent --strictPort flag to the command in the
Playwright configuration. Preserve the existing port and baseURL values.

In `@apps/playground/server/api/collab-doc.ts`:
- Line 80: Replace each direct default export of defineWebSocketHandler with a
typed const assignment followed by exporting that const. Apply the explicit
handler annotation in apps/playground/server/api/collab-doc.ts lines 80-80,
apps/playground/server/api/presence.ts lines 77-77, and
apps/playground/server/api/collab-sync.ts line 62, using the existing WebSocket
handler type so no internal h3 path appears in the inferred export type.
- Around line 38-46: Implement room lifecycle limits across all relays: in
apps/playground/server/api/collab-doc.ts:38-46, cap each RoomState.batches map
and remove the rooms entry when the final peer disconnects; in
apps/playground/server/api/collab-sync.ts:27-43, cap RoomState.events, delete
rooms on the last disconnect, and remove the unused peers field or maintain it
in the close hook; in apps/playground/server/api/presence.ts:177-193, delete the
rooms entry when the room’s user map becomes empty.
- Around line 51-65: Update parseDocMessage to validate knownBatchIds before
casting and returning the parsed message: require it to be an array containing
only non-empty strings, or apply equivalent non-array validation when the field
is represented as a Set. Preserve null for malformed messages so downstream new
Set(parsed.knownBatchIds ?? []) cannot receive numeric or object values.

In `@apps/playground/src/modules/collab-transport/urls.ts`:
- Around line 20-27: Update toWebSocketOrigin to preserve existing WebSocket
schemes: keep wss: as wss:, map https: to wss:, and map http: to ws: without
downgrading secure inputs. Remove the unused trimTrailingSlash helper and return
the URL origin directly.

---

Outside diff comments:
In `@apps/playground/src/routes/demo/online-collab-editor.tsx`:
- Around line 66-70: Update both history URL writes in the room-joining flow,
including handleJoinRoom, to modify the existing URL’s room parameter while
preserving any existing transport parameter. Avoid reconstructing the query
string from only pathname and roomId so broadcast transport remains encoded
across reloads and shared URLs.

---

Nitpick comments:
In `@apps/playground/e2e/lexical-eg-walker.spec.ts`:
- Around line 283-324: Split the tests in the “Lexical EG-walker WebSocket
cross-browser” describe block into dedicated WebSocket and BroadcastChannel
specification files, keeping each under 200 lines. Move shared helpers such as
room setup and document assertions into a small utility module, and update both
specifications to reuse those helpers without changing test behavior.

In `@apps/playground/src/env.ts`:
- Around line 19-24: Update the VITE_COLLAB_DOC_WS_URL,
VITE_COLLAB_PRESENCE_WS_URL, and VITE_COLLAB_SYNC_WS_URL schemas in the
environment validation to use z.url() instead of only z.string().min(1).
Restrict accepted values to WebSocket schemes (ws or wss) if supported by the
existing validation conventions.

In `@apps/playground/src/modules/collab-transport/urls.test.ts`:
- Around line 61-104: Extend the websocket broadcast channel tests around
createWebSocketBroadcastChannel to cover reconnect scheduling after a close
event using vi.useFakeTimers and reconnectDelayMs, inbound message handling that
passes parsed JSON to onmessage, and malformed frames being ignored without
throwing. Also verify channel.close() prevents subsequent reconnect attempts,
reusing the existing fake socket listener registry and dispatching events
directly; restore fake timers after each test.
- Around line 15-22: Add a `wss:` input assertion to the existing “maps http(s)
origins to ws(s)” test for `toWebSocketOrigin`, verifying that a secure
WebSocket origin remains `wss:` with its host and path preserved.

In `@apps/playground/src/modules/collab-transport/urls.ts`:
- Around line 87-103: Update the URL option types and fallback handling for
docWsUrl, presenceWsUrl, and syncWsUrl so null is not silently converted to a
default—either narrow them to string | undefined or preserve null as an explicit
disabled endpoint. In the broadcast branch, return the broadcast endpoints
directly without passing the unused localhost origin to
buildSameOriginEndpoints. Replace the SSR localhost:3000 fallback with the
shared configurable Playwright port source.

In `@apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts`:
- Around line 95-98: Update the close listener in the socket connection flow to
capture the connected socket in a local variable and only clear the shared
socket and call scheduleReconnect when the closed socket is still the current
socket. Preserve the existing reconnect behavior for the active socket while
ignoring late close events from replaced sockets.
- Around line 46-68: Update scheduleReconnect to track reconnect attempts, apply
capped exponential backoff with jitter, and stop scheduling after a defined
maximum attempt count; reset the attempt counter in the socket open listener.
Bound outboundQueue when enqueueing messages in the send path, retaining only
the permitted number of pending messages, and ensure reconnect behavior remains
unchanged after a successful connection.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c7329e4-095b-4429-8a87-29af238e0138

📥 Commits

Reviewing files that changed from the base of the PR and between 51195a5 and 3ec403a.

📒 Files selected for processing (20)
  • apps/playground/e2e/lexical-eg-walker.spec.ts
  • apps/playground/playwright.config.ts
  • apps/playground/server/README.md
  • apps/playground/server/api/collab-doc.ts
  • apps/playground/server/api/collab-sync.ts
  • apps/playground/server/api/presence.ts
  • apps/playground/src/env.ts
  • apps/playground/src/modules/collab-transport/urls.test.ts
  • apps/playground/src/modules/collab-transport/urls.ts
  • apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts
  • apps/playground/src/modules/lexical-eg-walker/LexicalEgWalkerDemo.tsx
  • apps/playground/src/modules/lexical-eg-walker/StatusRail.tsx
  • apps/playground/src/modules/lexical-eg-walker/presence.test.ts
  • apps/playground/src/modules/lexical-eg-walker/presence.ts
  • apps/playground/src/modules/lexical-eg-walker/transport.ts
  • apps/playground/src/modules/lexical-eg-walker/useLexicalRoom.ts
  • apps/playground/src/routes/demo/online-collab-editor.tsx
  • apps/playground/src/routes/index.tsx
  • apps/playground/vite.config.ts
  • docs/design/surface-bindings.md

Comment thread apps/playground/playwright.config.ts Outdated
Comment thread apps/playground/server/api/collab-doc.ts
Comment thread apps/playground/server/api/collab-doc.ts
Comment thread apps/playground/server/api/collab-doc.ts Outdated
Comment thread apps/playground/src/modules/collab-transport/urls.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
apps/playground/server/api/collab-sync.ts (1)

81-88: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Broken Authentication (CWE-287): Improper Authentication

Reachability: External

Keep userId advisory or bind it before the relay uses it.

collab-sync accepts userId only from the parsed message and uses it for room.peers, so clients can spoof another user. Either reject spoofed IDs against an authenticated peer identity or document that userId is advisory only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/server/api/collab-sync.ts` around lines 81 - 88, Update the
join/leave handling around the parsed userId usage so clients cannot alter
room.peers membership by spoofing another identity: validate parsed.userId
against the authenticated peer identity and reject mismatches before mutating or
publishing, or explicitly mark and treat userId as advisory throughout the
relay. Preserve the existing add/delete and publish behavior for valid messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/playground/server/api/collab-sync.ts`:
- Around line 81-88: Update the join/leave handling around the parsed userId
usage so clients cannot alter room.peers membership by spoofing another
identity: validate parsed.userId against the authenticated peer identity and
reject mismatches before mutating or publishing, or explicitly mark and treat
userId as advisory throughout the relay. Preserve the existing add/delete and
publish behavior for valid messages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: abe0b29c-618b-40c1-bdfc-18be9d000ec7

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec403a and 443961a.

📒 Files selected for processing (3)
  • apps/playground/server/api/collab-doc.ts
  • apps/playground/server/api/collab-sync.ts
  • apps/playground/server/api/presence.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/playground/server/api/presence.ts

Harden room lifecycle caps/cleanup, knownBatchIds validation, WS URL
resolution, reconnect backoff, and Playwright strictPort; split e2e
transport specs and extend unit coverage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/playground/playground-port.ts (1)

1-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename utility modules to camelCase.

Both new utility module paths use kebab case.

  • apps/playground/playground-port.ts#L1-L2: Rename the module to playgroundPort.ts and update its imports.
  • apps/playground/e2e/helpers/lexical-eg-walker.ts#L1-L7: Rename the module to lexicalEgWalker.ts and update spec imports.

As per coding guidelines, use camelCase filenames for utility modules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/playground-port.ts` around lines 1 - 2, Rename
apps/playground/playground-port.ts to playgroundPort.ts and update every import
referencing it; rename apps/playground/e2e/helpers/lexical-eg-walker.ts to
lexicalEgWalker.ts and update all spec imports accordingly. Preserve the
existing exports and behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/playground/playground-port.ts`:
- Around line 1-2: Rename apps/playground/playground-port.ts to
playgroundPort.ts and update every import referencing it; rename
apps/playground/e2e/helpers/lexical-eg-walker.ts to lexicalEgWalker.ts and
update all spec imports accordingly. Preserve the existing exports and behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 41d5d97b-553e-468d-a36a-61ff227426fd

📥 Commits

Reviewing files that changed from the base of the PR and between 443961a and 5b47a74.

📒 Files selected for processing (14)
  • apps/playground/e2e/helpers/lexical-eg-walker.ts
  • apps/playground/e2e/lexical-eg-walker-broadcast.spec.ts
  • apps/playground/e2e/lexical-eg-walker-websocket.spec.ts
  • apps/playground/e2e/lexical-eg-walker.spec.ts
  • apps/playground/playground-port.ts
  • apps/playground/playwright.config.ts
  • apps/playground/server/api/collab-doc.ts
  • apps/playground/server/api/collab-sync.ts
  • apps/playground/server/api/presence.ts
  • apps/playground/src/env.ts
  • apps/playground/src/modules/collab-transport/urls.test.ts
  • apps/playground/src/modules/collab-transport/urls.ts
  • apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts
  • apps/playground/src/routes/demo/online-collab-editor.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/playground/playwright.config.ts
  • apps/playground/server/api/collab-doc.ts
  • apps/playground/src/env.ts
  • apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts
  • apps/playground/src/modules/collab-transport/urls.ts
  • apps/playground/src/modules/collab-transport/urls.test.ts

Lead with Lexical × EG-walker WebSocket collab, drop TanStack marketing
cards, and update home e2e coverage for the new demo grid.
…e UI

Request batch repair when the document WebSocket reopens, publish
presence reconnecting/error states, and show degraded connection status
in the Lexical StatusRail (including sync paused hint).
@chromatic-com

chromatic-com Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Important

UI Tests need review – Review now

🟡 UI Tests: softmaple_packages/awareness: 38 visual and accessibility changes must be accepted as baselines
🟢 UI Review: softmaple_packages/awareness: 38 stories published -- no changes
Storybook icon Storybook Publish: softmaple_packages/awareness: 38 stories published

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts (2)

40-202: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Split this module below 200 lines.

Extract the reconnect or channel-wrapper logic into a focused module. The current file has 215 lines.

As per coding guidelines: “Keep modules under 200 lines; split larger modules into smaller ones.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts`
around lines 40 - 202, Split createWebSocketBroadcastChannel into focused
modules so the current file is under 200 lines. Extract either the reconnect
lifecycle helpers around connect, scheduleReconnect, and clearReconnect or the
returned channel wrapper around postMessage and close, while preserving the
existing connection, queue, and event-handler behavior.

Source: Coding guidelines


40-42: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use a camelCase utility filename.

Rename websocket-broadcast-channel.ts to a camelCase filename such as websocketBroadcastChannel.ts. Update its direct imports.

As per coding guidelines: “Use camelCase filenames for utility modules.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts`
around lines 40 - 42, Rename the utility module file from
websocket-broadcast-channel.ts to websocketBroadcastChannel.ts and update every
direct import or reference to the new camelCase filename, keeping
createWebSocketBroadcastChannel unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/playground/src/modules/lexical-eg-walker/StatusRail.tsx`:
- Around line 196-204: Update the reconnect warning condition in StatusRail so
it is not rendered while connectionState is connecting; restrict it to
reconnecting, disconnected, or error states while preserving the existing
websocket requirement and message.

In `@packages/awareness/src/adapters/websocket/websocket.ts`:
- Around line 239-241: Update handleClose to call
clearConnectionTimeout(internal) after stopHeartbeat and before beginReconnect,
ensuring the closed socket’s pending connection timeout cannot affect the
replacement connection.

---

Outside diff comments:
In `@apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts`:
- Around line 40-202: Split createWebSocketBroadcastChannel into focused modules
so the current file is under 200 lines. Extract either the reconnect lifecycle
helpers around connect, scheduleReconnect, and clearReconnect or the returned
channel wrapper around postMessage and close, while preserving the existing
connection, queue, and event-handler behavior.
- Around line 40-42: Rename the utility module file from
websocket-broadcast-channel.ts to websocketBroadcastChannel.ts and update every
direct import or reference to the new camelCase filename, keeping
createWebSocketBroadcastChannel unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ef928776-eed4-4c7e-88ce-d1d038cd0aac

📥 Commits

Reviewing files that changed from the base of the PR and between 5b47a74 and 5b6bbd8.

📒 Files selected for processing (13)
  • apps/playground/e2e/home.spec.ts
  • apps/playground/src/modules/collab-transport/urls.test.ts
  • apps/playground/src/modules/collab-transport/websocket-broadcast-channel.ts
  • apps/playground/src/modules/lexical-eg-walker/LexicalEgWalkerDemo.tsx
  • apps/playground/src/modules/lexical-eg-walker/StatusRail.tsx
  • apps/playground/src/modules/lexical-eg-walker/persistence/channel.test.ts
  • apps/playground/src/modules/lexical-eg-walker/persistence/channel.ts
  • apps/playground/src/modules/lexical-eg-walker/persistence/coordinator.test.ts
  • apps/playground/src/modules/lexical-eg-walker/persistence/coordinator.ts
  • apps/playground/src/modules/lexical-eg-walker/useLexicalRoom.test.ts
  • apps/playground/src/routes/index.tsx
  • packages/awareness/src/adapters/websocket/websocket.test.ts
  • packages/awareness/src/adapters/websocket/websocket.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/playground/src/modules/lexical-eg-walker/LexicalEgWalkerDemo.tsx

Comment thread apps/playground/src/modules/lexical-eg-walker/StatusRail.tsx
Comment thread packages/awareness/src/adapters/websocket/websocket.ts
Hide the sync-paused badge during initial connecting, clear presence
connection timeouts on close, and rename/split the WebSocket broadcast
channel helpers into camelCase modules under 200 lines.
@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Coverage Report for packages/awareness

Status Category Percentage Covered / Total
🟢 Lines 98.97% (🎯 90%) 1732 / 1750
🟢 Statements 97.4% (🎯 90%) 1836 / 1885
🟢 Functions 99.47% (🎯 90%) 377 / 379
🟢 Branches 92.05% (🎯 90%) 927 / 1007
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/awareness/src/adapters/websocket/websocket.ts 94.59% 88.88% 100% 96.45% 76-77, 100, 129, 201, 230-234
Generated in workflow #824 for commit 2f6a6b3 by the Vitest Coverage Report Action

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/playground/src/modules/collab-transport/websocketBroadcastChannelLifecycle.ts (1)

78-94: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Close the previous socket when connect runs again.

connect is part of the public WebSocketLifecycle interface. It replaces socket without closing the previous socket. The current caller in websocketBroadcastChannel.ts calls connect() once, and the reconnect timer only fires after a close event, so no leak occurs today. If a future caller calls connect() while a socket is open, the old socket stays open and its listeners return early because of the socket !== currentSocket guard, so the leak is silent.

♻️ Proposed guard
   const connect = (): void => {
     if (options.isClosed()) return;
     clearReconnect();
+    const previous = socket;
+    socket = null;
+    previous?.close();
     if (options.getConnectionState() !== "reconnecting") {
       options.setConnectionState("connecting");
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/playground/src/modules/collab-transport/websocketBroadcastChannelLifecycle.ts`
around lines 78 - 94, Update the public connect function to close the existing
socket before replacing it with a newly created WebSocket. Preserve the current
socket state and reconnect behavior, and ensure the previous socket is closed
only when one is already assigned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/playground/src/modules/collab-transport/websocketBroadcastChannelLifecycle.ts`:
- Around line 96-127: Add a connection-timeout timer in connect for sockets that
remain in the connecting state, using the existing connectionTimeoutMs
configuration. Clear the timer when the socket opens or closes; on expiry,
verify the socket is still current and active, close it, and invoke
scheduleReconnect() so queued messages and connection state recover.

---

Nitpick comments:
In
`@apps/playground/src/modules/collab-transport/websocketBroadcastChannelLifecycle.ts`:
- Around line 78-94: Update the public connect function to close the existing
socket before replacing it with a newly created WebSocket. Preserve the current
socket state and reconnect behavior, and ensure the previous socket is closed
only when one is already assigned.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 254cd607-6c50-4854-ac7f-ecee93317026

📥 Commits

Reviewing files that changed from the base of the PR and between 5b6bbd8 and 7bce8d5.

📒 Files selected for processing (8)
  • apps/playground/src/modules/collab-transport/urls.test.ts
  • apps/playground/src/modules/collab-transport/websocketBroadcastChannel.ts
  • apps/playground/src/modules/collab-transport/websocketBroadcastChannelLifecycle.ts
  • apps/playground/src/modules/lexical-eg-walker/StatusRail.tsx
  • apps/playground/src/modules/lexical-eg-walker/persistence/channel.test.ts
  • apps/playground/src/modules/lexical-eg-walker/transport.ts
  • packages/awareness/src/adapters/message-edge-cases.test.ts
  • packages/awareness/src/adapters/websocket/websocket.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/awareness/src/adapters/websocket/websocket.ts
  • apps/playground/src/modules/collab-transport/urls.test.ts
  • apps/playground/src/modules/lexical-eg-walker/persistence/channel.test.ts
  • apps/playground/src/modules/lexical-eg-walker/StatusRail.tsx

Arm a connectionTimeoutMs timer while sockets stay connecting, clear it
on open/close, and close any existing socket before connect() replaces it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/playground/src/modules/collab-transport/urls.test.ts`:
- Around line 316-320: Update the lifecycle.connect test around webSocketFactory
and sockets to record the factory invocation and existing socket close calls,
then assert the first socket’s close occurs before the second webSocketFactory
invocation. Preserve the existing socket-count and replacement assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d579cd8b-c8ce-4dae-b909-d4e40db5912c

📥 Commits

Reviewing files that changed from the base of the PR and between 5b6bbd8 and 94adc3d.

📒 Files selected for processing (8)
  • apps/playground/src/modules/collab-transport/urls.test.ts
  • apps/playground/src/modules/collab-transport/websocketBroadcastChannel.ts
  • apps/playground/src/modules/collab-transport/websocketBroadcastChannelLifecycle.ts
  • apps/playground/src/modules/lexical-eg-walker/StatusRail.tsx
  • apps/playground/src/modules/lexical-eg-walker/persistence/channel.test.ts
  • apps/playground/src/modules/lexical-eg-walker/transport.ts
  • packages/awareness/src/adapters/message-edge-cases.test.ts
  • packages/awareness/src/adapters/websocket/websocket.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/playground/src/modules/lexical-eg-walker/persistence/channel.test.ts
  • packages/awareness/src/adapters/websocket/websocket.ts
  • apps/playground/src/modules/lexical-eg-walker/StatusRail.tsx
  • apps/playground/src/modules/collab-transport/websocketBroadcastChannelLifecycle.ts
  • apps/playground/src/modules/lexical-eg-walker/transport.ts
  • packages/awareness/src/adapters/message-edge-cases.test.ts
  • apps/playground/src/modules/collab-transport/websocketBroadcastChannel.ts

Comment thread apps/playground/src/modules/collab-transport/urls.test.ts
Record lifecycle connect ordering so the first socket close is verified
to happen before the second webSocketFactory invocation.
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.

2 participants