zustand-yjs
Version:
Zustand Stores with Yjs
278 lines (234 loc) • 8.64 kB
JavaScript
import create from 'zustand';
import { useEffect, useMemo, useCallback } from 'react';
import shallow from 'zustand/shallow';
import * as Y from 'yjs';
var YDocEnum;
(function (YDocEnum) {
YDocEnum[YDocEnum["GUID"] = 0] = "GUID";
YDocEnum[YDocEnum["DOC"] = 1] = "DOC";
YDocEnum[YDocEnum["MOUNT"] = 2] = "MOUNT";
YDocEnum[YDocEnum["UN_MOUNT"] = 3] = "UN_MOUNT";
YDocEnum[YDocEnum["USE_CNT"] = 4] = "USE_CNT";
})(YDocEnum || (YDocEnum = {}));
var ObserveEnum;
(function (ObserveEnum) {
ObserveEnum[ObserveEnum["TYPE"] = 0] = "TYPE";
ObserveEnum[ObserveEnum["OBSERVE"] = 1] = "OBSERVE";
ObserveEnum[ObserveEnum["USE_CNT"] = 2] = "USE_CNT";
})(ObserveEnum || (ObserveEnum = {}));
const newStartAwareness = setAwareness => {
const startAwareness = provider => {
const awareness = provider.awareness;
const updateAwareness = () => {
setAwareness(provider, Array.from(awareness.getStates().values()));
};
updateAwareness();
awareness.on('update', updateAwareness);
return () => {
awareness.off('update', updateAwareness);
};
};
return startAwareness;
};
const useYStore = create((set, get) => {
return {
yDocs: [],
yAwareness: [],
observers: [],
data: [],
update(type, data) {
const dataSet = get().data;
set({
data: [...dataSet.filter(([t]) => t !== type), [type, data]]
});
},
listenType(type, listen) {
const observers = get().observers;
const match = observers.find(([observer]) => observer === type);
if (match) {
set({
observers: [...observers.filter(obs => obs !== match), [match[ObserveEnum.TYPE], match[ObserveEnum.OBSERVE], match[ObserveEnum.USE_CNT] + 1]]
});
} else {
type.observe(listen);
set({
observers: [...observers, [type, listen, 1]]
});
}
},
unListenType(type) {
const observers = get().observers;
const match = observers.find(([t]) => t === type);
if (!match) {
console.warn('Type ', type, " wasn't mounted");
return;
}
const observerCount = match[ObserveEnum.USE_CNT] - 1;
if (observerCount > 0) {
set({
observers: [...observers.filter(obs => obs !== match), [match[ObserveEnum.TYPE], match[ObserveEnum.OBSERVE], observerCount]]
});
} else {
type.unobserve(match[ObserveEnum.OBSERVE]);
set({
observers: [...observers.filter(obs => obs !== match)],
data: [...get().data.filter(([t]) => t !== type)]
});
}
},
unMountYDoc(yDoc) {
const yDocs = get().yDocs;
const yAwareness = get().yAwareness;
const match = yDocs.find(([, doc]) => doc === yDoc);
if (!match) {
console.warn('Can not unmount doc ', yDoc.guid, ' . Doc not found');
return;
}
const observerCount = match[YDocEnum.USE_CNT] - 1;
if (observerCount > 0) {
set({
yDocs: [...yDocs.filter(yDoc => yDoc !== match), [match[YDocEnum.GUID], match[YDocEnum.DOC], match[YDocEnum.MOUNT], match[YDocEnum.UN_MOUNT], observerCount]]
});
} else {
const unMount = match[YDocEnum.UN_MOUNT];
if (unMount) unMount();
set({
yDocs: yDocs.filter(yDoc => yDoc !== match),
yAwareness: yAwareness.filter(([yDocName]) => yDocName !== match[0])
});
}
},
mountYDoc(yDoc, mount) {
const yDocs = get().yDocs;
const yAwareness = get().yAwareness;
const match = yDocs.find(([name]) => name === yDoc.guid);
if (match) {
if (match[YDocEnum.MOUNT] !== mount) {
console.warn('A different connection function was already provided for ', match[YDocEnum.GUID], '. This unMount function will be ignored');
}
match[YDocEnum.USE_CNT]++;
set({
yDocs: [...yDocs.filter(([name]) => name !== yDoc.guid), match]
});
} else {
const unMount = mount(yDoc, newStartAwareness((protocol, changes) => {
set({
yAwareness: [...yAwareness, [yDoc.guid, protocol.awareness, changes]]
});
}));
set({
yDocs: [...yDocs, [yDoc.guid, yDoc, mount, unMount, 1]]
});
}
}
};
});
var useYStore$1 = useYStore;
const useYObserve = (yType, serialize) => {
const [listen, unListen, update] = useYStore$1(state => [state.listenType, state.unListenType, state.update], shallow);
useEffect(() => {
listen(yType, () => update(yType, serialize()));
update(yType, serialize());
return () => unListen(yType);
}, []);
};
var useYObserve$1 = useYObserve;
const useYArray = yArray => {
useYObserve$1(yArray, () => yArray.toArray());
const dataSet = useYStore$1(state => state.data);
const data = useMemo(() => {
const match = dataSet.find(([type]) => type === yArray);
if (!match) return [];
return match[1];
}, [yArray, dataSet]);
const noBinding = funcKey => () => {
throw new Error(`Y.Array#${funcKey.toString()} is not implemented`);
};
return {
forEach: yArray.forEach ? yArray.forEach.bind(yArray) : noBinding('forEach'),
map: yArray.map ? yArray.map.bind(yArray) : noBinding('map'),
slice: yArray.slice ? yArray.slice.bind(yArray) : noBinding('slice'),
get: yArray.get ? yArray.get.bind(yArray) : noBinding('get'),
delete: yArray.delete ? yArray.delete.bind(yArray) : noBinding('delete'),
unshift: yArray.unshift ? yArray.unshift.bind(yArray) : noBinding('unshift'),
push: yArray.push ? yArray.push.bind(yArray) : noBinding('push'),
insert: yArray.insert ? yArray.insert.bind(yArray) : noBinding('insert'),
data
};
};
var useYArray$1 = useYArray;
const useYMap = yMap => {
useYObserve$1(yMap, () => yMap.toJSON());
const dataSet = useYStore$1(state => state.data);
const data = useMemo(() => {
const match = dataSet.find(([type]) => type === yMap);
if (!match) return {};
return match[1];
}, [yMap, dataSet]);
const noBinding = funcKey => () => {
throw new Error(`Y.Map#${funcKey.toString()} is not implemented`);
};
return {
keys: yMap.keys ? yMap.keys.bind(yMap) : noBinding('keys'),
values: yMap.values ? yMap.values.bind(yMap) : noBinding('values'),
entries: yMap.entries ? yMap.entries.bind(yMap) : noBinding('entries'),
forEach: yMap.forEach ? yMap.forEach.bind(yMap) : noBinding('forEach'),
delete: yMap.delete ? yMap.delete.bind(yMap) : noBinding('delete'),
set: yMap.set ? yMap.set.bind(yMap) : noBinding('set'),
get: yMap.get ? yMap.get.bind(yMap) : noBinding('get'),
has: yMap.has ? yMap.has.bind(yMap) : noBinding('has'),
data
};
};
var useYMap$1 = useYMap;
const useYDoc = (name, mount) => {
const [mountYDoc, unMountYDoc, yDocs] = useYStore$1(state => [state.mountYDoc, state.unMountYDoc, state.yDocs], shallow);
const yDoc = useMemo(() => {
const match = yDocs.find(([docGuid]) => docGuid === name);
if (match) return match[YDocEnum.DOC];
const yDoc = new Y.Doc();
yDoc.guid = name;
mountYDoc(yDoc, mount);
return yDoc;
}, [yDocs, name]);
useEffect(() => {
return () => unMountYDoc(yDoc);
}, [unMountYDoc, name]);
return yDoc;
};
var useYDoc$1 = useYDoc;
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
const useYAwareness = yDoc => {
const matches = useYStore$1(useCallback(state => state.yAwareness.filter(([guid]) => guid === yDoc.guid), [yDoc.guid]));
const [awarenessData, awareness] = useMemo(() => {
if (matches.length === 0) return [[], {
error: 'Awareness not registered. Use startAwareness method from your YDoc connect function.'
}];
const [_docGuid, awareness, changes] = matches[0];
return [changes, awareness];
}, [matches]);
const awarenessSetData = useCallback(newState => {
if (awareness.error) {
console.error(awareness.error);
} else {
const previousState = awareness.getLocalState();
awareness.setLocalState(_extends({}, previousState, newState));
}
}, [awareness]);
return [awarenessData, awarenessSetData];
};
var useYAwareness$1 = useYAwareness;
export { ObserveEnum, YDocEnum, useYArray$1 as useYArray, useYAwareness$1 as useYAwareness, useYDoc$1 as useYDoc, useYMap$1 as useYMap };