@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.
619 lines (602 loc) • 19.3 kB
JavaScript
import * as reactive from '@reactivedata/reactive';
import { $reactive, $reactiveproxy, reactive as reactive$1, markRaw } from '@reactivedata/reactive';
import { enableReactiveBindings, makeYDocObservable } from '@syncedstore/yjs-reactive-bindings';
export { enableMobxBindings, enableVueBindings } from '@syncedstore/yjs-reactive-bindings';
import * as Y from 'yjs';
export { Y };
export { Array as SyncedArray, Doc as SyncedDoc, Map as SyncedMap, Text as SyncedText, XmlFragment as SyncedXml } from 'yjs';
/**
* @ignore
*/
var Box = function Box(value) {
this.value = void 0;
this.value = value;
};
function boxed(value) {
if (ArrayBuffer.isView(value)) {
// can't freeze arraybuffer
return new Box(value);
} else {
return new Box(Object.freeze(value));
}
}
function arrayImplementation(arr) {
var slice = function slice() {
var _this$$reactiveproxy;
var ic = (_this$$reactiveproxy = this[$reactiveproxy]) == null ? void 0 : _this$$reactiveproxy.implicitObserver;
arr._implicitObserver = ic;
var items = arr.slice.bind(arr).apply(arr, arguments);
return items.map(function (item) {
var ret = parseYjsReturnValue(item, ic);
if (ic && typeof ret === "object") {
// when using Reactive, we should make sure the returned
// object is made reactive with the implicit observer ic
return reactive$1(ret, ic);
} else {
return ret;
}
});
};
var wrapItems = function wrapItems(items) {
return items.map(function (item) {
var wrapped = crdtValue(item); // TODO
var 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;
});
};
var findIndex = function findIndex() {
return [].findIndex.apply(slice.apply(this), arguments);
};
var methods = {
// get length() {
// return arr.length;
// },
// set length(val: number) {
// throw new Error("set length of yjs array is unsupported");
// },
slice: slice,
unshift: function unshift() {
arr.unshift(wrapItems([].slice.call(arguments)));
return arr.lengthUntracked;
},
push: function push() {
arr.push(wrapItems([].slice.call(arguments)));
return arr.lengthUntracked;
},
insert: arr.insert.bind(arr),
toJSON: arr.toJSON.bind(arr),
forEach: function forEach() {
return [].forEach.apply(slice.apply(this), arguments);
},
every: function every() {
return [].every.apply(slice.apply(this), arguments);
},
filter: function filter() {
return [].filter.apply(slice.apply(this), arguments);
},
find: function find() {
return [].find.apply(slice.apply(this), arguments);
},
findIndex: findIndex,
some: function some() {
return [].some.apply(slice.apply(this), arguments);
},
includes: function includes() {
return [].includes.apply(slice.apply(this), arguments);
},
map: function map() {
return [].map.apply(slice.apply(this), arguments);
},
indexOf: function indexOf() {
var arg = arguments[0];
return findIndex.call(this, function (el) {
return areSame(el, arg);
});
},
splice: function splice() {
var start = arguments[0] < 0 ? arr.length - Math.abs(arguments[0]) : arguments[0];
var deleteCount = arguments[1];
var items = Array.from(Array.from(arguments).slice(2));
var deleted = slice.apply(this, [start, Number.isInteger(deleteCount) ? start + deleteCount : undefined]);
if (arr.doc) {
arr.doc.transact(function () {
arr.delete(start, deleteCount);
arr.insert(start, wrapItems(items));
});
} else {
arr.delete(start, deleteCount);
arr.insert(start, wrapItems(items));
}
return deleted;
}
// toJSON = () => {
// return this.arr.toJSON() slice();
// };
// delete = this.arr.delete.bind(this.arr) as (Y.Array<T>)["delete"];
};
var ret = [];
for (var method in methods) {
ret[method] = methods[method];
}
// this is necessary to prevent errors like "trap reported non-configurability for property 'length' which is either non-existent or configurable in the proxy target" when adding support for ownKeys and Reflect.keysx
// (not necessary anymore now we changed ret from object to array)
// Object.defineProperty(ret, "length", {
// enumerable: false,
// configurable: false,
// writable: true,
// value: (arr as any).lengthUntracked,
// });
return ret;
}
function propertyToNumber(p) {
if (typeof p === "string" && p.trim().length) {
var asNum = Number(p);
// https://stackoverflow.com/questions/10834796/validate-that-a-string-is-a-positive-integer
if (Number.isInteger(asNum)) {
return asNum;
}
}
return p;
}
function crdtArray(initializer, arr) {
if (arr === void 0) {
arr = new Y.Array();
}
if (arr[$reactive]) {
throw new Error("unexpected");
// arr = arr[$reactive].raw;
}
var implementation = arrayImplementation(arr);
var proxy = new Proxy(implementation, {
set: function set(target, pArg, value) {
var p = propertyToNumber(pArg);
if (typeof p !== "number") {
throw new Error();
}
// TODO map.set(p, smartValue(value));
throw new Error("array assignment is not implemented / supported");
},
get: function get(target, pArg, receiver) {
var p = propertyToNumber(pArg);
if (p === INTERNAL_SYMBOL) {
return arr;
}
if (typeof p === "number") {
var ic;
if (receiver && receiver[$reactiveproxy]) {
var _receiver$$reactivepr;
ic = (_receiver$$reactivepr = receiver[$reactiveproxy]) == null ? void 0 : _receiver$$reactivepr.implicitObserver;
arr._implicitObserver = ic;
}
var _ret = arr.get(p);
_ret = parseYjsReturnValue(_ret, ic);
return _ret;
}
if (p === Symbol.toStringTag) {
return "Array";
}
if (p === Symbol.iterator) {
var values = arr.slice();
return Reflect.get(values, p);
}
if (p === "length") {
return arr.length;
}
// forward to arrayimplementation
var ret = Reflect.get(target, p, receiver);
return ret;
},
// getOwnPropertyDescriptor: (target, pArg) => {
// const p = propertyToNumber(pArg);
// if (typeof p === "number" && p < arr.length && p >= 0) {
// return { configurable: true, enumerable: true, value: arr.get(p) };
// } else {
// return undefined;
// }
// },
deleteProperty: function deleteProperty(target, pArg) {
var p = propertyToNumber(pArg);
if (typeof p !== "number") {
throw new Error();
}
if (p < arr.lengthUntracked && p >= 0) {
arr.delete(p);
return true;
} else {
return false;
}
},
has: function has(target, pArg) {
var p = propertyToNumber(pArg);
if (typeof p !== "number") {
// forward to arrayimplementation
return Reflect.has(target, p);
}
if (p < arr.lengthUntracked && p >= 0) {
return true;
} else {
return false;
}
},
getOwnPropertyDescriptor: function getOwnPropertyDescriptor(target, pArg) {
var p = propertyToNumber(pArg);
if (p === "length") {
return {
enumerable: false,
configurable: false,
writable: true
};
}
if (typeof p === "number" && p >= 0 && p < arr.lengthUntracked) {
return {
enumerable: true,
configurable: true,
writable: true
};
}
return undefined;
},
ownKeys: function ownKeys(target) {
var keys = [];
for (var i = 0; i < arr.length; i++) {
keys.push(i + "");
}
keys.push("length");
return keys;
}
});
implementation.push.apply(proxy, initializer);
return proxy;
}
function crdtObject(initializer, map) {
if (map === void 0) {
map = new Y.Map();
}
if (map[$reactive]) {
throw new Error("unexpected");
// map = map[$reactive].raw;
}
var proxy = new Proxy({}, {
set: function set(target, p, value) {
if (typeof p !== "string") {
throw new Error();
}
var wrapped = crdtValue(value); // TODO: maybe set cache
var 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: function 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");
}
var ic;
if (receiver && receiver[$reactiveproxy]) {
var _receiver$$reactivepr;
ic = (_receiver$$reactivepr = receiver[$reactiveproxy]) == null ? void 0 : _receiver$$reactivepr.implicitObserver;
map._implicitObserver = ic;
}
var ret = map.get(p);
ret = parseYjsReturnValue(ret, ic);
return ret;
},
deleteProperty: function deleteProperty(target, p) {
if (typeof p !== "string") {
throw new Error();
}
if (map.has(p)) {
map.delete(p);
return true;
} else {
return false;
}
},
has: function has(target, p) {
if (typeof p === "string" && map.has(p)) {
return true;
}
return false;
},
getOwnPropertyDescriptor: function getOwnPropertyDescriptor(target, p) {
if (typeof p === "string" && map.has(p)) {
return {
enumerable: true,
configurable: true
};
}
return undefined;
},
ownKeys: function ownKeys(target) {
return Array.from(map.keys());
}
});
yToWrappedCache.set(map, proxy);
for (var key in initializer) {
proxy[key] = initializer[key];
}
return proxy;
}
function isYType(element) {
return element instanceof Y.AbstractType;
}
var yToWrappedCache = new WeakMap();
function parseYjsReturnValue(value, implicitObserver) {
if (isYType(value)) {
value._implicitObserver = implicitObserver;
if (value instanceof Y.Array || value instanceof Y.Map) {
if (!yToWrappedCache.has(value)) {
var wrapped = crdtValue(value);
yToWrappedCache.set(value, wrapped);
}
value = yToWrappedCache.get(value);
} else if (value instanceof Y.XmlElement || value instanceof Y.XmlFragment || value instanceof Y.XmlText || value instanceof Y.XmlHook || value instanceof Y.Text) {
markRaw(value);
value.__v_skip = true; // for vue Reactive
} else {
throw new Error("unknown YType");
}
return value;
} else if (value === null) {
return null;
} else if (typeof value === "object") {
return boxed(value); // TODO: how do we recognize a boxed "null" value?
}
return value;
}
function crdtValue(value) {
if (value === null || value === undefined) {
return value;
}
value = getYjsValue(value) || value; // unwrap
if (value instanceof Y.Array) {
return crdtArray([], value);
} else if (value instanceof Y.Map) {
return crdtObject({}, value);
} else if (typeof value === "string") {
return value; // TODO
} else if (Array.isArray(value)) {
return crdtArray(value);
} else if (value instanceof Y.XmlElement || value instanceof Y.XmlFragment || value instanceof Y.XmlText || value instanceof Y.XmlHook) {
return value;
} else if (value instanceof Y.Text) {
return value;
} else if (typeof value === "object") {
if (value instanceof Box) {
return value;
} else {
return crdtObject(value);
}
} else if (typeof value === "number" || typeof value === "boolean") {
return value;
} else {
throw new Error("invalid");
}
}
// export type rootTypeDescription<T extends rootTypeDescriptionParent> = {
// [P in keyof T]?: T[P];
// };
function validateRootTypeDescription(typeDescription) {
for (var _i = 0, _Object$entries = Object.entries(typeDescription); _i < _Object$entries.length; _i++) {
var _Object$entries$_i = _Object$entries[_i],
val = _Object$entries$_i[1];
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(doc, typeDescription, p) {
var 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);
}
function crdtDoc(doc, typeDescription) {
if (doc[$reactive]) {
throw new Error("unexpected");
}
validateRootTypeDescription(typeDescription);
var proxy = new Proxy({}, {
set: function set(target, p, value) {
if (typeof p !== "string") {
throw new Error();
}
throw new Error("cannot set new elements on root doc");
},
get: function 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");
}
var ic;
if (receiver && receiver[$reactiveproxy]) {
var _receiver$$reactivepr;
ic = (_receiver$$reactivepr = receiver[$reactiveproxy]) == null ? void 0 : _receiver$$reactivepr.implicitObserver;
doc._implicitObserver = ic;
}
if (p === "toJSON") {
for (var _i2 = 0, _Object$keys = Object.keys(typeDescription); _i2 < _Object$keys.length; _i2++) {
var key = _Object$keys[_i2];
// initialize all values
getYjsByTypeDescription(doc, typeDescription, key);
}
var _ret = Reflect.get(doc, p);
return _ret;
}
var ret = getYjsByTypeDescription(doc, typeDescription, p);
ret = parseYjsReturnValue(ret, ic);
return ret;
},
deleteProperty: function deleteProperty(target, p) {
throw new Error("deleteProperty not available for doc");
},
has: function has(target, p) {
if (typeof p === "string" && doc.share.has(p)) {
return true;
}
return false;
},
getOwnPropertyDescriptor: function getOwnPropertyDescriptor(target, p) {
if (typeof p === "string" && doc.share.has(p) || p === "toJSON") {
return {
enumerable: true,
configurable: true
};
}
return undefined;
},
ownKeys: function ownKeys(target) {
return Array.from(doc.share.keys());
}
});
yToWrappedCache.set(doc, proxy);
return proxy;
}
/**
* Filter a SyncedStore array
* @param arr array to filter
* @param filter predicate to filter the array `arr` by
*/
function filterArray(arr, filter) {
for (var i = arr.length - 1; i >= 0; i--) {
if (!filter(arr[i])) {
arr.splice(i, 1);
}
}
}
// setup yjs-reactive-bindings
enableReactiveBindings(reactive); // use reactive bindings by default
/**
* @ignore
*/
var 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
*/
function observeDeep(object, handler) {
var internal = getYjsValue(object) || object;
if (!internal) {
throw new Error("not a valid synced object");
}
if (internal instanceof Y.Doc) {
internal.on("update", handler);
return function () {
internal.off("update", handler);
};
} else {
internal.observeDeep(handler);
return function () {
internal.unobserveDeep(handler);
};
}
}
/**
* Access the internal Yjs Doc.
*
* @param store a store returned by
* @returns the Yjs doc (Y.Doc) underneath.
*/
function getYjsDoc(store) {
var 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
*/
function getYjsValue(object) {
if (typeof object !== "object" || object === null) {
return undefined;
}
var ret = object[INTERNAL_SYMBOL];
if (ret) {
markRaw(ret);
ret.__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
*/
function areSame(objectA, objectB) {
if (objectA === objectB) {
return true;
}
if (typeof objectA === "object" && typeof objectB === "object") {
var internalA = getYjsValue(objectA);
var 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
*/
function syncedStore(shape, doc) {
if (doc === void 0) {
doc = new Y.Doc();
}
makeYDocObservable(doc);
return crdtDoc(doc, shape);
}
export { Box, INTERNAL_SYMBOL, areSame, boxed, syncedStore as default, filterArray, getYjsDoc, getYjsValue, observeDeep, syncedStore };
//# sourceMappingURL=syncedstore.module.js.map