Skip to content

feat(ws): browser client cut-over to /rpc-ws (#326, PR 3/3 of #314) - #349

Merged
omridevk merged 6 commits into
mainfrom
ws-client-326
Aug 8, 2026
Merged

feat(ws): browser client cut-over to /rpc-ws (#326, PR 3/3 of #314)#349
omridevk merged 6 commits into
mainfrom
ws-client-326

Conversation

@omridevk

@omridevk omridevk commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Closes #326. Final PR of the #314 rollout: the widget's browser rpc moves from fetch/SSE to one WebSocket per (tab, apiBase), killing the 6-connections-per-host starvation.

DRAFT — one rework commit still landing (standalone-override test, lockfile hygiene, probe error-state assertion, recorder frame-boundary hardening). The cut-over commit is complete and reviewable.

Design (binding spec on #326 + user amendment on #314)

  • Boot-time ws probe (bounded) → sticky per-(tab, apiBase) fetch/SSE fallback; NO UA sniffing; mid-session drops never switch transport; SSE-fails-too → widget error state; config override.
  • partysocket (approved dep) behind a thin delegate overriding ONLY readyState (stale-CLOSED-during-backoff fix); close always forwards; explicit ~250ms/1s backoff.
  • Unary resilience = oRPC ClientRetryPlugin re-issue; iterators survive reconnect + server restart with NO outer retry loops.
  • Rebind = function url provider + reconnect() via oRPC DynamicLink; one RPCLink per socket (WeakMap); registry identity on a versioned globalThis var.
  • Teardown: disposeSocket dispatches a close Event through the socket's EventTarget so oRPC's peer settles all pending calls (also covers partysocket's no-close-event-during-backoff hang); a disposed delegate classifies frames — teardown frames no-op, fresh requests fail fast. (Upstream oRPC bug identified: unguarded async abort listener in @orpc/standard-server-peer@1.14.7.)
  • Node consumers (CLI/testkit) STAY on fetch.

Evidence

Codex full-branch adversarial review: frame classification proven sound vs oRPC 1.14.7 frame taxonomy, no lifecycle leaks, probe bounded + sticky. Rework items from that review are the pending commit.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added configurable browser transport modes: automatic, WebSocket-only, or fetch/SSE.
    • Browser tabs now share a connection, improving reliability when multiple tabs are open.
    • Added automatic fallback to fetch/SSE when WebSockets are unavailable.
    • Added reconnection and recovery after connection loss, rebinding, and widget remounting.
    • Improved connection cleanup when widgets are unmounted.
  • Documentation
    • Documented transport selection, fallback behavior, reconnection, and network considerations.
  • Tests
    • Added coverage for multi-tab usage, transport selection, recovery, and clean teardown.

omridevk and others added 2 commits August 8, 2026 19:02
…#314)

One websocket per (tab, apiBase), owned by a versioned globalThis registry in
@conciv/contract. A DynamicLink is the single selection point: at boot it dials
/rpc-ws with a bounded open timeout and, on failure, sticks to fetch/SSE for that
tab with a console breadcrumb. widget.transport pins either transport explicitly.
makeRpcClient stays on fetch for the CLI, testkit and node integration tests;
makeExtRpcClient resolves the shared connection in the browser and keeps its
fetch form for node fixtures.

- partysocket wraps the socket; the oRPC link gets a thin delegate that overrides
  only readyState (partysocket reports the stale CLOSED socket during backoff) and
  forwards close unchanged, so iterators never stall silently.
- ClientRetryPlugin owns call re-issue; the wrapper owns socket reconnect. The
  recorder's hand-rolled 1s control backoff is gone, so the two no longer stack.
- Boot no longer blocks first paint on navigation.get: the widget renders with an
  empty history cache and applies the restored entry only if no local write landed
  first; the pending read is cancelled on rebind and dispose.
- Six tabs in one browser context now each get one socket, zero rpc over http and
  a working chat round trip. On the fetch transport the same gate cannot even mount
  the sixth widget.
- Test estate: rpc observers attach at page creation (a late observer cannot see an
  already-open socket), proxy counts websocket upgrades alongside http, and the
  whiteboard drag-batching test counts procedure calls through the rpc observer
  instead of http requests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… coverage (#326)

- entry-standalone.tsx threads settings.transport into makeBrowserRpcClient
  and stamps window.__CONCIV_API_BASE__ once at boot so every downstream
  ext-rpc client (composer terminal, highlight) resolves the same apiBase
  instead of re-reading a URL the router has since rewritten.
- a disposed rpc connection drops peer control frames instead of throwing
  (mount-impl.tsx, browser-transport.ts) plus the forced-drop/rebind ITs
  that prove it.
- new packages/embed-style playwright IT (apps/conciv/test/transport-standalone.it.test.ts)
  proves the standalone entry: pinned fetch never opens a websocket, pinned
  websocket never falls back to fetch — driven against the built standalone
  page, not just the embed widget.
- pnpm-lock.yaml rebuilt from the pre-drift base commit with only
  partysocket@1.3.0 + its event-target-polyfill transitive added; the
  unrelated floating @tanstack/* re-resolution is gone.
- recorder flush-socket.it.test.ts now measures actual outbound ws frame
  bytes at the socket.send boundary (post oRPC envelope encode) against
  @conciv/serve's real DEFAULT_MAX_PAYLOAD_BYTES, not the client's own
  MAX_FLUSH_BYTES constant compared against itself pre-encode.
- extracted the duplicated http-rpc-request-tracking snippet into
  @conciv/extension-testkit's httpRpcRequestUrls so the new standalone IT
  does not triple a fragment fallow already saw twice in embed's tests.

Probe error-state item from the #326 rework list is NOT included: the
"SSE also fails at boot -> widget connection-error state" promised in the
canonical spec has no implementation anywhere in the app (no meta/context
signal, no UI) to write an honest test against. Flagged for a follow-up
design decision rather than inventing product UI here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds shared browser RPC transport selection with WebSocket pooling, fetch fallback, explicit preferences, lifecycle teardown, rebinding, extension integration, and transport-focused integration tests.

Changes

Browser RPC transport cutover

Layer / File(s) Summary
Transport and client contracts
packages/contract/..., packages/core/test/api/rpc-ws.it.test.ts
Adds cached browser connections, WebSocket probing, fetch fallback, lifecycle handling, browser client factories, and per-call session context coverage.
Application and widget lifecycle
apps/conciv/..., packages/embed/src/...
Parses transport preferences, wires browser clients, restores navigation asynchronously, and closes connections during teardown and rebinding.
Extension and stream consumers
packages/extension/..., packages/extensions/..., packages/extension-testkit/fixtures/...
Routes browser extension RPC through shared connections and moves retry handling into request-level consumers.
Test infrastructure
apps/conciv/test/helpers/*, packages/embed/test/helpers/*, packages/extension-testkit/src/*
Adds local servers, HTTP/WebSocket proxies, static app hosting, RPC observers, shared Playwright helpers, and a typed ping fixture.
Transport and lifecycle validation
packages/embed/test/*, apps/conciv/test/*, packages/extension-testkit/test/*
Covers six-tab pooling, fallback, forced reconnects, unmount cleanup, rebinding, transport reprobes, standalone startup, and extension RPC observation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Widget
  participant BrowserRpcConnection
  participant RPCWebSocket
  participant FetchSSE
  Widget->>BrowserRpcConnection: request RPC link
  BrowserRpcConnection->>RPCWebSocket: probe /rpc-ws
  alt WebSocket available
    RPCWebSocket-->>BrowserRpcConnection: accept connection
    BrowserRpcConnection-->>Widget: return WebSocket link
  else WebSocket unavailable
    BrowserRpcConnection->>FetchSSE: select fetch/SSE
    FetchSSE-->>BrowserRpcConnection: return fetch link
    BrowserRpcConnection-->>Widget: return fetch link
  end
Loading

Possibly related issues

  • #339 — The PR adds RPC observer infrastructure and transport-focused tests that match the issue objective.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers shared sockets, rebinding, teardown, recorder limits, and six-tab tests, but uses runtime probing and fallback contrary to [#326]. Align transport selection with [#326] by using statically selected fetch/SSE fallback per environment instead of runtime try-then-fallback.
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 (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the browser WebSocket client cut-over to /rpc-ws.
Out of Scope Changes check ✅ Passed The changes support the browser transport cut-over, teardown, recorder handling, standalone configuration, and related test infrastructure.
✨ 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 ws-client-326

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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

Copilot AI 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.

Pull request overview

Moves browser RPC traffic from fetch/SSE to a shared WebSocket transport, addressing per-origin HTTP connection starvation.

Changes:

  • Adds shared, reconnecting WebSocket transport with probing and fetch fallback.
  • Integrates transport selection, rebind behavior, and asynchronous navigation restoration.
  • Adds browser integration coverage for transport selection, reconnection, and multi-tab usage.

Reviewed changes

Copilot reviewed 36 out of 37 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
.changeset/ws-browser-client-cutover.md Documents the transport cutover.
apps/conciv/src/data/settings.ts Adds transport configuration parsing.
apps/conciv/src/entry-standalone.tsx Moves standalone RPC to the browser client.
apps/conciv/test/helpers/fake-core.ts Pins fake-core tests to fetch.
apps/conciv/test/settings.test.ts Covers the default transport setting.
packages/contract/package.json Adds PartySocket.
packages/contract/src/browser-transport.ts Implements shared browser transport management.
packages/contract/src/client.ts Adds browser, deferred, and rebindable clients.
packages/contract/src/index.ts Exports browser transport APIs.
packages/contract/tsconfig.json Enables DOM types.
packages/embed/README.md Documents transport behavior and overrides.
packages/embed/src/mount-impl.tsx Integrates browser transport and nonblocking boot.
packages/embed/src/navigation-storage.ts Restores navigation asynchronously.
packages/embed/test/connection-pool.it.test.ts Adds the six-tab regression gate.
packages/embed/test/embed.it.test.ts Installs RPC observers before navigation.
packages/embed/test/helpers/navigation.ts Shares per-page RPC observers.
packages/embed/test/helpers/proxy.ts Adds WebSocket blocking and drop controls.
packages/embed/test/rebind.it.test.ts Updates rebind assertions for WebSockets.
packages/embed/test/transport-selection.it.test.ts Tests probe, fallback, and override behavior.
packages/extension/src/client-host.ts Uses browser RPC for source opening.
packages/extension/src/ext-rpc.ts Shares browser connections with extension RPC.
packages/extension/src/index.ts Updates extension RPC type exports.
packages/extensions/recorder/src/client/boot.ts Uses RPC retry for the control stream.
packages/extensions/whiteboard/src/client/change-feed.ts Moves reconnect handling into call context.
packages/extensions/whiteboard/test/canvas-drag-batching.it.test.ts Observes RPC calls across transports.
pnpm-lock.yaml Records dependency graph changes.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (1)

pnpm-lock.yaml:921

  • The only manifest dependency change is partysocket, but this lockfile regeneration also upgrades the React Router/Start stack and rewrites unrelated Hono, Vitest, and OXC resolutions. That unrelated churn contradicts the stated lockfile-hygiene rework and makes this transport change carry unreviewed dependency updates. Preserve the existing resolutions and add only the partysocket dependency graph.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +150 to +153
close: () => {
state.open = false
socket.close()
},
Comment on lines +189 to +190
const existing = connections.get(key)
if (existing) return existing
Comment thread apps/conciv/src/entry-standalone.tsx Outdated
const params = new URLSearchParams(window.location.search)
const router = createConcivRouter({
rpc: makeRpcClient(params.get('core') ?? ''),
rpc: makeBrowserRpcClient(params.get('core') ?? ''),
export type NavigationStorage = WebStorage & {restored: Promise<void>; dispose: () => void}

export function makeNavigationStorage(rpc: RpcClient, onRestore: (href: string) => void): NavigationStorage {
const state = {cache: null as string | null, lastStamp: 0, wroteLocally: false, cancelled: false}

describe('six widget tabs sharing one browser connection pool', () => {
it('gives every tab one rpc websocket, no rpc over http, and a working chat round trip in the last tab', async () => {
const context = await browser.newContext()
Comment on lines 127 to +128
const disposers = [
storage.dispose,
…bes, honest socket state (#326)

- unmount now closes the (tab, apiBase) connection and drops its registry
  entry, so partysocket and its reconnect timers no longer outlive the widget;
  a later mount re-creates the connection through the same registry and runs
  the full transport probe again.
- rebind to the base the widget is already on is no longer a no-op: it drops
  the connection so the next call re-probes, which is what makes the #314
  "error-retry re-runs the FULL probe" amendment reachable from the handle.
- socketDelegate reports partysocket's real readyState via shouldReconnect
  (open / connecting-while-it-will-reconnect / closed) instead of calling
  every non-open state CONNECTING, so oRPC fails a send fast rather than
  awaiting an open that is never coming.
- disposeSocket no longer double-dispatches close: partysocket's close()
  already emits one synchronously, so the synthetic event is now only used
  for the branches where it emits none (no socket dialled, or already
  closing/closed).
- navigation-storage keeps an explicit state type instead of an `as` cast.
- embed ITs: shared handle/chat helpers, a toggleable upgrade block in the
  test proxy, plus the two new behaviour tests (unmount closes the socket and
  remount rides a fresh one; a fetch-fallback tab rides the websocket after a
  same-base rebind once upgrades work again).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omridevk

omridevk commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Copilot review triage (fixes in 7dbb9645 and 969fd151):

Fixed

  • browser-transport.ts:190 — sticky fetch decision was uninvalidatable, so a retry could never re-probe. rebind no longer early-returns when the base is unchanged, so handle.rebind(currentBase) drops the registry entry and the next call re-probes. Test: rebind.it.test.ts → "re-runs the transport probe", failing-first (expected 'fetch' to be 'websocket') with a runtime-toggleable upgrade-blocking proxy.
  • mount-impl.tsx:128 — unmount left the socket + reconnect timers alive for the tab. Fixed with no ref-counting: a mounted widget is 1:1 with the tab connection (the single window.__CONCIV_PAGE_DRIVER__ stamp, createConciv's one-at-a-time state machine, and the data-conciv-script-root guard all make multi-mount unsupported), and rebind has always closed the shared entry with zero bookkeeping — unmount closing is the same operation. Test asserts the socket closes within 5s of unmount() and a remount dials a fresh one.
  • navigation-storage.ts:20as cast replaced with an explicit NavigationStorageState type.

Already fixed before this review landed (both in 7dbb9645)

  • Close-during-backoff hang: disposeSocket replaced the bare socket.close() in both connection paths, so the peer always sees a terminal event. Refined further in 969fd151: partysocket's own close() dispatches close synchronously, so we now synthesise one only for its two no-event early-return branches.
  • Standalone transport override: entry-standalone.tsx parses settings once, stamps window.__CONCIV_API_BASE__, and passes {transport} into makeBrowserRpcClient.

Not changing

  • connection-pool.it.test.ts using browser.newContext() is the sanctioned exception to the newPage() rule: six tabs must share ONE context, because a shared connection pool is precisely what the gate measures. newPage() would make the test prove nothing.

Also in 969fd151, a partysocket make-vs-use audit: readyState now derives from the library's own shouldReconnect instead of flattening every non-OPEN state to CONNECTING; send-buffering, backoff and retryCount confirmed native (we re-derive none of them). Follow-up filed: #352 (second assistant reply missing after a generation change — pre-existing, reproduced on the pre-fix build).

🤖 Generated with Claude Code

omridevk and others added 2 commits August 8, 2026 22:52
Resolve rebind.it.test.ts conflict: keep main's fuller "delivers the
next turn" assertions (title + post-rebind message-count checks) on
top of our ws-transport rebind test helpers (observedPage,
sendChatMessage, rebindHandle, trafficCount).
…326)

The extension testkit navigated its page inside launch(), so a test that
called observeRpc() afterwards attached page.on('websocket') after the
widget's shared rpc socket was already open and saw nothing. Under the
old fetch transport every call was a fresh HTTP request a late observer
still caught, which is why canvas-drag-batching only broke once the
browser rpc client started probing websockets.

getExtensionTestApi now opens every page (including secondClient) with a
memoized per-page observer attached before goto, embed's duplicate
helper delegates to it, and the ping fixture moved out of test/ so the
test host rebuilds when it changes (build inputs exclude test/**).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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: 6

🧹 Nitpick comments (3)
apps/conciv/test/settings.test.ts (1)

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

Cover all transport normalization branches.

Line 11 checks only the default value. Add assertions for explicit websocket, explicit fetch, and an invalid value that falls back to auto. These values control makeBrowserRpcClient in apps/conciv/src/entry-standalone.tsx.

Suggested assertions
+  it('normalizes transport preferences', () => {
+    expect(parseConcivSettings('{"transport":"websocket"}').transport).toBe('websocket')
+    expect(parseConcivSettings('{"transport":"fetch"}').transport).toBe('fetch')
+    expect(parseConcivSettings('{"transport":"invalid"}').transport).toBe('auto')
+  })
🤖 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/conciv/test/settings.test.ts` at line 11, Add test coverage in the
settings test around the existing transport assertion for all normalization
branches: explicit websocket, explicit fetch, and an invalid value falling back
to auto. Keep the assertions aligned with the transport value consumed by
makeBrowserRpcClient.
packages/contract/src/browser-transport.ts (2)

144-151: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a larger reconnect ceiling.

MAX_RECONNECT_DELAY_MS is 1000. If the core server stays down, each tab retries about once per second indefinitely. With six tabs this produces sustained connection attempts. A higher ceiling, for example 5000-10000 ms, reduces load while keeping recovery fast.

🤖 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 `@packages/contract/src/browser-transport.ts` around lines 144 - 151, Increase
the MAX_RECONNECT_DELAY_MS ceiling used by reconnectingSocket to a substantially
larger value, such as 5000–10000 ms, while preserving the existing exponential
reconnection behavior and other timing constants.

75-80: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard JSON.parse against non-JSON string frames.

isPeerRequestFrame parses data without error handling. If a non-JSON string frame reaches send after the connection is closed, JSON.parse throws a SyntaxError instead of the intended CLOSED_CONNECTION_MESSAGE error. Callers then see a misleading failure.

♻️ Proposed fix
 function isPeerRequestFrame(data: string | ArrayBufferLike | Blob | ArrayBufferView): boolean {
   if (typeof data !== 'string') return true
-  const frame: unknown = JSON.parse(data)
-  if (typeof frame !== 'object' || frame === null) return true
-  return !('t' in frame)
+  try {
+    const frame: unknown = JSON.parse(data)
+    if (typeof frame !== 'object' || frame === null) return true
+    return !('t' in frame)
+  } catch {
+    return true
+  }
 }
🤖 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 `@packages/contract/src/browser-transport.ts` around lines 75 - 80, Update
isPeerRequestFrame to guard JSON.parse with error handling for non-JSON string
frames, returning the existing peer-request classification that allows send to
produce CLOSED_CONNECTION_MESSAGE instead of propagating SyntaxError. Preserve
the current handling for valid JSON, non-string data, null, and non-object
frames.
🤖 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/conciv/test/helpers/static-app.ts`:
- Around line 26-30: Update the candidate selection in the static app request
handler to use statSync(candidate).isFile() rather than existsSync(candidate),
selecting candidate only when it is a regular file and otherwise falling back to
index.html.

In `@packages/contract/src/browser-transport.ts`:
- Around line 82-91: Update disposeSocket to account for the socket’s readyState
before closing: when the underlying socket is already CLOSING, retain the close
listener and avoid dispatching the synthetic close event, allowing the pending
native close to be observed; only remove the listener and synthesize close when
no later close event can arrive. Preserve idempotent close handling for OPEN and
already CLOSED sockets.

In `@packages/contract/src/client.ts`:
- Around line 45-50: Update the guard in bind to check state.base !== null,
matching bound(), so an empty API base is treated as already bound and
subsequent bind calls throw instead of replacing state.ready.

In `@packages/embed/README.md`:
- Around line 17-19: Remove the connection-error and retry behavior claim from
the README section describing WebSocket and fetch/SSE failures, leaving only
behavior that is currently implemented.

In `@packages/embed/src/navigation-storage.ts`:
- Around line 27-32: Update the navigation write setup in the storage
implementation to retain the Debouncer returned by debounce, guard its callback
with state.cancelled, and cancel that debouncer during storage.dispose(). Ensure
pending writes are skipped after disposal and cannot use a re-bound rpc client.

In `@packages/embed/test/connection-pool.it.test.ts`:
- Line 67: Add a concise comment at the browser.newContext() call explaining
that the test intentionally creates six pages within one BrowserContext, and
that using browser.newPage() would isolate them and alter the behavior being
tested.

---

Nitpick comments:
In `@apps/conciv/test/settings.test.ts`:
- Line 11: Add test coverage in the settings test around the existing transport
assertion for all normalization branches: explicit websocket, explicit fetch,
and an invalid value falling back to auto. Keep the assertions aligned with the
transport value consumed by makeBrowserRpcClient.

In `@packages/contract/src/browser-transport.ts`:
- Around line 144-151: Increase the MAX_RECONNECT_DELAY_MS ceiling used by
reconnectingSocket to a substantially larger value, such as 5000–10000 ms, while
preserving the existing exponential reconnection behavior and other timing
constants.
- Around line 75-80: Update isPeerRequestFrame to guard JSON.parse with error
handling for non-JSON string frames, returning the existing peer-request
classification that allows send to produce CLOSED_CONNECTION_MESSAGE instead of
propagating SyntaxError. Preserve the current handling for valid JSON,
non-string data, null, and non-object frames.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 13a692a8-acf6-4583-ab57-9606bd180339

📥 Commits

Reviewing files that changed from the base of the PR and between 78977f0 and 18803d9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (53)
  • .changeset/ws-browser-client-cutover.md
  • .changeset/ws-connection-teardown.md
  • apps/conciv/src/data/settings.ts
  • apps/conciv/src/entry-standalone.tsx
  • apps/conciv/test/helpers/fake-core.ts
  • apps/conciv/test/helpers/listen-local.ts
  • apps/conciv/test/helpers/proxy.ts
  • apps/conciv/test/helpers/static-app.ts
  • apps/conciv/test/settings.test.ts
  • apps/conciv/test/transport-standalone.it.test.ts
  • packages/contract/package.json
  • packages/contract/src/browser-transport.ts
  • packages/contract/src/client.ts
  • packages/contract/src/index.ts
  • packages/contract/tsconfig.json
  • packages/core/test/api/rpc-ws.it.test.ts
  • packages/embed/README.md
  • packages/embed/src/mount-impl.tsx
  • packages/embed/src/navigation-storage.ts
  • packages/embed/test/connection-pool.it.test.ts
  • packages/embed/test/create-conciv.it.test.ts
  • packages/embed/test/embed.it.test.ts
  • packages/embed/test/forced-drop.it.test.ts
  • packages/embed/test/helpers/chat.ts
  • packages/embed/test/helpers/handle.ts
  • packages/embed/test/helpers/navigation.ts
  • packages/embed/test/helpers/proxy.ts
  • packages/embed/test/rebind.it.test.ts
  • packages/embed/test/transport-selection.it.test.ts
  • packages/extension-testkit/fixtures/ping/client.tsx
  • packages/extension-testkit/fixtures/ping/router.ts
  • packages/extension-testkit/fixtures/ping/server.ts
  • packages/extension-testkit/package.json
  • packages/extension-testkit/src/get-extension-test-api.ts
  • packages/extension-testkit/src/launch.ts
  • packages/extension-testkit/src/rpc-observer.ts
  • packages/extension-testkit/test/boot-server.it.test.ts
  • packages/extension-testkit/test/call-tool.it.test.ts
  • packages/extension-testkit/test/fixtures/ping/client.tsx
  • packages/extension-testkit/test/page-rpc-observer.it.test.ts
  • packages/extension-testkit/test/session.it.test.ts
  • packages/extension-testkit/test/smoke.it.test.ts
  • packages/extension-testkit/tsconfig.json
  • packages/extension-testkit/vite.test-host.config.ts
  • packages/extension/src/client-host.ts
  • packages/extension/src/ext-rpc.ts
  • packages/extension/src/index.ts
  • packages/extensions/recorder/package.json
  • packages/extensions/recorder/src/client/boot.ts
  • packages/extensions/recorder/test/flush-socket.it.test.ts
  • packages/extensions/whiteboard/src/client/change-feed.ts
  • packages/extensions/whiteboard/test/canvas-drag-batching.it.test.ts
  • packages/serve/src/serve.ts
💤 Files with no reviewable changes (1)
  • packages/extension-testkit/test/fixtures/ping/client.tsx

Comment thread apps/conciv/test/helpers/static-app.ts
Comment thread packages/contract/src/browser-transport.ts Outdated
Comment thread packages/contract/src/client.ts
Comment thread packages/embed/README.md Outdated
Comment on lines 27 to 32
const write = debounce(
(state: NavigationState, updatedAt: number) => {
void rpc.navigation.set({...state, updatedAt}).catch(() => {})
(navigation: NavigationState, updatedAt: number) => {
void rpc.navigation.set({...navigation, updatedAt}).catch(() => {})
},
{wait: WRITE_DELAY_MS},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the debounce import and its declared API.
rg -n 'debounce' packages/embed/src/navigation-storage.ts
rg -n --type=json '"`@tanstack/pacer`"|"debounce"' -g 'package.json' | head -20
fd -t d 'pacer' node_modules --max-depth 3

Repository: conciv-dev/conciv

Length of output: 227


🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- navigation-storage.ts ---'
cat -n packages/embed/src/navigation-storage.ts

printf '%s\n' '--- mount-impl.tsx relevant calls ---'
rg -n -C 8 'storage\.dispose|closeConnection|rebind' packages/embed/src/mount-impl.tsx

printf '%s\n' '--- debounce package declarations and versions ---'
rg -n -C 3 '"`@tanstack/pacer`"|`@tanstack/pacer`|debounce\(' \
  package.json pnpm-lock.yaml packages pnpm-workspace.yaml 2>/dev/null | head -200

printf '%s\n' '--- available pacer files ---'
fd -i 'pacer' . --type f --exclude node_modules --exclude dist | head -100

Repository: conciv-dev/conciv

Length of output: 15848


🌐 Web query:

@tanstack/pacer 0.21.1 debounce cancel API

💡 Result:

In TanStack Pacer, the mechanism for stopping execution depends on whether you are using the synchronous Debouncer or the asynchronous AsyncDebouncer [1][2]. Synchronous Debouncing The Debouncer class provides a cancel method to stop any pending execution [1][3]. debouncer.cancel // Clears the pending timeout and resets internal state [3] Asynchronous Debouncing For AsyncDebouncer, there are two distinct methods for handling execution flow, as asynchronous work often involves in-flight promises that may need to be aborted [4][2]. 1. cancel: This method cancels any pending execution that has not yet started [4][2]. It does not affect any execution already in progress [4][2]. debouncer.cancel // Cancels pending execution, does not abort in-flight work [4][2] 2. abort: This method aborts all ongoing executions using an internal AbortController [4][2]. It does not cancel pending executions that have not yet started [4][2]. To utilize this effectively, you must pass the abort signal provided by the debouncer to your asynchronous operation [2]. const debouncer = new AsyncDebouncer(async (...) => { const signal = debouncer.getAbortSignal; // Access the signal const response = await fetch(url, { signal }); // Pass to operation return response.json; }, options); debouncer.abort; // Aborts the in-flight fetch operation [2] Summary of Differences Method | Scope | Effect --- | --- | --- cancel | Pending (not yet started) | Clears the timeout or queue, preventing future execution [3][4][2]. abort | In-flight (currently running) | Signals underlying async operations (like fetch) to stop [4][2]. Framework hooks (e.g., useDebouncer, useAsyncDebouncedCallback) typically handle cleanup automatically by calling cancel or abort on component unmount [1][2]. You can often customize this behavior using the onUnmount option to call flush instead if desired [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- rebindable RPC implementation ---'
rg -n -C 12 'makeRebindableRpcClient|rebindClient' packages

printf '%s\n' '--- debounce implementation for v0.21.1 ---'
curl -fsSL https://raw.githubusercontent.com/TanStack/pacer/v0.21.1/packages/pacer/src/debounce.ts 2>/dev/null || true
curl -fsSL https://raw.githubusercontent.com/TanStack/pacer/v0.21.1/packages/pacer/src/debouncer.ts 2>/dev/null || true

printf '%s\n' '--- package metadata and exported debounce source ---'
curl -fsSL https://unpkg.com/@tanstack/pacer@0.21.1/package.json
curl -fsSL https://unpkg.com/@tanstack/pacer@0.21.1/dist/esm/debounce.js 2>/dev/null || true

Repository: conciv-dev/conciv

Length of output: 9976


🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- exported debounce implementation ---'
curl -fsSL https://unpkg.com/@tanstack/pacer@0.21.1/dist/index.js \
  | grep -n -A35 -B10 'function debounce\|const debounce\|export.*debounce' | head -160

printf '%s\n' '--- debounce type declarations ---'
curl -fsSL https://unpkg.com/@tanstack/pacer@0.21.1/dist/index.d.cts \
  | grep -n -A20 -B10 'debounce' | head -160

printf '%s\n' '--- DynamicLink behavior ---'
rg -n -C 20 'class DynamicLink|function DynamicLink|type DynamicLink' packages

Repository: conciv-dev/conciv

Length of output: 4324


🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- Debouncer implementation ---'
curl -fsSL https://unpkg.com/@tanstack/pacer@0.21.1/dist/debouncer.js \
  | grep -n -A80 -B10 'class Debouncer\|function debounce' | head -220

printf '%s\n' '--- Debouncer declarations ---'
curl -fsSL https://unpkg.com/@tanstack/pacer@0.21.1/dist/debouncer.d.cts \
  | grep -n -A35 -B10 'declare class Debouncer\|declare function debounce' | head -220

printf '%s\n' '--- DynamicLink definitions in all tracked files ---'
rg -n -C 15 'DynamicLink' --glob '!pnpm-lock.yaml' --glob '!*.map' .

Repository: conciv-dev/conciv

Length of output: 17022


🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- Debouncer cancellation behavior ---'
curl -fsSL https://unpkg.com/@tanstack/pacer@0.21.1/dist/debouncer.js \
  | sed -n '90,185p'

printf '%s\n' '--- API-shape verifier ---'
python3 - <<'PY'
import re
import urllib.request

js = urllib.request.urlopen(
    'https://unpkg.com/@tanstack/pacer@0.21.1/dist/debouncer.js',
    timeout=10,
).read().decode()
dts = urllib.request.urlopen(
    'https://unpkg.com/@tanstack/pacer@0.21.1/dist/debouncer.d.cts',
    timeout=10,
).read().decode()

assert re.search(r'function debounce\(fn, initialOptions\)\s*\{\s*return new Debouncer\(fn, initialOptions\)\.maybeExecute;', js)
assert re.search(r'cancel:\s*\(\)\s*=>\s*void', js) or '.cancel = ' in js or 'cancel() {' in js
assert re.search(r'declare function debounce[\s\S]*\): \(\.\.\.args: Parameters<TFn>\) => void;', dts)
assert re.search(r'cancel:\s*\(\)\s*=>\s*void', dts)

print('debounce() returns only maybeExecute; cancel() belongs to Debouncer.')
PY

Repository: conciv-dev/conciv

Length of output: 5712


Cancel disposed navigation writes

When rebindClient changes the shared rpc link before storage.dispose(), a pending write can send the previous navigation state to the new API base. Guard the callback with state.cancelled and retain a Debouncer instance so dispose can call debouncer.cancel(). debounce() returns only maybeExecute; write.cancel() is not available.

🤖 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 `@packages/embed/src/navigation-storage.ts` around lines 27 - 32, Update the
navigation write setup in the storage implementation to retain the Debouncer
returned by debounce, guard its callback with state.cancelled, and cancel that
debouncer during storage.dispose(). Ensure pending writes are skipped after
disposal and cannot use a re-bound rpc client.


describe('six widget tabs sharing one browser connection pool', () => {
it('gives every tab one rpc websocket, no rpc over http, and a working chat round trip in the last tab', async () => {
const context = await browser.newContext()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
file='packages/embed/test/connection-pool.it.test.ts'
printf '%s\n' '--- file excerpt ---'
sed -n '1,130p' "$file"
printf '%s\n' '--- browser page/context usage ---'
rg -n -C 2 'browser\.(newContext|newPage)|context\.newPage|page\.|test\(' "$file"
printf '%s\n' '--- related test files ---'
git ls-files 'packages/embed/test' | rg '\.(test|spec)\.(ts|tsx|js|jsx)$' | head -80

Repository: conciv-dev/conciv

Length of output: 6210


Document the BrowserContext exception. This test intentionally creates six pages in one context; replacing browser.newContext() with browser.newPage() would isolate the pages and change the behavior under test.

🤖 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 `@packages/embed/test/connection-pool.it.test.ts` at line 67, Add a concise
comment at the browser.newContext() call explaining that the test intentionally
creates six pages within one BrowserContext, and that using browser.newPage()
would isolate them and alter the behavior being tested.

Source: Coding guidelines

…ferred bind, navigation write races, static EISDIR)

- browser-transport.ts: disposeSocket no longer synthesizes a close event
  when the underlying socket is still CLOSING, since partysocket's close()
  leaves its close listener attached in that branch and the real close
  event will still arrive — synthesizing one too was a double terminal
  event for the peer.
- client.ts: makeDeferredRpcClient's bind() and bound() now agree on
  `state.base !== null`, and an empty api base is rejected at bind time
  instead of silently marking the client bound.
- navigation-storage.ts: the debounced navigation write is now a retained
  Debouncer so dispose() can cancel() it, closing the window where a
  rebind swaps the shared rpc link while a write scheduled against the
  old base is still pending.
- static-app.ts (test helper): use statSync(...).isFile() instead of
  existsSync, which is also true for directories and let createReadStream
  throw an unhandled EISDIR.
- embed README: drop the documented connection-error retry state that
  does not exist yet (#350).
- connection-pool.it.test.ts: name the describe block to explain why it
  shares one browserContext across six tabs instead of six newPage()s,
  without adding a banned code comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omridevk

omridevk commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

CodeRabbit review addressed in 84db04a9 — all six were genuine, each fixed failing-first with a revert-check.

  1. Double terminal event on dispose (browser-transport.ts) — confirmed against partysocket's source: close() sets _closeCalled then early-returns without _disconnect() when the underlying socket is CLOSING or CLOSED, and the public readyState getter reports CLOSED once _closeCalled is set, so it can't be used post-close to tell the branches apart. CLOSING means the real close still arrives later; our synthetic one doubled it. Fix captures readyState === CLOSING before calling .close() and skips the synthetic dispatch in that case.
  2. bind('') inconsistency (client.ts) — reproduced: bind('') mutated state.base before browserRpcConnection('') threw, leaving bound() === true. Both checks now use state.base !== null, and an empty base is explicitly illegal — it throws before any mutation.
  3. Stale navigation write across rebind (navigation-storage.ts) — a write scheduled just before rebindClient() delivered the old state to the new base ~300ms later. Fixed with the library-native handle: new Debouncer(fn, {wait}) retained so dispose() calls write.cancel(). No hand-rolled flag added.
  4. EISDIR (static-app.ts) — GET /assets crashed the worker via createReadStream on a directory with no error listener. Now statSync(...).isFile().
  5. README — removed the documented connection-error retry behavior that does not exist; replaced with a note referencing Global connection-error handling: the widget never tells the user the engine is unreachable #350.
  6. newContext rationale — encoded in the describe name rather than a comment (repo bans comments): six tabs must share one context because the shared-context connection limit is exactly what the gate measures. Test body and assertions unchanged.

Two small seams added solely for testability: disposeSocket is now exported, and navigation-storage's rpc param is narrowed to Pick<RpcClient, 'navigation'>. No call-site behavior changes.

Gates green (contract/embed/app, serial), fallow 0 introduced. rebind.it.test.ts 4/4 and forced-drop.it.test.ts 1/1 still green — the peer still observes exactly one terminal event.

🤖 Generated with Claude Code

@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: 1

🧹 Nitpick comments (1)
packages/contract/test/browser-transport.test.ts (1)

10-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the class-based test double.

FakeNativeSocket is a class in a TypeScript file. Replace it with a constructable function-based test double.

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Use functions instead of classes, except BaseTextAdapter in packages/harness/src/_shared/text-adapter.ts, which may remain a subclass because library typing requires it."

🤖 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 `@packages/contract/test/browser-transport.test.ts` around lines 10 - 30,
Replace the class-based FakeNativeSocket test double with a constructable
function that preserves its EventTarget behavior, static instances tracking,
socket state constants, properties, send, and close behavior. Update the
function’s prototype or construction logic as needed so existing tests can
instantiate it with new while avoiding a class declaration.

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/conciv/test/static-app.test.ts`:
- Around line 10-12: Update the test teardown around app to declare app as
optional and guard the app.close() call in afterAll so cleanup runs only when
setup assigned an app instance.

---

Nitpick comments:
In `@packages/contract/test/browser-transport.test.ts`:
- Around line 10-30: Replace the class-based FakeNativeSocket test double with a
constructable function that preserves its EventTarget behavior, static instances
tracking, socket state constants, properties, send, and close behavior. Update
the function’s prototype or construction logic as needed so existing tests can
instantiate it with new while avoiding a class declaration.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 687bf725-f5e4-4268-8e21-eb3873f19ffe

📥 Commits

Reviewing files that changed from the base of the PR and between 18803d9 and 84db04a.

📒 Files selected for processing (10)
  • apps/conciv/test/helpers/static-app.ts
  • apps/conciv/test/static-app.test.ts
  • packages/contract/src/browser-transport.ts
  • packages/contract/src/client.ts
  • packages/contract/test/browser-transport.test.ts
  • packages/contract/test/deferred-client.test.ts
  • packages/embed/README.md
  • packages/embed/src/navigation-storage.ts
  • packages/embed/test/connection-pool.it.test.ts
  • packages/embed/test/navigation-storage.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/embed/README.md
  • packages/embed/test/connection-pool.it.test.ts
  • packages/contract/src/browser-transport.ts
  • apps/conciv/test/helpers/static-app.ts
  • packages/contract/src/client.ts
  • packages/embed/src/navigation-storage.ts

Comment on lines +10 to +12
afterAll(async () => {
await app.close()
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target file ---'
cat -n apps/conciv/test/static-app.test.ts

printf '%s\n' '--- related hook patterns ---'
rg -n -U 'beforeAll\\([\\s\\S]{0,500}?afterAll\\(' apps packages --glob '*.{test,spec}.ts' --glob '*.{test,spec}.tsx' | head -200

Repository: conciv-dev/conciv

Length of output: 1102


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target file ---'
cat -n apps/conciv/test/static-app.test.ts

printf '%s\n' '--- related hook patterns ---'
rg -n -U 'beforeAll\([\s\S]{0,500}?afterAll\(' apps packages --glob '*.{test,spec}.ts' --glob '*.{test,spec}.tsx' | head -200

Repository: conciv-dev/conciv

Length of output: 16668


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- helper implementation ---'
fd -i 'static-app' . --type f
helper=$(fd -i 'static-app' . --type f | head -1)
if [ -n "${helper:-}" ]; then
  cat -n "$helper"
fi

Repository: conciv-dev/conciv

Length of output: 2168


🌐 Web query:

Vitest documentation beforeAll fails afterAll runs

💡 Result:

In Vitest, if a beforeAll hook fails, the subsequent tests in that suite are skipped [1], and the afterAll hook will typically not run for that suite [2]. This behavior can make it difficult to perform necessary cleanup if the setup performed in beforeAll fails midway. To ensure that cleanup logic always executes—even if setup fails—the recommended approach is to use the cleanup function returned by beforeAll or use the aroundAll hook. 1. Cleanup function in beforeAll: You can return a function from your beforeAll hook. Vitest guarantees that this cleanup function will be executed after the suite finishes, even if the setup logic itself encountered issues [3][4]. beforeAll(async => { await setup; return async => { await teardown; }; }); 2. aroundAll hook: Alternatively, you can use aroundAll to wrap your entire suite. This allows you to use a try...finally block to ensure teardown logic runs regardless of whether the setup (or the suite execution itself) fails [2][5]. aroundAll(async (runSuite) => { try { await setup; await runSuite; } finally { await teardown; } }); Using these patterns helps avoid resource leaks that occur when a test suite is aborted before it can reach the afterAll block [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- listenLocal implementation ---'
fd -i 'listen-local' apps packages --type f --exec cat -n {}

printf '%s\n' '--- Vitest version and configuration references ---'
rg -n 'vitest|pool|sequence|hook' package.json pnpm-lock.yaml vitest.config.* apps/conciv --glob '!**/node_modules/**' | head -240

Repository: conciv-dev/conciv

Length of output: 25443


🌐 Web query:

site:vitest.dev/api/hooks beforeAll afterAll failure cleanup Vitest 4

💡 Result:

In Vitest, if a beforeAll hook fails, the tests within that suite will not execute, and the corresponding afterAll hook will generally not be executed for that suite [1]. To ensure cleanup logic runs even if setup fails, you can use the cleanup function feature of beforeAll. When a beforeAll hook returns a function, Vitest treats that returned function as a cleanup task (equivalent to afterAll) that is guaranteed to run after the suite finishes, regardless of whether the setup succeeded or if the tests passed or failed [1]. Example usage: beforeAll(async => { await startMocking; // This returned function acts as an afterAll cleanup return async => { await stopMocking; }; });

Citations:


🌐 Web query:

site:vitest.dev/guide/lifecycle beforeAll afterAll failure Vitest

💡 Result:

In Vitest, if a beforeAll hook fails, the tests within that suite will not be executed [1]. The afterAll hook will still execute even if the beforeAll hook fails, provided that the suite itself was initiated [1]. This ensures that any necessary cleanup operations defined in afterAll are performed regardless of whether the setup phase in beforeAll succeeded [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

for url in \
  'https://vitest.dev/api/hooks.html' \
  'https://vitest.dev/guide/lifecycle.html' \
  'https://raw.githubusercontent.com/vitest-dev/vitest/v4.1.10/packages/runner/src/run.ts'
do
  printf '\n--- %s ---\n' "$url"
  curl -LfsS "$url" | grep -in -E -C 4 'beforeAll|afterAll|cleanup|fail' | head -240 || true
done

Repository: conciv-dev/conciv

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -eu

url='https://raw.githubusercontent.com/vitest-dev/vitest/v4.1.10/packages/runner/src/run.ts'
printf '%s\n' "--- $url ---"
curl -LfsS "$url" | nl -ba | grep -E -C 12 'beforeAll|afterAll|runSuite|callSuiteHook|suite\.result' | head -240

printf '%s\n' '--- lifecycle page text around failure terms ---'
curl -LfsS 'https://vitest.dev/guide/lifecycle.html' |
  sed 's/<[^>]*>/ /g; s/&amp;/\&/g; s/&`#39`;/'"'"'/g' |
  tr -s ' ' |
  grep -ioE '.{0,180}(beforeAll|afterAll|fail|skip).{0,240}' |
  head -80

Repository: conciv-dev/conciv

Length of output: 4424


🏁 Script executed:

#!/bin/bash
set -eu

url='https://raw.githubusercontent.com/vitest-dev/vitest/v4.1.10/packages/runner/src/run.ts'
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
curl -LfsS "$url" -o "$tmp"
printf '%s\n' "--- $url ($(wc -c < "$tmp") bytes) ---"
grep -n -E -C 18 'beforeAll|afterAll|runSuite|callSuiteHook|suite\.result|hasFailed' "$tmp" | head -320

Repository: conciv-dev/conciv

Length of output: 10665


🏁 Script executed:

#!/bin/bash
set -eu

url='https://raw.githubusercontent.com/vitest-dev/vitest/v4.1.10/packages/runner/src/run.ts'
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
curl -LfsS "$url" -o "$tmp"
grep -n -E 'export async function runSuite|async function runSuite|runSuite\(' "$tmp"
sed -n '760,980p' "$tmp"

Repository: conciv-dev/conciv

Length of output: 6778


Guard teardown when setup fails.

Vitest runs afterAll even when beforeAll rejects. In that case, app is undefined, so app.close() throws a second error. Declare app as optional and close it only when assigned.

🤖 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/conciv/test/static-app.test.ts` around lines 10 - 12, Update the test
teardown around app to declare app as optional and guard the app.close() call in
afterAll so cleanup runs only when setup assigned an app instance.

@omridevk
omridevk merged commit ea23bf6 into main Aug 8, 2026
23 checks passed
@omridevk
omridevk deleted the ws-client-326 branch August 8, 2026 21:57
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.

ws transport PR 3/3: browser client cut-over to /rpc-ws (#314)

2 participants