UNPKG

@liveblocks/yjs

Version:

Integrate your existing or new Yjs documents with Liveblocks.

550 lines (517 loc) 19.3 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3;// src/index.ts var _core = require('@liveblocks/core'); // src/version.ts var PKG_NAME = "@liveblocks/yjs"; var PKG_VERSION = "2.22.3"; var PKG_FORMAT = "cjs"; // src/provider.ts var _jsbase64 = require('js-base64'); // ../../node_modules/lib0/map.js var create = () => /* @__PURE__ */ new Map(); var setIfUndefined = (map, key, createT) => { let set = map.get(key); if (set === void 0) { map.set(key, set = createT()); } return set; }; // ../../node_modules/lib0/set.js var create2 = () => /* @__PURE__ */ new Set(); // ../../node_modules/lib0/array.js var from = Array.from; var isArray = Array.isArray; // ../../node_modules/lib0/observable.js var Observable = class { constructor() { this._observers = create(); } /** * @param {N} name * @param {function} f */ on(name, f) { setIfUndefined(this._observers, name, create2).add(f); } /** * @param {N} name * @param {function} f */ once(name, f) { const _f = (...args) => { this.off(name, _f); f(...args); }; this.on(name, _f); } /** * @param {N} name * @param {function} f */ off(name, f) { const observers = this._observers.get(name); if (observers !== void 0) { observers.delete(f); if (observers.size === 0) { this._observers.delete(name); } } } /** * Emit a named event. All registered event listeners that listen to the * specified name will receive the event. * * @todo This should catch exceptions * * @param {N} name The event name. * @param {Array<any>} args The arguments that are applied to the event listener. */ emit(name, args) { return from((this._observers.get(name) || create()).values()).forEach((f) => f(...args)); } destroy() { this._observers = create(); } }; // src/provider.ts var _yindexeddb = require('y-indexeddb'); var _yjs = require('yjs'); var Y = _interopRequireWildcard(_yjs); // src/awareness.ts var Y_PRESENCE_KEY = "__yjs"; var Y_PRESENCE_ID_KEY = "__yjs_clientid"; var Awareness = (_class = class extends Observable { __init() {this.states = /* @__PURE__ */ new Map()} // used to map liveblock's ActorId to Yjs ClientID, both unique numbers representing a client __init2() {this.actorToClientMap = /* @__PURE__ */ new Map()} // Meta is used to keep track and timeout users who disconnect. Liveblocks provides this for us, so we don't need to // manage it here. Unfortunately, it's expected to exist by various integrations, so it's an empty map. __init3() {this.meta = /* @__PURE__ */ new Map()} // _checkInterval this would hold a timer to remove users, but Liveblock's presence already handles this // unfortunately it's typed by various integrations __init4() {this._checkInterval = 0} constructor(doc, room) { super();_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);; this.doc = doc; this.room = room; this.room.updatePresence({ [Y_PRESENCE_ID_KEY]: this.doc.clientID }); this.othersUnsub = this.room.events.others.subscribe((event) => { let updates; if (event.type === "leave") { const targetClientId = this.actorToClientMap.get( event.user.connectionId ); if (targetClientId !== void 0) { updates = { added: [], updated: [], removed: [targetClientId] }; } this.rebuildActorToClientMap(event.others); } if (event.type === "enter" || event.type === "update") { this.rebuildActorToClientMap(event.others); const targetClientId = this.actorToClientMap.get( event.user.connectionId ); if (targetClientId !== void 0) { updates = { added: event.type === "enter" ? [targetClientId] : [], updated: event.type === "update" ? [targetClientId] : [], removed: [] }; } } if (event.type === "reset") { this.rebuildActorToClientMap(event.others); } if (updates !== void 0) { this.emit("change", [updates, "presence"]); this.emit("update", [updates, "presence"]); } }); } rebuildActorToClientMap(others) { this.actorToClientMap.clear(); others.forEach((user) => { if (user.presence[Y_PRESENCE_ID_KEY] !== void 0) { this.actorToClientMap.set( user.connectionId, user.presence[Y_PRESENCE_ID_KEY] ); } }); } destroy() { this.emit("destroy", [this]); this.othersUnsub(); this.setLocalState(null); super.destroy(); } getLocalState() { const presence = this.room.getPresence(); if (Object.keys(presence).length === 0 || typeof presence[Y_PRESENCE_KEY] === "undefined") { return null; } return presence[Y_PRESENCE_KEY]; } setLocalState(state) { const presence = _optionalChain([this, 'access', _2 => _2.room, 'access', _3 => _3.getSelf, 'call', _4 => _4(), 'optionalAccess', _5 => _5.presence]); if (state === null) { if (presence === void 0) { return; } this.room.updatePresence({ ...presence, [Y_PRESENCE_KEY]: null }); this.emit("update", [ { added: [], updated: [], removed: [this.doc.clientID] }, "local" ]); return; } const yPresence = _optionalChain([presence, 'optionalAccess', _6 => _6[Y_PRESENCE_KEY]]); const added = yPresence === void 0 ? [this.doc.clientID] : []; const updated = yPresence === void 0 ? [] : [this.doc.clientID]; this.room.updatePresence({ [Y_PRESENCE_KEY]: { ...yPresence || {}, ...state || {} } }); this.emit("update", [{ added, updated, removed: [] }, "local"]); } setLocalStateField(field, value) { const presence = _optionalChain([this, 'access', _7 => _7.room, 'access', _8 => _8.getSelf, 'call', _9 => _9(), 'optionalAccess', _10 => _10.presence, 'access', _11 => _11[Y_PRESENCE_KEY]]); const update = { [field]: value }; this.room.updatePresence({ [Y_PRESENCE_KEY]: { ...presence || {}, ...update } }); } // Translate liveblocks presence to yjs awareness getStates() { const others = this.room.getOthers(); const states = others.reduce((acc, otherUser) => { const otherPresence = otherUser.presence[Y_PRESENCE_KEY]; const otherClientId = otherUser.presence[Y_PRESENCE_ID_KEY]; if (otherPresence !== void 0 && otherClientId !== void 0) { acc.set(otherClientId, otherPresence || {}); } return acc; }, /* @__PURE__ */ new Map()); const localPresence = _optionalChain([this, 'access', _12 => _12.room, 'access', _13 => _13.getSelf, 'call', _14 => _14(), 'optionalAccess', _15 => _15.presence, 'access', _16 => _16[Y_PRESENCE_KEY]]); if (localPresence !== void 0) { states.set(this.doc.clientID, localPresence); } return states; } }, _class); // src/doc.ts var yDocHandler = (_class2 = class extends Observable { __init5() {this.unsubscribers = []} __init6() {this._synced = false} constructor({ doc, isRoot, updateDoc, fetchDoc, useV2Encoding }) { super();_class2.prototype.__init5.call(this);_class2.prototype.__init6.call(this);_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);; this.doc = doc; this.useV2Encoding = useV2Encoding; this.doc.on(useV2Encoding ? "updateV2" : "update", this.updateHandler); this.updateRoomDoc = (update) => { updateDoc(update, isRoot ? void 0 : this.doc.guid); }; this.fetchRoomDoc = (vector) => { fetchDoc(vector, isRoot ? void 0 : this.doc.guid); }; this.syncDoc(); } __init7() {this.handleServerUpdate = ({ update, stateVector, readOnly, v2 }) => { const applyUpdate2 = v2 ? Y.applyUpdateV2 : Y.applyUpdate; applyUpdate2(this.doc, update, "backend"); if (stateVector) { if (!readOnly) { try { const encodeUpdate = this.useV2Encoding ? Y.encodeStateAsUpdateV2 : Y.encodeStateAsUpdate; const localUpdate = encodeUpdate( this.doc, _jsbase64.Base64.toUint8Array(stateVector) ); this.updateRoomDoc(localUpdate); } catch (e) { console.warn(e); } } this.synced = true; } }} __init8() {this.syncDoc = () => { this.synced = false; const encodedVector = _jsbase64.Base64.fromUint8Array(Y.encodeStateVector(this.doc)); this.fetchRoomDoc(encodedVector); }} // The sync'd property is required by some provider implementations get synced() { return this._synced; } set synced(state) { if (this._synced !== state) { this._synced = state; this.emit("synced", [state]); this.emit("sync", [state]); } } __init9() {this.updateHandler = (update, origin) => { const isFromLocal = origin instanceof _yindexeddb.IndexeddbPersistence; if (origin !== "backend" && !isFromLocal) { this.updateRoomDoc(update); } }} destroy() { this.doc.off("update", this.updateHandler); this.unsubscribers.forEach((unsub) => unsub()); this._observers = /* @__PURE__ */ new Map(); this.doc.destroy(); } }, _class2); // src/provider.ts var LiveblocksYjsProvider = (_class3 = class extends Observable { __init10() {this.indexeddbProvider = null} __init11() {this.isPaused = false} __init12() {this.unsubscribers = []} __init13() {this.subdocHandlers = /* @__PURE__ */ new Map()} __init14() {this.pending = []} constructor(room, doc, options = {}) { super();_class3.prototype.__init10.call(this);_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);_class3.prototype.__init14.call(this);_class3.prototype.__init15.call(this);_class3.prototype.__init16.call(this);_class3.prototype.__init17.call(this);_class3.prototype.__init18.call(this);_class3.prototype.__init19.call(this);_class3.prototype.__init20.call(this);_class3.prototype.__init21.call(this);_class3.prototype.__init22.call(this);; this.rootDoc = doc; this.room = room; this.options = options; this.rootDocHandler = new yDocHandler({ doc, isRoot: true, updateDoc: this.updateDoc, fetchDoc: this.fetchDoc, useV2Encoding: _nullishCoalesce(this.options.useV2Encoding_experimental, () => ( false)) }); if (this.options.enablePermanentUserData) { this.permanentUserData = new (0, _yjs.PermanentUserData)(doc); } room[_core.kInternal].setYjsProvider(this); this.awareness = new Awareness(this.rootDoc, this.room); this.unsubscribers.push( this.room.events.status.subscribe((status) => { if (status === "connected") { this.rootDocHandler.syncDoc(); } else { this.rootDocHandler.synced = false; } this.emit("status", [this.getStatus()]); }) ); this.unsubscribers.push( this.room.events.ydoc.subscribe((message) => { const { type } = message; if (type === _core.ClientMsgCode.UPDATE_YDOC) { return; } const { stateVector, update: updateStr, guid, v2 } = message; const canWrite = _nullishCoalesce(_optionalChain([this, 'access', _34 => _34.room, 'access', _35 => _35.getSelf, 'call', _36 => _36(), 'optionalAccess', _37 => _37.canWrite]), () => ( true)); const update = _jsbase64.Base64.toUint8Array(updateStr); let foundPendingUpdate = false; const updateId = this.getUniqueUpdateId(update); this.pending = this.pending.filter((pendingUpdate) => { if (pendingUpdate === updateId) { foundPendingUpdate = true; return false; } return true; }); if (!foundPendingUpdate) { if (guid !== void 0) { _optionalChain([this, 'access', _38 => _38.subdocHandlers, 'access', _39 => _39.get, 'call', _40 => _40(guid), 'optionalAccess', _41 => _41.handleServerUpdate, 'call', _42 => _42({ update, stateVector, readOnly: !canWrite, v2 })]); } else { this.rootDocHandler.handleServerUpdate({ update, stateVector, readOnly: !canWrite, v2 }); } } this.emit("status", [this.getStatus()]); }) ); if (options.offlineSupport_experimental) { this.setupOfflineSupport(); } this.rootDocHandler.on("synced", () => { const state = this.rootDocHandler.synced; for (const [_, handler] of this.subdocHandlers) { handler.syncDoc(); } this.emit("synced", [state]); this.emit("sync", [state]); this.emit("status", [this.getStatus()]); }); this.rootDoc.on("subdocs", this.handleSubdocs); this.syncDoc(); } __init15() {this.setupOfflineSupport = () => { this.indexeddbProvider = new (0, _yindexeddb.IndexeddbPersistence)( this.room.id, this.rootDoc ); const onIndexedDbSync = () => { this.rootDocHandler.synced = true; }; this.indexeddbProvider.on("synced", onIndexedDbSync); this.unsubscribers.push(() => { _optionalChain([this, 'access', _43 => _43.indexeddbProvider, 'optionalAccess', _44 => _44.off, 'call', _45 => _45("synced", onIndexedDbSync)]); }); }} __init16() {this.handleSubdocs = ({ loaded, removed, added }) => { loaded.forEach(this.createSubdocHandler); if (this.options.autoloadSubdocs) { for (const subdoc of added) { if (!this.subdocHandlers.has(subdoc.guid)) { subdoc.load(); } } } for (const subdoc of removed) { if (this.subdocHandlers.has(subdoc.guid)) { _optionalChain([this, 'access', _46 => _46.subdocHandlers, 'access', _47 => _47.get, 'call', _48 => _48(subdoc.guid), 'optionalAccess', _49 => _49.destroy, 'call', _50 => _50()]); this.subdocHandlers.delete(subdoc.guid); } } }} __init17() {this.getUniqueUpdateId = (update) => { const clock = _nullishCoalesce(_yjs.parseUpdateMeta.call(void 0, update).to.get(this.rootDoc.clientID), () => ( "-1")); return this.rootDoc.clientID + ":" + clock; }} __init18() {this.updateDoc = (update, guid) => { const canWrite = _nullishCoalesce(_optionalChain([this, 'access', _51 => _51.room, 'access', _52 => _52.getSelf, 'call', _53 => _53(), 'optionalAccess', _54 => _54.canWrite]), () => ( true)); if (canWrite && !this.isPaused) { const updateId = this.getUniqueUpdateId(update); this.pending.push(updateId); this.room.updateYDoc( _jsbase64.Base64.fromUint8Array(update), guid, this.useV2Encoding ); this.emit("status", [this.getStatus()]); } }} __init19() {this.fetchDoc = (vector, guid) => { this.room.fetchYDoc(vector, guid, this.useV2Encoding); }} __init20() {this.createSubdocHandler = (subdoc) => { if (this.subdocHandlers.has(subdoc.guid)) { _optionalChain([this, 'access', _55 => _55.subdocHandlers, 'access', _56 => _56.get, 'call', _57 => _57(subdoc.guid), 'optionalAccess', _58 => _58.syncDoc, 'call', _59 => _59()]); return; } const handler = new yDocHandler({ doc: subdoc, isRoot: false, updateDoc: this.updateDoc, fetchDoc: this.fetchDoc, useV2Encoding: _nullishCoalesce(this.options.useV2Encoding_experimental, () => ( false)) }); this.subdocHandlers.set(subdoc.guid, handler); }} // attempt to load a subdoc of a given guid __init21() {this.loadSubdoc = (guid) => { for (const subdoc of this.rootDoc.subdocs) { if (subdoc.guid === guid) { subdoc.load(); return true; } } return false; }} __init22() {this.syncDoc = () => { this.rootDocHandler.syncDoc(); for (const [_, handler] of this.subdocHandlers) { handler.syncDoc(); } }} get useV2Encoding() { return _nullishCoalesce(this.options.useV2Encoding_experimental, () => ( false)); } // The sync'd property is required by some provider implementations get synced() { return this.rootDocHandler.synced; } async pause() { await _optionalChain([this, 'access', _60 => _60.indexeddbProvider, 'optionalAccess', _61 => _61.destroy, 'call', _62 => _62()]); this.indexeddbProvider = null; this.isPaused = true; } unpause() { this.isPaused = false; if (this.options.offlineSupport_experimental) { this.setupOfflineSupport(); } this.rootDocHandler.syncDoc(); } getStatus() { if (!this.synced) { return "loading"; } return this.pending.length === 0 ? "synchronized" : "synchronizing"; } destroy() { this.unsubscribers.forEach((unsub) => unsub()); this.awareness.destroy(); this.rootDocHandler.destroy(); this._observers = /* @__PURE__ */ new Map(); for (const [_, handler] of this.subdocHandlers) { handler.destroy(); } this.subdocHandlers.clear(); super.destroy(); } async clearOfflineData() { if (!this.indexeddbProvider) return; return this.indexeddbProvider.clearData(); } getYDoc() { return this.rootDoc; } // Some provider implementations expect to be able to call connect/disconnect, implement as noop disconnect() { } connect() { } }, _class3); // src/providerContext.ts var providersMap = /* @__PURE__ */ new WeakMap(); var getYjsProviderForRoom = (room, options = {}) => { const provider = providersMap.get(room); if (provider !== void 0) { return provider; } const doc = new (0, _yjs.Doc)(); const newProvider = new LiveblocksYjsProvider(room, doc, options); room.events.roomWillDestroy.subscribeOnce(() => { newProvider.destroy(); }); providersMap.set(room, newProvider); return newProvider; }; // src/index.ts _core.detectDupes.call(void 0, PKG_NAME, PKG_VERSION, PKG_FORMAT); exports.LiveblocksYjsProvider = LiveblocksYjsProvider; exports.getYjsProviderForRoom = getYjsProviderForRoom; //# sourceMappingURL=index.cjs.map