2023-12-13 02:25:00 +00:00
|
|
|
import { describe, expect, it } from "vitest";
|
2024-03-30 03:38:24 +00:00
|
|
|
import { Loro, LoroList, LoroMap, LoroText, VersionVector } from "../src";
|
2023-05-11 03:59:48 +00:00
|
|
|
import { expectTypeOf } from "vitest";
|
2023-10-30 10:32:36 +00:00
|
|
|
|
2023-04-03 01:29:25 +00:00
|
|
|
function assertEquals(a: any, b: any) {
|
|
|
|
expect(a).toStrictEqual(b);
|
|
|
|
}
|
|
|
|
|
|
|
|
describe("transaction", () => {
|
|
|
|
it("transaction", async () => {
|
|
|
|
const loro = new Loro();
|
|
|
|
const text = loro.getText("text");
|
|
|
|
let count = 0;
|
|
|
|
const sub = loro.subscribe(() => {
|
|
|
|
count += 1;
|
|
|
|
loro.unsubscribe(sub);
|
|
|
|
});
|
2023-10-30 10:32:36 +00:00
|
|
|
expect(count).toBe(0);
|
|
|
|
text.insert(0, "hello world");
|
|
|
|
expect(count).toBe(0);
|
|
|
|
text.insert(0, "hello world");
|
|
|
|
assertEquals(count, 0);
|
|
|
|
loro.commit();
|
2023-11-16 13:04:31 +00:00
|
|
|
await one_ms();
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(count, 1);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("transaction origin", async () => {
|
|
|
|
const loro = new Loro();
|
|
|
|
const text = loro.getText("text");
|
|
|
|
let count = 0;
|
|
|
|
const sub = loro.subscribe((event: { origin: string }) => {
|
|
|
|
count += 1;
|
|
|
|
loro.unsubscribe(sub);
|
|
|
|
assertEquals(event.origin, "origin");
|
|
|
|
});
|
2023-10-30 10:32:36 +00:00
|
|
|
|
|
|
|
assertEquals(count, 0);
|
|
|
|
text.insert(0, "hello world");
|
|
|
|
assertEquals(count, 0);
|
|
|
|
text.insert(0, "hello world");
|
|
|
|
assertEquals(count, 0);
|
|
|
|
loro.commit("origin");
|
2023-11-16 13:04:31 +00:00
|
|
|
await one_ms();
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(count, 1);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe("subscribe", () => {
|
|
|
|
it("subscribe_lock", async () => {
|
|
|
|
const loro = new Loro();
|
|
|
|
const text = loro.getText("text");
|
|
|
|
const list = loro.getList("list");
|
|
|
|
let count = 0;
|
|
|
|
let i = 1;
|
|
|
|
const sub = loro.subscribe(() => {
|
|
|
|
if (i > 0) {
|
2023-10-30 10:32:36 +00:00
|
|
|
list.insert(0, i);
|
|
|
|
loro.commit();
|
|
|
|
i--;
|
2023-04-03 01:29:25 +00:00
|
|
|
}
|
2023-07-30 08:30:41 +00:00
|
|
|
|
2023-04-03 01:29:25 +00:00
|
|
|
count += 1;
|
|
|
|
});
|
2023-10-30 10:32:36 +00:00
|
|
|
|
|
|
|
text.insert(0, "hello world");
|
|
|
|
loro.commit();
|
2023-11-16 13:04:31 +00:00
|
|
|
await one_ms();
|
2023-07-30 08:30:41 +00:00
|
|
|
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(count, 2);
|
2023-10-30 10:32:36 +00:00
|
|
|
text.insert(0, "hello world");
|
|
|
|
loro.commit();
|
2023-11-16 13:04:31 +00:00
|
|
|
await one_ms();
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(count, 3);
|
|
|
|
loro.unsubscribe(sub);
|
2023-10-30 10:32:36 +00:00
|
|
|
text.insert(0, "hello world");
|
|
|
|
loro.commit();
|
2023-11-16 13:04:31 +00:00
|
|
|
await one_ms();
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(count, 3);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("subscribe_lock2", async () => {
|
|
|
|
const loro = new Loro();
|
|
|
|
const text = loro.getText("text");
|
|
|
|
let count = 0;
|
|
|
|
const sub = loro.subscribe(() => {
|
|
|
|
count += 1;
|
|
|
|
loro.unsubscribe(sub);
|
|
|
|
});
|
|
|
|
assertEquals(count, 0);
|
2023-10-30 10:32:36 +00:00
|
|
|
text.insert(0, "hello world");
|
|
|
|
loro.commit();
|
2023-11-16 13:04:31 +00:00
|
|
|
await one_ms();
|
2023-07-30 08:30:41 +00:00
|
|
|
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(count, 1);
|
2023-10-30 10:32:36 +00:00
|
|
|
text.insert(0, "hello world");
|
|
|
|
loro.commit();
|
2023-11-16 13:04:31 +00:00
|
|
|
await one_ms();
|
2023-07-30 08:30:41 +00:00
|
|
|
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(count, 1);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("subscribe", async () => {
|
|
|
|
const loro = new Loro();
|
|
|
|
const text = loro.getText("text");
|
|
|
|
let count = 0;
|
|
|
|
const sub = loro.subscribe(() => {
|
|
|
|
count += 1;
|
|
|
|
});
|
2023-10-30 10:32:36 +00:00
|
|
|
text.insert(0, "hello world");
|
|
|
|
loro.commit();
|
2023-11-16 13:04:31 +00:00
|
|
|
await one_ms();
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(count, 1);
|
2023-10-30 10:32:36 +00:00
|
|
|
text.insert(0, "hello world");
|
|
|
|
loro.commit();
|
2023-11-16 13:04:31 +00:00
|
|
|
await one_ms();
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(count, 2);
|
|
|
|
loro.unsubscribe(sub);
|
2023-10-30 10:32:36 +00:00
|
|
|
text.insert(0, "hello world");
|
|
|
|
loro.commit();
|
2023-11-16 13:04:31 +00:00
|
|
|
await one_ms();
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(count, 2);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe("sync", () => {
|
|
|
|
it("two insert at beginning", async () => {
|
|
|
|
const a = new Loro();
|
|
|
|
const b = new Loro();
|
2024-01-18 05:28:28 +00:00
|
|
|
let a_version: undefined | VersionVector = undefined;
|
|
|
|
let b_version: undefined | VersionVector = undefined;
|
2024-04-03 09:56:01 +00:00
|
|
|
a.subscribe((e) => {
|
|
|
|
if (e.by == "local") {
|
2023-04-03 01:29:25 +00:00
|
|
|
const exported = a.exportFrom(a_version);
|
|
|
|
b.import(exported);
|
|
|
|
a_version = a.version();
|
|
|
|
}
|
|
|
|
});
|
2024-04-03 09:56:01 +00:00
|
|
|
b.subscribe((e) => {
|
|
|
|
if (e.by == "local") {
|
2023-04-03 01:29:25 +00:00
|
|
|
const exported = b.exportFrom(b_version);
|
|
|
|
a.import(exported);
|
|
|
|
b_version = b.version();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
const aText = a.getText("text");
|
|
|
|
const bText = b.getText("text");
|
2023-10-30 10:32:36 +00:00
|
|
|
aText.insert(0, "abc");
|
|
|
|
a.commit();
|
2023-11-16 13:04:31 +00:00
|
|
|
await one_ms();
|
2023-07-30 08:30:41 +00:00
|
|
|
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(aText.toString(), bText.toString());
|
|
|
|
});
|
|
|
|
|
|
|
|
it("sync", () => {
|
|
|
|
const loro = new Loro();
|
|
|
|
const text = loro.getText("text");
|
2023-10-30 10:32:36 +00:00
|
|
|
text.insert(0, "hello world");
|
2023-07-30 08:30:41 +00:00
|
|
|
|
2023-04-03 01:29:25 +00:00
|
|
|
const loro_bk = new Loro();
|
|
|
|
loro_bk.import(loro.exportFrom(undefined));
|
2024-04-29 08:23:00 +00:00
|
|
|
assertEquals(loro_bk.toJSON(), loro.toJSON());
|
2023-04-03 01:29:25 +00:00
|
|
|
const text_bk = loro_bk.getText("text");
|
|
|
|
assertEquals(text_bk.toString(), "hello world");
|
2023-10-30 10:32:36 +00:00
|
|
|
text_bk.insert(0, "a ");
|
2023-07-30 08:30:41 +00:00
|
|
|
|
2023-04-03 01:29:25 +00:00
|
|
|
loro.import(loro_bk.exportFrom(undefined));
|
|
|
|
assertEquals(text.toString(), "a hello world");
|
|
|
|
const map = loro.getMap("map");
|
2023-10-30 10:32:36 +00:00
|
|
|
map.set("key", "value");
|
2023-04-03 01:29:25 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe("wasm", () => {
|
|
|
|
const loro = new Loro();
|
|
|
|
const a = loro.getText("ha");
|
2023-10-30 10:32:36 +00:00
|
|
|
a.insert(0, "hello world");
|
|
|
|
a.delete(6, 5);
|
|
|
|
a.insert(6, "everyone");
|
|
|
|
loro.commit();
|
2023-07-30 08:30:41 +00:00
|
|
|
|
2023-04-03 01:29:25 +00:00
|
|
|
const b = loro.getMap("ha");
|
2023-10-30 10:32:36 +00:00
|
|
|
b.set("ab", 123);
|
|
|
|
loro.commit();
|
2023-04-03 01:29:25 +00:00
|
|
|
|
2024-03-30 03:38:24 +00:00
|
|
|
const bText = b.setContainer("hh", new LoroText());
|
2023-10-30 10:32:36 +00:00
|
|
|
loro.commit();
|
2023-04-03 01:29:25 +00:00
|
|
|
|
|
|
|
it("map get", () => {
|
|
|
|
assertEquals(b.get("ab"), 123);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("getValueDeep", () => {
|
2023-10-30 10:32:36 +00:00
|
|
|
bText.insert(0, "hello world Text");
|
2024-04-29 08:23:00 +00:00
|
|
|
assertEquals(b.toJSON(), { ab: 123, hh: "hello world Text" });
|
2023-04-03 01:29:25 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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());
|
2023-04-03 01:29:25 +00:00
|
|
|
assertEquals(b2.id, id);
|
2023-10-30 10:32:36 +00:00
|
|
|
b2.set("0", 12);
|
2023-07-30 08:30:41 +00:00
|
|
|
|
2024-04-29 08:23:00 +00:00
|
|
|
assertEquals(b2.toJSON(), b.toJSON());
|
2023-04-03 01:29:25 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-05-11 03:59:48 +00:00
|
|
|
describe("type", () => {
|
|
|
|
it("test map type", () => {
|
|
|
|
const loro = new Loro<{ map: LoroMap<{ name: "he" }> }>();
|
2024-04-04 17:43:36 +00:00
|
|
|
const map = loro.getMap("map");
|
|
|
|
const v = map.get("name");
|
2023-05-11 03:59:48 +00:00
|
|
|
expectTypeOf(v).toEqualTypeOf<"he">();
|
|
|
|
});
|
|
|
|
|
|
|
|
it("test recursive map type", () => {
|
|
|
|
const loro = new Loro<{ map: LoroMap<{ map: LoroMap<{ name: "he" }> }> }>();
|
2024-04-04 17:43:36 +00:00
|
|
|
const map = loro.getMap("map");
|
2024-03-30 03:38:24 +00:00
|
|
|
map.setContainer("map", new LoroMap());
|
2023-07-30 08:30:41 +00:00
|
|
|
|
2024-04-04 17:43:36 +00:00
|
|
|
const subMap = map.get("map");
|
|
|
|
const name = subMap.get("name");
|
2023-05-11 03:59:48 +00:00
|
|
|
expectTypeOf(name).toEqualTypeOf<"he">();
|
|
|
|
});
|
|
|
|
|
|
|
|
it("works for list type", () => {
|
2024-04-04 17:43:36 +00:00
|
|
|
const loro = new Loro<{ list: LoroList<string> }>();
|
|
|
|
const list = loro.getList("list");
|
|
|
|
list.insert(0, "123");
|
|
|
|
const v0 = list.get(0);
|
2023-05-11 03:59:48 +00:00
|
|
|
expectTypeOf(v0).toEqualTypeOf<string>();
|
|
|
|
});
|
2023-09-12 07:57:06 +00:00
|
|
|
|
|
|
|
it("test binary type", () => {
|
2024-04-04 17:43:36 +00:00
|
|
|
const loro = new Loro<{ 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
|
|
|
});
|
2023-05-11 03:59:48 +00:00
|
|
|
});
|
|
|
|
|
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 Loro();
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-04-03 01:29:25 +00:00
|
|
|
function one_ms(): Promise<void> {
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, 1));
|
|
|
|
}
|