max / audiofiles
4 files changed,
+26 insertions,
-24 deletions
| @@ -16,8 +16,7 @@ | |||
| 16 | 16 | //! `device_id`s are the design assumption. | |
| 17 | 17 | ||
| 18 | 18 | use rusqlite::{Connection, OptionalExtension}; | |
| 19 | - | use synckit_client::Hlc; | |
| 20 | - | use uuid::Uuid; | |
| 19 | + | use synckit_client::{DeviceId, Hlc}; | |
| 21 | 20 | ||
| 22 | 21 | use super::{get_sync_state, set_sync_state}; | |
| 23 | 22 | use crate::error::Result; | |
| @@ -30,7 +29,7 @@ fn now_ms() -> i64 { | |||
| 30 | 29 | /// Load the persisted device clock. Returns the zero clock on first sync (no | |
| 31 | 30 | /// persisted value); on a *corrupt* persisted value it seeds from physical time | |
| 32 | 31 | /// rather than zero — see below. | |
| 33 | - | pub(crate) fn load_clock(conn: &Connection, node: Uuid) -> Hlc { | |
| 32 | + | pub(crate) fn load_clock(conn: &Connection, node: DeviceId) -> Hlc { | |
| 34 | 33 | match get_sync_state(conn, "last_hlc") { | |
| 35 | 34 | Ok(s) if !s.is_empty() => serde_json::from_str(&s).unwrap_or_else(|e| { | |
| 36 | 35 | // Corrupt persisted clock: do NOT reset to zero. Zero sends the clock | |
| @@ -87,7 +86,7 @@ pub(crate) fn set_committed(conn: &Connection, table: &str, row_id: &str, hlc: & | |||
| 87 | 86 | /// | |
| 88 | 87 | /// Idempotent: a row that already carries an HLC keeps it, so the value is stable | |
| 89 | 88 | /// whether it is read first by conflict detection (pull) or by push. | |
| 90 | - | pub(crate) fn stamp_pending(conn: &Connection, node: Uuid) -> Result<()> { | |
| 89 | + | pub(crate) fn stamp_pending(conn: &Connection, node: DeviceId) -> Result<()> { | |
| 91 | 90 | let ids: Vec<i64> = { | |
| 92 | 91 | let mut stmt = conn.prepare( | |
| 93 | 92 | "SELECT id FROM sync_changelog WHERE pushed = 0 AND hlc IS NULL ORDER BY id", | |
| @@ -114,7 +113,7 @@ pub(crate) fn stamp_pending(conn: &Connection, node: Uuid) -> Result<()> { | |||
| 114 | 113 | ||
| 115 | 114 | /// Advance the device clock by observing a remote HLC, so our next local edit is | |
| 116 | 115 | /// ordered after every change we've seen (standard HLC receive rule). | |
| 117 | - | pub(crate) fn observe(conn: &Connection, node: Uuid, remote: Hlc) -> Result<()> { | |
| 116 | + | pub(crate) fn observe(conn: &Connection, node: DeviceId, remote: Hlc) -> Result<()> { | |
| 118 | 117 | let next = Hlc::observe(load_clock(conn, node), remote, now_ms(), node); | |
| 119 | 118 | store_clock(conn, &next) | |
| 120 | 119 | } | |
| @@ -122,6 +121,7 @@ pub(crate) fn observe(conn: &Connection, node: Uuid, remote: Hlc) -> Result<()> | |||
| 122 | 121 | #[cfg(test)] | |
| 123 | 122 | mod tests { | |
| 124 | 123 | use super::*; | |
| 124 | + | use uuid::Uuid; | |
| 125 | 125 | ||
| 126 | 126 | fn mem() -> Connection { | |
| 127 | 127 | // A standalone in-memory DB with just the tables the HLC layer touches. | |
| @@ -142,7 +142,7 @@ mod tests { | |||
| 142 | 142 | #[test] | |
| 143 | 143 | fn stamp_pending_assigns_monotonic_hlcs() { | |
| 144 | 144 | let conn = mem(); | |
| 145 | - | let node = Uuid::from_u128(1); | |
| 145 | + | let node = DeviceId::new(Uuid::from_u128(1)); | |
| 146 | 146 | for i in 0..3 { | |
| 147 | 147 | conn.execute( | |
| 148 | 148 | "INSERT INTO sync_changelog (table_name, op, row_id) VALUES ('tags', 'INSERT', ?1)", | |
| @@ -178,7 +178,7 @@ mod tests { | |||
| 178 | 178 | #[test] | |
| 179 | 179 | fn corrupt_clock_seeds_forward_not_zero() { | |
| 180 | 180 | let conn = mem(); | |
| 181 | - | let node = Uuid::from_u128(1); | |
| 181 | + | let node = DeviceId::new(Uuid::from_u128(1)); | |
| 182 | 182 | // First sync (empty) is genuinely the zero clock. | |
| 183 | 183 | assert_eq!(load_clock(&conn, node), Hlc::zero(node)); | |
| 184 | 184 | // A corrupt persisted clock must NOT reset to zero (that moves the clock | |
| @@ -194,7 +194,7 @@ mod tests { | |||
| 194 | 194 | #[test] | |
| 195 | 195 | fn ledger_roundtrip_and_gate() { | |
| 196 | 196 | let conn = mem(); | |
| 197 | - | let node = Uuid::from_u128(7); | |
| 197 | + | let node = DeviceId::new(Uuid::from_u128(7)); | |
| 198 | 198 | assert!(committed_hlc(&conn, "tags", "r1").is_none()); | |
| 199 | 199 | let older = Hlc { wall_ms: 100, counter: 0, node }; | |
| 200 | 200 | let newer = Hlc { wall_ms: 200, counter: 0, node }; | |
| @@ -210,8 +210,8 @@ mod tests { | |||
| 210 | 210 | #[test] | |
| 211 | 211 | fn observe_keeps_clock_ahead() { | |
| 212 | 212 | let conn = mem(); | |
| 213 | - | let node = Uuid::from_u128(2); | |
| 214 | - | let remote = Hlc { wall_ms: i64::MAX / 2, counter: 5, node: Uuid::from_u128(99) }; | |
| 213 | + | let node = DeviceId::new(Uuid::from_u128(2)); | |
| 214 | + | let remote = Hlc { wall_ms: i64::MAX / 2, counter: 5, node: DeviceId::new(Uuid::from_u128(99)) }; | |
| 215 | 215 | observe(&conn, node, remote).unwrap(); | |
| 216 | 216 | let clock = load_clock(&conn, node); | |
| 217 | 217 | assert!(clock >= remote, "local clock must dominate the observed remote"); |
| @@ -28,7 +28,7 @@ pub(crate) fn apply_pulled( | |||
| 28 | 28 | if remote.is_empty() { | |
| 29 | 29 | return Ok(0); | |
| 30 | 30 | } | |
| 31 | - | let node = our_device.as_uuid(); | |
| 31 | + | let node = our_device; | |
| 32 | 32 | ||
| 33 | 33 | // Advance our clock past every remote change (HLC receive rule), and stamp any | |
| 34 | 34 | // un-pushed local changes so conflict detection has clocks to compare. | |
| @@ -111,9 +111,11 @@ fn load_local_pending(conn: &Connection) -> Result<Vec<ChangeEntry>> { | |||
| 111 | 111 | .map(|dt| dt.with_timezone(&chrono::Utc)) | |
| 112 | 112 | .unwrap_or_else(|_| chrono::Utc::now()); | |
| 113 | 113 | let data = data.and_then(|d| serde_json::from_str(&d).ok()); | |
| 114 | + | // A pending row lacking an HLC is a legacy/pre-HLC entry: floor it (nil | |
| 115 | + | // node, always loses) — it is re-stamped by `stamp_pending` before push. | |
| 114 | 116 | let hlc = hlc_json | |
| 115 | 117 | .and_then(|s| serde_json::from_str(&s).ok()) | |
| 116 | - | .unwrap_or_default(); | |
| 118 | + | .unwrap_or_else(|| synckit_client::Hlc::zero(synckit_client::DeviceId::nil())); | |
| 117 | 119 | out.push(ChangeEntry { table, op, row_id, timestamp: ts, hlc, data, extra: Default::default() }); | |
| 118 | 120 | } | |
| 119 | 121 | Ok(out) | |
| @@ -523,8 +525,8 @@ mod tests { | |||
| 523 | 525 | // Two changes to the same row within one batch (e.g. two devices), the | |
| 524 | 526 | // HLC-newer one arriving FIRST in seq order. Collapsing must keep the | |
| 525 | 527 | // newer HLC, not the later-in-vec entry. | |
| 526 | - | let older = Hlc { wall_ms: 100, counter: 0, node: Uuid::from_u128(1) }; | |
| 527 | - | let newer = Hlc { wall_ms: 200, counter: 0, node: Uuid::from_u128(2) }; | |
| 528 | + | let older = Hlc { wall_ms: 100, counter: 0, node: DeviceId::new(Uuid::from_u128(1)) }; | |
| 529 | + | let newer = Hlc { wall_ms: 200, counter: 0, node: DeviceId::new(Uuid::from_u128(2)) }; | |
| 528 | 530 | ||
| 529 | 531 | let mut newer_entry = entry("k1", newer); | |
| 530 | 532 | newer_entry.data = Some(serde_json::json!({"sample_hash": "h1", "tag": "newer"})); | |
| @@ -544,7 +546,7 @@ mod tests { | |||
| 544 | 546 | ||
| 545 | 547 | #[test] | |
| 546 | 548 | fn collapse_preserves_distinct_rows() { | |
| 547 | - | let h = Hlc { wall_ms: 100, counter: 0, node: Uuid::from_u128(1) }; | |
| 549 | + | let h = Hlc { wall_ms: 100, counter: 0, node: DeviceId::new(Uuid::from_u128(1)) }; | |
| 548 | 550 | let out = collapse_to_max_hlc(vec![entry("k1", h), entry("k2", h)]); | |
| 549 | 551 | assert_eq!(out.len(), 2); | |
| 550 | 552 | } | |
| @@ -554,7 +556,7 @@ mod tests { | |||
| 554 | 556 | // The new behavior: applying a remote change records its HLC as that row's | |
| 555 | 557 | // committed clock, so a later stale remote is gated against it. | |
| 556 | 558 | let conn = setup_conn(); | |
| 557 | - | let hlc = Hlc { wall_ms: 300, counter: 0, node: Uuid::from_u128(2) }; | |
| 559 | + | let hlc = Hlc { wall_ms: 300, counter: 0, node: DeviceId::new(Uuid::from_u128(2)) }; | |
| 558 | 560 | let applied = apply_remote_changes(&conn, &[entry("k1", hlc)]).unwrap(); | |
| 559 | 561 | assert_eq!(applied, 1); | |
| 560 | 562 | let tag: String = conn | |
| @@ -692,7 +694,7 @@ mod tests { | |||
| 692 | 694 | ) | |
| 693 | 695 | .unwrap(); | |
| 694 | 696 | ||
| 695 | - | let hlc = Hlc { wall_ms: 100, counter: 0, node: Uuid::from_u128(1) }; | |
| 697 | + | let hlc = Hlc { wall_ms: 100, counter: 0, node: DeviceId::new(Uuid::from_u128(1)) }; | |
| 696 | 698 | let mk = |sample_hash: &str, tag: &str| ChangeEntry { | |
| 697 | 699 | table: "tags".into(), | |
| 698 | 700 | op: ChangeOp::Insert, |
| @@ -271,7 +271,7 @@ mod tests { | |||
| 271 | 271 | op, | |
| 272 | 272 | row_id: row_id.to_string(), | |
| 273 | 273 | timestamp: chrono::Utc::now(), | |
| 274 | - | hlc: Default::default(), | |
| 274 | + | hlc: synckit_client::Hlc::zero(synckit_client::DeviceId::nil()), | |
| 275 | 275 | data, | |
| 276 | 276 | extra: Default::default(), | |
| 277 | 277 | } | |
| @@ -1612,7 +1612,7 @@ mod tests { | |||
| 1612 | 1612 | }, | |
| 1613 | 1613 | row_id, | |
| 1614 | 1614 | timestamp: chrono::Utc::now(), | |
| 1615 | - | hlc: Default::default(), | |
| 1615 | + | hlc: synckit_client::Hlc::zero(synckit_client::DeviceId::nil()), | |
| 1616 | 1616 | data: data.and_then(|d| serde_json::from_str(&d).ok()), | |
| 1617 | 1617 | extra: Default::default(), | |
| 1618 | 1618 | }) |
| @@ -249,7 +249,7 @@ fn prepare_batch(rows: Vec<ChangelogRow>, node: Uuid) -> PreparedBatch { | |||
| 249 | 249 | // if a row somehow lacks one (defensive — keeps the push going). | |
| 250 | 250 | let hlc = hlc_json | |
| 251 | 251 | .and_then(|s| serde_json::from_str(&s).ok()) | |
| 252 | - | .unwrap_or_else(|| synckit_client::Hlc::zero(node)); | |
| 252 | + | .unwrap_or_else(|| synckit_client::Hlc::zero(DeviceId::new(node))); | |
| 253 | 253 | ||
| 254 | 254 | pushed_ids.push(id); | |
| 255 | 255 | Some(ChangeEntry { | |
| @@ -313,7 +313,7 @@ async fn push_one_batch( | |||
| 313 | 313 | // Mint an HLC for every un-stamped pending change before reading them, so | |
| 314 | 314 | // each pushed ChangeEntry carries a real clock (was Default/zero, which | |
| 315 | 315 | // made every local edit lose conflict resolution). | |
| 316 | - | super::hlc::stamp_pending(&conn, node)?; | |
| 316 | + | super::hlc::stamp_pending(&conn, DeviceId::new(node))?; | |
| 317 | 317 | let mut stmt = conn.prepare( | |
| 318 | 318 | "SELECT id, table_name, op, row_id, timestamp, data, hlc | |
| 319 | 319 | FROM sync_changelog | |
| @@ -385,7 +385,7 @@ mod tests { | |||
| 385 | 385 | use synckit_client::Hlc; | |
| 386 | 386 | ||
| 387 | 387 | fn hlc(wall_ms: i64, node: Uuid) -> Hlc { | |
| 388 | - | Hlc { wall_ms, counter: 0, node } | |
| 388 | + | Hlc { wall_ms, counter: 0, node: DeviceId::new(node) } | |
| 389 | 389 | } | |
| 390 | 390 | ||
| 391 | 391 | /// (id, table, op, row_id, timestamp, data, hlc) with sane defaults. | |
| @@ -465,8 +465,8 @@ mod tests { | |||
| 465 | 465 | ]; | |
| 466 | 466 | let b = prepare_batch(rows, node); | |
| 467 | 467 | assert_eq!(b.changes.len(), 2); | |
| 468 | - | assert_eq!(b.changes[0].hlc, Hlc::zero(node)); | |
| 469 | - | assert_eq!(b.changes[1].hlc, Hlc::zero(node)); | |
| 468 | + | assert_eq!(b.changes[0].hlc, Hlc::zero(DeviceId::new(node))); | |
| 469 | + | assert_eq!(b.changes[1].hlc, Hlc::zero(DeviceId::new(node))); | |
| 470 | 470 | } | |
| 471 | 471 | ||
| 472 | 472 | #[test] |