@syncedstore/yjs-reactive-bindings
Version:
A bridge between Reactive programming libraries (reactive, Vue or MobX) and Yjs
173 lines (148 loc) • 4.72 kB
text/typescript
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;
}