#![cfg(feature = "sync")]
pub mod common;
use std::sync::Arc;
use std::time::Duration;
use futures::{SinkExt, StreamExt};
use nodedb_lite::engine::crdt::CrdtEngine;
use nodedb_lite::sync::*;
use nodedb_types::sync::shape::{ShapeDefinition, ShapeType};
use nodedb_types::sync::wire::{
ResyncReason, ShapeDeltaMsg, ShapeSnapshotMsg, ShapeSubscribeMsg, SyncFrame, SyncMessageType,
};
use tokio_tungstenite::tungstenite::Message;
use common::origin::{OriginServer, connect_and_handshake};
async fn subscribe_and_recv_snapshot(
ws: &mut tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
>,
shape_id: &str,
collection: &str,
) -> ShapeSnapshotMsg {
let msg = ShapeSubscribeMsg {
shape: ShapeDefinition {
shape_id: shape_id.into(),
tenant_id: 0,
shape_type: ShapeType::Document {
collection: collection.into(),
predicate: Vec::new(),
},
description: "interop-shape-test".into(),
field_filter: vec![],
},
};
let bytes = SyncFrame::try_encode(SyncMessageType::ShapeSubscribe, &msg)
.expect("encode ShapeSubscribe")
.to_bytes();
ws.send(Message::Binary(bytes.into()))
.await
.expect("send ShapeSubscribe");
let resp = tokio::time::timeout(Duration::from_secs(10), ws.next())
.await
.expect("timeout waiting for ShapeSnapshot")
.expect("stream closed before ShapeSnapshot")
.expect("WebSocket error waiting for ShapeSnapshot");
let frame =
SyncFrame::from_bytes(resp.into_data().as_ref()).expect("decode ShapeSnapshot frame");
assert_eq!(
frame.msg_type,
SyncMessageType::ShapeSnapshot,
"expected ShapeSnapshot, got {:?}",
frame.msg_type
);
frame
.decode_body::<ShapeSnapshotMsg>()
.expect("decode ShapeSnapshotMsg body")
}
#[tokio::test]
async fn shape_snapshot_populates_sync_client_state() {
let Some(_server) = OriginServer::try_spawn() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_server.ws_url).await;
let client = Arc::new(SyncClient::new(SyncConfig::new(_server.ws_url, "")));
{
let mut shapes = client.shapes().lock().await;
shapes.subscribe(ShapeDefinition {
shape_id: "§9.1-shape".into(),
tenant_id: 0,
shape_type: ShapeType::Document {
collection: "shape_test_9_1".into(),
predicate: Vec::new(),
},
description: "§9.1 interop test".into(),
field_filter: vec![],
});
}
let snapshot = subscribe_and_recv_snapshot(&mut ws, "§9.1-shape", "shape_test_9_1").await;
client.handle_shape_snapshot(&snapshot).await;
let shapes = client.shapes().lock().await;
let sub = shapes.get("§9.1-shape").expect("subscription must exist");
assert!(
sub.snapshot_loaded,
"snapshot_loaded must be true after Origin delivers ShapeSnapshot"
);
assert_eq!(
sub.last_lsn, snapshot.snapshot_lsn,
"last_lsn must equal the snapshot_lsn from Origin"
);
}
#[tokio::test]
async fn shape_delta_advances_lsn_after_snapshot() {
let Some(_server) = OriginServer::try_spawn() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_server.ws_url).await;
let client = Arc::new(SyncClient::new(SyncConfig::new(_server.ws_url, "")));
{
let mut shapes = client.shapes().lock().await;
shapes.subscribe(ShapeDefinition {
shape_id: "§9.2-shape".into(),
tenant_id: 0,
shape_type: ShapeType::Document {
collection: "shape_test_9_2".into(),
predicate: Vec::new(),
},
description: "§9.2 interop test".into(),
field_filter: vec![],
});
}
let snapshot = subscribe_and_recv_snapshot(&mut ws, "§9.2-shape", "shape_test_9_2").await;
client.handle_shape_snapshot(&snapshot).await;
let baseline_lsn = {
let shapes = client.shapes().lock().await;
shapes.get("§9.2-shape").expect("sub").last_lsn
};
let delta_lsn = baseline_lsn + 10;
let delta_msg = ShapeDeltaMsg {
shape_id: "§9.2-shape".into(),
collection: "shape_test_9_2".into(),
document_id: "doc-delta".into(),
operation: "INSERT".into(),
delta: vec![0xDE, 0xAD], lsn: delta_lsn,
};
client.handle_shape_delta(&delta_msg).await;
let shapes = client.shapes().lock().await;
assert_eq!(
shapes.get("§9.2-shape").expect("sub").last_lsn,
delta_lsn,
"last_lsn must advance to the delta's LSN"
);
}
#[tokio::test]
async fn sequence_gap_detection_and_resync_on_real_connection() {
let Some(_server) = OriginServer::try_spawn() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_server.ws_url).await;
let client = Arc::new(SyncClient::new(SyncConfig::new(_server.ws_url, "")));
{
let mut shapes = client.shapes().lock().await;
shapes.subscribe(ShapeDefinition {
shape_id: "§9.3-shape".into(),
tenant_id: 0,
shape_type: ShapeType::Document {
collection: "shape_test_9_3".into(),
predicate: Vec::new(),
},
description: "§9.3 interop test".into(),
field_filter: vec![],
});
}
let snapshot = subscribe_and_recv_snapshot(&mut ws, "§9.3-shape", "shape_test_9_3").await;
client.handle_shape_snapshot(&snapshot).await;
let base = snapshot.snapshot_lsn;
let no_gap = client.check_sequence_gap("§9.3-shape", base + 1).await;
assert!(
no_gap.is_none(),
"first delta (base+1) must not trigger a resync"
);
let resync = client.check_sequence_gap("§9.3-shape", base + 5).await;
let resync_msg = resync.expect("gap of 4 must trigger a ResyncRequest");
assert_eq!(
resync_msg.from_mutation_id,
base + 2,
"catch-up must start from the first missing LSN"
);
assert!(
matches!(
resync_msg.reason,
ResyncReason::SequenceGap {
expected,
received,
} if expected == base + 2 && received == base + 5
),
"SequenceGap reason must carry correct expected/received: {:?}",
resync_msg.reason
);
client.set_pending_resync(resync_msg.clone()).await;
let pending = client.take_pending_resync().await;
assert!(
pending.is_some(),
"pending resync must be retrievable by the push loop"
);
let frame = SyncFrame::try_encode(SyncMessageType::ResyncRequest, &pending.unwrap())
.expect("ResyncRequest frame must be encodable for wire send");
assert_eq!(frame.msg_type, SyncMessageType::ResyncRequest);
let no_second = client.check_sequence_gap("§9.3-shape", base + 20).await;
assert!(
no_second.is_none(),
"second gap must not fire a second resync (one per connection)"
);
client.reset_sequence_tracking().await;
let after_reset = client.check_sequence_gap("§9.3-shape", base + 2).await;
assert!(
after_reset.is_none(),
"after reset, contiguous delta must not trigger resync"
);
}
#[tokio::test]
async fn shape_snapshot_data_queryable_after_import() {
let mut remote = CrdtEngine::new(9004).expect("remote CRDT engine");
remote
.upsert(
"query_test",
"doc-q1",
&[
("name", loro::LoroValue::String("Alice".into())),
("active", loro::LoroValue::Bool(true)),
],
)
.expect("remote upsert");
let snapshot_bytes = remote
.export_snapshot("query_test")
.expect("export snapshot");
assert!(!snapshot_bytes.is_empty(), "snapshot must have content");
let mut local = CrdtEngine::new(9005).expect("local CRDT engine");
local
.import_remote("query_test", &snapshot_bytes)
.expect("import_remote must succeed on valid snapshot bytes");
assert!(
local.exists("query_test", "doc-q1"),
"doc-q1 must exist in local engine after snapshot import"
);
let value = local
.read("query_test", "doc-q1")
.expect("read after import must return Some");
let debug_repr = format!("{value:?}");
assert!(
debug_repr.contains("Alice"),
"imported document's name field must round-trip through snapshot; got: {debug_repr}"
);
let name_field = local.read_field("query_test", "doc-q1", "name");
assert_eq!(
name_field,
Some(loro::LoroValue::String("Alice".into())),
"read_field('name') must return 'Alice' after snapshot import"
);
}