@syncedstore/core
Version:
SyncedStore is an easy-to-use library for building collaborative applications that sync automatically. It's built on top of Yjs, a proven, high performance CRDT implementation.
104 lines (93 loc) • 2.75 kB
text/typescript
import { $reactive, $reactiveproxy } from "@reactivedata/reactive";
import * as Y from "yjs";
import { getYjsValue, INTERNAL_SYMBOL } from ".";
import { CRDTArray } from "./array";
import { Box } from "./boxed";
import { crdtValue, parseYjsReturnValue, yToWrappedCache, ObjectSchemaType } from "./internal";
export type CRDTObject<T extends ObjectSchemaType> = {
[P in keyof T]?: T[P] extends Box<infer A>
? Box<A>
: T[P] extends Array<infer A>
? CRDTArray<A>
: T[P] extends ObjectSchemaType
? CRDTObject<T[P]>
: T[P];
} & {
[INTERNAL_SYMBOL]?: Y.Map<T>;
};
export function crdtObject<T extends ObjectSchemaType>(initializer: T, map = new Y.Map<any>()) {
if (map[$reactive]) {
throw new Error("unexpected");
// map = map[$reactive].raw;
}
const proxy = new Proxy({} as any as CRDTObject<T>, {
set: (target, p, value) => {
if (typeof p !== "string") {
throw new Error();
}
const wrapped = crdtValue(value); // TODO: maybe set cache
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.");
}
map.set(p, valueToSet);
return true;
},
get: (target, p, receiver) => {
if (p === INTERNAL_SYMBOL) {
return map;
}
if (typeof p !== "string") {
return Reflect.get(target, p);
// throw new Error("get non string parameter");
}
let ic: any;
if (receiver && receiver[$reactiveproxy]) {
ic = receiver[$reactiveproxy]?.implicitObserver;
(map as any)._implicitObserver = ic;
} else {
// console.warn("no receiver getting property", p);
}
let ret = map.get(p);
ret = parseYjsReturnValue(ret, ic);
return ret;
},
deleteProperty: (target, p) => {
if (typeof p !== "string") {
throw new Error();
}
if (map.has(p)) {
map.delete(p);
return true;
} else {
return false;
}
},
has: (target, p) => {
if (typeof p === "string" && map.has(p)) {
return true;
}
return false;
},
getOwnPropertyDescriptor(target, p) {
if (typeof p === "string" && map.has(p)) {
return {
enumerable: true,
configurable: true,
};
}
return undefined;
},
ownKeys: (target) => {
return Array.from(map.keys());
},
});
yToWrappedCache.set(map, proxy);
for (let key in initializer) {
proxy[key] = initializer[key] as any;
}
return proxy;
}