Skip to content

fix(devtools): let birpc be singlesource of turth for server<->client comms#978

Open
huang-julien wants to merge 2 commits intomainfrom
fix/extendsRpc
Open

fix(devtools): let birpc be singlesource of turth for server<->client comms#978
huang-julien wants to merge 2 commits intomainfrom
fix/extendsRpc

Conversation

@huang-julien
Copy link
Copy Markdown
Member

@huang-julien huang-julien commented Apr 5, 2026

… comms

🔗 Linked issue

📚 Description

Found bug when testing out nuxt/hints#318 with Vite 8 + devtools 4.0.0-alpha.3

This solves a race condiftion and remove extendedRpcMap to make the code a bit simpler. if extendClientRpc is called while connectDevToolsRpc( is mid-flight (after it already iterated extendedRpcMap but before it sets rpcClient), neither path registers the handlers.

Need also #977 to fix the issue with RPC in nuxt hints.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

The diff removes the centralized extendedRpcMap and changes how client-side RPC handlers are registered and invoked. extendClientRpc now registers each function directly on a DevToolsRpcClient via client.client.register using ${namespace}:${name} and returns a proxy of per-key async functions that lazily await the active RPC connection (rpcClient or connectPromise) before calling client.call. The module-level rpc proxy no longer intercepts colon-delimited methods for local dispatch. rpcClient and connectPromise are exported.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: removing extendedRpcMap to let birpc be the single source of truth for RPC communications.
Description check ✅ Passed The description explains the race condition bug, the solution (removing extendedRpcMap), and relates it to the code changes shown in the summary.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/extendsRpc

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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/devtools/client/composables/client.ts (1)

63-73: Make extendClientRpc() re-registration idempotent.

registerClientFunctions() in packages/devtools/client/composables/rpc.ts already uses update() with a register() fallback. Mirroring that here would keep ${namespace}:${name} safe across HMR/re-init instead of assuming register() replaces an existing handler.

♻️ Proposed change
         const register = (client: DevToolsRpcClient) => {
           for (const [name, handler] of Object.entries(functions)) {
             if (typeof handler === 'function') {
-              client.client.register({
-                name: `${namespace}:${name}`,
-                type: 'event',
-                handler: handler as any,
-              })
+              try {
+                client.client.update({
+                  name: `${namespace}:${name}`,
+                  type: 'event',
+                  handler: handler as any,
+                })
+              }
+              catch {
+                client.client.register({
+                  name: `${namespace}:${name}`,
+                  type: 'event',
+                  handler: handler as any,
+                })
+              }
             }
           }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools/client/composables/client.ts` around lines 63 - 73, The
current register loop in the register function unconditionally calls
client.client.register for each `${namespace}:${name}`, which can clash on
HMR/re-init; make re-registration idempotent by attempting
client.client.update({ name: `${namespace}:${name}`, type: 'event', handler })
first and only calling client.client.register(...) as a fallback if update
fails/not available. Update the register function (and extendClientRpc usage if
present) to use update() with register() fallback for each entry in functions to
keep handlers safe across re-inits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/devtools/client/composables/client.ts`:
- Around line 75-78: The branch that calls connectPromise.then(register) drops
the returned promise and can cause an unhandled rejection if the RPC connection
fails; update the code around rpcClient/register/connectPromise to consume
rejections (for example by chaining .catch and forwarding the error to the same
wsError/console handler or invoking the existing error handler) so the promise
returned by connectPromise.then(register) is handled and its errors are
processed consistently with the other failure path.

---

Nitpick comments:
In `@packages/devtools/client/composables/client.ts`:
- Around line 63-73: The current register loop in the register function
unconditionally calls client.client.register for each `${namespace}:${name}`,
which can clash on HMR/re-init; make re-registration idempotent by attempting
client.client.update({ name: `${namespace}:${name}`, type: 'event', handler })
first and only calling client.client.register(...) as a fallback if update
fails/not available. Update the register function (and extendClientRpc usage if
present) to use update() with register() fallback for each entry in functions to
keep handlers safe across re-inits.
🪄 Autofix (Beta)

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

Run ID: da97b456-c8bd-437c-9f81-ec0f4995d125

📥 Commits

Reviewing files that changed from the base of the PR and between 4c594ce and 3472304.

📒 Files selected for processing (2)
  • packages/devtools/client/composables/client.ts
  • packages/devtools/client/composables/rpc.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 5, 2026

Deploying nuxt-devtools with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9bfd78e
Status: ✅  Deploy successful!
Preview URL: https://e49bcd50.nuxt-devtools.pages.dev
Branch Preview URL: https://fix-extendsrpc.nuxt-devtools.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/devtools/client/composables/client.ts (1)

63-78: Consider applying the update/register fallback pattern to extendClientRpc.

The register helper uses only client.client.register() without the update/register fallback pattern used in registerClientFunctions (see packages/devtools/client/composables/rpc.ts lines 67-85). Since extendClientRpc is a public API that external consumers can call, they may inadvertently call it multiple times with the same namespace and function name. To be defensive, apply the same pattern:

Suggested change
         for (const [name, handler] of Object.entries(functions)) {
           if (typeof handler === 'function') {
-            client.client.register({
-              name: `${namespace}:${name}`,
-              type: 'event',
-              handler: handler as any,
-            })
+            try {
+              client.client.update({
+                name: `${namespace}:${name}`,
+                type: 'event',
+                handler: handler as any,
+              })
+            }
+            catch {
+              client.client.register({
+                name: `${namespace}:${name}`,
+                type: 'event',
+                handler: handler as any,
+              })
+            }
           }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools/client/composables/client.ts` around lines 63 - 78, The
current register helper in extendClientRpc calls only
client.client.register(...) which can fail or duplicate when consumers call
extendClientRpc multiple times; mirror the update/register fallback used in
registerClientFunctions by attempting client.client.update({...}) first and, if
that fails (catch), call client.client.register({...}) as a fallback. Update the
register function (the one defined inside extendClientRpc) to use
client.client.update and fallback to client.client.register for each { name:
`${namespace}:${name}`, type: 'event', handler } to make the API idempotent and
defensive.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/devtools/client/composables/client.ts`:
- Around line 63-78: The current register helper in extendClientRpc calls only
client.client.register(...) which can fail or duplicate when consumers call
extendClientRpc multiple times; mirror the update/register fallback used in
registerClientFunctions by attempting client.client.update({...}) first and, if
that fails (catch), call client.client.register({...}) as a fallback. Update the
register function (the one defined inside extendClientRpc) to use
client.client.update and fallback to client.client.register for each { name:
`${namespace}:${name}`, type: 'event', handler } to make the API idempotent and
defensive.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b458595f-d13c-4e07-bd8a-586610fac1e3

📥 Commits

Reviewing files that changed from the base of the PR and between 3472304 and 9bfd78e.

📒 Files selected for processing (1)
  • packages/devtools/client/composables/client.ts

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant