Skip to content

feat(pubsub): sync backplane#192

Merged
pi0 merged 15 commits into
mainfrom
feat/sync
Jun 30, 2026
Merged

feat(pubsub): sync backplane#192
pi0 merged 15 commits into
mainfrom
feat/sync

Conversation

@pi0x

@pi0x pi0x commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Docs: https://feat-sync.h3-crossws.pages.dev/guide/sync

Sync backplane

Opt-in sync adapter that relays pub/sub between crossws instances over a shared backplane, so peer.publish() / adapter.publish() reach subscribers connected to other regions, processes, or replicas. When sync is absent, pub/sub stays local exactly as before.

Built-in drivers (crossws/sync, zero third-party deps):

  • redis — Redis pub/sub (ioredis + node-redis, auto-detected). Multi-region/host.
  • pgsql — Postgres LISTEN/NOTIFY (node-postgres + postgres.js). For clusters already on Postgres.
  • cluster — Node cluster IPC (+ setupPrimaryCluster()). Forked processes on one host (Node cluster / PM2).
  • broadcastChannelBroadcastChannel. In-process worker threads / tests.

Custom backplanes implement the SyncAdapter interface; encodeEnvelope / decodeEnvelope are exported for the wire format (sender-id echo suppression + base64 binary).

Delivery semantics: best-effort, at-most-once, unordered, no replay/buffering. Local delivery is independent of backplane health. Relay failures never throw into publish() — pass onError(error, { stage }) (stage: subscribe | publish | delivery) to observe a degraded backplane; defaults to console.error.

Cloudflare: the single default Durable Object is already cluster-global, so a backplane only matters for multi-DO sharding or the fallback (no-DO) path. Inbound delivery into a hibernated Durable Object is best-effort; outbound relay is reliable. Built-in network drivers target Node-like runtimes — bring a workerd-native custom SyncAdapter.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a new public ./sync entry point for cross-instance pub/sub backplanes (with SyncMessage, SyncDriver, and SyncAdapter types).
    • Introduced built-in sync drivers, including zero-dependency BroadcastChannel, plus Redis, PostgreSQL, and cluster/IPC.
    • Improved sync relay behavior to prevent duplicate deliveries and added onError(error, { stage }) for publish/subscribe/delivery failures.
  • Documentation

    • Updated the pub/sub guide and added a dedicated sync backplane guide.
  • Tests

    • Expanded sync test coverage across drivers and adapter teardown/error handling.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an opt-in sync backplane for relaying pub/sub across instances, with new sync contracts, built-in drivers, peer and adapter relay changes, public exports, tests, and docs.

Changes

Sync Backplane Feature

Layer / File(s) Summary
Sync contracts, encoding, and facade
src/sync/types.ts, src/sync/encoding.ts, src/utils.ts, src/sync.ts
Defines SyncMessage, SyncDriver, and SyncAdapter; adds envelope encoding/decoding and message serialization helpers; and turns src/sync.ts into a re-export facade with driver-name and options types.
Built-in sync drivers
src/sync/broadcast-channel.ts, src/sync/redis.ts, src/sync/pgsql.ts, src/sync/cluster.ts
Implements broadcastChannel, redis, pgsql, and cluster sync drivers with echo suppression, client-flavor detection, envelope handling, channel validation, and primary/worker relay wiring.
Peer publish relay split
src/peer.ts
AdapterInternal gains sync state, Peer.publish becomes a concrete relay-aware method that calls _publish for local fan-out and forwards serialized messages to the sync backplane, and _publish becomes the abstract local-delivery hook.
Adapter utils sync orchestration
src/adapter.ts
adapterUtils accepts sync options, creates and subscribes the sync driver, republishes inbound backplane messages locally through relay-free localPublish, forwards local publishes to the backplane, and closes the sync driver during teardown; AdapterInstance and AdapterOptions gain sync fields.
Adapter sync wiring and local publish rename
src/adapters/bun.ts, src/adapters/bunny.ts, src/adapters/cloudflare.ts, src/adapters/deno.ts, src/adapters/node.ts, src/adapters/sse.ts, src/adapters/uws.ts
Bun, Bunny, Deno, Node, SSE, and uWS adapters thread options into adapterUtils, capture baseUtils.sync in peer construction, extend peer state with sync, and rename peer publication methods to _publish; Cloudflare renames its peer publish methods to _publish as well.
Public exports and package entry
src/index.ts, package.json, build.config.ts
Exposes sync types from src/index.ts, adds the ./sync package export to package.json, and includes src/sync.ts in the build entry list.
Sync tests and uWS regression
test/sync.test.ts, test/adapters/sync.test.ts, test/adapters/uws.test.ts
test/sync.test.ts covers sync drivers, envelopes, guards, and connector behavior; test/adapters/sync.test.ts covers end-to-end cross-instance relay and shutdown behavior; test/adapters/uws.test.ts adds a native global publish regression test.
Sync guide and pub/sub note
docs/1.guide/6.sync.md, docs/1.guide/5.pubsub.md
docs/1.guide/6.sync.md documents the sync backplane concept, built-in drivers, and custom driver contract; docs/1.guide/5.pubsub.md adds a note about local-only default pub/sub delivery and links to the sync guide.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • h3js/crossws#179: Introduced the initial bunny adapter implementation; this PR updates the same adapter file to wire sync backplane support and rename the peer publish hook.
  • h3js/crossws#188: Touched src/adapters/cloudflare.ts’s durable peer restore path; this PR also updates that same code path to thread sync through peer restoration.

Suggested labels

enhancement

Poem

🐇 I hopped through channels, one and two,
A sync-backplane hums the message true.
No echo loops, just careful hops,
Across the wires and server tops.
Redis, cluster, BC in play—
Rabbit-approved relay day!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.88% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding sync backplane support to pubsub.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sync

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.

@pi0
pi0 marked this pull request as draft June 17, 2026 17:25

@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

Caution

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

⚠️ Outside diff range comments (1)
src/adapters/cloudflare.ts (1)

68-68: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sync backplane not wired for Cloudflare adapter.

Unlike other adapters, this calls adapterUtils(globalPeers) without passing opts, so even if a user configures sync in their Cloudflare adapter options, it will be ignored. This inconsistency may confuse users expecting sync to work uniformly across adapters.

If sync support is intentionally excluded for Cloudflare (perhaps due to Durable Objects having their own cross-instance mechanism), consider documenting this or throwing an error if opts.sync is provided.

🔧 Suggested fix to wire sync (if intended)
-  const { publish: durablePublish, ...utils } = adapterUtils(globalPeers);
+  const { publish: durablePublish, ...utils } = adapterUtils(globalPeers, opts);
🤖 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/cloudflare.ts` at line 68, The adapterUtils function call in the
Cloudflare adapter is missing the opts parameter. In the line where adapterUtils
is invoked with only globalPeers as an argument, add opts as the second
parameter (like adapterUtils(globalPeers, opts)) to ensure the sync backplane
configuration from the user's options is properly wired and passed through to
the adapter utilities, making it consistent with how other adapters handle this
call.
🤖 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/5.pubsub.md`:
- Line 48: The sentence on line 48 starting with "No change to your hooks
and..." has awkward phrasing that affects readability. Restructure this sentence
in the sync adapter explanation to improve clarity - consider rephrasing it so
that the core message (that no code changes are needed while subscribe/publish
functionality now spans the cluster) flows more naturally and is easier for
users to understand.

In `@src/adapter.ts`:
- Around line 36-47: The sync subscription created in the sync driver
initialization block is not being cleaned up when the adapter is destroyed.
Capture the return value of the sync.subscribe() call and store it (as it likely
returns an unsubscribe function or handle). Then implement cleanup logic in the
adapter's shutdown or destroy method to call the unsubscribe function, ensuring
the subscription is properly disposed of when the adapter is no longer needed.
This prevents memory leaks and duplicate message delivery from stale
subscriptions.
- Around line 14-34: In the localPublish function, both send() and _publish()
are called on firstPeerWithTopic, which causes duplicate message delivery in
native pub/sub adapters (Bun, uWS) since their _publish() implementations
broadcast to all peers including self. Decide on the design intent: either
remove the send() call and rely solely on _publish() for all delivery across all
adapter types, or implement adapter-specific handling where loop-based adapters
use send() + _publish() while native pub/sub adapters use only _publish().
Document the chosen approach clearly so all adapters behave consistently without
message duplication.

In `@src/peer.ts`:
- Around line 110-116: The sync.publish() call within the publish method can
return a Promise but unhandled rejections are never caught, which will cause
unhandled promise rejection errors at runtime. Wrap the sync.publish() call with
Promise.resolve().catch() to handle both void and Promise return values,
matching the same error handling pattern used for sync.subscribe() elsewhere in
the codebase. This ensures transient network failures from networked sync
drivers are properly caught and handled instead of surfacing as unhandled
promise rejections.

In `@src/sync.ts`:
- Around line 88-95: The subscribe method's message event handler calls
deliver(envelope.msg) without validating that the msg property is actually a
valid SyncMessage. Add validation logic after the envelope null and id check to
ensure envelope.msg is a valid SyncMessage before passing it to the deliver
function. This will prevent malformed or invalid messages from propagating into
the local fan-out path and causing runtime errors.

In `@test/adapters/bun.test.ts`:
- Line 12: The expect assertion on line 12 is passing result.stderr as a custom
assertion message rather than actually checking its contents against the
assertion. Modify the expect call to test both result.stdout and result.stderr
together so that the assertion actually validates both output streams and
correctly detects "bun sync ok" whether it appears in stdout or stderr.

---

Outside diff comments:
In `@src/adapters/cloudflare.ts`:
- Line 68: The adapterUtils function call in the Cloudflare adapter is missing
the opts parameter. In the line where adapterUtils is invoked with only
globalPeers as an argument, add opts as the second parameter (like
adapterUtils(globalPeers, opts)) to ensure the sync backplane configuration from
the user's options is properly wired and passed through to the adapter
utilities, making it consistent with how other adapters handle this call.
🪄 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: d12e4393-4e9f-4380-912a-99af3a64f666

📥 Commits

Reviewing files that changed from the base of the PR and between 2643005 and cddc9ba.

📒 Files selected for processing (18)
  • build.config.ts
  • docs/1.guide/5.pubsub.md
  • package.json
  • src/adapter.ts
  • src/adapters/bun.ts
  • src/adapters/bunny.ts
  • src/adapters/cloudflare.ts
  • src/adapters/deno.ts
  • src/adapters/node.ts
  • src/adapters/sse.ts
  • src/adapters/uws.ts
  • src/index.ts
  • src/peer.ts
  • src/sync.ts
  • src/utils.ts
  • test/adapters/bun.test.ts
  • test/adapters/sync.test.ts
  • test/fixture/bun-sync.ts

Comment thread docs/1.guide/5.pubsub.md Outdated
Comment thread src/adapter.ts
Comment thread src/adapter.ts
Comment thread src/peer.ts
Comment thread src/sync.ts Outdated
Comment thread test/adapters/bun.test.ts Outdated
@pi0
pi0 marked this pull request as ready for review June 18, 2026 13:01
@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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

🧹 Nitpick comments (2)
test/adapters/uws.test.ts (1)

103-105: ⚡ Quick win

Replace the fixed readiness sleep with a condition-based wait.

Line 104 uses setTimeout(50) to wait for open hooks. This can be nondeterministic in CI. Wait for both peers to register (and optionally subscribe) before publishing.

♻️ Proposed change
-    // Let both `open` hooks run so each peer is subscribed before we publish.
-    await new Promise((resolve) => setTimeout(resolve, 50));
+    await waitForPeerCount(server.ws, 2);
🤖 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 `@test/adapters/uws.test.ts` around lines 103 - 105, The setTimeout with a
fixed 50ms delay in the test is unreliable in CI environments. Replace this
fixed delay with a condition-based wait mechanism that actually verifies both
peers have completed their open hooks and are subscribed before the test
proceeds to publish. Instead of relying on time, check for an actual readiness
condition (such as a counter or flag that tracks when both peer subscriptions
are complete) and wait until that condition is satisfied before resolving the
promise.
test/adapters/sync.test.ts (1)

146-147: ⚡ Quick win

Use state-based waiting instead of a fixed sleep for peer registration.

Line 146 relies on setTimeout(50), which can be flaky under slow CI. Poll inst.ws.peers with a bounded timeout before asserting Line 148.

♻️ Proposed change
-      // Let the open hook run so the peer is registered on the server.
-      await new Promise((resolve) => setTimeout(resolve, 50));
+      await waitForPeerCount(inst.ws, 1);
async function waitForPeerCount(
  ws: { peers: Map<string, Set<unknown>> },
  expected: number,
  timeoutMs = 1000,
  intervalMs = 10,
) {
  const start = Date.now();
  for (;;) {
    const count = [...ws.peers.values()].reduce((n, set) => n + set.size, 0);
    if (count === expected) {
      return;
    }
    if (Date.now() - start > timeoutMs) {
      throw new Error(`Timed out waiting for peerCount=${expected}, got ${count}`);
    }
    await new Promise((r) => setTimeout(r, intervalMs));
  }
}
🤖 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 `@test/adapters/sync.test.ts` around lines 146 - 147, Replace the fixed
setTimeout sleep on line 146 with a state-based polling approach. Create a
helper function waitForPeerCount that polls inst.ws.peers with a configurable
timeout and interval, checking if the peer count matches the expected value
before returning or throwing an error if the timeout expires. Call this new
waitForPeerCount helper with inst.ws and the expected peer count instead of the
hardcoded 50ms sleep, ensuring the peer registration state is verified before
the subsequent assertion on line 148.
🤖 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 `@src/sync/broadcast-channel.ts`:
- Around line 30-40: The message event handler in the BroadcastChannel
addEventListener callback only validates that envelope.msg.topic is a string,
but does not validate the namespace and data properties of the SyncMessage
before passing it to deliver(). Add validation to the existing if condition that
checks envelope.id and envelope.msg?.topic to also verify that envelope.msg has
a valid namespace property (should be a string) and a valid data property
(should be a string or undefined). Only call deliver() if all required
properties pass validation, matching the payload validation rigor of text
drivers.

