refactor: replace "local" and "fromCheckout" in event with "triggeredBy" (#312)

* refactor: replace "local" and "fromCheckout" in event with "triggeredBy"

* refactor: simplify the name to `by`
This commit is contained in:
Zixuan Chen 2024-04-03 17:56:01 +08:00 committed by GitHub
parent fc20f1f7d0
commit 7561448323
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 275 additions and 178 deletions

View file

@ -6,9 +6,10 @@ const validTargets = Array.from(
Deno.readDirSync(resolve(__dirname, "../fuzz/fuzz_targets")),
).map((x) => x.name.replace(/.rs$/, ""));
const targets = Deno.args.length === 0
? validTargets
: Deno.args.filter((x) => validTargets.includes(x));
const targets =
Deno.args.length === 0
? validTargets
: Deno.args.filter((x) => validTargets.includes(x));
const promises = [];
for (const target of targets) {

View file

@ -30,6 +30,44 @@ pub struct ContainerDiff {
pub diff: Diff,
}
///
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EventTriggerKind {
/// The event is triggered by a local transaction.
Local,
/// The event is triggered by importing
Import,
/// The event is triggered by checkout
Checkout,
}
impl std::fmt::Display for EventTriggerKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EventTriggerKind::Local => write!(f, "local"),
EventTriggerKind::Import => write!(f, "import"),
EventTriggerKind::Checkout => write!(f, "checkout"),
}
}
}
impl EventTriggerKind {
#[inline]
pub fn is_local(&self) -> bool {
matches!(self, EventTriggerKind::Local)
}
#[inline]
pub fn is_import(&self) -> bool {
matches!(self, EventTriggerKind::Import)
}
#[inline]
pub fn is_checkout(&self) -> bool {
matches!(self, EventTriggerKind::Checkout)
}
}
#[derive(Debug, Clone)]
pub struct DiffEvent<'a> {
/// The receiver of the event.
@ -49,9 +87,7 @@ pub struct DocDiff {
pub from: Frontiers,
pub to: Frontiers,
pub origin: InternalString,
pub local: bool,
/// Whether the diff is created from the checkout operation.
pub from_checkout: bool,
pub by: EventTriggerKind,
pub diff: Vec<ContainerDiff>,
}
@ -89,8 +125,7 @@ pub(crate) enum DiffVariant {
#[derive(Debug, Clone)]
pub(crate) struct InternalDocDiff<'a> {
pub(crate) origin: InternalString,
pub(crate) local: bool,
pub(crate) from_checkout: bool,
pub(crate) by: EventTriggerKind,
pub(crate) diff: Cow<'a, [InternalContainerDiff]>,
pub(crate) new_version: Cow<'a, Frontiers>,
}
@ -99,15 +134,14 @@ impl<'a> InternalDocDiff<'a> {
pub fn into_owned(self) -> InternalDocDiff<'static> {
InternalDocDiff {
origin: self.origin,
local: self.local,
from_checkout: self.from_checkout,
by: self.by,
diff: Cow::Owned((*self.diff).to_owned()),
new_version: Cow::Owned((*self.new_version).to_owned()),
}
}
pub fn can_merge(&self, other: &Self) -> bool {
self.origin == other.origin && self.local == other.local
self.by == other.by
}
}

View file

