loro/loro-js/tests/misc.test.ts

296 lines
7.5 KiB
TypeScript
Raw Normal View History

import { assert, describe, expect, it } from "vitest";
import { LoroDoc, LoroList, LoroMap, LoroText, VersionVector } from "../src";
import { expectTypeOf } from "vitest";
function assertEquals(a: any, b: any) {
expect(a).toStrictEqual(b);
}
describe("transaction", () => {
it("transaction", async () => {
const loro = new LoroDoc();
const text = loro.getText("text");
let count = 0;
const sub = loro.subscribe(() => {
count += 1;
loro.unsubscribe(sub);
});
expect(count).toBe(0);
text.insert(0, "hello world");
expect(count).toBe(0);
text.insert(0, "hello world");
assertEquals(count, 0);
loro.commit();
await one_ms();
assertEquals(count, 1);
});
it("transaction origin", async () => {
const loro = new LoroDoc();
const text = loro.getText("text");
let count = 0;
const sub = loro.subscribe((event: { origin: string }) => {
count += 1;
loro.unsubscribe(sub);
assertEquals(event.origin, "origin");
});
assertEquals(count, 0);
text.insert(0, "hello world");
assertEquals(count, 0);
text.insert(0, "hello world");
assertEquals(count, 0);
loro.commit({ origin: "origin" });
await one_ms();
assertEquals(count, 1);
});
});
describe("subscribe", () => {
it("subscribe_lock", async () => {
const loro = new LoroDoc();
const text = loro.getText("text");
const list = loro.getList("list");
let count = 0;
let i = 1;
const sub = loro.subscribe(() => {
if (i > 0) {
list.insert(0, i);
loro.commit();
i--;
}
count += 1;
});
text.insert(0, "hello world");
loro.commit();
await one_ms();
assertEquals(count, 2);
text.insert(0, "hello world");
loro.commit();
await one_ms();
assertEquals(count, 3);
loro.unsubscribe(sub);
text.insert(0, "hello world");
loro.commit();
await one_ms();
assertEquals(count, 3);
});
it("subscribe_lock2", async () => {
const loro = new LoroDoc();
const text = loro.getText("text");
let count = 0;
const sub = loro.subscribe(() => {
count += 1;
loro.unsubscribe(sub);
});
assertEquals(count, 0);
text.insert(0, "hello world");
loro.commit();
await one_ms();
assertEquals(count, 1);
text.insert(0, "hello world");
loro.commit();
await one_ms();
assertEquals(count, 1);
});
it("subscribe", async () => {
const loro = new LoroDoc();
const text = loro.getText("text");
let count = 0;
const sub = loro.subscribe(() => {
count += 1;
});
text.insert(0, "hello world");
loro.commit();
await one_ms();
assertEquals(count, 1);
text.insert(0, "hello world");
loro.commit();
await one_ms();
assertEquals(count, 2);
loro.unsubscribe(sub);
text.insert(0, "hello world");
loro.commit();
await one_ms();
assertEquals(count, 2);
});
});
describe("sync", () => {
it("two insert at beginning", async () => {
const a = new LoroDoc();
const b = new LoroDoc();
let a_version: undefined | VersionVector = undefined;
let b_version: undefined | VersionVector = undefined;
a.subscribe((e) => {
if (e.by == "local") {
const exported = a.exportFrom(a_version);
b.import(exported);
a_version = a.version();
}
});
b.subscribe((e) => {
if (e.by == "local") {
const exported = b.exportFrom(b_version);
a.import(exported);
b_version = b.version();
}
});
const aText = a.getText("text");
const bText = b.getText("text");
aText.insert(0, "abc");
a.commit();
await one_ms();
assertEquals(aText.toString(), bText.toString());
});
it("sync", () => {
const loro = new LoroDoc();
const text = loro.getText("text");
text.insert(0, "hello world");
const loro_bk = new LoroDoc();
loro_bk.import(loro.exportFrom(undefined));
2024-04-29 08:23:00 +00:00
assertEquals(loro_bk.toJSON(), loro.toJSON());
const text_bk = loro_bk.getText("text");
assertEquals(text_bk.toString(), "hello world");
text_bk.insert(0, "a ");
loro.import(loro_bk.exportFrom(undefined));
assertEquals(text.toString(), "a hello world");
const map = loro.getMap("map");
map.set("key", "value");
});
});
describe("wasm", () => {
const loro = new LoroDoc();
const a = loro.getText("ha");
a.insert(0, "hello world");
a.delete(6, 5);
a.insert(6, "everyone");
loro.commit();
const b = loro.getMap("ha");
b.set("ab", 123);
loro.commit();
const bText = b.setContainer("hh", new LoroText());
loro.commit();
it("map get", () => {
assertEquals(b.get("ab"), 123);
});
it("getValueDeep", () => {
bText.insert(0, "hello world Text");
2024-04-29 08:23:00 +00:00
assertEquals(b.toJSON(), { ab: 123, hh: "hello world Text" });
});
it("get container by id", () => {
const id = b.id;
const b2 = loro.getContainerById(id) as LoroMap;
2024-04-29 08:23:00 +00:00
assertEquals(b2.toJSON(), b.toJSON());
assertEquals(b2.id, id);
b2.set("0", 12);
2024-04-29 08:23:00 +00:00
assertEquals(b2.toJSON(), b.toJSON());
});
});
describe("type", () => {
it("test map type", () => {
const loro = new LoroDoc<{ map: LoroMap<{ name: "he" }> }>();
const map = loro.getMap("map");
const v = map.get("name");
expectTypeOf(v).toEqualTypeOf<"he">();
});
it("test recursive map type", () => {
const loro = new LoroDoc<{ map: LoroMap<{ map: LoroMap<{ name: "he" }> }> }>();
const map = loro.getMap("map");
map.setContainer("map", new LoroMap());
const subMap = map.get("map");
const name = subMap.get("name");
expectTypeOf(name).toEqualTypeOf<"he">();
});
it("works for list type", () => {
const loro = new LoroDoc<{ list: LoroList<string> }>();
const list = loro.getList("list");
list.insert(0, "123");
const v0 = list.get(0);
expectTypeOf(v0).toEqualTypeOf<string>();
});
2023-09-12 07:57:06 +00:00
it("test binary type", () => {
const loro = new LoroDoc<{ list: LoroList<Uint8Array> }>();
const list = loro.getList("list");
list.insert(0, new Uint8Array(10));
const v0 = list.get(0);
expectTypeOf(v0).toEqualTypeOf<Uint8Array>();
2023-09-12 07:57:06 +00:00
});
});
feat: movable tree support (#120) * feat: tree state * feat: tree value * feat: tree handler * fix: tree diff * test: fuzz tree * feat: tree snapshot * fix: tree default value * fix: test new node * fix: tree diff * fix: tree unresolved value * fix: tree fuzz * fix: tree fuzz move * fix: sort by tree id * fix: tree diff sorted by lamport * fix: sort roots before tree converted to string * fix: rebase main * fix: tree fuzz * fix: delete undo * fix: tree to json children sorted * fix: diff calculate * fix: diff cycle move * fix: tree old parent cache * feat: cache * fix: local op add tree cache * fix: don't add same tree move to cache * fix: need update cache * feat: new cache * bench: add checkout bench * chore: clean * fix: apply node uncheck * perf: lamport bound * fix: calc old parent * feat: tree wasm * fix: change tree diff * fix: tree diff retreat * fix: tree diff should not apply when add node * feat: new tree loro value * chore: typo * fix: tree deep value * fix: snapshot tree index -1 * fix: decode tree snapshot use state * fix: release state lock when emit event * fix: tree node meta container * fix: need set map container when covert to local tree op * fix: tree value add deleted * fix: more then one op in a change * fix: tree fuzz deleted equal * fix: tree calc min lamport * feat: tree encoding v2 * doc: movable tree * fix: test tree meta * test: remove import bytes check * refactor: diff of text and map * refactor: del span * perf: tree state use deleted cache * fix: some details * fix: loro js tree create * feat: add un exist tree node * bench: tree depth * fix: check out should emit event * refactor: event * fix: fuzz err * fix: pass all tests * fix: fuzz err * fix: list child cache err * chore: rm debug code * fix: encode enhanced err * fix: encode enchanced * fix: fix several richtext issue * fix: richtext anchor err * chore: rm debug code * fix: richtext fuzz err * feat: speedup text snapshot decode * perf: optimize snapshot encoding * perf: speed up decode & insert * fix: fugue span merge err * perf: speedup delete & id cursor map * fix: fugue merge err * chore: update utils * fix: fix merge * fix: return err apply op * fix: fix merge * fix: get map container as tree meta
2023-10-30 03:13:52 +00:00
Cursors (#290) This PR introduces support for retrieving and querying cursors. ## Motivation Using "index" to denote cursor positions can be unstable, as positions may shift with document edits. To reliably represent a position or range within a document, it is more effective to leverage the unique ID of each item/character in a List CRDT or Text CRDT. ## Updating Cursors Loro optimizes State metadata by not storing the IDs of deleted elements. This approach, while efficient, complicates tracking cursor positions since they rely on these IDs for precise locations within the document. The solution recalculates position by replaying relevant history to update stable positions accurately. To minimize the performance impact of history replay, the system updates cursor info to reference only the IDs of currently present elements, thereby reducing the need for replay. Each position has a "Side" information, indicating the actual cursor position is on the left, right, or directly in the center of the target ID. Note: In JavaScript, the offset returned when querying a Stable Position is based on the UTF-16 index. # Example ```ts const loro = new Loro(); const list = loro.getList("list"); list.insert(0, "a"); const pos0 = list.getStablePos(0); list.insert(1, "b"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(0); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.insert(0, "c"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.delete(1, 1); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(-1); expect(ans.update).toBeDefined(); } ```
2024-04-09 08:01:37 +00:00
describe("list stable position", () => {
it("basic tests", () => {
const loro = new LoroDoc();
Cursors (#290) This PR introduces support for retrieving and querying cursors. ## Motivation Using "index" to denote cursor positions can be unstable, as positions may shift with document edits. To reliably represent a position or range within a document, it is more effective to leverage the unique ID of each item/character in a List CRDT or Text CRDT. ## Updating Cursors Loro optimizes State metadata by not storing the IDs of deleted elements. This approach, while efficient, complicates tracking cursor positions since they rely on these IDs for precise locations within the document. The solution recalculates position by replaying relevant history to update stable positions accurately. To minimize the performance impact of history replay, the system updates cursor info to reference only the IDs of currently present elements, thereby reducing the need for replay. Each position has a "Side" information, indicating the actual cursor position is on the left, right, or directly in the center of the target ID. Note: In JavaScript, the offset returned when querying a Stable Position is based on the UTF-16 index. # Example ```ts const loro = new Loro(); const list = loro.getList("list"); list.insert(0, "a"); const pos0 = list.getStablePos(0); list.insert(1, "b"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(0); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.insert(0, "c"); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(0); expect(ans.update).toBeUndefined(); } list.delete(1, 1); { const ans = loro.queryStablePos(pos0!); expect(ans.offset).toEqual(1); expect(ans.side).toEqual(-1); expect(ans.update).toBeDefined(); } ```
2024-04-09 08:01:37 +00:00
const list = loro.getList("list");
list.insert(0, "a");
const pos0 = list.getCursor(0);
list.insert(1, "b");
{
const ans = loro.getCursorPos(pos0!);
expect(ans.offset).toEqual(0);
expect(ans.side).toEqual(0);
expect(ans.update).toBeUndefined();
}
list.insert(0, "c");
{
const ans = loro.getCursorPos(pos0!);
expect(ans.offset).toEqual(1);
expect(ans.side).toEqual(0);
expect(ans.update).toBeUndefined();
}
list.delete(1, 1);
{
const ans = loro.getCursorPos(pos0!);
expect(ans.offset).toEqual(1);
expect(ans.side).toEqual(-1);
expect(ans.update).toBeDefined();
}
});
});
describe("to json", () => {
it("to shallow json", async () => {
const loro = new LoroDoc();
loro.getText("text");
loro.getMap("map");
loro.getList("list");
loro.getTree("tree");
loro.getMovableList("movable_list");
const value = loro.getShallowValue();
assert(Object.keys(value).includes("text"));
assert(Object.keys(value).includes("map"));
assert(Object.keys(value).includes("list"));
assert(Object.keys(value).includes("tree"));
assert(Object.keys(value).includes("movable_list"));
});
});
function one_ms(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 1));
}