server

package
v1.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package server implements the y-websocket / Hocuspocus-compatible WebSocket sync server for ygo documents.

The server exposes an http.Handler that adopters mount on their own http.ServeMux at any path prefix. Per port-note §"Go translation choices" — every adopter already has an HTTP server, so we layer on top rather than impose our own runtime. A 30-line cmd/ygo-server/main.go binary wraps this for stand-alone use.

Wire format compatibility: the bare y-websocket subset of the Hocuspocus envelope (tags 0=Sync, 1=Awareness, 3=QueryAwareness). Auth (tag 2), Stateless (5/6), Close (7), SyncStatus (8) are silently ignored — see docs/tech-debt.md. The Sync subset is sufficient for full interop with y-websocket clients and the Sync+Awareness subset of Hocuspocus clients.

Per-document state lives in a map keyed by docName (the last path segment of the WS URL). Documents are loaded lazily on the first connection and evicted after the last connection closes; if a persist.Store is configured, every applied update is persisted and a final snapshot is written at eviction time.

Index

Constants

This section is empty.

Variables

View Source
var ErrServerClosed = errors.New("server: closed")

ErrServerClosed is returned from operations on a Server that has been Closed. Reserved for future use; currently no method returns it.

Functions

This section is empty.

Types

type Options

type Options struct {
	// Store optionally persists every applied update keyed by
	// docName. When set, new documents load their history on first
	// connect (drain through the pending buffer if necessary).
	// When nil, documents are in-memory only and lost on the last
	// disconnect.
	Store persist.Store

	// DocNameFn extracts the docName from the WS upgrade request.
	// Defaults to last-path-segment, mirroring y-websocket's
	// req.url.slice(1).split('?')[0] rule (port-note §3). Override
	// when mounting on a complex URL scheme.
	DocNameFn func(r *http.Request) string

	// OriginPatterns lists the allowed Origin headers for CORS-
	// style WS upgrade rejection. Defaults to an empty list which
	// rejects all browser cross-origin connections; pass "*" to
	// allow any origin (development only — relaxes browser
	// same-origin protection). Forwarded verbatim to coder/websocket
	// AcceptOptions.
	OriginPatterns []string

	// OnAuthenticate is the Hocuspocus auth callback. When set,
	// the server expects every client to send a MessageAuth(Token)
	// envelope shortly after connecting; the callback receives the
	// docName + token and returns nil to accept or error to deny.
	// On denial the server emits AuthPermissionDenied + Close and
	// closes the WS with code 4401 (CloseStatusUnauthorized).
	//
	// When nil (the bare y-websocket default), MessageAuth tokens
	// are accepted silently — the server responds with
	// AuthAuthenticated so Hocuspocus clients flip their internal
	// "authenticated" flag and proceed.
	OnAuthenticate syncpkg.AuthHandler

	// OnStateless is the Hocuspocus stateless-channel callback.
	// Receives docName + payload string for both MessageStateless
	// and MessageBroadcastStateless envelopes. Long-running work
	// should be dispatched off-thread — this runs on the conn's
	// read goroutine.
	//
	// MessageBroadcastStateless also fans out to other conns on
	// the doc regardless of whether the callback is set.
	OnStateless syncpkg.StatelessHandler

	// OnConnect, when set, is called once a connection has been admitted
	// to its room (past the per-doc and global caps) but before the
	// initial sync is sent. It receives a stable per-connection id
	// (unique for the server's lifetime), the docName, and the original
	// upgrade *http.Request, so an adopter can gate on headers, cookies,
	// TLS state, or remote address — request context that the token-only
	// OnAuthenticate does not see. Returning a non-nil error rejects the
	// connection: it is torn down and the WS is closed with
	// StatusPolicyViolation, and OnDisconnect does NOT fire for it. A
	// panic inside OnConnect is also torn down cleanly (the room slot is
	// released, no leak) and likewise does not fire OnDisconnect. This
	// runs on the connection's serve goroutine (so it runs concurrently
	// across connections); keep it fast.
	OnConnect func(connID, docName string, r *http.Request) error

	// OnDisconnect, when set, is called when a connection that OnConnect
	// accepted (or any admitted connection, if OnConnect is nil) is
	// released, with the same connID and docName passed to OnConnect.
	// It pairs one-to-one with a successful admission, so an adopter can
	// keep a balanced per-connection ledger. It runs on the connection's
	// serve goroutine after the room teardown; keep it fast.
	OnDisconnect func(connID, docName string)

	// ReadOnly, when set and it returns true for a connection, marks
	// that connection read-only: the server drops any document update it
	// sends (the update reaches neither the document nor other peers),
	// while still delivering updates to it and accepting its awareness
	// (presence). Decide from the request the way OnConnect gates
	// admission (a JWT claim, header, or path). A read-only client
	// should also disable editing in its UI; the server enforcement is
	// the backstop. Nil means every connection is read-write.
	ReadOnly func(docName string, r *http.Request) bool

	// OnChange, when set, is called after a document update is applied
	// and broadcast, with the docName and the V1 update bytes that were
	// applied. Use it for side effects on edits (search indexing,
	// webhooks, audit, external persistence). It fires for every accepted
	// document mutation whether or not a Store is configured, and NOT for
	// an update a read-only connection sent (that is dropped before
	// broadcast) nor for awareness or handshake frames. It runs
	// synchronously on the originating connection's read goroutine, so
	// keep it fast and dispatch long-running work elsewhere; a panic
	// tears down that connection. When a Store is also configured,
	// OnChange runs after the StoreUpdate attempt and fires even if that
	// StoreUpdate failed (the document already changed in memory and on
	// peers; the failure is logged), so do not treat an OnChange call as
	// confirmation of durable persistence. Treat the update slice as
	// valid only for the duration of the call; copy it if you retain it.
	OnChange func(docName string, update []byte)

	// OnLoadDocument, when set, is called the first time a document is
	// loaded into memory (a cache miss), after any Store history has been
	// applied, with the docName. It lets an adopter seed or load initial
	// content from a source other than the Store. A returned V1 update is
	// applied to the fresh document; a returned error aborts the load and
	// the connection is closed with StatusInternalError, leaving no
	// document registered.
	//
	// The returned update is applied in memory and is not itself
	// persisted, so it re-applies on every first load (including after an
	// eviction). For that to be safe the seed MUST be deterministic with
	// STABLE item IDs across calls — build it from a fixed reserved
	// ClientID with fixed clocks, or return the same bytes each time. A
	// seed built from a fresh/random ClientID each call (the default of
	// doc.NewDoc) will, together with a Store, both duplicate its content
	// on every reload and strand persisted client edits that referenced
	// the seed (their origin never reappears). Prefer the Store for
	// durable content; use this hook for stable, regenerable seeds.
	//
	// It runs synchronously under the internal document-registry lock, so
	// it must be fast and MUST NOT call back into the Server (Stats,
	// Close, or opening another connection) or it will deadlock. While it
	// runs, all connection admission and document eviction across the
	// server are stalled, not just this document's creation.
	OnLoadDocument func(docName string) ([]byte, error)

	// VersionInterval enables periodic auto-versioning: every
	// interval, each document that received updates since the last
	// sweep is captured as a named version ("auto") via
	// persist.SaveVersion. Requires Store to also implement
	// persist.VersionStore (the sqlite backend does); ignored with a
	// logged notice otherwise. Zero disables auto-versioning.
	VersionInterval time.Duration

	// KeepVersions prunes each auto-versioned document to its newest
	// N versions after every capture. Zero keeps every version
	// (history grows unbounded; prune externally).
	KeepVersions int

	// AwarenessTimeout bounds how long a silent presence entry
	// survives before the server sweeps it: any non-local awareness
	// client whose last update is older than this is marked offline
	// and the removal is broadcast to the room. The sweep also
	// garbage-collects tombstones older than twice this value,
	// reclaiming memory from churned clients. Zero selects the
	// y-protocols default (30s). Healthy clients heartbeat well
	// inside the window (y-websocket every 15s), so the sweep only
	// evicts the wedged.
	AwarenessTimeout time.Duration

	// MaxAwarenessClients bounds the distinct presence clientIDs a
	// single room tracks, capping the memory one peer can force by
	// flooding fabricated clientIDs. Zero selects a safe default
	// (4096); a negative value disables the cap. New clients beyond
	// the cap are dropped; already-tracked clients keep updating.
	MaxAwarenessClients int

	// ReadLimit sets the maximum WebSocket message size in bytes that
	// the server will read from a single client frame. Defaults to
	// coder/websocket's built-in 32 768-byte limit when zero. Raise
	// this for large collaborative documents (e.g. 128 << 20 for
	// 128 MiB). Set to -1 for unlimited.
	ReadLimit int64

	// WriteTimeout bounds a single broadcast/fan-out write to one peer.
	// Without it a slow or half-open consumer (full TCP send buffer, no
	// FIN) blocks the write forever, freezing the originating read loop
	// and the global awareness sweep for every other document. On
	// timeout the offending peer is closed and dropped. Zero selects a
	// safe default (10s).
	WriteTimeout time.Duration

	// MaxConnsPerDoc bounds the number of simultaneous WebSocket
	// connections to a single document, capping the goroutines and
	// memory one peer can pin by opening many sockets to the same room.
	// Zero selects a safe default (4096); a negative value disables the
	// cap. A connection beyond the cap is rejected at the upgrade with
	// websocket.StatusPolicyViolation; connections already in the room
	// keep working. The newest socket is the one refused, so an
	// established collaborator is never displaced by a flood.
	MaxConnsPerDoc int

	// MaxDocs bounds the number of distinct documents the server holds
	// in memory at once, capping the docStates (and Store loads) a peer
	// can force by connecting to many fabricated docNames. Unlike the
	// per-room caps above, this defaults to UNLIMITED (zero or negative):
	// a global document count scales with deployment size, so a default
	// cap would silently break a large multi-tenant server. Operators
	// fronting untrusted clients should set a positive bound. Documents
	// evict when their last connection departs, so the cap tracks
	// concurrently-active rooms, not lifetime document count. A
	// connection that would create a document past the cap is refused at
	// the upgrade with websocket.StatusTryAgainLater (1013); an already
	// resident room is always admitted.
	MaxDocs int
}

