@hocuspocus/server
Advanced tools
Comparing version 1.0.0-alpha.65 to 1.0.0-alpha.66
@@ -6,2 +6,10 @@ # Change Log | ||
# [1.0.0-alpha.66](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.65...@hocuspocus/server@1.0.0-alpha.66) (2021-08-31) | ||
**Note:** Version bump only for package @hocuspocus/server | ||
# [1.0.0-alpha.65](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.64...@hocuspocus/server@1.0.0-alpha.65) (2021-08-29) | ||
@@ -8,0 +16,0 @@ |
@@ -428,4 +428,10 @@ import WebSocket from 'ws'; | ||
})(MessageType || (MessageType = {})); | ||
/** | ||
* State of the WebSocket connection. | ||
* https://developer.mozilla.org/de/docs/Web/API/WebSocket/readyState | ||
*/ | ||
var WsReadyStates; | ||
(function (WsReadyStates) { | ||
WsReadyStates[WsReadyStates["Connecting"] = 0] = "Connecting"; | ||
WsReadyStates[WsReadyStates["Open"] = 1] = "Open"; | ||
WsReadyStates[WsReadyStates["Closing"] = 2] = "Closing"; | ||
@@ -1095,2 +1101,3 @@ WsReadyStates[WsReadyStates["Closed"] = 3] = "Closed"; | ||
createSyncMessage() { | ||
this.type = MessageType.Sync; | ||
writeVarUint(this.encoder, MessageType.Sync); | ||
@@ -1100,2 +1107,4 @@ return this; | ||
createAwarenessUpdateMessage(awareness, changedClients) { | ||
this.type = MessageType.Awareness; | ||
this.category = 'Update'; | ||
const message = encodeAwarenessUpdate(awareness, changedClients || Array.from(awareness.getStates().keys())); | ||
@@ -1107,2 +1116,4 @@ writeVarUint(this.encoder, MessageType.Awareness); | ||
writeAuthenticated() { | ||
this.type = MessageType.Auth; | ||
this.category = 'Authenticated'; | ||
writeVarUint(this.encoder, MessageType.Auth); | ||
@@ -1113,2 +1124,4 @@ writeAuthenticated(this.encoder); | ||
writePermissionDenied(reason) { | ||
this.type = MessageType.Auth; | ||
this.category = 'PermissionDenied'; | ||
writeVarUint(this.encoder, MessageType.Auth); | ||
@@ -1119,2 +1132,3 @@ writePermissionDenied(this.encoder, reason); | ||
writeFirstSyncStepFor(document) { | ||
this.category = 'SyncStep1'; | ||
writeSyncStep1(this.encoder, document); | ||
@@ -1124,2 +1138,3 @@ return this; | ||
writeUpdate(update) { | ||
this.category = 'Update'; | ||
writeUpdate(this.encoder, update); | ||
@@ -1133,2 +1148,48 @@ return this; | ||
// import * as time from 'lib0/time' | ||
class MessageLogger { | ||
constructor() { | ||
this.logs = []; | ||
this.listen = false; | ||
this.output = false; | ||
} | ||
enable() { | ||
this.listen = true; | ||
} | ||
disable() { | ||
this.listen = false; | ||
} | ||
verbose() { | ||
this.output = true; | ||
} | ||
quiet() { | ||
this.output = false; | ||
} | ||
log(message) { | ||
if (!this.listen) { | ||
return this; | ||
} | ||
const item = { | ||
...message, | ||
type: MessageType[message.type], | ||
// time: time.getUnixTime(), | ||
}; | ||
this.logs.push(item); | ||
if (this.output) { | ||
console.log('[DEBUGGER]', item.direction === 'in' ? '–>' : '<–', `${item.type}/${item.category}`); | ||
} | ||
return this; | ||
} | ||
flush() { | ||
this.logs = []; | ||
return this; | ||
} | ||
get() { | ||
return { | ||
logs: this.logs, | ||
}; | ||
} | ||
} | ||
const Debugger = new MessageLogger(); | ||
class Document extends Doc { | ||
@@ -1145,2 +1206,3 @@ /** | ||
this.connections = new Map(); | ||
this.debugger = Debugger; | ||
this.name = name; | ||
@@ -1181,3 +1243,3 @@ this.mux = createMutex(); | ||
addConnection(connection) { | ||
this.connections.set(connection.instance, { | ||
this.connections.set(connection.webSocket, { | ||
clients: new Set(), | ||
@@ -1192,3 +1254,3 @@ connection, | ||
hasConnection(connection) { | ||
return this.connections.has(connection.instance); | ||
return this.connections.has(connection.webSocket); | ||
} | ||
@@ -1199,4 +1261,4 @@ /** | ||
removeConnection(connection) { | ||
removeAwarenessStates(this.awareness, Array.from(this.getClients(connection.instance)), null); | ||
this.connections.delete(connection.instance); | ||
removeAwarenessStates(this.awareness, Array.from(this.getClients(connection.webSocket)), null); | ||
this.connections.delete(connection.webSocket); | ||
return this; | ||
@@ -1233,3 +1295,3 @@ } | ||
applyAwarenessUpdate(connection, update) { | ||
applyAwarenessUpdate(this.awareness, update, connection.instance); | ||
applyAwarenessUpdate(this.awareness, update, connection.webSocket); | ||
return this; | ||
@@ -1250,5 +1312,12 @@ } | ||
} | ||
this.getConnections().forEach(connection => connection.send(new OutgoingMessage() | ||
.createAwarenessUpdateMessage(this.awareness, changedClients) | ||
.toUint8Array())); | ||
this.getConnections().forEach(connection => { | ||
const awarenessMessage = new OutgoingMessage() | ||
.createAwarenessUpdateMessage(this.awareness, changedClients); | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: awarenessMessage.type, | ||
category: awarenessMessage.category, | ||
}); | ||
connection.send(awarenessMessage.toUint8Array()); | ||
}); | ||
return this; | ||
@@ -1264,3 +1333,10 @@ } | ||
.writeUpdate(update); | ||
this.getConnections().forEach(connection => connection.send(message.toUint8Array())); | ||
this.getConnections().forEach(connection => { | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: message.type, | ||
category: message.category, | ||
}); | ||
connection.send(message.toUint8Array()); | ||
}); | ||
return this; | ||
@@ -1297,2 +1373,3 @@ } | ||
constructor(message) { | ||
this.debugger = Debugger; | ||
this.message = message; | ||
@@ -1313,2 +1390,7 @@ } | ||
case MessageType.Awareness: | ||
this.debugger.log({ | ||
direction: 'in', | ||
type, | ||
category: 'Update', | ||
}); | ||
applyAwarenessUpdate(document.awareness, message.readVarUint8Array(), connection); | ||
@@ -1324,5 +1406,15 @@ break; | ||
case messageYjsSyncStep1: | ||
this.debugger.log({ | ||
direction: 'in', | ||
type, | ||
category: 'SyncStep1', | ||
}); | ||
readSyncStep1(message.decoder, message.encoder, document); | ||
break; | ||
case messageYjsSyncStep2: | ||
this.debugger.log({ | ||
direction: 'in', | ||
type, | ||
category: 'SyncStep2', | ||
}); | ||
if (connection === null || connection === void 0 ? void 0 : connection.readOnly) { | ||
@@ -1334,2 +1426,7 @@ break; | ||
case messageYjsUpdate: | ||
this.debugger.log({ | ||
direction: 'in', | ||
type, | ||
category: 'Update', | ||
}); | ||
if (connection === null || connection === void 0 ? void 0 : connection.readOnly) { | ||
@@ -1356,3 +1453,4 @@ break; | ||
}; | ||
this.connection = connection; | ||
this.debugger = Debugger; | ||
this.webSocket = connection; | ||
this.context = context; | ||
@@ -1365,8 +1463,8 @@ this.document = document; | ||
this.lock = new AsyncLock(); | ||
this.connection.binaryType = 'arraybuffer'; | ||
this.webSocket.binaryType = 'arraybuffer'; | ||
this.document.addConnection(this); | ||
this.pingInterval = setInterval(this.check.bind(this), this.timeout); | ||
this.connection.on('close', this.close.bind(this)); | ||
this.connection.on('message', this.handleMessage.bind(this)); | ||
this.connection.on('pong', () => { this.pongReceived = true; }); | ||
this.webSocket.on('close', this.close.bind(this)); | ||
this.webSocket.on('message', this.handleMessage.bind(this)); | ||
this.webSocket.on('pong', () => { this.pongReceived = true; }); | ||
this.sendFirstSyncStep(); | ||
@@ -1385,8 +1483,8 @@ } | ||
send(message) { | ||
if (this.connection.readyState === WsReadyStates.Closing | ||
|| this.connection.readyState === WsReadyStates.Closed) { | ||
if (this.webSocket.readyState === WsReadyStates.Closing | ||
|| this.webSocket.readyState === WsReadyStates.Closed) { | ||
this.close(); | ||
} | ||
try { | ||
this.connection.send(message, (error) => { | ||
this.webSocket.send(message, (error) => { | ||
if (error != null) | ||
@@ -1401,5 +1499,5 @@ this.close(); | ||
/** | ||
* Close the connection | ||
* Graceful wrapper around the WebSocket close method. | ||
*/ | ||
close() { | ||
close(event) { | ||
this.lock.acquire('close', (done) => { | ||
@@ -1414,3 +1512,3 @@ if (this.pingInterval) { | ||
this.callbacks.onClose(this.document); | ||
this.connection.close(); | ||
this.webSocket.close(event === null || event === void 0 ? void 0 : event.code, event === null || event === void 0 ? void 0 : event.reason); | ||
done(); | ||
@@ -1430,3 +1528,3 @@ }); | ||
try { | ||
this.connection.ping(); | ||
this.webSocket.ping(); | ||
} | ||
@@ -1443,12 +1541,22 @@ catch (exception) { | ||
sendFirstSyncStep() { | ||
this.send(new OutgoingMessage() | ||
const syncMessage = new OutgoingMessage() | ||
.createSyncMessage() | ||
.writeFirstSyncStepFor(this.document) | ||
.toUint8Array()); | ||
.writeFirstSyncStepFor(this.document); | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: syncMessage.type, | ||
category: syncMessage.category, | ||
}); | ||
this.send(syncMessage.toUint8Array()); | ||
if (!this.document.hasAwarenessStates()) { | ||
return; | ||
} | ||
this.send(new OutgoingMessage() | ||
.createAwarenessUpdateMessage(this.document.awareness) | ||
.toUint8Array()); | ||
const awarenessMessage = new OutgoingMessage() | ||
.createAwarenessUpdateMessage(this.document.awareness); | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: awarenessMessage.type, | ||
category: awarenessMessage.category, | ||
}); | ||
this.send(awarenessMessage.toUint8Array()); | ||
} | ||
@@ -1464,6 +1572,16 @@ /** | ||
* Get the underlying connection instance | ||
* @deprecated | ||
*/ | ||
get instance() { | ||
return this.connection; | ||
console.warn('connection.instance is deprecated, use `connection.webSocket` instead.'); | ||
return this.webSocket; | ||
} | ||
/** | ||
* Get the underlying connection instance | ||
* @deprecated | ||
*/ | ||
get connection() { | ||
console.warn('connection.connection is deprecated, use `connection.webSocket` instead.'); | ||
return this.webSocket; | ||
} | ||
} | ||
@@ -1475,6 +1593,10 @@ | ||
}; | ||
const ResetConnection = { | ||
code: 4205, | ||
reason: 'Reset Connection', | ||
}; | ||
var name = "@hocuspocus/server"; | ||
var description = "plug & play collaboration backend"; | ||
var version = "1.0.0-alpha.64"; | ||
var version = "1.0.0-alpha.65"; | ||
var homepage = "https://hocuspocus.dev"; | ||
@@ -1551,2 +1673,3 @@ var keywords = [ | ||
this.documents = new Map(); | ||
this.debugger = Debugger; | ||
} | ||
@@ -1577,2 +1700,3 @@ /** | ||
yjsVersion: null, | ||
instance: this, | ||
}); | ||
@@ -1590,8 +1714,8 @@ return this; | ||
async listen() { | ||
const websocketServer = new WebSocket.Server({ noServer: true }); | ||
websocketServer.on('connection', (incoming, request) => { | ||
const webSocketServer = new WebSocket.Server({ noServer: true }); | ||
webSocketServer.on('connection', (incoming, request) => { | ||
this.handleConnection(incoming, request, Hocuspocus.getDocumentName(request)); | ||
}); | ||
const server = createServer((request, response) => { | ||
this.hooks('onRequest', { request, response }) | ||
this.hooks('onRequest', { request, response, instance: this }) | ||
.then(() => { | ||
@@ -1618,4 +1742,4 @@ // default response if all prior hooks don't interfere | ||
// @ts-ignore | ||
websocketServer.handleUpgrade(request, socket, head, ws => { | ||
websocketServer.emit('connection', ws, request); | ||
webSocketServer.handleUpgrade(request, socket, head, ws => { | ||
webSocketServer.emit('connection', ws, request); | ||
}); | ||
@@ -1633,3 +1757,3 @@ }) | ||
this.httpServer = server; | ||
this.websocketServer = websocketServer; | ||
this.webSocketServer = webSocketServer; | ||
await new Promise((resolve, reject) => { | ||
@@ -1644,2 +1768,19 @@ server.listen(this.configuration.port, () => { | ||
/** | ||
* Force closes one or more connections | ||
*/ | ||
closeConnections(documentName) { | ||
// Iterate through all connections for all documents | ||
// and invoke their close method, which is a graceful | ||
// disconnect wrapper around the underlying websocket.close | ||
this.documents.forEach((document) => { | ||
// If a documentName was specified, bail if it doesnt match | ||
if (documentName && document.name !== documentName) { | ||
return; | ||
} | ||
document.connections.forEach(({ connection } = { connection: Connection }) => { | ||
connection.close(ResetConnection); | ||
}); | ||
}); | ||
} | ||
/** | ||
* Destroy the server | ||
@@ -1650,3 +1791,3 @@ */ | ||
(_a = this.httpServer) === null || _a === void 0 ? void 0 : _a.close(); | ||
(_b = this.websocketServer) === null || _b === void 0 ? void 0 : _b.close(); | ||
(_b = this.webSocketServer) === null || _b === void 0 ? void 0 : _b.close(); | ||
await this.hooks('onDestroy', {}); | ||
@@ -1663,2 +1804,3 @@ } | ||
documentName, | ||
instance: this, | ||
request, | ||
@@ -1676,3 +1818,3 @@ requestHeaders: request.headers, | ||
// if no hook interrupts create a document and connection | ||
this.createDocument(documentName, request, socketId, context).then(document => { | ||
this.createDocument(documentName, request, socketId, connection, context).then(document => { | ||
this.createConnection(incoming, request, document, socketId, connection.readOnly, context); | ||
@@ -1697,2 +1839,7 @@ // Remove the queue listener | ||
const token = readVarString(decoder); | ||
this.debugger.log({ | ||
direction: 'in', | ||
type, | ||
category: 'Token', | ||
}); | ||
this.hooks('onAuthenticate', { token, ...hookPayload }, (contextAdditions) => { | ||
@@ -1705,2 +1852,7 @@ // merge context from hook | ||
const message = new OutgoingMessage().writeAuthenticated(); | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: message.type, | ||
category: message.category, | ||
}); | ||
incoming.send(message.toUint8Array()); | ||
@@ -1714,2 +1866,7 @@ }) | ||
const message = new OutgoingMessage().writePermissionDenied('permission-denied'); | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: message.type, | ||
category: message.category, | ||
}); | ||
// Ensure that the permission denied message is sent before the | ||
@@ -1763,3 +1920,3 @@ // connection is closed | ||
*/ | ||
async createDocument(documentName, request, socketId, context) { | ||
async createDocument(documentName, request, socketId, connection, context) { | ||
if (this.documents.has(documentName)) { | ||
@@ -1773,2 +1930,3 @@ const document = this.documents.get(documentName); | ||
context, | ||
connection, | ||
document, | ||
@@ -1862,2 +2020,22 @@ documentName, | ||
} | ||
enableDebugging() { | ||
this.debugger.enable(); | ||
} | ||
enableLogging() { | ||
this.debugger.verbose(); | ||
} | ||
disableLogging() { | ||
this.debugger.quiet(); | ||
} | ||
disableDebugging() { | ||
this.debugger.disable(); | ||
} | ||
flushMessageLogs() { | ||
this.debugger.flush(); | ||
return this; | ||
} | ||
getMessageLogs() { | ||
var _a; | ||
return (_a = this.debugger.get()) === null || _a === void 0 ? void 0 : _a.logs; | ||
} | ||
} | ||
@@ -1864,0 +2042,0 @@ const Server = new Hocuspocus(); |
import { CloseEvent } from './types'; | ||
export declare const Forbidden: CloseEvent; | ||
export declare const ResetConnection: CloseEvent; | ||
export declare const CloseEvents: CloseEvent[]; |
@@ -6,4 +6,6 @@ /// <reference types="node" /> | ||
import Document from './Document'; | ||
import { CloseEvent } from './types'; | ||
import { MessageLogger } from './Debugger'; | ||
declare class Connection { | ||
connection: WebSocket; | ||
webSocket: WebSocket; | ||
context: any; | ||
@@ -19,2 +21,3 @@ document: Document; | ||
readOnly: Boolean; | ||
debugger: MessageLogger; | ||
/** | ||
@@ -33,5 +36,5 @@ * Constructor. | ||
/** | ||
* Close the connection | ||
* Graceful wrapper around the WebSocket close method. | ||
*/ | ||
close(): void; | ||
close(event?: CloseEvent): void; | ||
/** | ||
@@ -54,5 +57,11 @@ * Check if pong was received and close the connection otherwise | ||
* Get the underlying connection instance | ||
* @deprecated | ||
*/ | ||
get instance(): WebSocket; | ||
/** | ||
* Get the underlying connection instance | ||
* @deprecated | ||
*/ | ||
get connection(): WebSocket; | ||
} | ||
export default Connection; |
@@ -6,2 +6,3 @@ import WebSocket from 'ws'; | ||
import Connection from './Connection'; | ||
import { MessageLogger } from './Debugger'; | ||
declare class Document extends Doc { | ||
@@ -15,2 +16,3 @@ awareness: Awareness; | ||
mux: mutex; | ||
debugger: MessageLogger; | ||
/** | ||
@@ -17,0 +19,0 @@ * Constructor. |
@@ -5,2 +5,3 @@ /// <reference types="node" /> | ||
import { Configuration } from './types'; | ||
import { MessageLogger } from './Debugger'; | ||
export declare const defaultConfiguration: { | ||
@@ -17,3 +18,4 @@ port: number; | ||
httpServer?: HTTPServer; | ||
websocketServer?: WebSocket.Server; | ||
webSocketServer?: WebSocket.Server; | ||
debugger: MessageLogger; | ||
/** | ||
@@ -29,2 +31,6 @@ * Configure the server | ||
/** | ||
* Force closes one or more connections | ||
*/ | ||
closeConnections(documentName?: string): void; | ||
/** | ||
* Destroy the server | ||
@@ -68,3 +74,9 @@ */ | ||
private static getDocumentName; | ||
enableDebugging(): void; | ||
enableLogging(): void; | ||
disableLogging(): void; | ||
disableDebugging(): void; | ||
flushMessageLogs(): this; | ||
getMessageLogs(): any[]; | ||
} | ||
export declare const Server: Hocuspocus; |
import Connection from './Connection'; | ||
import { IncomingMessage } from './IncomingMessage'; | ||
import { MessageLogger } from './Debugger'; | ||
export declare class MessageReceiver { | ||
message: IncomingMessage; | ||
debugger: MessageLogger; | ||
constructor(message: IncomingMessage); | ||
@@ -6,0 +8,0 @@ apply(connection: Connection): void; |
@@ -6,2 +6,4 @@ import { Encoder } from 'lib0/encoding'; | ||
encoder: Encoder; | ||
type?: number; | ||
category?: string; | ||
constructor(); | ||
@@ -8,0 +10,0 @@ createSyncMessage(): OutgoingMessage; |
@@ -6,2 +6,3 @@ /// <reference types="node" /> | ||
import Document from './Document'; | ||
import { Hocuspocus } from './Hocuspocus'; | ||
export declare enum MessageType { | ||
@@ -13,3 +14,9 @@ Unknown = -1, | ||
} | ||
/** | ||
* State of the WebSocket connection. | ||
* https://developer.mozilla.org/de/docs/Web/API/WebSocket/readyState | ||
*/ | ||
export declare enum WsReadyStates { | ||
Connecting = 0, | ||
Open = 1, | ||
Closing = 2, | ||
@@ -23,2 +30,6 @@ Closed = 3 | ||
} | ||
export interface ConnectionConfig { | ||
readOnly: boolean; | ||
isAuthenticated: boolean; | ||
} | ||
export interface Extension { | ||
@@ -37,4 +48,13 @@ onAuthenticate?(data: onAuthenticatePayload): Promise<any>; | ||
export interface Configuration extends Extension { | ||
/** | ||
* A list of hocuspocus extenions. | ||
*/ | ||
extensions: Array<Extension>; | ||
/** | ||
* The port which the server listens on. | ||
*/ | ||
port: number | null; | ||
/** | ||
* Defines in which interval the server sends a ping, and closes the connection when no pong is sent back. | ||
*/ | ||
timeout: number; | ||
@@ -44,2 +64,3 @@ } | ||
documentName: string; | ||
instance: Hocuspocus; | ||
requestHeaders: IncomingHttpHeaders; | ||
@@ -49,8 +70,7 @@ requestParameters: URLSearchParams; | ||
token: string; | ||
connection: { | ||
readOnly: boolean; | ||
}; | ||
connection: ConnectionConfig; | ||
} | ||
export interface onConnectPayload { | ||
documentName: string; | ||
instance: Hocuspocus; | ||
request: IncomingMessage; | ||
@@ -60,5 +80,3 @@ requestHeaders: IncomingHttpHeaders; | ||
socketId: string; | ||
connection: { | ||
readOnly: boolean; | ||
}; | ||
connection: ConnectionConfig; | ||
} | ||
@@ -72,2 +90,3 @@ export interface onCreateDocumentPayload { | ||
socketId: string; | ||
connection: ConnectionConfig; | ||
} | ||
@@ -96,2 +115,3 @@ export interface onChangePayload { | ||
response: ServerResponse; | ||
instance: Hocuspocus; | ||
} | ||
@@ -102,2 +122,3 @@ export interface onUpgradePayload { | ||
socket: Socket; | ||
instance: Hocuspocus; | ||
} | ||
@@ -113,2 +134,3 @@ export interface onListenPayload { | ||
yjsVersion: string; | ||
instance: Hocuspocus; | ||
} | ||
@@ -115,0 +137,0 @@ export interface CloseEvent { |
{ | ||
"name": "@hocuspocus/server", | ||
"description": "plug & play collaboration backend", | ||
"version": "1.0.0-alpha.65", | ||
"version": "1.0.0-alpha.66", | ||
"homepage": "https://hocuspocus.dev", | ||
@@ -35,3 +35,3 @@ "keywords": [ | ||
}, | ||
"gitHead": "b0e575f7941fc52e0d11f28c61af5dd024caa981" | ||
"gitHead": "f4b2c62f0322daa4c95f6be2009fc7a9c48557cb" | ||
} |
@@ -8,4 +8,10 @@ import { CloseEvent } from './types' | ||
export const ResetConnection: CloseEvent = { | ||
code: 4205, | ||
reason: 'Reset Connection', | ||
} | ||
export const CloseEvents: CloseEvent[] = [ | ||
Forbidden, | ||
ResetConnection, | ||
] |
@@ -7,9 +7,10 @@ import AsyncLock from 'async-lock' | ||
import { IncomingMessage } from './IncomingMessage' | ||
import { WsReadyStates } from './types' | ||
import { CloseEvent, WsReadyStates } from './types' | ||
import { OutgoingMessage } from './OutgoingMessage' | ||
import { MessageReceiver } from './MessageReceiver' | ||
import { Debugger, MessageLogger } from './Debugger' | ||
class Connection { | ||
connection: WebSocket | ||
webSocket: WebSocket | ||
@@ -38,2 +39,4 @@ context: any | ||
debugger: MessageLogger = Debugger | ||
/** | ||
@@ -51,3 +54,3 @@ * Constructor. | ||
) { | ||
this.connection = connection | ||
this.webSocket = connection | ||
this.context = context | ||
@@ -62,3 +65,3 @@ this.document = document | ||
this.connection.binaryType = 'arraybuffer' | ||
this.webSocket.binaryType = 'arraybuffer' | ||
this.document.addConnection(this) | ||
@@ -68,5 +71,5 @@ | ||
this.connection.on('close', this.close.bind(this)) | ||
this.connection.on('message', this.handleMessage.bind(this)) | ||
this.connection.on('pong', () => { this.pongReceived = true }) | ||
this.webSocket.on('close', this.close.bind(this)) | ||
this.webSocket.on('message', this.handleMessage.bind(this)) | ||
this.webSocket.on('pong', () => { this.pongReceived = true }) | ||
@@ -90,4 +93,4 @@ this.sendFirstSyncStep() | ||
if ( | ||
this.connection.readyState === WsReadyStates.Closing | ||
|| this.connection.readyState === WsReadyStates.Closed | ||
this.webSocket.readyState === WsReadyStates.Closing | ||
|| this.webSocket.readyState === WsReadyStates.Closed | ||
) { | ||
@@ -98,3 +101,3 @@ this.close() | ||
try { | ||
this.connection.send(message, (error: any) => { | ||
this.webSocket.send(message, (error: any) => { | ||
if (error != null) this.close() | ||
@@ -108,5 +111,5 @@ }) | ||
/** | ||
* Close the connection | ||
* Graceful wrapper around the WebSocket close method. | ||
*/ | ||
close(): void { | ||
close(event?: CloseEvent): void { | ||
this.lock.acquire('close', (done: Function) => { | ||
@@ -124,3 +127,3 @@ | ||
this.callbacks.onClose(this.document) | ||
this.connection.close() | ||
this.webSocket.close(event?.code, event?.reason) | ||
@@ -145,3 +148,3 @@ done() | ||
try { | ||
this.connection.ping() | ||
this.webSocket.ping() | ||
} catch (exception) { | ||
@@ -158,9 +161,14 @@ this.close() | ||
private sendFirstSyncStep(): void { | ||
this.send( | ||
new OutgoingMessage() | ||
.createSyncMessage() | ||
.writeFirstSyncStepFor(this.document) | ||
.toUint8Array(), | ||
) | ||
const syncMessage = new OutgoingMessage() | ||
.createSyncMessage() | ||
.writeFirstSyncStepFor(this.document) | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: syncMessage.type, | ||
category: syncMessage.category, | ||
}) | ||
this.send(syncMessage.toUint8Array()) | ||
if (!this.document.hasAwarenessStates()) { | ||
@@ -170,7 +178,12 @@ return | ||
this.send( | ||
new OutgoingMessage() | ||
.createAwarenessUpdateMessage(this.document.awareness) | ||
.toUint8Array(), | ||
) | ||
const awarenessMessage = new OutgoingMessage() | ||
.createAwarenessUpdateMessage(this.document.awareness) | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: awarenessMessage.type, | ||
category: awarenessMessage.category, | ||
}) | ||
this.send(awarenessMessage.toUint8Array()) | ||
} | ||
@@ -190,8 +203,21 @@ | ||
* Get the underlying connection instance | ||
* @deprecated | ||
*/ | ||
get instance(): WebSocket { | ||
return this.connection | ||
console.warn('connection.instance is deprecated, use `connection.webSocket` instead.') | ||
return this.webSocket | ||
} | ||
/** | ||
* Get the underlying connection instance | ||
* @deprecated | ||
*/ | ||
public get connection(): WebSocket { | ||
console.warn('connection.connection is deprecated, use `connection.webSocket` instead.') | ||
return this.webSocket | ||
} | ||
} | ||
export default Connection |
@@ -5,6 +5,6 @@ import WebSocket from 'ws' | ||
import { mutex, createMutex } from 'lib0/mutex.js' | ||
import { AwarenessUpdate } from './types' | ||
import Connection from './Connection' | ||
import { OutgoingMessage } from './OutgoingMessage' | ||
import { Debugger, MessageLogger } from './Debugger' | ||
@@ -26,2 +26,4 @@ class Document extends Doc { | ||
debugger: MessageLogger = Debugger | ||
/** | ||
@@ -76,3 +78,3 @@ * Constructor. | ||
addConnection(connection: Connection): Document { | ||
this.connections.set(connection.instance, { | ||
this.connections.set(connection.webSocket, { | ||
clients: new Set(), | ||
@@ -89,3 +91,3 @@ connection, | ||
hasConnection(connection: Connection): boolean { | ||
return this.connections.has(connection.instance) | ||
return this.connections.has(connection.webSocket) | ||
} | ||
@@ -99,7 +101,7 @@ | ||
this.awareness, | ||
Array.from(this.getClients(connection.instance)), | ||
Array.from(this.getClients(connection.webSocket)), | ||
null, | ||
) | ||
this.connections.delete(connection.instance) | ||
this.connections.delete(connection.webSocket) | ||
@@ -146,3 +148,3 @@ return this | ||
update, | ||
connection.instance, | ||
connection.webSocket, | ||
) | ||
@@ -172,8 +174,17 @@ | ||
this.getConnections().forEach(connection => connection.send( | ||
new OutgoingMessage() | ||
this.getConnections().forEach(connection => { | ||
const awarenessMessage = new OutgoingMessage() | ||
.createAwarenessUpdateMessage(this.awareness, changedClients) | ||
.toUint8Array(), | ||
)) | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: awarenessMessage.type, | ||
category: awarenessMessage.category, | ||
}) | ||
connection.send( | ||
awarenessMessage.toUint8Array(), | ||
) | ||
}) | ||
return this | ||
@@ -192,6 +203,14 @@ } | ||
this.getConnections().forEach(connection => connection.send( | ||
message.toUint8Array(), | ||
)) | ||
this.getConnections().forEach(connection => { | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: message.type, | ||
category: message.category, | ||
}) | ||
connection.send( | ||
message.toUint8Array(), | ||
) | ||
}) | ||
return this | ||
@@ -198,0 +217,0 @@ } |
@@ -7,9 +7,9 @@ import * as decoding from 'lib0/decoding' | ||
import { v4 as uuid } from 'uuid' | ||
import { MessageType, Configuration } from './types' | ||
import { MessageType, Configuration, ConnectionConfig } from './types' | ||
import Document from './Document' | ||
import Connection from './Connection' | ||
import { Forbidden } from './CloseEvents' | ||
import { Forbidden, ResetConnection } from './CloseEvents' | ||
import { OutgoingMessage } from './OutgoingMessage' | ||
import packageJson from '../package.json' | ||
import { Debugger, MessageLogger } from './Debugger' | ||
@@ -25,3 +25,2 @@ export const defaultConfiguration = { | ||
export class Hocuspocus { | ||
configuration: Configuration = { | ||
@@ -45,4 +44,6 @@ ...defaultConfiguration, | ||
websocketServer?: WebSocket.Server | ||
webSocketServer?: WebSocket.Server | ||
debugger: MessageLogger = Debugger | ||
/** | ||
@@ -52,3 +53,2 @@ * Configure the server | ||
configure(configuration: Partial<Configuration>): Hocuspocus { | ||
this.configuration = { | ||
@@ -76,6 +76,6 @@ ...this.configuration, | ||
yjsVersion: null, | ||
instance: this, | ||
}) | ||
return this | ||
} | ||
@@ -93,5 +93,4 @@ | ||
async listen(): Promise<void> { | ||
const websocketServer = new WebSocket.Server({ noServer: true }) | ||
websocketServer.on('connection', (incoming: WebSocket, request: IncomingMessage) => { | ||
const webSocketServer = new WebSocket.Server({ noServer: true }) | ||
webSocketServer.on('connection', (incoming: WebSocket, request: IncomingMessage) => { | ||
this.handleConnection(incoming, request, Hocuspocus.getDocumentName(request)) | ||
@@ -101,3 +100,3 @@ }) | ||
const server = createServer((request, response) => { | ||
this.hooks('onRequest', { request, response }) | ||
this.hooks('onRequest', { request, response, instance: this }) | ||
.then(() => { | ||
@@ -124,4 +123,4 @@ // default response if all prior hooks don't interfere | ||
// @ts-ignore | ||
websocketServer.handleUpgrade(request, socket, head, ws => { | ||
websocketServer.emit('connection', ws, request) | ||
webSocketServer.handleUpgrade(request, socket, head, ws => { | ||
webSocketServer.emit('connection', ws, request) | ||
}) | ||
@@ -139,3 +138,3 @@ }) | ||
this.httpServer = server | ||
this.websocketServer = websocketServer | ||
this.webSocketServer = webSocketServer | ||
@@ -149,3 +148,21 @@ await new Promise((resolve: Function, reject: Function) => { | ||
}) | ||
} | ||
/** | ||
* Force closes one or more connections | ||
*/ | ||
closeConnections(documentName?: string) { | ||
// Iterate through all connections for all documents | ||
// and invoke their close method, which is a graceful | ||
// disconnect wrapper around the underlying websocket.close | ||
this.documents.forEach((document: Document) => { | ||
// If a documentName was specified, bail if it doesnt match | ||
if (documentName && document.name !== documentName) { | ||
return | ||
} | ||
document.connections.forEach(({ connection } = { connection: Connection }) => { | ||
connection.close(ResetConnection) | ||
}) | ||
}) | ||
} | ||
@@ -157,8 +174,6 @@ | ||
async destroy(): Promise<any> { | ||
this.httpServer?.close() | ||
this.websocketServer?.close() | ||
this.webSocketServer?.close() | ||
await this.hooks('onDestroy', {}) | ||
} | ||
@@ -170,9 +185,9 @@ | ||
handleConnection(incoming: WebSocket, request: IncomingMessage, documentName: string, context: any = null): void { | ||
// create a unique identifier for every socket connection | ||
const socketId = uuid() | ||
const connection = { readOnly: false, isAuthenticated: false } | ||
const connection: ConnectionConfig = { readOnly: false, isAuthenticated: false } | ||
const hookPayload = { | ||
documentName, | ||
instance: this, | ||
request, | ||
@@ -193,3 +208,3 @@ requestHeaders: request.headers, | ||
// if no hook interrupts create a document and connection | ||
this.createDocument(documentName, request, socketId, context).then(document => { | ||
this.createDocument(documentName, request, socketId, connection, context).then(document => { | ||
this.createConnection(incoming, request, document, socketId, connection.readOnly, context) | ||
@@ -219,2 +234,8 @@ | ||
this.debugger.log({ | ||
direction: 'in', | ||
type, | ||
category: 'Token', | ||
}) | ||
this.hooks('onAuthenticate', { token, ...hookPayload }, (contextAdditions: any) => { | ||
@@ -228,2 +249,9 @@ // merge context from hook | ||
const message = new OutgoingMessage().writeAuthenticated() | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: message.type, | ||
category: message.category, | ||
}) | ||
incoming.send(message.toUint8Array()) | ||
@@ -238,2 +266,8 @@ }) | ||
this.debugger.log({ | ||
direction: 'out', | ||
type: message.type, | ||
category: message.category, | ||
}) | ||
// Ensure that the permission denied message is sent before the | ||
@@ -265,3 +299,2 @@ // connection is closed | ||
}) | ||
} | ||
@@ -274,3 +307,2 @@ | ||
private handleDocumentUpdate(document: Document, connection: Connection, update: Uint8Array, request: IncomingMessage, socketId: string): void { | ||
const hookPayload = { | ||
@@ -290,3 +322,2 @@ clientsCount: document.connectionsCount(), | ||
}) | ||
} | ||
@@ -298,3 +329,3 @@ | ||
*/ | ||
private async createDocument(documentName: string, request: IncomingMessage, socketId: string, context?: any): Promise<Document> { | ||
private async createDocument(documentName: string, request: IncomingMessage, socketId: string, connection: ConnectionConfig, context?: any): Promise<Document> { | ||
if (this.documents.has(documentName)) { | ||
@@ -310,2 +341,3 @@ const document = this.documents.get(documentName) | ||
context, | ||
connection, | ||
document, | ||
@@ -342,3 +374,2 @@ documentName, | ||
private createConnection(connection: WebSocket, request: IncomingMessage, document: Document, socketId: string, readOnly = false, context?: any): Connection { | ||
const instance = new Connection(connection, request, document, this.configuration.timeout, socketId, context, readOnly) | ||
@@ -369,3 +400,2 @@ | ||
return instance | ||
} | ||
@@ -420,4 +450,30 @@ | ||
} | ||
enableDebugging() { | ||
this.debugger.enable() | ||
} | ||
enableLogging() { | ||
this.debugger.verbose() | ||
} | ||
disableLogging() { | ||
this.debugger.quiet() | ||
} | ||
disableDebugging() { | ||
this.debugger.disable() | ||
} | ||
flushMessageLogs() { | ||
this.debugger.flush() | ||
return this | ||
} | ||
getMessageLogs() { | ||
return this.debugger.get()?.logs | ||
} | ||
} | ||
export const Server = new Hocuspocus() |
@@ -13,2 +13,3 @@ import { | ||
import { IncomingMessage } from './IncomingMessage' | ||
import { Debugger, MessageLogger } from './Debugger' | ||
@@ -19,2 +20,4 @@ export class MessageReceiver { | ||
debugger: MessageLogger = Debugger | ||
constructor(message: IncomingMessage) { | ||
@@ -40,2 +43,8 @@ this.message = message | ||
case MessageType.Awareness: | ||
this.debugger.log({ | ||
direction: 'in', | ||
type, | ||
category: 'Update', | ||
}) | ||
applyAwarenessUpdate(document.awareness, message.readVarUint8Array(), connection) | ||
@@ -55,5 +64,17 @@ | ||
case messageYjsSyncStep1: | ||
this.debugger.log({ | ||
direction: 'in', | ||
type, | ||
category: 'SyncStep1', | ||
}) | ||
readSyncStep1(message.decoder, message.encoder, document) | ||
break | ||
case messageYjsSyncStep2: | ||
this.debugger.log({ | ||
direction: 'in', | ||
type, | ||
category: 'SyncStep2', | ||
}) | ||
if (connection?.readOnly) { | ||
@@ -66,2 +87,8 @@ break | ||
case messageYjsUpdate: | ||
this.debugger.log({ | ||
direction: 'in', | ||
type, | ||
category: 'Update', | ||
}) | ||
if (connection?.readOnly) { | ||
@@ -68,0 +95,0 @@ break |
@@ -19,2 +19,6 @@ import { | ||
type?: number | ||
category?: string | ||
constructor() { | ||
@@ -25,2 +29,4 @@ this.encoder = createEncoder() | ||
createSyncMessage(): OutgoingMessage { | ||
this.type = MessageType.Sync | ||
writeVarUint(this.encoder, MessageType.Sync) | ||
@@ -32,2 +38,5 @@ | ||
createAwarenessUpdateMessage(awareness: Awareness, changedClients?: Array<any>): OutgoingMessage { | ||
this.type = MessageType.Awareness | ||
this.category = 'Update' | ||
const message = encodeAwarenessUpdate( | ||
@@ -45,2 +54,5 @@ awareness, | ||
writeAuthenticated(): OutgoingMessage { | ||
this.type = MessageType.Auth | ||
this.category = 'Authenticated' | ||
writeVarUint(this.encoder, MessageType.Auth) | ||
@@ -53,2 +65,5 @@ writeAuthenticated(this.encoder) | ||
writePermissionDenied(reason: string): OutgoingMessage { | ||
this.type = MessageType.Auth | ||
this.category = 'PermissionDenied' | ||
writeVarUint(this.encoder, MessageType.Auth) | ||
@@ -61,2 +76,4 @@ writePermissionDenied(this.encoder, reason) | ||
writeFirstSyncStepFor(document: Document): OutgoingMessage { | ||
this.category = 'SyncStep1' | ||
writeSyncStep1(this.encoder, document) | ||
@@ -68,2 +85,4 @@ | ||
writeUpdate(update: Uint8Array): OutgoingMessage { | ||
this.category = 'Update' | ||
writeUpdate(this.encoder, update) | ||
@@ -70,0 +89,0 @@ |
@@ -7,2 +7,3 @@ import { | ||
import Document from './Document' | ||
import { Hocuspocus } from './Hocuspocus' | ||
@@ -16,3 +17,9 @@ export enum MessageType { | ||
/** | ||
* State of the WebSocket connection. | ||
* https://developer.mozilla.org/de/docs/Web/API/WebSocket/readyState | ||
*/ | ||
export enum WsReadyStates { | ||
Connecting = 0, | ||
Open = 1, | ||
Closing = 2, | ||
@@ -28,2 +35,7 @@ Closed = 3, | ||
export interface ConnectionConfig { | ||
readOnly: boolean | ||
isAuthenticated: boolean | ||
} | ||
export interface Extension { | ||
@@ -43,4 +55,13 @@ onAuthenticate?(data: onAuthenticatePayload): Promise<any>, | ||
export interface Configuration extends Extension { | ||
/** | ||
* A list of hocuspocus extenions. | ||
*/ | ||
extensions: Array<Extension>, | ||
/** | ||
* The port which the server listens on. | ||
*/ | ||
port: number | null, | ||
/** | ||
* Defines in which interval the server sends a ping, and closes the connection when no pong is sent back. | ||
*/ | ||
timeout: number, | ||
@@ -51,2 +72,3 @@ } | ||
documentName: string, | ||
instance: Hocuspocus, | ||
requestHeaders: IncomingHttpHeaders, | ||
@@ -56,5 +78,3 @@ requestParameters: URLSearchParams, | ||
token: string, | ||
connection: { | ||
readOnly: boolean, | ||
}, | ||
connection: ConnectionConfig | ||
} | ||
@@ -64,2 +84,3 @@ | ||
documentName: string, | ||
instance: Hocuspocus, | ||
request: IncomingMessage, | ||
@@ -69,5 +90,3 @@ requestHeaders: IncomingHttpHeaders, | ||
socketId: string, | ||
connection: { | ||
readOnly: boolean, | ||
}, | ||
connection: ConnectionConfig | ||
} | ||
@@ -82,2 +101,3 @@ | ||
socketId: string, | ||
connection: ConnectionConfig | ||
} | ||
@@ -109,2 +129,3 @@ | ||
response: ServerResponse, | ||
instance: Hocuspocus, | ||
} | ||
@@ -116,2 +137,3 @@ | ||
socket: Socket, | ||
instance: Hocuspocus, | ||
} | ||
@@ -130,2 +152,3 @@ | ||
yjsVersion: string, | ||
instance: Hocuspocus, | ||
} | ||
@@ -132,0 +155,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
517165
68
5945