fix: Resolve TypeScript errors

Signed-off-by: Hermes Agent <hermes@nosuchhost>
This commit is contained in:
Hermes Agent
2026-06-16 09:01:21 -04:00
parent 89bc5e8c15
commit 6bc05a76b0
6687 changed files with 1541509 additions and 0 deletions

21
client/node_modules/y-websocket/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019 Kevin Jahns <kevin.jahns@protonmail.com>.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

168
client/node_modules/y-websocket/README.md generated vendored Normal file
View File

@@ -0,0 +1,168 @@
# y-websocket :tophat:
> 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](https://github.com/yjs/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](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
and
[localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)
as fallback).
* Supports exchange of awareness information (e.g. cursors).
## Quick Start
### Install dependencies
```sh
npm i y-websocket
```
### Start a y-websocket server
This repository implements a basic server that you can adopt to your specific use-case. [(source code)](./bin/)
Start a y-websocket server:
```sh
HOST=localhost PORT=1234 npx y-websocket
```
### Client Code:
```js
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`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object to create connection to a server. You can polyfill WebSocket support in Node.js using the [`ws` package](https://www.npmjs.com/package/ws).
```js
const wsProvider = new WebsocketProvider('ws://localhost:1234', 'my-roomname', doc, { WebSocketPolyfill: require('ws') })
```
## API
```js
import { WebsocketProvider } from 'y-websocket'
```
<dl>
<b><code>wsProvider = new WebsocketProvider(serverUrl: string, room: string, ydoc: Y.Doc [, wsOpts: WsOpts])</code></b>
<dd>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. </dd>
</dl>
```js
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
}
```
<dl>
<b><code>wsProvider.wsconnected: boolean</code></b>
<dd>True if this instance is currently connected to the server.</dd>
<b><code>wsProvider.wsconnecting: boolean</code></b>
<dd>True if this instance is currently connecting to the server.</dd>
<b><code>wsProvider.shouldConnect: boolean</code></b>
<dd>If false, the client will not try to reconnect.</dd>
<b><code>wsProvider.bcconnected: boolean</code></b>
<dd>True if this instance is currently communicating to other browser-windows via BroadcastChannel.</dd>
<b><code>wsProvider.synced: boolean</code></b>
<dd>True if this instance is currently connected and synced with the server.</dd>
<b><code>wsProvider.params : boolean</code></b>
<dd>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.</dd>
<b><code>wsProvider.disconnect()</code></b>
<dd>Disconnect from the server and don't try to reconnect.</dd>
<b><code>wsProvider.connect()</code></b>
<dd>Establish a websocket connection to the websocket-server. Call this if you recently disconnected or if you set wsOpts.connect = false.</dd>
<b><code>wsProvider.destroy()</code></b>
<dd>Destroy this wsProvider instance. Disconnects from the server and removes all event handlers.</dd>
<b><code>wsProvider.on('sync', function(isSynced: boolean))</code></b>
<dd>Add an event listener for the sync event that is fired when the client received content from the server.</dd>
<b><code>wsProvider.on('status', function({ status: 'disconnected' | 'connecting' | 'connected' }))</code></b>
<dd>Receive updates about the current connection status.</dd>
<b><code>wsProvider.on('connection-close', function(WSClosedEvent))</code></b>
<dd>Fires when the underlying websocket connection is closed. It forwards the websocket event to this event handler.</dd>
<b><code>wsProvider.on('connection-error', function(WSErrorEvent))</code></b>
<dd>Fires when the underlying websocket connection closes with an error. It forwards the websocket event to this event handler.</dd>
</dl>
## Websocket Server
Start a y-websocket server:
```sh
HOST=localhost PORT=1234 npx y-websocket
```
Since npm symlinks the `y-websocket` executable from your local `./node_modules/.bin` folder, you can simply run npx. The `PORT` environment variable already defaults to 1234, and `HOST` defaults to `localhost`.
### Websocket Server with Persistence
Persist document updates in a LevelDB database.
See [LevelDB Persistence](https://github.com/yjs/y-leveldb) for more info.
```sh
HOST=localhost PORT=1234 YPERSISTENCE=./dbDir node ./node_modules/y-websocket/bin/server.js
```
### Websocket Server with HTTP callback
Send a debounced callback to an HTTP server (`POST`) on document update. Note that this implementation doesn't implement a retry logic in case the `CALLBACK_URL` does not work.
Can take the following ENV variables:
* `CALLBACK_URL` : Callback server URL
* `CALLBACK_DEBOUNCE_WAIT` : Debounce time between callbacks (in ms). Defaults to 2000 ms
* `CALLBACK_DEBOUNCE_MAXWAIT` : Maximum time to wait before callback. Defaults to 10 seconds
* `CALLBACK_TIMEOUT` : Timeout for the HTTP call. Defaults to 5 seconds
* `CALLBACK_OBJECTS` : JSON of shared objects to get data (`'{"SHARED_OBJECT_NAME":"SHARED_OBJECT_TYPE}'`)
```sh
CALLBACK_URL=http://localhost:3000/ CALLBACK_OBJECTS='{"prosemirror":"XmlFragment"}' npm start
```
This sends a debounced callback to `localhost:3000` 2 seconds after receiving an update (default `DEBOUNCE_WAIT`) with the data of an XmlFragment named `"prosemirror"` in the body.
## License
[The MIT License](./LICENSE) © Kevin Jahns

77
client/node_modules/y-websocket/bin/callback.cjs generated vendored Normal file
View File

@@ -0,0 +1,77 @@
const http = require('http')
const number = require('lib0/number')
const CALLBACK_URL = process.env.CALLBACK_URL ? new URL(process.env.CALLBACK_URL) : null
const CALLBACK_TIMEOUT = number.parseInt(process.env.CALLBACK_TIMEOUT || '5000')
const CALLBACK_OBJECTS = process.env.CALLBACK_OBJECTS ? JSON.parse(process.env.CALLBACK_OBJECTS) : {}
exports.isCallbackSet = !!CALLBACK_URL
/**
* @param {Uint8Array} update
* @param {any} origin
* @param {import('./utils.cjs').WSSharedDoc} doc
*/
exports.callbackHandler = (update, origin, doc) => {
const room = doc.name
const dataToSend = {
room,
data: {}
}
const sharedObjectList = Object.keys(CALLBACK_OBJECTS)
sharedObjectList.forEach(sharedObjectName => {
const sharedObjectType = CALLBACK_OBJECTS[sharedObjectName]
dataToSend.data[sharedObjectName] = {
type: sharedObjectType,
content: getContent(sharedObjectName, sharedObjectType, doc).toJSON()
}
})
CALLBACK_URL && callbackRequest(CALLBACK_URL, CALLBACK_TIMEOUT, dataToSend)
}
/**
* @param {URL} url
* @param {number} timeout
* @param {Object} data
*/
const callbackRequest = (url, timeout, data) => {
data = JSON.stringify(data)
const options = {
hostname: url.hostname,
port: url.port,
path: url.pathname,
timeout,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
}
}
const req = http.request(options)
req.on('timeout', () => {
console.warn('Callback request timed out.')
req.abort()
})
req.on('error', (e) => {
console.error('Callback request error.', e)
req.abort()
})
req.write(data)
req.end()
}
/**
* @param {string} objName
* @param {string} objType
* @param {import('./utils.cjs').WSSharedDoc} doc
*/
const getContent = (objName, objType, doc) => {
switch (objType) {
case 'Array': return doc.getArray(objName)
case 'Map': return doc.getMap(objName)
case 'Text': return doc.getText(objName)
case 'XmlFragment': return doc.getXmlFragment(objName)
case 'XmlElement': return doc.getXmlElement(objName)
default : return {}
}
}

31
client/node_modules/y-websocket/bin/server.cjs generated vendored Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env node
const WebSocket = require('ws')
const http = require('http')
const number = require('lib0/number')
const wss = new WebSocket.Server({ noServer: true })
const setupWSConnection = require('./utils.cjs').setupWSConnection
const host = process.env.HOST || 'localhost'
const port = number.parseInt(process.env.PORT || '1234')
const server = http.createServer((_request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' })
response.end('okay')
})
wss.on('connection', setupWSConnection)
server.on('upgrade', (request, socket, head) => {
// You may check auth of request here..
// Call `wss.HandleUpgrade` *after* you checked whether the client has access
// (e.g. by checking cookies, or url parameters).
// See https://github.com/websockets/ws#client-authentication
wss.handleUpgrade(request, socket, head, /** @param {any} ws */ ws => {
wss.emit('connection', ws, request)
})
})
server.listen(port, host, () => {
console.log(`running at '${host}' on port ${port}`)
})

306
client/node_modules/y-websocket/bin/utils.cjs generated vendored Normal file
View File

@@ -0,0 +1,306 @@
const Y = require('yjs')
const syncProtocol = require('y-protocols/sync')
const awarenessProtocol = require('y-protocols/awareness')
const encoding = require('lib0/encoding')
const decoding = require('lib0/decoding')
const map = require('lib0/map')
const debounce = require('lodash.debounce')
const callbackHandler = require('./callback.cjs').callbackHandler
const isCallbackSet = require('./callback.cjs').isCallbackSet
const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env.CALLBACK_DEBOUNCE_WAIT || '2000')
const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000')
const wsReadyStateConnecting = 0
const wsReadyStateOpen = 1
const wsReadyStateClosing = 2 // eslint-disable-line
const wsReadyStateClosed = 3 // eslint-disable-line
// disable gc when using snapshots!
const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0'
const persistenceDir = process.env.YPERSISTENCE
/**
* @type {{bindState: function(string,WSSharedDoc):void, writeState:function(string,WSSharedDoc):Promise<any>, provider: any}|null}
*/
let persistence = null
if (typeof persistenceDir === 'string') {
console.info('Persisting documents to "' + persistenceDir + '"')
// @ts-ignore
const LeveldbPersistence = require('y-leveldb').LeveldbPersistence
const ldb = new LeveldbPersistence(persistenceDir)
persistence = {
provider: ldb,
bindState: async (docName, ydoc) => {
const persistedYdoc = await ldb.getYDoc(docName)
const newUpdates = Y.encodeStateAsUpdate(ydoc)
ldb.storeUpdate(docName, newUpdates)
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc))
ydoc.on('update', update => {
ldb.storeUpdate(docName, update)
})
},
writeState: async (_docName, _ydoc) => {}
}
}
/**
* @param {{bindState: function(string,WSSharedDoc):void,
* writeState:function(string,WSSharedDoc):Promise<any>,provider:any}|null} persistence_
*/
exports.setPersistence = persistence_ => {
persistence = persistence_
}
/**
* @return {null|{bindState: function(string,WSSharedDoc):void,
* writeState:function(string,WSSharedDoc):Promise<any>}|null} used persistence layer
*/
exports.getPersistence = () => persistence
/**
* @type {Map<string,WSSharedDoc>}
*/
const docs = new Map()
// exporting docs so that others can use it
exports.docs = docs
const messageSync = 0
const messageAwareness = 1
// const messageAuth = 2
/**
* @param {Uint8Array} update
* @param {any} _origin
* @param {WSSharedDoc} doc
* @param {any} _tr
*/
const updateHandler = (update, _origin, doc, _tr) => {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
syncProtocol.writeUpdate(encoder, update)
const message = encoding.toUint8Array(encoder)
doc.conns.forEach((_, conn) => send(doc, conn, message))
}
/**
* @type {(ydoc: Y.Doc) => Promise<void>}
*/
let contentInitializor = _ydoc => Promise.resolve()
/**
* This function is called once every time a Yjs document is created. You can
* use it to pull data from an external source or initialize content.
*
* @param {(ydoc: Y.Doc) => Promise<void>} f
*/
exports.setContentInitializor = (f) => {
contentInitializor = f
}
class WSSharedDoc extends Y.Doc {
/**
* @param {string} name
*/
constructor (name) {
super({ gc: gcEnabled })
this.name = name
/**
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
* @type {Map<Object, Set<number>>}
*/
this.conns = new Map()
/**
* @type {awarenessProtocol.Awareness}
*/
this.awareness = new awarenessProtocol.Awareness(this)
this.awareness.setLocalState(null)
/**
* @param {{ added: Array<number>, updated: Array<number>, removed: Array<number> }} changes
* @param {Object | null} conn Origin is the connection that made the change
*/
const awarenessChangeHandler = ({ added, updated, removed }, conn) => {
const changedClients = added.concat(updated, removed)
if (conn !== null) {
const connControlledIDs = /** @type {Set<number>} */ (this.conns.get(conn))
if (connControlledIDs !== undefined) {
added.forEach(clientID => { connControlledIDs.add(clientID) })
removed.forEach(clientID => { connControlledIDs.delete(clientID) })
}
}
// broadcast awareness update
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients))
const buff = encoding.toUint8Array(encoder)
this.conns.forEach((_, c) => {
send(this, c, buff)
})
}
this.awareness.on('update', awarenessChangeHandler)
this.on('update', /** @type {any} */ (updateHandler))
if (isCallbackSet) {
this.on('update', /** @type {any} */ (debounce(
callbackHandler,
CALLBACK_DEBOUNCE_WAIT,
{ maxWait: CALLBACK_DEBOUNCE_MAXWAIT }
)))
}
this.whenInitialized = contentInitializor(this)
}
}
exports.WSSharedDoc = WSSharedDoc
/**
* Gets a Y.Doc by name, whether in memory or on disk
*
* @param {string} docname - the name of the Y.Doc to find or create
* @param {boolean} gc - whether to allow gc on the doc (applies only when created)
* @return {WSSharedDoc}
*/
const getYDoc = (docname, gc = true) => map.setIfUndefined(docs, docname, () => {
const doc = new WSSharedDoc(docname)
doc.gc = gc
if (persistence !== null) {
persistence.bindState(docname, doc)
}
docs.set(docname, doc)
return doc
})
exports.getYDoc = getYDoc
/**
* @param {any} conn
* @param {WSSharedDoc} doc
* @param {Uint8Array} message
*/
const messageListener = (conn, doc, message) => {
try {
const encoder = encoding.createEncoder()
const decoder = decoding.createDecoder(message)
const messageType = decoding.readVarUint(decoder)
switch (messageType) {
case messageSync:
encoding.writeVarUint(encoder, messageSync)
syncProtocol.readSyncMessage(decoder, encoder, doc, conn)
// If the `encoder` only contains the type of reply message and no
// message, there is no need to send the message. When `encoder` only
// contains the type of reply, its length is 1.
if (encoding.length(encoder) > 1) {
send(doc, conn, encoding.toUint8Array(encoder))
}
break
case messageAwareness: {
awarenessProtocol.applyAwarenessUpdate(doc.awareness, decoding.readVarUint8Array(decoder), conn)
break
}
}
} catch (err) {
console.error(err)
// @ts-ignore
doc.emit('error', [err])
}
}
/**
* @param {WSSharedDoc} doc
* @param {any} conn
*/
const closeConn = (doc, conn) => {
if (doc.conns.has(conn)) {
/**
* @type {Set<number>}
*/
// @ts-ignore
const controlledIds = doc.conns.get(conn)
doc.conns.delete(conn)
awarenessProtocol.removeAwarenessStates(doc.awareness, Array.from(controlledIds), null)
if (doc.conns.size === 0 && persistence !== null) {
// if persisted, we store state and destroy ydocument
persistence.writeState(doc.name, doc).then(() => {
doc.destroy()
})
docs.delete(doc.name)
}
}
conn.close()
}
/**
* @param {WSSharedDoc} doc
* @param {import('ws').WebSocket} conn
* @param {Uint8Array} m
*/
const send = (doc, conn, m) => {
if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) {
closeConn(doc, conn)
}
try {
conn.send(m, {}, err => { err != null && closeConn(doc, conn) })
} catch (e) {
closeConn(doc, conn)
}
}
const pingTimeout = 30000
/**
* @param {import('ws').WebSocket} conn
* @param {import('http').IncomingMessage} req
* @param {any} opts
*/
exports.setupWSConnection = (conn, req, { docName = (req.url || '').slice(1).split('?')[0], gc = true } = {}) => {
conn.binaryType = 'arraybuffer'
// get doc, initialize if it does not exist yet
const doc = getYDoc(docName, gc)
doc.conns.set(conn, new Set())
// listen and reply to events
conn.on('message', /** @param {ArrayBuffer} message */ message => messageListener(conn, doc, new Uint8Array(message)))
// Check if connection is still alive
let pongReceived = true
const pingInterval = setInterval(() => {
if (!pongReceived) {
if (doc.conns.has(conn)) {
closeConn(doc, conn)
}
clearInterval(pingInterval)
} else if (doc.conns.has(conn)) {
pongReceived = false
try {
conn.ping()
} catch (e) {
closeConn(doc, conn)
clearInterval(pingInterval)
}
}
}, pingTimeout)
conn.on('close', () => {
closeConn(doc, conn)
clearInterval(pingInterval)
})
conn.on('pong', () => {
pongReceived = true
})
// put the following in a variables in a block so the interval handlers don't keep in in
// scope
{
// send sync step 1
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
syncProtocol.writeSyncStep1(encoder, doc)
send(doc, conn, encoding.toUint8Array(encoder))
const awarenessStates = doc.awareness.getStates()
if (awarenessStates.size > 0) {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())))
send(doc, conn, encoding.toUint8Array(encoder))
}
}
}