Options configures a Server. The zero value is valid: in-memory state only, no persistence, no auth, docName extracted as the last URL path segment.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the http.Handler implementation. Construct with New and mount via Handler(). Safe for concurrent use.

func New

func New(opts Options) *Server

New returns a Server with the given options. The returned Server is ready to accept WS connections; call Handler() to obtain the http.Handler that performs the upgrade.

func (*Server) Close

func (s *Server) Close(ctx context.Context) error

Close evicts every in-memory document, calling Flush on the configured Store. Pending in-flight WS reads will fail with context cancellation; callers should drain via an http.Server Shutdown rather than Close in production.

Returns the first error encountered while flushing, but continues attempting eviction past errors so partial failure leaves no leaks.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the http.Handler that performs the WebSocket upgrade and routes the resulting connection through the sync state machine. Mount it on a mux pattern such as "/" or "/collab/{docName}".

func (*Server) Stats added in v1.11.0

func (s *Server) Stats() Stats

Stats returns a point-in-time snapshot of resident documents and live connections. It is safe for concurrent use and cheap: it walks the document registry under the registry lock, taking each document's connection lock only briefly. Adopters typically poll it to feed their own metrics, health, or capacity surfaces (pair it with the OnConnect and OnDisconnect hooks for event-level accounting). Lock order docsMu -> connsMu matches the rest of the server.

type Stats added in v1.11.0

type Stats struct {
	// Documents is the number of documents resident in memory: rooms
	// with at least one live connection (a document evicts when its last
	// connection departs).
	Documents int
	// Connections is the total number of live WebSocket connections
	// across all resident documents.
	Connections int
}

Stats is a point-in-time snapshot of server load returned by Stats.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL