@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
text/typescript
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;