View File

@@ -0,0 +1,2 @@
export const isCallbackSet: boolean;
export function callbackHandler(update: Uint8Array, origin: any, doc: import('./utils.cjs').WSSharedDoc): void;

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};

42
client/node_modules/y-websocket/dist/bin/utils.d.cts generated vendored Normal file
View File

@@ -0,0 +1,42 @@
export function setPersistence(persistence_: {
bindState: (arg0: string, arg1: WSSharedDoc) => void;
writeState: (arg0: string, arg1: WSSharedDoc) => Promise<any>;
provider: any;
} | null): void;
export function getPersistence(): null | {
bindState: (arg0: string, arg1: WSSharedDoc) => void;
writeState: (arg0: string, arg1: WSSharedDoc) => Promise<any>;
} | null;
export function setContentInitializor(f: (ydoc: Y.Doc) => Promise<void>): void;
export function setupWSConnection(conn: import('ws').WebSocket, req: import('http').IncomingMessage, { docName, gc }?: any): void;
export class WSSharedDoc extends Y.Doc {
/**
* @param {string} name
*/
constructor(name: string);
name: string;
/**
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
* @type {Map<Object, Set<number>>}
*/
conns: Map<any, Set<number>>;
/**
* @type {awarenessProtocol.Awareness}
*/
awareness: awarenessProtocol.Awareness;
whenInitialized: Promise<void>;
}
/**
* @type {Map<string,WSSharedDoc>}
*/
export const docs: Map<string, WSSharedDoc>;
import Y = require("yjs");
/**
* Gets a Y.Doc by name, whether in memory or on disk
*
* @param {string} docname - the name of the Y.Doc to find or create
* @param {boolean} gc - whether to allow gc on the doc (applies only when created)
* @return {WSSharedDoc}
*/
export function getYDoc(docname: string, gc?: boolean): WSSharedDoc;
import awarenessProtocol = require("y-protocols/awareness");

View File

@@ -0,0 +1,139 @@
export const messageSync: 0;
export const messageQueryAwareness: 3;
export const messageAwareness: 1;
export const messageAuth: 2;
/**
* Websocket Provider for Yjs. Creates a websocket connection to sync the shared document.
* The document name is attached to the provided url. I.e. the following example
* creates a websocket connection to http://localhost:1234/my-document-name
*
* @example
* import * as Y from 'yjs'
* import { WebsocketProvider } from 'y-websocket'
* const doc = new Y.Doc()
* const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc)
*
* @extends {ObservableV2<{ 'connection-close': (event: CloseEvent | null, provider: WebsocketProvider) => any, 'status': (event: { status: 'connected' | 'disconnected' | 'connecting' }) => any, 'connection-error': (event: Event, provider: WebsocketProvider) => any, 'sync': (state: boolean) => any }>}
*/
export class WebsocketProvider extends ObservableV2<{
'connection-close': (event: CloseEvent | null, provider: WebsocketProvider) => any;
status: (event: {
status: 'connected' | 'disconnected' | 'connecting';
}) => any;
'connection-error': (event: Event, provider: WebsocketProvider) => any;
sync: (state: boolean) => any;
}> {
/**
* @param {string} serverUrl
* @param {string} roomname
* @param {Y.Doc} doc
* @param {object} opts
* @param {boolean} [opts.connect]
* @param {awarenessProtocol.Awareness} [opts.awareness]
* @param {Object<string,string>} [opts.params] specify url parameters
* @param {Array<string>} [opts.protocols] specify websocket protocols
* @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill
* @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds
* @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff)
* @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication
*/
constructor(serverUrl: string, roomname: string, doc: Y.Doc, { connect, awareness, params, protocols, WebSocketPolyfill, resyncInterval, maxBackoffTime, disableBc }?: {
connect?: boolean | undefined;
awareness?: awarenessProtocol.Awareness | undefined;
params?: {
[x: string]: string;
} | undefined;
protocols?: string[] | undefined;
WebSocketPolyfill?: {
new (url: string | URL, protocols?: string | string[] | undefined): WebSocket;
prototype: WebSocket;
readonly CLOSED: number;
readonly CLOSING: number;
readonly CONNECTING: number;
readonly OPEN: number;
} | undefined;
resyncInterval?: number | undefined;
maxBackoffTime?: number | undefined;
disableBc?: boolean | undefined;
});
serverUrl: string;
bcChannel: string;
maxBackoffTime: number;
/**
* The specified url parameters. This can be safely updated. The changed parameters will be used
* when a new connection is established.
* @type {Object<string,string>}
*/
params: {
[x: string]: string;
};
protocols: string[];
roomname: string;
doc: Y.Doc;
_WS: {
new (url: string | URL, protocols?: string | string[] | undefined): WebSocket;
prototype: WebSocket;
readonly CLOSED: number;
readonly CLOSING: number;
readonly CONNECTING: number;
readonly OPEN: number;
};
awareness: awarenessProtocol.Awareness;
wsconnected: boolean;
wsconnecting: boolean;
bcconnected: boolean;
disableBc: boolean;
wsUnsuccessfulReconnects: number;
messageHandlers: ((arg0: encoding.Encoder, arg1: decoding.Decoder, arg2: WebsocketProvider, arg3: boolean, arg4: number) => void)[];
/**
* @type {boolean}
*/
_synced: boolean;
/**
* @type {WebSocket?}
*/
ws: WebSocket | null;
wsLastMessageReceived: number;
/**
* Whether to connect to other peers or not
* @type {boolean}
*/
shouldConnect: boolean;
/**
* @type {number}
*/
_resyncInterval: number;
/**
* @param {ArrayBuffer} data
* @param {any} origin
*/
_bcSubscriber: (data: ArrayBuffer, origin: any) => void;
/**
* Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel)
* @param {Uint8Array} update
* @param {any} origin
*/
_updateHandler: (update: Uint8Array, origin: any) => void;
/**
* @param {any} changed
* @param {any} _origin
*/
_awarenessUpdateHandler: ({ added, updated, removed }: any, _origin: any) => void;
_exitHandler: () => void;
_checkInterval: any;
get url(): string;
set synced(arg: boolean);
/**
* @type {boolean}
*/
get synced(): boolean;
connectBc(): void;
disconnectBc(): void;
disconnect(): void;
connect(): void;
}
import { ObservableV2 } from "lib0/observable";
import * as Y from "yjs";
import * as awarenessProtocol from "y-protocols/awareness";
import * as encoding from "lib0/encoding";
import * as decoding from "lib0/decoding";

549
client/node_modules/y-websocket/dist/y-websocket.cjs generated vendored Normal file
View File

@@ -0,0 +1,549 @@
'use strict';
require('yjs');
var bc = require('lib0/dist/broadcastchannel.cjs');
var time = require('lib0/dist/time.cjs');
var encoding = require('lib0/dist/encoding.cjs');
var decoding = require('lib0/dist/decoding.cjs');
var syncProtocol = require('y-protocols/dist/sync.cjs');
var authProtocol = require('y-protocols/dist/auth.cjs');
var awarenessProtocol = require('y-protocols/dist/awareness.cjs');
var observable = require('lib0/dist/observable.cjs');
var math = require('lib0/dist/math.cjs');
var url = require('lib0/dist/url.cjs');
var env = require('lib0/dist/environment.cjs');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var bc__namespace = /*#__PURE__*/_interopNamespaceDefault(bc);
var time__namespace = /*#__PURE__*/_interopNamespaceDefault(time);
var encoding__namespace = /*#__PURE__*/_interopNamespaceDefault(encoding);
var decoding__namespace = /*#__PURE__*/_interopNamespaceDefault(decoding);
var syncProtocol__namespace = /*#__PURE__*/_interopNamespaceDefault(syncProtocol);
var authProtocol__namespace = /*#__PURE__*/_interopNamespaceDefault(authProtocol);
var awarenessProtocol__namespace = /*#__PURE__*/_interopNamespaceDefault(awarenessProtocol);
var math__namespace = /*#__PURE__*/_interopNamespaceDefault(math);
var url__namespace = /*#__PURE__*/_interopNamespaceDefault(url);
var env__namespace = /*#__PURE__*/_interopNamespaceDefault(env);
/**
* @module provider/websocket
*/
const messageSync = 0;
const messageQueryAwareness = 3;
const messageAwareness = 1;
const messageAuth = 2;
/**
* encoder, decoder, provider, emitSynced, messageType
* @type {Array<function(encoding.Encoder, decoding.Decoder, WebsocketProvider, boolean, number):void>}
*/
const messageHandlers = [];
messageHandlers[messageSync] = (
encoder,
decoder,
provider,
emitSynced,
_messageType
) => {
encoding__namespace.writeVarUint(encoder, messageSync);
const syncMessageType = syncProtocol__namespace.readSyncMessage(
decoder,
encoder,
provider.doc,
provider
);
if (
emitSynced && syncMessageType === syncProtocol__namespace.messageYjsSyncStep2 &&
!provider.synced
) {
provider.synced = true;
}
};
messageHandlers[messageQueryAwareness] = (
encoder,
_decoder,
provider,
_emitSynced,
_messageType
) => {
encoding__namespace.writeVarUint(encoder, messageAwareness);
encoding__namespace.writeVarUint8Array(
encoder,
awarenessProtocol__namespace.encodeAwarenessUpdate(
provider.awareness,
Array.from(provider.awareness.getStates().keys())
)
);
};
messageHandlers[messageAwareness] = (
_encoder,
decoder,
provider,
_emitSynced,
_messageType
) => {
awarenessProtocol__namespace.applyAwarenessUpdate(
provider.awareness,
decoding__namespace.readVarUint8Array(decoder),
provider
);
};
messageHandlers[messageAuth] = (
_encoder,
decoder,
provider,
_emitSynced,
_messageType
) => {
authProtocol__namespace.readAuthMessage(
decoder,
provider.doc,
(_ydoc, reason) => permissionDeniedHandler(provider, reason)
);
};
// @todo - this should depend on awareness.outdatedTime
const messageReconnectTimeout = 30000;
/**
* @param {WebsocketProvider} provider
* @param {string} reason
*/
const permissionDeniedHandler = (provider, reason) =>
console.warn(`Permission denied to access ${provider.url}.\n${reason}`);
/**
* @param {WebsocketProvider} provider
* @param {Uint8Array} buf
* @param {boolean} emitSynced
* @return {encoding.Encoder}
*/
const readMessage = (provider, buf, emitSynced) => {
const decoder = decoding__namespace.createDecoder(buf);
const encoder = encoding__namespace.createEncoder();
const messageType = decoding__namespace.readVarUint(decoder);
const messageHandler = provider.messageHandlers[messageType];
if (/** @type {any} */ (messageHandler)) {
messageHandler(encoder, decoder, provider, emitSynced, messageType);
} else {
console.error('Unable to compute message');
}
return encoder
};
/**
* Outsource this function so that a new websocket connection is created immediately.
* I suspect that the `ws.onclose` event is not always fired if there are network issues.
*
* @param {WebsocketProvider} provider
* @param {WebSocket} ws
* @param {CloseEvent | null} event
*/
const closeWebsocketConnection = (provider, ws, event) => {
if (ws === provider.ws) {
provider.emit('connection-close', [event, provider]);
provider.ws = null;
ws.close();
provider.wsconnecting = false;
if (provider.wsconnected) {
provider.wsconnected = false;
provider.synced = false;
// update awareness (all users except local left)
awarenessProtocol__namespace.removeAwarenessStates(
provider.awareness,
Array.from(provider.awareness.getStates().keys()).filter((client) =>
client !== provider.doc.clientID
),
provider
);
provider.emit('status', [{
status: 'disconnected'
}]);
} else {
provider.wsUnsuccessfulReconnects++;
}
// Start with no reconnect timeout and increase timeout by
// using exponential backoff starting with 100ms
setTimeout(
setupWS,
math__namespace.min(
math__namespace.pow(2, provider.wsUnsuccessfulReconnects) * 100,
provider.maxBackoffTime
),
provider
);
}
};
/**
* @param {WebsocketProvider} provider
*/
const setupWS = (provider) => {
if (provider.shouldConnect && provider.ws === null) {
const websocket = new provider._WS(provider.url, provider.protocols);
websocket.binaryType = 'arraybuffer';
provider.ws = websocket;
provider.wsconnecting = true;
provider.wsconnected = false;
provider.synced = false;
websocket.onmessage = (event) => {
provider.wsLastMessageReceived = time__namespace.getUnixTime();
const encoder = readMessage(provider, new Uint8Array(event.data), true);
if (encoding__namespace.length(encoder) > 1) {
websocket.send(encoding__namespace.toUint8Array(encoder));
}
};
websocket.onerror = (event) => {
provider.emit('connection-error', [event, provider]);
};
websocket.onclose = (event) => {
closeWebsocketConnection(provider, websocket, event);
};
websocket.onopen = () => {
provider.wsLastMessageReceived = time__namespace.getUnixTime();
provider.wsconnecting = false;
provider.wsconnected = true;
provider.wsUnsuccessfulReconnects = 0;
provider.emit('status', [{
status: 'connected'
}]);
// always send sync step 1 when connected
const encoder = encoding__namespace.createEncoder();
encoding__namespace.writeVarUint(encoder, messageSync);
syncProtocol__namespace.writeSyncStep1(encoder, provider.doc);
websocket.send(encoding__namespace.toUint8Array(encoder));
// broadcast local awareness state
if (provider.awareness.getLocalState() !== null) {
const encoderAwarenessState = encoding__namespace.createEncoder();
encoding__namespace.writeVarUint(encoderAwarenessState, messageAwareness);
encoding__namespace.writeVarUint8Array(
encoderAwarenessState,
awarenessProtocol__namespace.encodeAwarenessUpdate(provider.awareness, [
provider.doc.clientID
])
);
websocket.send(encoding__namespace.toUint8Array(encoderAwarenessState));
}
};
provider.emit('status', [{
status: 'connecting'
}]);
}
};
/**
* @param {WebsocketProvider} provider
* @param {ArrayBuffer} buf
*/
const broadcastMessage = (provider, buf) => {
const ws = provider.ws;
if (provider.wsconnected && ws && ws.readyState === ws.OPEN) {
ws.send(buf);
}
if (provider.bcconnected) {
bc__namespace.publish(provider.bcChannel, buf, provider);
}
};
/**
* Websocket Provider for Yjs. Creates a websocket connection to sync the shared document.
* The document name is attached to the provided url. I.e. the following example
* creates a websocket connection to http://localhost:1234/my-document-name
*
* @example
* import * as Y from 'yjs'
* import { WebsocketProvider } from 'y-websocket'
* const doc = new Y.Doc()
* const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc)
*
* @extends {ObservableV2<{ 'connection-close': (event: CloseEvent | null, provider: WebsocketProvider) => any, 'status': (event: { status: 'connected' | 'disconnected' | 'connecting' }) => any, 'connection-error': (event: Event, provider: WebsocketProvider) => any, 'sync': (state: boolean) => any }>}
*/
class WebsocketProvider extends observable.ObservableV2 {
/**
* @param {string} serverUrl
* @param {string} roomname
* @param {Y.Doc} doc
* @param {object} opts
* @param {boolean} [opts.connect]
* @param {awarenessProtocol.Awareness} [opts.awareness]
* @param {Object<string,string>} [opts.params] specify url parameters
* @param {Array<string>} [opts.protocols] specify websocket protocols
* @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill
* @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds
* @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff)
* @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication
*/
constructor (serverUrl, roomname, doc, {
connect = true,
awareness = new awarenessProtocol__namespace.Awareness(doc),
params = {},
protocols = [],
WebSocketPolyfill = WebSocket,
resyncInterval = -1,
maxBackoffTime = 2500,
disableBc = false
} = {}) {
super();
// ensure that url is always ends with /
while (serverUrl[serverUrl.length - 1] === '/') {
serverUrl = serverUrl.slice(0, serverUrl.length - 1);
}
this.serverUrl = serverUrl;
this.bcChannel = serverUrl + '/' + roomname;
this.maxBackoffTime = maxBackoffTime;
/**
* The specified url parameters. This can be safely updated. The changed parameters will be used
* when a new connection is established.
* @type {Object<string,string>}
*/
this.params = params;
this.protocols = protocols;
this.roomname = roomname;
this.doc = doc;
this._WS = WebSocketPolyfill;
this.awareness = awareness;
this.wsconnected = false;
this.wsconnecting = false;
this.bcconnected = false;
this.disableBc = disableBc;
this.wsUnsuccessfulReconnects = 0;
this.messageHandlers = messageHandlers.slice();
/**
* @type {boolean}
*/
this._synced = false;
/**
* @type {WebSocket?}
*/
this.ws = null;
this.wsLastMessageReceived = 0;
/**
* Whether to connect to other peers or not
* @type {boolean}
*/
this.shouldConnect = connect;
/**
* @type {number}
*/
this._resyncInterval = 0;
if (resyncInterval > 0) {
this._resyncInterval = /** @type {any} */ (setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
// resend sync step 1
const encoder = encoding__namespace.createEncoder();
encoding__namespace.writeVarUint(encoder, messageSync);
syncProtocol__namespace.writeSyncStep1(encoder, doc);
this.ws.send(encoding__namespace.toUint8Array(encoder));
}
}, resyncInterval));
}
/**
* @param {ArrayBuffer} data
* @param {any} origin
*/
this._bcSubscriber = (data, origin) => {
if (origin !== this) {
const encoder = readMessage(this, new Uint8Array(data), false);
if (encoding__namespace.length(encoder) > 1) {
bc__namespace.publish(this.bcChannel, encoding__namespace.toUint8Array(encoder), this);
}
}
};
/**
* Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel)
* @param {Uint8Array} update
* @param {any} origin
*/
this._updateHandler = (update, origin) => {
if (origin !== this) {
const encoder = encoding__namespace.createEncoder();
encoding__namespace.writeVarUint(encoder, messageSync);
syncProtocol__namespace.writeUpdate(encoder, update);
broadcastMessage(this, encoding__namespace.toUint8Array(encoder));
}
};
this.doc.on('update', this._updateHandler);
/**
* @param {any} changed
* @param {any} _origin
*/
this._awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
const changedClients = added.concat(updated).concat(removed);
const encoder = encoding__namespace.createEncoder();
encoding__namespace.writeVarUint(encoder, messageAwareness);
encoding__namespace.writeVarUint8Array(
encoder,
awarenessProtocol__namespace.encodeAwarenessUpdate(awareness, changedClients)
);
broadcastMessage(this, encoding__namespace.toUint8Array(encoder));
};
this._exitHandler = () => {
awarenessProtocol__namespace.removeAwarenessStates(
this.awareness,
[doc.clientID],
'app closed'
);
};
if (env__namespace.isNode && typeof process !== 'undefined') {
process.on('exit', this._exitHandler);
}
awareness.on('update', this._awarenessUpdateHandler);
this._checkInterval = /** @type {any} */ (setInterval(() => {
if (
this.wsconnected &&
messageReconnectTimeout <
time__namespace.getUnixTime() - this.wsLastMessageReceived
) {
// no message received in a long time - not even your own awareness
// updates (which are updated every 15 seconds)
closeWebsocketConnection(this, /** @type {WebSocket} */ (this.ws), null);
}
}, messageReconnectTimeout / 10));
if (connect) {
this.connect();
}
}
get url () {
const encodedParams = url__namespace.encodeQueryParams(this.params);
return this.serverUrl + '/' + this.roomname +
(encodedParams.length === 0 ? '' : '?' + encodedParams)
}
/**
* @type {boolean}
*/
get synced () {
return this._synced
}
set synced (state) {
if (this._synced !== state) {
this._synced = state;
// @ts-ignore
this.emit('synced', [state]);
this.emit('sync', [state]);
}
}
destroy () {
if (this._resyncInterval !== 0) {
clearInterval(this._resyncInterval);
}
clearInterval(this._checkInterval);
this.disconnect();
if (env__namespace.isNode && typeof process !== 'undefined') {
process.off('exit', this._exitHandler);
}
this.awareness.off('update', this._awarenessUpdateHandler);
this.doc.off('update', this._updateHandler);
super.destroy();
}
connectBc () {
if (this.disableBc) {
return
}
if (!this.bcconnected) {
bc__namespace.subscribe(this.bcChannel, this._bcSubscriber);
this.bcconnected = true;
}
// send sync step1 to bc
// write sync step 1
const encoderSync = encoding__namespace.createEncoder();
encoding__namespace.writeVarUint(encoderSync, messageSync);
syncProtocol__namespace.writeSyncStep1(encoderSync, this.doc);
bc__namespace.publish(this.bcChannel, encoding__namespace.toUint8Array(encoderSync), this);
// broadcast local state
const encoderState = encoding__namespace.createEncoder();
encoding__namespace.writeVarUint(encoderState, messageSync);
syncProtocol__namespace.writeSyncStep2(encoderState, this.doc);
bc__namespace.publish(this.bcChannel, encoding__namespace.toUint8Array(encoderState), this);
// write queryAwareness
const encoderAwarenessQuery = encoding__namespace.createEncoder();
encoding__namespace.writeVarUint(encoderAwarenessQuery, messageQueryAwareness);
bc__namespace.publish(
this.bcChannel,
encoding__namespace.toUint8Array(encoderAwarenessQuery),
this
);
// broadcast local awareness state
const encoderAwarenessState = encoding__namespace.createEncoder();
encoding__namespace.writeVarUint(encoderAwarenessState, messageAwareness);
encoding__namespace.writeVarUint8Array(
encoderAwarenessState,
awarenessProtocol__namespace.encodeAwarenessUpdate(this.awareness, [
this.doc.clientID
])
);
bc__namespace.publish(
this.bcChannel,
encoding__namespace.toUint8Array(encoderAwarenessState),
this
);
}
disconnectBc () {
// broadcast message with local awareness state set to null (indicating disconnect)
const encoder = encoding__namespace.createEncoder();
encoding__namespace.writeVarUint(encoder, messageAwareness);
encoding__namespace.writeVarUint8Array(
encoder,
awarenessProtocol__namespace.encodeAwarenessUpdate(this.awareness, [
this.doc.clientID
], new Map())
);
broadcastMessage(this, encoding__namespace.toUint8Array(encoder));
if (this.bcconnected) {
bc__namespace.unsubscribe(this.bcChannel, this._bcSubscriber);
this.bcconnected = false;
}
}
disconnect () {
this.shouldConnect = false;
this.disconnectBc();
if (this.ws !== null) {
closeWebsocketConnection(this, this.ws, null);
}
}
connect () {
this.shouldConnect = true;
if (!this.wsconnected && this.ws === null) {
setupWS(this);
this.connectBc();
}
}
}
exports.WebsocketProvider = WebsocketProvider;
exports.messageAuth = messageAuth;
exports.messageAwareness = messageAwareness;
exports.messageQueryAwareness = messageQueryAwareness;
exports.messageSync = messageSync;
//# sourceMappingURL=y-websocket.cjs.map

