Skip to content

feat: application-level ping/pong hooks and peer.ping() - #202

Merged
pi0 merged 2 commits into
mainfrom
feat/ping
Jul 3, 2026
Merged

pi0 merged 2 commits into
mainfrom
feat/ping

Conversation

@pi0x

@pi0x pi0x commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #154 (the remaining follow-up scope after #201 landed automatic server-side liveness):

  • Adds ping(peer, data) / pong(peer, data) hooks to the Hooks interface, firing when an application-level ping/pong control frame arrives from the peer.
  • Adds peer.ping(data?) to send an application-level ping.
  • Wired natively for Node (ws), uWebSockets (previously dropped its ping/pong behavior handlers), and Bun (which turns out to fully support ws.ping()/ws.pong() and ping/pong handlers, better than the issue assumed). Deno, Cloudflare (incl. Durable Objects), Bunny, and SSE have no such control-frame API exposed to user code, so peer.ping() warns and no-ops there and the hooks never fire.
  • Documents a compatibility matrix for ping() / ping+pong hooks alongside the existing drain row in the peer guide, and adds both hooks to the hooks guide example.

Every runtime already auto-replies to an inbound ping with a pong per the WebSocket spec — these hooks only observe the control frames, they don't need to answer them.

Test plan

  • pnpm lint / pnpm typecheck pass
  • pnpm vitest run — full suite green (248 passed, 6 skipped)
  • New tests added in test/tests.ts (pingPongTests, using the ws package client since raw ping/pong control frames aren't reachable through the standard WebSocket API), wired into node.test.ts, uws.test.ts, and bun.test.ts:
    • inbound client ping → ping hook fires
    • inbound client pong → pong hook fires
    • peer.ping() → client observes a real ping frame

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for WebSocket ping/pong events in supported adapters.
    • Introduced a ping() method on peers to send ping frames and help measure connection latency.
    • Expanded documentation with examples and compatibility notes for ping/pong handling.
  • Bug Fixes

    • Improved runtime compatibility by handling ping/pong frames consistently across supported environments.
    • Added coverage to verify ping/pong behavior end to end.

Closes the remaining follow-up from #154 after #201 landed automatic
liveness. Adds `ping`/`pong` hooks to observe inbound control frames and
`peer.ping(data?)` to send one, wired for Node (ws), uWebSockets, and Bun
(all natively support it); Deno, Cloudflare, Bunny, and SSE have no such
runtime API and fall back to a warning no-op.

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

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pi0x, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c95b0973-9588-4773-8def-72ac0cc01bb0

📥 Commits

Reviewing files that changed from the base of the PR and between e44e11c and dc8306d.

📒 Files selected for processing (6)
  • docs/1.guide/3.peer.md
  • src/adapters/bun.ts
  • src/adapters/node.ts
  • src/adapters/uws.ts
  • src/peer.ts
  • test/tests.ts
📝 Walkthrough

Walkthrough

Adds application-level ping/pong support to crossws: new Hooks.ping/Hooks.pong callbacks, a Peer.ping(data?) method with per-adapter implementations (Bun, Node, uWS), expanded HOOK_NAMES for inline-hook detection, new pingPongTests test suite wired into adapter tests, and updated documentation with a compatibility matrix and footnote.

Changes

Ping/Pong support

Layer / File(s) Summary
Hooks contract and default Peer.ping
src/hooks.ts, src/peer.ts, src/server/_resolve.ts
Adds ping/pong callbacks to the Hooks interface, a default no-op Peer.ping that warns when unsupported, and extends HOOK_NAMES to recognize ping/pong as inline hooks.
Bun adapter wiring
src/adapters/bun.ts
Intercepts native Bun ping/pong control frames and forwards them via hooks.callHook; BunPeer.ping delegates to the underlying Bun WebSocket.
Node adapter wiring
src/adapters/node.ts
Registers ping/pong event listeners forwarding payloads to hooks; NodePeer.ping delegates to the underlying ws instance.
uWS adapter wiring
src/adapters/uws.ts
Adds ping/pong handlers resolving the peer and dispatching hooks with Uint8Array payloads; UWSPeer.ping forwards to the uWS socket.
Tests
test/tests.ts, test/fixture/_shared.ts, test/adapters/bun.test.ts, test/adapters/node.test.ts, test/adapters/uws.test.ts
Adds exported pingPongTests suite using a raw ws client, fixture hook handlers responding to ping/pong, and wires the new suite into Bun, Node, and uWS adapter test files.
Documentation
docs/1.guide/2.hooks.md, docs/1.guide/3.peer.md
Documents ping/pong hooks example, peer.ping(data?) usage with a latency example, an updated compatibility matrix, and a new footnote on runtime limitations.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested labels: enhancement

Suggested reviewers: pi0

Poem

A rabbit taps the wire, tap-tap-ping,
and waits to hear the pong frame sing.
Bun, Node, and uWS all reply,
"I'm alive!" beneath the wire's sky.
🐇📡 Latency measured, hop complete!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: application-level ping/pong hooks plus peer.ping().
Linked Issues check ✅ Passed The PR implements the requested ping/pong hooks, peer.ping(), runtime wiring, compatibility docs, and tests.
Out of Scope Changes check ✅ Passed Changes stay focused on the ping/pong API, adapter wiring, docs, and tests; no unrelated functionality is introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ping

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.

@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 (4)
src/peer.ts (1)

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

Unthrottled warning on unsupported adapters.

The default ping() calls console.warn on every invocation. If a caller sets up a periodic app-level heartbeat via peer.ping() (a natural use case per the docs) on an adapter without support (Deno, Cloudflare, Bunny, SSE), this will spam the log on every tick indefinitely.

Consider warning only once per process (e.g. module-level flag) rather than per call.

♻️ Proposed fix to warn once
+let _pingWarned = false;
+
 export abstract class Peer<Internal extends AdapterInternal = AdapterInternal> {
   ...
   ping(_data?: unknown): number | void | undefined {
-    console.warn("[crossws] `peer.ping()` is not supported by this adapter.");
+    if (!_pingWarned) {
+      _pingWarned = true;
+      console.warn("[crossws] `peer.ping()` is not supported by this adapter.");
+    }
   }
🤖 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 `@src/peer.ts` around lines 187 - 189, The unsupported-adapter warning in
peer.ping() is emitted on every call, which can spam logs for periodic
heartbeats; update the ping method in peer.ts to warn only once per process by
adding a module-level guard/flag around the console.warn call, so repeated
peer.ping() invocations on adapters like Deno, Cloudflare, Bunny, or SSE do not
keep logging.
src/adapters/bun.ts (1)

171-174: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Ping payload bypasses the normalization used by send/_publish.

send() and _publish() both route through toBufferLike(data) to safely coerce arbitrary input (objects, numbers, etc.) into a sendable form. ping() instead force-casts the unknown payload straight to any and hands it to Bun's ws.ping(), which only accepts string | BufferSource. If a caller passes a plain object/number (as they can with send), this will likely throw an opaque runtime error from Bun's native binding rather than failing predictably or being normalized. The same pattern repeats in the Node and uWS adapters.

💡 Possible approach
-  override ping(data?: unknown): number {
-    return this._internal.ws.ping(data as any);
-  }
+  override ping(data?: unknown): number {
+    return this._internal.ws.ping(toBufferLike(data) as any);
+  }
🤖 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 `@src/adapters/bun.ts` around lines 171 - 174, The ping payload is bypassing
the same normalization used by send and _publish, so arbitrary inputs can reach
Bun’s ws.ping() and fail at runtime. Update ping() in bun.ts to normalize the
optional payload with toBufferLike(data) before calling
this._internal.ws.ping(), and apply the same fix in the matching ping
implementations in the Node and uWS adapters so behavior stays consistent across
adapters.
src/adapters/uws.ts (1)

232-235: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Same normalization gap noted in the Bun/Node adapters.

this._internal.uws.ping(data) is called with the raw payload typed as uws.RecognizedString, without going through toBufferLike the way send/_publish do. This is consistent with the native uws.ping() signature itself, but if a caller passes a value that doesn't satisfy RecognizedString (allowed at the base Peer.ping(data?: unknown) level), it will fail at the native binding rather than being normalized like send() does.

🤖 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 `@src/adapters/uws.ts` around lines 232 - 235, The ping path in the uWS adapter
still bypasses the same normalization used by send and _publish, so
Peer.ping(data?: unknown) can reach the native binding with an unnormalized
value. Update the uws.ts override of ping to normalize the incoming payload
through toBufferLike before calling this._internal.uws.ping, and keep the method
behavior aligned with the adapter’s other data-handling paths and the same
pattern used in the Bun/Node adapters.
src/adapters/node.ts (1)

301-304: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Same normalization gap as the Bun/uWS adapters.

ws.ping(data) is called with the raw unknown payload; send() normalizes via toBufferLike first. Passing an object/number here would be handed straight to the underlying ws library, which expects string | Buffer | ArrayBuffer | ..., not arbitrary JS values. Worth aligning with send()'s handling for consistency across the peer API surface.

🤖 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 `@src/adapters/node.ts` around lines 301 - 304, The Node adapter’s ping path
has the same payload-normalization gap as the Bun/uWS adapters:
`NodeWebSocket.ping` currently forwards the raw `unknown` value directly to
`this._internal.ws.ping`. Update `ping()` to normalize the optional payload the
same way `send()` does by converting it through `toBufferLike` before calling
the underlying `ws` method, so the behavior stays consistent across the adapter
API and only supported binary/string-like values are passed through.
🤖 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 `@docs/1.guide/3.peer.md`:
- Around line 140-157: The RTT example in the peer guide is inconsistent with
the prose because it stores the timestamp in peer.context and sends an empty
ping instead of embedding the timestamp in ping data. Update the example in
defineHooks so the message handler attaches the timestamp to the ping payload,
and the pong handler reads that payload to compute RTT, keeping the code aligned
with the “embed a timestamp in data” wording.

---

Nitpick comments:
In `@src/adapters/bun.ts`:
- Around line 171-174: The ping payload is bypassing the same normalization used
by send and _publish, so arbitrary inputs can reach Bun’s ws.ping() and fail at
runtime. Update ping() in bun.ts to normalize the optional payload with
toBufferLike(data) before calling this._internal.ws.ping(), and apply the same
fix in the matching ping implementations in the Node and uWS adapters so
behavior stays consistent across adapters.

In `@src/adapters/node.ts`:
- Around line 301-304: The Node adapter’s ping path has the same
payload-normalization gap as the Bun/uWS adapters: `NodeWebSocket.ping`
currently forwards the raw `unknown` value directly to `this._internal.ws.ping`.
Update `ping()` to normalize the optional payload the same way `send()` does by
converting it through `toBufferLike` before calling the underlying `ws` method,
so the behavior stays consistent across the adapter API and only supported
binary/string-like values are passed through.

In `@src/adapters/uws.ts`:
- Around line 232-235: The ping path in the uWS adapter still bypasses the same
normalization used by send and _publish, so Peer.ping(data?: unknown) can reach
the native binding with an unnormalized value. Update the uws.ts override of
ping to normalize the incoming payload through toBufferLike before calling
this._internal.uws.ping, and keep the method behavior aligned with the adapter’s
other data-handling paths and the same pattern used in the Bun/Node adapters.

In `@src/peer.ts`:
- Around line 187-189: The unsupported-adapter warning in peer.ping() is emitted
on every call, which can spam logs for periodic heartbeats; update the ping
method in peer.ts to warn only once per process by adding a module-level
guard/flag around the console.warn call, so repeated peer.ping() invocations on
adapters like Deno, Cloudflare, Bunny, or SSE do not keep logging.
🪄 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: 246d6604-2659-4402-8444-6aae7341b467

📥 Commits

Reviewing files that changed from the base of the PR and between 13af6ed and e44e11c.

📒 Files selected for processing (13)
  • docs/1.guide/2.hooks.md
  • docs/1.guide/3.peer.md
  • src/adapters/bun.ts
  • src/adapters/node.ts
  • src/adapters/uws.ts
  • src/hooks.ts
  • src/peer.ts
  • src/server/_resolve.ts
  • test/adapters/bun.test.ts
  • test/adapters/node.test.ts
  • test/adapters/uws.test.ts
  • test/fixture/_shared.ts
  • test/tests.ts

Comment thread docs/1.guide/3.peer.md Outdated
…ongs

- peer.ping() now catches native validation errors (e.g. non-buffer
  payloads, >125-byte control frames) and routes them through the
  error hook instead of crashing the process.
- Node's internal keepalive pings are tagged so their echoed pongs
  don't fire the app-level pong hook; the RTT doc example now embeds
  a correlatable payload since Bun/uWS keepalive pings can't be tagged.
- Warn once per peer (not per call) when ping() is unsupported.
- Skip the uWS ping/pong Uint8Array copy when no hook/resolver needs it.
- Simplify the pingPongTests message queue to a plain FIFO.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pi0
pi0 merged commit e1a13f8 into main Jul 3, 2026
6 checks passed
@pi0x pi0x mentioned this pull request Aug 21, 2026
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.

Universal heartbeat/ping-pong support

2 participants