@ -2477,7 +2477,7 @@ mod test {
let loro2 = LoroDoc::new();
loro2.subscribe_root(Arc::new(|e| {
println!("{} {:?} ", e.event_meta.local, e.event_meta.diff)
println!("{} {:?} ", e.event_meta.by, e.event_meta.diff)
}));
loro2.import(&loro.export_from(&loro2.oplog_vv())).unwrap();
assert_eq!(loro.get_deep_value(), loro2.get_deep_value());

View file

@ -21,7 +21,7 @@ use crate::{
encoding::{
decode_snapshot, export_snapshot, parse_header_and_body, EncodeMode, ParsedHeaderAndBody,
},
event::{str_to_path, Index},
event::{str_to_path, EventTriggerKind, Index},
handler::{Handler, TextHandler, TreeHandler, ValueOrHandler},
id::PeerID,
oplog::dag::FrontiersNotIncluded,
@ -471,9 +471,8 @@ impl LoroDoc {
let mut state = self.state.lock().unwrap();
state.apply_diff(InternalDocDiff {
origin,
local: false,
diff: (diff).into(),
from_checkout: false,
by: EventTriggerKind::Import,
new_version: Cow::Owned(oplog.frontiers().clone()),
});
} else {
@ -507,9 +506,8 @@ impl LoroDoc {
let mut state = self.state.lock().unwrap();
state.apply_diff(InternalDocDiff {
origin: "".into(),
local: false,
diff: (diff).into(),
from_checkout: false,
by: EventTriggerKind::Import,
new_version: Cow::Owned(oplog.frontiers().clone()),
});
}
@ -784,9 +782,8 @@ impl LoroDoc {
);
state.apply_diff(InternalDocDiff {
origin: "checkout".into(),
local: true,
by: EventTriggerKind::Checkout,
diff: Cow::Owned(diff),
from_checkout: true,
new_version: Cow::Owned(frontiers.clone()),
});
drop(state);

View file

@ -16,7 +16,7 @@ use crate::{
},
delta::DeltaItem,
encoding::{StateSnapshotDecodeContext, StateSnapshotEncoder},
event::{Diff, Index, InternalContainerDiff, InternalDiff},
event::{Diff, EventTriggerKind, Index, InternalContainerDiff, InternalDiff},
fx_map,
handler::ValueOrHandler,
id::PeerID,
@ -310,7 +310,7 @@ impl DocState {
}
/// This should be called when DocState is going to apply a transaction / a diff.
fn pre_txn(&mut self, next_origin: InternalString, next_local: bool) {
fn pre_txn(&mut self, next_origin: InternalString, next_trigger: EventTriggerKind) {
if !self.is_recording() {
return;
}
@ -319,7 +319,7 @@ impl DocState {
return;
};
if last_diff.origin == next_origin && last_diff.local == next_local {
if last_diff.origin == next_origin && last_diff.by == next_trigger {
return;
}
@ -359,7 +359,7 @@ impl DocState {
}
// tracing::info!("Diff = {:#?}", &diff);
let is_recording = self.is_recording();
self.pre_txn(diff.origin.clone(), diff.local);
self.pre_txn(diff.origin.clone(), diff.by);
let Cow::Owned(inner) = std::mem::take(&mut diff.diff) else {
unreachable!()
};
@ -480,8 +480,8 @@ impl DocState {
state.apply_local_op(raw_op, op)
}
pub(crate) fn start_txn(&mut self, origin: InternalString, local: bool) {
self.pre_txn(origin, local);
pub(crate) fn start_txn(&mut self, origin: InternalString, trigger: EventTriggerKind) {
self.pre_txn(origin, trigger);
self.in_txn = true;
}
@ -626,7 +626,7 @@ impl DocState {
frontiers: Frontiers,
) {
assert!(self.states.is_empty(), "overriding states");
self.pre_txn(Default::default(), false);
self.pre_txn(Default::default(), EventTriggerKind::Import);
self.states = states;
for (idx, state) in self.states.iter() {
for child_id in state.get_child_containers() {
@ -652,8 +652,7 @@ impl DocState {
.collect();
self.record_diff(InternalDocDiff {
origin: Default::default(),
local: false,
from_checkout: false,
by: EventTriggerKind::Import,
diff,
new_version: Cow::Borrowed(&frontiers),
});
@ -896,11 +895,11 @@ impl DocState {
panic!("diffs is empty");
}
let triggered_by = diffs[0].by;
assert!(diffs.iter().all(|x| x.by == triggered_by));
let mut containers = FxHashMap::default();
let to = (*diffs.last().unwrap().new_version).to_owned();
let origin = diffs[0].origin.clone();
let local = diffs[0].local;
let from_checkout = diffs[0].from_checkout;
for diff in diffs {
#[allow(clippy::unnecessary_to_owned)]
for container_diff in diff.diff.into_owned() {
@ -950,8 +949,7 @@ impl DocState {
from,
to,
origin,
from_checkout,
local,
by: triggered_by,
diff,
}
}

View file

@ -203,7 +203,7 @@ impl Transaction {
}
let oplog_lock = oplog.lock().unwrap();
state_lock.start_txn(origin, true);
state_lock.start_txn(origin, crate::event::EventTriggerKind::Local);
let arena = state_lock.arena.clone();
let frontiers = state_lock.frontiers.clone();
let peer = state_lock.peer;
@ -300,7 +300,7 @@ impl Transaction {
state.commit_txn(
Frontiers::from_id(last_id),
diff.map(|arr| InternalDocDiff {
local: true,
by: crate::event::EventTriggerKind::Local,
origin: self.origin.clone(),
diff: Cow::Owned(
arr.into_iter()
@ -312,7 +312,6 @@ impl Transaction {
})
.collect(),
),
from_checkout: false,
new_version: Cow::Borrowed(oplog.frontiers()),
}),
);

View file

@ -4,7 +4,7 @@ use fxhash::FxHashMap;
use loro_common::{ContainerID, ContainerType, LoroResult, LoroValue, ID};
use loro_internal::{
delta::ResolvedMapValue,
event::Diff,
event::{Diff, EventTriggerKind},
handler::{Handler, TextDelta, ValueOrHandler},
version::Frontiers,
ApplyDiff, HandlerTrait, ListHandler, LoroDoc, MapHandler, TextHandler, ToJson,
@ -94,7 +94,10 @@ fn mark_with_the_same_key_value_should_be_skipped() {
fn event_from_checkout() {
let a = LoroDoc::new_auto_commit();
let sub_id = a.subscribe_root(Arc::new(|event| {
assert!(!event.event_meta.from_checkout);
assert!(matches!(
event.event_meta.by,
EventTriggerKind::Checkout | EventTriggerKind::Local
));
}));
a.get_text("text").insert(0, "hello").unwrap();
a.commit_then_renew();
@ -105,7 +108,7 @@ fn event_from_checkout() {
let ran = Arc::new(AtomicBool::new(false));
let ran_cloned = ran.clone();
a.subscribe_root(Arc::new(move |event| {
assert!(event.event_meta.from_checkout);
assert!(event.event_meta.by.is_checkout());
ran.store(true, std::sync::atomic::Ordering::Relaxed);
}));
a.checkout(&version).unwrap();

View file

@ -37,7 +37,7 @@ async function build() {
if (profile !== "dev") {
await Promise.all(
TARGETS.map(async (target) => {
// --snip-rust-panicking-code --snip-rust-fmt-code
// --snip-rust-panicking-code --snip-rust-fmt-code
const snip = `wasm-snip ./${target}/loro_wasm_bg.wasm -o ./${target}/loro_wasm_bg.wasm`;
console.log(">", snip);
await Deno.run({ cmd: snip.split(" "), cwd: LoroWasmDir }).status();

View file

@ -61,6 +61,8 @@ pub struct Loro(Arc<LoroDoc>);
extern "C" {
#[wasm_bindgen(typescript_type = "number | bigint | `${number}`")]
pub type JsIntoPeerID;
#[wasm_bindgen(typescript_type = "`${number}`")]
pub type JsStrPeerID;
#[wasm_bindgen(typescript_type = "ContainerID")]
pub type JsContainerID;
#[wasm_bindgen(typescript_type = "ContainerID | string")]
@ -468,8 +470,9 @@ impl Loro {
/// Get peer id in decimal string.
#[wasm_bindgen(js_name = "peerIdStr", method, getter)]
pub fn peer_id_str(&self) -> String {
format!("{}", self.0.peer_id())
pub fn peer_id_str(&self) -> JsStrPeerID {
let v: JsValue = format!("{}", self.0.peer_id()).into();
v.into()
}
/// Set the peer ID of the current writer.
@ -1106,9 +1109,7 @@ fn call_subscriber(ob: observer::Observer, e: DiffEvent, doc: &Arc<LoroDoc>) {
// [1]: https://caniuse.com/?search=FinalizationRegistry
// [2]: https://rustwasm.github.io/wasm-bindgen/reference/weak-references.html
let event = diff_event_to_js_value(e, doc);
if let Err(e) = ob.call1(&event) {
console_error!("Error when calling observer: {:#?}", e);
}
ob.call1(&event).unwrap_throw();
}
#[allow(unused)]
@ -1138,13 +1139,7 @@ impl Default for Loro {
fn diff_event_to_js_value(event: DiffEvent, doc: &Arc<LoroDoc>) -> JsValue {
let obj = js_sys::Object::new();
Reflect::set(&obj, &"local".into(), &event.event_meta.local.into()).unwrap();
Reflect::set(
&obj,
&"fromCheckout".into(),
&event.event_meta.from_checkout.into(),
)
.unwrap();
Reflect::set(&obj, &"by".into(), &event.event_meta.by.to_string().into()).unwrap();
let origin: &str = &event.event_meta.origin;
Reflect::set(&obj, &"origin".into(), &JsValue::from_str(origin)).unwrap();
if let Some(t) = event.current_target.as_ref() {
@ -2765,7 +2760,7 @@ export interface ImportBlobMetadata {
partialStartVersionVector: VersionVector;
/**
* The version vector of the end of the import.
*
*
* Import blob includes all the ops from `partial_start_vv` to `partial_end_vv`.
* However, it does not constitute a complete version vector, as it only contains counters
* from peers included within the import blob.

View file

@ -1,6 +1,7 @@
use enum_as_inner::EnumAsInner;
use loro_internal::container::ContainerID;
use loro_internal::delta::{DeltaItem, TreeDiff};
use loro_internal::event::EventTriggerKind;
use loro_internal::handler::{TextDelta, ValueOrHandler};
use loro_internal::FxHashMap;
use loro_internal::{
@ -15,8 +16,7 @@ pub type Subscriber = Arc<dyn (for<'a> Fn(DiffEvent<'a>)) + Send + Sync>;
#[derive(Debug)]
pub struct DiffEvent<'a> {
pub local: bool,
pub from_checkout: bool,
pub triggered_by: EventTriggerKind,
pub origin: &'a str,
pub current_target: Option<ContainerID>,
pub events: Vec<ContainerDiff<'a>>,
@ -52,8 +52,7 @@ pub struct MapDelta<'a> {
impl<'a> From<DiffEventInner<'a>> for DiffEvent<'a> {
fn from(value: DiffEventInner<'a>) -> Self {
DiffEvent {
local: value.event_meta.local,
from_checkout: value.event_meta.from_checkout,
triggered_by: value.event_meta.by,
origin: &value.event_meta.origin,
current_target: value.current_target,
events: value.events.iter().map(|&diff| diff.into()).collect(),

View file

@ -315,7 +315,7 @@ impl LoroDoc {
/// doc.subscribe(
/// &text.id(),
/// Arc::new(move |event| {
/// assert!(event.local);
/// assert!(event.triggered_by.is_local());
/// for event in event.events {
/// let delta = event.diff.as_text().unwrap();
/// let d = TextDelta::Insert {

View file

@ -376,7 +376,10 @@ fn subscribe() {
doc.subscribe(
&text.id(),
Arc::new(move |event| {
assert!(event.local);
assert!(matches!(
event.triggered_by,
loro_internal::event::EventTriggerKind::Local
));
for event in event.events {
let delta = event.diff.as_text().unwrap();
let d = TextDelta::Insert {

View file

@ -11,13 +11,14 @@ import isEqual from "is-equal";
const Delta = Quill.import("delta");
// setDebug("*");
const EXPAND_CONFIG: { [key in string]: 'before' | 'after' | 'both' | 'none' } = {
bold: 'after',
italic: 'after',
underline: 'after',
link: 'none',
header: 'none',
}
const EXPAND_CONFIG: { [key in string]: "before" | "after" | "both" | "none" } =
{
bold: "after",
italic: "after",
underline: "after",
link: "none",
header: "none",
};
export class QuillBinding {
private richtext: LoroText;
@ -31,7 +32,7 @@ export class QuillBinding {
underline: { expand: "after" },
link: { expand: "none" },
header: { expand: "none" },
})
});
this.quill = quill;
this.richtext = doc.getText("text");
this.richtext.subscribe(doc, (event) => {
@ -122,9 +123,9 @@ export class QuillBinding {
for (const key of Object.keys(op.attributes)) {
let value = op.attributes[key];
if (value == null) {
this.richtext.unmark({ start: index, end, }, key)
this.richtext.unmark({ start: index, end }, key);
} else {
this.richtext.mark({ start: index, end }, key, value,)
this.richtext.mark({ start: index, end }, key, value);
}
}
}
@ -137,20 +138,20 @@ export class QuillBinding {
for (const key of Object.keys(op.attributes)) {
let value = op.attributes[key];
if (value == null) {
this.richtext.unmark({ start: index, end, }, key)
this.richtext.unmark({ start: index, end }, key);
} else {
this.richtext.mark({ start: index, end }, key, value)
this.richtext.mark({ start: index, end }, key, value);
}
}
}
index = end;
} else {
throw new Error("Not implemented")
throw new Error("Not implemented");
}
} else if (op.delete != null) {
this.richtext.delete(index, op.delete);
} else {
throw new Error("Unreachable")
throw new Error("Unreachable");
}
}
this.doc.commit();
@ -172,7 +173,7 @@ function assertEqual(a: Delta<string>[], b: Delta<string>[]): boolean {
/**
* Removes the ending '\n's if it has no attributes.
*
*
* Extract line-break to a single op
*
* Normalize attributes field
@ -212,7 +213,7 @@ export const normQuillDelta = (delta: Delta<string>[]) => {
}
}
const ans: Delta<string>[] = []
const ans: Delta<string>[] = [];
for (const span of delta) {
if (span.insert != null && span.insert.includes("\n")) {
const lines = span.insert.split("\n");
@ -224,7 +225,7 @@ export const normQuillDelta = (delta: Delta<string>[]) => {
if (i < lines.length - 1) {
const attr = { ...span.attributes };
const v: Delta<string> = { insert: "\n" };
for (const style of ['bold', 'link', 'italic', 'underline']) {
for (const style of ["bold", "link", "italic", "underline"]) {
if (attr && attr[style]) {
delete attr[style];
}

View file

@ -8,7 +8,6 @@ import {
LoroMap,
LoroText,
LoroTree,
LoroTreeNode,
OpId,
TreeID,
Value,
@ -56,18 +55,21 @@ export type Path = (number | string | TreeID)[];
/**
* A batch of events that created by a single `import`/`transaction`/`checkout`.
*
* @prop local - Indicates whether the event is local.
* @prop by - How the event is triggered.
* @prop origin - (Optional) Provides information about the origin of the event.
* @prop diff - Contains the differential information related to the event.
* @prop target - Identifies the container ID of the event's target.
* @prop path - Specifies the absolute path of the event's emitter, which can be an index of a list container or a key of a map container.
*/
export interface LoroEventBatch {
local: boolean;
/**
* If true, this event was triggered by a checkout.
* How the event is triggered.
*
* - `local`: The event is triggered by a local transaction.
* - `import`: The event is triggered by an import operation.
* - `checkout`: The event is triggered by a checkout operation.
*/
fromCheckout: boolean;
by: "local" | "import" | "checkout";
origin?: string;
/**
* The container ID of the current event receiver.

View file

@ -1,27 +1,33 @@
import { describe, expect, it } from "vitest";
import {
Loro,
} from "../src";
import { Loro } from "../src";
describe("Checkout", () => {
it("simple checkout", () => {
it("simple checkout", async () => {
const doc = new Loro();
doc.setPeerId(0n);
const text = doc.getText("text");
text.insert(0, "H");
doc.commit();
let triggered = false;
doc.subscribe((e) => {
expect(e.by).not.toBe("import");
expect(e.by === "checkout" || e.by === "local").toBeTruthy();
triggered = true;
});
const v = doc.frontiers();
text.insert(1, "i");
expect(doc.toJson()).toStrictEqual({
text: "Hi"
text: "Hi",
});
expect(doc.isDetached()).toBeFalsy();
doc.checkout([{ peer: "0", counter: 0 }]);
expect(doc.isDetached()).toBeTruthy();
expect(doc.toJson()).toStrictEqual({
text: "H"
text: "H",
});
await new Promise((resolve) => setTimeout(resolve, 1));
expect(triggered).toBeTruthy();
});
it("Chinese char", () => {
@ -34,19 +40,19 @@ describe("Checkout", () => {
v[0].counter -= 1;
doc.checkout(v);
expect(doc.toJson()).toStrictEqual({
text: "你好世"
text: "你好世",
});
v[0].counter -= 1;
doc.checkout(v);
expect(doc.toJson()).toStrictEqual({
text: "你好"
text: "你好",
});
v[0].counter -= 1;
doc.checkout(v);
expect(doc.toJson()).toStrictEqual({
text: "你"
text: "你",
});
})
});
it("two clients", () => {
const doc = new Loro();

View file

@ -17,6 +17,7 @@ describe("event", () => {
const loro = new Loro();
let lastEvent: undefined | LoroEventBatch;
loro.subscribe((event) => {
expect(event.by).toBe("local");
lastEvent = event;
});
const text = loro.getText("text");
@ -131,7 +132,6 @@ describe("event", () => {
const loro = new Loro();
let lastEvent: undefined | LoroEventBatch;
loro.subscribe((event) => {
console.log(event);
lastEvent = event;
});
const tree = loro.getTree("tree");
@ -328,6 +328,50 @@ describe("event", () => {
await new Promise((resolve) => setTimeout(resolve, 1));
expect(ran).toBeTruthy();
});
it("remote event", async () => {
const doc = new Loro();
const list = doc.getList("list");
list.insert(0, 123);
{
const doc2 = new Loro();
let triggered = false;
doc2.subscribe((event) => {
expect(event.by).toBe("import");
triggered = true;
});
doc2.import(doc.exportFrom());
await oneMs();
expect(triggered).toBeTruthy();
}
{
const doc2 = new Loro();
let triggered = false;
doc2.subscribe((event) => {
expect(event.by).toBe("import");
triggered = true;
});
doc2.import(doc.exportSnapshot());
await oneMs();
expect(triggered).toBeTruthy();
}
});
it("checkout event", async () => {
const doc = new Loro();
const list = doc.getList("list");
list.insert(0, 123);
doc.commit();
let triggered = false;
doc.subscribe((e) => {
expect(e.by).toBe("checkout");
triggered = true;
});
doc.checkout([]);
await oneMs();
expect(triggered).toBeTruthy();
});
});
function oneMs(): Promise<void> {

View file

@ -1,73 +1,69 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import {
Loro,
} from "../src";
import { Loro } from "../src";
import { Container, LoroText, OpId } from "../dist/loro";
import { setDebug } from "loro-wasm";
it("#211", () => {
const loro1 = new Loro()
loro1.setPeerId(0n)
const text1 = loro1.getText("text")
const loro1 = new Loro();
loro1.setPeerId(0n);
const text1 = loro1.getText("text");
const loro2 = new Loro()
loro2.setPeerId(1n)
const text2 = loro2.getText("text")
const loro2 = new Loro();
loro2.setPeerId(1n);
const text2 = loro2.getText("text");
console.log("[1] Insert T to #0")
text1.insert(0, 'T')
loro1.commit()
show(text1, loro1, text2, loro2)
console.log("[1] Insert T to #0");
text1.insert(0, "T");
loro1.commit();
show(text1, loro1, text2, loro2);
console.log("[2] Synchronize")
loro1.import(loro2.exportFrom(loro1.version()))
loro2.import(loro1.exportFrom(loro2.version()))
show(text1, loro1, text2, loro2)
const frontiers1After2 = loro1.frontiers()
const frontiers2After2 = loro2.frontiers()
console.log("[2] Synchronize");
loro1.import(loro2.exportFrom(loro1.version()));
loro2.import(loro1.exportFrom(loro2.version()));
show(text1, loro1, text2, loro2);
const frontiers1After2 = loro1.frontiers();
const frontiers2After2 = loro2.frontiers();
console.log("[3] Append A to #0")
text1.insert(1, 'A')
loro1.commit()
show(text1, loro1, text2, loro2)
console.log("[3] Append A to #0");
text1.insert(1, "A");
loro1.commit();
show(text1, loro1, text2, loro2);
console.log("[4] Append B to #1")
text2.insert(1, 'B')
loro2.commit()
show(text1, loro1, text2, loro2)
console.log("[4] Append B to #1");
text2.insert(1, "B");
loro2.commit();
show(text1, loro1, text2, loro2);
console.log("[5] Play back to the frontiers after 2")
loro1.checkout(frontiers1After2)
loro2.checkout(frontiers2After2)
show(text1, loro1, text2, loro2)
console.log("[5] Play back to the frontiers after 2");
loro1.checkout(frontiers1After2);
loro2.checkout(frontiers2After2);
show(text1, loro1, text2, loro2);
console.log("[6] Check both to the latest")
loro1.checkoutToLatest()
loro2.checkoutToLatest()
show(text1, loro1, text2, loro2)
const frontiers1Before7 = loro1.frontiers()
const frontiers2Before7 = loro2.frontiers()
console.log("[6] Check both to the latest");
loro1.checkoutToLatest();
loro2.checkoutToLatest();
show(text1, loro1, text2, loro2);
const frontiers1Before7 = loro1.frontiers();
const frontiers2Before7 = loro2.frontiers();
console.log("[7] Append B to #1")
text2.insert(2, 'B')
loro2.commit()
show(text1, loro1, text2, loro2)
console.log("[7] Append B to #1");
text2.insert(2, "B");
loro2.commit();
show(text1, loro1, text2, loro2);
console.log("[8] Play back to the frontiers before 7")
console.log("[8] Play back to the frontiers before 7");
console.log("----------------------------------------------------------");
loro1.checkout(frontiers1Before7)
loro1.checkout(frontiers1Before7);
console.log("----------------------------------------------------------");
loro2.checkout(frontiers2Before7)
show(text1, loro1, text2, loro2)
})
loro2.checkout(frontiers2Before7);
show(text1, loro1, text2, loro2);
});
function show(text1: LoroText, loro1: Loro, text2: LoroText, loro2: Loro) {
console.log(` #0 has content: ${JSON.stringify(text1.toString())}`)
console.log(` #0 has frontiers: ${showFrontiers(loro1.frontiers())}`)
console.log(` #1 has content: ${JSON.stringify(text2.toString())}`)
console.log(` #1 has frontiers: ${showFrontiers(loro2.frontiers())}`)
console.log(` #0 has content: ${JSON.stringify(text1.toString())}`);
console.log(` #0 has frontiers: ${showFrontiers(loro1.frontiers())}`);
console.log(` #1 has content: ${JSON.stringify(text2.toString())}`);
console.log(` #1 has frontiers: ${showFrontiers(loro2.frontiers())}`);
}
function showFrontiers(frontiers: OpId[]) {

View file

@ -129,15 +129,15 @@ describe("sync", () => {
const b = new Loro();
let a_version: undefined | VersionVector = undefined;
let b_version: undefined | VersionVector = undefined;
a.subscribe((e: { local: boolean }) => {
if (e.local) {
a.subscribe((e) => {
if (e.by == "local") {
const exported = a.exportFrom(a_version);
b.import(exported);
a_version = a.version();
}
});
b.subscribe((e: { local: boolean }) => {
if (e.local) {
b.subscribe((e) => {
if (e.by == "local") {
const exported = b.exportFrom(b_version);
a.import(exported);
b_version = b.version();

View file

@ -33,26 +33,33 @@ describe("richtext", () => {
expect(text.toString()).toBe("👨👩👦a");
});
it("emit event correctly", () => {
it("emit event correctly", async () => {
const doc = new Loro();
const text = doc.getText("text");
text.subscribe(doc, (event) => {
if (event.diff.type == "text") {
expect(event.diff.diff).toStrictEqual([
{
insert: "Hello",
attributes: {
bold: true,
let triggered = false;
text.subscribe(doc, (e) => {
for (const event of e.events) {
if (event.diff.type == "text") {
expect(event.diff.diff).toStrictEqual([
{
insert: "Hello",
attributes: {
bold: true,
},
},
},
{
insert: " World!",
},
] as Delta<string>[]);
{
insert: " World!",
},
] as Delta<string>[]);
triggered = true;
}
}
});
text.insert(0, "Hello World!");
text.mark({ start: 0, end: 5 }, "bold", true);
doc.commit();
await new Promise((r) => setTimeout(r, 1));
expect(triggered).toBeTruthy();
});
it("emit event from merging doc correctly", async () => {

View file

@ -47,24 +47,39 @@ describe("Frontiers", () => {
doc1.commit();
expect(() => {
doc1.cmpFrontiers([{ peer: "1", counter: 1 }], [{
peer: "2",
counter: 10,
}]);
doc1.cmpFrontiers(
[{ peer: "1", counter: 1 }],
[
{
peer: "2",
counter: 10,
},
],
);
}).toThrow();
expect(doc1.cmpFrontiers([], [{ peer: "1", counter: 1 }])).toBe(-1);
expect(doc1.cmpFrontiers([], [])).toBe(0);
expect(
doc1.cmpFrontiers([{ peer: "1", counter: 4 }], [{
peer: "2",
counter: 3,
}]),
doc1.cmpFrontiers(
[{ peer: "1", counter: 4 }],
[
{
peer: "2",
counter: 3,
},
],
),
).toBe(-1);
expect(
doc1.cmpFrontiers([{ peer: "1", counter: 5 }], [{
peer: "2",
counter: 3,
}]),
doc1.cmpFrontiers(
[{ peer: "1", counter: 5 }],
[
{
peer: "2",
counter: 3,
},
],
),
).toBe(1);
});
});
@ -77,9 +92,7 @@ it("peer id repr should be consistent", () => {
const f = doc.frontiers();
expect(f[0].peer).toBe(id);
const child = new LoroMap();
console.dir(child);
const map = doc.getList("list").insertContainer(0, child);
console.dir(child);
const mapId = map.id;
const peerIdInContainerId = mapId.split(":")[1].split("@")[1];
expect(peerIdInContainerId).toBe(id);
@ -115,9 +128,9 @@ describe("Version", () => {
const v = a.version();
const temp = a.vvToFrontiers(v);
expect(temp).toStrictEqual(a.frontiers());
expect(a.frontiers()).toStrictEqual(
[{ peer: "0", counter: 2 }] as OpId[],
);
expect(a.frontiers()).toStrictEqual([
{ peer: "0", counter: 2 },
] as OpId[]);
}
});
@ -156,7 +169,6 @@ it("get import blob metadata", () => {
expect(meta.startTimestamp).toBe(0);
expect(meta.endTimestamp).toBe(0);
expect(meta.isSnapshot).toBeFalsy();
console.log(meta.startFrontiers);
expect(meta.startFrontiers.length).toBe(0);
}