Yjs Server
An extensible, y-websocket-compatible server. Written in
TypeScript. Supports authentication. ESM-only.
Quickstart
Install it:
npm i yjs-server
A public server:
import { WebSocketServer } from 'ws'
import { createYjsServer } from 'yjs-server'
const wss = new WebSocketServer({ port: 8080 })
const yjss = createYjsServer({
createDoc: () => new Y.Doc(),
})
wss.on('connection', (socket, request) => {
yjss.handleConnection(socket, request)
})
Run it with node server.js
.
A server with authentication:
import { WebSocketServer, WebSocket } from 'ws'
import { createYjsServer, defaultDocNameFromRequest } from 'yjs-server'
const wss = new WebSocketServer({ port: 8080 })
const yjss = createYjsServer({
createDoc: () => new Y.Doc(),
})
wss.on('connection', (socket, request) => {
const whenAuthorized = authorize(socket, request).catch(() => {
conn.close(4001)
return false
})
yjss.handleConnection(socket, request, whenAuthorized)
})
async function authorize(socket: WebSocket, request: http.IncomingMessage) {
const docName = defaultDocNameFromRequest(req)
if (!docName) throw new Error('invalid doc name')
const auth = new URL(req.url!, 'http://localhost').searchParams.get('authQueryParam')
return true
}
On the client:
import { WebsocketProvider } from 'y-websocket'
const wsProvider = new WebsocketProvider('ws://localhost:8080', 'roomName', yjsDoc, {
params: { authQueryParam: 'authToken...' },
})
wsProvider.on('connection-close', (event: CloseEvent) => {
if (event.code === 4001) {
logger.error({ event }, 'received unauthorized error from server')
wsProvider.shouldConnect = false
}
})
The server will buffer all messages until the whenAuthorized
promise resolves. Only if the promise
resolves with true
, the connection will be considered authenticated. See
the should-connect.test.ts for more supported scenarios.
Motivation
Yjs is a great library, but the server included in y-websocket is limited in its capabilities: it is
difficult to extend from the outside, tests are missing, authentication is not easy to implement,
the server can't be imported as a module in an existing server, and there are not many security
checks in place (try sending a string message instead of a bytearray in an open websocket client,
the server will infinitely loop)
This library aims to solve these problems.
Usage Examples
With an external HTTP server
const httpServer = http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' })
response.end()
})
const wss = new WebSocketServer({ noServer: true })
const yjsServer = createYjsServer({
createDoc: () => new Y.Doc(),
})
wss.on('connection', yjsServer.handleConnection)
httpServer.on('upgrade', (request, socket, head) => {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request)
})
})
const port = process.env['PORT'] ?? 8080
httpServer.listen(port, () => {
console.info(`listening on port ${port}`)
})
With persistent storage
const client = new SomeExternalDbClient()
const wss = new WebSocketServer({ port: 8080 })
const yjss = createYjsServer({
createDoc: () => new Y.Doc(),
docStorage: {
loadDoc: async (docName, doc) => {
const persistedDocBytes = await client.getDoc(docName)
if (persistedDocBytes) Y.applyUpdate(doc, persistedDocBytes)
},
storeDoc: async (docName, doc) => {
await client.setDoc(docName, Y.encodeStateAsUpdate(doc))
},
},
})
wss.on('connection', (socket, request) => {
yjss.handleConnection(socket, request)
})
API
createYjsServer(options: CreateYjsServerOptions) => YjsServer
The server acts as a container for document state and handles multiple WebSocket connections per
document. It does not bind to any port or expose any functionality over HTTP. You must use an
external WebSocket server such as ws to handle the WebSocket
connections.
You can create many servers in the same process.
type CreateYjsServerOptions = {
createDoc: () => Y.Doc
logger?: Logger
docNameFromRequest?: (request: IRequest) => string | undefined
docStorage?: DocStorage
pingTimeoutMs?: number
maxBufferedBytes?: number
maxBufferedBytesBeforeConnect?: number
}
type YjsServer = {
handleConnection(conn: IWebSocket, req: IRequest, shouldConnect?: Promise<boolean>): void
close(code?: number, terminateTimeout?: number | null): void
}
DocStorage
The docStorage
option allows you to load and save documents from a database.
There are generally two ways to implement this interface:
- Load the document from the database when the first connection is established, and save the
document when the last connection is closed. This is the most straightforward approach, but it
has the downside that the document will be lost if the server crashes before the last connection
is closed. In practice, if clients use y-indexeddb, the
downside is mitigated because the document is stored locally in the browser. The document will
sync to the server when the connection is re-established.
- Load the document from the database when the first connection is established, and save the
document every time a change is made. This is more complex, but it has the advantage that the
document will not be lost if the server crashes.
For option 1, you can implement the loadDoc
and saveDoc
functions. For option 2, you can
implement the loadDoc
and onUpdate
functions.
type DocStorage = {
loadDoc: LoadDocFn
storeDoc?: StoreDocFn
onUpdate?: OnUpdateFn
}
type LoadDocFn = (name: string, doc: Y.Doc) => Promise<void>
type StoreDocFn = (name: string, doc: Y.Doc) => Promise<void>
type OnUpdateFn = (name: string, update: Uint8Array, doc: Y.Doc) => Promise<void>
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
Future plans
- Support horizontal scaling. Right now, the server is not horizontally scalable. It is
possible to run multiple server instances (even on the same node instance), but they will not
share the same state. I recommend deploying many instances of the server for different document
types. In the future, we could support horizontal scaling using Redis or direct server-to-server
communication with the Yjs protocol.
- Multi-document support per connection. This is probably needed to support server-to-server
communications.
License
MIT
Some code was directly copied from y-websocket.