@hocuspocus/provider
Version:
hocuspocus provider
999 lines (982 loc) • 35.5 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) {
__defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
}
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let _hocuspocus_common = require("@hocuspocus/common");
let y_protocols_awareness = require("y-protocols/awareness");
y_protocols_awareness = __toESM(y_protocols_awareness);
let yjs = require("yjs");
yjs = __toESM(yjs);
let _lifeomic_attempt = require("@lifeomic/attempt");
let lib0_decoding = require("lib0/decoding");
let lib0_encoding = require("lib0/encoding");
lib0_encoding = __toESM(lib0_encoding);
let lib0_time = require("lib0/time");
lib0_time = __toESM(lib0_time);
let y_protocols_sync = require("y-protocols/sync");
y_protocols_sync = __toESM(y_protocols_sync);
//#region packages/provider/src/EventEmitter.ts
var EventEmitter = class {
constructor() {
this.callbacks = {};
}
on(event, fn) {
if (!this.callbacks[event]) this.callbacks[event] = [];
this.callbacks[event].push(fn);
return this;
}
emit(event, ...args) {
const callbacks = this.callbacks[event];
if (callbacks) callbacks.forEach((callback) => callback.apply(this, args));
return this;
}
off(event, fn) {
const callbacks = this.callbacks[event];
if (callbacks) if (fn) this.callbacks[event] = callbacks.filter((callback) => callback !== fn);
else delete this.callbacks[event];
return this;
}
removeAllListeners() {
this.callbacks = {};
}
};
//#endregion
//#region packages/provider/src/IncomingMessage.ts
var IncomingMessage = class {
constructor(data) {
this.data = data;
this.encoder = (0, lib0_encoding.createEncoder)();
this.decoder = (0, lib0_decoding.createDecoder)(new Uint8Array(this.data));
}
peekVarString() {
return (0, lib0_decoding.peekVarString)(this.decoder);
}
readVarUint() {
return (0, lib0_decoding.readVarUint)(this.decoder);
}
readVarString() {
return (0, lib0_decoding.readVarString)(this.decoder);
}
readVarUint8Array() {
return (0, lib0_decoding.readVarUint8Array)(this.decoder);
}
writeVarUint(type) {
return (0, lib0_encoding.writeVarUint)(this.encoder, type);
}
writeVarString(string) {
return (0, lib0_encoding.writeVarString)(this.encoder, string);
}
writeVarUint8Array(data) {
return (0, lib0_encoding.writeVarUint8Array)(this.encoder, data);
}
length() {
return (0, lib0_encoding.length)(this.encoder);
}
};
//#endregion
//#region packages/provider/src/types.ts
let MessageType = /* @__PURE__ */ function(MessageType) {
MessageType[MessageType["Sync"] = 0] = "Sync";
MessageType[MessageType["Awareness"] = 1] = "Awareness";
MessageType[MessageType["Auth"] = 2] = "Auth";
MessageType[MessageType["QueryAwareness"] = 3] = "QueryAwareness";
MessageType[MessageType["Stateless"] = 5] = "Stateless";
MessageType[MessageType["CLOSE"] = 7] = "CLOSE";
MessageType[MessageType["SyncStatus"] = 8] = "SyncStatus";
MessageType[MessageType["Ping"] = 9] = "Ping";
MessageType[MessageType["Pong"] = 10] = "Pong";
return MessageType;
}({});
let WebSocketStatus = /* @__PURE__ */ function(WebSocketStatus) {
WebSocketStatus["Connecting"] = "connecting";
WebSocketStatus["Connected"] = "connected";
WebSocketStatus["Disconnected"] = "disconnected";
return WebSocketStatus;
}({});
//#endregion
//#region packages/provider/src/OutgoingMessage.ts
var OutgoingMessage = class {
constructor() {
this.encoder = (0, lib0_encoding.createEncoder)();
}
get(args) {
return args.encoder;
}
toUint8Array() {
return (0, lib0_encoding.toUint8Array)(this.encoder);
}
};
//#endregion
//#region packages/provider/src/OutgoingMessages/CloseMessage.ts
var CloseMessage = class extends OutgoingMessage {
constructor(..._args) {
super(..._args);
this.type = MessageType.CLOSE;
this.description = "Ask the server to close the connection";
}
get(args) {
lib0_encoding.writeVarString(this.encoder, args.documentName);
lib0_encoding.writeVarUint(this.encoder, this.type);
return this.encoder;
}
};
//#endregion
//#region packages/provider/src/HocuspocusProviderWebsocket.ts
var HocuspocusProviderWebsocket = class HocuspocusProviderWebsocket extends EventEmitter {
static {
this.DEDUPLICATABLE_TYPES = new Set([MessageType.Awareness, MessageType.QueryAwareness]);
}
constructor(configuration) {
super();
this.messageQueue = [];
this.configuration = {
url: "",
autoConnect: true,
preserveTrailingSlash: false,
document: void 0,
WebSocketPolyfill: void 0,
messageReconnectTimeout: 3e4,
delay: 1e3,
initialDelay: 0,
factor: 2,
maxAttempts: 0,
minDelay: 1e3,
maxDelay: 3e4,
jitter: true,
timeout: 0,
onOpen: () => null,
onConnect: () => null,
onMessage: () => null,
onOutgoingMessage: () => null,
onStatus: () => null,
onDisconnect: () => null,
onClose: () => null,
onDestroy: () => null,
onAwarenessUpdate: () => null,
onAwarenessChange: () => null,
handleTimeout: null,
providerMap: /* @__PURE__ */ new Map()
};
this.webSocket = null;
this.webSocketHandlers = {};
this.shouldConnect = true;
this.status = WebSocketStatus.Disconnected;
this.lastMessageReceived = 0;
this.identifier = 0;
this.intervals = { connectionChecker: null };
this.connectionAttempt = null;
this.receivedOnOpenPayload = void 0;
this.closeTries = 0;
this.setConfiguration(configuration);
this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
this.on("open", this.configuration.onOpen);
this.on("open", this.onOpen.bind(this));
this.on("connect", this.configuration.onConnect);
this.on("message", this.configuration.onMessage);
this.on("outgoingMessage", this.configuration.onOutgoingMessage);
this.on("status", this.configuration.onStatus);
this.on("disconnect", this.configuration.onDisconnect);
this.on("close", this.configuration.onClose);
this.on("destroy", this.configuration.onDestroy);
this.on("awarenessUpdate", this.configuration.onAwarenessUpdate);
this.on("awarenessChange", this.configuration.onAwarenessChange);
this.on("close", this.onClose.bind(this));
this.on("message", this.onMessage.bind(this));
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
if (this.shouldConnect) this.connect();
}
async onOpen(event) {
this.status = WebSocketStatus.Connected;
this.emit("status", { status: WebSocketStatus.Connected });
this.cancelWebsocketRetry = void 0;
this.receivedOnOpenPayload = event;
}
attach(provider) {
const key = provider.effectiveName;
const existing = this.configuration.providerMap.get(key);
if (existing && existing !== provider) {
if (existing.isAuthenticated) throw new Error(`Cannot attach two providers with the same effective name "${key}". Use sessionAwareness: true to multiplex providers with the same document name.`);
}
this.configuration.providerMap.set(key, provider);
if (this.status === WebSocketStatus.Disconnected && this.shouldConnect) this.connect();
if (this.receivedOnOpenPayload && this.status === WebSocketStatus.Connected) provider.onOpen(this.receivedOnOpenPayload);
}
detach(provider) {
const key = provider.effectiveName;
if (this.configuration.providerMap.has(key)) {
provider.send(CloseMessage, { documentName: key });
this.configuration.providerMap.delete(key);
}
}
setConfiguration(configuration = {}) {
this.configuration = {
...this.configuration,
...configuration
};
if (!this.configuration.autoConnect) this.shouldConnect = false;
}
async connect() {
if (this.status === WebSocketStatus.Connected) return;
if (this.cancelWebsocketRetry) {
this.cancelWebsocketRetry();
this.cancelWebsocketRetry = void 0;
}
this.receivedOnOpenPayload = void 0;
this.shouldConnect = true;
const abortableRetry = () => {
let cancelAttempt = false;
return {
retryPromise: (0, _lifeomic_attempt.retry)(this.createWebSocketConnection.bind(this), {
delay: this.configuration.delay,
initialDelay: this.configuration.initialDelay,
factor: this.configuration.factor,
maxAttempts: this.configuration.maxAttempts,
minDelay: this.configuration.minDelay,
maxDelay: this.configuration.maxDelay,
jitter: this.configuration.jitter,
timeout: this.configuration.timeout,
handleTimeout: this.configuration.handleTimeout,
beforeAttempt: (context) => {
if (!this.shouldConnect || cancelAttempt) context.abort();
}
}).catch((error) => {
if (error && error.code !== "ATTEMPT_ABORTED") throw error;
}),
cancelFunc: () => {
cancelAttempt = true;
}
};
};
const { retryPromise, cancelFunc } = abortableRetry();
this.cancelWebsocketRetry = cancelFunc;
return retryPromise;
}
attachWebSocketListeners(ws, reject) {
const { identifier } = ws;
const onMessageHandler = (payload) => this.emit("message", payload);
const onCloseHandler = (payload) => this.emit("close", { event: payload });
const onOpenHandler = (payload) => this.emit("open", payload);
const onErrorHandler = (err) => {
reject(err);
};
this.webSocketHandlers[identifier] = {
message: onMessageHandler,
close: onCloseHandler,
open: onOpenHandler,
error: onErrorHandler
};
const handlers = this.webSocketHandlers[ws.identifier];
Object.keys(handlers).forEach((name) => {
ws.addEventListener(name, handlers[name]);
});
}
cleanupWebSocket() {
if (!this.webSocket) return;
const { identifier } = this.webSocket;
const handlers = this.webSocketHandlers[identifier];
Object.keys(handlers).forEach((name) => {
this.webSocket?.removeEventListener(name, handlers[name]);
delete this.webSocketHandlers[identifier];
});
this.webSocket.close();
this.webSocket = null;
}
createWebSocketConnection() {
return new Promise((resolve, reject) => {
if (this.webSocket) {
this.messageQueue = [];
this.cleanupWebSocket();
}
this.lastMessageReceived = 0;
this.identifier += 1;
const ws = new this.configuration.WebSocketPolyfill(this.url);
ws.binaryType = "arraybuffer";
ws.identifier = this.identifier;
this.attachWebSocketListeners(ws, reject);
this.webSocket = ws;
this.status = WebSocketStatus.Connecting;
this.emit("status", { status: WebSocketStatus.Connecting });
this.connectionAttempt = {
resolve,
reject
};
});
}
onMessage(event) {
this.resolveConnectionAttempt();
this.lastMessageReceived = lib0_time.getUnixTime();
const data = new Uint8Array(event.data);
if (data.length === 1 && data[0] === MessageType.Ping) {
this.sendPong();
return;
}
const rawKey = new IncomingMessage(data).peekVarString();
this.configuration.providerMap.get(rawKey)?.onMessage(event);
}
/**
* Send application-level Pong response to server Ping
*/
sendPong() {
const encoder = lib0_encoding.createEncoder();
lib0_encoding.writeVarUint(encoder, MessageType.Pong);
this.send(lib0_encoding.toUint8Array(encoder));
}
resolveConnectionAttempt() {
if (this.connectionAttempt) {
this.connectionAttempt.resolve();
this.connectionAttempt = null;
this.status = WebSocketStatus.Connected;
this.emit("status", { status: WebSocketStatus.Connected });
this.emit("connect");
this.messageQueue.forEach((message) => this.send(message));
this.messageQueue = [];
}
}
stopConnectionAttempt() {
this.connectionAttempt = null;
}
rejectConnectionAttempt() {
this.connectionAttempt?.reject();
this.connectionAttempt = null;
}
checkConnection() {
if (this.status !== WebSocketStatus.Connected) return;
if (!this.lastMessageReceived) return;
if (this.configuration.messageReconnectTimeout >= lib0_time.getUnixTime() - this.lastMessageReceived) return;
this.closeTries += 1;
if (this.closeTries > 2) {
this.onClose({ event: {
code: 4408,
reason: "forced"
} });
this.closeTries = 0;
} else {
this.webSocket?.close();
this.messageQueue = [];
}
}
get serverUrl() {
if (this.configuration.preserveTrailingSlash) return this.configuration.url;
let url = this.configuration.url;
while (url[url.length - 1] === "/") url = url.slice(0, url.length - 1);
return url;
}
get url() {
return this.serverUrl;
}
disconnect() {
this.shouldConnect = false;
if (this.webSocket === null) return;
try {
this.webSocket.close();
this.messageQueue = [];
} catch (e) {
console.error(e);
}
}
parseQueuedMessage(message) {
try {
const decoder = (0, lib0_decoding.createDecoder)(message);
return {
documentName: (0, lib0_decoding.readVarString)(decoder),
messageType: (0, lib0_decoding.readVarUint)(decoder)
};
} catch {
return null;
}
}
addToQueue(message) {
if (message instanceof Uint8Array) {
const parsed = this.parseQueuedMessage(message);
if (parsed && HocuspocusProviderWebsocket.DEDUPLICATABLE_TYPES.has(parsed.messageType)) this.messageQueue = this.messageQueue.filter((queued) => {
if (!(queued instanceof Uint8Array)) return true;
const queuedParsed = this.parseQueuedMessage(queued);
if (!queuedParsed) return true;
return !(queuedParsed.documentName === parsed.documentName && queuedParsed.messageType === parsed.messageType);
});
}
this.messageQueue.push(message);
}
send(message) {
if (this.webSocket?.readyState === _hocuspocus_common.WsReadyStates.Open) this.webSocket.send(message);
else this.addToQueue(message);
}
onClose({ event }) {
this.closeTries = 0;
this.cleanupWebSocket();
if (this.connectionAttempt) this.rejectConnectionAttempt();
this.status = WebSocketStatus.Disconnected;
this.emit("status", { status: WebSocketStatus.Disconnected });
this.emit("disconnect", { event });
if (!this.cancelWebsocketRetry && this.shouldConnect) setTimeout(() => {
this.connect();
}, this.configuration.delay);
}
destroy() {
this.emit("destroy");
clearInterval(this.intervals.connectionChecker);
this.stopConnectionAttempt();
this.disconnect();
this.removeAllListeners();
this.cleanupWebSocket();
}
};
//#endregion
//#region packages/provider/src/MessageReceiver.ts
var MessageReceiver = class {
constructor(message) {
this.message = message;
}
apply(provider, emitSynced) {
const { message } = this;
const type = message.readVarUint();
const emptyMessageLength = message.length();
switch (type) {
case MessageType.Sync:
this.applySyncMessage(provider, emitSynced);
break;
case MessageType.Awareness:
this.applyAwarenessMessage(provider);
break;
case MessageType.Auth:
this.applyAuthMessage(provider);
break;
case MessageType.QueryAwareness:
this.applyQueryAwarenessMessage(provider);
break;
case MessageType.Stateless:
provider.receiveStateless((0, lib0_decoding.readVarString)(message.decoder));
break;
case MessageType.SyncStatus:
this.applySyncStatusMessage(provider, (0, lib0_decoding.readVarInt)(message.decoder) === 1);
break;
case MessageType.CLOSE: {
const event = {
code: 1e3,
reason: (0, lib0_decoding.readVarString)(message.decoder)
};
provider.onClose();
provider.configuration.onClose({ event });
provider.forwardClose({ event });
break;
}
default: console.error(`Can’t apply message of unknown type: ${type}`);
}
if (message.length() > emptyMessageLength + 1) provider.send(OutgoingMessage, { encoder: message.encoder });
}
applySyncMessage(provider, emitSynced) {
const { message } = this;
message.writeVarUint(MessageType.Sync);
const syncMessageType = (0, y_protocols_sync.readSyncMessage)(message.decoder, message.encoder, provider.document, provider);
if (emitSynced && syncMessageType === y_protocols_sync.messageYjsSyncStep2) provider.synced = true;
}
applySyncStatusMessage(provider, applied) {
if (applied) provider.decrementUnsyncedChanges();
}
applyAwarenessMessage(provider) {
if (!provider.awareness) return;
const { message } = this;
y_protocols_awareness.applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
}
applyAuthMessage(provider) {
const { message } = this;
(0, _hocuspocus_common.readAuthMessage)(message.decoder, provider.sendToken.bind(provider), provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
}
applyQueryAwarenessMessage(provider) {
if (!provider.awareness) return;
const { message } = this;
message.writeVarUint(MessageType.Awareness);
message.writeVarUint8Array(y_protocols_awareness.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
}
};
//#endregion
//#region packages/provider/src/MessageSender.ts
var MessageSender = class {
constructor(Message, args = {}) {
this.message = new Message();
this.encoder = this.message.get(args);
}
create() {
return (0, lib0_encoding.toUint8Array)(this.encoder);
}
send(webSocket) {
webSocket?.send(this.create());
}
};
//#endregion
//#region packages/provider/src/version.ts
const version = "4.4.0";
//#endregion
//#region packages/provider/src/OutgoingMessages/AuthenticationMessage.ts
var AuthenticationMessage = class extends OutgoingMessage {
constructor(..._args) {
super(..._args);
this.type = MessageType.Auth;
this.description = "Authentication";
}
get(args) {
if (typeof args.token === "undefined") throw new Error("The authentication message requires `token` as an argument.");
(0, lib0_encoding.writeVarString)(this.encoder, args.documentName);
(0, lib0_encoding.writeVarUint)(this.encoder, this.type);
(0, _hocuspocus_common.writeAuthentication)(this.encoder, args.token);
(0, lib0_encoding.writeVarString)(this.encoder, version);
return this.encoder;
}
};
//#endregion
//#region packages/provider/src/OutgoingMessages/AwarenessMessage.ts
var AwarenessMessage = class extends OutgoingMessage {
constructor(..._args) {
super(..._args);
this.type = MessageType.Awareness;
this.description = "Awareness states update";
}
get(args) {
if (typeof args.awareness === "undefined") throw new Error("The awareness message requires awareness as an argument");
if (typeof args.clients === "undefined") throw new Error("The awareness message requires clients as an argument");
lib0_encoding.writeVarString(this.encoder, args.documentName);
lib0_encoding.writeVarUint(this.encoder, this.type);
let awarenessUpdate;
if (args.states === void 0) awarenessUpdate = (0, y_protocols_awareness.encodeAwarenessUpdate)(args.awareness, args.clients);
else awarenessUpdate = (0, y_protocols_awareness.encodeAwarenessUpdate)(args.awareness, args.clients, args.states);
lib0_encoding.writeVarUint8Array(this.encoder, awarenessUpdate);
return this.encoder;
}
};
//#endregion
//#region packages/provider/src/OutgoingMessages/StatelessMessage.ts
var StatelessMessage = class extends OutgoingMessage {
constructor(..._args) {
super(..._args);
this.type = MessageType.Stateless;
this.description = "A stateless message";
}
get(args) {
(0, lib0_encoding.writeVarString)(this.encoder, args.documentName);
(0, lib0_encoding.writeVarUint)(this.encoder, this.type);
(0, lib0_encoding.writeVarString)(this.encoder, args.payload ?? "");
return this.encoder;
}
};
//#endregion
//#region packages/provider/src/OutgoingMessages/SyncStepOneMessage.ts
var SyncStepOneMessage = class extends OutgoingMessage {
constructor(..._args) {
super(..._args);
this.type = MessageType.Sync;
this.description = "First sync step";
}
get(args) {
if (typeof args.document === "undefined") throw new Error("The sync step one message requires document as an argument");
lib0_encoding.writeVarString(this.encoder, args.documentName);
lib0_encoding.writeVarUint(this.encoder, this.type);
y_protocols_sync.writeSyncStep1(this.encoder, args.document);
return this.encoder;
}
};
//#endregion
//#region packages/provider/src/OutgoingMessages/UpdateMessage.ts
var UpdateMessage = class extends OutgoingMessage {
constructor(..._args) {
super(..._args);
this.type = MessageType.Sync;
this.description = "A document update";
}
get(args) {
(0, lib0_encoding.writeVarString)(this.encoder, args.documentName);
(0, lib0_encoding.writeVarUint)(this.encoder, this.type);
(0, y_protocols_sync.writeUpdate)(this.encoder, args.update);
return this.encoder;
}
};
//#endregion
//#region packages/provider/src/HocuspocusProvider.ts
var AwarenessError = class extends Error {
constructor(..._args) {
super(..._args);
this.code = 1001;
}
};
var HocuspocusProvider = class extends EventEmitter {
/**
* The effective name used as the first VarString in messages.
* When `sessionAwareness` is enabled, returns a composite key (documentName\0sessionId).
* Otherwise, returns the plain document name.
*/
get effectiveName() {
return this.configuration.sessionAwareness ? (0, _hocuspocus_common.makeRoutingKey)(this.configuration.name, this.sessionId) : this.configuration.name;
}
constructor(configuration) {
super();
this.configuration = {
name: "",
document: void 0,
awareness: void 0,
token: null,
sessionAwareness: false,
forceSyncInterval: false,
flushDelay: false,
onAuthenticated: () => null,
onAuthenticationFailed: () => null,
onOpen: () => null,
onConnect: () => null,
onMessage: () => null,
onOutgoingMessage: () => null,
onSynced: () => null,
onStatus: () => null,
onDisconnect: () => null,
onClose: () => null,
onDestroy: () => null,
onAwarenessUpdate: () => null,
onAwarenessChange: () => null,
onStateless: () => null,
onUnsyncedChanges: () => null
};
this.isSynced = false;
this.unsyncedChanges = 0;
this.isAuthenticated = false;
this.authorizedScope = void 0;
this.manageSocket = false;
this._isAttached = false;
this.pendingUpdates = [];
this.pendingAwarenessClients = /* @__PURE__ */ new Set();
this.flushTimeout = null;
this.sessionId = Math.random().toString(36).slice(2);
this.intervals = { forceSync: null };
this.boundDocumentUpdateHandler = this.documentUpdateHandler.bind(this);
this.boundAwarenessUpdateHandler = this.awarenessUpdateHandler.bind(this);
this.boundPageHide = this.pageHide.bind(this);
this.boundOnOpen = this.onOpen.bind(this);
this.boundOnClose = this.onClose.bind(this);
this.forwardConnect = () => this.emit("connect");
this.forwardStatus = (e) => this.emit("status", e);
this.forwardClose = (e) => this.emit("close", e);
this.forwardDisconnect = (e) => this.emit("disconnect", e);
this.forwardDestroy = () => this.emit("destroy");
this.setConfiguration(configuration);
this.configuration.document = configuration.document ? configuration.document : new yjs.Doc();
this.configuration.awareness = configuration.awareness !== void 0 ? configuration.awareness : new y_protocols_awareness.Awareness(this.document);
this.on("open", this.configuration.onOpen);
this.on("message", this.configuration.onMessage);
this.on("outgoingMessage", this.configuration.onOutgoingMessage);
this.on("synced", this.configuration.onSynced);
this.on("destroy", this.configuration.onDestroy);
this.on("awarenessUpdate", this.configuration.onAwarenessUpdate);
this.on("awarenessChange", this.configuration.onAwarenessChange);
this.on("stateless", this.configuration.onStateless);
this.on("unsyncedChanges", this.configuration.onUnsyncedChanges);
this.on("authenticated", this.configuration.onAuthenticated);
this.on("authenticationFailed", this.configuration.onAuthenticationFailed);
this.awareness?.on("update", () => {
this.emit("awarenessUpdate", { states: (0, _hocuspocus_common.awarenessStatesToArray)(this.awareness.getStates()) });
});
this.awareness?.on("change", () => {
this.emit("awarenessChange", { states: (0, _hocuspocus_common.awarenessStatesToArray)(this.awareness.getStates()) });
});
this.document.on("update", this.boundDocumentUpdateHandler);
this.awareness?.on("update", this.boundAwarenessUpdateHandler);
this.registerEventListeners();
if (this.configuration.forceSyncInterval && typeof this.configuration.forceSyncInterval === "number") this.intervals.forceSync = setInterval(this.forceSync.bind(this), this.configuration.forceSyncInterval);
if (this.manageSocket) this.attach();
}
setConfiguration(configuration = {}) {
if (!configuration.websocketProvider) {
this.manageSocket = true;
this.configuration.websocketProvider = new HocuspocusProviderWebsocket(configuration);
}
this.configuration = {
...this.configuration,
...configuration
};
}
get document() {
return this.configuration.document;
}
get isAttached() {
return this._isAttached;
}
get awareness() {
return this.configuration.awareness;
}
get hasUnsyncedChanges() {
return this.unsyncedChanges > 0;
}
resetUnsyncedChanges() {
this.unsyncedChanges = 1;
this.emit("unsyncedChanges", { number: this.unsyncedChanges });
}
incrementUnsyncedChanges() {
this.unsyncedChanges += 1;
this.emit("unsyncedChanges", { number: this.unsyncedChanges });
}
decrementUnsyncedChanges() {
if (this.unsyncedChanges > 0) this.unsyncedChanges -= 1;
if (this.unsyncedChanges === 0) this.synced = true;
this.emit("unsyncedChanges", { number: this.unsyncedChanges });
}
forceSync() {
this.resetUnsyncedChanges();
this.send(SyncStepOneMessage, {
document: this.document,
documentName: this.effectiveName
});
}
pageHide() {
if (this.awareness) (0, y_protocols_awareness.removeAwarenessStates)(this.awareness, [this.document.clientID], "page hide");
}
registerEventListeners() {
if (typeof window === "undefined" || !("addEventListener" in window)) return;
window.addEventListener("pagehide", this.boundPageHide);
}
sendStateless(payload) {
this.send(StatelessMessage, {
documentName: this.effectiveName,
payload
});
}
async sendToken() {
let token;
try {
token = await this.getToken();
} catch (error) {
this.permissionDeniedHandler(`Failed to get token during sendToken(): ${error}`);
return;
}
this.send(AuthenticationMessage, {
token: token ?? "",
documentName: this.effectiveName
});
}
get batchingEnabled() {
return !!this.configuration.flushDelay && typeof this.configuration.flushDelay === "number";
}
documentUpdateHandler(update, origin) {
if (origin === this) return;
if (!this.batchingEnabled) {
this.incrementUnsyncedChanges();
this.send(UpdateMessage, {
update,
documentName: this.effectiveName
});
return;
}
if (this.pendingUpdates.length === 0) this.incrementUnsyncedChanges();
this.pendingUpdates.push(update);
this.scheduleFlush();
}
awarenessUpdateHandler({ added, updated, removed }, origin) {
const changedClients = added.concat(updated).concat(removed);
if (!this.batchingEnabled) {
this.send(AwarenessMessage, {
awareness: this.awareness,
clients: changedClients,
documentName: this.effectiveName
});
return;
}
for (const client of changedClients) this.pendingAwarenessClients.add(client);
this.scheduleFlush();
}
scheduleFlush() {
if (this.flushTimeout !== null) return;
this.flushTimeout = setTimeout(() => {
this.flushTimeout = null;
this.flushPendingUpdates();
}, this.configuration.flushDelay);
}
/**
* Send everything buffered for the current `flushDelay` window right away.
* Buffered document updates are merged into a single message and awareness
* collapses to the latest state of each changed client. Safe to call when
* nothing is pending.
*/
flushPendingUpdates() {
if (this.flushTimeout !== null) {
clearTimeout(this.flushTimeout);
this.flushTimeout = null;
}
if (this.pendingUpdates.length > 0) {
const update = this.pendingUpdates.length === 1 ? this.pendingUpdates[0] : yjs.mergeUpdates(this.pendingUpdates);
this.pendingUpdates = [];
this.send(UpdateMessage, {
update,
documentName: this.effectiveName
});
}
if (this.pendingAwarenessClients.size > 0) {
const clients = Array.from(this.pendingAwarenessClients);
this.pendingAwarenessClients.clear();
this.send(AwarenessMessage, {
awareness: this.awareness,
clients,
documentName: this.effectiveName
});
}
}
/**
* Indicates whether a first handshake with the server has been established
*
* Note: this does not mean all updates from the client have been persisted to the backend. For this,
* use `hasUnsyncedChanges`.
*/
get synced() {
return this.isSynced;
}
set synced(state) {
if (this.isSynced === state) return;
this.isSynced = state;
if (state) this.emit("synced", { state });
}
receiveStateless(payload) {
this.emit("stateless", { payload });
}
async connect() {
if (this.manageSocket) return this.configuration.websocketProvider.connect();
console.warn("HocuspocusProvider::connect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers.");
}
disconnect() {
if (this.manageSocket) return this.configuration.websocketProvider.disconnect();
console.warn("HocuspocusProvider::disconnect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers.");
}
async onOpen(event) {
this.isAuthenticated = false;
this.emit("open", { event });
await this.sendToken();
this.startSync();
}
async getToken() {
if (typeof this.configuration.token === "function") return await this.configuration.token();
return this.configuration.token;
}
startSync() {
this.resetUnsyncedChanges();
this.send(SyncStepOneMessage, {
document: this.document,
documentName: this.effectiveName
});
if (this.awareness && this.awareness.getLocalState() !== null) this.send(AwarenessMessage, {
awareness: this.awareness,
clients: [this.document.clientID],
documentName: this.effectiveName
});
}
send(message, args) {
if (!this._isAttached) return;
const messageSender = new MessageSender(message, args);
this.emit("outgoingMessage", { message: messageSender.message });
messageSender.send(this.configuration.websocketProvider);
}
onMessage(event) {
const message = new IncomingMessage(event.data);
const { documentName } = (0, _hocuspocus_common.parseRoutingKey)(message.readVarString());
message.writeVarString(this.effectiveName);
this.emit("message", {
event,
message: new IncomingMessage(event.data)
});
new MessageReceiver(message).apply(this, true);
}
onClose() {
this.isAuthenticated = false;
this.synced = false;
if (this.flushTimeout !== null) {
clearTimeout(this.flushTimeout);
this.flushTimeout = null;
}
this.pendingUpdates = [];
this.pendingAwarenessClients.clear();
if (this.awareness) (0, y_protocols_awareness.removeAwarenessStates)(this.awareness, Array.from(this.awareness.getStates().keys()).filter((client) => client !== this.document.clientID), this);
}
destroy() {
this.emit("destroy");
if (this.intervals.forceSync) clearInterval(this.intervals.forceSync);
if (this.awareness) {
(0, y_protocols_awareness.removeAwarenessStates)(this.awareness, [this.document.clientID], "provider destroy");
this.awareness.off("update", this.boundAwarenessUpdateHandler);
this.awareness.destroy();
}
this.flushPendingUpdates();
this.document.off("update", this.boundDocumentUpdateHandler);
this.removeAllListeners();
this.detach();
if (this.manageSocket) this.configuration.websocketProvider.destroy();
if (typeof window === "undefined" || !("removeEventListener" in window)) return;
window.removeEventListener("pagehide", this.boundPageHide);
}
detach() {
this.configuration.websocketProvider.off("connect", this.configuration.onConnect);
this.configuration.websocketProvider.off("connect", this.forwardConnect);
this.configuration.websocketProvider.off("status", this.forwardStatus);
this.configuration.websocketProvider.off("status", this.configuration.onStatus);
this.configuration.websocketProvider.off("open", this.boundOnOpen);
this.configuration.websocketProvider.off("close", this.boundOnClose);
this.configuration.websocketProvider.off("close", this.configuration.onClose);
this.configuration.websocketProvider.off("close", this.forwardClose);
this.configuration.websocketProvider.off("disconnect", this.configuration.onDisconnect);
this.configuration.websocketProvider.off("disconnect", this.forwardDisconnect);
this.configuration.websocketProvider.off("destroy", this.configuration.onDestroy);
this.configuration.websocketProvider.off("destroy", this.forwardDestroy);
this.configuration.websocketProvider.detach(this);
this._isAttached = false;
}
attach() {
if (this._isAttached) return;
this.configuration.websocketProvider.on("connect", this.configuration.onConnect);
this.configuration.websocketProvider.on("connect", this.forwardConnect);
this.configuration.websocketProvider.on("status", this.configuration.onStatus);
this.configuration.websocketProvider.on("status", this.forwardStatus);
this.configuration.websocketProvider.on("open", this.boundOnOpen);
this.configuration.websocketProvider.on("close", this.boundOnClose);
this.configuration.websocketProvider.on("close", this.configuration.onClose);
this.configuration.websocketProvider.on("close", this.forwardClose);
this.configuration.websocketProvider.on("disconnect", this.configuration.onDisconnect);
this.configuration.websocketProvider.on("disconnect", this.forwardDisconnect);
this.configuration.websocketProvider.on("destroy", this.configuration.onDestroy);
this.configuration.websocketProvider.on("destroy", this.forwardDestroy);
this.configuration.websocketProvider.attach(this);
this._isAttached = true;
}
permissionDeniedHandler(reason) {
this.emit("authenticationFailed", { reason });
this.isAuthenticated = false;
}
authenticatedHandler(scope) {
this.isAuthenticated = true;
this.authorizedScope = scope;
this.emit("authenticated", { scope });
}
setAwarenessField(key, value) {
if (!this.awareness) throw new AwarenessError(`Cannot set awareness field "${key}" to ${JSON.stringify(value)}. You have disabled Awareness for this provider by explicitly passing awareness: null in the provider configuration.`);
this.awareness.setLocalStateField(key, value);
}
};
//#endregion
exports.AwarenessError = AwarenessError;
exports.HocuspocusProvider = HocuspocusProvider;
exports.HocuspocusProviderWebsocket = HocuspocusProviderWebsocket;
exports.MessageType = MessageType;
exports.WebSocketStatus = WebSocketStatus;
//# sourceMappingURL=hocuspocus-provider.cjs.map