export * from "loro-wasm"; export type * from "loro-wasm"; import { AwarenessWasm, PeerID, Container, ContainerID, ContainerType, LoroCounter, LoroDoc, LoroList, LoroMap, LoroText, LoroTree, OpId, Value, AwarenessListener, } from "loro-wasm"; /** * @deprecated Please use LoroDoc */ export class Loro extends LoroDoc { } const CONTAINER_TYPES = [ "Map", "Text", "List", "Tree", "MovableList", "Counter", ]; export function isContainerId(s: string): s is ContainerID { 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 * ``` */ export function isContainer(value: any): value is Container { 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" * ``` */ export function getType( value: T, ): T extends LoroText ? "Text" : T extends LoroMap ? "Map" : T extends LoroTree ? "Tree" : T extends LoroList ? "List" : T extends LoroCounter ? "Counter" : "Json" { if (isContainer(value)) { return value.kind() as unknown as any; } return "Json" as any; } export function newContainerID(id: OpId, type: ContainerType): ContainerID { return `cid:${id.counter}@${id.peer}:${type}`; } export function newRootContainerID( name: string, type: ContainerType, ): ContainerID { return `cid:root-${name}:${type}`; } /** * 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 off-line state of a peer. */ export class Awareness { inner: AwarenessWasm; private peer: PeerID; private timer: number | undefined; private timeout: number; private listeners: Set = new Set(); constructor(peer: PeerID, timeout: number = 30000) { this.inner = new AwarenessWasm(peer, timeout); this.peer = peer; this.timeout = timeout; } apply(bytes: Uint8Array, origin = "remote") { const { updated, added } = this.inner.apply(bytes); this.listeners.forEach((listener) => { listener({ updated, added, removed: [] }, origin); }); this.startTimerIfNotEmpty(); } setLocalState(state: T) { 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(): T | undefined { return this.inner.getState(this.peer); } getAllStates(): Record { return this.inner.getAllStates(); } encode(peers: PeerID[]): Uint8Array { return this.inner.encode(peers); } encodeAll(): Uint8Array { return this.inner.encodeAll(); } addListener(listener: AwarenessListener) { this.listeners.add(listener); } removeListener(listener: AwarenessListener) { this.listeners.delete(listener); } peers(): PeerID[] { return this.inner.peers(); } destroy() { clearInterval(this.timer); this.listeners.clear(); } private 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) as unknown as number; } }