UNPKG

@syncedstore/yjs-reactive-bindings

Version:

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

173 lines (148 loc) 4.72 kB
import * as Y from "yjs"; import { Atom, createAtom } from "../observableProvider"; const arraysObserved = new WeakSet<Y.Array<any>>(); export function observeArray(array: Y.Array<any>) { if (arraysObserved.has(array)) { // already patched return array; } arraysObserved.add(array); let selfAtom: Atom | undefined; const atoms = new Map<number, Atom>(); function reportSelfAtom() { if (!selfAtom) { const handler = (event: Y.YArrayEvent<any>) => { if ( event.changes.added.size || event.changes.deleted.size || event.changes.keys.size || event.changes.delta.length ) { selfAtom!.reportChanged(); } }; selfAtom = createAtom( "map", () => { array.observe(handler); }, () => { array.unobserve(handler); } ); } selfAtom.reportObserved((array as any)._implicitObserver); } function reportArrayElementAtom(key: number) { let atom = atoms.get(key); // possible optimization: only register a single handler for all keys if (!atom) { const handler = (event: Y.YArrayEvent<any>) => { // 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 + "", () => { array.observe(handler); }, () => { array.unobserve(handler); } ); atoms.set(key, atom); } atom.reportObserved((array as any)._implicitObserver); } const originalGet = array.get; array.get = function (key: number) { if (typeof key !== "number") { throw new Error("unexpected"); } reportArrayElementAtom(key); const ret = Reflect.apply(originalGet, this, arguments); return ret; }; function patch(method: string) { const originalFunction = array[method]; array[method] = function () { reportSelfAtom(); const ret = Reflect.apply(originalFunction, this, arguments); return ret; }; } function patchGetter(method: string) { let target = array; let 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"); } const originalFunction = descriptor.get!; descriptor.get = function () { if (!this._disableTracking) { reportSelfAtom(); } const ret = Reflect.apply(originalFunction, this, arguments); return ret; }; Object.defineProperty(array, method, descriptor); } function copyGetter(method: string, newMethodName: string) { let target = array; let 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 const originalPush = array.push; array.push = function (this: any, content: any) { this._disableTracking = true; const ret = originalPush.call(this, content); this._disableTracking = false; return ret; }; const originalSlice = array.slice; array.slice = function (this: any, start: any, end: any) { this._disableTracking = true; const ret = originalSlice.call(this, start, end); this._disableTracking = false; return ret; }; // TODO: iterator return array; }