How does y-s2 work with Yjs and WebSockets?

HEAD
60f695aci: upgrade and SHA-pin GitHub Actions (#13)

tell me how this works. yjs expect a websocket, no?

Avatar of openai/gpt-5.6-sol
GPT 5.6 Sol
commented

Yjs itself does not require WebSockets. It only defines the CRDT document and binary update format. A provider transports those updates—y-websocket, WebRTC, HTTP, BroadcastChannel, or something custom.

This project is a WebSocket provider/backend compatible with the Yjs sync protocol, but it uses S2 as the durable distribution layer.

###Data flow

Y.Doc
  ↕ y-websocket protocol
Cloudflare Worker WebSocket
  ↕ append/read
S2 room stream
  ↕ periodic compaction
R2 snapshot

For each WebSocket connection, the Worker:

  1. Accepts an upgrade under /ws/ (src/index.ts:73).
  2. Reads room and yauth query parameters (src/index.ts:92-98).
  3. Maps the room to an S2 stream:
    rooms/<encoded-room>/index
    
  4. Loads the latest compacted Yjs snapshot from R2.
  5. Replays newer S2 records into a temporary Y.Doc.
  6. Sends Yjs sync step 1 and step 2 messages to the client.
  7. Appends updates received from that client to S2.
  8. Continuously reads the S2 stream and sends records to the client.

Consequently, Worker instances do not need to share an in-memory room. S2 acts as the ordered event log and fan-out channel. If Alice and Bob connect through different Worker instances, Alice’s update is appended to S2, both Workers read it, and Bob receives it over his WebSocket.

The protocol message encoders are in src/protocol.ts. They use the same basic message IDs as y-websocket:

  • 0: sync
  • 1: awareness
  • sync subtype 0: step 1
  • sync subtype 1: step 2
  • sync subtype 2: update

Snapshots are Yjs state updates stored at:

snapshots/<encoded-room>/latest.bin

in R2 (src/snapshot.ts). Fencing records ensure only one Worker compacts a room at a time, then trim records mark the S2 history covered by the snapshot.

##Important README mismatch

The current README client example does not match the current server parameters. It uses authToken, while the Worker expects yauth; the Worker also requires room as a query parameter.

A matching y-websocket setup would be:

import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'

const room = 'room-name'
const doc = new Y.Doc()

const provider = new WebsocketProvider(
  'wss://your-worker.your-subdomain.workers.dev/ws',
  room,
  doc,
  {
    params: {
      room,
      yauth: 'your-auth-token',
    },
  },
)

This should produce approximately:

wss://your-worker.../ws/room-name?room=room-name&yauth=your-auth-token

The path satisfies /ws/, while the query parameters satisfy the actual handler.

Also note that authentication is currently only a placeholder: /auth/token returns a fixed demo token, and the WebSocket handler merely checks that yauth is nonempty—it does not validate the token (src/index.ts:683-701 and 101-105).


END OF POST

How does y-s2 work with Yjs and WebSockets? — s2-streamstore/y-s2