@liveblocks/yjs
Version:
Integrate your existing or new Yjs documents with Liveblocks.
624 lines (579 loc) • 21.7 kB
JavaScript
"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 = "3.15.5";
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 _sha2 = require('@noble/hashes/sha2');
var yDocHandler = (_class2 = class _yDocHandler extends Observable {
__init5() {this.unsubscribers = []}
__init6() {this._synced = false}
__init7() {this.debounceTimer = null}
static __initStatic() {this.DEBOUNCE_INTERVAL_MS = 200}
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);_class2.prototype.__init10.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();
const encodedSnapshot = this.useV2Encoding ? Y.encodeSnapshotV2(Y.snapshot(this.doc)) : Y.encodeSnapshot(Y.snapshot(this.doc));
this.localSnapshotHash\u03A3 = new (0, _core.Signal)(
_jsbase64.Base64.fromUint8Array(_sha2.sha256.call(void 0, encodedSnapshot))
);
this.remoteSnapshotHash\u03A3 = new (0, _core.Signal)(null);
this.hasEverSynced\u03A3 = new (0, _core.Signal)(false);
this.isLocalAndRemoteSnapshotEqual\u03A3 = _core.DerivedSignal.from(() => {
const remoteSnapshotHash = this.remoteSnapshotHash\u03A3.get();
if (remoteSnapshotHash === null) return false;
const localSnapshotHash = this.localSnapshotHash\u03A3.get();
if (localSnapshotHash !== remoteSnapshotHash) {
return false;
}
return true;
});
}
__init8() {this.handleServerUpdate = ({
update,
stateVector,
readOnly,
v2,
remoteSnapshotHash
}) => {
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;
this.hasEverSynced\u03A3.set(true);
}
this.remoteSnapshotHash\u03A3.set(remoteSnapshotHash);
}}
__init9() {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]);
}
}
debounced_updateLocalSnapshot() {
if (this.debounceTimer) clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
const encodedSnapshot = this.useV2Encoding ? Y.encodeSnapshotV2(Y.snapshot(this.doc)) : Y.encodeSnapshot(Y.snapshot(this.doc));
this.localSnapshotHash\u03A3.set(
_jsbase64.Base64.fromUint8Array(_sha2.sha256.call(void 0, encodedSnapshot))
);
this.debounceTimer = null;
}, _yDocHandler.DEBOUNCE_INTERVAL_MS);
}
__init10() {this.updateHandler = (update, origin) => {
this.debounced_updateLocalSnapshot();
const isFromLocal = origin instanceof _yindexeddb.IndexeddbPersistence;
if (origin !== "backend" && !isFromLocal) {
this.updateRoomDoc(update);
}
}}
experimental_getSyncStatus() {
const hasEverSynced = this.hasEverSynced\u03A3.get();
if (!hasEverSynced) {
return "loading";
}
if (!this.isLocalAndRemoteSnapshotEqual\u03A3.get()) {
return "synchronizing";
}
return "synchronized";
}
destroy() {
if (this.debounceTimer) clearTimeout(this.debounceTimer);
this.doc.off("update", this.updateHandler);
this.unsubscribers.forEach((unsub) => unsub());
this._observers = /* @__PURE__ */ new Map();
this.doc.destroy();
}
}, _class2.__initStatic(), _class2);
// src/provider.ts
var LiveblocksYjsProvider = (_class3 = class extends Observable {
__init11() {this.indexeddbProvider = null}
__init12() {this.isPaused = false}
__init13() {this.unsubscribers = []}
__init14() {this.subdocHandlers\u03A3 = new (0, _core.MutableSignal)(/* @__PURE__ */ new Map())}
constructor(room, doc, options = {}) {
super();_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);;
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.unsubscribers.push(
this.room.events.ydoc.subscribe((message) => {
const { type } = message;
if (type === _core.ClientMsgCode.UPDATE_YDOC) {
return;
}
const {
stateVector,
update: updateStr,
guid,
v2,
remoteSnapshotHash
} = message;
const canWrite = _nullishCoalesce(_optionalChain([this, 'access', _32 => _32.room, 'access', _33 => _33.getSelf, 'call', _34 => _34(), 'optionalAccess', _35 => _35.canWrite]), () => ( true));
const update = _jsbase64.Base64.toUint8Array(updateStr);
if (guid !== void 0) {
_optionalChain([this, 'access', _36 => _36.subdocHandlers\u03A3, 'access', _37 => _37.get, 'call', _38 => _38(), 'access', _39 => _39.get, 'call', _40 => _40(guid), 'optionalAccess', _41 => _41.handleServerUpdate, 'call', _42 => _42({
update,
stateVector,
readOnly: !canWrite,
v2,
remoteSnapshotHash
})]);
} else {
this.rootDocHandler.handleServerUpdate({
update,
stateVector,
readOnly: !canWrite,
v2,
remoteSnapshotHash
});
}
})
);
if (options.offlineSupport_experimental) {
this.setupOfflineSupport();
}
this.rootDocHandler.on("synced", () => {
const state = this.rootDocHandler.synced;
for (const [_, handler] of this.subdocHandlers\u03A3.get()) {
handler.syncDoc();
}
this.emit("synced", [state]);
this.emit("sync", [state]);
});
this.rootDoc.on("subdocs", this.handleSubdocs);
this.syncDoc();
this.syncStatus\u03A3 = _core.DerivedSignal.from(() => {
const rootDocumentStatus = this.rootDocHandler.experimental_getSyncStatus();
if (rootDocumentStatus === "loading" || rootDocumentStatus === "synchronizing") {
return rootDocumentStatus;
}
const subdocumentStatuses = Array.from(
this.subdocHandlers\u03A3.get().values()
).map((handler) => handler.experimental_getSyncStatus());
if (subdocumentStatuses.some((state) => state !== "synchronized")) {
return "synchronizing";
}
return "synchronized";
});
this.emit("status", [this.getStatus()]);
this.unsubscribers.push(
this.syncStatus\u03A3.subscribe(() => {
this.emit("status", [this.getStatus()]);
})
);
}
__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);
const subdocHandlers = this.subdocHandlers\u03A3.get();
if (this.options.autoloadSubdocs) {
for (const subdoc of added) {
if (!subdocHandlers.has(subdoc.guid)) {
subdoc.load();
}
}
}
for (const subdoc of removed) {
if (subdocHandlers.has(subdoc.guid)) {
_optionalChain([subdocHandlers, 'access', _46 => _46.get, 'call', _47 => _47(subdoc.guid), 'optionalAccess', _48 => _48.destroy, 'call', _49 => _49()]);
subdocHandlers.delete(subdoc.guid);
}
}
}}
__init17() {this.updateDoc = (update, guid) => {
const canWrite = _nullishCoalesce(_optionalChain([this, 'access', _50 => _50.room, 'access', _51 => _51.getSelf, 'call', _52 => _52(), 'optionalAccess', _53 => _53.canWrite]), () => ( true));
if (canWrite && !this.isPaused) {
this.room.updateYDoc(
_jsbase64.Base64.fromUint8Array(update),
guid,
this.useV2Encoding
);
}
}}
__init18() {this.fetchDoc = (vector, guid) => {
this.room.fetchYDoc(vector, guid, this.useV2Encoding);
}}
__init19() {this.createSubdocHandler = (subdoc) => {
const subdocHandlers = this.subdocHandlers\u03A3.get();
if (subdocHandlers.has(subdoc.guid)) {
_optionalChain([subdocHandlers, 'access', _54 => _54.get, 'call', _55 => _55(subdoc.guid), 'optionalAccess', _56 => _56.syncDoc, 'call', _57 => _57()]);
return;
}
const handler = new yDocHandler({
doc: subdoc,
isRoot: false,
updateDoc: this.updateDoc,
fetchDoc: this.fetchDoc,
useV2Encoding: _nullishCoalesce(this.options.useV2Encoding_experimental, () => ( false))
});
subdocHandlers.set(subdoc.guid, handler);
}}
// attempt to load a subdoc of a given guid
__init20() {this.loadSubdoc = (guid) => {
for (const subdoc of this.rootDoc.subdocs) {
if (subdoc.guid === guid) {
subdoc.load();
return true;
}
}
return false;
}}
__init21() {this.syncDoc = () => {
this.rootDocHandler.syncDoc();
for (const [_, handler] of this.subdocHandlers\u03A3.get()) {
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', _58 => _58.indexeddbProvider, 'optionalAccess', _59 => _59.destroy, 'call', _60 => _60()]);
this.indexeddbProvider = null;
this.isPaused = true;
}
unpause() {
this.isPaused = false;
if (this.options.offlineSupport_experimental) {
this.setupOfflineSupport();
}
this.rootDocHandler.syncDoc();
}
getStatus() {
return this.syncStatus\u03A3.get();
}
destroy() {
this.unsubscribers.forEach((unsub) => unsub());
this.awareness.destroy();
this.rootDocHandler.destroy();
this._observers = /* @__PURE__ */ new Map();
for (const [_, handler] of this.subdocHandlers\u03A3.get()) {
handler.destroy();
}
this.subdocHandlers\u03A3.get().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() {
}
get subdocHandlers() {
return this.subdocHandlers\u03A3.get();
}
set subdocHandlers(value) {
this.subdocHandlers\u03A3.mutate((map) => {
map.clear();
for (const [key, handler] of value) {
map.set(key, handler);
}
});
}
}, _class3);
// src/providerContext.ts
var providersMap = /* @__PURE__ */ new WeakMap();
var getYjsProviderForRoom = (room, options = {}, forceNewProvider = false) => {
const provider = providersMap.get(room);
if (provider !== void 0) {
if (!forceNewProvider) {
return provider;
}
provider.destroy();
providersMap.delete(room);
}
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