Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: assign arrays and root-elements directly #89

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 31 additions & 17 deletions packages/core/src/array.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { $reactive, $reactiveproxy } from "@reactivedata/reactive";
import { $reactive, $reactiveproxy, $skipreactive } from "@reactivedata/reactive";
import * as Y from "yjs";
import { areSame, getYjsValue, INTERNAL_SYMBOL } from ".";
import { Box } from "./boxed";
import { crdtValue, ObjectSchemaType, parseYjsReturnValue } from "./internal";
import { CRDTObject } from "./object";
import reconcile from "./reconcile";

export type CRDTArray<T> = {
[INTERNAL_SYMBOL]?: Y.Array<T>;
Expand All @@ -16,6 +17,20 @@ export type CRDTArray<T> = {
: T;
} & T[]; // TODO: should return ArrayImplementation<T> on getter

export const wrapItems = function wrapItems(items) {
return items.map((item) => {
const wrapped = crdtValue(item as any); // TODO
let valueToSet = getYjsValue(wrapped) || wrapped;
if (valueToSet instanceof Box) {
valueToSet = valueToSet.value;
}
if (valueToSet instanceof Y.AbstractType && valueToSet.parent) {
throw new Error("Not supported: reassigning object that already occurs in the tree.");
}
return valueToSet;
});
};

function arrayImplementation<T>(arr: Y.Array<T>) {
const slice = function slice() {
let ic = this[$reactiveproxy]?.implicitObserver;
Expand All @@ -27,20 +42,6 @@ function arrayImplementation<T>(arr: Y.Array<T>) {
});
} as T[]["slice"];

const wrapItems = function wrapItems(items) {
return items.map((item) => {
const wrapped = crdtValue(item as any); // TODO
let valueToSet = getYjsValue(wrapped) || wrapped;
if (valueToSet instanceof Box) {
valueToSet = valueToSet.value;
}
if (valueToSet instanceof Y.AbstractType && valueToSet.parent) {
throw new Error("Not supported: reassigning object that already occurs in the tree.");
}
return valueToSet;
});
};

const findIndex = function findIndex() {
return [].findIndex.apply(slice.apply(this), arguments);
} as T[]["find"];
Expand Down Expand Up @@ -158,8 +159,10 @@ export function crdtArray<T>(initializer: T[], arr = new Y.Array<T>()) {
if (typeof p !== "number") {
throw new Error();
}
// TODO map.set(p, smartValue(value));
throw new Error("array assignment is not implemented / supported");

reconcile(value, arr, p);

return value;
},
get: (target, pArg, receiver) => {
const p = propertyToNumber(pArg);
Expand Down Expand Up @@ -193,6 +196,17 @@ export function crdtArray<T>(initializer: T[], arr = new Y.Array<T>()) {
if (p === "length") {
return arr.length;
}

// proxy-trap to enable an idiomatic use of arrays and objects in Solid's Flow-component (without spreading)
if (typeof p === "symbol" && p !== $reactiveproxy && p !== $reactive && p !== $skipreactive) {
let ic = receiver[$reactiveproxy]?.implicitObserver;
// (arr as any)._implicitObserver = ic;
return arr.map((item) => {
const ret = parseYjsReturnValue(item, ic);
return ret;
});
}

// forward to arrayimplementation
const ret = Reflect.get(target, p, receiver);
return ret;
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/doc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { $reactive, $reactiveproxy } from "@reactivedata/reactive";
import * as Y from "yjs";
import { INTERNAL_SYMBOL } from ".";
import { wrapItems } from "./array";
import { parseYjsReturnValue, yToWrappedCache } from "./internal";
import reconcile from "./reconcile";

export type docElementTypeDescription = "xml" | "text" | Array<any> | object;
export type DocTypeDescription = {
Expand Down Expand Up @@ -70,7 +72,11 @@ export function crdtDoc<T extends DocTypeDescription>(doc: Y.Doc, typeDescriptio
if (typeof p !== "string") {
throw new Error();
}
throw new Error("cannot set new elements on root doc");

reconcile(value, doc, p);

return value;
// throw new Error("cannot set new elements on root doc");
},
get: (target, p, receiver) => {
if (p === INTERNAL_SYMBOL) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "yjs";
import { crdtDoc, DocTypeDescription } from "./doc";

export { enableMobxBindings, enableVueBindings } from "@syncedstore/yjs-reactive-bindings";
export { enableMobxBindings, enableVueBindings, enableSolidBindings } from "@syncedstore/yjs-reactive-bindings";
export { Box, boxed } from "./boxed";
export * from "./util";
/**
Expand Down
88 changes: 88 additions & 0 deletions packages/core/src/reconcile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as Y from "yjs";
import { wrapItems } from "./array";

export default function reconcile(target: any, parent: any, property: PropertyKey) {
const adjust = (target, parent, property) => {
if (parent instanceof Y.Array && typeof property === "number") {
if (parent.length > property) (parent as Y.Array<any>).delete(property as number);
parent.insert(property, wrapItems([target]));
} else if ((parent instanceof Y.Map || parent instanceof Y.Doc) && typeof property === "string") {
parent.set(property, target);
} else {
console.error("unexpected", target, parent, property);
}
};

const compare = (
target: any,
parent: Y.Map<any> | Y.Array<any> | Y.Doc,
property: PropertyKey,
path: (string | number)[]
) => {
if (!(parent instanceof Y.Map || parent instanceof Y.Array || parent instanceof Y.Doc)) {
console.error("unexpected", target, parent, property, path);
return;
}

if (!(property in parent.toJSON())) {
if (parent instanceof Y.Array && typeof property === "number") {
// insert undefineds in case an index is being set which is larger then the length of the array
const delta = property - parent.length;
const nulls = new Array(delta).fill(null);

parent.insert(parent.length, wrapItems([...nulls, target]));
} else {
adjust(target, parent, property);
}
return;
}

let yPrevious;

if ((parent instanceof Y.Map || parent instanceof Y.Doc) && typeof property === "string") {
yPrevious = parent.get(property);
} else if (parent instanceof Y.Array && typeof property === "number") {
yPrevious = parent.get(property);
} else {
console.error("unexpected", target, parent, property);
}

const previous = yPrevious instanceof Y.AbstractType ? yPrevious.toJSON() : yPrevious;

/* if (previous === undefined) {
if (parent instanceof Y.Array && typeof property === "number") {
// insert undefineds in case an index is being set which is larger then the length of the array
const delta = property - parent.length;
const nulls = new Array(delta).fill(null);

parent.insert(parent.length, wrapItems([...nulls, target]));
}
return;
} */

if (Array.isArray(target)) {
// remove excess elements
if (previous.length > target.length) yPrevious.delete(target.length, previous.length - target.length);

target.forEach((value, index) => {
compare(value, yPrevious, index, [...path, index]);
});
} else if (typeof target === "object") {
const targetKeys = Object.keys(target);
// remove excess keys
Object.keys(previous)
.filter((key) => targetKeys.indexOf(key) === -1)
.forEach((key) => {
yPrevious.delete(key);
});

Object.entries(target).forEach(([key, value]) => {
compare(value, yPrevious, key, [...path, key]);
});
} else if (target !== previous) {
adjust(target, parent, property);
}
return;
};
compare(target, parent, property, []);
}
7 changes: 6 additions & 1 deletion packages/yjs-reactive-bindings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,9 @@ export function makeYDocObservable(doc: Y.Doc) {
});
}

export { enableMobxBindings, enableReactiveBindings, enableVueBindings } from "./observableProvider";
export {
enableMobxBindings,
enableReactiveBindings,
enableVueBindings,
enableSolidBindings,
} from "./observableProvider";
25 changes: 25 additions & 0 deletions packages/yjs-reactive-bindings/src/observableProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,31 @@ export function enableVueBindings(vue: any) {
customReaction = undefined;
}

/**
* Enable Solid integration
*
* @param solid An instance of Solid, e.g. import * as solid from "solid/store";
*/
export function enableSolidBindings(solid: any) {
customCreateAtom = function (name: any, onBecomeObserved: any) {
let id = 0;
const data = solid.createMutable({ data: id });
const atom = {
reportObserved() {
return data.data as any as boolean;
},
reportChanged() {
data.data = ++id;
},
};
if (onBecomeObserved) {
onBecomeObserved();
}
return atom;
};
customReaction = undefined;
}

export function enableReactiveBindings(reactive: any) {
customCreateAtom = function (name, onBecomeObserved, onBecomeUnobserved) {
// TMP
Expand Down