UNPKG

@syncedstore/yjs-reactive-bindings

Version:

A bridge between Reactive programming libraries (reactive, Vue or MobX) and Yjs

521 lines (509 loc) 15.5 kB
import * as Y from 'yjs'; var customCreateAtom; var customReaction; var defaultReaction = function defaultReaction(func) { return func(); }; function reaction(func, effect) { if (customReaction) { return customReaction(func, effect); } else { defaultReaction(func); return undefined; } } function createAtom(_name, _onBecomeObservedHandler, _onBecomeUnobservedHandler) { if (customCreateAtom) { return customCreateAtom.apply(null, arguments); } else { throw new Error("observable implementation not provided. Call enableReactiveBindings, enableVueBindings or enableMobxBindings."); } } /** * Enable MobX integration * * @param mobx An instance of mobx, e.g. import * as mobx from "mobx"; */ function enableMobxBindings(mobx) { customCreateAtom = mobx.createAtom; customReaction = undefined; } /** * Enable Vue3 integration * * @param vue An instance of Vue or Vue reactivity, e.g. import * as Vue from "vue"; */ function enableVueBindings(vue) { customCreateAtom = function customCreateAtom(name, onBecomeObserved) { var id = 0; var data = vue.reactive({ data: id }); var atom = { reportObserved: function reportObserved() { return data.data; }, reportChanged: function reportChanged() { data.data = ++id; } }; if (onBecomeObserved) { onBecomeObserved(); } return atom; }; customReaction = undefined; } function enableReactiveBindings(reactive) { customCreateAtom = function customCreateAtom(name, onBecomeObserved, onBecomeUnobserved) { // TMP var atom = reactive.createAtom(name); if (onBecomeObserved) { onBecomeObserved(); } return atom; }; customReaction = function customReaction(func, effect) { return reactive.reaction(func, effect, { fireImmediately: false }); }; } var arraysObserved = new WeakSet(); function observeArray(array) { if (arraysObserved.has(array)) { // already patched return array; } arraysObserved.add(array); var selfAtom; var atoms = new Map(); function reportSelfAtom() { if (!selfAtom) { var handler = function handler(event) { if (event.changes.added.size || event.changes.deleted.size || event.changes.keys.size || event.changes.delta.length) { selfAtom.reportChanged(); } }; selfAtom = createAtom("map", function () { array.observe(handler); }, function () { array.unobserve(handler); }); } selfAtom.reportObserved(array._implicitObserver); } function reportArrayElementAtom(key) { var atom = atoms.get(key); // possible optimization: only register a single handler for all keys if (!atom) { var handler = function handler(event) { // TODO: detect key of changed element // if (event.keys.has(key + "")) { // if ( // event.changes.added.size || // event.changes.deleted.size || // event.changes.keys.size || // event.changes.delta.length // ) { atom.reportChanged(); // } }; atom = createAtom(key + "", function () { array.observe(handler); }, function () { array.unobserve(handler); }); atoms.set(key, atom); } atom.reportObserved(array._implicitObserver); } var originalGet = array.get; array.get = function (key) { if (typeof key !== "number") { throw new Error("unexpected"); } reportArrayElementAtom(key); var ret = Reflect.apply(originalGet, this, arguments); return ret; }; function patch(method) { var originalFunction = array[method]; array[method] = function () { reportSelfAtom(); var ret = Reflect.apply(originalFunction, this, arguments); return ret; }; } function patchGetter(method) { var target = array; var descriptor = Object.getOwnPropertyDescriptor(target, method); // properties might be defined down the prototype chain (e.g., properties on XmlFragment when working on an XmlElement) if (!descriptor) { target = Object.getPrototypeOf(target); descriptor = Object.getOwnPropertyDescriptor(target, method); } if (!descriptor) { target = Object.getPrototypeOf(target); descriptor = Object.getOwnPropertyDescriptor(target, method); } if (!descriptor) { throw new Error("property not found"); } var originalFunction = descriptor.get; descriptor.get = function () { if (!this._disableTracking) { reportSelfAtom(); } var ret = Reflect.apply(originalFunction, this, arguments); return ret; }; Object.defineProperty(array, method, descriptor); } function copyGetter(method, newMethodName) { var target = array; var descriptor = Object.getOwnPropertyDescriptor(target, method); // properties might be defined down the prototype chain (e.g., properties on XmlFragment when working on an XmlElement) if (!descriptor) { target = Object.getPrototypeOf(target); descriptor = Object.getOwnPropertyDescriptor(target, method); } if (!descriptor) { target = Object.getPrototypeOf(target); descriptor = Object.getOwnPropertyDescriptor(target, method); } if (!descriptor) { throw new Error("property not found"); } Object.defineProperty(array, newMethodName, descriptor); } patch("forEach"); patch("toJSON"); patch("toArray"); patch("slice"); patch("map"); copyGetter("length", "lengthUntracked"); patchGetter("length"); // make push and slice use _disableTracking so calls to .length don't get observed var originalPush = array.push; array.push = function (content) { this._disableTracking = true; var ret = originalPush.call(this, content); this._disableTracking = false; return ret; }; var originalSlice = array.slice; array.slice = function (start, end) { this._disableTracking = true; var ret = originalSlice.call(this, start, end); this._disableTracking = false; return ret; }; // TODO: iterator return array; } var docsObserved$1 = new WeakSet(); // TODO: add atoms, etc function observeDoc(doc) { if (docsObserved$1.has(doc)) { // already patched return doc; } docsObserved$1.add(doc); var selfAtom; function reportSelfAtom() { if (!selfAtom) { var oldKeys = Array.from(doc.share.keys()); var handler = function handler(tr) { var newKeys = Array.from(doc.share.keys()); if (JSON.stringify(oldKeys) !== JSON.stringify(newKeys)) { oldKeys = newKeys; selfAtom.reportChanged(); } }; selfAtom = createAtom("map", function () { doc.on("beforeObserverCalls", handler); }, function () { doc.off("beforeObserverCalls", handler); }); } selfAtom.reportObserved(doc._implicitObserver); } var originalGet = doc.get; doc.get = function (key) { if (typeof key !== "string") { throw new Error("unexpected"); } var ret = Reflect.apply(originalGet, this, arguments); observeYJS(ret); return ret; }; function patch(method) { var originalFunction = doc[method]; var previous; doc[method] = function () { var ret; var args = arguments; reportSelfAtom(); if (previous) { previous.removeObservers(); // dispose } // we run this in a reaction, because the originalfunction might also trigger // observers in nested functions. In particular, if toJSON is called. previous = reaction(function () { ret = Reflect.apply(originalFunction, doc, args); return ret; }, function () { return selfAtom.reportChanged(); }); return ret; }; } patch("toJSON"); Object.defineProperty(doc, "keys", { get: function get() { reportSelfAtom(); return Object.keys(doc.share); } }); return doc; } var mapsObserved = new WeakSet(); function observeMap(map) { if (mapsObserved.has(map)) { // already patched return map; } mapsObserved.add(map); var selfAtom; var atoms = new Map(); function reportSelfAtom() { if (!selfAtom) { var handler = function handler(event) { if (event.changes.added.size || event.changes.deleted.size || event.changes.keys.size || event.changes.delta.length) { selfAtom.reportChanged(); } }; selfAtom = createAtom("map", function () { map.observe(handler); }, function () { map.unobserve(handler); }); } selfAtom.reportObserved(map._implicitObserver); } function reportMapKeyAtom(key) { var atom = atoms.get(key); // possible optimization: only register a single handler for all keys if (!atom) { var handler = function handler(event) { if (event.keysChanged.has(key)) { if (event.changes.added.size || event.changes.deleted.size || event.changes.keys.size || event.changes.delta.length) { atom.reportChanged(); } } }; atom = createAtom(key, function () { map.observe(handler); }, function () { map.unobserve(handler); }); atoms.set(key, atom); } atom.reportObserved(map._implicitObserver); } var originalGet = map.get; map.get = function (key) { if (typeof key !== "string") { throw new Error("unexpected"); } reportMapKeyAtom(key); var ret = Reflect.apply(originalGet, this, arguments); return ret; }; function patch(method) { var originalFunction = map[method]; map[method] = function () { reportSelfAtom(); var ret = Reflect.apply(originalFunction, this, arguments); return ret; }; } patch("values"); patch("entries"); patch("keys"); patch("forEach"); patch("toJSON"); // TODO: has, iterator return map; } var textsObserved = new WeakSet(); function observeText(value) { if (textsObserved.has(value)) { // already patched return value; } textsObserved.add(value); var atom; var handler = function handler(_changes) { atom.reportChanged(); }; atom = createAtom("text", function () { value.observe(handler); }, function () { value.unobserve(handler); }); function patch(method) { var originalFunction = value[method]; value[method] = function () { atom.reportObserved(this._implicitObserver); var ret = Reflect.apply(originalFunction, this, arguments); return ret; }; } patch("toString"); patch("toJSON"); return value; } var xmlsObserved = new WeakSet(); function observeXml(value) { if (xmlsObserved.has(value)) { // already patched return value; } xmlsObserved.add(value); var atom; var handler = function handler(event) { if (event.changes.added.size || event.changes.deleted.size || event.changes.keys.size || event.changes.delta.length) { atom.reportChanged(); } }; atom = createAtom("xml", function () { value.observe(handler); }, function () { value.unobserve(handler); }); function patch(method) { var originalFunction = value[method]; value[method] = function () { atom.reportObserved(this._implicitObserver); var ret = Reflect.apply(originalFunction, this, arguments); return ret; }; } function patchGetter(method) { var target = value; var descriptor = Object.getOwnPropertyDescriptor(target, method); // properties might be defined down the prototype chain (e.g., properties on XmlFragment when working on an XmlElement) if (!descriptor) { target = Object.getPrototypeOf(target); descriptor = Object.getOwnPropertyDescriptor(target, method); } if (!descriptor) { target = Object.getPrototypeOf(target); descriptor = Object.getOwnPropertyDescriptor(target, method); } if (!descriptor) { throw new Error("property not found"); } var originalFunction = descriptor.get; descriptor.get = function () { atom.reportObserved(this._implicitObserver); var ret = Reflect.apply(originalFunction, this, arguments); return ret; }; Object.defineProperty(value, method, descriptor); } patch("toString"); patch("toDOM"); patch("toArray"); patch("getAttribute"); patchGetter("firstChild"); return value; } function isYType(element) { return element instanceof Y.AbstractType || Object.prototype.hasOwnProperty.call(element, "autoLoad"); // detect subdocs. Is there a better way for this? } function observeYJS(element) { if (element instanceof Y.XmlText) { return observeText(element); } else if (element instanceof Y.Text) { return observeText(element); } else if (element instanceof Y.Array) { return observeArray(element); } else if (element instanceof Y.Map) { return observeMap(element); } else if (element instanceof Y.Doc || Object.prototype.hasOwnProperty.call(element, "autoLoad")) { // subdoc. Ok way to detect this? return observeDoc(element); } else if (element instanceof Y.XmlFragment) { return observeXml(element); } else if (element instanceof Y.XmlElement) { return observeXml(element); } else ; return element; } function makeYDocRootLevelTypesObservable(doc) { doc.share.forEach(function (type) { // the explicit check is necessary because we sometimes initialize "anonymous" types that the user can't (and shouldn't) access. if (type.constructor !== Y.AbstractType) { // console.log("root", type) observeYJS(type); } }); } function makeStructsObservable(structs, startPos) { for (var i = structs.length - 1; i >= startPos; i--) { var struct = structs[i]; if (!struct.deleted) { var _struct$content; if (struct instanceof Y.GC) { continue; } (_struct$content = struct.content) == null ? void 0 : _struct$content.getContent().forEach(function (content) { if (content instanceof Y.AbstractType) { // console.log("struct", content) observeYJS(content); // console.log(content, "is a created type type"); } }); } } } var docsObserved = new WeakSet(); function makeYDocObservable(doc) { if (docsObserved.has(doc)) { return; } docsObserved.add(doc); // based on https://github.com/yjs/yjs/pull/298#issuecomment-937636849 // hook new root type creations (when calling getMap() or getArray(), etc) observeYJS(doc); // observe all structs already in the document doc.store.clients.forEach(function (entry) { if (entry) { makeStructsObservable(entry, 0); } }); // observe all root-types makeYDocRootLevelTypesObservable(doc); // observe newly created types from now on doc.on("beforeObserverCalls", function (tr) { // observe new root types makeYDocRootLevelTypesObservable(doc); // observe new structs tr.afterState.forEach(function (clock, client) { var beforeClock = tr.beforeState.get(client) || 0; if (beforeClock !== clock) { var structs = tr.doc.store.clients.get(client); if (!structs) { return; } var firstChangePos = Y.findIndexSS(structs, beforeClock); makeStructsObservable(structs, firstChangePos); } }); }); } export { enableMobxBindings, enableReactiveBindings, enableVueBindings, isYType, makeYDocObservable, observeYJS }; //# sourceMappingURL=yjs-reactive-bindings.module.js.map