In `@src/sync/encoding.ts`:
- Around line 20-43: The decodeEnvelope function in src/sync/encoding.ts
validates that id, msg, topic, and namespace are strings, but does not validate
that msg.data is a string. Add a check within the existing validation block (the
if statement starting around line 26) to ensure parsed.msg.data is a string
type. If msg.data is not a string, the function should return undefined to
prevent invalid data types from being passed through as SyncMessage.data values.

---

Nitpick comments:
In `@test/adapters/sync.test.ts`:
- Around line 146-147: Replace the fixed setTimeout sleep on line 146 with a
state-based polling approach. Create a helper function waitForPeerCount that
polls inst.ws.peers with a configurable timeout and interval, checking if the
peer count matches the expected value before returning or throwing an error if
the timeout expires. Call this new waitForPeerCount helper with inst.ws and the
expected peer count instead of the hardcoded 50ms sleep, ensuring the peer
registration state is verified before the subsequent assertion on line 148.

In `@test/adapters/uws.test.ts`:
- Around line 103-105: The setTimeout with a fixed 50ms delay in the test is
unreliable in CI environments. Replace this fixed delay with a condition-based
wait mechanism that actually verifies both peers have completed their open hooks
and are subscribed before the test proceeds to publish. Instead of relying on
time, check for an actual readiness condition (such as a counter or flag that
tracks when both peer subscriptions are complete) and wait until that condition
is satisfied before resolving the promise.
🪄 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: e21b5c6c-a266-4f74-89c5-642d52d4c31e

📥 Commits

Reviewing files that changed from the base of the PR and between cddc9ba and ed470e3.

📒 Files selected for processing (19)
  • docs/1.guide/5.pubsub.md
  • docs/1.guide/6.sync.md
  • docs/1.guide/7.resolver.md
  • docs/1.guide/8.proxy.md
  • src/adapter.ts
  • src/adapters/bun.ts
  • src/adapters/deno.ts
  • src/adapters/uws.ts
  • src/peer.ts
  • src/sync.ts
  • src/sync/broadcast-channel.ts
  • src/sync/cluster.ts
  • src/sync/encoding.ts
  • src/sync/pgsql.ts
  • src/sync/redis.ts
  • src/sync/types.ts
  • test/adapters/sync.test.ts
  • test/adapters/uws.test.ts
  • test/sync.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/sync/types.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/adapters/bun.ts
  • src/adapters/uws.ts
  • src/peer.ts
  • src/adapters/deno.ts
  • src/adapter.ts

Comment thread src/sync/broadcast-channel.ts
Comment thread src/sync/encoding.ts
pi0 and others added 3 commits June 30, 2026 19:46
- cloudflare: honor `sync`/`onError` (was silently ignored); wire the
  driver into fallback + durable peers and give fallback peers real local
  fan-out. Inbound delivery into a hibernated Durable Object is best-effort;
  outbound relay is reliable (documented).
- add `onError(error, { stage })` to observe a degraded backplane; centralize
  relay error isolation in adapterUtils so peer/adapter publish fire-and-forget
  through one reporter, and survive a synchronous subscribe throw.
- docs: add "Delivery semantics" (at-most-once, unordered, no replay) + a
  Cloudflare note.
- mark `Peer._publish` @internal; fix `String.fromCharCode(...)` stack overflow
  on large binary payloads in `toString`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the runtime-specific Durable Object / fallback / hibernation notes with
the Cloudflare adapter docs, leaving a short pointer in the sync guide.

Co-Authored-By: Claude Opus 4.8 (1M context) <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: 1

Caution

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

⚠️ Outside diff range comments (2)
src/utils.ts (1)

45-45: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Avoid broad new Uint8Array(data) coercion in serializeMessage() and toString(). Message.data can be a Blob, and this path turns it into empty bytes, so relayed or encoded payloads lose content. Handle ArrayBuffer/typed-array views explicitly or reject unsupported payloads.

