UNPKG

@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.

133 lines (118 loc) 3.91 kB
import { $reactive, $reactiveproxy } from "@reactivedata/reactive"; import * as Y from "yjs"; import { INTERNAL_SYMBOL } from "."; import { parseYjsReturnValue, yToWrappedCache } from "./internal"; export type docElementTypeDescription = "xml" | "text" | Array<any> | object; export type DocTypeDescription = { [key: string]: docElementTypeDescription; }; export type MappedTypeDescription<T extends DocTypeDescription> = { readonly [P in keyof T]: T[P] extends "xml" ? Y.XmlFragment : T[P] extends "text" ? Y.Text : T[P] extends Array<any> ? T[P] : T[P] extends object ? Partial<T[P]> : never; }; // export type rootTypeDescription<T extends rootTypeDescriptionParent> = { // [P in keyof T]?: T[P]; // }; function validateRootTypeDescription<T extends DocTypeDescription>(typeDescription: T) { for (let [key, val] of Object.entries(typeDescription)) { if (Array.isArray(val)) { if (val.length !== 0) { throw new Error("Root Array initializer must always be empty array"); } } else if (val && typeof val === "object") { if (Object.keys(val).length !== 0 || Object.getPrototypeOf(val) !== Object.prototype) { throw new Error("Root Object initializer must always be {}"); } } else if (val !== "xml" && val !== "text") { throw new Error("unknown Root initializer"); } } } function getYjsByTypeDescription<T extends DocTypeDescription>(doc: Y.Doc, typeDescription: T, p: string) { let description = typeDescription[p]; if (!description) { // exclude expected Vue Reactive checks from logging a warning if (p !== "__v_raw" && p !== "__v_isRef" && p !== "__v_isReadonly") { console.warn("property not found on root doc", p); } return undefined; } return description === "xml" ? doc.getXmlFragment(p) : description === "text" ? doc.getText(p) : Array.isArray(description) ? doc.getArray(p) : doc.getMap(p); } export function crdtDoc<T extends DocTypeDescription>(doc: Y.Doc, typeDescription: T) { if (doc[$reactive]) { throw new Error("unexpected"); } validateRootTypeDescription(typeDescription); const proxy = new Proxy({} as MappedTypeDescription<T>, { set: (target, p, value) => { if (typeof p !== "string") { throw new Error(); } throw new Error("cannot set new elements on root doc"); }, get: (target, p, receiver) => { if (p === INTERNAL_SYMBOL) { return doc; } 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; (doc as any)._implicitObserver = ic; } else { // console.warn("no receiver getting property", p); } if (p === "toJSON") { for (let key of Object.keys(typeDescription)) { // initialize all values getYjsByTypeDescription(doc, typeDescription, key); } const ret = Reflect.get(doc, p); return ret; } let ret = getYjsByTypeDescription(doc, typeDescription, p); ret = parseYjsReturnValue(ret, ic); return ret; }, deleteProperty: (target, p) => { throw new Error("deleteProperty not available for doc"); }, has: (target, p) => { if (typeof p === "string" && doc.share.has(p)) { return true; } return false; }, getOwnPropertyDescriptor(target, p) { if ((typeof p === "string" && doc.share.has(p)) || p === "toJSON") { return { enumerable: true, configurable: true, }; } return undefined; }, ownKeys: (target) => { return Array.from(doc.share.keys()); }, }); yToWrappedCache.set(doc, proxy); return proxy; }