1use std::collections::BTreeMap;
34use std::sync::{Arc, Mutex};
35
36use serde::{Deserialize, Serialize};
37
38use crate::checkpoint::Checkpoint;
39use crate::crypto::WrappedOrgKey;
40use crate::lease::{InMemoryLeaseCoordinator, Lease, LeaseCoordinator, LeaseError};
41use crate::oplog::{Hlc, OpRecord, WallClock};
42use crate::org_key_directory::{
43 FsOrgKeyDirectory, MemberPublicKey, OrgKeyDirectory, OrgKeyDirectoryError,
44};
45use crate::relay::{
46 AckOutcome, Frontier, GcReport, PullResult, PushOutcome, Relay, RelayConfig, RelayError,
47 RosterEntry,
48};
49
50#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum TransportError {
55 Unavailable(String),
58 Unauthorized(String),
60 Protocol(String),
62 FrontierTruncated {
68 device_id: String,
69 dropped_below: u64,
70 },
71}
72
73impl std::fmt::Display for TransportError {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 TransportError::Unavailable(m) => write!(f, "sync transport unavailable: {m}"),
77 TransportError::Unauthorized(m) => write!(f, "sync transport unauthorized: {m}"),
78 TransportError::Protocol(m) => write!(f, "sync transport protocol error: {m}"),
79 TransportError::FrontierTruncated {
80 device_id,
81 dropped_below,
82 } => write!(
83 f,
84 "sync transport frontier truncated: device {device_id} ops below seq \
85 {dropped_below} were GC'd (cold-bootstrap from checkpoint)"
86 ),
87 }
88 }
89}
90impl std::error::Error for TransportError {}
91
92impl From<TransportError> for RelayError {
93 fn from(e: TransportError) -> Self {
94 match e {
95 TransportError::FrontierTruncated {
98 device_id,
99 dropped_below,
100 } => RelayError::FrontierTruncated {
101 device_id,
102 dropped_below,
103 },
104 other => RelayError::Io(std::io::Error::other(other.to_string())),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
115pub enum LeaseWire {
116 Granted(Lease),
118 Held {
120 holder: String,
121 epoch: u64,
122 expires_at_ms: u64,
123 },
124 Lost {
126 claimed_epoch: u64,
127 current_epoch: u64,
128 },
129 Released,
131 Current(Option<Lease>),
133}
134
135pub trait SyncTransport: Send + Sync {
141 fn enroll(&self, scope: &str, device_id: &str) -> Result<RosterEntry, TransportError>;
142 fn push(
143 &self,
144 scope: &str,
145 device_id: &str,
146 ops: &[OpRecord],
147 ) -> Result<PushOutcome, TransportError>;
148 fn pull(
149 &self,
150 scope: &str,
151 device_id: &str,
152 since: &Frontier,
153 ) -> Result<PullResult, TransportError>;
154 fn ack(
155 &self,
156 scope: &str,
157 device_id: &str,
158 frontier: Hlc,
159 ) -> Result<AckOutcome, TransportError>;
160 fn checkpoint_put(
161 &self,
162 scope: &str,
163 device_id: &str,
164 checkpoint: &Checkpoint,
165 ) -> Result<bool, TransportError>;
166 fn checkpoint_get(&self, scope: &str) -> Result<Option<Checkpoint>, TransportError>;
167 fn roster(&self, scope: &str) -> Result<Vec<RosterEntry>, TransportError>;
168 fn stable_frontier(&self, scope: &str) -> Result<Option<Hlc>, TransportError>;
169 fn gc(&self, scope: &str) -> Result<GcReport, TransportError>;
170
171 fn lease_acquire(
173 &self,
174 scope: &str,
175 agent_id: &str,
176 device_id: &str,
177 ttl_ms: u64,
178 ) -> Result<LeaseWire, TransportError>;
179 fn lease_renew(
180 &self,
181 scope: &str,
182 agent_id: &str,
183 device_id: &str,
184 epoch: u64,
185 ttl_ms: u64,
186 ) -> Result<LeaseWire, TransportError>;
187 fn lease_release(
188 &self,
189 scope: &str,
190 agent_id: &str,
191 device_id: &str,
192 epoch: u64,
193 ) -> Result<LeaseWire, TransportError>;
194 fn lease_current(&self, scope: &str, agent_id: &str) -> Result<LeaseWire, TransportError>;
195}
196
197pub trait OrgKeyTransport: Send + Sync {
209 fn publish_wrapped(&self, scope: &str, wrapped: &WrappedOrgKey) -> Result<(), TransportError>;
210 fn fetch_wrapped(
211 &self,
212 scope: &str,
213 epoch: u64,
214 recipient_user_id: &str,
215 ) -> Result<Option<WrappedOrgKey>, TransportError>;
216 fn fetch_wrapped_for(
217 &self,
218 scope: &str,
219 recipient_user_id: &str,
220 ) -> Result<Vec<WrappedOrgKey>, TransportError>;
221 fn publish_pubkey(
222 &self,
223 scope: &str,
224 account_id: &str,
225 public_hex: &str,
226 ) -> Result<(), TransportError>;
227 fn fetch_pubkeys(&self, scope: &str) -> Result<Vec<MemberPublicKey>, TransportError>;
228}
229
230pub struct NetworkRelay {
233 transport: Arc<dyn SyncTransport>,
234 scope: String,
235}
236
237impl NetworkRelay {
238 pub fn new(transport: Arc<dyn SyncTransport>, scope: impl Into<String>) -> Self {
239 Self {
240 transport,
241 scope: scope.into(),
242 }
243 }
244}
245
246impl Relay for NetworkRelay {
247 fn register(&mut self, device_id: &str) -> Result<RosterEntry, RelayError> {
248 Ok(self.transport.enroll(&self.scope, device_id)?)
249 }
250 fn push(&mut self, device_id: &str, ops: &[OpRecord]) -> Result<PushOutcome, RelayError> {
251 Ok(self.transport.push(&self.scope, device_id, ops)?)
252 }
253 fn pull(&mut self, device_id: &str, since: &Frontier) -> Result<PullResult, RelayError> {
254 Ok(self.transport.pull(&self.scope, device_id, since)?)
255 }
256 fn ack(&mut self, device_id: &str, frontier: Hlc) -> Result<AckOutcome, RelayError> {
257 Ok(self.transport.ack(&self.scope, device_id, frontier)?)
258 }
259 fn checkpoint_put(
260 &mut self,
261 device_id: &str,
262 checkpoint: &Checkpoint,
263 ) -> Result<bool, RelayError> {
264 Ok(self
265 .transport
266 .checkpoint_put(&self.scope, device_id, checkpoint)?)
267 }
268 fn checkpoint_get(&mut self) -> Result<Option<Checkpoint>, RelayError> {
269 Ok(self.transport.checkpoint_get(&self.scope)?)
270 }
271 fn roster(&mut self) -> Result<Vec<RosterEntry>, RelayError> {
272 Ok(self.transport.roster(&self.scope)?)
273 }
274 fn stable_frontier(&mut self) -> Result<Option<Hlc>, RelayError> {
275 Ok(self.transport.stable_frontier(&self.scope)?)
276 }
277 fn gc(&mut self) -> Result<GcReport, RelayError> {
278 Ok(self.transport.gc(&self.scope)?)
279 }
280}
281
282pub struct NetworkLeaseCoordinator {
287 transport: Arc<dyn SyncTransport>,
288 scope: String,
289}
290
291impl NetworkLeaseCoordinator {
292 pub fn new(transport: Arc<dyn SyncTransport>, scope: impl Into<String>) -> Self {
293 Self {
294 transport,
295 scope: scope.into(),
296 }
297 }
298}
299
300fn wire_to_lease(agent_id: &str, wire: LeaseWire, ctx: &'static str) -> Result<Lease, LeaseError> {
301 match wire {
302 LeaseWire::Granted(l) => Ok(l),
303 LeaseWire::Held {
304 holder,
305 epoch,
306 expires_at_ms,
307 } => Err(LeaseError::Held {
308 agent_id: agent_id.to_string(),
309 holder,
310 epoch,
311 expires_at_ms,
312 }),
313 LeaseWire::Lost {
314 claimed_epoch,
315 current_epoch,
316 } => Err(LeaseError::Lost {
317 agent_id: agent_id.to_string(),
318 claimed_epoch,
319 current_epoch,
320 }),
321 other => Err(LeaseError::Backend(format!(
322 "{ctx}: unexpected lease verdict {other:?}"
323 ))),
324 }
325}
326
327impl LeaseCoordinator for NetworkLeaseCoordinator {
328 fn acquire(
329 &mut self,
330 agent_id: &str,
331 device_id: &str,
332 ttl_ms: u64,
333 ) -> Result<Lease, LeaseError> {
334 let wire = self
335 .transport
336 .lease_acquire(&self.scope, agent_id, device_id, ttl_ms)
337 .map_err(|e| LeaseError::Backend(e.to_string()))?;
338 wire_to_lease(agent_id, wire, "acquire")
339 }
340
341 fn renew(
342 &mut self,
343 agent_id: &str,
344 device_id: &str,
345 epoch: u64,
346 ttl_ms: u64,
347 ) -> Result<Lease, LeaseError> {
348 let wire = self
349 .transport
350 .lease_renew(&self.scope, agent_id, device_id, epoch, ttl_ms)
351 .map_err(|e| LeaseError::Backend(e.to_string()))?;
352 wire_to_lease(agent_id, wire, "renew")
353 }
354
355 fn release(&mut self, agent_id: &str, device_id: &str, epoch: u64) -> Result<(), LeaseError> {
356 let wire = self
357 .transport
358 .lease_release(&self.scope, agent_id, device_id, epoch)
359 .map_err(|e| LeaseError::Backend(e.to_string()))?;
360 match wire {
361 LeaseWire::Released => Ok(()),
362 LeaseWire::Lost {
363 claimed_epoch,
364 current_epoch,
365 } => Err(LeaseError::Lost {
366 agent_id: agent_id.to_string(),
367 claimed_epoch,
368 current_epoch,
369 }),
370 other => Err(LeaseError::Backend(format!(
371 "release: unexpected lease verdict {other:?}"
372 ))),
373 }
374 }
375
376 fn current(&mut self, agent_id: &str) -> Result<Option<Lease>, LeaseError> {
377 let wire = self
378 .transport
379 .lease_current(&self.scope, agent_id)
380 .map_err(|e| LeaseError::Backend(e.to_string()))?;
381 match wire {
382 LeaseWire::Current(l) => Ok(l),
383 other => Err(LeaseError::Backend(format!(
384 "current: unexpected lease verdict {other:?}"
385 ))),
386 }
387 }
388}
389
390struct ScopeBackend {
395 relay: crate::relay::FsRelay,
396 lease: InMemoryLeaseCoordinator,
397 org_keys: FsOrgKeyDirectory,
402}
403
404struct LoopbackInner {
405 dir: tempfile::TempDir,
406 wall: WallClock,
407 config: RelayConfig,
408 scopes: BTreeMap<String, ScopeBackend>,
409}
410
411#[derive(Clone)]
415pub struct LoopbackTransport {
416 inner: Arc<Mutex<LoopbackInner>>,
417}
418
419impl LoopbackTransport {
420 pub fn new() -> std::io::Result<Self> {
422 Self::with_config(RelayConfig::default(), crate::oplog::system_clock())
423 }
424
425 pub fn with_config(config: RelayConfig, wall: WallClock) -> std::io::Result<Self> {
426 Ok(Self {
427 inner: Arc::new(Mutex::new(LoopbackInner {
428 dir: tempfile::TempDir::new()?,
429 wall,
430 config,
431 scopes: BTreeMap::new(),
432 })),
433 })
434 }
435
436 fn with_scope<R>(
438 &self,
439 scope: &str,
440 f: impl FnOnce(&mut ScopeBackend) -> Result<R, TransportError>,
441 ) -> Result<R, TransportError> {
442 let mut inner = self
443 .inner
444 .lock()
445 .map_err(|_| TransportError::Unavailable("loopback lock poisoned".into()))?;
446 if !inner.scopes.contains_key(scope) {
447 let dir = inner.dir.path().join(sanitize_scope(scope));
449 std::fs::create_dir_all(&dir)
450 .map_err(|e| TransportError::Unavailable(format!("loopback mkdir: {e}")))?;
451 let relay = crate::relay::FsRelay::open(&dir, inner.config.clone(), inner.wall.clone())
452 .map_err(|e| TransportError::Unavailable(format!("loopback FsRelay: {e}")))?;
453 let org_keys = FsOrgKeyDirectory::open(&dir)
454 .map_err(|e| TransportError::Unavailable(format!("loopback org-keys: {e}")))?;
455 let lease = InMemoryLeaseCoordinator::new(inner.wall.clone());
456 inner.scopes.insert(
457 scope.to_string(),
458 ScopeBackend {
459 relay,
460 lease,
461 org_keys,
462 },
463 );
464 }
465 let backend = inner.scopes.get_mut(scope).expect("just inserted");
466 f(backend)
467 }
468}
469
470fn sanitize_scope(scope: &str) -> String {
477 scope
478 .chars()
479 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
480 .collect()
481}
482
483fn relay_err(e: RelayError) -> TransportError {
484 TransportError::Unavailable(e.to_string())
485}
486
487impl SyncTransport for LoopbackTransport {
488 fn enroll(&self, scope: &str, device_id: &str) -> Result<RosterEntry, TransportError> {
489 self.with_scope(scope, |b| b.relay.register(device_id).map_err(relay_err))
490 }
491 fn push(
492 &self,
493 scope: &str,
494 device_id: &str,
495 ops: &[OpRecord],
496 ) -> Result<PushOutcome, TransportError> {
497 self.with_scope(scope, |b| b.relay.push(device_id, ops).map_err(relay_err))
498 }
499 fn pull(
500 &self,
501 scope: &str,
502 device_id: &str,
503 since: &Frontier,
504 ) -> Result<PullResult, TransportError> {
505 self.with_scope(scope, |b| b.relay.pull(device_id, since).map_err(relay_err))
506 }
507 fn ack(
508 &self,
509 scope: &str,
510 device_id: &str,
511 frontier: Hlc,
512 ) -> Result<AckOutcome, TransportError> {
513 self.with_scope(scope, |b| {
514 b.relay.ack(device_id, frontier).map_err(relay_err)
515 })
516 }
517 fn checkpoint_put(
518 &self,
519 scope: &str,
520 device_id: &str,
521 checkpoint: &Checkpoint,
522 ) -> Result<bool, TransportError> {
523 self.with_scope(scope, |b| {
524 b.relay
525 .checkpoint_put(device_id, checkpoint)
526 .map_err(relay_err)
527 })
528 }
529 fn checkpoint_get(&self, scope: &str) -> Result<Option<Checkpoint>, TransportError> {
530 self.with_scope(scope, |b| b.relay.checkpoint_get().map_err(relay_err))
531 }
532 fn roster(&self, scope: &str) -> Result<Vec<RosterEntry>, TransportError> {
533 self.with_scope(scope, |b| b.relay.roster().map_err(relay_err))
534 }
535 fn stable_frontier(&self, scope: &str) -> Result<Option<Hlc>, TransportError> {
536 self.with_scope(scope, |b| b.relay.stable_frontier().map_err(relay_err))
537 }
538 fn gc(&self, scope: &str) -> Result<GcReport, TransportError> {
539 self.with_scope(scope, |b| b.relay.gc().map_err(relay_err))
540 }
541
542 fn lease_acquire(
543 &self,
544 scope: &str,
545 agent_id: &str,
546 device_id: &str,
547 ttl_ms: u64,
548 ) -> Result<LeaseWire, TransportError> {
549 self.with_scope(scope, |b| {
550 Ok(match b.lease.acquire(agent_id, device_id, ttl_ms) {
551 Ok(l) => LeaseWire::Granted(l),
552 Err(LeaseError::Held {
553 holder,
554 epoch,
555 expires_at_ms,
556 ..
557 }) => LeaseWire::Held {
558 holder,
559 epoch,
560 expires_at_ms,
561 },
562 Err(e) => return Err(TransportError::Unavailable(e.to_string())),
563 })
564 })
565 }
566 fn lease_renew(
567 &self,
568 scope: &str,
569 agent_id: &str,
570 device_id: &str,
571 epoch: u64,
572 ttl_ms: u64,
573 ) -> Result<LeaseWire, TransportError> {
574 self.with_scope(scope, |b| {
575 Ok(match b.lease.renew(agent_id, device_id, epoch, ttl_ms) {
576 Ok(l) => LeaseWire::Granted(l),
577 Err(LeaseError::Lost {
578 claimed_epoch,
579 current_epoch,
580 ..
581 }) => LeaseWire::Lost {
582 claimed_epoch,
583 current_epoch,
584 },
585 Err(e) => return Err(TransportError::Unavailable(e.to_string())),
586 })
587 })
588 }
589 fn lease_release(
590 &self,
591 scope: &str,
592 agent_id: &str,
593 device_id: &str,
594 epoch: u64,
595 ) -> Result<LeaseWire, TransportError> {
596 self.with_scope(scope, |b| {
597 Ok(match b.lease.release(agent_id, device_id, epoch) {
598 Ok(()) => LeaseWire::Released,
599 Err(LeaseError::Lost {
600 claimed_epoch,
601 current_epoch,
602 ..
603 }) => LeaseWire::Lost {
604 claimed_epoch,
605 current_epoch,
606 },
607 Err(e) => return Err(TransportError::Unavailable(e.to_string())),
608 })
609 })
610 }
611 fn lease_current(&self, scope: &str, agent_id: &str) -> Result<LeaseWire, TransportError> {
612 self.with_scope(scope, |b| {
613 b.lease
614 .current(agent_id)
615 .map(LeaseWire::Current)
616 .map_err(|e| TransportError::Unavailable(e.to_string()))
617 })
618 }
619}
620
621fn org_key_err(e: OrgKeyDirectoryError) -> TransportError {
622 TransportError::Unavailable(e.to_string())
623}
624
625impl OrgKeyTransport for LoopbackTransport {
626 fn publish_wrapped(&self, scope: &str, wrapped: &WrappedOrgKey) -> Result<(), TransportError> {
627 self.with_scope(scope, |b| {
628 b.org_keys.publish_wrapped(wrapped).map_err(org_key_err)
629 })
630 }
631 fn fetch_wrapped(
632 &self,
633 scope: &str,
634 epoch: u64,
635 recipient_user_id: &str,
636 ) -> Result<Option<WrappedOrgKey>, TransportError> {
637 self.with_scope(scope, |b| {
638 b.org_keys
639 .fetch_wrapped(epoch, recipient_user_id)
640 .map_err(org_key_err)
641 })
642 }
643 fn fetch_wrapped_for(
644 &self,
645 scope: &str,
646 recipient_user_id: &str,
647 ) -> Result<Vec<WrappedOrgKey>, TransportError> {
648 self.with_scope(scope, |b| {
649 b.org_keys
650 .fetch_wrapped_for(recipient_user_id)
651 .map_err(org_key_err)
652 })
653 }
654 fn publish_pubkey(
655 &self,
656 scope: &str,
657 account_id: &str,
658 public_hex: &str,
659 ) -> Result<(), TransportError> {
660 self.with_scope(scope, |b| {
661 b.org_keys
662 .publish_pubkey(account_id, public_hex)
663 .map_err(org_key_err)
664 })
665 }
666 fn fetch_pubkeys(&self, scope: &str) -> Result<Vec<MemberPublicKey>, TransportError> {
667 self.with_scope(scope, |b| b.org_keys.fetch_pubkeys().map_err(org_key_err))
668 }
669}
670
671pub struct NetworkOrgKeyDirectory {
680 transport: Arc<dyn OrgKeyTransport>,
681 scope: String,
682}
683
684impl NetworkOrgKeyDirectory {
685 pub fn new(transport: Arc<dyn OrgKeyTransport>, scope: impl Into<String>) -> Self {
686 Self {
687 transport,
688 scope: scope.into(),
689 }
690 }
691}
692
693fn transport_err(e: TransportError) -> OrgKeyDirectoryError {
694 match e {
695 TransportError::Unauthorized(m) => OrgKeyDirectoryError::Unauthorized(m),
698 other => OrgKeyDirectoryError::Io(std::io::Error::other(other.to_string())),
701 }
702}
703
704impl OrgKeyDirectory for NetworkOrgKeyDirectory {
705 fn publish_wrapped(&mut self, wrapped: &WrappedOrgKey) -> Result<(), OrgKeyDirectoryError> {
706 self.transport
707 .publish_wrapped(&self.scope, wrapped)
708 .map_err(transport_err)
709 }
710 fn fetch_wrapped(
711 &self,
712 epoch: u64,
713 recipient_user_id: &str,
714 ) -> Result<Option<WrappedOrgKey>, OrgKeyDirectoryError> {
715 self.transport
716 .fetch_wrapped(&self.scope, epoch, recipient_user_id)
717 .map_err(transport_err)
718 }
719 fn fetch_wrapped_for(
720 &self,
721 recipient_user_id: &str,
722 ) -> Result<Vec<WrappedOrgKey>, OrgKeyDirectoryError> {
723 self.transport
724 .fetch_wrapped_for(&self.scope, recipient_user_id)
725 .map_err(transport_err)
726 }
727 fn publish_pubkey(
728 &mut self,
729 account_id: &str,
730 public_hex: &str,
731 ) -> Result<(), OrgKeyDirectoryError> {
732 self.transport
733 .publish_pubkey(&self.scope, account_id, public_hex)
734 .map_err(transport_err)
735 }
736 fn fetch_pubkeys(&self) -> Result<Vec<MemberPublicKey>, OrgKeyDirectoryError> {
737 self.transport
738 .fetch_pubkeys(&self.scope)
739 .map_err(transport_err)
740 }
741}
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746 use crate::org_key_directory::conformance;
747 use crate::relay::Relay;
748
749 fn loopback() -> Arc<LoopbackTransport> {
750 let clock: WallClock = Arc::new(|| 1000);
751 Arc::new(LoopbackTransport::with_config(RelayConfig::default(), clock).unwrap())
752 }
753
754 fn transport() -> Arc<dyn SyncTransport> {
755 loopback()
756 }
757
758 #[test]
759 fn loopback_org_key_directory_satisfies_the_shared_contract() {
760 let mut dir = NetworkOrgKeyDirectory::new(loopback(), "org:acme");
765 conformance::round_trip_suite(&mut dir);
766 }
767
768 #[test]
769 fn org_key_scopes_are_tenant_isolated() {
770 let svc = loopback();
773 let mut acme = NetworkOrgKeyDirectory::new(svc.clone(), "org:acme");
774 let globex = NetworkOrgKeyDirectory::new(svc.clone(), "org:globex");
775
776 let w = conformance::make_wrap("acme", 1, "alice");
777 acme.publish_wrapped(&w).unwrap();
778 acme.publish_pubkey("alice", "cafe").unwrap();
779
780 assert_eq!(acme.fetch_wrapped(1, "alice").unwrap().as_ref(), Some(&w));
781 assert!(globex.fetch_wrapped(1, "alice").unwrap().is_none());
782 assert!(globex.fetch_wrapped_for("alice").unwrap().is_empty());
783 assert!(globex.fetch_pubkeys().unwrap().is_empty());
784 }
785
786 #[test]
787 fn org_keys_persist_across_transport_handles_at_same_scope() {
788 let svc = loopback();
793 let mut writer = NetworkOrgKeyDirectory::new(svc.clone(), "org:acme");
794 let reader = NetworkOrgKeyDirectory::new(svc.clone(), "org:acme");
795 let w = conformance::make_wrap("acme", 7, "bob");
796 writer.publish_wrapped(&w).unwrap();
797 assert_eq!(reader.fetch_wrapped(7, "bob").unwrap().as_ref(), Some(&w));
798 }
799
800 #[test]
801 fn transport_err_preserves_unauthorized_and_funnels_the_rest() {
802 assert!(matches!(
806 transport_err(TransportError::Unauthorized("nope".into())),
807 OrgKeyDirectoryError::Unauthorized(_)
808 ));
809 assert!(matches!(
810 transport_err(TransportError::Unavailable("down".into())),
811 OrgKeyDirectoryError::Io(_)
812 ));
813 assert!(matches!(
814 transport_err(TransportError::Protocol("drift".into())),
815 OrgKeyDirectoryError::Io(_)
816 ));
817 }
818
819 #[test]
820 fn two_devices_converge_through_a_shared_service() {
821 let svc = transport();
825 let scope = "user:matt";
826 let mut mac = NetworkRelay::new(svc.clone(), scope);
827 let mut phone = NetworkRelay::new(svc.clone(), scope);
828
829 mac.register("mac").unwrap();
830 phone.register("phone").unwrap();
831
832 let op = crate::oplog::OpRecord::new(
835 Hlc {
836 wall_ms: 1000,
837 counter: 0,
838 device_id: "mac".into(),
839 },
840 0,
841 None,
842 crate::oplog::Scope::Personal,
843 crate::oplog::Surface::Routing,
844 serde_json::json!({"hello": "phone"}),
845 );
846 let out = mac.push("mac", std::slice::from_ref(&op)).unwrap();
847 assert_eq!(out.accepted, 1);
848
849 let pulled = phone.pull("phone", &Frontier::new()).unwrap();
851 assert_eq!(pulled.ops.len(), 1);
852 assert_eq!(pulled.ops[0].op_id, op.op_id);
853
854 let mut other = NetworkRelay::new(svc.clone(), "user:someone_else");
856 assert!(other.pull("d", &Frontier::new()).unwrap().ops.is_empty());
857 }
858
859 #[test]
860 fn frontier_truncation_maps_to_the_relay_cold_bootstrap_signal() {
861 let te = TransportError::FrontierTruncated {
865 device_id: "mac".into(),
866 dropped_below: 7,
867 };
868 match RelayError::from(te) {
869 RelayError::FrontierTruncated {
870 device_id,
871 dropped_below,
872 } => {
873 assert_eq!(device_id, "mac");
874 assert_eq!(dropped_below, 7);
875 }
876 other => panic!("expected FrontierTruncated, got {other:?}"),
877 }
878 }
879
880 #[test]
881 fn distributed_lease_is_mutually_exclusive_across_devices() {
882 let svc = transport();
887 let scope = "user:matt";
888 let mut mac = NetworkLeaseCoordinator::new(svc.clone(), scope);
889 let mut phone = NetworkLeaseCoordinator::new(svc.clone(), scope);
890
891 let l1 = mac.acquire("milo", "mac", 10_000).unwrap();
892 assert_eq!(l1.epoch, 1);
893 assert_eq!(l1.holder, "mac");
894
895 match phone.acquire("milo", "phone", 10_000) {
897 Err(LeaseError::Held { holder, epoch, .. }) => {
898 assert_eq!(holder, "mac");
899 assert_eq!(epoch, 1);
900 }
901 other => panic!("expected Held, got {other:?}"),
902 }
903
904 assert_eq!(phone.current("milo").unwrap().unwrap().holder, "mac");
906
907 mac.release("milo", "mac", 1).unwrap();
909 let l2 = phone.acquire("milo", "phone", 10_000).unwrap();
910 assert_eq!(l2.epoch, 2);
911 assert_eq!(l2.holder, "phone");
912
913 match mac.renew("milo", "mac", 1, 10_000) {
915 Err(LeaseError::Lost { current_epoch, .. }) => assert_eq!(current_epoch, 2),
916 other => panic!("expected Lost, got {other:?}"),
917 }
918 }
919}