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.

137 lines (124 loc) 3.87 kB
import * as reactive from "@reactivedata/reactive"; import { markRaw } from "@reactivedata/reactive"; import { enableReactiveBindings, makeYDocObservable } from "@syncedstore/yjs-reactive-bindings"; import * as Y from "yjs"; import { Array as SyncedArray, Doc as SyncedDoc, Map as SyncedMap, Text as SyncedText, XmlFragment as SyncedXml, } from "yjs"; import { crdtDoc, DocTypeDescription } from "./doc"; export { enableMobxBindings, enableVueBindings } from "@syncedstore/yjs-reactive-bindings"; export { Box, boxed } from "./boxed"; export * from "./util"; /** * @ignore */ export { Y }; /** * @ignore */ export { SyncedDoc, SyncedArray, SyncedMap, SyncedXml, SyncedText }; // setup yjs-reactive-bindings enableReactiveBindings(reactive); // use reactive bindings by default /** * @ignore */ export const INTERNAL_SYMBOL = Symbol("INTERNAL_SYMBOL"); /** * Register a listener for when any changes to `object` or its nested objects occur. * * @param object the synced object (store, object, map, or Yjs value to observe) * @param handler the callback to be raised. * @returns a function to dispose (unregister) the handler */ export function observeDeep(object: any, handler: () => void): () => void { const internal = getYjsValue(object) || object; if (!internal) { throw new Error("not a valid synced object"); } if (internal instanceof Y.Doc) { internal.on("update", handler); return () => { internal.off("update", handler); }; } else { internal.observeDeep(handler); return () => { internal.unobserveDeep(handler); }; } } /** * Access the internal Yjs Doc. * * @param store a store returned by * @returns the Yjs doc (Y.Doc) underneath. */ export function getYjsDoc<T>(store: T): Y.Doc { const ret = getYjsValue(store); if (!(ret instanceof Y.Doc)) { throw new Error("store is not a valid syncedStore that maps to a Y.Doc"); } return ret; } /** * Access the internal Yjs value that backs the syncing of the passed in object. * * @param object a value retrieved from the store * @returns the Yjs value underneath. This can be a Y.Doc, Y.Array, Y.Map or other Y-type based on the value passed in */ export function getYjsValue(object: any): Y.Doc | Y.AbstractType<any> | undefined { if (typeof object !== "object" || object === null) { return undefined; } const ret = object[INTERNAL_SYMBOL]; if (ret) { markRaw(ret); (ret as any).__v_skip = true; // for vue Reactive } return ret; } /** * Check whether two objects represent the same value. * A strict equality (===) check doesn't always work, * because SyncedStore can wrap the object with a Proxy depending on where you retrieved it. * * @param objectA Object to compare with objectB * @param objectB Object to compare with objectA * @returns true if they represent the same object, false otherwise */ export function areSame(objectA: any, objectB: any) { if (objectA === objectB) { return true; } if (typeof objectA === "object" && typeof objectB === "object") { const internalA = getYjsValue(objectA); const internalB = getYjsValue(objectB); if (!internalA || !internalB) { // one of them doesn't have an internal value return false; } return internalA === internalB; } return false; } /** * Create a SyncedStore store * @param shape an object that describes the root types of the store. e.g.: * const shape = { * exampleArrayData: [], * exampleObjectData: {}, * exampleXMLData: "xml", * exampleTextData: "text", * }; * @param doc (optional) a Y.Doc to use as the backing system * @returns a SyncedStore store */ export function syncedStore<T extends DocTypeDescription>(shape: T, doc: Y.Doc = new Y.Doc()) { makeYDocObservable(doc); return crdtDoc<T>(doc, shape); } export default syncedStore;