File diff suppressed because one or more lines are too long

86
client/node_modules/y-websocket/package.json generated vendored Normal file
View File

@@ -0,0 +1,86 @@
{
"name": "y-websocket",
"version": "2.1.0",
"description": "Websockets provider for Yjs",
"main": "./dist/y-websocket.cjs",
"module": "./src/y-websocket.js",
"types": "./dist/src/y-websocket.d.ts",
"type": "module",
"sideEffects": false,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
},
"scripts": {
"start": "node ./bin/server.cjs",
"dist": "rm -rf dist && rollup -c && tsc",
"lint": "standard && tsc",
"test": "npm run lint",
"preversion": "npm run lint && npm run dist && test -e dist/src/y-websocket.d.ts && test -e dist/y-websocket.cjs"
},
"bin": {
"y-websocket-server": "./bin/server.cjs",
"y-websocket": "./bin/server.cjs"
},
"files": [
"dist/*",
"bin/*",
"src/*"
],
"exports": {
"./package.json": "./package.json",
"./bin/utils": "./bin/utils.cjs",
"./bin/callback": "./bin/callback.cjs",
".": {
"module": "./src/y-websocket.js",
"import": "./src/y-websocket.js",
"require": "./dist/y-websocket.cjs",
"types": "./dist/src/y-websocket.d.ts",
"default": "./dist/y-websocket.js"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/yjs/y-websocket.git"
},
"keywords": [
"Yjs"
],
"author": "Kevin Jahns <kevin.jahns@protonmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/yjs/y-websocket/issues"
},
"homepage": "https://github.com/yjs/y-websocket#readme",
"standard": {
"ignore": [
"/dist",
"/node_modules"
]
},
"dependencies": {
"lib0": "^0.2.52",
"lodash.debounce": "^4.0.8",
"y-protocols": "^1.0.5"
},
"devDependencies": {
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^18.15.0",
"@types/ws": "^8.5.10",
"rollup": "^3.19.1",
"standard": "^12.0.1",
"typescript": "^4.9.5",
"yjs": "^13.5.0"
},
"peerDependencies": {
"yjs": "^13.5.6"
},
"optionalDependencies": {
"ws": "^6.2.1",
"y-leveldb": "^0.1.0"
},
"engines": {
"npm": ">=8.0.0",
"node": ">=16.0.0"
}
}

