mod blob;
mod bootstrap;
mod commit;
mod contract;
mod http;
mod platform;
mod protocol;
mod repository;
mod runtime;
#[cfg(test)]
mod simulation_tests;
#[cfg(test)]
mod upload_metrics;
mod upload_plan;
mod upload_proof;
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::Duration;
use crate::LixError;
use parking_lot::RwLock;
#[cfg(feature = "server-protocol")]
pub(crate) use blob::validate_sync_blob_manifest;
pub(crate) use bootstrap::{
SyncBootstrapAdmission, inspect_sync_bootstrap_with_adapter, install_sync_bootstrap,
prepare_sync_bootstrap,
};
pub(crate) use commit::{
SYNC_CHECKPOINT_SOURCE_SPACE, SYNC_MATERIALIZED_STATE_ALIAS_SPACE,
stage_delete_materialized_sync_state_alias, stage_delete_sync_checkpoint_source,
stage_sync_checkpoint_source,
};
pub(crate) use commit::{
SyncCommit, SyncCommitMemberRef, SyncCommitStateAlias, encode_sync_commit_member,
};
pub(crate) use contract::SyncTransport;
pub(crate) use http::normalize_sync_locator;
pub(crate) use platform::sleep;
pub(crate) use platform::{AuthorityHttp, authority_http};
#[cfg(target_family = "wasm")]
#[doc(hidden)]
pub use platform::{
BROWSER_TRANSPORT_CONFIG_HEADER, register_browser_sync_transport,
unregister_browser_sync_transport,
};
pub(crate) use platform::{SyncTransportBounds, SyncTransportFuture};
#[cfg(feature = "server-protocol")]
pub(crate) use protocol::SyncRefUpdate;
pub(crate) use protocol::{
SyncBlobChunk, SyncBlobManifest, SyncBlobRegistration, SyncBranchHead,
SyncCheckpointInventoryPage, SyncCommitHeader, SyncEvent, SyncHistoryBoundary,
SyncHistoryResponse, SyncPushRequest, SyncPushResponse, SyncRepositoryPullResponse,
SyncSnapshotRow, SyncSnapshotRowPage, encoded_delta_event_len,
};
#[cfg(feature = "server-protocol")]
pub(crate) use repository::admit_sync_authority_storage;
pub(crate) use repository::has_any_sync_replica_state;
pub(crate) use repository::{
AUTHORITY_STATE_VALUE, SYNC_AUTHORITY_STATE_SPACE, SYNC_REPLICA_STATE_SPACE,
SYNC_REPOSITORY_EVENT_SPACE, SYNC_SEQUENCE_SPACE, authority_state_key,
load_pending_sync_export_commit_ids, load_replayable_repository_event_commit_ids,
replica_state_key, stage_repository_transaction_event, stage_sync_restore_intents,
validate_repository_transaction_event_transfer,
};
pub(crate) use runtime::{SyncDemand, SyncDemandRetry, SyncRuntime, activate_sync_mode};
pub(crate) use upload_plan::{
SYNC_UPLOAD_GENERATION_SPACE, stage_invalidate as stage_upload_plan_invalidation,
};
pub(crate) use upload_proof::SYNC_UPLOAD_PROOF_SPACE;
pub(crate) const MAX_SYNC_PULL_RESPONSE_BYTES: usize = 64 * 1024 * 1024;
pub(crate) const MAX_SYNC_HISTORY_PAGE_SIZE: usize = 100;
pub(crate) const MAX_SYNC_BLOB_BATCH_ITEMS: usize = 16;
pub(crate) const MAX_SYNC_REQUEST_ITEMS: usize = 512;
pub(crate) const SYNC_LONG_POLL_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) const SYNC_PROTOCOL_VERSION: u32 = 9;
pub(crate) const SYNC_PROTOCOL_VERSION_HEADER: &str = "lix-sync-protocol-version";
pub(crate) const SYNC_PROTOCOL_MISMATCH_CODE: &str = "LIX_SYNC_PROTOCOL_MISMATCH";
pub(crate) const SYNC_REPOSITORY_ID_MISMATCH_CODE: &str = "LIX_SYNC_REPOSITORY_ID_MISMATCH";
pub(crate) const SYNC_IMMUTABLE_OBJECT_MISMATCH_CODE: &str = "LIX_SYNC_IMMUTABLE_OBJECT_MISMATCH";
const MAX_SYNC_REMOTE_ID_BYTES: usize = 4 * 1024;
#[derive(Clone, Copy, Debug)]
pub(crate) struct CertifiedReplicaWriteCapability {
_private: (),
}
fn certified_replica_write_capability() -> CertifiedReplicaWriteCapability {
CertifiedReplicaWriteCapability { _private: () }
}
pub(crate) fn sync_server_protocol_mismatch(server_version: Option<u32>) -> LixError {
let server = server_version
.map(|version| version.to_string())
.unwrap_or_else(|| "missing".to_owned());
LixError::new(
SYNC_PROTOCOL_MISMATCH_CODE,
format!(
"incompatible sync protocol: client version {SYNC_PROTOCOL_VERSION}, server version {server}; upgrade the client and server to compatible Lix versions"
),
)
.with_details(serde_json::json!({
"clientSyncProtocolVersion": SYNC_PROTOCOL_VERSION,
"serverSyncProtocolVersion": server_version,
}))
}
pub(crate) fn sync_server_protocol_missing_field(field: &str) -> LixError {
LixError::new(
SYNC_PROTOCOL_MISMATCH_CODE,
format!("incompatible sync protocol: server handshake omitted required {field}"),
)
.with_details(serde_json::json!({ "missingField": field }))
}
pub(crate) fn sync_repository_id_mismatch(local: &str, authority: &str) -> LixError {
LixError::new(
SYNC_REPOSITORY_ID_MISMATCH_CODE,
"sync authority lixId does not match the local repository",
)
.with_details(serde_json::json!({
"localLixId": local,
"authorityLixId": authority,
}))
}
#[cfg(feature = "server-protocol")]
pub(crate) fn sync_client_protocol_mismatch(client_version: Option<u32>) -> LixError {
let client = client_version
.map(|version| version.to_string())
.unwrap_or_else(|| "invalid".to_owned());
LixError::new(
SYNC_PROTOCOL_MISMATCH_CODE,
format!(
"incompatible sync protocol: client version {client}, server version {SYNC_PROTOCOL_VERSION}; upgrade the client and server to compatible Lix versions"
),
)
.with_details(serde_json::json!({
"clientSyncProtocolVersion": client_version,
"serverSyncProtocolVersion": SYNC_PROTOCOL_VERSION,
}))
}
pub(crate) fn validate_blake3_id(value: &str, context: &str) -> Result<(), LixError> {
if value.len() == 64
&& value
.as_bytes()
.iter()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte))
{
return Ok(());
}
Err(LixError::new(
LixError::CODE_INVALID_PARAM,
format!("{context} must be 64 lowercase hexadecimal characters"),
))
}
pub(crate) fn validate_sync_remote_id(remote_id: &str) -> Result<(), LixError> {
if remote_id.is_empty() || remote_id.len() > MAX_SYNC_REMOTE_ID_BYTES {
return Err(LixError::new(
LixError::CODE_INVALID_PARAM,
format!("sync remoteId must contain 1 to {MAX_SYNC_REMOTE_ID_BYTES} bytes"),
));
}
Ok(())
}
pub(crate) fn validate_sync_branch_id(branch_id: &str) -> Result<(), LixError> {
if crate::storage_codec::id_string::uuid_bytes_from_canonical(branch_id).is_none() {
return Err(LixError::new(
LixError::CODE_INVALID_PARAM,
"sync branchId must be a canonical UUID",
));
}
Ok(())
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(u8)]
pub(crate) enum SyncRole {
#[default]
Disabled,
Authority,
Replica,
}
#[derive(Clone, Debug)]
pub(crate) struct SyncModeState {
role: Arc<AtomicU8>,
replica_remote_id: Arc<RwLock<Option<Arc<str>>>>,
change_watch: tokio::sync::watch::Sender<u64>,
}
impl Default for SyncModeState {
fn default() -> Self {
Self {
role: Arc::new(AtomicU8::new(SyncRole::Disabled as u8)),
replica_remote_id: Arc::new(RwLock::new(None)),
change_watch: tokio::sync::watch::channel(0).0,
}
}
}
impl SyncModeState {
pub(crate) fn role(&self) -> SyncRole {
match self.role.load(Ordering::Acquire) {
value if value == SyncRole::Disabled as u8 => SyncRole::Disabled,
value if value == SyncRole::Authority as u8 => SyncRole::Authority,
value if value == SyncRole::Replica as u8 => SyncRole::Replica,
_ => unreachable!("sync role stores only enum discriminants"),
}
}
pub(crate) fn set_role(&self, role: SyncRole) {
self.role.store(role as u8, Ordering::Release);
}
pub(crate) fn replica_remote_id(&self) -> Option<Arc<str>> {
self.replica_remote_id.read().clone()
}
pub(crate) fn set_replica_remote_id(&self, remote_id: impl Into<Arc<str>>) {
*self.replica_remote_id.write() = Some(remote_id.into());
}
pub(crate) fn change_watcher(&self) -> tokio::sync::watch::Receiver<u64> {
self.change_watch.subscribe()
}
pub(crate) fn notify_sync_change(&self) {
self.change_watch
.send_modify(|version| *version = version.wrapping_add(1));
}
}