npm.io
3.1.0 • Published 3 weeks ago

y-websocket

Licence
MIT
Version
3.1.0
Deps
2
Size
92 kB
Vulns
0
Weekly
0
Stars
710

y-websocket

WebSocket Provider for Yjs

The Websocket Provider implements a classical client server model. Clients connect to a single endpoint over Websocket. The server distributes awareness information and document updates among clients.

This repository contains a simple in-memory backend that can persist to databases, but it can't be scaled easily. The y-redis repository contains an alternative backend that is scalable, provides auth*, and can persist to different backends.

The Websocket Provider is a solid choice if you want a central source that handles authentication and authorization. Websockets also send header information and cookies, so you can use existing authentication mechanisms with this server.

  • Supports cross-tab communication. When you open the same document in the same browser, changes on the document are exchanged via cross-tab communication (Broadcast Channel and localStorage as fallback).
  • Supports exchange of awareness information (e.g. cursors).

Quick Start

Install dependencies

npm i y-websocket
Start a y-websocket server

There are multiple y-websocket compatible backends for y-websocket:

  • y-sweet
  • y-redis
  • ypy-websocket
  • pycrdt-websocket
  • yrs-warp
  • ...

The fastest way to get started is to run the @y/websocket-server backend. This package was previously included in y-websocket and now lives in a forkable repository.

Install and start y-websocket-server:

npm install @y/websocket-server
HOST=localhost PORT=1234 npx y-websocket
Client Code:
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'

const doc = new Y.Doc()
const wsProvider = new WebsocketProvider('ws://localhost:1234', 'my-roomname', doc)

wsProvider.on('status', event => {
  console.log(event.status) // logs "connected" or "disconnected"
})
Client Code in Node.js

The WebSocket provider requires a WebSocket object to create connection to a server. You can polyfill WebSocket support in Node.js using the ws package.

const wsProvider = new WebsocketProvider('ws://localhost:1234', 'my-roomname', doc, { WebSocketPolyfill: require('ws') })

API

import { WebsocketProvider } from 'y-websocket'
wsProvider = new WebsocketProvider(serverUrl: string, room: string, ydoc: Y.Doc [, wsOpts: WsOpts]) Create a new websocket-provider instance. As long as this provider, or the connected ydoc, is not destroyed, the changes will be synced to other clients via the connected server. Optionally, you may specify a configuration object. The following default values of wsOpts can be overwritten.
wsOpts = {
  // Set this to `false` if you want to connect manually using wsProvider.connect()
  connect: true,
  // Specify a query-string / url parameters that will be url-encoded and attached to the `serverUrl`
  // I.e. params = { auth: "bearer" } will be transformed to "?auth=bearer"
  params: {}, // Object<string,string>
  // You may polyill the Websocket object (https://developer.mozilla.org/en-US/docs/Web/API/WebSocket).
  // E.g. In nodejs, you could specify WebsocketPolyfill = require('ws')
  WebsocketPolyfill: Websocket,
  // Specify an existing Awareness instance - see https://github.com/yjs/y-protocols
  awareness: new awarenessProtocol.Awareness(ydoc),
  // Specify the maximum amount to wait between reconnects (we use exponential backoff).
  maxBackoffTime: 2500,
  // Decide whether to reconnect after the *server* closed the connection. By default, close
  // codes in the 4400-4499 range are permanent: the provider stops reconnecting and fires the
  // `closed` event. See "Close codes & reconnecting" below.
  // This is never called when you close the connection yourself (e.g. wsProvider.disconnect()).
  shouldReconnect: (event, provider) => !(event.code >= 4400 && event.code < 4500)
}
wsProvider.wsconnected: boolean True if this instance is currently connected to the server. wsProvider.wsconnecting: boolean True if this instance is currently connecting to the server. wsProvider.shouldConnect: boolean If false, the client will not try to reconnect. wsProvider.bcconnected: boolean True if this instance is currently communicating to other browser-windows via BroadcastChannel. wsProvider.synced: boolean True if this instance is currently connected and synced with the server. wsProvider.params : boolean The specified url parameters. This can be safely updated, the new values will be used when a new connction is established. If this contains an auth token, it should be updated regularly. wsProvider.disconnect() Disconnect from the server and don't try to reconnect. wsProvider.connect() Establish a websocket connection to the websocket-server. Call this if you recently disconnected or if you set wsOpts.connect = false. wsProvider.destroy() Destroy this wsProvider instance. Disconnects from the server and removes all event handlers. wsProvider.on('sync', function(isSynced: boolean)) Add an event listener for the sync event that is fired when the client received content from the server. wsProvider.on('status', function({ status: 'disconnected' | 'connecting' | 'connected' })) Receive updates about the current connection status. wsProvider.on('connection-close', function(WSClosedEvent)) Fires when the underlying websocket connection is closed. It forwards the websocket event to this event handler. wsProvider.on('closed', function({ code: number, reason: string }, provider)) Fires when the server closed the connection and shouldReconnect returned false. The provider stops reconnecting (shouldConnect becomes false) but is not destroyed: cross-tab communication keeps running, and you may resume deliberately with wsProvider.connect() or clean up with wsProvider.destroy(). Unlike connection-close, which fires on every blip, this fires only when the server told you to go away - and why. wsProvider.on('connection-error', function(WSErrorEvent)) Fires when the underlying websocket connection closes with an error. It forwards the websocket event to this event handler.

Close Codes & Reconnecting

The provider reconnects automatically after every disconnect, backing off exponentially up to maxBackoffTime. But some disconnects are not worth retrying: the permission to access the document was revoked, or the document doesn't exist anymore. A server signals this with the websocket close code.

By convention the private-use range (4000-4999, reserved for applications by RFC 6455) is split so that a client can classify a close code it has never seen before:

Close code Meaning Reconnect?
4400-4499 permanent - retrying returns the same result until the app acts no
4500-4599 transient - the matching "try again later" range yes
everything else transient - 1006 abnormal closure, 1011 internal error, 1013 try again later, ... yes

The band is normative; the trailing digits are only an HTTP mnemonic. A retryable rate-limit close is 45xx, never 4429.

shouldReconnect implements exactly this rule by default. Override it to opt out entirely, or to classify codes your backend uses differently:

const wsProvider = new WebsocketProvider('ws://localhost:1234', 'my-roomname', doc, {
  // never give up
  shouldReconnect: () => true
})

wsProvider.on('closed', ({ code, reason }) => {
  console.log(`the server closed us for good: ${code} ${reason}`)
  // the provider is idle, not destroyed - resume deliberately once the cause is fixed
  // await refreshToken()
  // wsProvider.connect()
})

Breaking change: previous versions reconnected after every close, regardless of the close code. Pass shouldReconnect: () => true to restore that behavior.

Signalling a permanent error from the server

Rejecting the HTTP upgrade (401, 403, ...) does not work: browsers deliberately hide the upgrade status from the WebSocket API, so the client only sees an opaque 1006 with no code and no reason - and keeps retrying. To tell the client why, accept the upgrade and then close the socket:

ws.close(4401, 'permission revoked')

@y/hub documents a worked example of this scheme.

License

The MIT License Kevin Jahns

Keywords