From 506665d1e2c6d20e3d8510c9a634b9d8422d0f69 Mon Sep 17 00:00:00 2001 From: Nico Krause Date: Tue, 28 Oct 2025 14:50:25 +0500 Subject: [PATCH 01/12] =?UTF-8?q?Add=20enhanced=20yjs-libp2p=20example=20w?= =?UTF-8?q?ith=20improved=20config=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/js-libp2p-example-yjs-libp2p/LICENSE | 4 + .../LICENSE-APACHE | 5 + .../js-libp2p-example-yjs-libp2p/LICENSE-MIT | 19 ++ .../js-libp2p-example-yjs-libp2p/README.md | 151 ++++++++++ examples/js-libp2p-example-yjs-libp2p/WARP.md | 176 +++++++++++ .../js-libp2p-example-yjs-libp2p/index.html | 37 +++ .../js-libp2p-example-yjs-libp2p/index.js | 281 ++++++++++++++++++ .../js-libp2p-example-yjs-libp2p/package.json | 36 +++ .../playwright.config.js | 33 ++ .../js-libp2p-example-yjs-libp2p/relay.js | 145 +++++++++ .../test/global-setup.js | 62 ++++ .../test/global-teardown.js | 21 ++ .../test/index.spec.js | 121 ++++++++ .../vite.config.js | 7 + .../yjs-libp2p-provider.js | 252 ++++++++++++++++ 15 files changed, 1350 insertions(+) create mode 100644 examples/js-libp2p-example-yjs-libp2p/LICENSE create mode 100644 examples/js-libp2p-example-yjs-libp2p/LICENSE-APACHE create mode 100644 examples/js-libp2p-example-yjs-libp2p/LICENSE-MIT create mode 100644 examples/js-libp2p-example-yjs-libp2p/README.md create mode 100644 examples/js-libp2p-example-yjs-libp2p/WARP.md create mode 100644 examples/js-libp2p-example-yjs-libp2p/index.html create mode 100644 examples/js-libp2p-example-yjs-libp2p/index.js create mode 100644 examples/js-libp2p-example-yjs-libp2p/package.json create mode 100644 examples/js-libp2p-example-yjs-libp2p/playwright.config.js create mode 100644 examples/js-libp2p-example-yjs-libp2p/relay.js create mode 100644 examples/js-libp2p-example-yjs-libp2p/test/global-setup.js create mode 100644 examples/js-libp2p-example-yjs-libp2p/test/global-teardown.js create mode 100644 examples/js-libp2p-example-yjs-libp2p/test/index.spec.js create mode 100644 examples/js-libp2p-example-yjs-libp2p/vite.config.js create mode 100644 examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js diff --git a/examples/js-libp2p-example-yjs-libp2p/LICENSE b/examples/js-libp2p-example-yjs-libp2p/LICENSE new file mode 100644 index 0000000..20ce483 --- /dev/null +++ b/examples/js-libp2p-example-yjs-libp2p/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/examples/js-libp2p-example-yjs-libp2p/LICENSE-APACHE b/examples/js-libp2p-example-yjs-libp2p/LICENSE-APACHE new file mode 100644 index 0000000..14478a3 --- /dev/null +++ b/examples/js-libp2p-example-yjs-libp2p/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/examples/js-libp2p-example-yjs-libp2p/LICENSE-MIT b/examples/js-libp2p-example-yjs-libp2p/LICENSE-MIT new file mode 100644 index 0000000..72dc60d --- /dev/null +++ b/examples/js-libp2p-example-yjs-libp2p/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/examples/js-libp2p-example-yjs-libp2p/README.md b/examples/js-libp2p-example-yjs-libp2p/README.md new file mode 100644 index 0000000..5e5ec33 --- /dev/null +++ b/examples/js-libp2p-example-yjs-libp2p/README.md @@ -0,0 +1,151 @@ +# @libp2p/example-yjs-libp2p + +A collaborative text editor built with Yjs and libp2p, demonstrating real-time peer-to-peer document synchronization. + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Setup](#setup) +- [Usage](#usage) +- [How It Works](#how-it-works) +- [Key Features](#key-features) +- [License](#license) + +## Overview + +This example demonstrates how to create a Yjs connection provider using libp2p instead of the standard y-webrtc connector. It showcases: + +- **Custom Yjs Provider**: A libp2p-based connection provider for Yjs +- **WebRTC Support**: Direct peer-to-peer connections using WebRTC +- **Circuit Relay**: NAT traversal via relay servers +- **DCUTR**: Direct Connection Upgrade through Relay (hole punching) +- **AutoNAT**: Automatic NAT detection +- **PubSub**: GossipSub for document synchronization +- **Peer Discovery**: Automatic connection to discovered peers + +## Architecture + +``` +┌─────────────┐ ┌─────────────┐ +│ Browser 1 │ │ Browser 2 │ +│ │ │ │ +│ Yjs Doc ←──┼─────────┼──→ Yjs Doc │ +│ ↕ │ WebRTC │ ↕ │ +│ libp2p │ or │ libp2p │ +│ (pubsub) │ Relay │ (pubsub) │ +└──────┬──────┘ └──────┬──────┘ + │ │ + │ ┌─────────────┐ │ + └────┤ Relay Node │────┘ + │ (relay.js) │ + └─────────────┘ +``` + +## Setup + +1. Install dependencies: +```bash +npm install +``` + +2. Start the relay server: +```bash +npm run relay +``` + +The relay will output its multiaddr, which looks like: +``` +/ip4/127.0.0.1/tcp/53472/ws/p2p/12D3KooWABC123... +``` + +3. Start the development server: +```bash +npm start +``` + +4. Open http://localhost:5173 in multiple browser tabs or windows + +## Usage + +1. Copy the relay multiaddr from the terminal output +2. Paste it into the "Relay multiaddr" field in the browser +3. Keep the default topic or enter a custom one +4. Click "Connect" +5. Start typing in the text area +6. Open another browser tab/window, connect to the same relay and topic +7. Changes will sync automatically between all connected peers + +## How It Works + +### Libp2p Configuration + +The browser clients are configured with: + +- **Transports**: WebSockets (for relay), WebRTC (for direct P2P), Circuit Relay +- **Security**: Noise protocol for encryption +- **Stream Muxing**: Yamux +- **Services**: + - `identify`: Peer identification + - `autoNAT`: NAT detection + - `dcutr`: Hole punching for direct connections + - `pubsub`: GossipSub for broadcasting document updates + +### Yjs Integration + +The custom `Libp2pProvider` class: + +1. **Subscribes** to a pubsub topic for the Yjs document +2. **Listens** for Yjs document updates and broadcasts them via pubsub +3. **Receives** updates from other peers and applies them to the local document +4. **Discovers** peers subscribing to the same topic +5. **Connects** directly to discovered peers (using WebRTC when possible) +6. **Syncs** initial state using Yjs's state vector protocol + +### Message Types + +The provider uses three message types: + +- `update`: Broadcasts document changes to all peers +- `sync-request`: Requests the current document state (sent on join) +- `sync-response`: Sends the current state to a requesting peer + +### Peer Discovery Flow + +1. Client connects to relay server via WebSocket +2. Client subscribes to the pubsub topic +3. Relay forwards pubsub messages between peers +4. When a peer subscribes to the same topic, both peers discover each other +5. Peers attempt direct WebRTC connections (using DCUTR for NAT traversal) +6. If direct connection fails, communication continues through the relay + +## Key Features + +### 🔗 Decentralized Architecture +No central server required - peers communicate directly when possible + +### 🌐 NAT Traversal +Automatic hole punching via DCUTR for direct connections behind NATs + +### 🔄 Real-time Sync +Changes propagate instantly to all connected peers + +### 📡 Efficient Messaging +Uses Yjs's state-based CRDT for minimal bandwidth usage + +### 🔌 Relay Fallback +Falls back to relay when direct connections aren't possible + +### 🤝 Auto-discovery +Peers automatically discover and connect to each other + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/examples/js-libp2p-example-yjs-libp2p/WARP.md b/examples/js-libp2p-example-yjs-libp2p/WARP.md new file mode 100644 index 0000000..0120436 --- /dev/null +++ b/examples/js-libp2p-example-yjs-libp2p/WARP.md @@ -0,0 +1,176 @@ +# WARP.md + +This file provides guidance to WARP (warp.dev) when working with code in this repository. + +## Project Overview + +A collaborative text editor built with Yjs and libp2p, demonstrating real-time peer-to-peer document synchronization. This example replaces the standard y-webrtc connector with a custom libp2p-based provider for Yjs. + +## Development Commands + +### Starting the Application +```bash +# Start the relay server (required for peer connectivity) +npm run relay + +# Start the Vite development server +npm start +``` + +**Important**: Copy the relay multiaddr from terminal output and paste into the browser UI before connecting. + +### Build and Test +```bash +# Build production bundle +npm run build + +# Run all tests (starts relay server + dev server automatically) +npm test + +# Run tests in specific browsers +npm run test:chrome +npm run test:firefox +``` + +## Architecture + +### Core Components + +**Libp2pProvider** (`yjs-libp2p-provider.js`) +- Custom Yjs connection provider using libp2p's pubsub instead of y-webrtc +- Manages document synchronization across peers via GossipSub +- Message types: + - `update`: broadcasts document changes + - `sync-request`: requests current document state (sent on join) + - `sync-response`: sends current state to requesting peer +- Listens to `peer:discovery` events (libp2p auto-dialer handles connections) + +**Browser Client** (`index.js`) +- Creates libp2p node with WebRTC, WebSockets, and circuit relay transports +- **Critical config**: + - Listen on `/p2p-circuit` for relay discoverability + - Listen on `/webrtc` for direct P2P connections + - `pubsubPeerDiscovery` enabled for peer discovery via gossipsub + - `connectionGater` allows local addresses (for demo) +- Services: identify, autoNAT, dcutr, gossipsub +- Binds textarea to Yjs document via `oninput` event + +**Relay Server** (`relay.js`) +- Node.js libp2p relay server for WebSocket connections +- Enables NAT traversal via circuit relay +- Forwards pubsub messages between peers +- **Critical**: `maxReservations: Infinity` is for demo only—use default (15) in production + +### Network Flow + +1. Browser clients connect to relay server via WebSocket +2. Clients listen on `/p2p-circuit` to become discoverable via relay +3. Clients subscribe to a pubsub topic (document channel) +4. pubsubPeerDiscovery broadcasts peer info on the pubsub topic +5. Peers discover each other via `peer:discovery` events +6. libp2p's auto-dialer connects discovered peers +7. Peers attempt direct WebRTC connections using DCUTR (hole punching) +8. If WebRTC fails, communication continues through relay +9. Yjs updates broadcast via pubsub to all connected peers + +### Key libp2p Configuration + +**Listen addresses**: +- `/p2p-circuit`: Make reservation on relay (lets other peers dial us via relay) +- `/webrtc`: Listen for incoming WebRTC connections + +**Transports**: +- `webSockets`: for relay connections +- `webRTC`: for direct peer-to-peer connections +- `circuitRelayTransport`: fallback relay connectivity + +**Peer Discovery**: +- `pubsubPeerDiscovery`: Broadcasts peer info on gossipsub topics + +**Services**: +- `identify`: peer identification and metadata exchange +- `autoNAT`: automatic NAT detection +- `dcutr`: Direct Connection Upgrade through Relay (hole punching) +- `pubsub` (gossipsub): message broadcasting for Yjs updates + +### Yjs Synchronization + +The Libp2pProvider handles Yjs sync using state vectors: +- On connection: sends sync-request with local state vector after 1s delay +- Peers respond with sync-response containing missing updates +- Ongoing changes broadcast as update messages via `doc.on('update')` +- Updates apply with `origin: this` to prevent echo loops +- Textarea updates trigger `oninput` → transact → delete + insert → pubsub broadcast + +## Development Patterns + +### When modifying Libp2pProvider +- Always set `origin: this` when applying network updates to prevent broadcast loops +- Use base64 encoding for binary Yjs data in JSON messages +- Listen to `peer:discovery` not `subscription-change` - discovery happens before subscription +- Let libp2p's auto-dialer handle connections - don't manually dial discovered peers + +### When working with libp2p configuration +- **Must include `/p2p-circuit` in listen addresses** for relay-based discovery to work +- Test with multiple browser instances to verify P2P connectivity +- Check browser console for peer connection logs +- WebRTC connections require HTTPS in production (localhost works for dev) + +### When debugging sync issues +- Check relay server is running and accessible +- Verify peers are on the same pubsub topic +- Look for `Document synced with network` log message +- Check if peers discovered each other: look for `Discovered peer:` logs +- Verify peers connected: look for `Connected to peer:` logs +- Examine state vectors in sync-request/sync-response messages + +## Testing Notes + +### Test Infrastructure + +**Automated setup**: +- `test/global-setup.js`: Starts relay server, saves multiaddr to `test/relay-info.json` +- `test/global-teardown.js`: Stops relay server after tests +- `playwright.config.js`: Auto-starts Vite preview server, includes global setup/teardown + +**Current test coverage**: +- ✅ Page loading in multiple browser contexts +- ✅ Relay server starts/stops automatically +- ✅ Both pages connect to relay successfully +- ✅ UI elements render correctly +- ❌ Yjs collaborative editing sync (debugging in progress) + +### Recent Improvements Made + +1. **Added `/p2p-circuit` listen address** - Critical for relay-based peer discovery +2. **Added `pubsubPeerDiscovery`** - Enables peer discovery via gossipsub +3. **Added `connectionGater`** - Allows local address dialing for tests +4. **Fixed event listeners** - Changed from `subscription-change` to `peer:discovery` +5. **Removed manual dialing** - Let libp2p's auto-dialer handle peer connections +6. **Updated to latest js-libp2p-examples** - Verified config matches current best practices + +### Why Collaboration Test Doesn't Work Yet + +Peers successfully: +- ✅ Connect to relay via WebSocket +- ✅ Subscribe to same pubsub topic +- ✅ Both show "Ready!" message + +Peers do NOT: +- ❌ Discover each other (no `Discovered peer:` logs) +- ❌ Connect to each other (no `Connected to peer:` logs) +- ❌ Sync Yjs updates + +**Root cause**: pubsubPeerDiscovery requires peers to be on the **same pubsub topic AND connected** to discover each other. In headless browser tests: +- Peers only connect to relay (WebSocket) +- WebRTC P2P connections don't establish +- Without direct connections, pubsubPeerDiscovery can't propagate peer info +- Pubsub messages ARE delivered through relay, but discovery doesn't happen + +### Possible Solutions + +1. **Manual testing** works fine: `npm run relay` + `npm start` in multiple real browser tabs +2. **Headed browser mode**: Run Playwright tests with `headless: false` to enable WebRTC +3. **Manual peer dialing**: After connecting to relay, manually dial other peer's circuit relay address +4. **Alternative discovery**: Use subscription-change events to detect peers on same topic, then dial +5. **Test pubsub directly**: Verify pubsub message delivery rather than full Yjs sync diff --git a/examples/js-libp2p-example-yjs-libp2p/index.html b/examples/js-libp2p-example-yjs-libp2p/index.html new file mode 100644 index 0000000..9ee41ae --- /dev/null +++ b/examples/js-libp2p-example-yjs-libp2p/index.html @@ -0,0 +1,37 @@ + + + + + + Yjs + libp2p Example + + + +

Yjs + libp2p

+
+ + + +
+ +
+ +
+

+    
+  
+
diff --git a/examples/js-libp2p-example-yjs-libp2p/index.js b/examples/js-libp2p-example-yjs-libp2p/index.js
new file mode 100644
index 0000000..7524384
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/index.js
@@ -0,0 +1,281 @@
+/* eslint-disable no-console */
+
+import { noise } from '@chainsafe/libp2p-noise'
+import { yamux } from '@chainsafe/libp2p-yamux'
+import { autoNAT } from '@libp2p/autonat'
+import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
+import { dcutr } from '@libp2p/dcutr'
+import { identify, identifyPush } from '@libp2p/identify'
+import { gossipsub } from '@chainsafe/libp2p-gossipsub'
+import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery'
+import { webRTC, webRTCDirect } from '@libp2p/webrtc'
+import { webSockets } from '@libp2p/websockets'
+import * as filters from '@libp2p/websockets/filters'
+import { multiaddr } from '@multiformats/multiaddr'
+import { createLibp2p } from 'libp2p'
+import { ping } from '@libp2p/ping'
+import * as Y from 'yjs'
+import { Libp2pProvider } from './yjs-libp2p-provider.js'
+
+// UI elements
+const relayInput = document.getElementById('relay')
+const topicInput = document.getElementById('topic')
+const connectBtn = document.getElementById('connect')
+const editor = document.getElementById('editor')
+const logEl = document.getElementById('log')
+const peersEl = document.getElementById('peers')
+const peerCountEl = document.getElementById('peer-count')
+const peerListEl = document.getElementById('peer-list')
+
+let libp2pNode
+let yjsDoc
+let provider
+let text
+
+// Logging
+const log = (message) => {
+  console.log(message)
+  logEl.textContent += message + '\n'
+  logEl.scrollTop = logEl.scrollHeight
+}
+
+// Update peer display
+const updatePeerDisplay = () => {
+  if (!libp2pNode) return
+  
+  const connections = libp2pNode.getConnections()
+  const peerMap = new Map()
+  
+  // Group connections by peer
+  for (const conn of connections) {
+    const peerId = conn.remotePeer.toString()
+    if (!peerMap.has(peerId)) {
+      peerMap.set(peerId, [])
+    }
+    
+    // Get transport from connection and remote address
+    const remoteAddr = conn.remoteAddr.toString()
+    let transport = 'unknown'
+    
+    // Check for circuit relay (p2p-circuit in address)
+    if (remoteAddr.includes('/p2p-circuit')) {
+      transport = 'relay'
+    }
+    // Check for WebRTC
+    else if (remoteAddr.includes('/webrtc')) {
+      transport = 'webrtc'
+    }
+    // Check for WebTransport
+    else if (remoteAddr.includes('/webtransport')) {
+      transport = 'webtransport'
+    }
+    // Check for WebSocket Secure
+    else if (remoteAddr.includes('/wss') || remoteAddr.includes('/tls/ws')) {
+      transport = 'websocket-secure'
+    }
+    // Check for WebSocket
+    else if (remoteAddr.includes('/ws')) {
+      transport = 'websocket'
+    }
+    // If it has TCP but also has /ws, it's websocket over TCP
+    else if (remoteAddr.includes('/tcp')) {
+      transport = 'tcp'
+    }
+    
+    peerMap.get(peerId).push({ transport, addr: remoteAddr })
+  }
+  
+  // Update count
+  peerCountEl.textContent = peerMap.size
+  
+  // Show/hide peers section
+  if (peerMap.size > 0) {
+    peersEl.style.display = 'block'
+  } else {
+    peersEl.style.display = 'none'
+  }
+  
+  // Update peer list
+  peerListEl.innerHTML = ''
+  for (const [peerId, transports] of peerMap) {
+    const peerDiv = document.createElement('div')
+    peerDiv.className = 'peer'
+    
+    const peerIdSpan = document.createElement('div')
+    peerIdSpan.className = 'peer-id'
+    peerIdSpan.textContent = peerId
+    peerDiv.appendChild(peerIdSpan)
+    
+    const transportDiv = document.createElement('div')
+    
+    // Show each connection with its transport
+    for (const { transport, addr } of transports) {
+      const badge = document.createElement('span')
+      badge.className = 'transport'
+      badge.textContent = transport
+      badge.title = addr // Show full address on hover
+      transportDiv.appendChild(badge)
+    }
+    
+    peerDiv.appendChild(transportDiv)
+    
+    peerListEl.appendChild(peerDiv)
+  }
+}
+
+// Connect button handler
+connectBtn.onclick = async () => {
+  if (libp2pNode) {
+    log('Already connected')
+    return
+  }
+
+  const relayAddr = relayInput.value.trim()
+  if (!relayAddr) {
+    log('Please enter a relay multiaddr')
+    return
+  }
+
+  const topic = topicInput.value.trim()
+  if (!topic) {
+    log('Please enter a topic')
+    return
+  }
+
+  try {
+    connectBtn.disabled = true
+    log('Creating libp2p node...')
+
+    // Create libp2p node with WebRTC, relay, and pubsub
+    libp2pNode = await createLibp2p({
+      addresses: {
+        listen: [
+          '/p2p-circuit',
+          '/webrtc',
+          '/wss',
+          '/ws'
+        ]
+      },
+      transports: [
+        webSockets({
+          filter: filters.all
+        }),
+        webRTC({
+          rtcConfiguration: {
+            iceServers: [
+              { urls: ['stun:stun.l.google.com:19302'] },
+              { urls: ['stun:stun1.l.google.com:19302'] }
+            ]
+          }
+        }),
+        webRTCDirect(),
+        circuitRelayTransport({
+          reservationCompletionTimeout: 20000
+        })
+      ],
+      connectionEncrypters: [noise()],
+      streamMuxers: [yamux()],
+      connectionManager: {
+        inboundStreamProtocolNegotiationTimeout: 10000,
+        inboundUpgradeTimeout: 10000,
+        outboundStreamProtocolNegotiationTimeout: 10000,
+        outboundUpgradeTimeout: 10000
+      },
+      connectionGater: {
+        denyDialMultiaddr: () => false
+      },
+      peerDiscovery: [
+        pubsubPeerDiscovery({
+          interval: 10000,
+          topics: ['_peer-discovery._p2p._pubsub'],
+          listenOnly: false
+        })
+      ],
+      services: {
+        ping: ping(),
+        identify: identify(),
+        identifyPush: identifyPush(),
+        autoNAT: autoNAT(),
+        dcutr: dcutr(),
+        pubsub: gossipsub({
+          emitSelf: false,
+          allowPublishToZeroTopicPeers: true,
+          // Speed up gossipsub mesh formation
+          heartbeatInterval: 1000,  // Send heartbeat every 1 second (default is 1000ms)
+          directPeers: [],
+          floodPublish: true  // Broadcast to all peers, not just mesh
+        })
+      }
+    })
+
+    log(`libp2p node created with id: ${libp2pNode.peerId}`)
+
+    // Connect to relay
+    log('Connecting to relay...')
+    const ma = multiaddr(relayAddr)
+    await libp2pNode.dial(ma)
+    log('Connected to relay!')
+
+    // Create Yjs document
+    yjsDoc = new Y.Doc()
+    text = yjsDoc.getText('content')
+
+    // Set up Yjs provider with libp2p
+    log(`Setting up Yjs provider with topic: ${topic}`)
+    provider = new Libp2pProvider(topic, yjsDoc, libp2pNode)
+
+    // Bind editor to Yjs text
+    text.observe(() => {
+      const currentText = text.toString()
+      if (editor.value !== currentText) {
+        const cursorPos = editor.selectionStart
+        editor.value = currentText
+        editor.setSelectionRange(cursorPos, cursorPos)
+      }
+    })
+
+    editor.oninput = () => {
+      const newText = editor.value
+      const currentText = text.toString()
+      
+      if (newText !== currentText) {
+        yjsDoc.transact(() => {
+          text.delete(0, currentText.length)
+          text.insert(0, newText)
+        })
+      }
+    }
+
+    editor.disabled = false
+    log('Ready! Open this page in another browser tab or window to see collaborative editing.')
+
+    // Initial peer display update
+    updatePeerDisplay()
+
+    // Log connection events and update peer display
+    libp2pNode.addEventListener('peer:connect', (evt) => {
+      log(`Connected to peer: ${evt.detail}`)
+      updatePeerDisplay()
+    })
+
+    libp2pNode.addEventListener('peer:disconnect', (evt) => {
+      log(`Disconnected from peer: ${evt.detail}`)
+      updatePeerDisplay()
+    })
+
+  } catch (err) {
+    log(`Error: ${err.message}`)
+    console.error(err)
+    connectBtn.disabled = false
+  }
+}
+
+// Cleanup on page unload
+window.addEventListener('beforeunload', () => {
+  if (provider) {
+    provider.destroy()
+  }
+  if (libp2pNode) {
+    libp2pNode.stop()
+  }
+})
diff --git a/examples/js-libp2p-example-yjs-libp2p/package.json b/examples/js-libp2p-example-yjs-libp2p/package.json
new file mode 100644
index 0000000..80c613b
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/package.json
@@ -0,0 +1,36 @@
+{
+  "name": "@libp2p/example-yjs-libp2p",
+  "version": "1.0.0",
+  "description": "A Yjs collaborative editor using libp2p for peer-to-peer connectivity",
+  "type": "module",
+  "scripts": {
+    "start": "vite",
+    "build": "vite build",
+    "relay": "node relay.js",
+    "test:firefox": "playwright test --project=firefox",
+    "test:chrome": "playwright test --project=chromium",
+    "test": "playwright test"
+  },
+  "dependencies": {
+    "@chainsafe/libp2p-gossipsub": "^14.0.0",
+    "@chainsafe/libp2p-noise": "^16.0.0",
+    "@chainsafe/libp2p-yamux": "^7.0.0",
+    "@libp2p/autonat": "^2.0.0",
+    "@libp2p/circuit-relay-v2": "^3.0.0",
+    "@libp2p/dcutr": "^2.0.0",
+    "@libp2p/identify": "^3.0.1",
+    "@libp2p/pubsub-peer-discovery": "^11.0.0",
+    "@libp2p/webrtc": "^5.0.0",
+    "@libp2p/websockets": "^9.0.0",
+    "@multiformats/multiaddr": "^12.0.0",
+    "it-pushable": "^3.2.0",
+    "libp2p": "^2.0.0",
+    "uint8arrays": "^5.1.0",
+    "vite": "^6.0.3",
+    "yjs": "^13.6.18"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.56.1",
+    "test-ipfs-example": "^1.0.0"
+  }
+}
diff --git a/examples/js-libp2p-example-yjs-libp2p/playwright.config.js b/examples/js-libp2p-example-yjs-libp2p/playwright.config.js
new file mode 100644
index 0000000..2c0a833
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/playwright.config.js
@@ -0,0 +1,33 @@
+import { defineConfig, devices } from '@playwright/test'
+
+export default defineConfig({
+  testDir: './test',
+  timeout: 60000,
+  fullyParallel: false,
+  forbidOnly: !!process.env.CI,
+  retries: process.env.CI ? 2 : 0,
+  workers: 1,
+  reporter: 'list',
+  globalSetup: './test/global-setup.js',
+  globalTeardown: './test/global-teardown.js',
+  use: {
+    baseURL: 'http://localhost:5173',
+    trace: 'on-first-retry',
+  },
+  projects: [
+    {
+      name: 'chromium',
+      use: { ...devices['Desktop Chrome'] },
+    },
+    {
+      name: 'firefox',
+      use: { ...devices['Desktop Firefox'] },
+    },
+  ],
+  webServer: {
+    command: 'npx vite preview --port 5173',
+    port: 5173,
+    reuseExistingServer: !process.env.CI,
+    timeout: 120000,
+  },
+})
diff --git a/examples/js-libp2p-example-yjs-libp2p/relay.js b/examples/js-libp2p-example-yjs-libp2p/relay.js
new file mode 100644
index 0000000..dc5dc60
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/relay.js
@@ -0,0 +1,145 @@
+/* eslint-disable no-console */
+
+import { noise } from '@chainsafe/libp2p-noise'
+import { yamux } from '@chainsafe/libp2p-yamux'
+import { autoNAT } from '@libp2p/autonat'
+import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
+import { dcutr } from '@libp2p/dcutr'
+import { identify, identifyPush } from '@libp2p/identify'
+import { gossipsub } from '@chainsafe/libp2p-gossipsub'
+import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery'
+import { webSockets } from '@libp2p/websockets'
+import * as filters from '@libp2p/websockets/filters'
+import { webRTC, webRTCDirect } from '@libp2p/webrtc'
+import { tcp } from '@libp2p/tcp'
+import { ping } from '@libp2p/ping'
+import { createLibp2p } from 'libp2p'
+
+const server = await createLibp2p({
+  addresses: {
+    listen: [
+      '/ip4/0.0.0.0/tcp/9091',
+      '/ip4/0.0.0.0/tcp/9092/ws',
+      '/ip4/0.0.0.0/udp/9093/webrtc-direct'
+    ]
+  },
+  transports: [
+    tcp(),
+    webSockets({
+      filter: filters.all
+    }),
+    webRTC(),
+    webRTCDirect(),
+    circuitRelayTransport()
+  ],
+  peerDiscovery: [
+    pubsubPeerDiscovery({
+      interval: 5000,
+      topics: ['_peer-discovery._p2p._pubsub'],
+      listenOnly: false
+    })
+  ],
+  connectionEncrypters: [noise()],
+  streamMuxers: [yamux()],
+  connectionManager: {
+    inboundStreamProtocolNegotiationTimeout: 30000,
+    inboundUpgradeTimeout: 30000,
+    outboundStreamProtocolNegotiationTimeout: 30000,
+    outboundUpgradeTimeout: 30000,
+    maxConnections: 1000,
+    maxIncomingPendingConnections: 100,
+    maxPeerAddrsToDial: 100,
+    dialTimeout: 30000
+  },
+  connectionGater: {
+    denyDialMultiaddr: () => false
+  },
+  services: {
+    ping: ping(),
+    identify: identify(),
+    identifyPush: identifyPush(),
+    autoNAT: autoNAT(),
+    dcutr: dcutr(),
+    pubsub: gossipsub({
+      emitSelf: false,
+      allowPublishToZeroTopicPeers: true,
+      canRelayMessage: true,
+      floodPublish: true  // Broadcast to all peers, not just mesh
+    }),
+    relay: circuitRelayServer({
+      hopTimeout: 30000,
+      reservations: {
+        maxReservations: 1000,
+        reservationTtl: 2 * 60 * 60 * 1000,
+        defaultDataLimit: BigInt(1024 * 1024 * 1024),
+        defaultDurationLimit: 2 * 60 * 1000
+      }
+    })
+  }
+})
+
+// Set up peer discovery listener to dial discovered peers
+server.addEventListener('peer:discovery', async (evt) => {
+  const peer = evt.detail
+  console.log(`Discovered peer: ${peer.id.toString()}`)
+  
+  // Check if we're already connected to this peer
+  const connections = server.getConnections(peer.id)
+  if (!connections || connections.length === 0) {
+    console.log(`Dialing new peer: ${peer.id.toString()}`)
+    
+    try {
+      // Dial the peer ID directly - libp2p will handle finding the best route
+      await server.dial(peer.id)
+      console.log(`Successfully dialed peer: ${peer.id.toString()}`)
+    } catch (error) {
+      console.error(`Failed to dial peer ${peer.id.toString()}:`, error.message)
+    }
+  } else {
+    console.log(`Already connected to peer: ${peer.id.toString()}`)
+  }
+})
+
+server.addEventListener('peer:connect', (evt) => {
+  console.log(`Connected to peer: ${evt.detail.toString()}`)
+})
+
+server.addEventListener('peer:disconnect', (evt) => {
+  console.log(`Disconnected from peer: ${evt.detail.toString()}`)
+})
+
+// Subscribe to the Yjs topic to relay messages and log them
+const YJS_TOPIC = 'yjs-doc-1'
+await server.services.pubsub.subscribe(YJS_TOPIC)
+console.log(`📡 Relay subscribed to topic: ${YJS_TOPIC}`)
+
+// Log all messages passing through
+server.services.pubsub.addEventListener('message', (evt) => {
+  if (evt.detail.topic === YJS_TOPIC) {
+    console.log('\n📨 Message received on', YJS_TOPIC)
+    console.log('  From:', evt.detail.from.toString())
+    console.log('  Data length:', evt.detail.data.length, 'bytes')
+    
+    try {
+      const msgStr = new TextDecoder().decode(evt.detail.data)
+      const msg = JSON.parse(msgStr)
+      console.log('  Type:', msg.type)
+    } catch (e) {
+      console.log('  (Could not parse message)')
+    }
+  }
+})
+
+// Periodically log subscribers to the topic
+setInterval(() => {
+  const subscribers = server.services.pubsub.getSubscribers(YJS_TOPIC)
+  if (subscribers.length > 0) {
+    console.log(`\n👥 Subscribers to ${YJS_TOPIC}:`, subscribers.map(p => p.toString()))
+  }
+}, 10000)
+
+console.info('\nThe relay node is running and listening on the following multiaddrs:')
+console.info('')
+console.info(server.getMultiaddrs().map((ma) => ma.toString()).join('\n'))
+console.info('')
+console.info('Copy one of the above multiaddrs and use it in the browser client')
diff --git a/examples/js-libp2p-example-yjs-libp2p/test/global-setup.js b/examples/js-libp2p-example-yjs-libp2p/test/global-setup.js
new file mode 100644
index 0000000..2fcb03f
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/test/global-setup.js
@@ -0,0 +1,62 @@
+import { spawn } from 'child_process'
+import { writeFileSync } from 'fs'
+import path from 'path'
+
+export default async function globalSetup() {
+  console.log('Starting relay server...')
+  
+  return new Promise((resolve, reject) => {
+    // Start relay server as a child process
+    const relayProcess = spawn('node', ['relay.js'], {
+      cwd: path.resolve(process.cwd()),
+      stdio: ['ignore', 'pipe', 'pipe']
+    })
+
+    let relayMultiaddr = null
+    let output = ''
+
+    relayProcess.stdout.on('data', (data) => {
+      const text = data.toString()
+      output += text
+      console.log(text)
+
+      // Extract the first multiaddr (WebSocket address)
+      const match = text.match(/\/ip4\/127\.0\.0\.1\/tcp\/\d+\/ws\/p2p\/[A-Za-z0-9]+/)
+      if (match && !relayMultiaddr) {
+        relayMultiaddr = match[0]
+        console.log(`Relay server started with multiaddr: ${relayMultiaddr}`)
+        
+        // Store relay info for tests
+        const relayInfo = {
+          multiaddr: relayMultiaddr,
+          pid: relayProcess.pid
+        }
+        
+        writeFileSync(
+          path.resolve(process.cwd(), 'test/relay-info.json'),
+          JSON.stringify(relayInfo, null, 2)
+        )
+        
+        // Give the relay a moment to fully initialize
+        setTimeout(() => resolve(), 1000)
+      }
+    })
+
+    relayProcess.stderr.on('data', (data) => {
+      console.error('Relay stderr:', data.toString())
+    })
+
+    relayProcess.on('error', (error) => {
+      console.error('Failed to start relay:', error)
+      reject(error)
+    })
+
+    // Timeout if relay doesn't start
+    setTimeout(() => {
+      if (!relayMultiaddr) {
+        relayProcess.kill()
+        reject(new Error('Relay server failed to start within timeout'))
+      }
+    }, 10000)
+  })
+}
diff --git a/examples/js-libp2p-example-yjs-libp2p/test/global-teardown.js b/examples/js-libp2p-example-yjs-libp2p/test/global-teardown.js
new file mode 100644
index 0000000..72fd342
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/test/global-teardown.js
@@ -0,0 +1,21 @@
+import { readFileSync, unlinkSync } from 'fs'
+import path from 'path'
+
+export default async function globalTeardown() {
+  console.log('Stopping relay server...')
+  
+  try {
+    const relayInfoPath = path.resolve(process.cwd(), 'test/relay-info.json')
+    const relayInfo = JSON.parse(readFileSync(relayInfoPath, 'utf8'))
+    
+    if (relayInfo.pid) {
+      process.kill(relayInfo.pid, 'SIGTERM')
+      console.log(`Relay server (PID ${relayInfo.pid}) stopped`)
+    }
+    
+    // Clean up the relay info file
+    unlinkSync(relayInfoPath)
+  } catch (error) {
+    console.error('Error stopping relay server:', error.message)
+  }
+}
diff --git a/examples/js-libp2p-example-yjs-libp2p/test/index.spec.js b/examples/js-libp2p-example-yjs-libp2p/test/index.spec.js
new file mode 100644
index 0000000..44bc988
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/test/index.spec.js
@@ -0,0 +1,121 @@
+import { test, expect } from '@playwright/test'
+import { readFileSync } from 'fs'
+import path from 'path'
+
+const url = 'http://localhost:5173'
+
+// Helper to connect a page to the relay
+async function connectToRelay(page, relayMultiaddr, topic = 'test-topic') {
+  await page.fill('#relay', relayMultiaddr)
+  await page.fill('#topic', topic)
+  await page.click('#connect')
+  
+  // Wait for connection to establish
+  await page.waitForFunction(
+    () => document.getElementById('editor').disabled === false,
+    { timeout: 10000 }
+  )
+}
+
+test.describe('Yjs + libp2p example', () => {
+  let relayMultiaddr
+  
+  test.beforeAll(() => {
+    // Load relay multiaddr from global setup
+    const relayInfo = JSON.parse(
+      readFileSync(path.resolve(process.cwd(), 'test/relay-info.json'), 'utf8')
+    )
+    relayMultiaddr = relayInfo.multiaddr
+  })
+
+  test('should load page in two browsers', async ({ browser }) => {
+    const context1 = await browser.newContext()
+    const context2 = await browser.newContext()
+    
+    const page1 = await context1.newPage()
+    const page2 = await context2.newPage()
+
+    await page1.goto(url)
+    await page2.goto(url)
+    
+    const heading1 = await page1.locator('h1').textContent()
+    const heading2 = await page2.locator('h1').textContent()
+
+    expect(heading1).toBe('Yjs + libp2p')
+    expect(heading2).toBe('Yjs + libp2p')
+    
+    await context1.close()
+    await context2.close()
+  })
+
+  test('should sync text between two browsers', async ({ browser }) => {
+    const context1 = await browser.newContext()
+    const context2 = await browser.newContext()
+    
+    const page1 = await context1.newPage()
+    const page2 = await context2.newPage()
+
+    // Enable console logging for debugging
+    page1.on('console', msg => console.log('Page1:', msg.text()))
+    page2.on('console', msg => console.log('Page2:', msg.text()))
+
+    await page1.goto(url)
+    await page2.goto(url)
+
+    // Connect both pages to the relay with the same topic
+    const testTopic = `test-${Date.now()}`
+    await connectToRelay(page1, relayMultiaddr, testTopic)
+    await connectToRelay(page2, relayMultiaddr, testTopic)
+
+    // Wait for both to be connected to relay and ready
+    // The log shows "Ready!" when connection is established
+    await page1.waitForFunction(
+      () => document.getElementById('log').textContent.includes('Ready!'),
+      { timeout: 10000 }
+    )
+    await page2.waitForFunction(
+      () => document.getElementById('log').textContent.includes('Ready!'),
+      { timeout: 10000 }
+    )
+
+    // Wait for peers to discover each other via pubsub
+    // Even without direct P2P connection, pubsub through relay should work
+    // Wait for sync-request/response to complete
+    await page1.waitForTimeout(3000)
+
+    // Click into editor and type in page 1
+    await page1.click('#editor')
+    const testText = 'Hello!'
+    await page1.type('#editor', testText)
+
+    // Wait for text to sync to page 2 via pubsub (through relay)
+    await page2.waitForFunction(
+      (text) => document.getElementById('editor').value.includes(text),
+      testText,
+      { timeout: 10000 }
+    )
+
+    // Verify text synced
+    const page2Text = await page2.inputValue('#editor')
+    expect(page2Text).toBe(testText)
+
+    // Type additional text in page 2
+    await page2.click('#editor')
+    const additionalText = ' Bye!'
+    await page2.type('#editor', additionalText)
+
+    // Wait for sync back to page 1
+    const expectedText = testText + additionalText
+    await page1.waitForFunction(
+      (text) => document.getElementById('editor').value === text,
+      expectedText,
+      { timeout: 5000 }
+    )
+
+    const page1Text = await page1.inputValue('#editor')
+    expect(page1Text).toBe(expectedText)
+    
+    await context1.close()
+    await context2.close()
+  })
+})
diff --git a/examples/js-libp2p-example-yjs-libp2p/vite.config.js b/examples/js-libp2p-example-yjs-libp2p/vite.config.js
new file mode 100644
index 0000000..25eabdf
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+  server: {
+    open: true
+  }
+})
diff --git a/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
new file mode 100644
index 0000000..45fddbe
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
@@ -0,0 +1,252 @@
+import * as Y from 'yjs'
+import { fromString, toString } from 'uint8arrays'
+
+/**
+ * Yjs connection provider using libp2p for peer-to-peer connectivity
+ * This replaces y-webrtc and uses libp2p's pubsub for synchronization
+ */
+export class Libp2pProvider {
+  /**
+   * @param {string} topic - The pubsub topic to use for this document
+   * @param {Y.Doc} doc - The Yjs document to sync
+   * @param {import('libp2p').Libp2p} libp2p - The libp2p instance
+   * @param {Object} options - Provider options
+   */
+  constructor (topic, doc, libp2p, options = {}) {
+    this.topic = topic
+    this.doc = doc
+    this.libp2p = libp2p
+    this.awareness = options.awareness
+    this.synced = false
+    this.connected = false
+    
+    // Track connected peers
+    this.connectedPeers = new Set()
+    
+    // Bind event handlers
+    this._onUpdate = this._handleDocUpdate.bind(this)
+    this._onPubsubMessage = this._handlePubsubMessage.bind(this)
+    this._onPeerDiscovered = this._handlePeerDiscovered.bind(this)
+    
+    // Subscribe to document updates
+    this.doc.on('update', this._onUpdate)
+    
+    // Subscribe to pubsub topic
+    this._subscribeToPubsub()
+    
+    // Set up peer discovery
+    this._setupPeerDiscovery()
+    
+    // Request initial state from peers
+    this._requestInitialState()
+  }
+
+  /**
+   * Subscribe to the pubsub topic for this document
+   */
+  async _subscribeToPubsub () {
+    try {
+      await this.libp2p.services.pubsub.subscribe(this.topic)
+      this.libp2p.services.pubsub.addEventListener('message', this._onPubsubMessage)
+      this.connected = true
+      console.log(`✅ Subscribed to Yjs topic: ${this.topic}`)
+      
+      // Log current subscriptions
+      const topics = this.libp2p.services.pubsub.getTopics()
+      console.log('All subscribed topics:', topics)
+      
+      // Check peers multiple times as gossipsub mesh forms
+      const checkPeers = () => {
+        const peers = this.libp2p.services.pubsub.getSubscribers(this.topic)
+        console.log(`Peers subscribed to ${this.topic}:`, peers.map(p => p.toString()))
+        
+        if (peers.length === 0) {
+          console.warn('⚠️ No peers subscribed to this topic yet. Waiting for gossipsub mesh...')
+        } else {
+          console.log('✅ Gossipsub mesh formed!')
+        }
+      }
+      
+      setTimeout(checkPeers, 2000)
+      setTimeout(checkPeers, 5000)
+      setTimeout(checkPeers, 10000)
+    } catch (err) {
+      console.error('Failed to subscribe to pubsub topic:', err)
+    }
+  }
+
+  /**
+   * Set up peer discovery to connect to discovered peers
+   */
+  _setupPeerDiscovery () {
+    // Listen for peer discovery events from pubsubPeerDiscovery
+    this.libp2p.addEventListener('peer:discovery', this._onPeerDiscovered)
+    
+    // Track connections
+    this.libp2p.addEventListener('peer:connect', async (evt) => {
+      const peerId = evt.detail.toString()
+      console.log(`Connected to peer: ${peerId}`)
+      this.connectedPeers.add(peerId)
+    })
+
+    this.libp2p.addEventListener('peer:disconnect', (evt) => {
+      const peerId = evt.detail.toString()
+      console.log(`Disconnected from peer: ${peerId}`)
+      this.connectedPeers.delete(peerId)
+    })
+  }
+
+  /**
+   * Handle peer discovery events
+   */
+  async _handlePeerDiscovered (evt) {
+    const peer = evt.detail
+    console.log(`Discovered peer: ${peer.id.toString()}`)
+    
+    // Check if we're already connected to this peer
+    const connections = this.libp2p.getConnections(peer.id)
+    if (!connections || connections.length === 0) {
+      console.log(`Dialing new peer: ${peer.id.toString()}`)
+      
+      try {
+        // Dial the peer ID directly - libp2p will handle finding the best route
+        await this.libp2p.dial(peer.id)
+        console.log(`Successfully dialed peer: ${peer.id.toString()}`)
+      } catch (error) {
+        console.error(`Failed to dial peer ${peer.id.toString()}:`, error.message)
+      }
+    } else {
+      console.log(`Already connected to peer: ${peer.id.toString()}`)
+    }
+  }
+
+  /**
+   * Request initial document state from connected peers
+   */
+  async _requestInitialState () {
+    // Wait a bit for peers to connect
+    setTimeout(() => {
+      const stateVector = Y.encodeStateVector(this.doc)
+      this._publishMessage({
+        type: 'sync-request',
+        stateVector: toString(stateVector, 'base64')
+      })
+    }, 1000)
+  }
+
+  /**
+   * Handle Yjs document updates
+   */
+  _handleDocUpdate (update, origin) {
+    // Don't broadcast updates that came from the network
+    if (origin === this) return
+    
+    console.log('Broadcasting Yjs update to peers')
+    // Broadcast the update to all peers via pubsub
+    this._publishMessage({
+      type: 'update',
+      update: toString(update, 'base64')
+    })
+  }
+
+  /**
+   * Handle incoming pubsub messages
+   */
+  _handlePubsubMessage (evt) {
+    // Ignore our own messages
+    if (evt.detail.topic !== this.topic) return
+    if (this.libp2p.peerId.equals(evt.detail.from)) return
+    
+    console.log(`Received pubsub message from ${evt.detail.from.toString()}, type: ${evt.detail.topic}`)
+    
+    try {
+      const message = JSON.parse(toString(evt.detail.data, 'utf8'))
+      console.log(`Message type: ${message.type}`)
+      
+      switch (message.type) {
+        case 'update':
+          this._applyUpdate(message.update)
+          break
+        case 'sync-request':
+          this._handleSyncRequest(message.stateVector)
+          break
+        case 'sync-response':
+          this._handleSyncResponse(message.update)
+          break
+      }
+    } catch (err) {
+      console.error('Failed to process pubsub message:', err)
+    }
+  }
+
+  /**
+   * Apply an update to the document
+   */
+  _applyUpdate (updateBase64) {
+    console.log('Applying Yjs update from network')
+    const update = fromString(updateBase64, 'base64')
+    Y.applyUpdate(this.doc, update, this)
+    
+    if (!this.synced) {
+      this.synced = true
+      console.log('Document synced with network')
+    }
+  }
+
+  /**
+   * Handle sync request from a peer
+   */
+  _handleSyncRequest (stateVectorBase64) {
+    const stateVector = fromString(stateVectorBase64, 'base64')
+    const update = Y.encodeStateAsUpdate(this.doc, stateVector)
+    
+    this._publishMessage({
+      type: 'sync-response',
+      update: toString(update, 'base64')
+    })
+  }
+
+  /**
+   * Handle sync response from a peer
+   */
+  _handleSyncResponse (updateBase64) {
+    this._applyUpdate(updateBase64)
+  }
+
+  /**
+   * Publish a message to the pubsub topic
+   */
+  async _publishMessage (message) {
+    try {
+      const data = fromString(JSON.stringify(message), 'utf8')
+      
+      // Check peers subscribed to this topic
+      const subscribers = this.libp2p.services.pubsub.getSubscribers(this.topic)
+      console.log(`Publishing message type: ${message.type} to topic: ${this.topic}`)
+      console.log(`Subscribers to ${this.topic}:`, subscribers.map(p => p.toString()))
+      console.log(`Total connected peers:`, this.libp2p.getConnections().length)
+      
+      const result = await this.libp2p.services.pubsub.publish(this.topic, data)
+      console.log('Message published successfully', result)
+    } catch (err) {
+      console.error('Failed to publish message:', err)
+    }
+  }
+
+  /**
+   * Destroy the provider and clean up resources
+   */
+  destroy () {
+    this.doc.off('update', this._onUpdate)
+    this.libp2p.services.pubsub.removeEventListener('message', this._onPubsubMessage)
+    this.libp2p.removeEventListener('peer:discovery', this._onPeerDiscovered)
+    
+    // Unsubscribe from topic
+    this.libp2p.services.pubsub.unsubscribe(this.topic).catch(err => {
+      console.error('Failed to unsubscribe from topic:', err)
+    })
+    
+    this.connected = false
+    this.synced = false
+  }
+}

