Documentation
¶
Overview ¶
Package sync implements the y-protocols/sync wire format and the Hocuspocus outer message envelope. It owns the message-type constants, encode/decode primitives, and per-connection state machine that wraps an *encoding.Pending-backed Doc plus an *awareness.Awareness.
Two layers as described in docs/yrs-port-notes/protocol-sync.md:
Inner sync subprotocol: SyncStep1 / SyncStep2 / Update. The handshake every Yjs client-server pair performs on connect, plus the steady-state Update broadcast.
Outer envelope: a varuint message-type tag that multiplexes Sync alongside Awareness (and, for full Hocuspocus support, Auth / QueryAwareness / Stateless / SyncStatus / Ping / Pong). The bare y-websocket subset uses only tags 0 and 1; ygo v0.1 ships that subset plus tag 3 (QueryAwareness, necessary for Hocuspocus client compatibility per gotcha 1 in the port note).
The package does not own the WebSocket transport — that lives in `server/`. Splitting transport from protocol keeps the framing re-usable for testing, alternative transports (a TCP/WS hybrid for benchmarks, a unix-socket variant for in-process tests), and future Hocuspocus extensions.
Index ¶
- Constants
- Variables
- func EncodeAuthAuthenticated() []byte
- func EncodeAuthPermissionDenied(reason string) []byte
- func EncodeAuthToken(token string) []byte
- func EncodeAwareness(awarenessUpdate []byte) []byte
- func EncodeBroadcastStateless(payload string) []byte
- func EncodeClose(reason string) []byte
- func EncodePong() []byte
- func EncodeQueryAwareness() []byte
- func EncodeStateless(payload string) []byte
- func EncodeSyncStatus(synced bool) []byte
- func EncodeSyncStep1(sv []byte) []byte
- func EncodeSyncStep2(update []byte) []byte
- func EncodeSyncUpdate(update []byte) []byte
- type AuthHandler
- type AuthSubType
- type Broadcaster
- type Conn
- type Frame
- type MessageType
- type Sender
- type StatelessHandler
- type SyncSubType
Constants ¶
const ( // reserved code Hocuspocus uses for failed authentication. CloseStatusUnauthorized = 4401 )
CloseStatus codes the server may include in a MessageClose envelope. These map onto WebSocket close codes per the Hocuspocus convention.
Variables ¶
var ErrSendFailed = errors.New("sync: send failed")
ErrSendFailed is returned by transport implementations of Sender when a write to the underlying socket fails. The handler does not wrap this; it propagates so the read loop can tear down the connection cleanly.
var ErrTruncated = errors.New("sync: envelope truncated")
ErrTruncated wraps a decode failure where the envelope ended before all expected fields were read.
var ErrUnknownSyncSubType = errors.New("sync: unknown sync sub-type")
ErrUnknownSyncSubType is returned when a MessageSync envelope carries a sub-type outside the known {0, 1, 2} set. The receiver has no way to interpret an unknown sub-message; the caller should surface the error and close the connection per y-protocols convention.
Functions ¶
func EncodeAuthAuthenticated ¶
func EncodeAuthAuthenticated() []byte
EncodeAuthAuthenticated builds the server-side "your token was accepted" ack. Hocuspocus clients use this to flip an internal "authenticated" flag before proceeding with Sync.
func EncodeAuthPermissionDenied ¶
EncodeAuthPermissionDenied builds the server-side "your token was rejected" response with a human-readable reason. After sending, the server typically follows with an EncodeClose envelope and closes the WS with code 4401 (CloseStatusUnauthorized).
func EncodeAuthToken ¶
EncodeAuthToken builds a MessageAuth envelope carrying the client-side Token handshake — sent by a Hocuspocus client at connection time, consumed by the server's OnAuthenticate hook.
Wire layout: varuint(MessageAuth) varuint(AuthToken) varstring(token).
func EncodeAwareness ¶
EncodeAwareness builds a MessageAwareness envelope carrying the given awareness update bytes (as produced by internal/awareness.Awareness.Encode).
Wire layout: varuint(1) • varbuffer(awarenessUpdate)
func EncodeBroadcastStateless ¶
EncodeBroadcastStateless builds a MessageBroadcastStateless envelope. Same payload as EncodeStateless but the recipient fans out to every connection on the doc.
func EncodeClose ¶
EncodeClose builds a MessageClose envelope with a reason string. Typically followed by a WS-level close with a numeric status code (e.g. 4401 for unauthorized).
func EncodePong ¶
func EncodePong() []byte
EncodePong builds a MessagePong envelope — the application-layer ping reply (Hocuspocus only; y-websocket uses WS-level ping).
func EncodeQueryAwareness ¶
func EncodeQueryAwareness() []byte
EncodeQueryAwareness builds an empty-payload QueryAwareness envelope — the client's request for the current full awareness snapshot. Server replies with an EncodeAwareness frame covering every known client.
func EncodeStateless ¶
EncodeStateless builds a MessageStateless envelope carrying an opaque string payload. Routed to the recipient's OnStateless callback (single-conn delivery).
func EncodeSyncStatus ¶
EncodeSyncStatus builds a MessageSyncStatus envelope. The payload is a single byte: 0x00 = not synced, 0x01 = synced. Servers emit 0x01 after the initial SyncStep1/SyncStep2 round completes so clients can flip a "ready" UI state.
func EncodeSyncStep1 ¶
EncodeSyncStep1 builds a MessageSync envelope carrying SyncStep1. sv is the V1-encoded state vector bytes (as produced by encoding.EncodeStateVector). The envelope is self-delimited via the inner varbuffer and ready to be written as a single WS frame.
Wire layout (port-note §1):
varuint(0) = MessageSync varuint(0) = SyncStep1 varbuffer(sv)
func EncodeSyncStep2 ¶
EncodeSyncStep2 builds a MessageSync envelope carrying SyncStep2. update is the V1 update bytes (as produced by encoding.EncodeDiff or encoding.EncodeStateAsUpdate). An empty update is legal and signals "I have nothing the sender is missing" — see port-note gotcha 2.
func EncodeSyncUpdate ¶
EncodeSyncUpdate builds a MessageSync envelope carrying an unsolicited Update — the steady-state broadcast.
Types ¶
type AuthHandler ¶
AuthHandler is the optional server-side callback invoked when a Conn receives a MessageAuth token from the client. Returns nil to accept (server replies with EncodeAuthAuthenticated; sync proceeds normally), or an error to deny (server replies with EncodeAuthPermissionDenied(error.Error()), then EncodeClose, and the transport tears down the connection with WS close code 4401). The DocName is the docName the WS was bound to at upgrade time; AuthHandler may use it for per-document authorization.
type AuthSubType ¶
type AuthSubType uint8
AuthSubType discriminates the inner Auth-message variant inside a MessageAuth envelope. Values match @hocuspocus/server MessageReceiver.ts MessageAuth enum.
Wire shape:
varuint(MessageAuth) outer tag = 2 varuint(AuthSubType) 0=PermissionDenied, 1=Authenticated, 2=Token varstring(payload) reason | empty | token (per sub-type)
const ( // AuthPermissionDenied is the server's "your token was // rejected" response. Payload is a varstring with a human- // readable reason. AuthPermissionDenied AuthSubType = 0 // AuthAuthenticated is the server's "your token was accepted" // ack. Payload is an empty varstring. AuthAuthenticated AuthSubType = 1 // AuthToken is the client's "here is my token" handshake. // Payload is a varstring with the opaque token. The server's // OnAuthenticate callback consumes this and returns nil // (accept) or an error (deny — server replies with // AuthPermissionDenied + MessageClose). AuthToken AuthSubType = 2 )
type Broadcaster ¶
type Broadcaster func(envelope []byte)
Broadcaster fans one outer-envelope frame to every connection on the same doc, including the originator. Per port-note gotcha 6, V1 updates are idempotent so skip-self is unnecessary at the network layer. The transport layer implements this against a per-doc connection set.
type Conn ¶
type Conn struct {
// Doc is the document this connection edits. All Sync messages
// route here. Multiple Conns sharing the same Doc form one
// collaborative session.
Doc *doc.Doc
// Awareness is the presence layer multiplexed on the same WS.
// Multiple Conns share one *Awareness per doc.
Awareness *awareness.Awareness
// ID identifies this connection in diagnostics and as the
// origin tag passed to awareness.Apply. The transport layer
// generates this (typically a random string or remote addr).
ID string
// DocName is the docName this connection was bound to at
// upgrade time. Forwarded to AuthHandler / StatelessHandler.
DocName string
// Send writes one envelope to this connection only.
Send Sender
// Broadcast writes one envelope to every connection on this
// doc, including self.
Broadcast Broadcaster
// OnAuthenticate, OnStateless are optional Hocuspocus-extension
// hooks. nil = the corresponding message types are silently
// dropped (matches the bare y-websocket subset).
OnAuthenticate AuthHandler
OnStateless StatelessHandler
// AuthFailed reports whether this conn was rejected by the
// AuthHandler. The transport layer should check after each
// HandleFrame call and tear down the WS with code 4401 when
// true. The conn does not close itself — that's the transport's
// job (the WS-close call needs access to the websocket.Conn).
AuthFailed bool
// contains filtered or unexported fields
}
Conn is the per-connection sync state machine. It owns no transport — Send and Broadcast are caller-supplied. The package tests exercise it with in-memory channels; the server/ package wires it to coder/websocket.
Conn is safe for sequential use from a single read goroutine. HandleFrame must not be called concurrently with itself on the same Conn; the transport's read loop serializes naturally. Send and Broadcast may be called from other goroutines (e.g. an awareness OnChange callback running on a different conn's read goroutine).
func New ¶
New returns a Conn ready to handle frames. The transport must set Send and Broadcast before any HandleFrame / SendInitialSync call; New does not check because the transport may wire those after fully constructing the Conn (typical pattern: build Conn, register in the broadcaster registry, then start the read loop).
func (*Conn) ControlledClients ¶
ControlledClients returns a snapshot of the awareness clientIDs this connection has authoritatively touched. The transport calls this on disconnect to tombstone them on behalf of departing clients.
Returned slice is sorted ascending for deterministic disconnect behaviour in tests.
func (*Conn) Disconnect ¶
Disconnect tombstones every controlled awareness clientID. The transport calls this when the WS closes. After Disconnect the Conn's Send / Broadcast functions MUST NOT be called by the caller — the underlying WS is dead.
Returns the IDs that were tombstoned, for diagnostic logging.
func (*Conn) HandleFrame ¶
HandleFrame routes one decoded envelope through the state machine per docs/yrs-port-notes/protocol-sync.md. Returns an error only for genuine protocol violations (malformed payload bytes); unknown message types are silently ignored to preserve forward compatibility with Hocuspocus extensions.
func (*Conn) SendInitialSync ¶
SendInitialSync emits the server-side opening of the sync handshake: a SyncStep1 carrying the current local state vector, followed by an Awareness frame carrying the current full awareness snapshot (the implicit response to a future QueryAwareness).
Per port-note state machine §1.5, the server opens by sending SyncStep1 immediately on connect. Clients open with their own SyncStep1 in parallel; the two cross on the wire and both peers reply with SyncStep2.
The awareness send is technically optional under the bare y-websocket protocol (clients learn about peers via the broadcast stream as they update). We send it eagerly so a fresh client gets a complete snapshot without waiting for the next change.
type Frame ¶
type Frame struct {
Type MessageType
// SyncSub is set only when Type == MessageSync or MessageSyncReply.
SyncSub SyncSubType
// AuthSub is set only when Type == MessageAuth.
AuthSub AuthSubType
// Payload is the post-discriminator bytes:
// MessageSync → the inner update or state vector bytes
// (already unwrapped from varbuffer)
// MessageAwareness → awareness update bytes
// (already unwrapped from varbuffer)
// MessageQueryAwareness→ nil (no payload)
// MessageAuth → token (AuthToken sub) or reason
// (AuthPermissionDenied) — string bytes
// MessageStateless → the stateless payload string bytes
// MessageBroadcastStateless → same
// MessageClose → close reason string bytes
// MessageSyncStatus → single byte 0x00 or 0x01
// MessagePing/Pong → nil
// other → the raw remaining envelope bytes,
// left opaque for caller-defined handling
Payload []byte
}
Frame is the decoded outer-envelope message. Type indicates which MessageType discriminator the wire bytes started with; the remaining fields populate based on Type per docs/yrs-port-notes/protocol-sync.md §2.
The decode helpers return a *Frame so callers can switch on Type without reaching back into raw bytes. Mutating fields on a Frame has no effect on the original wire bytes — the decoder copies.
func DecodeEnvelope ¶
DecodeEnvelope parses one envelope from b. Returns the decoded Frame plus the unconsumed tail (in case multiple envelopes are concatenated; over WebSocket each Read returns exactly one envelope and the tail is empty).
Returns ErrTruncated when bytes run out mid-field, or ErrUnknownSyncSubType when a Sync envelope's inner discriminator is outside {0, 1, 2}.
type MessageType ¶
type MessageType uint8
MessageType is the outer-envelope varuint discriminator. Values match @hocuspocus/server `MessageType` enum from packages/server/src/types.ts:56-67.
ygo v0.1 implements MessageSync, MessageAwareness, and MessageQueryAwareness. The remaining tags are decoded but dispatched to the OnUnknownMessage hook so adopters can add custom handling without recompiling.
const ( // MessageSync wraps a y-protocols/sync sub-message (SyncStep1 / // SyncStep2 / SyncUpdate). Payload layout: // varuint(SyncSubType) • varbuffer(payload) MessageSync MessageType = 0 // MessageAwareness wraps an awareness update blob — see // internal/awareness for the inner wire format. Payload layout: // varbuffer(awarenessUpdateBytes) MessageAwareness MessageType = 1 // MessageAuth carries the Hocuspocus auth-token handshake. Not // implemented in v0.1; bare y-websocket does not support it. // Deferred per docs/tech-debt.md. MessageAuth MessageType = 2 // MessageQueryAwareness is sent by a client (typically right // after connect) to request the full current awareness snapshot. // Payload is empty. Server replies with a MessageAwareness frame // containing every known client's state. Mandatory for // Hocuspocus-client interop even when nothing else from // Hocuspocus is implemented (port-note gotcha 1). MessageQueryAwareness MessageType = 3 // MessageSyncReply mirrors MessageSync but originates from the // server side (used by Hocuspocus internals). Wire layout is // identical. We decode it but treat it as MessageSync. MessageSyncReply MessageType = 4 // MessageStateless / MessageBroadcastStateless / MessageClose / // MessageSyncStatus / MessagePing / MessagePong are Hocuspocus // extensions. Decoded into typed frames; not actively handled // in v0.1. MessageStateless MessageType = 5 MessageBroadcastStateless MessageType = 6 MessageClose MessageType = 7 MessageSyncStatus MessageType = 8 MessagePing MessageType = 9 MessagePong MessageType = 10 )
type Sender ¶
Sender writes one outer-envelope frame back to a single connection. The transport layer implements this — for WebSocket it serializes via the conn's write mutex; for in-memory tests it appends to a slice.
type StatelessHandler ¶
type StatelessHandler func(docName, payload string)
StatelessHandler is the optional callback invoked when a Conn receives a MessageStateless or MessageBroadcastStateless envelope. The broadcast variant additionally triggers a broadcast of the same envelope to other conns on the doc.
The handler runs synchronously on the read loop's goroutine — long-running work should be dispatched elsewhere.
type SyncSubType ¶
type SyncSubType uint8
SyncSubType is the inner-message varuint discriminator inside a MessageSync frame. Values match y-protocols/sync.js:37-39.
const ( // SyncStep1 carries a state vector. The receiver replies with // SyncStep2 containing the diff the sender is missing. SyncStep1 SyncSubType = 0 // SyncStep2 carries a V1 update — the response to a SyncStep1. // May carry an empty update (well-formed bytes encoding zero // blocks and zero delete-set ranges) when the receiver has // nothing the sender is missing. SyncStep2 SyncSubType = 1 // SyncUpdate carries an unsolicited V1 update — a steady-state // edit being broadcast to peers. Wire format is identical to // SyncStep2; the only semantic difference is that SyncUpdate // is not expected to be a reply to a SyncStep1. SyncUpdate SyncSubType = 2 )