🤖 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/utils.ts` at line 45, The payload coercion in
serializeMessage()/toString() is too broad: Message.data may be a Blob, and the
current Uint8Array conversion can silently drop its content. Update the handling
in utils.ts around the data-to-bytes path to explicitly support ArrayBuffer and
typed-array views, and either properly read Blob contents or reject unsupported
payload types rather than falling back to new Uint8Array(data). Use the existing
serializeMessage and toString helpers as the main places to adjust.
src/adapters/cloudflare.ts (1)

300-323: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard fallback fan-out against durable peers in the same namespace. getPeers(globalPeers, namespace) is shared by both upgrade paths, so a custom resolveDurableStub can leave CloudflareDurablePeer and CloudflareFallbackPeer in the same Set. _publish() assumes every peer has wsServer, so it will throw as soon as it hits a durable peer. A runtime/type guard here would keep namespace publish from crashing.

🤖 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/cloudflare.ts` around lines 300 - 323, The fallback fan-out in
CloudflareFallbackPeer._publish assumes every peer in this._internal.peers has
wsServer, but getPeers(globalPeers, namespace) can mix CloudflareFallbackPeer
and CloudflareDurablePeer in the same namespace set. Add a runtime/type guard
inside _publish to only send to peers that actually have a wsServer (or
otherwise narrow to fallback peers) before calling send, while keeping the
existing topic check and send path in CloudflareFallbackPeer.send intact.
🤖 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 `@src/peer.ts`:
- Around line 197-205: The fire-and-forget publish path in Peer.sync.publish
still lets a synchronous driver.publish throw escape, since only async
rejections are currently handled. Update the sync publish wrapper in Peer (and
the adapterUtils-wrapped driver path it uses) so the publish call itself is
protected and both sync throws and Promise rejections are routed to onError,
preserving the existing fire-and-forget behavior without leaking errors.

---

Outside diff comments:
In `@src/adapters/cloudflare.ts`:
- Around line 300-323: The fallback fan-out in CloudflareFallbackPeer._publish
assumes every peer in this._internal.peers has wsServer, but
getPeers(globalPeers, namespace) can mix CloudflareFallbackPeer and
CloudflareDurablePeer in the same namespace set. Add a runtime/type guard inside
_publish to only send to peers that actually have a wsServer (or otherwise
narrow to fallback peers) before calling send, while keeping the existing topic
check and send path in CloudflareFallbackPeer.send intact.

In `@src/utils.ts`:
- Line 45: The payload coercion in serializeMessage()/toString() is too broad:
Message.data may be a Blob, and the current Uint8Array conversion can silently
drop its content. Update the handling in utils.ts around the data-to-bytes path
to explicitly support ArrayBuffer and typed-array views, and either properly
read Blob contents or reject unsupported payload types rather than falling back
to new Uint8Array(data). Use the existing serializeMessage and toString helpers
as the main places to adjust.
🪄 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: 2ab1b234-9a80-40eb-b0d5-8d553e78681d

📥 Commits

Reviewing files that changed from the base of the PR and between a682778 and 067985d.

📒 Files selected for processing (7)
  • docs/1.guide/6.sync.md
  • src/adapter.ts
  • src/adapters/cloudflare.ts
  • src/index.ts
  • src/peer.ts
  • src/utils.ts
  • test/adapters/sync.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/index.ts
  • src/adapter.ts

Comment thread src/peer.ts
pi0 and others added 2 commits June 30, 2026 20:32
Addresses CodeRabbit review on #192:
- adapter: a *synchronous* driver.publish() throw escaped the fire-and-forget
  wrapper into the caller's publish(); now routed to onError like rejections.
- encoding: decodeEnvelope rejects a non-string `data` (a foreign producer
  could leak an object/null through as SyncMessage.data).
- broadcast-channel: validate namespace + data shape, not just topic, matching
  the contract the text drivers enforce via decodeEnvelope.
- docs: reword the sync intro sentence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fallback _publish rewrite fanned out by calling `.send()` on other peers'
sockets, but in the Workers model each WebSocket lives in its own `fetch`
invocation and sending to a socket owned by another request context throws
"Network connection lost" — hanging the Worker (CI timeouts on the fallback
cloudflare suite). Cross-connection pub/sub requires Durable Objects, which is
exactly why the original code was a warn stub. Restore it, drop the misleading
fallback sync wiring, and correct the docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pi0
pi0 merged commit b6f5191 into main Jun 30, 2026
6 checks passed
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