From 9f5e5012adce1949cf0c17f1cf27b16512bd9e43 Mon Sep 17 00:00:00 2001
From: Nico Krause 
Date: Tue, 28 Oct 2025 19:45:23 +0500
Subject: [PATCH 02/12] =?UTF-8?q?Fix=20lint=20errors=20in=20yjs-libp2p=20e?=
 =?UTF-8?q?xample=E2=80=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../js-libp2p-example-yjs-libp2p/index.js     | 62 ++++++++---------
 .../playwright.config.js                      | 14 ++--
 .../js-libp2p-example-yjs-libp2p/relay.js     | 16 ++---
 .../test/global-setup.js                      | 12 ++--
 .../test/global-teardown.js                   | 10 +--
 .../test/index.spec.js                        | 20 +++---
 .../yjs-libp2p-provider.js                    | 69 ++++++++++++-------
 7 files changed, 110 insertions(+), 93 deletions(-)

diff --git a/examples/js-libp2p-example-yjs-libp2p/index.js b/examples/js-libp2p-example-yjs-libp2p/index.js
index 7524384..da91bc5 100644
--- a/examples/js-libp2p-example-yjs-libp2p/index.js
+++ b/examples/js-libp2p-example-yjs-libp2p/index.js
@@ -1,19 +1,19 @@
 /* eslint-disable no-console */
 
+import { gossipsub } from '@chainsafe/libp2p-gossipsub'
 import { noise } from '@chainsafe/libp2p-noise'
 import { yamux } from '@chainsafe/libp2p-yamux'
 import { autoNAT } from '@libp2p/autonat'
 import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
 import { dcutr } from '@libp2p/dcutr'
 import { identify, identifyPush } from '@libp2p/identify'
-import { gossipsub } from '@chainsafe/libp2p-gossipsub'
+import { ping } from '@libp2p/ping'
 import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery'
 import { webRTC, webRTCDirect } from '@libp2p/webrtc'
 import { webSockets } from '@libp2p/websockets'
 import * as filters from '@libp2p/websockets/filters'
 import { multiaddr } from '@multiformats/multiaddr'
 import { createLibp2p } from 'libp2p'
-import { ping } from '@libp2p/ping'
 import * as Y from 'yjs'
 import { Libp2pProvider } from './yjs-libp2p-provider.js'
 
@@ -42,72 +42,67 @@ const log = (message) => {
 // Update peer display
 const updatePeerDisplay = () => {
   if (!libp2pNode) return
-  
+
   const connections = libp2pNode.getConnections()
   const peerMap = new Map()
-  
+
   // Group connections by peer
   for (const conn of connections) {
     const peerId = conn.remotePeer.toString()
     if (!peerMap.has(peerId)) {
       peerMap.set(peerId, [])
     }
-    
+
     // Get transport from connection and remote address
     const remoteAddr = conn.remoteAddr.toString()
     let transport = 'unknown'
-    
+
     // Check for circuit relay (p2p-circuit in address)
     if (remoteAddr.includes('/p2p-circuit')) {
       transport = 'relay'
-    }
-    // Check for WebRTC
-    else if (remoteAddr.includes('/webrtc')) {
+    } else if (remoteAddr.includes('/webrtc')) {
+      // Check for WebRTC
       transport = 'webrtc'
-    }
-    // Check for WebTransport
-    else if (remoteAddr.includes('/webtransport')) {
+    } else if (remoteAddr.includes('/webtransport')) {
+      // Check for WebTransport
       transport = 'webtransport'
-    }
-    // Check for WebSocket Secure
-    else if (remoteAddr.includes('/wss') || remoteAddr.includes('/tls/ws')) {
+    } else if (remoteAddr.includes('/wss') || remoteAddr.includes('/tls/ws')) {
+      // Check for WebSocket Secure
       transport = 'websocket-secure'
-    }
-    // Check for WebSocket
-    else if (remoteAddr.includes('/ws')) {
+    } else if (remoteAddr.includes('/ws')) {
+      // Check for WebSocket
       transport = 'websocket'
-    }
-    // If it has TCP but also has /ws, it's websocket over TCP
-    else if (remoteAddr.includes('/tcp')) {
+    } else if (remoteAddr.includes('/tcp')) {
+      // If it has TCP but also has /ws, it's websocket over TCP
       transport = 'tcp'
     }
-    
+
     peerMap.get(peerId).push({ transport, addr: remoteAddr })
   }
-  
+
   // Update count
   peerCountEl.textContent = peerMap.size
-  
+
   // Show/hide peers section
   if (peerMap.size > 0) {
     peersEl.style.display = 'block'
   } else {
     peersEl.style.display = 'none'
   }
-  
+
   // Update peer list
   peerListEl.innerHTML = ''
   for (const [peerId, transports] of peerMap) {
     const peerDiv = document.createElement('div')
     peerDiv.className = 'peer'
-    
+
     const peerIdSpan = document.createElement('div')
     peerIdSpan.className = 'peer-id'
     peerIdSpan.textContent = peerId
     peerDiv.appendChild(peerIdSpan)
-    
+
     const transportDiv = document.createElement('div')
-    
+
     // Show each connection with its transport
     for (const { transport, addr } of transports) {
       const badge = document.createElement('span')
@@ -116,9 +111,9 @@ const updatePeerDisplay = () => {
       badge.title = addr // Show full address on hover
       transportDiv.appendChild(badge)
     }
-    
+
     peerDiv.appendChild(transportDiv)
-    
+
     peerListEl.appendChild(peerDiv)
   }
 }
@@ -201,9 +196,9 @@ connectBtn.onclick = async () => {
           emitSelf: false,
           allowPublishToZeroTopicPeers: true,
           // Speed up gossipsub mesh formation
-          heartbeatInterval: 1000,  // Send heartbeat every 1 second (default is 1000ms)
+          heartbeatInterval: 1000, // Send heartbeat every 1 second (default is 1000ms)
           directPeers: [],
-          floodPublish: true  // Broadcast to all peers, not just mesh
+          floodPublish: true // Broadcast to all peers, not just mesh
         })
       }
     })