514
client/node_modules/y-websocket/src/y-websocket.js generated vendored Normal file
View File

@@ -0,0 +1,514 @@
/**
* @module provider/websocket
*/
/* eslint-env browser */
import * as Y from 'yjs' // eslint-disable-line
import * as bc from 'lib0/broadcastchannel'
import * as time from 'lib0/time'
import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding'
import * as syncProtocol from 'y-protocols/sync'
import * as authProtocol from 'y-protocols/auth'
import * as awarenessProtocol from 'y-protocols/awareness'
import { ObservableV2 } from 'lib0/observable'
import * as math from 'lib0/math'
import * as url from 'lib0/url'
import * as env from 'lib0/environment'
export const messageSync = 0
export const messageQueryAwareness = 3
export const messageAwareness = 1
export const messageAuth = 2
/**
* encoder, decoder, provider, emitSynced, messageType
* @type {Array<function(encoding.Encoder, decoding.Decoder, WebsocketProvider, boolean, number):void>}
*/
const messageHandlers = []
messageHandlers[messageSync] = (
encoder,
decoder,
provider,
emitSynced,
_messageType
) => {
encoding.writeVarUint(encoder, messageSync)
const syncMessageType = syncProtocol.readSyncMessage(
decoder,
encoder,
provider.doc,
provider
)
if (
emitSynced && syncMessageType === syncProtocol.messageYjsSyncStep2 &&
!provider.synced
) {
provider.synced = true
}
}
messageHandlers[messageQueryAwareness] = (
encoder,
_decoder,
provider,
_emitSynced,
_messageType
) => {
encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarUint8Array(
encoder,
awarenessProtocol.encodeAwarenessUpdate(
provider.awareness,
Array.from(provider.awareness.getStates().keys())
)
)
}
messageHandlers[messageAwareness] = (
_encoder,
decoder,
provider,
_emitSynced,
_messageType
) => {
awarenessProtocol.applyAwarenessUpdate(
provider.awareness,
decoding.readVarUint8Array(decoder),
provider
)
}
messageHandlers[messageAuth] = (
_encoder,
decoder,
provider,
_emitSynced,
_messageType
) => {
authProtocol.readAuthMessage(
decoder,
provider.doc,
(_ydoc, reason) => permissionDeniedHandler(provider, reason)
)
}
// @todo - this should depend on awareness.outdatedTime
const messageReconnectTimeout = 30000
/**
* @param {WebsocketProvider} provider
* @param {string} reason
*/
const permissionDeniedHandler = (provider, reason) =>
console.warn(`Permission denied to access ${provider.url}.\n${reason}`)
/**
* @param {WebsocketProvider} provider
* @param {Uint8Array} buf
* @param {boolean} emitSynced
* @return {encoding.Encoder}
*/
const readMessage = (provider, buf, emitSynced) => {
const decoder = decoding.createDecoder(buf)
const encoder = encoding.createEncoder()
const messageType = decoding.readVarUint(decoder)
const messageHandler = provider.messageHandlers[messageType]
if (/** @type {any} */ (messageHandler)) {
messageHandler(encoder, decoder, provider, emitSynced, messageType)
} else {
console.error('Unable to compute message')
}
return encoder
}
/**
* Outsource this function so that a new websocket connection is created immediately.
* I suspect that the `ws.onclose` event is not always fired if there are network issues.
*
* @param {WebsocketProvider} provider
* @param {WebSocket} ws
* @param {CloseEvent | null} event
*/
const closeWebsocketConnection = (provider, ws, event) => {
if (ws === provider.ws) {
provider.emit('connection-close', [event, provider])
provider.ws = null
ws.close()
provider.wsconnecting = false
if (provider.wsconnected) {
provider.wsconnected = false
provider.synced = false
// update awareness (all users except local left)
awarenessProtocol.removeAwarenessStates(
provider.awareness,
Array.from(provider.awareness.getStates().keys()).filter((client) =>
client !== provider.doc.clientID
),
provider
)
provider.emit('status', [{
status: 'disconnected'
}])
} else {
provider.wsUnsuccessfulReconnects++
}
// Start with no reconnect timeout and increase timeout by
// using exponential backoff starting with 100ms
setTimeout(
setupWS,
math.min(
math.pow(2, provider.wsUnsuccessfulReconnects) * 100,
provider.maxBackoffTime
),
provider
)
}
}
/**
* @param {WebsocketProvider} provider
*/
const setupWS = (provider) => {
if (provider.shouldConnect && provider.ws === null) {
const websocket = new provider._WS(provider.url, provider.protocols)
websocket.binaryType = 'arraybuffer'
provider.ws = websocket
provider.wsconnecting = true
provider.wsconnected = false
provider.synced = false
websocket.onmessage = (event) => {
provider.wsLastMessageReceived = time.getUnixTime()
const encoder = readMessage(provider, new Uint8Array(event.data), true)
if (encoding.length(encoder) > 1) {
websocket.send(encoding.toUint8Array(encoder))
}
}
websocket.onerror = (event) => {
provider.emit('connection-error', [event, provider])
}
websocket.onclose = (event) => {
closeWebsocketConnection(provider, websocket, event)
}
websocket.onopen = () => {
provider.wsLastMessageReceived = time.getUnixTime()
provider.wsconnecting = false
provider.wsconnected = true
provider.wsUnsuccessfulReconnects = 0
provider.emit('status', [{
status: 'connected'
}])
// always send sync step 1 when connected
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
syncProtocol.writeSyncStep1(encoder, provider.doc)
websocket.send(encoding.toUint8Array(encoder))
// broadcast local awareness state
if (provider.awareness.getLocalState() !== null) {
const encoderAwarenessState = encoding.createEncoder()
encoding.writeVarUint(encoderAwarenessState, messageAwareness)
encoding.writeVarUint8Array(
encoderAwarenessState,
awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [
provider.doc.clientID
])
)
websocket.send(encoding.toUint8Array(encoderAwarenessState))
}
}
provider.emit('status', [{
status: 'connecting'
}])
}
}
/**
* @param {WebsocketProvider} provider
* @param {ArrayBuffer} buf
*/
const broadcastMessage = (provider, buf) => {
const ws = provider.ws
if (provider.wsconnected && ws && ws.readyState === ws.OPEN) {
ws.send(buf)
}
if (provider.bcconnected) {
bc.publish(provider.bcChannel, buf, provider)
}
}
/**
* Websocket Provider for Yjs. Creates a websocket connection to sync the shared document.
* The document name is attached to the provided url. I.e. the following example
* creates a websocket connection to http://localhost:1234/my-document-name
*
* @example
* import * as Y from 'yjs'
* import { WebsocketProvider } from 'y-websocket'
* const doc = new Y.Doc()
* const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc)
*
* @extends {ObservableV2<{ 'connection-close': (event: CloseEvent | null, provider: WebsocketProvider) => any, 'status': (event: { status: 'connected' | 'disconnected' | 'connecting' }) => any, 'connection-error': (event: Event, provider: WebsocketProvider) => any, 'sync': (state: boolean) => any }>}
*/
export class WebsocketProvider extends ObservableV2 {
/**
* @param {string} serverUrl
* @param {string} roomname
* @param {Y.Doc} doc
* @param {object} opts
* @param {boolean} [opts.connect]
* @param {awarenessProtocol.Awareness} [opts.awareness]
* @param {Object<string,string>} [opts.params] specify url parameters
* @param {Array<string>} [opts.protocols] specify websocket protocols
* @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill
* @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds
* @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff)
* @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication
*/
constructor (serverUrl, roomname, doc, {
connect = true,
awareness = new awarenessProtocol.Awareness(doc),
params = {},
protocols = [],
WebSocketPolyfill = WebSocket,
resyncInterval = -1,
maxBackoffTime = 2500,
disableBc = false
} = {}) {
super()
// ensure that url is always ends with /
while (serverUrl[serverUrl.length - 1] === '/') {
serverUrl = serverUrl.slice(0, serverUrl.length - 1)
}
this.serverUrl = serverUrl
this.bcChannel = serverUrl + '/' + roomname
this.maxBackoffTime = maxBackoffTime
/**
* The specified url parameters. This can be safely updated. The changed parameters will be used
* when a new connection is established.
* @type {Object<string,string>}
*/
this.params = params
this.protocols = protocols
this.roomname = roomname
this.doc = doc
this._WS = WebSocketPolyfill
this.awareness = awareness
this.wsconnected = false
this.wsconnecting = false
this.bcconnected = false
this.disableBc = disableBc
this.wsUnsuccessfulReconnects = 0
this.messageHandlers = messageHandlers.slice()
/**
* @type {boolean}
*/
this._synced = false
/**
* @type {WebSocket?}
*/
this.ws = null
this.wsLastMessageReceived = 0
/**
* Whether to connect to other peers or not
* @type {boolean}
*/
this.shouldConnect = connect
/**
* @type {number}
*/
this._resyncInterval = 0
if (resyncInterval > 0) {
this._resyncInterval = /** @type {any} */ (setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
// resend sync step 1
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
syncProtocol.writeSyncStep1(encoder, doc)
this.ws.send(encoding.toUint8Array(encoder))
}
}, resyncInterval))
}
/**
* @param {ArrayBuffer} data
* @param {any} origin
*/
this._bcSubscriber = (data, origin) => {
if (origin !== this) {
const encoder = readMessage(this, new Uint8Array(data), false)
if (encoding.length(encoder) > 1) {
bc.publish(this.bcChannel, encoding.toUint8Array(encoder), this)
}
}
}
/**
* Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel)
* @param {Uint8Array} update
* @param {any} origin
*/
this._updateHandler = (update, origin) => {
if (origin !== this) {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
syncProtocol.writeUpdate(encoder, update)
broadcastMessage(this, encoding.toUint8Array(encoder))
}
}
this.doc.on('update', this._updateHandler)
/**
* @param {any} changed
* @param {any} _origin
*/
this._awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
const changedClients = added.concat(updated).concat(removed)
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarUint8Array(
encoder,
awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients)
)
broadcastMessage(this, encoding.toUint8Array(encoder))
}
this._exitHandler = () => {
awarenessProtocol.removeAwarenessStates(
this.awareness,
[doc.clientID],
'app closed'
)
}
if (env.isNode && typeof process !== 'undefined') {
process.on('exit', this._exitHandler)
}
awareness.on('update', this._awarenessUpdateHandler)
this._checkInterval = /** @type {any} */ (setInterval(() => {
if (
this.wsconnected &&
messageReconnectTimeout <
time.getUnixTime() - this.wsLastMessageReceived
) {
// no message received in a long time - not even your own awareness
// updates (which are updated every 15 seconds)
closeWebsocketConnection(this, /** @type {WebSocket} */ (this.ws), null)
}
}, messageReconnectTimeout / 10))
if (connect) {
this.connect()
}
}
get url () {
const encodedParams = url.encodeQueryParams(this.params)
return this.serverUrl + '/' + this.roomname +
(encodedParams.length === 0 ? '' : '?' + encodedParams)
}
/**
* @type {boolean}
*/
get synced () {
return this._synced
}
set synced (state) {
if (this._synced !== state) {
this._synced = state
// @ts-ignore
this.emit('synced', [state])
this.emit('sync', [state])
}
}
destroy () {
if (this._resyncInterval !== 0) {
clearInterval(this._resyncInterval)
}
clearInterval(this._checkInterval)
this.disconnect()
if (env.isNode && typeof process !== 'undefined') {
process.off('exit', this._exitHandler)
}
this.awareness.off('update', this._awarenessUpdateHandler)
this.doc.off('update', this._updateHandler)
super.destroy()
}
connectBc () {
if (this.disableBc) {
return
}
if (!this.bcconnected) {
bc.subscribe(this.bcChannel, this._bcSubscriber)
this.bcconnected = true
}
// send sync step1 to bc
// write sync step 1
const encoderSync = encoding.createEncoder()
encoding.writeVarUint(encoderSync, messageSync)
syncProtocol.writeSyncStep1(encoderSync, this.doc)
bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync), this)
// broadcast local state
const encoderState = encoding.createEncoder()
encoding.writeVarUint(encoderState, messageSync)
syncProtocol.writeSyncStep2(encoderState, this.doc)
bc.publish(this.bcChannel, encoding.toUint8Array(encoderState), this)
// write queryAwareness
const encoderAwarenessQuery = encoding.createEncoder()
encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness)
bc.publish(
this.bcChannel,
encoding.toUint8Array(encoderAwarenessQuery),
this
)
// broadcast local awareness state
const encoderAwarenessState = encoding.createEncoder()
encoding.writeVarUint(encoderAwarenessState, messageAwareness)
encoding.writeVarUint8Array(
encoderAwarenessState,
awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
this.doc.clientID
])
)
bc.publish(
this.bcChannel,
encoding.toUint8Array(encoderAwarenessState),
this
)
}
disconnectBc () {
// broadcast message with local awareness state set to null (indicating disconnect)
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarUint8Array(
encoder,
awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
this.doc.clientID
], new Map())
)
broadcastMessage(this, encoding.toUint8Array(encoder))
if (this.bcconnected) {
bc.unsubscribe(this.bcChannel, this._bcSubscriber)
this.bcconnected = false
}
}
disconnect () {
this.shouldConnect = false
this.disconnectBc()
if (this.ws !== null) {
closeWebsocketConnection(this, this.ws, null)
}
}
connect () {
this.shouldConnect = true
if (!this.wsconnected && this.ws === null) {
setupWS(this)
this.connectBc()
}
}
}