loro-crdt
Version:
Loro CRDTs is a high-performance CRDT framework that makes your app state synchronized, collaborative and maintainable effortlessly.
305 lines (302 loc) • 8.88 kB
JavaScript
export { default } from "./loro_wasm.js";
import { LoroDoc, AwarenessWasm, EphemeralStoreWasm } from "./loro_wasm.js";
export * from "./loro_wasm.js";
/**
* @deprecated Please use LoroDoc
*/
class Loro extends LoroDoc {
}
const CONTAINER_TYPES = [
"Map",
"Text",
"List",
"Tree",
"MovableList",
"Counter",
];
function isContainerId(s) {
return s.startsWith("cid:");
}
/** Whether the value is a container.
*
* # Example
*
* ```ts
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* const list = doc.getList("list");
* const text = doc.getText("text");
* isContainer(map); // true
* isContainer(list); // true
* isContainer(text); // true
* isContainer(123); // false
* isContainer("123"); // false
* isContainer({}); // false
* ```
*/
function isContainer(value) {
if (typeof value !== "object" || value == null) {
return false;
}
const p = Object.getPrototypeOf(value);
if (p == null || typeof p !== "object" || typeof p["kind"] !== "function") {
return false;
}
return CONTAINER_TYPES.includes(value.kind());
}
/** Get the type of a value that may be a container.
*
* # Example
*
* ```ts
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* const list = doc.getList("list");
* const text = doc.getText("text");
* getType(map); // "Map"
* getType(list); // "List"
* getType(text); // "Text"
* getType(123); // "Json"
* getType("123"); // "Json"
* getType({}); // "Json"
* ```
*/
function getType(value) {
if (isContainer(value)) {
return value.kind();
}
return "Json";
}
function newContainerID(id, type) {
return `cid:${id.counter}@${id.peer}:${type}`;
}
function newRootContainerID(name, type) {
return `cid:root-${name}:${type}`;
}
/**
* @deprecated Please use `EphemeralStore` instead.
*
* Awareness is a structure that allows to track the ephemeral state of the peers.
*
* If we don't receive a state update from a peer within the timeout, we will remove their state.
* The timeout is in milliseconds. This can be used to handle the offline state of a peer.
*/
class Awareness {
constructor(peer, timeout = 30000) {
this.listeners = new Set();
this.inner = new AwarenessWasm(peer, timeout);
this.peer = peer;
this.timeout = timeout;
}
apply(bytes, origin = "remote") {
const { updated, added } = this.inner.apply(bytes);
this.listeners.forEach((listener) => {
listener({ updated, added, removed: [] }, origin);
});
this.startTimerIfNotEmpty();
}
setLocalState(state) {
const wasEmpty = this.inner.getState(this.peer) == null;
this.inner.setLocalState(state);
if (wasEmpty) {
this.listeners.forEach((listener) => {
listener({ updated: [], added: [this.inner.peer()], removed: [] }, "local");
});
}
else {
this.listeners.forEach((listener) => {
listener({ updated: [this.inner.peer()], added: [], removed: [] }, "local");
});
}
this.startTimerIfNotEmpty();
}
getLocalState() {
return this.inner.getState(this.peer);
}
getAllStates() {
return this.inner.getAllStates();
}
encode(peers) {
return this.inner.encode(peers);
}
encodeAll() {
return this.inner.encodeAll();
}
addListener(listener) {
this.listeners.add(listener);
}
removeListener(listener) {
this.listeners.delete(listener);
}
peers() {
return this.inner.peers();
}
destroy() {
clearInterval(this.timer);
this.listeners.clear();
}
startTimerIfNotEmpty() {
if (this.inner.isEmpty() || this.timer != null) {
return;
}
this.timer = setInterval(() => {
const removed = this.inner.removeOutdated();
if (removed.length > 0) {
this.listeners.forEach((listener) => {
listener({ updated: [], added: [], removed }, "timeout");
});
}
if (this.inner.isEmpty()) {
clearInterval(this.timer);
this.timer = undefined;
}
}, this.timeout / 2);
}
}
/**
* EphemeralStore is a structure that allows to track the ephemeral state of the peers.
*
* It can be used to synchronize cursor positions, selections, and the names of the peers.
* Each entry uses timestamp-based LWW (Last-Write-Wins) for conflict resolution.
*
* If we don't receive a state update from a peer within the timeout, we will remove their state.
* The timeout is in milliseconds. This can be used to handle the offline state of a peer.
*
* @example
*
* ```ts
* const store = new EphemeralStore();
* const store2 = new EphemeralStore();
* // Subscribe to local updates
* store.subscribeLocalUpdates((data)=>{
* store2.apply(data);
* })
* // Subscribe to all updates
* store2.subscribe((event)=>{
* console.log("event: ", event);
* })
* // Set a value
* store.set("key", "value");
* // Encode the value
* const encoded = store.encode("key");
* // Apply the encoded value
* store2.apply(encoded);
* ```
*/
class EphemeralStore {
constructor(timeout = 30000) {
this.inner = new EphemeralStoreWasm(timeout);
this.timeout = timeout;
}
apply(bytes) {
this.inner.apply(bytes);
this.startTimerIfNotEmpty();
}
set(key, value) {
this.inner.set(key, value);
this.startTimerIfNotEmpty();
}
delete(key) {
this.inner.delete(key);
}
get(key) {
return this.inner.get(key);
}
getAllStates() {
return this.inner.getAllStates();
}
encode(key) {
return this.inner.encode(key);
}
encodeAll() {
return this.inner.encodeAll();
}
keys() {
return this.inner.keys();
}
destroy() {
clearInterval(this.timer);
}
subscribe(listener) {
return this.inner.subscribe(listener);
}
subscribeLocalUpdates(listener) {
return this.inner.subscribeLocalUpdates(listener);
}
startTimerIfNotEmpty() {
if (this.inner.isEmpty() || this.timer != null) {
return;
}
this.timer = setInterval(() => {
this.inner.removeOutdated();
if (this.inner.isEmpty()) {
clearInterval(this.timer);
this.timer = undefined;
}
}, this.timeout / 2);
}
}
LoroDoc.prototype.toJsonWithReplacer = function (replacer) {
const processed = new Set();
const doc = this;
const m = (key, value) => {
if (typeof value === "string") {
if (isContainerId(value) && !processed.has(value)) {
processed.add(value);
const container = doc.getContainerById(value);
if (container == null) {
throw new Error(`ContainerID not found: ${value}`);
}
const ans = replacer(key, container);
if (ans === container) {
const ans = container.getShallowValue();
if (typeof ans === "object") {
return run(ans);
}
return ans;
}
if (isContainer(ans)) {
throw new Error("Using new container is not allowed in toJsonWithReplacer");
}
if (typeof ans === "object" && ans != null) {
return run(ans);
}
return ans;
}
}
if (typeof value === "object" && value != null) {
return run(value);
}
const ans = replacer(key, value);
if (isContainer(ans)) {
throw new Error("Using new container is not allowed in toJsonWithReplacer");
}
return ans;
};
const run = (layer) => {
if (Array.isArray(layer)) {
return layer.map((item, index) => {
return m(index, item);
}).filter((item) => item !== undefined);
}
const result = {};
for (const [key, value] of Object.entries(layer)) {
const ans = m(key, value);
if (ans !== undefined) {
result[key] = ans;
}
}
return result;
};
const layer = doc.getShallowValue();
return run(layer);
};
function idStrToId(idStr) {
const [counter, peer] = idStr.split("@");
return {
counter: parseInt(counter),
peer: peer,
};
}
export { Awareness, EphemeralStore, Loro, getType, idStrToId, isContainer, isContainerId, newContainerID, newRootContainerID };
//# sourceMappingURL=index.js.map