implemented frontend including separate message system; started to implement backend

This commit is contained in:
User
2026-03-10 14:48:48 +01:00
committed by Jannik Luboeinski
parent 4275cbd795
commit 78d5352a48
1058 changed files with 101527 additions and 1 deletions

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

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2025 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.

142
yjs-poll/node_modules/y-websocket/README.md generated vendored Normal file
View File

@@ -0,0 +1,142 @@
# 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
There are multiple y-websocket compatible backends for `y-websocket`:
* [@y/websocket-server](https://github.com/yjs/y-websocket-server/)
* hocuspocus
- y-sweet
- y-redis
- ypy-websocket
- pycrdt-websocket
- [yrs-warp](https://github.com/y-crdt/yrs-warp)
- ...
The fastest way to get started is to run the [@y/websocket-server](https://github.com/yjs/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:
```sh
npm install @y/y-websocket-server
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>
## License
[The MIT License](./LICENSE) © Kevin Jahns

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";

550
yjs-poll/node_modules/y-websocket/dist/y-websocket.cjs generated vendored Normal file
View File

@@ -0,0 +1,550 @@
'use strict';
require('yjs');
var bc = require('lib0/broadcastchannel');
var time = require('lib0/time');
var encoding = require('lib0/encoding');
var decoding = require('lib0/decoding');
var syncProtocol = require('y-protocols/sync');
var authProtocol = require('y-protocols/auth');
var awarenessProtocol = require('y-protocols/awareness');
var observable = require('lib0/observable');
var math = require('lib0/math');
var url = require('lib0/url');
var env = require('lib0/environment');
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 serverUrl does not end 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

71
yjs-poll/node_modules/y-websocket/package.json generated vendored Normal file
View File

@@ -0,0 +1,71 @@
{
"name": "y-websocket",
"version": "3.0.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": {
"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"
},
"files": [
"dist/*",
"src/*"
],
"exports": {
"./package.json": "./package.json",
".": {
"module": "./src/y-websocket.js",
"import": "./src/y-websocket.js",
"require": "./dist/y-websocket.cjs",
"types": "./dist/src/y-websocket.d.ts",
"default": "./src/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.102",
"y-protocols": "^1.0.5"
},
"devDependencies": {
"@types/node": "^22.14.0",
"rollup": "^3.19.1",
"standard": "^12.0.1",
"typescript": "^4.9.5",
"yjs": "^13.5.0"
},
"peerDependencies": {
"yjs": "^13.5.6"
},
"engines": {
"npm": ">=8.0.0",
"node": ">=16.0.0"
}
}

514
yjs-poll/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 serverUrl does not end 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()
}
}
}