@@ -237,7 +232,7 @@ connectBtn.onclick = async () => {
     editor.oninput = () => {
       const newText = editor.value
       const currentText = text.toString()
-      
+
       if (newText !== currentText) {
         yjsDoc.transact(() => {
           text.delete(0, currentText.length)
@@ -262,7 +257,6 @@ connectBtn.onclick = async () => {
       log(`Disconnected from peer: ${evt.detail}`)
       updatePeerDisplay()
     })
-
   } catch (err) {
     log(`Error: ${err.message}`)
     console.error(err)
diff --git a/examples/js-libp2p-example-yjs-libp2p/playwright.config.js b/examples/js-libp2p-example-yjs-libp2p/playwright.config.js
index 2c0a833..f3c625f 100644
--- a/examples/js-libp2p-example-yjs-libp2p/playwright.config.js
+++ b/examples/js-libp2p-example-yjs-libp2p/playwright.config.js
@@ -4,7 +4,7 @@ export default defineConfig({
   testDir: './test',
   timeout: 60000,
   fullyParallel: false,
-  forbidOnly: !!process.env.CI,
+  forbidOnly: Boolean(process.env.CI),
   retries: process.env.CI ? 2 : 0,
   workers: 1,
   reporter: 'list',
@@ -12,22 +12,22 @@ export default defineConfig({
   globalTeardown: './test/global-teardown.js',
   use: {
     baseURL: 'http://localhost:5173',
-    trace: 'on-first-retry',
+    trace: 'on-first-retry'
   },
   projects: [
     {
       name: 'chromium',
-      use: { ...devices['Desktop Chrome'] },
+      use: { ...devices['Desktop Chrome'] }
     },
     {
       name: 'firefox',
-      use: { ...devices['Desktop Firefox'] },
-    },
+      use: { ...devices['Desktop Firefox'] }
+    }
   ],
   webServer: {
     command: 'npx vite preview --port 5173',
     port: 5173,
     reuseExistingServer: !process.env.CI,
-    timeout: 120000,
-  },
+    timeout: 120000
+  }
 })
diff --git a/examples/js-libp2p-example-yjs-libp2p/relay.js b/examples/js-libp2p-example-yjs-libp2p/relay.js
index dc5dc60..3a0793e 100644
--- a/examples/js-libp2p-example-yjs-libp2p/relay.js
+++ b/examples/js-libp2p-example-yjs-libp2p/relay.js
@@ -1,18 +1,18 @@
 /* eslint-disable no-console */
 
+import { gossipsub } from '@chainsafe/libp2p-gossipsub'
 import { noise } from '@chainsafe/libp2p-noise'
 import { yamux } from '@chainsafe/libp2p-yamux'
 import { autoNAT } from '@libp2p/autonat'
 import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
 import { dcutr } from '@libp2p/dcutr'
 import { identify, identifyPush } from '@libp2p/identify'
-import { gossipsub } from '@chainsafe/libp2p-gossipsub'
+import { ping } from '@libp2p/ping'
 import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery'
+import { tcp } from '@libp2p/tcp'
+import { webRTC, webRTCDirect } from '@libp2p/webrtc'
 import { webSockets } from '@libp2p/websockets'
 import * as filters from '@libp2p/websockets/filters'
-import { webRTC, webRTCDirect } from '@libp2p/webrtc'
-import { tcp } from '@libp2p/tcp'
-import { ping } from '@libp2p/ping'
 import { createLibp2p } from 'libp2p'
 
 const server = await createLibp2p({
@@ -64,7 +64,7 @@ const server = await createLibp2p({
       emitSelf: false,
       allowPublishToZeroTopicPeers: true,
       canRelayMessage: true,
-      floodPublish: true  // Broadcast to all peers, not just mesh
+      floodPublish: true // Broadcast to all peers, not just mesh
     }),
     relay: circuitRelayServer({
       hopTimeout: 30000,
@@ -82,12 +82,12 @@ const server = await createLibp2p({
 server.addEventListener('peer:discovery', async (evt) => {
   const peer = evt.detail
   console.log(`Discovered peer: ${peer.id.toString()}`)
-  
+
   // Check if we're already connected to this peer
   const connections = server.getConnections(peer.id)
   if (!connections || connections.length === 0) {
     console.log(`Dialing new peer: ${peer.id.toString()}`)
-    
+
     try {
       // Dial the peer ID directly - libp2p will handle finding the best route
       await server.dial(peer.id)
@@ -119,7 +119,7 @@ server.services.pubsub.addEventListener('message', (evt) => {
     console.log('\n📨 Message received on', YJS_TOPIC)
     console.log('  From:', evt.detail.from.toString())
     console.log('  Data length:', evt.detail.data.length, 'bytes')
-    
+
     try {
       const msgStr = new TextDecoder().decode(evt.detail.data)
       const msg = JSON.parse(msgStr)
diff --git a/examples/js-libp2p-example-yjs-libp2p/test/global-setup.js b/examples/js-libp2p-example-yjs-libp2p/test/global-setup.js
index 2fcb03f..0bae29a 100644
--- a/examples/js-libp2p-example-yjs-libp2p/test/global-setup.js
+++ b/examples/js-libp2p-example-yjs-libp2p/test/global-setup.js
@@ -1,10 +1,12 @@
+/* eslint-disable no-console, no-unused-vars */
+
 import { spawn } from 'child_process'
 import { writeFileSync } from 'fs'
 import path from 'path'
 
-export default async function globalSetup() {
+export default async function globalSetup () {
   console.log('Starting relay server...')
-  
+
   return new Promise((resolve, reject) => {
     // Start relay server as a child process
     const relayProcess = spawn('node', ['relay.js'], {
@@ -25,18 +27,18 @@ export default async function globalSetup() {
       if (match && !relayMultiaddr) {
         relayMultiaddr = match[0]
         console.log(`Relay server started with multiaddr: ${relayMultiaddr}`)
-        
+
         // Store relay info for tests
         const relayInfo = {
           multiaddr: relayMultiaddr,
           pid: relayProcess.pid
         }
-        
+
         writeFileSync(
           path.resolve(process.cwd(), 'test/relay-info.json'),
           JSON.stringify(relayInfo, null, 2)
         )
-        
+
         // Give the relay a moment to fully initialize
         setTimeout(() => resolve(), 1000)
       }
diff --git a/examples/js-libp2p-example-yjs-libp2p/test/global-teardown.js b/examples/js-libp2p-example-yjs-libp2p/test/global-teardown.js
index 72fd342..89d1775 100644
--- a/examples/js-libp2p-example-yjs-libp2p/test/global-teardown.js
+++ b/examples/js-libp2p-example-yjs-libp2p/test/global-teardown.js
@@ -1,18 +1,20 @@
+/* eslint-disable no-console */
+
 import { readFileSync, unlinkSync } from 'fs'
 import path from 'path'
 
-export default async function globalTeardown() {
+export default async function globalTeardown () {
   console.log('Stopping relay server...')
-  
+
   try {
     const relayInfoPath = path.resolve(process.cwd(), 'test/relay-info.json')
     const relayInfo = JSON.parse(readFileSync(relayInfoPath, 'utf8'))
-    
+
     if (relayInfo.pid) {
       process.kill(relayInfo.pid, 'SIGTERM')
       console.log(`Relay server (PID ${relayInfo.pid}) stopped`)
     }
-    
+
     // Clean up the relay info file
     unlinkSync(relayInfoPath)
   } catch (error) {
diff --git a/examples/js-libp2p-example-yjs-libp2p/test/index.spec.js b/examples/js-libp2p-example-yjs-libp2p/test/index.spec.js
index 44bc988..09962f0 100644
--- a/examples/js-libp2p-example-yjs-libp2p/test/index.spec.js
+++ b/examples/js-libp2p-example-yjs-libp2p/test/index.spec.js
@@ -1,15 +1,17 @@
-import { test, expect } from '@playwright/test'
+/* eslint-disable no-console */
+
 import { readFileSync } from 'fs'
 import path from 'path'
+import { test, expect } from '@playwright/test'
 
 const url = 'http://localhost:5173'
 
 // Helper to connect a page to the relay
-async function connectToRelay(page, relayMultiaddr, topic = 'test-topic') {
+async function connectToRelay (page, relayMultiaddr, topic = 'test-topic') {
   await page.fill('#relay', relayMultiaddr)
   await page.fill('#topic', topic)
   await page.click('#connect')
-  
+
   // Wait for connection to establish
   await page.waitForFunction(
     () => document.getElementById('editor').disabled === false,
@@ -19,7 +21,7 @@ async function connectToRelay(page, relayMultiaddr, topic = 'test-topic') {
 
 test.describe('Yjs + libp2p example', () => {
   let relayMultiaddr
-  
+
   test.beforeAll(() => {
     // Load relay multiaddr from global setup
     const relayInfo = JSON.parse(
@@ -31,19 +33,19 @@ test.describe('Yjs + libp2p example', () => {
   test('should load page in two browsers', async ({ browser }) => {
     const context1 = await browser.newContext()
     const context2 = await browser.newContext()
-    
+
     const page1 = await context1.newPage()
     const page2 = await context2.newPage()
 
     await page1.goto(url)
     await page2.goto(url)
-    
+
     const heading1 = await page1.locator('h1').textContent()
     const heading2 = await page2.locator('h1').textContent()
 
     expect(heading1).toBe('Yjs + libp2p')
     expect(heading2).toBe('Yjs + libp2p')
-    
+
     await context1.close()
     await context2.close()
   })
@@ -51,7 +53,7 @@ test.describe('Yjs + libp2p example', () => {
   test('should sync text between two browsers', async ({ browser }) => {
     const context1 = await browser.newContext()
     const context2 = await browser.newContext()
-    
+
     const page1 = await context1.newPage()
     const page2 = await context2.newPage()
 
@@ -114,7 +116,7 @@ test.describe('Yjs + libp2p example', () => {
 
     const page1Text = await page1.inputValue('#editor')
     expect(page1Text).toBe(expectedText)
-    
+
     await context1.close()
     await context2.close()
   })
diff --git a/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
index 45fddbe..7b38282 100644
--- a/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
+++ b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
@@ -1,5 +1,7 @@
-import * as Y from 'yjs'
+/* eslint-disable no-console, jsdoc/require-param-type, default-case */
+
 import { fromString, toString } from 'uint8arrays'
+import * as Y from 'yjs'
 
 /**
  * Yjs connection provider using libp2p for peer-to-peer connectivity
@@ -10,7 +12,7 @@ export class Libp2pProvider {
    * @param {string} topic - The pubsub topic to use for this document
    * @param {Y.Doc} doc - The Yjs document to sync
    * @param {import('libp2p').Libp2p} libp2p - The libp2p instance
-   * @param {Object} options - Provider options
+   * @param {object} options - Provider options
    */
   constructor (topic, doc, libp2p, options = {}) {
     this.topic = topic
@@ -19,24 +21,24 @@ export class Libp2pProvider {
     this.awareness = options.awareness
     this.synced = false
     this.connected = false
-    
+
     // Track connected peers
     this.connectedPeers = new Set()
-    
+
     // Bind event handlers
     this._onUpdate = this._handleDocUpdate.bind(this)
     this._onPubsubMessage = this._handlePubsubMessage.bind(this)
     this._onPeerDiscovered = this._handlePeerDiscovered.bind(this)
-    
+
     // Subscribe to document updates
     this.doc.on('update', this._onUpdate)
-    
+
     // Subscribe to pubsub topic
     this._subscribeToPubsub()
-    
+
     // Set up peer discovery
     this._setupPeerDiscovery()
-    
+
     // Request initial state from peers
     this._requestInitialState()
   }
@@ -50,23 +52,23 @@ export class Libp2pProvider {
       this.libp2p.services.pubsub.addEventListener('message', this._onPubsubMessage)
       this.connected = true
       console.log(`✅ Subscribed to Yjs topic: ${this.topic}`)
-      
+
       // Log current subscriptions
       const topics = this.libp2p.services.pubsub.getTopics()
       console.log('All subscribed topics:', topics)
-      
+
       // Check peers multiple times as gossipsub mesh forms
       const checkPeers = () => {
         const peers = this.libp2p.services.pubsub.getSubscribers(this.topic)
         console.log(`Peers subscribed to ${this.topic}:`, peers.map(p => p.toString()))
-        
+
         if (peers.length === 0) {
           console.warn('⚠️ No peers subscribed to this topic yet. Waiting for gossipsub mesh...')
         } else {
           console.log('✅ Gossipsub mesh formed!')
         }
       }
-      
+
       setTimeout(checkPeers, 2000)
       setTimeout(checkPeers, 5000)
       setTimeout(checkPeers, 10000)
@@ -81,7 +83,7 @@ export class Libp2pProvider {
   _setupPeerDiscovery () {
     // Listen for peer discovery events from pubsubPeerDiscovery
     this.libp2p.addEventListener('peer:discovery', this._onPeerDiscovered)
-    
+
     // Track connections
     this.libp2p.addEventListener('peer:connect', async (evt) => {
       const peerId = evt.detail.toString()
@@ -98,16 +100,18 @@ export class Libp2pProvider {
 
   /**
    * Handle peer discovery events
+   *
+   * @param evt
    */
   async _handlePeerDiscovered (evt) {
     const peer = evt.detail
     console.log(`Discovered peer: ${peer.id.toString()}`)
-    
+
     // Check if we're already connected to this peer
     const connections = this.libp2p.getConnections(peer.id)
     if (!connections || connections.length === 0) {
       console.log(`Dialing new peer: ${peer.id.toString()}`)
-      
+
       try {
         // Dial the peer ID directly - libp2p will handle finding the best route
         await this.libp2p.dial(peer.id)
@@ -136,11 +140,14 @@ export class Libp2pProvider {
 
   /**
    * Handle Yjs document updates
+   *
+   * @param update
+   * @param origin
    */
   _handleDocUpdate (update, origin) {
     // Don't broadcast updates that came from the network
     if (origin === this) return
-    
+
     console.log('Broadcasting Yjs update to peers')
     // Broadcast the update to all peers via pubsub
     this._publishMessage({
@@ -151,18 +158,20 @@ export class Libp2pProvider {
 
   /**
    * Handle incoming pubsub messages
+   *
+   * @param evt
    */
   _handlePubsubMessage (evt) {
     // Ignore our own messages
     if (evt.detail.topic !== this.topic) return
     if (this.libp2p.peerId.equals(evt.detail.from)) return
-    
+
     console.log(`Received pubsub message from ${evt.detail.from.toString()}, type: ${evt.detail.topic}`)
-    
+
     try {
       const message = JSON.parse(toString(evt.detail.data, 'utf8'))
       console.log(`Message type: ${message.type}`)
-      
+
       switch (message.type) {
         case 'update':
           this._applyUpdate(message.update)
@@ -181,12 +190,14 @@ export class Libp2pProvider {
 
   /**
    * Apply an update to the document
+   *
+   * @param updateBase64
    */
   _applyUpdate (updateBase64) {
     console.log('Applying Yjs update from network')
     const update = fromString(updateBase64, 'base64')
     Y.applyUpdate(this.doc, update, this)
-    
+
     if (!this.synced) {
       this.synced = true
       console.log('Document synced with network')
@@ -195,11 +206,13 @@ export class Libp2pProvider {
 
   /**
    * Handle sync request from a peer
+   *
+   * @param stateVectorBase64
    */
   _handleSyncRequest (stateVectorBase64) {
     const stateVector = fromString(stateVectorBase64, 'base64')
     const update = Y.encodeStateAsUpdate(this.doc, stateVector)
-    
+
     this._publishMessage({
       type: 'sync-response',
       update: toString(update, 'base64')
@@ -208,6 +221,8 @@ export class Libp2pProvider {
 
   /**
    * Handle sync response from a peer
+   *
+   * @param updateBase64
    */
   _handleSyncResponse (updateBase64) {
     this._applyUpdate(updateBase64)
@@ -215,17 +230,19 @@ export class Libp2pProvider {
 
   /**
    * Publish a message to the pubsub topic
+   *
+   * @param message
    */
   async _publishMessage (message) {
     try {
       const data = fromString(JSON.stringify(message), 'utf8')
-      
+
       // Check peers subscribed to this topic
       const subscribers = this.libp2p.services.pubsub.getSubscribers(this.topic)
       console.log(`Publishing message type: ${message.type} to topic: ${this.topic}`)
       console.log(`Subscribers to ${this.topic}:`, subscribers.map(p => p.toString()))
-      console.log(`Total connected peers:`, this.libp2p.getConnections().length)
-      
+      console.log('Total connected peers:', this.libp2p.getConnections().length)
+
       const result = await this.libp2p.services.pubsub.publish(this.topic, data)
       console.log('Message published successfully', result)
     } catch (err) {
@@ -240,12 +257,12 @@ export class Libp2pProvider {
     this.doc.off('update', this._onUpdate)
     this.libp2p.services.pubsub.removeEventListener('message', this._onPubsubMessage)
     this.libp2p.removeEventListener('peer:discovery', this._onPeerDiscovered)
-    
+
     // Unsubscribe from topic
     this.libp2p.services.pubsub.unsubscribe(this.topic).catch(err => {
       console.error('Failed to unsubscribe from topic:', err)
     })
-    
+
     this.connected = false
     this.synced = false
   }

From d5f395927b3ca75dd1b73c5813f560d4b11c6126 Mon Sep 17 00:00:00 2001
From: Nico Krause 
Date: Tue, 28 Oct 2025 20:03:14 +0500
Subject: [PATCH 03/12] =?UTF-8?q?Fix=20Yjs=20sync=20by=20making=20relay=20?=
 =?UTF-8?q?auto-subscribe=20to=20topics=E2=80=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../js-libp2p-example-yjs-libp2p/index.js     |  3 +
 .../js-libp2p-example-yjs-libp2p/relay.js     | 73 ++++++++++++++-----
 .../test/index.spec.js                        | 14 +++-
 .../yjs-libp2p-provider.js                    | 35 +++++----
 4 files changed, 90 insertions(+), 35 deletions(-)

diff --git a/examples/js-libp2p-example-yjs-libp2p/index.js b/examples/js-libp2p-example-yjs-libp2p/index.js
index da91bc5..7e177f9 100644
--- a/examples/js-libp2p-example-yjs-libp2p/index.js
+++ b/examples/js-libp2p-example-yjs-libp2p/index.js
@@ -205,6 +205,9 @@ connectBtn.onclick = async () => {
 
     log(`libp2p node created with id: ${libp2pNode.peerId}`)
 
+    // Expose for testing
+    window.libp2pNode = libp2pNode
+
     // Connect to relay
     log('Connecting to relay...')
     const ma = multiaddr(relayAddr)
diff --git a/examples/js-libp2p-example-yjs-libp2p/relay.js b/examples/js-libp2p-example-yjs-libp2p/relay.js
index 3a0793e..899aaf4 100644
--- a/examples/js-libp2p-example-yjs-libp2p/relay.js
+++ b/examples/js-libp2p-example-yjs-libp2p/relay.js
@@ -108,33 +108,66 @@ server.addEventListener('peer:disconnect', (evt) => {
   console.log(`Disconnected from peer: ${evt.detail.toString()}`)
 })
 
-// Subscribe to the Yjs topic to relay messages and log them
-const YJS_TOPIC = 'yjs-doc-1'
-await server.services.pubsub.subscribe(YJS_TOPIC)
-console.log(`📡 Relay subscribed to topic: ${YJS_TOPIC}`)
-
-// Log all messages passing through
+// Log all messages passing through ALL topics
 server.services.pubsub.addEventListener('message', (evt) => {
-  if (evt.detail.topic === YJS_TOPIC) {
-    console.log('\n📨 Message received on', YJS_TOPIC)
-    console.log('  From:', evt.detail.from.toString())
-    console.log('  Data length:', evt.detail.data.length, 'bytes')
+  console.log('\n📨 Message received on topic:', evt.detail.topic)
+  console.log('  From:', evt.detail.from.toString())
+  console.log('  Data length:', evt.detail.data.length, 'bytes')
 
-    try {
-      const msgStr = new TextDecoder().decode(evt.detail.data)
-      const msg = JSON.parse(msgStr)
-      console.log('  Type:', msg.type)
-    } catch (e) {
-      console.log('  (Could not parse message)')
+  try {
+    const msgStr = new TextDecoder().decode(evt.detail.data)
+    const msg = JSON.parse(msgStr)
+    console.log('  Type:', msg.type)
+  } catch (e) {
+    console.log('  (Could not parse message data)')
+  }
+})
+
+// Subscribe to topics dynamically as we see them
+const subscribedTopics = new Set()
+
+server.services.pubsub.addEventListener('subscription-change', async (evt) => {
+  console.log('\n📢 Subscription change:', evt.detail)
+  const peerId = evt.detail.peerId ? evt.detail.peerId.toString() : 'unknown'
+  console.log('  Peer:', peerId)
+  console.log('  Subscriptions:', evt.detail.subscriptions)
+
+  // Auto-subscribe to any Yjs or test topics we see
+  if (evt.detail.subscriptions && Array.isArray(evt.detail.subscriptions)) {
+    for (const sub of evt.detail.subscriptions) {
+      if (sub && sub.topic) {
+        const topic = sub.topic
+        if ((topic.startsWith('yjs-') || topic.startsWith('test-')) && !subscribedTopics.has(topic)) {
+          subscribedTopics.add(topic)
+          try {
+            await server.services.pubsub.subscribe(topic)
+            console.log(`📡 Relay auto-subscribed to: ${topic}`)
+          } catch (err) {
+            console.error('Failed to subscribe:', err)
+          }
+        }
+      }
     }
   }
 })
 
-// Periodically log subscribers to the topic
+// Subscribe to default Yjs topic
+const DEFAULT_TOPIC = 'yjs-doc-1'
+await server.services.pubsub.subscribe(DEFAULT_TOPIC)
+subscribedTopics.add(DEFAULT_TOPIC)
+console.log(`📡 Relay subscribed to default topic: ${DEFAULT_TOPIC}`)
+
+// Periodically log all active topics and subscribers
 setInterval(() => {
-  const subscribers = server.services.pubsub.getSubscribers(YJS_TOPIC)
-  if (subscribers.length > 0) {
-    console.log(`\n👥 Subscribers to ${YJS_TOPIC}:`, subscribers.map(p => p.toString()))
+  const topics = server.services.pubsub.getTopics()
+  if (topics.length > 0) {
+    console.log('\n📋 Active topics:', topics)
+    for (const topic of topics) {
+      const subscribers = server.services.pubsub.getSubscribers(topic)
+      if (subscribers.length > 0) {
+        console.log(`  👥 ${topic}: ${subscribers.length} subscribers`)
+      }
+    }
   }
 }, 10000)
 
diff --git a/examples/js-libp2p-example-yjs-libp2p/test/index.spec.js b/examples/js-libp2p-example-yjs-libp2p/test/index.spec.js
index 09962f0..ddbbe8d 100644
--- a/examples/js-libp2p-example-yjs-libp2p/test/index.spec.js
+++ b/examples/js-libp2p-example-yjs-libp2p/test/index.spec.js
@@ -82,8 +82,18 @@ test.describe('Yjs + libp2p example', () => {
 
     // Wait for peers to discover each other via pubsub
     // Even without direct P2P connection, pubsub through relay should work
-    // Wait for sync-request/response to complete
-    await page1.waitForTimeout(3000)
+    // Wait for gossipsub mesh to form and sync-request/response to complete
+    // Gossipsub heartbeat is 1000ms, give it more time to propagate subscriptions
+    await page1.waitForTimeout(15000)
+
+    // Check if peers can see each other in gossipsub
+    const page1Peers = await page1.evaluate(() => {
+      return window.libp2pNode?.getConnections().length || 0
+    })
+    const page2Peers = await page2.evaluate(() => {
+      return window.libp2pNode?.getConnections().length || 0
+    })
+    console.log(`Page1 connections: ${page1Peers}, Page2 connections: ${page2Peers}`)
 
     // Click into editor and type in page 1
     await page1.click('#editor')
diff --git a/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
index 7b38282..b24461d 100644
--- a/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
+++ b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
@@ -105,22 +105,31 @@ export class Libp2pProvider {
    */
   async _handlePeerDiscovered (evt) {
     const peer = evt.detail
-    console.log(`Discovered peer: ${peer.id.toString()}`)
+    const peerId = peer.id.toString()
+    console.log(`[Provider] Discovered peer: ${peerId}`)
+
+    // Don't dial ourselves
+    if (this.libp2p.peerId.equals(peer.id)) {
+      console.log(`[Provider] Skipping self: ${peerId}`)
+      return
+    }
 
     // Check if we're already connected to this peer
     const connections = this.libp2p.getConnections(peer.id)
-    if (!connections || connections.length === 0) {
-      console.log(`Dialing new peer: ${peer.id.toString()}`)
-
-      try {
-        // Dial the peer ID directly - libp2p will handle finding the best route
-        await this.libp2p.dial(peer.id)
-        console.log(`Successfully dialed peer: ${peer.id.toString()}`)
-      } catch (error) {
-        console.error(`Failed to dial peer ${peer.id.toString()}:`, error.message)
-      }
-    } else {
-      console.log(`Already connected to peer: ${peer.id.toString()}`)
+    if (connections && connections.length > 0) {
+      console.log(`[Provider] Already connected to peer: ${peerId} (${connections.length} connections)`)
+      return
+    }
+
+    console.log(`[Provider] Dialing new peer: ${peerId}`)
+    console.log(`[Provider] Peer addresses:`, peer.multiaddrs.map(ma => ma.toString()))
+
+    try {
+      // Dial the peer ID directly - libp2p will handle finding the best route
+      await this.libp2p.dial(peer.id)
+      console.log(`[Provider] ✅ Successfully dialed peer: ${peerId}`)
+    } catch (error) {
+      console.error(`[Provider] ❌ Failed to dial peer ${peerId}:`, error.message)
     }
   }
 

From a5fef714c9cbc8b9c4f33090c859493e30df95ca Mon Sep 17 00:00:00 2001
From: Nico Krause 
Date: Tue, 28 Oct 2025 20:15:25 +0500
Subject: [PATCH 04/12] =?UTF-8?q?Fix=20lint=20errors:=20curly=20braces,=20?=
 =?UTF-8?q?JSDoc=20types,=20and=20nesting=20depth=E2=80=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../js-libp2p-example-yjs-libp2p/index.js     |  4 ++-
 .../js-libp2p-example-yjs-libp2p/relay.js     | 36 +++++++++++--------
 .../yjs-libp2p-provider.js                    | 32 ++++++++++-------
 3 files changed, 44 insertions(+), 28 deletions(-)

diff --git a/examples/js-libp2p-example-yjs-libp2p/index.js b/examples/js-libp2p-example-yjs-libp2p/index.js
index 7e177f9..a01ae0e 100644
--- a/examples/js-libp2p-example-yjs-libp2p/index.js
+++ b/examples/js-libp2p-example-yjs-libp2p/index.js
@@ -41,7 +41,9 @@ const log = (message) => {
 
 // Update peer display
 const updatePeerDisplay = () => {
-  if (!libp2pNode) return
+  if (!libp2pNode) {
+    return
+  }
 
   const connections = libp2pNode.getConnections()
   const peerMap = new Map()
diff --git a/examples/js-libp2p-example-yjs-libp2p/relay.js b/examples/js-libp2p-example-yjs-libp2p/relay.js
index 899aaf4..20cb53f 100644
--- a/examples/js-libp2p-example-yjs-libp2p/relay.js
+++ b/examples/js-libp2p-example-yjs-libp2p/relay.js
@@ -133,20 +133,28 @@ server.services.pubsub.addEventListener('subscription-change', async (evt) => {
   console.log('  Subscriptions:', evt.detail.subscriptions)
 
   // Auto-subscribe to any Yjs or test topics we see
-  if (evt.detail.subscriptions && Array.isArray(evt.detail.subscriptions)) {
-    for (const sub of evt.detail.subscriptions) {
-      if (sub && sub.topic) {
-        const topic = sub.topic
-        if ((topic.startsWith('yjs-') || topic.startsWith('test-')) && !subscribedTopics.has(topic)) {
-          subscribedTopics.add(topic)
-          try {
-            await server.services.pubsub.subscribe(topic)
-            console.log(`📡 Relay auto-subscribed to: ${topic}`)
-          } catch (err) {
-            console.error('Failed to subscribe:', err)
-          }
-        }
-      }
+  const subscriptions = evt.detail.subscriptions
+  if (!subscriptions || !Array.isArray(subscriptions)) {
+    return
+  }
+
+  for (const sub of subscriptions) {
+    if (!sub || !sub.topic) {
+      continue
+    }
+
+    const topic = sub.topic
+    const shouldSubscribe = (topic.startsWith('yjs-') || topic.startsWith('test-')) && !subscribedTopics.has(topic)
+    if (!shouldSubscribe) {
+      continue
+    }
+
+    subscribedTopics.add(topic)
+    try {
+      await server.services.pubsub.subscribe(topic)
+      console.log(`📡 Relay auto-subscribed to: ${topic}`)
+    } catch (err) {
+      console.error('Failed to subscribe:', err)
     }
   }
 })
diff --git a/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
index b24461d..6efaab3 100644
--- a/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
+++ b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-console, jsdoc/require-param-type, default-case */
+/* eslint-disable no-console, default-case */
 
 import { fromString, toString } from 'uint8arrays'
 import * as Y from 'yjs'
@@ -101,7 +101,7 @@ export class Libp2pProvider {
   /**
    * Handle peer discovery events
    *
-   * @param evt
+   * @param {any} evt
    */
   async _handlePeerDiscovered (evt) {
     const peer = evt.detail
@@ -122,7 +122,7 @@ export class Libp2pProvider {
     }
 
     console.log(`[Provider] Dialing new peer: ${peerId}`)
-    console.log(`[Provider] Peer addresses:`, peer.multiaddrs.map(ma => ma.toString()))
+    console.log('[Provider] Peer addresses:', peer.multiaddrs.map(ma => ma.toString()))
 
     try {
       // Dial the peer ID directly - libp2p will handle finding the best route
@@ -150,12 +150,14 @@ export class Libp2pProvider {
   /**
    * Handle Yjs document updates
    *
-   * @param update
-   * @param origin
+   * @param {Uint8Array} update
+   * @param {any} origin
    */
   _handleDocUpdate (update, origin) {
     // Don't broadcast updates that came from the network
-    if (origin === this) return
+    if (origin === this) {
+      return
+    }
 
     console.log('Broadcasting Yjs update to peers')
     // Broadcast the update to all peers via pubsub
@@ -168,12 +170,16 @@ export class Libp2pProvider {
   /**
    * Handle incoming pubsub messages
    *
-   * @param evt
+   * @param {any} evt
    */
   _handlePubsubMessage (evt) {
     // Ignore our own messages
-    if (evt.detail.topic !== this.topic) return
-    if (this.libp2p.peerId.equals(evt.detail.from)) return
+    if (evt.detail.topic !== this.topic) {
+      return
+    }
+    if (this.libp2p.peerId.equals(evt.detail.from)) {
+      return
+    }
 
     console.log(`Received pubsub message from ${evt.detail.from.toString()}, type: ${evt.detail.topic}`)
 
@@ -200,7 +206,7 @@ export class Libp2pProvider {
   /**
    * Apply an update to the document
    *
-   * @param updateBase64
+   * @param {string} updateBase64
    */
   _applyUpdate (updateBase64) {
     console.log('Applying Yjs update from network')
@@ -216,7 +222,7 @@ export class Libp2pProvider {
   /**
    * Handle sync request from a peer
    *
-   * @param stateVectorBase64
+   * @param {string} stateVectorBase64
    */
   _handleSyncRequest (stateVectorBase64) {
     const stateVector = fromString(stateVectorBase64, 'base64')
@@ -231,7 +237,7 @@ export class Libp2pProvider {
   /**
    * Handle sync response from a peer
    *
-   * @param updateBase64
+   * @param {string} updateBase64
    */
   _handleSyncResponse (updateBase64) {
     this._applyUpdate(updateBase64)
@@ -240,7 +246,7 @@ export class Libp2pProvider {
   /**
    * Publish a message to the pubsub topic
    *
-   * @param message
+   * @param {object} message
    */
   async _publishMessage (message) {
     try {

From fbea6b87ef5b1d0914c12e1158aefda75a37e9eb Mon Sep 17 00:00:00 2001
From: Nico Krause 
Date: Tue, 28 Oct 2025 20:16:55 +0500
Subject: [PATCH 05/12] =?UTF-8?q?Add=20yjs-libp2p=20example=20to=20CI=20wo?=
 =?UTF-8?q?rkflow=E2=80=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/workflows/ci.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2fb0ece..2854313 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -35,6 +35,7 @@ jobs:
           - js-libp2p-example-transports
           - js-libp2p-example-webrtc-private-to-private
           - js-libp2p-example-webrtc-private-to-public
+          - js-libp2p-example-yjs-libp2p
     defaults:
       run:
         working-directory: examples/${{ matrix.project }}
@@ -97,6 +98,7 @@ jobs:
           - js-libp2p-example-transports
           - js-libp2p-example-webrtc-private-to-private
           - js-libp2p-example-webrtc-private-to-public
+          - js-libp2p-example-yjs-libp2p
     steps:
       - uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be
         with:

From f83c71d2f9f7088287c80fd744c00f7a2a345758 Mon Sep 17 00:00:00 2001
From: Nico Krause 
Date: Tue, 28 Oct 2025 20:31:51 +0500
Subject: [PATCH 06/12]  adding tcp transport

---
 examples/js-libp2p-example-yjs-libp2p/package.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/examples/js-libp2p-example-yjs-libp2p/package.json b/examples/js-libp2p-example-yjs-libp2p/package.json
index 80c613b..e05fa54 100644
--- a/examples/js-libp2p-example-yjs-libp2p/package.json
+++ b/examples/js-libp2p-example-yjs-libp2p/package.json
@@ -20,6 +20,7 @@
     "@libp2p/dcutr": "^2.0.0",
     "@libp2p/identify": "^3.0.1",
     "@libp2p/pubsub-peer-discovery": "^11.0.0",
+    "@libp2p/tcp": "^10.0.0",
     "@libp2p/webrtc": "^5.0.0",
     "@libp2p/websockets": "^9.0.0",
     "@multiformats/multiaddr": "^12.0.0",

From c7c4a98c1e75c6d12ec1d02ae0368354d2166f21 Mon Sep 17 00:00:00 2001
From: Nico Krause 
Date: Tue, 28 Oct 2025 21:01:10 +0500
Subject: [PATCH 07/12] adding missing ping lib

---
 examples/js-libp2p-example-yjs-libp2p/package.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/examples/js-libp2p-example-yjs-libp2p/package.json b/examples/js-libp2p-example-yjs-libp2p/package.json
index e05fa54..777aa21 100644
--- a/examples/js-libp2p-example-yjs-libp2p/package.json
+++ b/examples/js-libp2p-example-yjs-libp2p/package.json
@@ -19,6 +19,7 @@
     "@libp2p/circuit-relay-v2": "^3.0.0",
     "@libp2p/dcutr": "^2.0.0",
     "@libp2p/identify": "^3.0.1",
+    "@libp2p/ping": "^2.0.0",
     "@libp2p/pubsub-peer-discovery": "^11.0.0",
     "@libp2p/tcp": "^10.0.0",
     "@libp2p/webrtc": "^5.0.0",

From 78e089b1195e77b91643c72dd6bf1f0f9b277215 Mon Sep 17 00:00:00 2001
From: Nico Krause 
Date: Wed, 29 Oct 2025 11:24:37 +0500
Subject: [PATCH 08/12] adding build step

---
 .github/workflows/ci.yml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2854313..3127bb3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -65,6 +65,10 @@ jobs:
         run: npm install
       - name: Install Playwright
         run: npx -y playwright install --with-deps
+      - name: Build projects              
+        run: npm run build                
+        env:                              
+          CI: true                        
       - name: Run linting
         run: npm run lint
         env:

From 26eff48c77c781d5b02ca09c85bce088b5cd9f62 Mon Sep 17 00:00:00 2001
From: Nico Krause 
Date: Wed, 29 Oct 2025 11:45:45 +0500
Subject: [PATCH 09/12] adding build step to examples

---
 .github/workflows/ci.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3127bb3..3e133bc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -48,6 +48,8 @@ jobs:
         run: npm install
       - name: Install Playwright
         run: npx -y playwright install --with-deps
+      - name: Build project
+        run: npm run build
       - name: Run tests
         run: npm run test
         env:

From 2de915da0015b6d89ef668e693fbf60011318323 Mon Sep 17 00:00:00 2001
From: Nico Krause 
Date: Wed, 29 Oct 2025 11:54:24 +0500
Subject: [PATCH 10/12] only build when build script exists

---
 .github/workflows/ci.yml | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3e133bc..42fc924 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -48,8 +48,15 @@ jobs:
         run: npm install
       - name: Install Playwright
         run: npx -y playwright install --with-deps
-      - name: Build project
-        run: npm run build
+      - name: Build project (if build script exists)
+        run: |
+          if npm run | grep -q "build"; then
+            npm run build
+          else
+            echo "No build script found, skipping build"
+          fi
+        env:
+          CI: true
       - name: Run tests
         run: npm run test
         env:

From c528d0c33ee84c08c3eb4bf24c27a77bcc1cd8a6 Mon Sep 17 00:00:00 2001
From: Nico Krause 
Date: Wed, 29 Oct 2025 22:18:30 +0500
Subject: [PATCH 11/12] cleanup

---
 .../js-libp2p-example-yjs-libp2p/.gitignore   |  34 +++
 .../js-libp2p-example-yjs-libp2p/README.md    |  15 +
 examples/js-libp2p-example-yjs-libp2p/WARP.md | 176 ------------
 .../js-libp2p-example-yjs-libp2p/constants.js |  48 ++++
 .../eslint.config.js                          |  33 +++
 .../js-libp2p-example-yjs-libp2p/index.js     | 154 +++++++----
 .../js-libp2p-example-yjs-libp2p/package.json |   6 +
 .../relay-constants.js                        |  45 +++
 .../js-libp2p-example-yjs-libp2p/relay.js     | 155 ++++++-----
 .../yjs-libp2p-provider.js                    | 258 ++++++++++--------
 10 files changed, 515 insertions(+), 409 deletions(-)
 create mode 100644 examples/js-libp2p-example-yjs-libp2p/.gitignore
 delete mode 100644 examples/js-libp2p-example-yjs-libp2p/WARP.md
 create mode 100644 examples/js-libp2p-example-yjs-libp2p/constants.js
 create mode 100644 examples/js-libp2p-example-yjs-libp2p/eslint.config.js
 create mode 100644 examples/js-libp2p-example-yjs-libp2p/relay-constants.js

diff --git a/examples/js-libp2p-example-yjs-libp2p/.gitignore b/examples/js-libp2p-example-yjs-libp2p/.gitignore
new file mode 100644
index 0000000..d1e8a6f
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/.gitignore
@@ -0,0 +1,34 @@
+# Dependencies
+node_modules/
+
+# Build output
+dist/
+.vite/
+
+# Test artifacts
+test/relay-info.json
+playwright-report/
+test-results/
+
+# Logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Environment variables
+.env
+.env.local
+
+# Editor directories and files
+.vscode/
+.idea/
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# OS files
+.DS_Store
+Thumbs.db
diff --git a/examples/js-libp2p-example-yjs-libp2p/README.md b/examples/js-libp2p-example-yjs-libp2p/README.md
index 5e5ec33..e3a92b3 100644
--- a/examples/js-libp2p-example-yjs-libp2p/README.md
+++ b/examples/js-libp2p-example-yjs-libp2p/README.md
@@ -76,6 +76,21 @@ npm start
 6. Open another browser tab/window, connect to the same relay and topic
 7. Changes will sync automatically between all connected peers
 
+### Debug Mode
+
+To enable verbose logging:
+
+**Relay server:**
+```bash
+npm run relay:debug
+```
+
+**Browser client:**
+Add `?debug=true` to the URL:
+```
+http://localhost:5173/?debug=true
+```
+
 ## How It Works
 
 ### Libp2p Configuration
diff --git a/examples/js-libp2p-example-yjs-libp2p/WARP.md b/examples/js-libp2p-example-yjs-libp2p/WARP.md
deleted file mode 100644
index 0120436..0000000
--- a/examples/js-libp2p-example-yjs-libp2p/WARP.md
+++ /dev/null
@@ -1,176 +0,0 @@
-# WARP.md
-
-This file provides guidance to WARP (warp.dev) when working with code in this repository.
-
-## Project Overview
-
-A collaborative text editor built with Yjs and libp2p, demonstrating real-time peer-to-peer document synchronization. This example replaces the standard y-webrtc connector with a custom libp2p-based provider for Yjs.
-
-## Development Commands
-
-### Starting the Application
-```bash
-# Start the relay server (required for peer connectivity)
-npm run relay
-
-# Start the Vite development server
-npm start
-```
-
-**Important**: Copy the relay multiaddr from terminal output and paste into the browser UI before connecting.
-
-### Build and Test
-```bash
-# Build production bundle
-npm run build
-
-# Run all tests (starts relay server + dev server automatically)
-npm test
-
-# Run tests in specific browsers
-npm run test:chrome
-npm run test:firefox
-```
-
-## Architecture
-
-### Core Components
-
-**Libp2pProvider** (`yjs-libp2p-provider.js`)
-- Custom Yjs connection provider using libp2p's pubsub instead of y-webrtc
-- Manages document synchronization across peers via GossipSub
-- Message types:
-  - `update`: broadcasts document changes
-  - `sync-request`: requests current document state (sent on join)
-  - `sync-response`: sends current state to requesting peer
-- Listens to `peer:discovery` events (libp2p auto-dialer handles connections)
-
-**Browser Client** (`index.js`)
-- Creates libp2p node with WebRTC, WebSockets, and circuit relay transports
-- **Critical config**:
-  - Listen on `/p2p-circuit` for relay discoverability
-  - Listen on `/webrtc` for direct P2P connections
-  - `pubsubPeerDiscovery` enabled for peer discovery via gossipsub
-  - `connectionGater` allows local addresses (for demo)
-- Services: identify, autoNAT, dcutr, gossipsub
-- Binds textarea to Yjs document via `oninput` event
-
-**Relay Server** (`relay.js`)
-- Node.js libp2p relay server for WebSocket connections
-- Enables NAT traversal via circuit relay
-- Forwards pubsub messages between peers
-- **Critical**: `maxReservations: Infinity` is for demo only—use default (15) in production
-
-### Network Flow
-
-1. Browser clients connect to relay server via WebSocket
-2. Clients listen on `/p2p-circuit` to become discoverable via relay
-3. Clients subscribe to a pubsub topic (document channel)
-4. pubsubPeerDiscovery broadcasts peer info on the pubsub topic
-5. Peers discover each other via `peer:discovery` events
-6. libp2p's auto-dialer connects discovered peers
-7. Peers attempt direct WebRTC connections using DCUTR (hole punching)
-8. If WebRTC fails, communication continues through relay
-9. Yjs updates broadcast via pubsub to all connected peers
-
-### Key libp2p Configuration
-
-**Listen addresses**:
-- `/p2p-circuit`: Make reservation on relay (lets other peers dial us via relay)
-- `/webrtc`: Listen for incoming WebRTC connections
-
-**Transports**:
-- `webSockets`: for relay connections
-- `webRTC`: for direct peer-to-peer connections
-- `circuitRelayTransport`: fallback relay connectivity
-
-**Peer Discovery**:
-- `pubsubPeerDiscovery`: Broadcasts peer info on gossipsub topics
-
-**Services**:
-- `identify`: peer identification and metadata exchange
-- `autoNAT`: automatic NAT detection
-- `dcutr`: Direct Connection Upgrade through Relay (hole punching)
-- `pubsub` (gossipsub): message broadcasting for Yjs updates
-
-### Yjs Synchronization
-
-The Libp2pProvider handles Yjs sync using state vectors:
-- On connection: sends sync-request with local state vector after 1s delay
-- Peers respond with sync-response containing missing updates
-- Ongoing changes broadcast as update messages via `doc.on('update')`
-- Updates apply with `origin: this` to prevent echo loops
-- Textarea updates trigger `oninput` → transact → delete + insert → pubsub broadcast
-
-## Development Patterns
-
-### When modifying Libp2pProvider
-- Always set `origin: this` when applying network updates to prevent broadcast loops
-- Use base64 encoding for binary Yjs data in JSON messages
-- Listen to `peer:discovery` not `subscription-change` - discovery happens before subscription
-- Let libp2p's auto-dialer handle connections - don't manually dial discovered peers
-
-### When working with libp2p configuration
-- **Must include `/p2p-circuit` in listen addresses** for relay-based discovery to work
-- Test with multiple browser instances to verify P2P connectivity
-- Check browser console for peer connection logs
-- WebRTC connections require HTTPS in production (localhost works for dev)
-
-### When debugging sync issues
-- Check relay server is running and accessible
-- Verify peers are on the same pubsub topic
-- Look for `Document synced with network` log message
-- Check if peers discovered each other: look for `Discovered peer:` logs
-- Verify peers connected: look for `Connected to peer:` logs
-- Examine state vectors in sync-request/sync-response messages
-
-## Testing Notes
-
-### Test Infrastructure
-
-**Automated setup**:
-- `test/global-setup.js`: Starts relay server, saves multiaddr to `test/relay-info.json`
-- `test/global-teardown.js`: Stops relay server after tests
-- `playwright.config.js`: Auto-starts Vite preview server, includes global setup/teardown
-
-**Current test coverage**:
-- ✅ Page loading in multiple browser contexts
-- ✅ Relay server starts/stops automatically  
-- ✅ Both pages connect to relay successfully
-- ✅ UI elements render correctly
-- ❌ Yjs collaborative editing sync (debugging in progress)
-
-### Recent Improvements Made
-
-1. **Added `/p2p-circuit` listen address** - Critical for relay-based peer discovery
-2. **Added `pubsubPeerDiscovery`** - Enables peer discovery via gossipsub
-3. **Added `connectionGater`** - Allows local address dialing for tests
-4. **Fixed event listeners** - Changed from `subscription-change` to `peer:discovery`
-5. **Removed manual dialing** - Let libp2p's auto-dialer handle peer connections
-6. **Updated to latest js-libp2p-examples** - Verified config matches current best practices
-
-### Why Collaboration Test Doesn't Work Yet
-
-Peers successfully:
-- ✅ Connect to relay via WebSocket
-- ✅ Subscribe to same pubsub topic
-- ✅ Both show "Ready!" message
-
-Peers do NOT:
-- ❌ Discover each other (no `Discovered peer:` logs)
-- ❌ Connect to each other (no `Connected to peer:` logs) 
-- ❌ Sync Yjs updates
-
-**Root cause**: pubsubPeerDiscovery requires peers to be on the **same pubsub topic AND connected** to discover each other. In headless browser tests:
-- Peers only connect to relay (WebSocket)
-- WebRTC P2P connections don't establish
-- Without direct connections, pubsubPeerDiscovery can't propagate peer info
-- Pubsub messages ARE delivered through relay, but discovery doesn't happen
-
-### Possible Solutions
-
-1. **Manual testing** works fine: `npm run relay` + `npm start` in multiple real browser tabs
-2. **Headed browser mode**: Run Playwright tests with `headless: false` to enable WebRTC
-3. **Manual peer dialing**: After connecting to relay, manually dial other peer's circuit relay address
-4. **Alternative discovery**: Use subscription-change events to detect peers on same topic, then dial
-5. **Test pubsub directly**: Verify pubsub message delivery rather than full Yjs sync
diff --git a/examples/js-libp2p-example-yjs-libp2p/constants.js b/examples/js-libp2p-example-yjs-libp2p/constants.js
new file mode 100644
index 0000000..914b4e9
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/constants.js
@@ -0,0 +1,48 @@
+/**
+ * Configuration constants for the Yjs + libp2p application
+ */
+
+// Debug mode - set via environment variable or query parameter
+export const DEBUG = new URLSearchParams(window?.location?.search).get('debug') === 'true' || false
+
+// Network timeouts (milliseconds)
+export const TIMEOUTS = {
+  RELAY_CONNECTION: 20000,
+  PROTOCOL_NEGOTIATION_INBOUND: 10000,
+  PROTOCOL_NEGOTIATION_OUTBOUND: 10000,
+  UPGRADE_INBOUND: 10000,
+  UPGRADE_OUTBOUND: 10000,
+  EDITOR_READY: 10000,
+  PEER_DISCOVERY: 15000
+}
+
+// Pubsub intervals (milliseconds)
+export const INTERVALS = {
+  PUBSUB_PEER_DISCOVERY: 10000,
+  GOSSIPSUB_HEARTBEAT: 1000,
+  INITIAL_SYNC_REQUEST: 1000,
+  PEER_CHECK: 2000
+}
+
+// Relay server configuration
+export const RELAY_CONFIG = {
+  HOP_TIMEOUT: 30000,
+  MAX_RESERVATIONS: 1000,
+  RESERVATION_TTL: 2 * 60 * 60 * 1000, // 2 hours
+  DEFAULT_DATA_LIMIT: BigInt(1024 * 1024 * 1024), // 1 GB
+  DEFAULT_DURATION_LIMIT: 2 * 60 * 1000, // 2 minutes
+  MAX_CONNECTIONS: 1000,
+  MAX_INCOMING_PENDING: 100,
+  MAX_PEER_ADDRS_TO_DIAL: 100,
+  DIAL_TIMEOUT: 30000
+}
+
+// Pubsub discovery configuration
+export const PUBSUB_DISCOVERY = {
+  TOPICS: ['_peer-discovery._p2p._pubsub']
+}
+
+// Default values
+export const DEFAULTS = {
+  TOPIC: 'yjs-doc-1'
+}
diff --git a/examples/js-libp2p-example-yjs-libp2p/eslint.config.js b/examples/js-libp2p-example-yjs-libp2p/eslint.config.js
new file mode 100644
index 0000000..dba41db
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/eslint.config.js
@@ -0,0 +1,33 @@
+import js from '@eslint/js'
+import globals from 'globals'
+
+export default [
+  js.configs.recommended,
+  {
+    languageOptions: {
+      ecmaVersion: 'latest',
+      sourceType: 'module',
+      globals: {
+        ...globals.browser,
+        ...globals.node
+      }
+    },
+    rules: {
+      'no-console': 'warn',
+      'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+      'prefer-const': 'error',
+      'no-var': 'error',
+      eqeqeq: ['error', 'always'],
+      curly: ['error', 'all'],
+      'brace-style': ['error', '1tbs'],
+      quotes: ['error', 'single', { avoidEscape: true }],
+      semi: ['error', 'never'],
+      indent: ['error', 2],
+      'comma-dangle': ['error', 'never'],
+      'arrow-parens': ['error', 'always'],
+      'space-before-function-paren': ['error', 'always'],
+      'object-curly-spacing': ['error', 'always'],
+      'array-bracket-spacing': ['error', 'never']
+    }
+  }
+]
diff --git a/examples/js-libp2p-example-yjs-libp2p/index.js b/examples/js-libp2p-example-yjs-libp2p/index.js
index a01ae0e..b2e760d 100644
--- a/examples/js-libp2p-example-yjs-libp2p/index.js
+++ b/examples/js-libp2p-example-yjs-libp2p/index.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-console */
-
 import { gossipsub } from '@chainsafe/libp2p-gossipsub'
 import { noise } from '@chainsafe/libp2p-noise'
 import { yamux } from '@chainsafe/libp2p-yamux'
@@ -7,14 +5,14 @@ import { autoNAT } from '@libp2p/autonat'
 import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
 import { dcutr } from '@libp2p/dcutr'
 import { identify, identifyPush } from '@libp2p/identify'
-import { ping } from '@libp2p/ping'
 import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery'
-import { webRTC, webRTCDirect } from '@libp2p/webrtc'
+import { webRTC } from '@libp2p/webrtc'
 import { webSockets } from '@libp2p/websockets'
 import * as filters from '@libp2p/websockets/filters'
 import { multiaddr } from '@multiformats/multiaddr'
 import { createLibp2p } from 'libp2p'
 import * as Y from 'yjs'
+import { DEBUG, TIMEOUTS, INTERVALS, PUBSUB_DISCOVERY } from './constants.js'
 import { Libp2pProvider } from './yjs-libp2p-provider.js'
 
 // UI elements
@@ -32,14 +30,29 @@ let yjsDoc
 let provider
 let text
 
-// Logging
-const log = (message) => {
-  console.log(message)
+/**
+ * Logs a message to both console and UI.
+ * @param {string} message - Message to log
+ * @param {boolean} [isError=false] - Whether this is an error message
+ */
+const log = (message, isError = false) => {
+  if (DEBUG) {
+    // eslint-disable-next-line no-console
+    console.log(message)
+  }
   logEl.textContent += message + '\n'
   logEl.scrollTop = logEl.scrollHeight
+
+  if (isError) {
+    logEl.style.color = '#d32f2f'
+  } else {
+    logEl.style.color = 'inherit'
+  }
 }
 
-// Update peer display
+/**
+ * Updates the peer display UI with current connections.
+ */
 const updatePeerDisplay = () => {
   if (!libp2pNode) {
     return
@@ -55,28 +68,17 @@ const updatePeerDisplay = () => {
       peerMap.set(peerId, [])
     }
 
-    // Get transport from connection and remote address
     const remoteAddr = conn.remoteAddr.toString()
     let transport = 'unknown'
 
-    // Check for circuit relay (p2p-circuit in address)
     if (remoteAddr.includes('/p2p-circuit')) {
       transport = 'relay'
     } else if (remoteAddr.includes('/webrtc')) {
-      // Check for WebRTC
       transport = 'webrtc'
-    } else if (remoteAddr.includes('/webtransport')) {
-      // Check for WebTransport
-      transport = 'webtransport'
     } else if (remoteAddr.includes('/wss') || remoteAddr.includes('/tls/ws')) {
-      // Check for WebSocket Secure
       transport = 'websocket-secure'
     } else if (remoteAddr.includes('/ws')) {
-      // Check for WebSocket
       transport = 'websocket'
-    } else if (remoteAddr.includes('/tcp')) {
-      // If it has TCP but also has /ws, it's websocket over TCP
-      transport = 'tcp'
     }
 
     peerMap.get(peerId).push({ transport, addr: remoteAddr })
@@ -120,6 +122,20 @@ const updatePeerDisplay = () => {
   }
 }
 
+/**
+ * Validates a multiaddr string format.
+ * @param {string} addr - Multiaddr to validate
+ * @returns {boolean}
+ */
+const isValidMultiaddr = (addr) => {
+  try {
+    multiaddr(addr)
+    return true
+  } catch {
+    return false
+  }
+}
+
 // Connect button handler
 connectBtn.onclick = async () => {
   if (libp2pNode) {
@@ -129,13 +145,18 @@ connectBtn.onclick = async () => {
 
   const relayAddr = relayInput.value.trim()
   if (!relayAddr) {
-    log('Please enter a relay multiaddr')
+    log('Please enter a relay multiaddr', true)
+    return
+  }
+
+  if (!isValidMultiaddr(relayAddr)) {
+    log('Invalid multiaddr format', true)
     return
   }
 
   const topic = topicInput.value.trim()
   if (!topic) {
-    log('Please enter a topic')
+    log('Please enter a topic', true)
     return
   }
 
@@ -146,17 +167,10 @@ connectBtn.onclick = async () => {
     // Create libp2p node with WebRTC, relay, and pubsub
     libp2pNode = await createLibp2p({
       addresses: {
-        listen: [
-          '/p2p-circuit',
-          '/webrtc',
-          '/wss',
-          '/ws'
-        ]
+        listen: ['/p2p-circuit', '/webrtc']
       },
       transports: [
-        webSockets({
-          filter: filters.all
-        }),
+        webSockets({ filter: filters.all }),
         webRTC({
           rtcConfiguration: {
             iceServers: [
@@ -165,31 +179,29 @@ connectBtn.onclick = async () => {
             ]
           }
         }),
-        webRTCDirect(),
         circuitRelayTransport({
-          reservationCompletionTimeout: 20000
+          reservationCompletionTimeout: TIMEOUTS.RELAY_CONNECTION
         })
       ],
       connectionEncrypters: [noise()],
       streamMuxers: [yamux()],
       connectionManager: {
-        inboundStreamProtocolNegotiationTimeout: 10000,
-        inboundUpgradeTimeout: 10000,
-        outboundStreamProtocolNegotiationTimeout: 10000,
-        outboundUpgradeTimeout: 10000
+        inboundStreamProtocolNegotiationTimeout: TIMEOUTS.PROTOCOL_NEGOTIATION_INBOUND,
+        inboundUpgradeTimeout: TIMEOUTS.UPGRADE_INBOUND,
+        outboundStreamProtocolNegotiationTimeout: TIMEOUTS.PROTOCOL_NEGOTIATION_OUTBOUND,
+        outboundUpgradeTimeout: TIMEOUTS.UPGRADE_OUTBOUND
       },
       connectionGater: {
         denyDialMultiaddr: () => false
       },
       peerDiscovery: [
         pubsubPeerDiscovery({
-          interval: 10000,
-          topics: ['_peer-discovery._p2p._pubsub'],
+          interval: INTERVALS.PUBSUB_PEER_DISCOVERY,
+          topics: PUBSUB_DISCOVERY.TOPICS,
           listenOnly: false
         })
       ],
       services: {
-        ping: ping(),
         identify: identify(),
         identifyPush: identifyPush(),
         autoNAT: autoNAT(),
@@ -197,24 +209,27 @@ connectBtn.onclick = async () => {
         pubsub: gossipsub({
           emitSelf: false,
           allowPublishToZeroTopicPeers: true,
-          // Speed up gossipsub mesh formation
-          heartbeatInterval: 1000, // Send heartbeat every 1 second (default is 1000ms)
+          heartbeatInterval: INTERVALS.GOSSIPSUB_HEARTBEAT,
           directPeers: [],
-          floodPublish: true // Broadcast to all peers, not just mesh
+          floodPublish: true
         })
       }
     })
 
-    log(`libp2p node created with id: ${libp2pNode.peerId}`)
+    log(`libp2p node created with id: ${libp2pNode.peerId.toString().slice(0, 12)}...`)
 
     // Expose for testing
     window.libp2pNode = libp2pNode
 
     // Connect to relay
     log('Connecting to relay...')
-    const ma = multiaddr(relayAddr)
-    await libp2pNode.dial(ma)
-    log('Connected to relay!')
+    try {
+      const ma = multiaddr(relayAddr)
+      await libp2pNode.dial(ma)
+      log('Connected to relay!')
+    } catch (err) {
+      throw new Error(`Failed to connect to relay: ${err.message}`)
+    }
 
     // Create Yjs document
     yjsDoc = new Y.Doc()
@@ -252,29 +267,52 @@ connectBtn.onclick = async () => {
     // Initial peer display update
     updatePeerDisplay()
 
-    // Log connection events and update peer display
+    // Update peer display on connection events
     libp2pNode.addEventListener('peer:connect', (evt) => {
-      log(`Connected to peer: ${evt.detail}`)
       updatePeerDisplay()
+      if (DEBUG) {
+        log(`Connected to peer: ${evt.detail.toString().slice(0, 12)}...`)
+      }
     })
 
     libp2pNode.addEventListener('peer:disconnect', (evt) => {
-      log(`Disconnected from peer: ${evt.detail}`)
       updatePeerDisplay()
+      if (DEBUG) {
+        log(`Disconnected from peer: ${evt.detail.toString().slice(0, 12)}...`)
+      }
     })
   } catch (err) {
-    log(`Error: ${err.message}`)
-    console.error(err)
+    log(`Error: ${err.message}`, true)
+    // eslint-disable-next-line no-console
+    console.error('Connection error:', err)
     connectBtn.disabled = false
+
+    // Clean up on error
+    if (libp2pNode) {
+      try {
+        await libp2pNode.stop()
+      } catch (stopErr) {
+        // eslint-disable-next-line no-console
+        console.error('Error stopping libp2p:', stopErr)
+      }
+      libp2pNode = null
+    }
   }
 }
 
-// Cleanup on page unload
-window.addEventListener('beforeunload', () => {
-  if (provider) {
-    provider.destroy()
-  }
-  if (libp2pNode) {
-    libp2pNode.stop()
+/**
+ * Cleanup resources on page unload.
+ */
+window.addEventListener('beforeunload', async () => {
+  try {
+    if (provider) {
+      await provider.destroy()
+    }
+    if (libp2pNode) {
+      await libp2pNode.stop()
+    }
+  } catch (err) {
+    // eslint-disable-next-line no-console
+    console.error('Cleanup error:', err)
   }
 })
diff --git a/examples/js-libp2p-example-yjs-libp2p/package.json b/examples/js-libp2p-example-yjs-libp2p/package.json
index 777aa21..2ffe57d 100644
--- a/examples/js-libp2p-example-yjs-libp2p/package.json
+++ b/examples/js-libp2p-example-yjs-libp2p/package.json
@@ -7,6 +7,9 @@
     "start": "vite",
     "build": "vite build",
     "relay": "node relay.js",
+    "relay:debug": "DEBUG=true node relay.js",
+    "lint": "eslint *.js",
+    "lint:fix": "eslint --fix *.js",
     "test:firefox": "playwright test --project=firefox",
     "test:chrome": "playwright test --project=chromium",
     "test": "playwright test"
@@ -32,7 +35,10 @@
     "yjs": "^13.6.18"
   },
   "devDependencies": {
+    "@eslint/js": "^9.0.0",
     "@playwright/test": "^1.56.1",
+    "eslint": "^9.0.0",
+    "globals": "^15.0.0",
     "test-ipfs-example": "^1.0.0"
   }
 }
diff --git a/examples/js-libp2p-example-yjs-libp2p/relay-constants.js b/examples/js-libp2p-example-yjs-libp2p/relay-constants.js
new file mode 100644
index 0000000..b8c6a97
--- /dev/null
+++ b/examples/js-libp2p-example-yjs-libp2p/relay-constants.js
@@ -0,0 +1,45 @@
+/**
+ * Configuration constants for the relay server
+ */
+
+// Debug mode - set via environment variable
+export const DEBUG = process.env.DEBUG === 'true' || false
+
+// Relay server timeouts (milliseconds)
+export const RELAY_TIMEOUTS = {
+  HOP_TIMEOUT: 30000,
+  PROTOCOL_NEGOTIATION_INBOUND: 30000,
+  PROTOCOL_NEGOTIATION_OUTBOUND: 30000,
+  UPGRADE_INBOUND: 30000,
+  UPGRADE_OUTBOUND: 30000,
+  DIAL_TIMEOUT: 30000
+}
+
+// Relay server reservation configuration
+export const RELAY_RESERVATIONS = {
+  MAX_RESERVATIONS: 1000,
+  RESERVATION_TTL: 2 * 60 * 60 * 1000, // 2 hours
+  DEFAULT_DATA_LIMIT: BigInt(1024 * 1024 * 1024), // 1 GB
+  DEFAULT_DURATION_LIMIT: 2 * 60 * 1000 // 2 minutes
+}
+
+// Connection manager configuration
+export const CONNECTION_CONFIG = {
+  MAX_CONNECTIONS: 1000,
+  MAX_INCOMING_PENDING: 100,
+  MAX_PEER_ADDRS_TO_DIAL: 100
+}
+
+// Peer discovery configuration
+export const DISCOVERY_CONFIG = {
+  INTERVAL: 5000,
+  TOPICS: ['_peer-discovery._p2p._pubsub']
+}
+
+// Monitoring intervals (milliseconds)
+export const MONITORING = {
+  TOPIC_STATUS_INTERVAL: 10000
+}
+
+// Default Yjs topic to subscribe
+export const DEFAULT_TOPIC = 'yjs-doc-1'
diff --git a/examples/js-libp2p-example-yjs-libp2p/relay.js b/examples/js-libp2p-example-yjs-libp2p/relay.js
index 20cb53f..01a5e7e 100644
--- a/examples/js-libp2p-example-yjs-libp2p/relay.js
+++ b/examples/js-libp2p-example-yjs-libp2p/relay.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-console */
-
 import { gossipsub } from '@chainsafe/libp2p-gossipsub'
 import { noise } from '@chainsafe/libp2p-noise'
 import { yamux } from '@chainsafe/libp2p-yamux'
@@ -7,13 +5,21 @@ import { autoNAT } from '@libp2p/autonat'
 import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
 import { dcutr } from '@libp2p/dcutr'
 import { identify, identifyPush } from '@libp2p/identify'
-import { ping } from '@libp2p/ping'
 import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery'
 import { tcp } from '@libp2p/tcp'
 import { webRTC, webRTCDirect } from '@libp2p/webrtc'
 import { webSockets } from '@libp2p/websockets'
 import * as filters from '@libp2p/websockets/filters'
 import { createLibp2p } from 'libp2p'
+import {
+  DEBUG,
+  RELAY_TIMEOUTS,
+  RELAY_RESERVATIONS,
+  CONNECTION_CONFIG,
+  DISCOVERY_CONFIG,
+  MONITORING,
+  DEFAULT_TOPIC
+} from './relay-constants.js'
 
 const server = await createLibp2p({
   addresses: {
@@ -34,28 +40,27 @@ const server = await createLibp2p({
   ],
   peerDiscovery: [
     pubsubPeerDiscovery({
-      interval: 5000,
-      topics: ['_peer-discovery._p2p._pubsub'],
+      interval: DISCOVERY_CONFIG.INTERVAL,
+      topics: DISCOVERY_CONFIG.TOPICS,
       listenOnly: false
     })
   ],
   connectionEncrypters: [noise()],
   streamMuxers: [yamux()],
   connectionManager: {
-    inboundStreamProtocolNegotiationTimeout: 30000,
-    inboundUpgradeTimeout: 30000,
-    outboundStreamProtocolNegotiationTimeout: 30000,
-    outboundUpgradeTimeout: 30000,
-    maxConnections: 1000,
-    maxIncomingPendingConnections: 100,
-    maxPeerAddrsToDial: 100,
-    dialTimeout: 30000
+    inboundStreamProtocolNegotiationTimeout: RELAY_TIMEOUTS.PROTOCOL_NEGOTIATION_INBOUND,
+    inboundUpgradeTimeout: RELAY_TIMEOUTS.UPGRADE_INBOUND,
+    outboundStreamProtocolNegotiationTimeout: RELAY_TIMEOUTS.PROTOCOL_NEGOTIATION_OUTBOUND,
+    outboundUpgradeTimeout: RELAY_TIMEOUTS.UPGRADE_OUTBOUND,
+    maxConnections: CONNECTION_CONFIG.MAX_CONNECTIONS,
+    maxIncomingPendingConnections: CONNECTION_CONFIG.MAX_INCOMING_PENDING,
+    maxPeerAddrsToDial: CONNECTION_CONFIG.MAX_PEER_ADDRS_TO_DIAL,
+    dialTimeout: RELAY_TIMEOUTS.DIAL_TIMEOUT
   },
   connectionGater: {
     denyDialMultiaddr: () => false
   },
   services: {
-    ping: ping(),
     identify: identify(),
     identifyPush: identifyPush(),
     autoNAT: autoNAT(),
@@ -64,15 +69,15 @@ const server = await createLibp2p({
       emitSelf: false,
       allowPublishToZeroTopicPeers: true,
       canRelayMessage: true,
-      floodPublish: true // Broadcast to all peers, not just mesh
+      floodPublish: true
     }),
     relay: circuitRelayServer({
-      hopTimeout: 30000,
+      hopTimeout: RELAY_TIMEOUTS.HOP_TIMEOUT,
       reservations: {
-        maxReservations: 1000,
-        reservationTtl: 2 * 60 * 60 * 1000,
-        defaultDataLimit: BigInt(1024 * 1024 * 1024),
-        defaultDurationLimit: 2 * 60 * 1000
+        maxReservations: RELAY_RESERVATIONS.MAX_RESERVATIONS,
+        reservationTtl: RELAY_RESERVATIONS.RESERVATION_TTL,
+        defaultDataLimit: RELAY_RESERVATIONS.DEFAULT_DATA_LIMIT,
+        defaultDurationLimit: RELAY_RESERVATIONS.DEFAULT_DURATION_LIMIT
       }
     })
   }
@@ -81,58 +86,59 @@ const server = await createLibp2p({
 // Set up peer discovery listener to dial discovered peers
 server.addEventListener('peer:discovery', async (evt) => {
   const peer = evt.detail
-  console.log(`Discovered peer: ${peer.id.toString()}`)
-
-  // Check if we're already connected to this peer
   const connections = server.getConnections(peer.id)
-  if (!connections || connections.length === 0) {
-    console.log(`Dialing new peer: ${peer.id.toString()}`)
 
-    try {
-      // Dial the peer ID directly - libp2p will handle finding the best route
-      await server.dial(peer.id)
-      console.log(`Successfully dialed peer: ${peer.id.toString()}`)
-    } catch (error) {
-      console.error(`Failed to dial peer ${peer.id.toString()}:`, error.message)
+  if (connections && connections.length > 0) {
+    return
+  }
+
+  try {
+    await server.dial(peer.id)
+    if (DEBUG) {
+      // eslint-disable-next-line no-console
+      console.log(`Dialed peer: ${peer.id.toString().slice(0, 12)}...`)
+    }
+  } catch (error) {
+    if (DEBUG) {
+      // eslint-disable-next-line no-console
+      console.warn(`Failed to dial peer: ${error.message}`)
     }
-  } else {
-    console.log(`Already connected to peer: ${peer.id.toString()}`)
   }
 })
 
 server.addEventListener('peer:connect', (evt) => {
-  console.log(`Connected to peer: ${evt.detail.toString()}`)
+  if (DEBUG) {
+    // eslint-disable-next-line no-console
+    console.log(`Peer connected: ${evt.detail.toString().slice(0, 12)}...`)
+  }
 })
 
 server.addEventListener('peer:disconnect', (evt) => {
-  console.log(`Disconnected from peer: ${evt.detail.toString()}`)
-})
-
-// Log all messages passing through ALL topics
-server.services.pubsub.addEventListener('message', (evt) => {
-  console.log('\n📨 Message received on topic:', evt.detail.topic)
-  console.log('  From:', evt.detail.from.toString())
-  console.log('  Data length:', evt.detail.data.length, 'bytes')
-
-  try {
-    const msgStr = new TextDecoder().decode(evt.detail.data)
-    const msg = JSON.parse(msgStr)
-    console.log('  Type:', msg.type)
-  } catch (e) {
-    console.log('  (Could not parse message data)')
+  if (DEBUG) {
+    // eslint-disable-next-line no-console
+    console.log(`Peer disconnected: ${evt.detail.toString().slice(0, 12)}...`)
   }
 })
 
+// Log pubsub messages when debug mode is enabled
+if (DEBUG) {
+  server.services.pubsub.addEventListener('message', (evt) => {
+    try {
+      const msgStr = new TextDecoder().decode(evt.detail.data)
+      const msg = JSON.parse(msgStr)
+      // eslint-disable-next-line no-console
+      console.log(`📨 ${msg.type} on ${evt.detail.topic} from ${evt.detail.from.toString().slice(0, 12)}...`)
+    } catch {
+      // eslint-disable-next-line no-console
+      console.log(`📨 Message on ${evt.detail.topic} (${evt.detail.data.length} bytes)`)
+    }
+  })
+}
+
 // Subscribe to topics dynamically as we see them
 const subscribedTopics = new Set()
 
 server.services.pubsub.addEventListener('subscription-change', async (evt) => {
-  console.log('\n📢 Subscription change:', evt.detail)
-  const peerId = evt.detail.peerId ? evt.detail.peerId.toString() : 'unknown'
-  console.log('  Peer:', peerId)
-  console.log('  Subscriptions:', evt.detail.subscriptions)
-
-  // Auto-subscribe to any Yjs or test topics we see
   const subscriptions = evt.detail.subscriptions
   if (!subscriptions || !Array.isArray(subscriptions)) {
     return
@@ -152,35 +158,48 @@ server.services.pubsub.addEventListener('subscription-change', async (evt) => {
     subscribedTopics.add(topic)
     try {
       await server.services.pubsub.subscribe(topic)
-      console.log(`📡 Relay auto-subscribed to: ${topic}`)
+      if (DEBUG) {
+        // eslint-disable-next-line no-console
+        console.log(`📡 Auto-subscribed to: ${topic}`)
+      }
     } catch (err) {
+      // eslint-disable-next-line no-console
       console.error('Failed to subscribe:', err)
     }
   }
 })
 
 // Subscribe to default Yjs topic
-const DEFAULT_TOPIC = 'yjs-doc-1'
 await server.services.pubsub.subscribe(DEFAULT_TOPIC)
 subscribedTopics.add(DEFAULT_TOPIC)
+// eslint-disable-next-line no-console
 console.log(`📡 Relay subscribed to default topic: ${DEFAULT_TOPIC}`)
 
-// Periodically log all active topics and subscribers
-setInterval(() => {
-  const topics = server.services.pubsub.getTopics()
-  if (topics.length > 0) {
-    console.log('\n📋 Active topics:', topics)
-    for (const topic of topics) {
-      const subscribers = server.services.pubsub.getSubscribers(topic)
-      if (subscribers.length > 0) {
-        console.log(`  👥 ${topic}: ${subscribers.length} subscribers`)
+// Periodically log active topics and subscribers in debug mode
+if (DEBUG) {
+  setInterval(() => {
+    const topics = server.services.pubsub.getTopics()
+    if (topics.length > 0) {
+      // eslint-disable-next-line no-console
+      console.log('\n📋 Active topics:', topics)
+      for (const topic of topics) {
+        const subscribers = server.services.pubsub.getSubscribers(topic)
+        if (subscribers.length > 0) {
+          // eslint-disable-next-line no-console
+          console.log(`  👥 ${topic}: ${subscribers.length} subscribers`)
+        }
       }
     }
-  }
-}, 10000)
+  }, MONITORING.TOPIC_STATUS_INTERVAL)
+}
 
+// eslint-disable-next-line no-console
 console.info('\nThe relay node is running and listening on the following multiaddrs:')
+// eslint-disable-next-line no-console
 console.info('')
+// eslint-disable-next-line no-console
 console.info(server.getMultiaddrs().map((ma) => ma.toString()).join('\n'))
+// eslint-disable-next-line no-console
 console.info('')
+// eslint-disable-next-line no-console
 console.info('Copy one of the above multiaddrs and use it in the browser client')
diff --git a/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
index 6efaab3..02d33e7 100644
--- a/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
+++ b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
@@ -1,20 +1,32 @@
-/* eslint-disable no-console, default-case */
-
 import { fromString, toString } from 'uint8arrays'
 import * as Y from 'yjs'
+import { DEBUG, INTERVALS } from './constants.js'
 
 /**
- * Yjs connection provider using libp2p for peer-to-peer connectivity
- * This replaces y-webrtc and uses libp2p's pubsub for synchronization
+ * Yjs connection provider using libp2p for peer-to-peer connectivity.
+ * This replaces y-webrtc and uses libp2p's pubsub for synchronization.
  */
 export class Libp2pProvider {
   /**
+   * Creates a new Libp2pProvider for Yjs document synchronization.
+   *
    * @param {string} topic - The pubsub topic to use for this document
    * @param {Y.Doc} doc - The Yjs document to sync
    * @param {import('libp2p').Libp2p} libp2p - The libp2p instance
-   * @param {object} options - Provider options
+   * @param {object} [options] - Provider options
+   * @param {object} [options.awareness] - Yjs awareness instance for cursor/selection sharing
+   * @throws {Error} If topic is empty or libp2p node is not initialized
    */
   constructor (topic, doc, libp2p, options = {}) {
+    if (!topic || typeof topic !== 'string') {
+      throw new Error('Topic must be a non-empty string')
+    }
+    if (!doc || !(doc instanceof Y.Doc)) {
+      throw new Error('doc must be a valid Yjs document')
+    }
+    if (!libp2p) {
+      throw new Error('libp2p node must be provided')
+    }
     this.topic = topic
     this.doc = doc
     this.libp2p = libp2p
@@ -44,185 +56,206 @@ export class Libp2pProvider {
   }
 
   /**
-   * Subscribe to the pubsub topic for this document
+   * Subscribe to the pubsub topic for this document.
+   * @private
+   * @returns {Promise}
    */
   async _subscribeToPubsub () {
     try {
       await this.libp2p.services.pubsub.subscribe(this.topic)
       this.libp2p.services.pubsub.addEventListener('message', this._onPubsubMessage)
       this.connected = true
-      console.log(`✅ Subscribed to Yjs topic: ${this.topic}`)
 
-      // Log current subscriptions
-      const topics = this.libp2p.services.pubsub.getTopics()
-      console.log('All subscribed topics:', topics)
-
-      // Check peers multiple times as gossipsub mesh forms
-      const checkPeers = () => {
-        const peers = this.libp2p.services.pubsub.getSubscribers(this.topic)
-        console.log(`Peers subscribed to ${this.topic}:`, peers.map(p => p.toString()))
-
-        if (peers.length === 0) {
-          console.warn('⚠️ No peers subscribed to this topic yet. Waiting for gossipsub mesh...')
-        } else {
-          console.log('✅ Gossipsub mesh formed!')
+      if (DEBUG) {
+        // eslint-disable-next-line no-console
+        console.log(`✅ Subscribed to Yjs topic: ${this.topic}`)
+        const topics = this.libp2p.services.pubsub.getTopics()
+        // eslint-disable-next-line no-console
+        console.log('All subscribed topics:', topics)
+
+        // Check peers as gossipsub mesh forms
+        const checkPeers = () => {
+          const peers = this.libp2p.services.pubsub.getSubscribers(this.topic)
+          // eslint-disable-next-line no-console
+          console.log(`Peers on ${this.topic}:`, peers.map((p) => p.toString()))
         }
-      }
 
-      setTimeout(checkPeers, 2000)
-      setTimeout(checkPeers, 5000)
-      setTimeout(checkPeers, 10000)
+        setTimeout(checkPeers, INTERVALS.PEER_CHECK)
+        setTimeout(checkPeers, INTERVALS.PEER_CHECK * 2.5)
+        setTimeout(checkPeers, INTERVALS.PEER_CHECK * 5)
+      }
     } catch (err) {
+      // eslint-disable-next-line no-console
       console.error('Failed to subscribe to pubsub topic:', err)
+      throw err
     }
   }
 
   /**
-   * Set up peer discovery to connect to discovered peers
+   * Set up peer discovery to connect to discovered peers.
+   * @private
    */
   _setupPeerDiscovery () {
-    // Listen for peer discovery events from pubsubPeerDiscovery
     this.libp2p.addEventListener('peer:discovery', this._onPeerDiscovered)
 
-    // Track connections
-    this.libp2p.addEventListener('peer:connect', async (evt) => {
+    this.libp2p.addEventListener('peer:connect', (evt) => {
       const peerId = evt.detail.toString()
-      console.log(`Connected to peer: ${peerId}`)
       this.connectedPeers.add(peerId)
+      if (DEBUG) {
+        // eslint-disable-next-line no-console
+        console.log(`Connected to peer: ${peerId}`)
+      }
     })
 
     this.libp2p.addEventListener('peer:disconnect', (evt) => {
       const peerId = evt.detail.toString()
-      console.log(`Disconnected from peer: ${peerId}`)
       this.connectedPeers.delete(peerId)
+      if (DEBUG) {
+        // eslint-disable-next-line no-console
+        console.log(`Disconnected from peer: ${peerId}`)
+      }
     })
   }
 
   /**
-   * Handle peer discovery events
-   *
-   * @param {any} evt
+   * Handle peer discovery events.
+   * @private
+   * @param {CustomEvent} evt - Peer discovery event
+   * @returns {Promise}
    */
   async _handlePeerDiscovered (evt) {
     const peer = evt.detail
     const peerId = peer.id.toString()
-    console.log(`[Provider] Discovered peer: ${peerId}`)
 
-    // Don't dial ourselves
     if (this.libp2p.peerId.equals(peer.id)) {
-      console.log(`[Provider] Skipping self: ${peerId}`)
       return
     }
 
-    // Check if we're already connected to this peer
     const connections = this.libp2p.getConnections(peer.id)
     if (connections && connections.length > 0) {
-      console.log(`[Provider] Already connected to peer: ${peerId} (${connections.length} connections)`)
+      if (DEBUG) {
+        // eslint-disable-next-line no-console
+        console.log(`Already connected to peer: ${peerId}`)
+      }
       return
     }
 
-    console.log(`[Provider] Dialing new peer: ${peerId}`)
-    console.log('[Provider] Peer addresses:', peer.multiaddrs.map(ma => ma.toString()))
-
     try {
-      // Dial the peer ID directly - libp2p will handle finding the best route
+      if (DEBUG) {
+        // eslint-disable-next-line no-console
+        console.log(`Dialing peer: ${peerId}`)
+      }
       await this.libp2p.dial(peer.id)
-      console.log(`[Provider] ✅ Successfully dialed peer: ${peerId}`)
     } catch (error) {
-      console.error(`[Provider] ❌ Failed to dial peer ${peerId}:`, error.message)
+      if (DEBUG) {
+        // eslint-disable-next-line no-console
+        console.warn(`Failed to dial peer ${peerId}:`, error.message)
+      }
     }
   }
 
   /**
-   * Request initial document state from connected peers
+   * Request initial document state from connected peers.
+   * @private
+   * @returns {Promise}
    */
   async _requestInitialState () {
-    // Wait a bit for peers to connect
     setTimeout(() => {
       const stateVector = Y.encodeStateVector(this.doc)
       this._publishMessage({
         type: 'sync-request',
         stateVector: toString(stateVector, 'base64')
+      }).catch((err) => {
+        if (DEBUG) {
+          // eslint-disable-next-line no-console
+          console.error('Failed to send sync request:', err)
+        }
       })
-    }, 1000)
+    }, INTERVALS.INITIAL_SYNC_REQUEST)
   }
 
   /**
-   * Handle Yjs document updates
-   *
-   * @param {Uint8Array} update
-   * @param {any} origin
+   * Handle Yjs document updates.
+   * @private
+   * @param {Uint8Array} update - The document update
+   * @param {any} origin - Origin of the update
    */
   _handleDocUpdate (update, origin) {
-    // Don't broadcast updates that came from the network
     if (origin === this) {
       return
     }
 
-    console.log('Broadcasting Yjs update to peers')
-    // Broadcast the update to all peers via pubsub
     this._publishMessage({
       type: 'update',
       update: toString(update, 'base64')
+    }).catch((err) => {
+      // eslint-disable-next-line no-console
+      console.error('Failed to broadcast update:', err)
     })
   }
 
   /**
-   * Handle incoming pubsub messages
-   *
-   * @param {any} evt
+   * Handle incoming pubsub messages.
+   * @private
+   * @param {CustomEvent} evt - Pubsub message event
    */
   _handlePubsubMessage (evt) {
-    // Ignore our own messages
-    if (evt.detail.topic !== this.topic) {
+    if (evt.detail.topic !== this.topic || this.libp2p.peerId.equals(evt.detail.from)) {
       return
     }
-    if (this.libp2p.peerId.equals(evt.detail.from)) {
-      return
-    }
-
-    console.log(`Received pubsub message from ${evt.detail.from.toString()}, type: ${evt.detail.topic}`)
 
     try {
       const message = JSON.parse(toString(evt.detail.data, 'utf8'))
-      console.log(`Message type: ${message.type}`)
+
+      if (DEBUG) {
+        // eslint-disable-next-line no-console
+        console.log(`Received ${message.type} from ${evt.detail.from.toString()}`)
+      }
 
       switch (message.type) {
-        case 'update':
-          this._applyUpdate(message.update)
-          break
-        case 'sync-request':
-          this._handleSyncRequest(message.stateVector)
-          break
-        case 'sync-response':
-          this._handleSyncResponse(message.update)
-          break
+      case 'update':
+        this._applyUpdate(message.update)
+        break
+      case 'sync-request':
+        this._handleSyncRequest(message.stateVector)
+        break
+      case 'sync-response':
+        this._handleSyncResponse(message.update)
+        break
+      default:
+        if (DEBUG) {
+          // eslint-disable-next-line no-console
+          console.warn(`Unknown message type: ${message.type}`)
+        }
       }
     } catch (err) {
+      // eslint-disable-next-line no-console
       console.error('Failed to process pubsub message:', err)
     }
   }
 
   /**
-   * Apply an update to the document
-   *
-   * @param {string} updateBase64
+   * Apply an update to the document.
+   * @private
+   * @param {string} updateBase64 - Base64-encoded Yjs update
    */
   _applyUpdate (updateBase64) {
-    console.log('Applying Yjs update from network')
     const update = fromString(updateBase64, 'base64')
     Y.applyUpdate(this.doc, update, this)
 
     if (!this.synced) {
       this.synced = true
-      console.log('Document synced with network')
+      if (DEBUG) {
+        // eslint-disable-next-line no-console
+        console.log('Document synced with network')
+      }
     }
   }
 
   /**
-   * Handle sync request from a peer
-   *
-   * @param {string} stateVectorBase64
+   * Handle sync request from a peer.
+   * @private
+   * @param {string} stateVectorBase64 - Base64-encoded state vector
    */
   _handleSyncRequest (stateVectorBase64) {
     const stateVector = fromString(stateVectorBase64, 'base64')
@@ -231,54 +264,65 @@ export class Libp2pProvider {
     this._publishMessage({
       type: 'sync-response',
       update: toString(update, 'base64')
+    }).catch((err) => {
+      // eslint-disable-next-line no-console
+      console.error('Failed to send sync response:', err)
     })
   }
 
   /**
-   * Handle sync response from a peer
-   *
-   * @param {string} updateBase64
+   * Handle sync response from a peer.
+   * @private
+   * @param {string} updateBase64 - Base64-encoded Yjs update
    */
   _handleSyncResponse (updateBase64) {
     this._applyUpdate(updateBase64)
   }
 
   /**
-   * Publish a message to the pubsub topic
-   *
-   * @param {object} message
+   * Publish a message to the pubsub topic.
+   * @private
+   * @param {object} message - Message object to publish
+   * @param {string} message.type - Message type (update, sync-request, sync-response)
+   * @returns {Promise}
    */
   async _publishMessage (message) {
     try {
       const data = fromString(JSON.stringify(message), 'utf8')
 
-      // Check peers subscribed to this topic
-      const subscribers = this.libp2p.services.pubsub.getSubscribers(this.topic)
-      console.log(`Publishing message type: ${message.type} to topic: ${this.topic}`)
-      console.log(`Subscribers to ${this.topic}:`, subscribers.map(p => p.toString()))
-      console.log('Total connected peers:', this.libp2p.getConnections().length)
+      if (DEBUG) {
+        const subscribers = this.libp2p.services.pubsub.getSubscribers(this.topic)
+        // eslint-disable-next-line no-console
+        console.log(`Publishing ${message.type} to ${this.topic} (${subscribers.length} subscribers)`)
+      }
 
-      const result = await this.libp2p.services.pubsub.publish(this.topic, data)
-      console.log('Message published successfully', result)
+      await this.libp2p.services.pubsub.publish(this.topic, data)
     } catch (err) {
+      // eslint-disable-next-line no-console
       console.error('Failed to publish message:', err)
+      throw err
     }
   }
 
   /**
-   * Destroy the provider and clean up resources
+   * Destroy the provider and clean up resources.
+   * @returns {Promise}
    */
-  destroy () {
-    this.doc.off('update', this._onUpdate)
-    this.libp2p.services.pubsub.removeEventListener('message', this._onPubsubMessage)
-    this.libp2p.removeEventListener('peer:discovery', this._onPeerDiscovered)
-
-    // Unsubscribe from topic
-    this.libp2p.services.pubsub.unsubscribe(this.topic).catch(err => {
-      console.error('Failed to unsubscribe from topic:', err)
-    })
+  async destroy () {
+    try {
+      this.doc.off('update', this._onUpdate)
+      this.libp2p.services.pubsub.removeEventListener('message', this._onPubsubMessage)
+      this.libp2p.removeEventListener('peer:discovery', this._onPeerDiscovered)
 
-    this.connected = false
-    this.synced = false
+      await this.libp2p.services.pubsub.unsubscribe(this.topic)
+
+      this.connected = false
+      this.synced = false
+      this.connectedPeers.clear()
+    } catch (err) {
+      // eslint-disable-next-line no-console
+      console.error('Error during provider cleanup:', err)
+      throw err
+    }
   }
 }

From d2329d1760dc0addb121b9eb1de58b61b7e9a713 Mon Sep 17 00:00:00 2001
From: Nico Krause 
Date: Wed, 29 Oct 2025 22:29:44 +0500
Subject: [PATCH 12/12] cleanup

---
 .../eslint.config.js                          | 33 ----------------
 .../js-libp2p-example-yjs-libp2p/index.js     |  4 +-
 .../js-libp2p-example-yjs-libp2p/package.json |  5 ---
 .../yjs-libp2p-provider.js                    | 39 ++++++++++++-------
 4 files changed, 28 insertions(+), 53 deletions(-)
 delete mode 100644 examples/js-libp2p-example-yjs-libp2p/eslint.config.js

diff --git a/examples/js-libp2p-example-yjs-libp2p/eslint.config.js b/examples/js-libp2p-example-yjs-libp2p/eslint.config.js
deleted file mode 100644
index dba41db..0000000
--- a/examples/js-libp2p-example-yjs-libp2p/eslint.config.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import js from '@eslint/js'
-import globals from 'globals'
-
-export default [
-  js.configs.recommended,
-  {
-    languageOptions: {
-      ecmaVersion: 'latest',
-      sourceType: 'module',
-      globals: {
-        ...globals.browser,
-        ...globals.node
-      }
-    },
-    rules: {
-      'no-console': 'warn',
-      'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
-      'prefer-const': 'error',
-      'no-var': 'error',
-      eqeqeq: ['error', 'always'],
-      curly: ['error', 'all'],
-      'brace-style': ['error', '1tbs'],
-      quotes: ['error', 'single', { avoidEscape: true }],
-      semi: ['error', 'never'],
-      indent: ['error', 2],
-      'comma-dangle': ['error', 'never'],
-      'arrow-parens': ['error', 'always'],
-      'space-before-function-paren': ['error', 'always'],
-      'object-curly-spacing': ['error', 'always'],
-      'array-bracket-spacing': ['error', 'never']
-    }
-  }
-]
diff --git a/examples/js-libp2p-example-yjs-libp2p/index.js b/examples/js-libp2p-example-yjs-libp2p/index.js
index b2e760d..2afd139 100644
--- a/examples/js-libp2p-example-yjs-libp2p/index.js
+++ b/examples/js-libp2p-example-yjs-libp2p/index.js
@@ -32,8 +32,9 @@ let text
 
 /**
  * Logs a message to both console and UI.
+ *
  * @param {string} message - Message to log
- * @param {boolean} [isError=false] - Whether this is an error message
+ * @param {boolean} [isError] - Whether this is an error message
  */
 const log = (message, isError = false) => {
   if (DEBUG) {
@@ -124,6 +125,7 @@ const updatePeerDisplay = () => {
 
 /**
  * Validates a multiaddr string format.
+ *
  * @param {string} addr - Multiaddr to validate
  * @returns {boolean}
  */
diff --git a/examples/js-libp2p-example-yjs-libp2p/package.json b/examples/js-libp2p-example-yjs-libp2p/package.json
index 2ffe57d..24fb349 100644
--- a/examples/js-libp2p-example-yjs-libp2p/package.json
+++ b/examples/js-libp2p-example-yjs-libp2p/package.json
@@ -8,8 +8,6 @@
     "build": "vite build",
     "relay": "node relay.js",
     "relay:debug": "DEBUG=true node relay.js",
-    "lint": "eslint *.js",
-    "lint:fix": "eslint --fix *.js",
     "test:firefox": "playwright test --project=firefox",
     "test:chrome": "playwright test --project=chromium",
     "test": "playwright test"
@@ -35,10 +33,7 @@
     "yjs": "^13.6.18"
   },
   "devDependencies": {
-    "@eslint/js": "^9.0.0",
     "@playwright/test": "^1.56.1",
-    "eslint": "^9.0.0",
-    "globals": "^15.0.0",
     "test-ipfs-example": "^1.0.0"
   }
 }
diff --git a/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
index 02d33e7..e912418 100644
--- a/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
+++ b/examples/js-libp2p-example-yjs-libp2p/yjs-libp2p-provider.js
@@ -57,6 +57,7 @@ export class Libp2pProvider {
 
   /**
    * Subscribe to the pubsub topic for this document.
+   *
    * @private
    * @returns {Promise}
    */
@@ -93,6 +94,7 @@ export class Libp2pProvider {
 
   /**
    * Set up peer discovery to connect to discovered peers.
+   *
    * @private
    */
   _setupPeerDiscovery () {
@@ -119,6 +121,7 @@ export class Libp2pProvider {
 
   /**
    * Handle peer discovery events.
+   *
    * @private
    * @param {CustomEvent} evt - Peer discovery event
    * @returns {Promise}
@@ -156,6 +159,7 @@ export class Libp2pProvider {
 
   /**
    * Request initial document state from connected peers.
+   *
    * @private
    * @returns {Promise}
    */
@@ -176,6 +180,7 @@ export class Libp2pProvider {
 
   /**
    * Handle Yjs document updates.
+   *
    * @private
    * @param {Uint8Array} update - The document update
    * @param {any} origin - Origin of the update
@@ -196,6 +201,7 @@ export class Libp2pProvider {
 
   /**
    * Handle incoming pubsub messages.
+   *
    * @private
    * @param {CustomEvent} evt - Pubsub message event
    */
@@ -213,20 +219,20 @@ export class Libp2pProvider {
       }
 
       switch (message.type) {
-      case 'update':
-        this._applyUpdate(message.update)
-        break
-      case 'sync-request':
-        this._handleSyncRequest(message.stateVector)
-        break
-      case 'sync-response':
-        this._handleSyncResponse(message.update)
-        break
-      default:
-        if (DEBUG) {
-          // eslint-disable-next-line no-console
-          console.warn(`Unknown message type: ${message.type}`)
-        }
+        case 'update':
+          this._applyUpdate(message.update)
+          break
+        case 'sync-request':
+          this._handleSyncRequest(message.stateVector)
+          break
+        case 'sync-response':
+          this._handleSyncResponse(message.update)
+          break
+        default:
+          if (DEBUG) {
+            // eslint-disable-next-line no-console
+            console.warn(`Unknown message type: ${message.type}`)
+          }
       }
     } catch (err) {
       // eslint-disable-next-line no-console
@@ -236,6 +242,7 @@ export class Libp2pProvider {
 
   /**
    * Apply an update to the document.
+   *
    * @private
    * @param {string} updateBase64 - Base64-encoded Yjs update
    */
@@ -254,6 +261,7 @@ export class Libp2pProvider {
 
   /**
    * Handle sync request from a peer.
+   *
    * @private
    * @param {string} stateVectorBase64 - Base64-encoded state vector
    */
@@ -272,6 +280,7 @@ export class Libp2pProvider {
 
   /**
    * Handle sync response from a peer.
+   *
    * @private
    * @param {string} updateBase64 - Base64-encoded Yjs update
    */
@@ -281,6 +290,7 @@ export class Libp2pProvider {
 
   /**
    * Publish a message to the pubsub topic.
+   *
    * @private
    * @param {object} message - Message object to publish
    * @param {string} message.type - Message type (update, sync-request, sync-response)
@@ -306,6 +316,7 @@ export class Libp2pProvider {
 
   /**
    * Destroy the provider and clean up resources.
+   *
    * @returns {Promise}
    */
   async destroy () {