mirror of
https://github.com/loro-dev/loro.git
synced 2025-02-02 11:06:14 +00:00
62a3a93552
* feat: make vitest tests pass * chore: update readme & add deno test for web bundle * chore: bump version to 1.0.8-alpha.0 * chore: bump loro-crdt version * fix: build script export init method from loro-wasm/web * chore: bump version * chore: specify which files to include for npm publish * refactor: rename loro-js to loro-js-test * refactor: remove the old loro-js folder * fix: build scripts * chore: 1.0.8-alpha.3 * chore: add release info
215 lines
5.2 KiB
TypeScript
215 lines
5.2 KiB
TypeScript
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<T>(
|
|
value: T,
|
|
): T extends LoroText ? "Text"
|
|
: T extends LoroMap<any> ? "Map"
|
|
: T extends LoroTree<any> ? "Tree"
|
|
: T extends LoroList<any> ? "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<T extends Value = Value> {
|
|
inner: AwarenessWasm<T>;
|
|
private peer: PeerID;
|
|
private timer: number | undefined;
|
|
private timeout: number;
|
|
private listeners: Set<AwarenessListener> = 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<PeerID, T> {
|
|
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;
|
|
}
|
|
}
|