pub struct OfflineProtocol { /* private fields */ }Expand description
Main entry point for the Offline Protocol SDK.
This struct combines all protocol components and provides a unified API for sending/receiving messages with automatic transport selection and reliable delivery.
Implementations§
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn create_group(&mut self, group_name: &str) -> Result<GroupInfo>
pub fn create_group(&mut self, group_name: &str) -> Result<GroupInfo>
Creates a new MLS group.
The group is created locally via MLS. Members can be invited with
invite_to_group(). Messages sent via send_group_message() are
MLS-encrypted and route through whichever transport DORS selects.
If Internet is available the group is also registered with the
relay server for optimized fan-out.
Sourcepub fn invite_to_group(
&mut self,
group_id: &str,
invitee_user_id: &str,
) -> Result<()>
pub fn invite_to_group( &mut self, group_id: &str, invitee_user_id: &str, ) -> Result<()>
Invites a user to an MLS group.
Requires the invitee’s key package to be available in pending_key_packages.
Sends a Welcome to the invitee and a Commit to all existing members.
Admission is where recipient validation belongs. The removal and role paths deliberately do not validate: a roster entry admitted by an older build (before the id rules tightened) must stay removable, and a gate there would turn “a bad member exists” into “a bad member cannot be removed” — the wrong direction for a moderation control.
Sourcepub fn remove_from_group(
&mut self,
group_id: &str,
member_id: &str,
) -> Result<()>
pub fn remove_from_group( &mut self, group_id: &str, member_id: &str, ) -> Result<()>
Removes a member from an MLS group.
Sends a Commit to all remaining members.
Sourcepub fn leave_group(&mut self, group_id: &str) -> Result<()>
pub fn leave_group(&mut self, group_id: &str) -> Result<()>
Leaves an MLS group.
Notifies remaining members and then removes local group state.
Note: The MLS layer does not support self-removal Commits, so remaining
members receive a plaintext leave notification. The deterministic election
in handle_group_mls_leave will select one member to issue the MLS
remove-commit to properly advance the epoch.
Ordering note: Leave notifications are sent before deleting local MLS state. If all notification sends fail, local state is preserved and the caller receives an error so the leave can be retried. This prevents orphaned membership where the leaver is gone locally but peers never learn about the departure.
Sourcepub fn send_group_message(
&mut self,
group_id: &str,
content: &str,
priority: Option<MessagePriority>,
reply_to_msg: Option<&str>,
) -> Result<Vec<MessageId>>
pub fn send_group_message( &mut self, group_id: &str, content: &str, priority: Option<MessagePriority>, reply_to_msg: Option<&str>, ) -> Result<Vec<MessageId>>
Sends a message to all members of an MLS group.
If Internet transport is available and relay-enabled, sends a single
relay broadcast (__GRP_RELAY_BCAST__). Otherwise falls back to
per-member fan-out via send_internal_message() where each member’s
delivery goes through the full DORS/ACK/retry stack independently.
Sourcepub fn send_group_message_with(
&mut self,
group_id: &str,
content: &str,
options: GroupSendOptions,
) -> Result<Vec<MessageId>>
pub fn send_group_message_with( &mut self, group_id: &str, content: &str, options: GroupSendOptions, ) -> Result<Vec<MessageId>>
Like Self::send_group_message, with rich media fields.
The rich fields travel inside the MLS-sealed __RICH_V1__ body of
the group ciphertext, and only when every other member advertised
rich_versions (see GroupSendOptions); otherwise they are
silently dropped, never cleartext.
Sourcepub fn forward_message_to_group(
&mut self,
original_message: &Message,
group_id: &str,
priority: Option<MessagePriority>,
) -> Result<Vec<MessageId>>
pub fn forward_message_to_group( &mut self, original_message: &Message, group_id: &str, priority: Option<MessagePriority>, ) -> Result<Vec<MessageId>>
Forwards a message to all members of a group with forwarding attribution.
Similar to send_group_message but attaches ForwardInfo to preserve
the original sender and forward count. The message content is encrypted
via MLS for the group and fan-out follows the same path as regular
group messages (including relay broadcast when available).
When every other group member advertised the sealed rich payload, the
attribution and the original media_metadata — including cloud-media
encryption_key/iv secrets, which only the sealed body may carry —
travel inside the group MLS ciphertext, so forwarded cloud media stays
openable. Otherwise the media metadata is dropped (never cleartext)
and only the hop-visible payload attribution survives.
Sourcepub fn group_rich_readiness(&self, group_id: &str) -> Result<GroupRichReadiness>
pub fn group_rich_readiness(&self, group_id: &str) -> Result<GroupRichReadiness>
Whether a rich group send right now would seal its extras, and which
members are in the way. See GroupRichReadiness. Read-only: uses
the fan-out cache with an MLS fallback but never mutates state, and
does not probe unknown members (the send path’s drop branch does).
Sourcepub fn group_relay_sync_state(&self, group_id: &str) -> RelaySyncState
pub fn group_relay_sync_state(&self, group_id: &str) -> RelaySyncState
The relay-side registration state of a group. See RelaySyncState.
Synced wins over an outstanding re-registration: once the relay has
positively acknowledged the group on this connection, its registry
entry survives regardless of any in-flight idempotent re-sync, so
relay-dependent commands are already safe to issue.
Sourcepub fn request_group_relay_registration(
&mut self,
group_id: &str,
) -> Result<bool>
pub fn request_group_relay_registration( &mut self, group_id: &str, ) -> Result<bool>
Registers (or re-registers) a group with the relay server on demand.
The supported path for making a mesh-created group known to the
relay before issuing relay-dependent server commands for it (invite
links, server-side fan-out) — never raw-send CreateGroup. The
outcome arrives asynchronously as GroupRelaySyncChanged:
synced: true, reason: "registered" on the relay’s ack, false
with "error" / "ack_timeout" on denial or a relay that never
answers. Idempotent: an already-Synced group returns Ok(true)
without re-sending (the automatic re-sync on membership changes
covers roster updates).
Returns Ok(true) when the registration frame was queued (or the
group is already synced), Ok(false) when relay grouping is
disabled or the Internet transport is unavailable, Err when the
group is unknown locally.
Sourcepub fn list_groups(&self) -> Result<Vec<String>>
pub fn list_groups(&self) -> Result<Vec<String>>
Lists all MLS groups (excluding 1:1 sessions).
Sourcepub fn get_group_info(&self, group_id: &str) -> Result<Option<GroupInfo>>
pub fn get_group_info(&self, group_id: &str) -> Result<Option<GroupInfo>>
Gets information about an MLS group.
Sourcepub fn set_member_role(
&mut self,
group_id: &str,
target_user_id: &str,
role: GroupRole,
) -> Result<()>
pub fn set_member_role( &mut self, group_id: &str, target_user_id: &str, role: GroupRole, ) -> Result<()>
Changes a member’s role in a group (admin only). Broadcasts the change to all group members.
Sourcepub fn get_member_role(
&self,
group_id: &str,
user_id: &str,
) -> Result<GroupRole>
pub fn get_member_role( &self, group_id: &str, user_id: &str, ) -> Result<GroupRole>
Gets a member’s role in a group.
A group that exists but has no role metadata (created before role
tracking) reports the same default an unrecorded user gets: Member.
Sourcepub fn get_group_roles(
&self,
group_id: &str,
) -> Result<HashMap<String, GroupRole>>
pub fn get_group_roles( &self, group_id: &str, ) -> Result<HashMap<String, GroupRole>>
Gets all member roles in a group.
A group that exists but has no role metadata (created before role tracking) reports no explicit roles.
Sourcepub fn rename_group(&mut self, group_id: &str, new_name: &str) -> Result<()>
pub fn rename_group(&mut self, group_id: &str, new_name: &str) -> Result<()>
Renames a group (admin only). Updates the local group name and broadcasts the change to all members.
Sourcepub fn set_relay_capabilities(&mut self, capabilities: Vec<String>)
pub fn set_relay_capabilities(&mut self, capabilities: Vec<String>)
Replaces the set of capability tokens advertised by the connected relay.
Called by the platform internet bridge with the capabilities list
from the relay’s Authenticated answer, before it reports the
transport up via internet_status_changed(true) — the false→true
flush must already see the capabilities, or the first sends after a
reconnect would take the capability-less path. Wholesale replace:
each authentication describes the relay actually connected now.
Relays that predate the advertisement yield an empty list, which
reads as “no v2 contract” and keeps group sends on per-member
fan-out.
The list is wire-supplied: it is bounded to
MAX_RELAY_CAPABILITIES tokens of at most
MAX_RELAY_CAPABILITY_TOKEN_BYTES bytes each; excess entries and
oversized tokens are dropped.
Sourcepub fn handle_relay_group_delivery_report(
&mut self,
report_json: &str,
) -> Result<()>
pub fn handle_relay_group_delivery_report( &mut self, report_json: &str, ) -> Result<()>
Consumes a relay GroupMessageSent settled delivery report
(forwarded verbatim by the platform bridge) for a broadcast this
sender has in flight.
Settles the RelayBroadcastPending entry correlated by
message_id, then re-sends a per-member __GRP_MLS_MSG__ copy —
through the ordinary outbox/ACK/park ladder — to every MLS roster
member the relay did not reach over a socket or a push: the members
it names in missed, plus every roster member it does not name at
all (the relay’s registered roster can be a strict subset of the MLS
roster, so “not mentioned” must read as “not reached”, never as
“covered”). Duplicates are safe: the copies carry the logical id, so
a member that did receive the relay’s copy absorbs them by dedup.
Reports that correlate with nothing (already settled by a previous
attempt’s report, or downgraded meanwhile) are ignored. A report
whose group_id does not match the armed entry is ignored too — the
entry stays armed and the timeout path recovers.
Emits Event::group_message_delivery_report with the relay’s
delivered/pushed lists and the members re-issued per-member.
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn block_user(&mut self, user_id: &str) -> Result<()>
pub fn block_user(&mut self, user_id: &str) -> Result<()>
Blocks a user. Messages from this user will be silently dropped (no ACK sent, no event emitted). The blocked user receives no notification. Encrypted frames of theirs already parked awaiting a session are discarded, persisted records included.
Blocking is idempotent — calling this for an already-blocked user succeeds silently.
Sourcepub fn unblock_user(&mut self, user_id: &str) -> Result<()>
pub fn unblock_user(&mut self, user_id: &str) -> Result<()>
Unblocks a previously blocked user.
Clears any stale MLS session state, pending key packages, and queued messages for the peer so that a fresh key exchange can occur on re-discovery.
Unblocking a non-blocked user succeeds silently.
Sourcepub fn get_blocked_users(&self) -> Vec<String>
pub fn get_blocked_users(&self) -> Vec<String>
Returns the list of currently blocked user IDs, sorted for stable output.
Sourcepub fn is_user_blocked(&self, user_id: &str) -> bool
pub fn is_user_blocked(&self, user_id: &str) -> bool
Checks whether a user is currently blocked.
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn state(&self) -> ProtocolState
pub fn state(&self) -> ProtocolState
Gets the current protocol state.
Sourcepub fn config(&self) -> &ProtocolConfig
pub fn config(&self) -> &ProtocolConfig
Gets the configuration.
Sourcepub fn mesh_services(&self) -> &MeshServices
pub fn mesh_services(&self) -> &MeshServices
Gets a reference to the mesh services registry.
Sourcepub fn transport_manager_mut(&mut self) -> &mut TransportManager
pub fn transport_manager_mut(&mut self) -> &mut TransportManager
Gets a mutable reference to the transport manager.
This allows external code (e.g., FFI) to add transports dynamically.
Sourcepub fn transport_manager(&self) -> &TransportManager
pub fn transport_manager(&self) -> &TransportManager
Gets a reference to the transport manager.
Sourcepub fn file_transfer_manager_mut(&mut self) -> &mut FileTransferManager
pub fn file_transfer_manager_mut(&mut self) -> &mut FileTransferManager
Returns a mutable reference to the file transfer manager.
Sourcepub fn file_transfer_manager(&self) -> &FileTransferManager
pub fn file_transfer_manager(&self) -> &FileTransferManager
Returns a reference to the file transfer manager.
Sourcepub fn mls_manager(&self) -> Option<&Arc<RwLock<MlsManager>>>
pub fn mls_manager(&self) -> Option<&Arc<RwLock<MlsManager>>>
Gets access to the MLS manager for advanced operations.
Returns None if MLS is not initialized.
Sourcepub fn update_dors_config(&mut self, config: DorsConfig)
pub fn update_dors_config(&mut self, config: DorsConfig)
Updates the DORS configuration at runtime.
This replaces the current DORS selector configuration with the provided config.
Sourcepub fn dors_config(&self) -> DorsConfig
pub fn dors_config(&self) -> DorsConfig
The DORS configuration transport selection is actually using.
Read from the selector, not from the stored ProtocolConfig, for the
reason Self::mesh_relay_config gives: a caller reading a section
back is asking what the consumer has, and a copy kept anywhere else is
one that can disagree with it. Here the two genuinely do diverge, since
Self::update_dors_config rewrites the selector and leaves the
stored config alone.
Sourcepub fn set_device_battery(&mut self, level: u8, is_charging: bool)
pub fn set_device_battery(&mut self, level: u8, is_charging: bool)
Records the host’s battery reading for this device.
The platform owns this number — no transport can observe it — and until
it arrives every battery-dependent policy in the SDK runs in its
unknown-level branch: DORS energy scoring skips its battery term, the
relay role is never evaluated (so relay_promoted/relay_demoted can
never fire), and message forwarding takes the “unknown means willing”
path past RelayConfig::min_battery_for_relay. Feeding it is what
makes those policies live.
is_charging is not cosmetic: a charging device is deliberately
excused the soft relay minimum and stopped only by the hard
CRITICAL_RELAY_BATTERY_LEVEL floor, so reporting a level without the
charging state strips relay duty from plugged-in devices that should
keep it.
level is clamped to 0-100.
Sourcepub fn device_battery(&self) -> (Option<u8>, bool)
pub fn device_battery(&self) -> (Option<u8>, bool)
Returns the host-reported (battery_level, is_charging) pair, or
(None, false) when no platform reading has been supplied.
Sourcepub fn update_relay_config(&mut self, config: RelayConfig) -> Result<()>
pub fn update_relay_config(&mut self, config: RelayConfig) -> Result<()>
Updates the relay configuration at runtime.
Validated the same way Self::update_ack_config is — by building the
candidate configuration and running the real validator — so a
constraint added to ProtocolConfig::validate applies on this path too.
Takes effect on the next forwarding decision and the next process()
tick, which is where the relay standing is re-derived. One copy, read by
every relay decision: the forwarding gate, the capability bias pushed
into the governor, and the battery floor the demotion event reports. An
earlier shape kept a second copy on the router’s relay manager and had
to write both, which is exactly the split that lets a device decline the
relay role while still carrying other people’s traffic.
Sourcepub fn relay_config(&self) -> &RelayConfig
pub fn relay_config(&self) -> &RelayConfig
Returns the current relay configuration.
Sourcepub fn is_relay(&self) -> bool
pub fn is_relay(&self) -> bool
Whether this device is currently acting as a relay.
Reports what this device has been doing — carrying traffic for other
devices — not what its battery and neighbor count suggest it could. It
is the standing the relay_promoted / relay_demoted /
relay_demoted_battery events announce, re-derived on each process()
tick from the forwarding governor’s own record of frames carried.
Consequently it answers false on a device that has every capability to
relay but has had no third-party traffic to carry — including any device
whose peers all have their own route to each other, and any device on
which an infrastructure carrier is up, since the mesh is offered frames
only when nothing else can reach the recipient.
Needs no battery feed. The feed scales how much this device forwards
(see Self::set_device_battery); forwarding is observable either way.
Sourcepub fn update_ack_config(&mut self, config: AckConfig) -> Result<()>
pub fn update_ack_config(&mut self, config: AckConfig) -> Result<()>
Updates the ACK configuration at runtime.
Note: This affects new ACK registrations; existing pending ACKs keep their original timeout.
Validated the same way Self::update_retry_config is — by building
the candidate configuration and running the real validator — rather than
by repeating its checks inline. Hand-rolled copies drift: a constraint
added to ProtocolConfig::validate for a new AckConfig field would be
enforced at construction and silently skipped on the runtime-update path.
Sourcepub fn update_retry_config(&mut self, config: RetryConfig) -> Result<()>
pub fn update_retry_config(&mut self, config: RetryConfig) -> Result<()>
Updates the retry configuration at runtime.
Note: This affects new retry entries; existing entries keep their original timing.
Sourcepub fn update_dedup_config(&mut self, config: DeduplicatorConfig) -> Result<()>
pub fn update_dedup_config(&mut self, config: DeduplicatorConfig) -> Result<()>
Updates the deduplication configuration at runtime.
The tracked set is carried across, re-bounded by the new configuration
(Deduplicator::reconfigure), rather than cleared. Clearing it was a
silent restart while the set lived in memory only; with the set
persisted it would discard the ids restored at launch — and the React
Native layer applies reliability.dedup through this method on every
start(), right after that restore — so the socket copy of a message
already consumed from a push injection would reach a spent ratchet
generation after all. The change is counted for persistence so the next
batch write re-states the record under the new bounds.
Validated the same way its two siblings are, and for a reason that only
appeared once they were. Both of those run the whole
ProtocolConfig::validate, which checks the Bloom parameters this
method installs — so while this one accepted anything, a configuration
it had already stored could make a perfectly valid
Self::update_retry_config fail, complaining about a Bloom filter the
caller never mentioned. Deduplicator::with_config fails safe on those
values rather than panicking, so the cost was a confusing rejection
rather than a crash — but a rejection attributed to the wrong call is
its own kind of expensive.
Sourcepub fn deduplicator_stats(&self) -> DeduplicatorStats
pub fn deduplicator_stats(&self) -> DeduplicatorStats
Gets deduplicator statistics for monitoring.
Sourcepub fn pending_queue_metrics(&self) -> PendingQueueMetrics
pub fn pending_queue_metrics(&self) -> PendingQueueMetrics
Gets pending encrypted message queue counters and gauges.
Sourcepub fn pending_ack_count(&self) -> usize
pub fn pending_ack_count(&self) -> usize
Gets the current ACK manager statistics.
Sourcepub fn retry_queue_size(&self) -> usize
pub fn retry_queue_size(&self) -> usize
Gets the current retry queue statistics.
Sourcepub fn is_known_peer(&self, peer_id: &str) -> bool
pub fn is_known_peer(&self, peer_id: &str) -> bool
Returns whether peer_id is currently tracked as a discovered
neighbor. Populated by Self::on_neighbor_discovered and cleared
by Self::on_neighbor_lost, by the periodic TTL sweep when the
peer has not been re-seen for KNOWN_PEER_TTL_SECS, or by
least-recently-seen eviction when tracking is at capacity — so this
can flip to false without any explicit loss signal. Blocked users
are never tracked.
Sourcepub fn mesh_relay_stats(&self) -> MeshRelayStats
pub fn mesh_relay_stats(&self) -> MeshRelayStats
Reports how much traffic this device is carrying for other people.
Useful for showing a user what their device is contributing, and for
spotting a neighborhood in trouble: a rising rate_deferred means
forwarding is hitting its ceiling, and peer_rate_limited means a
single neighbor is sending more than its share. dropped_for_capacity
is expected to stay at zero — see MeshRelayStats::dropped_for_capacity.
refused_queue_full and abandoned_overdue are the two that record
frames actually lost under pressure. Without them a shedding device
reports only healthy-looking deferrals, so a caller diagnosing a mesh
that is dropping traffic has nothing to see it with.
Sourcepub fn mesh_relay_config(&self) -> MeshRelayConfig
pub fn mesh_relay_config(&self) -> MeshRelayConfig
Reports the mesh forwarding tunables currently in force.
Read from the governor itself, not from the stored ProtocolConfig,
so this reports what forwarding decisions actually use. The section is
fixed at construction today, which makes the two identical — reading
the consumer is what keeps that an implementation detail rather than
something a caller has to know.
Sourcepub fn register_service(&mut self, descriptor: ServiceDescriptor) -> Result<()>
pub fn register_service(&mut self, descriptor: ServiceDescriptor) -> Result<()>
Registers a local service that this node offers.
Sourcepub fn unregister_service(&mut self, service_id: &str) -> Result<bool>
pub fn unregister_service(&mut self, service_id: &str) -> Result<bool>
Unregisters a local service. Returns true if the service was found and removed.
Sourcepub fn discover_services(&mut self, service_id: Option<&str>) -> Result<String>
pub fn discover_services(&mut self, service_id: Option<&str>) -> Result<String>
Broadcasts a service discovery query to all known peers.
Returns a query_id. Responses arrive asynchronously as ServiceDiscovered events.
A provider more than one hop away answers toward the query’s
originator, and each device in between forwards the response on to it
(offline_protocol_services), so discovery reaches as far as the query
was gossiped.
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn custody_stats(&self) -> CustodyStats
pub fn custody_stats(&self) -> CustodyStats
What this device has done as a custodian and as a depositor, and what
it is holding right now. See CustodyStats.
Sourcepub fn custody_config(&self) -> &CustodyConfig
pub fn custody_config(&self) -> &CustodyConfig
The custody configuration in force.
Sourcepub fn erase_custody(&mut self) -> Result<()>
pub fn erase_custody(&mut self) -> Result<()>
Deletes every held frame and resets the custody counters.
Callable on its own, and called by the data-layer wipe: a custody
store that survived a logout would hold other people’s traffic past
the point the user asked for erasure. Attempts every record and
reports the first failure, like the data wipe: answering Ok for an
erase that left records behind is the worst shape this call can take.
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn set_data_storage(
&mut self,
storage: Arc<dyn ProtocolStateStorage>,
) -> Result<()>
pub fn set_data_storage( &mut self, storage: Arc<dyn ProtocolStateStorage>, ) -> Result<()>
Point documents at a backend the application supplies.
Documents move; protocol secrets do not. Applied immediately, so a store opened afterwards reads and writes through the new backend while everything else stays where it is.
Any document already open is written into the new backend as a self-contained snapshot before this returns. That is not a convenience, it is the whole correctness of a mid-session swap: a delta record only describes the change since the previous one, so a document that merely kept writing deltas into the new backend would leave every earlier delta behind in the old one. The engine accepts such an orphan delta and parks it, which means the document reads empty from the new backend and the next compaction deletes the orphans for good.
If the migration cannot be written the swap does not happen at all and the error is returned, because a half-swapped layer is precisely the stranding this method exists to prevent.
Documents that are not open are not migrated: they live wholly in the old backend, which is what switching backends means.
Sourcepub fn data_create_doc(&mut self, space: &str, doc: &str) -> Result<()>
pub fn data_create_doc(&mut self, space: &str, doc: &str) -> Result<()>
Create a document, or do nothing if it already exists.
A name that was removed comes back empty, keeping its removal floor, so the old contents cannot arrive from a replica that never heard about the removal.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_set_interest(
&mut self,
space: &str,
patterns: Vec<String>,
) -> Result<()>
pub fn data_set_interest( &mut self, space: &str, patterns: Vec<String>, ) -> Result<()>
The documents this device wants from its peers in one space.
patterns is a list of document names, each of which MAY end in *
to match a prefix. ["*"] is everything and is the default; [] is
nothing. The document charset excludes *, so the wildcard can never
be part of a name it is matching.
Two halves, and only one of them is a request. Toward a peer this is carried in every version offer, so the peer sends nothing else: that is the traffic saving, and it depends on the peer reading the field. Locally it is a refusal, so a document outside it is never created from an offer and never imported however it arrives. The refusal is what makes this safe against a peer that ignores the request, which includes every peer on a build that predates it.
Narrowing does not delete what is already held. A document that falls
outside the new patterns stops being updated and stays where it is,
because deleting on a policy change would make a narrowing lose data
no caller asked to lose. data_delete_doc is how it leaves.
Widening asks: the newly wanted documents are absent from this device’s next offer and inside its declared want, so the peer answers with them.
The question that asks is skipped in the two cases where it would
cost something and buy nothing, because both are the ordinary way an
application calls this. Declaring the same patterns again asks for
nothing new: the offer would carry exactly what the last one did.
And declaring anything at all before start(), which is the
documented order, cannot reach a peer: the transports are not up, so
the frame is dropped for want of a session, and the offer path has
already stamped the window that suppresses the next sweep. That stamp
would then suppress the start-up sweep and the first rediscovery,
delaying the exchange this call exists to bring forward by the length
of the window. Both paths already carry the new interest: the sweep
in start and every rediscovery build their offer from it.
Sourcepub fn data_delete_doc(&mut self, space: &str, doc: &str) -> Result<()>
pub fn data_delete_doc(&mut self, space: &str, doc: &str) -> Result<()>
Drop this device’s copy of a document, leaving the peers’ copies alone.
Eviction, not removal: it reclaims the space a document occupies here
and records nothing about the name, so the next version exchange with
a replica that still holds it recreates and refills it. That is the
right call for a cache and the wrong one for content somebody wants
gone from the room, which is what Self::data_remove_doc is for.
Sourcepub fn data_remove_doc(&mut self, space: &str, doc: &str) -> Result<()>
pub fn data_remove_doc(&mut self, space: &str, doc: &str) -> Result<()>
Remove a document from every replica of its space.
Records the version the document stood at as its removal floor, then deletes it here and tells the space. A replica whose copy the floor covers deletes it too; a replica holding an edit the floor does not cover keeps its copy and hands the document back, contents and all, because that edit’s history is those contents. An edit therefore beats a removal, whichever happened first, and the pair never produces a half-emptied document.
Pending edits here are flushed first so the floor covers them: removing a document must not leave this device’s own unsent work looking like somebody else’s concurrent edit.
Removing a name this device does not hold does nothing. There is no version to take a floor from, and an empty floor would name no content at all.
Re-using the name afterwards starts a document whose history is disjoint from the floor. What it does not do is fence the old contents off: a replica that edited them concurrently keeps them and the ordinary exchange merges them into the new document, and a replica that never learned of the removal merges the new contents into its old copy. A name is cheap; a fresh one is the safe choice.
Sourcepub fn data_remove_space(&mut self, space: &str) -> Result<()>
pub fn data_remove_space(&mut self, space: &str) -> Result<()>
Remove every document a space holds, from every replica of it.
Each document is attempted even after one fails, and the first error is what the caller hears. What every removal shares is done once for the lot: the delta log is listed once, the space index is written once, and the space is told once, with one offer carrying every removal. Per document that is two full listings and one sealed index write fewer, which on a custom backend is three round trips under the engine lock, for each of up to a thousand names.
Sourcepub fn data_list_docs(&mut self, space: &str) -> Result<Vec<String>>
pub fn data_list_docs(&mut self, space: &str) -> Result<Vec<String>>
The documents in a space.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_list_spaces(&mut self) -> Result<Vec<String>>
pub fn data_list_spaces(&mut self) -> Result<Vec<String>>
Every space the store knows of.
Derived from what is on disk, so a space whose last document was deleted still lists while its index record survives, and a space whose documents were all removed lists for as long as its removal records do, which is the life of the space.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_map_set(
&mut self,
space: &str,
doc: &str,
collection: &str,
key: &str,
value: DataValue,
) -> Result<()>
pub fn data_map_set( &mut self, space: &str, doc: &str, collection: &str, key: &str, value: DataValue, ) -> Result<()>
Set a key in a map collection.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_map_delete(
&mut self,
space: &str,
doc: &str,
collection: &str,
key: &str,
) -> Result<()>
pub fn data_map_delete( &mut self, space: &str, doc: &str, collection: &str, key: &str, ) -> Result<()>
Remove a key from a map collection.
Sourcepub fn data_map_get(
&mut self,
space: &str,
doc: &str,
collection: &str,
key: &str,
) -> Result<Option<DataValue>>
pub fn data_map_get( &mut self, space: &str, doc: &str, collection: &str, key: &str, ) -> Result<Option<DataValue>>
Read a key from a map collection.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_list_push(
&mut self,
space: &str,
doc: &str,
collection: &str,
value: DataValue,
) -> Result<()>
pub fn data_list_push( &mut self, space: &str, doc: &str, collection: &str, value: DataValue, ) -> Result<()>
Append to a list collection.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_list_insert(
&mut self,
space: &str,
doc: &str,
collection: &str,
index: u32,
value: DataValue,
) -> Result<()>
pub fn data_list_insert( &mut self, space: &str, doc: &str, collection: &str, index: u32, value: DataValue, ) -> Result<()>
Insert into a list collection.
Sourcepub fn data_list_delete(
&mut self,
space: &str,
doc: &str,
collection: &str,
index: u32,
count: u32,
) -> Result<()>
pub fn data_list_delete( &mut self, space: &str, doc: &str, collection: &str, index: u32, count: u32, ) -> Result<()>
Delete entries from a list collection.
Sourcepub fn data_list_len(
&mut self,
space: &str,
doc: &str,
collection: &str,
) -> Result<u32>
pub fn data_list_len( &mut self, space: &str, doc: &str, collection: &str, ) -> Result<u32>
The number of entries in a list collection.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_text_insert(
&mut self,
space: &str,
doc: &str,
collection: &str,
position: u32,
text: &str,
) -> Result<()>
pub fn data_text_insert( &mut self, space: &str, doc: &str, collection: &str, position: u32, text: &str, ) -> Result<()>
Insert into a text collection at a character position.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_text_delete(
&mut self,
space: &str,
doc: &str,
collection: &str,
position: u32,
count: u32,
) -> Result<()>
pub fn data_text_delete( &mut self, space: &str, doc: &str, collection: &str, position: u32, count: u32, ) -> Result<()>
Delete characters from a text collection.
Sourcepub fn data_text_value(
&mut self,
space: &str,
doc: &str,
collection: &str,
) -> Result<String>
pub fn data_text_value( &mut self, space: &str, doc: &str, collection: &str, ) -> Result<String>
The contents of a text collection.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_counter_increment(
&mut self,
space: &str,
doc: &str,
collection: &str,
amount: f64,
) -> Result<()>
pub fn data_counter_increment( &mut self, space: &str, doc: &str, collection: &str, amount: f64, ) -> Result<()>
Add to a counter collection. Negative values subtract.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_counter_value(
&mut self,
space: &str,
doc: &str,
collection: &str,
) -> Result<f64>
pub fn data_counter_value( &mut self, space: &str, doc: &str, collection: &str, ) -> Result<f64>
The value of a counter collection.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_doc_json(&mut self, space: &str, doc: &str) -> Result<String>
pub fn data_doc_json(&mut self, space: &str, doc: &str) -> Result<String>
The document’s current state as plain JSON.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_export_raw(&mut self, space: &str, doc: &str) -> Result<Vec<u8>>
pub fn data_export_raw(&mut self, space: &str, doc: &str) -> Result<Vec<u8>>
The document’s full history, engine-encoded.
Half of the escape hatch: this and Self::data_doc_json are what
let an application take its data and leave.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_flush(&mut self, space: &str, doc: &str) -> Result<()>
pub fn data_flush(&mut self, space: &str, doc: &str) -> Result<()>
Persist any pending edits to a document.
Writes one delta record, then compacts if the log has outgrown the document. The order is load-bearing: the delta is durable before any bookkeeping assumes it, and at compaction the fresh snapshot is durable before the deltas it folded are deleted. A crash at any point leaves duplicate history, which the merge absorbs, rather than a gap, which it cannot.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn data_flush_all(&mut self) -> Result<()>
pub fn data_flush_all(&mut self) -> Result<()>
Persist every open document with pending edits.
Called on shutdown, so the debounce window between an edit and its record can never be the reason a change is lost.
Sourcepub fn data_wipe_all(&mut self) -> Result<()>
pub fn data_wipe_all(&mut self) -> Result<()>
Delete every record the data layer owns.
The logout path for an application that pointed documents at its own
backend. wipePersistedState clears the account directory of the
default provider, which a custom backend is not inside, so without
this call those documents would outlive the account that made them.
Only durable once replication has stopped. There are no deletion tombstones in this release, so nothing distinguishes a space this device wiped from one it has never seen. Called while the engine is running with live sessions, every document comes back: the peer’s next version offer names them and they are recreated and refilled from the peer’s copy, and an offer of our own naming nothing reads as a replica that has never seen the space. On the logout path the engine is being torn down anyway. Anywhere else, stop it first, and only for as long as it stays stopped: the peer still holds the documents, so they return when replication resumes with it. The call clears this device, it does not delete content.
Sourcepub fn data_doc_size(&mut self, space: &str, doc: &str) -> Result<u64>
pub fn data_doc_size(&mut self, space: &str, doc: &str) -> Result<u64>
The compacted size of a document, in bytes.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn data_attachment_hash(bytes: &[u8]) -> String
pub fn data_attachment_hash(bytes: &[u8]) -> String
The SHA-256 of bytes, in the spelling an attachment reference uses.
Exposed because an application writing a reference needs the address of its own bytes, and computing it anywhere else risks a second spelling of the same hash.
Sourcepub fn data_fetch_attachment(&mut self, space: &str, hash: &str) -> Result<()>
pub fn data_fetch_attachment(&mut self, space: &str, hash: &str) -> Result<()>
Ask the peer a 1:1 space is named after for the bytes behind a reference.
Pull rather than push, and the reason is bandwidth somebody else pays for: a space may hold references to more bytes than a phone wants over Bluetooth, so the decision to spend that belongs to the application, taken per blob, at the moment somebody opens one.
The answer arrives later as Event::DataAttachmentReceived or
Event::DataAttachmentUnavailable, never inline.
Group spaces are refused in this version. A blob travels the media path, which is a 1:1 transfer to a confirmed session, and two members of a group need not have one with each other.
Sourcepub fn data_fetch_attachment_from(
&mut self,
space: &str,
peer: &str,
hash: &str,
) -> Result<()>
pub fn data_fetch_attachment_from( &mut self, space: &str, peer: &str, hash: &str, ) -> Result<()>
Ask one member of a group for the bytes behind a reference.
A group needs the member named, where a 1:1 space does not: the reference replicates to everybody and says nothing about who holds the bytes. The SDK does not walk the roster on the application’s behalf, because each miss costs the whole silence timeout and a reference nobody holds would spend one per member. The application knows who wrote the reference; that is the member to ask.
The answer arrives as frames under the group key rather than as a transfer, so it needs no pairwise session with that member, and it is bounded at 32 pieces of 32 KiB: 1 MiB in all. Anything larger travels over a 1:1 session, and the holder is told so at the call.
Sourcepub fn data_provide_attachment(
&mut self,
space: &str,
peer: &str,
hash: &str,
bytes: Vec<u8>,
) -> Result<()>
pub fn data_provide_attachment( &mut self, space: &str, peer: &str, hash: &str, bytes: Vec<u8>, ) -> Result<()>
Answer a peer’s request with the bytes.
The bytes come from the application because this SDK never held them: a blob does not enter a document and does not enter protocol state, so the only copy is wherever the app put it.
Refuses bytes that do not hash to hash. That check is here rather
than only on the receiving side so a mistake is reported to the app
that made it, while it still has the file in hand, instead of
travelling the whole media path to be refused by somebody else.
Sourcepub fn data_decline_attachment(
&mut self,
space: &str,
peer: &str,
hash: &str,
) -> Result<()>
pub fn data_decline_attachment( &mut self, space: &str, peer: &str, hash: &str, ) -> Result<()>
Tell a peer their fetch will not be answered.
A first-class answer rather than silence. The asking side cannot otherwise tell a peer that no longer holds the bytes from one that is merely slow, and shows a person a spinner that never resolves.
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn resolve_username(&mut self, username: &str) -> Result<bool>
pub fn resolve_username(&mut self, username: &str) -> Result<bool>
Starts a username resolution.
§The return value means exactly one thing: an answer is coming
Ok(true) started a lookup and Ok(false) joined one already in
flight, and both are followed by exactly one
Event::UsernameResolved for the name. Every case where no event will
ever arrive is an error instead.
That split is the whole point. A caller awaits the event, so folding
“discovery is off” into false alongside “already in flight” leaves an
app unable to tell waiting from hanging — and the failure mode is a
spinner that never stops, on the path a user reaches by typing a name
and pressing search.
§Errors
Error::InvalidArgumentif the string is not a claimable username.Error::InvalidConfigurationif username discovery is off. It also requires cold contact, so this covers a claim that could only dead-end one hop later.Error::InvalidStateif too many lookups are already in flight. Transient: retry once earlier ones drain.Error::NotStartedif the protocol is not running. Nothing pumps relay traffic and nothing sweeps deadlines outsideRunning, so a lookup accepted here would be a promise with no machinery behind it. Transient: retry afterstart().
Sourcepub fn begin_username_resolution(
&mut self,
query_id: String,
username: Username,
)
pub fn begin_username_resolution( &mut self, query_id: String, username: Username, )
Registers a query the transport just minted, so its answers accumulate.
A mint whose request has already been answered by the timeout sweep is dropped rather than accumulated. That is what keeps the one-event contract when a relay reconnects after the sweep gave up: the query still goes out, its answers land on an unknown resolution and are discarded, and no second, contradictory set is emitted for a lookup the app has already been told the answer to.
Sourcepub fn handle_resolved_discovery_record(
&mut self,
query_id: &str,
author: &str,
data: &[u8],
)
pub fn handle_resolved_discovery_record( &mut self, query_id: &str, author: &str, data: &[u8], )
Accumulates one discovery record returned by a resolution query.
Verification happens here and every failure is ordinary: the tag is public, anyone may publish to it, and a query returns whatever the relay holds. A refused record is counted and dropped, never surfaced as an error — a resolver that reported junk as a failure would make an unremarkable directory look broken. The name a record is checked against is the resolution’s own, not one passed in beside the query id. The two agree today because the transport hands back the subject it stored, but taking both would let a caller seed one query’s claim set with records verified for another name, and the username check is what catches a genuine record copied onto a foreign tag.
Sourcepub fn flush_username_resolution(&mut self, query_id: &str)
pub fn flush_username_resolution(&mut self, query_id: &str)
Emits the accumulated set for a finished resolution.
Idempotent: a resolution already flushed (by the timeout sweep, say) is simply absent, so a late end-of-stored-events emits nothing rather than a second, contradictory set.
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn handle_resolved_key_package(&mut self, data: &[u8]) -> Result<()>
pub fn handle_resolved_key_package(&mut self, data: &[u8]) -> Result<()>
Handles a key-package record returned by a resolution query.
Deliberately narrow: this channel answers exactly one question, so anything that is not a key package is dropped rather than dispatched. The records come from a public routing tag that anyone may publish to, and a query returns whatever the relay holds there — without this gate, a squatter could deliver any internal control frame through a path that never went past the receive loop’s dedup and block checks.
What survives the gate is not trusted here either. It goes through
process_internal_message_via, so the Ed25519 signature and the
sender-address derivation decide whose key package it is: a record
planted at the queried peer’s tag by somebody else is signed by that
party’s key, which derives to that party’s address, so it registers
under their identity — never under the peer we asked about. A record
signed by nobody is refused outright.
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn receive_message(&mut self) -> Option<Message>
pub fn receive_message(&mut self) -> Option<Message>
Receives the next available message.
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn on_relay_address_declared(&self, declared: &str)
pub fn on_relay_address_declared(&self, declared: &str)
Verifies the relay’s AddressDeclared acknowledgement against the
address this node actually holds.
This is the lockstep assertion for the relay leg of addressing. The
bridge declares Self::local_address and signs a proof over a
per-connection challenge; the relay verifies that the declared address
derives from the signing key and echoes back what it bound. Agreement
means every frame the relay forwards from here on is attributed to the
same identity the core stamps into Message.sender, which is what
validate_transport_sender strict-matches on the receiving side.
A disagreement is reported, never acted on: see
SecurityWarningCode::RelayAddressBindingMismatch for why tearing the
connection down would buy nothing. Callers pass the echoed address
verbatim — an empty or malformed one simply cannot match and is reported
like any other mismatch.
Reached only through the FFI’s dedicated entry point, not through message-plane injection, so a notification payload cannot synthesize it.
Sourcepub fn on_relay_address_declaration_refused(&self, reason: &str)
pub fn on_relay_address_declaration_refused(&self, reason: &str)
Records the relay refusing this connection’s address declaration.
Non-fatal by contract on both sides — the connection stays authenticated
and keeps working in account-name space. What degrades is new MLS
session establishment over the relay; see
SecurityWarningCode::RelayAddressDeclarationRefused. reason is the
relay’s own text: opaque, remote-chosen, and logged rather than
emitted — an event field never carries text a remote party wrote.
Attributed to this node rather than to a peer (the failure is ours, and no peer is involved), following the same convention as the Nostr slot-exhaustion warning.
Sourcepub fn gateway_address_declaration(
&self,
challenge: &[u8],
) -> Result<(String, Vec<u8>, Vec<u8>)>
pub fn gateway_address_declaration( &self, challenge: &[u8], ) -> Result<(String, Vec<u8>, Vec<u8>)>
Builds the signed proof this device presents to attach to a gateway.
The bridge does not build these bytes. It holds the socket, so it holds the challenge, but the payload commits this device’s own address under a domain that must not be confusable with the relay’s, and the private key that signs it never leaves the core. Putting the construction here means there is one implementation, pinned by conformance vectors that CI runs, instead of one per bridge pinned by whatever that platform’s test target happens to compile.
Returns (address, public_key, signature). The caller base64-encodes
the two byte fields into DeclareAddress.
§Errors
- The challenge is not 32 bytes (
GATEWAY_CHALLENGE_LEN). A gateway that mints a shorter one has weakened the replay bound it exists to provide, and signing whatever it sent would be the wrong way to find that out. The check is here rather than in the bridge because a signing routine that will put its key over any bytes a remote party chose is the shape of a signing oracle. - This device has no identity yet, so there is nothing to declare
(
Error::MlsNotInitialized, which the FFI maps to the code the bridges are documented to expect).
Sourcepub fn on_gateway_address_declared(&mut self, declared: &str)
pub fn on_gateway_address_declared(&mut self, declared: &str)
Checks a gateway’s AddressDeclared answer against this device’s own
address.
The gateway twin of Self::on_relay_address_declared, and the same
disposition: report, do not act. A gateway verifies that the declared
address derives from the key that signed the proof before it echoes
anything, so an echo naming another address means it bound what it did
not verify, and a session attached under an address this device does
not control draws that address’s inbound traffic here and poisons the
presence answers given about it. Tearing the connection down from here
buys nothing: a gateway that owns the socket owns everything a local
teardown would protect.
What is acted on lives in the bridge and is narrower: it refuses to report the carrier available at all unless the session was bound, so a gateway that will not bind is a transport the selector never sees rather than one it trusts.
Reached only through the FFI’s dedicated entry point, not through message-plane injection, so a notification payload cannot synthesize it.
The warning is emitted at most once per
GATEWAY_ATTACH_WARNING_SUPPRESS_INTERVAL while the mismatch
persists, for the reason the refusal’s is: the bridge closes on a
mismatch and reconnects on its ladder. The warn! is not suppressed.
Sourcepub fn note_gateway_attached(&mut self)
pub fn note_gateway_attached(&mut self)
A gateway bound and announced this session. Resets the attach warning suppression, so a refusal or mismatch after a good session, or from a different box after a reconfigure, is reported immediately.
Sourcepub fn on_gateway_address_declaration_refused(&mut self, reason: &str)
pub fn on_gateway_address_declaration_refused(&mut self, reason: &str)
Records a gateway refusing this connection’s address declaration.
Unlike the relay case, this is not a degraded-but-working connection. The relay keeps delivering on established sessions in account-name space; a gateway has no such space, so an unproved session may submit and be told a verdict and is never registered as a recipient. The bridge therefore does not report the carrier available, and this warning is what explains a transport that stays down while its socket connects perfectly well.
reason is the gateway’s own text: opaque, remote-chosen, and logged
rather than emitted — an event field never carries text a remote party
wrote. Attributed to this node rather than to a peer, since the
failure is ours and no peer is involved.
Emitted at most once per
GATEWAY_ATTACH_WARNING_SUPPRESS_INTERVAL (five minutes): the bridge
closes a refused connection and reconnects on its ladder, so a gateway
that keeps refusing would otherwise produce a security event per rung,
forever. The warn! is not suppressed.
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn send_message(
&mut self,
recipient: impl Into<String>,
content: impl Into<String>,
priority: Option<MessagePriority>,
reply_to_msg: Option<impl Into<String>>,
) -> Result<MessageId>
pub fn send_message( &mut self, recipient: impl Into<String>, content: impl Into<String>, priority: Option<MessagePriority>, reply_to_msg: Option<impl Into<String>>, ) -> Result<MessageId>
Sends a message.
§Arguments
recipient- Recipient’s user IDcontent- Message contentpriority- Message priority (optional, defaults to Medium)reply_to_msg- ID of the message this is replying to (optional)
§Returns
Returns the message ID if successful.
§Auto-Encryption
When encryption is enabled and MLS is initialized, messages are automatically
encrypted before sending. If no session exists with the recipient but we have
their key package, a session is created automatically. If no key package is
available and store_pending is enabled, the message is queued until a key
package is received.
Sourcepub fn send_message_with(
&mut self,
recipient: impl Into<String>,
content: impl Into<String>,
options: SendMessageOptions,
) -> Result<MessageId>
pub fn send_message_with( &mut self, recipient: impl Into<String>, content: impl Into<String>, options: SendMessageOptions, ) -> Result<MessageId>
Sends a message with rich options: priority and reply threading (as
on Self::send_message), plus quoted-reply context, rich media
metadata, and forward attribution.
The rich fields only ever travel inside the MLS-sealed __RICH_V1__
body, and only toward recipients whose key package advertised
rich_versions support (gated by
EncryptionConfig::rich_payload_enabled). Toward anyone else they
are silently dropped — never sent cleartext — so the message degrades
to plain text with reply_to_msg threading intact.
Returns InvalidArgument for ContentType::FileChunk (an internal
transport content type — the receiver would swallow the message into
its file-transfer manager) and for rich extras whose serialized size
exceeds 32 KiB (oversized bodies inflate the MLS plaintext into heavy
transport fragmentation).
Sourcepub fn forward_message(
&mut self,
original_message: &Message,
new_recipient: impl Into<String>,
priority: Option<MessagePriority>,
) -> Result<MessageId>
pub fn forward_message( &mut self, original_message: &Message, new_recipient: impl Into<String>, priority: Option<MessagePriority>, ) -> Result<MessageId>
Forwards a message to a new recipient with attribution to the original sender.
Creates a new message with the original content and attaches ForwardInfo
tracking the original sender, message ID, timestamp, and forward count.
If the message was already forwarded, the original attribution is preserved
and forward_count is incremented.
Toward a recipient that advertised the sealed rich payload, the
attribution and the original media_metadata — including any
cloud-media encryption_key/iv secrets — travel inside the MLS-sealed
__RICH_V1__ body, so forwarded cloud media stays openable. Toward
anyone else they ride the legacy cleartext outer fields, from which
the transport chokepoint strips the secrets — attribution and display
hints survive, but the media key cannot be delivered.
§Arguments
original_message- The message to forwardnew_recipient- Recipient’s user IDpriority- Message priority (optional, defaults to Medium)
§Returns
Returns the new message ID if successful.
Sourcepub fn send_message_via_transport(
&mut self,
recipient: impl Into<String>,
content: impl Into<String>,
priority: Option<MessagePriority>,
transport: TransportType,
reply_to_msg: Option<impl Into<String>>,
) -> Result<MessageId>
pub fn send_message_via_transport( &mut self, recipient: impl Into<String>, content: impl Into<String>, priority: Option<MessagePriority>, transport: TransportType, reply_to_msg: Option<impl Into<String>>, ) -> Result<MessageId>
Sends a message via a specific transport type (bypassing DORS).
Sourcepub fn send_media(
&mut self,
recipient: impl Into<String>,
file_data: Vec<u8>,
file_name: impl Into<String>,
content_type: ContentType,
media_metadata: Option<MediaMetadata>,
) -> Result<String>
pub fn send_media( &mut self, recipient: impl Into<String>, file_data: Vec<u8>, file_name: impl Into<String>, content_type: ContentType, media_metadata: Option<MediaMetadata>, ) -> Result<String>
Sends a media attachment (image, video, audio, file, etc.) to a recipient.
The file data is chunked and each chunk is sent as an individual message
with content_type: FileChunk. The first chunk carries the full
MediaMetadata so the receiver can display a preview before all chunks
arrive. Individual chunk messages require ACKs and participate in retry
logic so delivery is tracked and recoverable per chunk. MediaSent is
emitted only after all chunks are ACKed.
§Encryption
With auto-encryption active (encryption enabled and MLS initialized),
chunk bytes — and the chunk-0 media metadata and original content type —
travel inside MLS ciphertext, using the same session the text path uses.
The session must already be confirmed: media is never queued pending
establishment and never falls back to plaintext. When the session is
not ready this returns Error::SessionNotReady (after kicking
establishment if a key package is available); retry after the
secure_session_established event. When encryption is required but MLS
is not initialized this returns Error::EncryptFailed. Plaintext
chunks are sent only when auto-encryption is inactive AND
require_encryption was explicitly set to false (it defaults to
true): the explicit encryption opt-out, or encryption enabled but
MLS never initialized (matching the text path). Every plaintext
transfer emits a
crate::events::SecurityWarningCode::PlaintextSend warning, once
per peer.
Returns a file_id that can be used to track progress or cancel.
Sourcepub fn send_media_with(
&mut self,
recipient: impl Into<String>,
file_data: Vec<u8>,
file_name: impl Into<String>,
content_type: ContentType,
options: MediaSendOptions,
) -> Result<String>
pub fn send_media_with( &mut self, recipient: impl Into<String>, file_data: Vec<u8>, file_name: impl Into<String>, content_type: ContentType, options: MediaSendOptions, ) -> Result<String>
Sends a media attachment with rich options: the chunk-0 media
metadata (as on Self::send_media), plus caption, reply threading,
quoted-reply context, forward attribution, and an optional
caller-supplied file_id.
The rich fields only ever travel inside the MLS-sealed chunk-0
plaintext (under the v2 media envelope), and only toward recipients
whose key package advertised rich_versions support (gated by
EncryptionConfig::rich_payload_enabled). Toward anyone else —
including every plaintext (encryption opt-out) transfer — they are
silently dropped, never sent cleartext.
Returns InvalidArgument for an unparsable reply_to_msg, rich
extras whose serialized size exceeds 32 KiB, or a file_id that is
empty, over the wire field bound, or already carried by an active
outbound transfer. Encryption preconditions match
Self::send_media.
Sourcepub fn on_transport_send_confirmed(&mut self, message_id: &str) -> Result<()>
pub fn on_transport_send_confirmed(&mut self, message_id: &str) -> Result<()>
Confirms that a message was successfully sent by the transport layer.
Sourcepub fn on_transport_send_failed(
&mut self,
message_id: &str,
transport_error: Option<String>,
) -> Result<()>
pub fn on_transport_send_failed( &mut self, message_id: &str, transport_error: Option<String>, ) -> Result<()>
Handles asynchronous transport send failures for pending welcome sends and outbound connection requests.
Sourcepub fn on_transport_send_failed_via(
&mut self,
message_id: &str,
transport_error: Option<String>,
carrier: Option<TransportType>,
) -> Result<()>
pub fn on_transport_send_failed_via( &mut self, message_id: &str, transport_error: Option<String>, carrier: Option<TransportType>, ) -> Result<()>
Self::on_transport_send_failed with the carrier that reported it.
The carrier is what turns a failure into a fact: “the recipient is not reachable” is meaningless on its own, since a device may hold several carriers and only one of them is making the claim. Bridges that know which transport answered pass it; the plain form above keeps working for callers that do not, and records no fact rather than guessing one.
Sourcepub fn send_report_is_carrier_failure(reason: Option<&str>) -> bool
pub fn send_report_is_carrier_failure(reason: Option<&str>) -> bool
Whether a platform send report also belongs in the carrier’s delivery
metrics (Transport::report_send_failure).
Every report does except the two push tokens. The bridges hand the
relay’s MessageSent { pushed: true } to
Self::on_transport_send_failed_via because that is their one call
for “the relay answered about this id”, but the relay took the frame
into a device push, which is not a failed send. A wrapper that scores
the report without asking here records a failure whenever the relay’s
answer beats the bridge’s own write confirmation, against the carrier
the router ranks by that ratio.
relay_stored is not in that company: it answers a DeliveryError,
where the write really did miss. That the mailbox kept a copy says
nothing about the carrier.
Sourcepub fn internet_control_op(
&self,
message: &Message,
) -> Option<(&'static str, String)>
pub fn internet_control_op( &self, message: &Message, ) -> Option<(&'static str, String)>
Classifies an outbound internet frame as a server-plane control op the
platform bridge should translate to (or mirror as) a relay-native
message instead of an opaque SendMessage.
Returns (op, payload) where payload is the JSON after the prefix
(empty string for payload-less ops). None means normal traffic —
send verbatim.
Ops and their bridge semantics:
group_relay_register/group_relay_broadcast— self-addressed relay hints (send_group_messageoptimization); REPLACE with relay-nativeCreateGroup+member deltas /SendGroupMessage. The relay does not intercept content prefixes, so untranslated frames would be echoed back uselessly.group_mls_leave— TAP, don’t replace: the per-member leave notification must still be delivered verbatim, but the bridge also sends one relay-nativeLeaveGroupso the relay’s group registry (which feeds invite links and broadcast fan-out) doesn’t go stale.
Connection ops (__CONN_REQ__/__CONN_ACC__/__CONN_REJ__/
__CONN_CAN__) deliberately do NOT classify: they ship verbatim as
opaque SendMessage frames so the Ed25519 control signature in the
message metadata survives to the receiver’s security gate. The former
relay-native translation (SendConnectionRequest & co.) rebuilt them
unsigned on the receiving bridge, which the gate now rejects
unconditionally — control traffic must be signed. Verbatim
also gains the relay’s push-notification fallback for offline
recipients, which the relay-native connection frames never had. The
cost is that pre-SDK relay clients (which only speak the relay-native
frames) no longer interoperate with connection ops — an accepted
trade: the SDK’s internet transport is the only supported relay
client.
Only frames originated by this device classify. The mesh relays
third-party messages verbatim (try_relay_message). The relay-hint
ops additionally require the self-addressed recipient (that is how
the core marks them as hints rather than traffic), so they classify
only when sender AND recipient are both this device.
Self-origination is proven by hop_count == 0, not by the sender
field: sender is an unauthenticated wire field, so a mesh peer
could forge sender == self on a frame we then relay — but
try_relay_message increments the hop before re-sending, while every
locally-originated frame leaves send_internal_message at hop 0.
(Defense in depth: the receive loop also drops inbound frames
claiming our own origin before they can reach the relay path.)
Sourcepub fn send_connection_request(
&mut self,
recipient: &str,
sender_name: &str,
key_package: Option<Vec<u8>>,
initial_message: Option<String>,
) -> Result<MessageId>
pub fn send_connection_request( &mut self, recipient: &str, sender_name: &str, key_package: Option<Vec<u8>>, initial_message: Option<String>, ) -> Result<MessageId>
Sends a connection request to another user via any available transport.
The request is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
§Arguments
recipient- The user ID of the recipientsender_name- Display name of the senderkey_package- Optional MLS key package for encrypted session setupinitial_message- Optional first message shown with the request (surfaced verbatim in the recipient’sConnectionRequestReceivedevent; apps typically seed the conversation with it on accept). At most 4096 UTF-8 bytes (MAX_INITIAL_MESSAGE_BYTES, crate-private) — longer input is rejected withError::InvalidArgument.
§Encryption
Connection requests are internal control messages sent in plaintext,
exempt from require_encryption (same as key packages and welcome
messages) — an initial_message is therefore NOT end-to-end
encrypted and should be treated like the sender display name.
Sourcepub fn accept_connection_request(
&mut self,
recipient: &str,
accepter_name: &str,
key_package: Option<Vec<u8>>,
) -> Result<MessageId>
pub fn accept_connection_request( &mut self, recipient: &str, accepter_name: &str, key_package: Option<Vec<u8>>, ) -> Result<MessageId>
Accepts a connection request from another user via any available transport.
The response is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
§Arguments
recipient- The user ID of the original requesteraccepter_name- Display name of the accepting partykey_package- Optional MLS key package for encrypted session setup
§Encryption
Connection accepts are internal control messages sent in plaintext,
exempt from require_encryption (same as key packages and welcome messages).
Sourcepub fn reject_connection_request(
&mut self,
recipient: &str,
) -> Result<MessageId>
pub fn reject_connection_request( &mut self, recipient: &str, ) -> Result<MessageId>
Rejects a connection request from another user via any available transport.
The response is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
§Arguments
recipient- The user ID of the original requester
§Encryption
Connection rejects are internal control messages sent in plaintext,
exempt from require_encryption (same as key packages and welcome messages).
Sourcepub fn cancel_connection_request(
&mut self,
recipient: &str,
) -> Result<MessageId>
pub fn cancel_connection_request( &mut self, recipient: &str, ) -> Result<MessageId>
Cancels a previously sent connection request via any available transport.
The cancellation is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
§Arguments
recipient- The user ID of the original request recipient
§Encryption
Connection cancellations are internal control messages sent in plaintext,
exempt from require_encryption (same as key packages and welcome messages).
Sourcepub fn send_presence_update(
&mut self,
recipient: &str,
status: PresenceStatus,
) -> Result<MessageId>
pub fn send_presence_update( &mut self, recipient: &str, status: PresenceStatus, ) -> Result<MessageId>
Sends a presence update to a single peer (unicast).
This sends to one recipient at a time. To broadcast presence to multiple peers, the caller must invoke this method once per peer.
§Arguments
recipient- The user ID of the peer to notifystatus- Presence status
Sourcepub fn send_typing_indicator(
&mut self,
recipient: &str,
conversation_id: &str,
is_typing: bool,
) -> Result<MessageId>
pub fn send_typing_indicator( &mut self, recipient: &str, conversation_id: &str, is_typing: bool, ) -> Result<MessageId>
Sends a typing indicator to a peer.
§Arguments
recipient- The user ID of the peer to notifyconversation_id- Opaque conversation key, carried to the peer and echoed back on its event. Never parsed or routed on. Conventionally the peer’s address for a DM and the group id for a group; any stable string works, but a display name does not (those change).is_typing- Whether the user started or stopped typing
Sourcepub fn send_read_receipt(
&mut self,
recipient: &str,
message_ids: Vec<String>,
) -> Result<MessageId>
pub fn send_read_receipt( &mut self, recipient: &str, message_ids: Vec<String>, ) -> Result<MessageId>
Sends a read receipt to a peer, indicating that the given messages have been read.
§Arguments
recipient- The user ID of the peer who sent the messagesmessage_ids- IDs of the messages that were read (max 256)
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn on_peer_presence(
&mut self,
peer_id: &str,
online: bool,
last_seen_ms: Option<i64>,
)
pub fn on_peer_presence( &mut self, peer_id: &str, online: bool, last_seen_ms: Option<i64>, )
Ingests an authoritative per-peer presence signal (the internet relay’s
PresenceStatusWithLastSeen answer, bridged by the platform layer).
online drives the same reachability machinery as a transport-level
discovery — outbox flush, welcome re-arm, auto key exchange — plus a
rescue for the store-less-relay hole: a welcome whose wire send was
confirmed (Sent) while the peer was actually offline was dropped by
the relay (it forwards or pushes, never stores), and
Self::rearm_welcome_for_peer deliberately never touches Sent.
The peer being provably online is the safe moment to rebuild and
re-send it: if the original did land, the receiver dedups by message
id and the confirmation probe resolves the lifecycle.
offline parks retry-pending welcomes pending a reachability edge,
without burning budget. In-flight records (SendAttempted) are left
to their confirm deadline — a DeliveryError-driven failure or the
confirm timeout resolves them, after which the next offline tick parks.
The welcome re-arm/re-send on the online edge is throttled per peer
with exponential backoff (WELCOME_PRESENCE_RESCUE_BASE_SECS ..
WELCOME_PRESENCE_RESCUE_MAX_SECS): presence answers arrive on the
platform’s ~20s watch tick, and a peer that is online but can never
prove the session (stale key package after a reinstall, incompatible
version) must not be re-sent the multi-frame MLS welcome on every
tick forever. A throttled online tick still flushes queued data-plane
traffic for the peer.
Emits presence_updated naming the carrier that answered — this
function is the gateway-sourced half of the unified stream and must
only be fed a gateway’s own observation (bridged via
internet_peer_presence or reticulum_peer_presence); peer-sent
__PRESENCE__ self-reports are handled in message_dispatch and emit
source: Peer. Self, blocked, or empty peer ids are dropped entirely:
an app waiting on presence_updated for a blocked peer will never see
it.
Sourcepub fn on_peer_presence_via(
&mut self,
peer_id: &str,
online: bool,
last_seen_ms: Option<i64>,
carrier: GatewayCarrier,
)
pub fn on_peer_presence_via( &mut self, peer_id: &str, online: bool, last_seen_ms: Option<i64>, carrier: GatewayCarrier, )
Self::on_peer_presence, for whichever gateway answered.
The carrier is threaded rather than assumed because every effect below is carrier-scoped and silently wrong under the wrong one: the fact is recorded against the carrier that made the claim, the un-park flush is pinned to the carrier that just proved it can reach the peer, and the event names the carrier so an app filtering on relay-observed presence still can. A Reticulum gateway’s answer forcing a send over the Internet transport would be a reachability proof about one carrier spent on another.
Sourcepub fn welcome_pending_peers(&self) -> Vec<String>
pub fn welcome_pending_peers(&self) -> Vec<String>
Peers with a welcome lifecycle whose session has not been proven —
the internet presence watch set. Includes Sent records (wire-
confirmed but never session-confirmed): only a presence signal can
rescue those over a store-less relay.
Lifecycles older than WELCOME_WATCHLIST_MAX_AGE_SECS are excluded:
a permanently-dead peer must not occupy watch-rotation slots (and keep
its parked record alive via expires_at pushes) forever.
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn note_nostr_event_received(&mut self, created_at_secs: i64)
pub fn note_nostr_event_received(&mut self, created_at_secs: i64)
Records that a Nostr relay event dated created_at_secs (unix seconds)
was received and decoded, advancing the receive watermark and persisting
it if it has moved far enough.
Call this only for frames that deserialized into a Message — see
NostrTransport::advance_receive_watermark for why an undecodable
frame must not move the mark.
Sourcepub fn restore_nostr_transport_state(&mut self)
pub fn restore_nostr_transport_state(&mut self)
Installs the persisted Nostr state into a transport that was registered
after Self::initialize_mls ran.
Both restores below are no-ops when no Nostr transport is registered,
which is exactly the state initialize_mls runs them in for the FFI:
the Nostr transport is derived from this device’s address, so the
bindings layer cannot construct it until initialize_mls has produced
one, and it installs the transport immediately afterwards. Without this
second pass that transport keeps the ephemeral signing key it was
constructed with — rotating this install’s Nostr identity on every
launch, so peers who sealed to the pubkey we advertised last time get
frames we can no longer open — and starts with no receive watermark, so
every cold start replays the full first-run backfill window.
Safe to call whether or not the first pass found a transport: the
signing-secret restore is guarded by nostr_secret_persisted, which a
transport-less pass leaves unset, and the watermark only ever moves
forward.
Sourcepub fn telemetry_install_id(&self) -> Option<String>
pub fn telemetry_install_id(&self) -> Option<String>
Returns a stable, opaque per-install telemetry identifier, or None
until the persistent scrub secret is available.
The id is SHA-256(secret || domain) truncated to a 32-character hex
string, where secret is the per-install scrub secret managed by
Self::restore_or_init_scrub_secret. The secret cannot be recovered
from the id, and the fixed domain string keeps the id un-correlatable
with opaque identifiers the scrubber produces for telemetry records:
the domain contains :, which id validation
(offline_protocol_core::types::validate_id_chars) rejects in every
UserId/AppId, so no validated identifier reaching the scrubber can
ever equal the domain and collide with the install id.
Returns None while the SDK is still on the random per-instance
fallback secret — i.e. before secure storage is provided via
super::OfflineProtocol::initialize_mls, or when persistence failed
this session. In that state the id would not be stable across launches,
so none is exposed.
Deliberately derived from the persistent fallback secret, not from an
installed crate::telemetry::TelemetryConfig::with_scrub_secret
override: the install id must not rotate when a sink is (re)installed,
and must not be computable from an app-chosen secret.
The domain string is part of the public contract: changing it would silently rotate every device’s install id. Frozen — do not edit.
Source§impl OfflineProtocol
impl OfflineProtocol
Sourcepub fn new(config: ProtocolConfig) -> Result<Self>
pub fn new(config: ProtocolConfig) -> Result<Self>
Creates a new protocol instance.
§Arguments
config- Protocol configuration
§Returns
Returns Ok(OfflineProtocol) if successful, Err if configuration is invalid.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn initialize_mls(
&mut self,
secure_storage: Arc<dyn MlsStorage>,
protocol_state_storage: Arc<dyn ProtocolStateStorage>,
) -> Result<()>
pub fn initialize_mls( &mut self, secure_storage: Arc<dyn MlsStorage>, protocol_state_storage: Arc<dyn ProtocolStateStorage>, ) -> Result<()>
Initializes MLS encryption and protocol persistence.
This must be called before encryption can be used. The storage
secure_storage must be a platform-native credential store.
protocol_state_storage must be scoped to the app container and is
removed when that container is deleted.
Ownership model:
OfflineProtocolis the single authoritative owner ofMlsManager- initialization is idempotent per protocol instance
- subsequent calls return without replacing the existing manager
- manager publication is transactional: restore must succeed before
mls_managerbecomes visible to callers
The two storage domains are intentionally different trait objects. This prevents message-plane state from inheriting the lifecycle of Keychain or another credential store.
Examples found in repository?
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58 let root = match std::env::args_os().nth(1) {
59 Some(root) => PathBuf::from(root),
60 None => {
61 let mut suffix = [0u8; 8];
62 OsRng.fill_bytes(&mut suffix);
63 std::env::temp_dir().join(format!("replicated-notes-{}", hex::encode(suffix)))
64 }
65 };
66 let key: Box<dyn StoreKeyProvider> =
67 if std::env::var_os(EnvStoreKey::DEFAULT_VARIABLE).is_some() {
68 Box::new(EnvStoreKey::default())
69 } else {
70 let mut key = [0u8; 32];
71 OsRng.fill_bytes(&mut key);
72 Box::new(StaticStoreKey::new(key))
73 };
74 println!("stores: {}", root.display());
75
76 // Both stores are on disk, which is what makes the reopen at the end a
77 // reopen rather than a fresh start. The secure store matters as much as
78 // the record store: documents are sealed with a key that lives in it, so
79 // losing only that half looks exactly like data loss.
80 let (secure, records) = open_stores(&root, key.as_ref())?;
81
82 // `data.enabled` defaults to true, so nothing here switches the layer on.
83 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
84 let mut protocol = OfflineProtocol::new(config)?;
85
86 // Documents are sealed at rest and the record key is minted here, so this
87 // call is the prerequisite for every method below.
88 protocol.initialize_mls(secure.clone(), records.clone())?;
89
90 // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91 // id for a group. Nothing replicates in this example, so the name only
92 // has to be a valid one.
93 let space = "demo-space";
94
95 protocol.data_create_doc(space, "trip")?;
96
97 // A map: last writer wins per key, and different keys never conflict.
98 protocol.data_map_set(
99 space,
100 "trip",
101 "meta",
102 "title",
103 DataValue::Text {
104 value: "Coast road".to_string(),
105 },
106 )?;
107
108 // A list: concurrent insertions both survive, in a deterministic order.
109 for item in ["water", "map", "torch"] {
110 protocol.data_list_push(
111 space,
112 "trip",
113 "packing",
114 DataValue::Text {
115 value: item.to_string(),
116 },
117 )?;
118 }
119
120 // Text: character-level merge, so two people typing in one paragraph keep
121 // both sets of words. Positions are character offsets, not byte offsets.
122 protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124 // A counter: increments add up rather than overwriting each other.
125 protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127 // Edits batch before they reach storage. This is the call that makes them
128 // durable, and `data_changed` fires after the same point, never before.
129 protocol.data_flush(space, "trip")?;
130
131 println!("spaces: {:?}", protocol.data_list_spaces()?);
132 println!("documents: {:?}", protocol.data_list_docs(space)?);
133 println!(
134 "compacted: {} bytes",
135 protocol.data_doc_size(space, "trip")?
136 );
137 println!(
138 "history: {} bytes",
139 protocol.data_export_raw(space, "trip")?.len()
140 );
141 println!("json: {}", protocol.data_doc_json(space, "trip")?);
142
143 // Both halves of the escape hatch are above: `data_doc_json` is the plain
144 // JSON of the current state, `data_export_raw` the engine-native history.
145 // An application can always take its data and leave.
146
147 // Reopen: a new engine over the same two directories, opened afresh as a
148 // restarted process would. The records were sealed by the first engine
149 // and are read back by the second, which is the property an application
150 // depends on across a restart.
151 drop(protocol);
152 drop((secure, records));
153 let (secure, records) = open_stores(&root, key.as_ref())?;
154 let config = ProtocolConfig::builder(APP_ID, PROFILE).build()?;
155 let mut reopened = OfflineProtocol::new(config)?;
156 reopened.initialize_mls(secure, records)?;
157
158 println!("after reopen");
159 println!(
160 " title: {:?}",
161 reopened.data_map_get(space, "trip", "meta", "title")?
162 );
163 println!(
164 " packing: {}",
165 reopened.data_list_len(space, "trip", "packing")?
166 );
167 println!(
168 " notes: {:?}",
169 reopened.data_text_value(space, "trip", "notes")?
170 );
171 println!(
172 " opened: {}",
173 reopened.data_counter_value(space, "trip", "opened")?
174 );
175
176 Ok(())
177}Sourcepub fn local_address(&self) -> Option<&str>
pub fn local_address(&self) -> Option<&str>
Returns this device’s address, once it has one.
None before initialize_mls: the address is the hash of an identity
key that lives in this profile’s storage, and storage does not arrive
until then. It never changes afterwards, and it is stable across
restarts of the same profile because the key is.
Sourcepub fn is_mls_initialized(&self) -> bool
pub fn is_mls_initialized(&self) -> bool
Checks if MLS encryption is initialized.
Sourcepub fn set_mls_event_emitter(&mut self, emitter: Arc<dyn MlsEventEmitter>)
pub fn set_mls_event_emitter(&mut self, emitter: Arc<dyn MlsEventEmitter>)
Configures the MLS lifecycle event emitter.
Sourcepub fn enable_telemetry(
&mut self,
config: TelemetryConfig,
host: TelemetryHost,
) -> Result<()>
pub fn enable_telemetry( &mut self, config: TelemetryConfig, host: TelemetryHost, ) -> Result<()>
Enables telemetry: the SDK collects, batches and uploads accepted events to the ingest itself.
config must carry the api_key and app_id the developer portal
issued; TelemetryConfig::validate names any field that is refused,
and the refusal is Error::TelemetryConfigInvalid. host names the
platform stamped on every batch and the application state right now.
Nothing waits on I/O here: the pipe’s storage load and first upload
run on its own thread. Batches are durable across a restart once a
protocol-state store is attached, which on the mobile bindings
happens inside initialize_mls; before that they are held in memory.
Calling this again replaces the running pipe after its final flush.
What leaves the device, when, and how to switch it off are in
docs/telemetry.md.
Sourcepub fn disable_telemetry(&mut self)
pub fn disable_telemetry(&mut self)
Disables telemetry: one final flush within FINAL_FLUSH_BUDGET, then
the uploader thread stops and every emit site goes back to its
telemetry-off cost. Callers holding an outer lock should prefer
Self::detach_telemetry_pipe and stop the pipe outside it.
Sourcepub fn detach_telemetry_pipe(&mut self) -> Option<Arc<TelemetryPipe>>
pub fn detach_telemetry_pipe(&mut self) -> Option<Arc<TelemetryPipe>>
Detaches the running pipe without stopping it: the sink is
uninstalled, the routing callback cleared, and the pipe handed back
for the caller to stop once it holds no engine lock. Its final flush
may block for up to FINAL_FLUSH_BUDGET, which is why the two steps
are separable.
Sourcepub fn telemetry_pipe(&self) -> Option<Arc<TelemetryPipe>>
pub fn telemetry_pipe(&self) -> Option<Arc<TelemetryPipe>>
The running telemetry pipe, if any.
Sourcepub fn set_telemetry_enabled(&self, enabled: bool)
pub fn set_telemetry_enabled(&self, enabled: bool)
Stops or resumes collection without tearing the pipe down. Off, every emit site costs one atomic load; what is already queued still drains.
Sourcepub fn flush_telemetry(&self)
pub fn flush_telemetry(&self)
Asks the uploader to flush now. Returns immediately.
Sourcepub fn flush_telemetry_blocking(&self, budget: Duration) -> bool
pub fn flush_telemetry_blocking(&self, budget: Duration) -> bool
Flushes and waits up to budget for the uploader to finish.
Sourcepub fn telemetry_stats(&self) -> Option<TelemetryStats>
pub fn telemetry_stats(&self) -> Option<TelemetryStats>
The pipe’s counters, or None while telemetry is not enabled.
Sourcepub fn end_telemetry_session(&self)
pub fn end_telemetry_session(&self)
Closes the current telemetry session: summary, a blocking flush, then a fresh session id.
Sourcepub fn notify_app_state(&self, state: AppState)
pub fn notify_app_state(&self, state: AppState)
Reports an application lifecycle transition to the pipe.
Sourcepub fn on_neighbor_discovered(&mut self, peer_id: &str)
pub fn on_neighbor_discovered(&mut self, peer_id: &str)
Called when a new neighbor is discovered.
When auto key exchange is enabled, this method sends our key package to the newly discovered peer to enable encrypted communication.
§Arguments
peer_id- The ID of the discovered peer
Sourcepub fn flush_outbox_all(&mut self)
pub fn flush_outbox_all(&mut self)
Immediately attempts to send all pending outbox messages across all peers.
Called when a transport becomes available (e.g. internet reconnects) to flush all queued messages, bypassing backoff timers.
Sourcepub fn set_gateway_capabilities(&mut self, capabilities: Vec<String>)
pub fn set_gateway_capabilities(&mut self, capabilities: Vec<String>)
Records what the attached gateway daemon says it can do.
Wholesale replace, like the relay’s: each attach describes the gateway actually connected now. The bridge calls this before it reports the carrier available, which is the contract’s own ordering — a device must not drain into a gateway whose features it has not been told — and it clears the set when the carrier drops.
The list is wire-supplied, so it is bounded exactly as the relay’s is,
which is what the contract points at rather than inventing a second
pair of numbers: at most 64 tokens of at most 128 bytes each, the same
MAX_RELAY_CAPABILITIES and MAX_RELAY_CAPABILITY_TOKEN_BYTES the
relay’s advertisement is bounded by. Oversized tokens are
dropped before the count is applied, so padding cannot evict real
ones.
Nothing here gates a security decision. Tokens gate features, and an attacker who can set them is already the gateway.
Sourcepub fn clear_gateway_capabilities(&mut self)
pub fn clear_gateway_capabilities(&mut self)
Forgets the attached gateway’s advertisement, on carrier drop.
Sourcepub fn gateway_capabilities(&self) -> Vec<String>
pub fn gateway_capabilities(&self) -> Vec<String>
The attached gateway’s advertised capability tokens, sorted so the order a caller sees does not depend on hashing.
Sourcepub fn presence_watch_peers(&self) -> Vec<String>
pub fn presence_watch_peers(&self) -> Vec<String>
Peers the platform layer should watch via a gateway CheckPresence
query: every peer with an undelivered or session-unproven MLS welcome
(Self::welcome_pending_peers) plus every recipient of an outbox
message not currently awaiting an ACK — which includes DMs parked on a
recipient_unreachable verdict. A gateway’s presence-online answer
lands in Self::on_peer_presence_via, whose
flush_outbox_for_peer_via is what re-drives (un-parks) those
messages, so the SDK — not the app — owns the “watch my DeliveryError
recipients” duty. Bounded by the outbox cap (500 entries).
Carrier-agnostic on purpose: the set is “who this device is waiting to hear about”, which does not depend on who is asked. Every attached gateway watches the same peers.
Sourcepub fn on_neighbor_lost(&mut self, peer_id: &str)
pub fn on_neighbor_lost(&mut self, peer_id: &str)
Called when a neighbor is lost.
Cleans up tracking state for the lost peer.
§Arguments
peer_id- The ID of the lost peer
Sourcepub fn rekey_session(&mut self, peer_id: &str) -> Result<bool>
pub fn rekey_session(&mut self, peer_id: &str) -> Result<bool>
Tears down the 1:1 session with peer_id and rebuilds it, advancing
post-compromise security.
§Why an application has to ask
Post-compromise security arrives when a commit rotates a member’s leaf in the ratchet tree. This engine originates one on a re-key, and until now the only thing that drove a re-key was an epoch desync: a healthy pair that never forked never rotated, so the window an attacker holding old key material keeps open was bounded by nothing.
That is felt hardest against a leaf node, which never commits at all, so every rotation in such a pair is the phone’s to originate. It is not exclusive to one: two phones that simply never desync are in the same position.
The cadence is the application’s because this engine cannot pick one. A rotation costs a teardown, a key-package exchange and a re-establish, and what that is worth depends on the deployment: a door lock on mains power and a phone on a metered link do not want the same interval, and nothing on the wire says which one is on the other end.
§What the peer sees
Exactly what a desync-driven re-key sends, because it is the same code:
a __MLS_KEY_PKG__ frame carrying session_reset. A peer running this
engine discards its stale session and converges in one round trip; a
leaf node discards its session, mints a fresh key package and re-pairs.
Queued plaintext survives, because it is sealed at flush time against
whatever session is current then.
§Rate limit
Shared with the desync path, one re-key per peer per window. A caller looping on this cannot do to a pair what that bound exists to stop an attacker doing.
§Returns
Ok(true)- the reset was advertised and the session torn down.Ok(false)- the rate-limit window has not lapsed; nothing was done, and a later call succeeds. Not an error: a caller on a fixed schedule that briefly runs faster than the floor is behaving correctly.
§A rotation that fails changes nothing
The reset is advertised before the local session is torn down, so
an Err leaves the pair exactly as it was: the session is still there,
still usable, and the rate-limit window is not spent. Rotate while the
peer is reachable, and treat a failure as “try again later” rather than
as a session to rebuild.
The order matters most against a leaf node, which speaks only when spoken to. Tearing down first and failing to send would leave this side with no session, the device holding one it will never be told to drop, and nothing on either side that re-opens the exchange.
§Errors
Error::MlsNotInitialized- encryption is not set up.Error::UserBlocked- the peer is blocked.Error::MlswithSessionNotFound- there is no session to rebuild. Establishing a first session isSelf::establish_secure_session; this heals one that exists.Error::Transport- no transport accepted the reset frame. The session is untouched, as above.
Sourcepub fn establish_secure_session(
&mut self,
peer_id: &str,
) -> Result<Option<WelcomeMessage>>
pub fn establish_secure_session( &mut self, peer_id: &str, ) -> Result<Option<WelcomeMessage>>
Establishes a secure MLS session with a peer.
This high-level method handles the complete session establishment flow:
- Checks if a session already exists (returns Ok(None) if so)
- Checks for a pending key package from the peer
- If found, imports the key package, creates the session, and sends the Welcome message
- If no key package is available, returns an error
This method is designed for use by application code that needs explicit control over session establishment, as opposed to the automatic encryption flow.
§Arguments
peer_id- The ID of the peer to establish a session with
§Returns
Ok(Some(WelcomeMessage))- Session created, Welcome message returned (and sent to peer)Ok(None)- Session already existsErr(SessionNotReady(state))- Establishment in progress; caller can retry or show “Establishing…”
Sourcepub fn has_pending_key_package(&self, peer_id: &str) -> bool
pub fn has_pending_key_package(&self, peer_id: &str) -> bool
Sourcepub fn get_establishment_state(
&self,
peer_id: &str,
) -> Result<EstablishmentState>
pub fn get_establishment_state( &self, peer_id: &str, ) -> Result<EstablishmentState>
Returns the current establishment state for a peer.
Use this to show “Establishing…” or drive retries without calling send/establish.
Sourcepub fn peer_supports_rich_payload(&self, peer_id: &str) -> bool
pub fn peer_supports_rich_payload(&self, peer_id: &str) -> bool
Whether rich extras (reply context, rich media metadata, forward
attribution) on a send toward peer_id would travel in the sealed
rich body — i.e. the peer’s last key package advertised the
capability (surviving restarts) and our own rich_payload_enabled
kill switch is on. When false the extras are silently dropped and
the message degrades to plain text, so apps can use this to render
honest degraded-reply UX instead of discovering the drop after the
fact.
Sourcepub fn manual_mls_create_session(
&mut self,
peer_id: &str,
) -> Result<WelcomeMessage>
pub fn manual_mls_create_session( &mut self, peer_id: &str, ) -> Result<WelcomeMessage>
Creates a session using manually imported key material.
This entrypoint is for bindings that expose low-level MLS APIs. It must still keep protocol-level session lifecycle state in sync with MLS state.
Sourcepub fn manual_mls_import_key_package(
&mut self,
peer_id: &str,
key_package_data: &[u8],
) -> Result<()>
pub fn manual_mls_import_key_package( &mut self, peer_id: &str, key_package_data: &[u8], ) -> Result<()>
Imports a contact’s key package and records them encryption-capable.
The entry point for bindings that expose low-level MLS APIs. It exists
because the FFI wrapper used to reach straight for the MlsManager and
pass the since-deleted KeyPackageTrust::FirstUse — so an app could
import a key package
under any peer id with no correlation to that peer, bypassing the check
establish_secure_session performs. The identity claim no longer needs
the detour: import_key_package re-derives the address from the leaf
signature key and refuses a mismatch on its own. The routing stays
because the capability record lives here, not on the manager.
Sourcepub fn manual_mls_delete_session(&mut self, peer_id: &str) -> Result<()>
pub fn manual_mls_delete_session(&mut self, peer_id: &str) -> Result<()>
Deletes a 1:1 session and clears protocol-level lifecycle state.
Sourcepub fn manual_mls_join_session(
&mut self,
welcome: &WelcomeMessage,
) -> Result<GroupInfo>
pub fn manual_mls_join_session( &mut self, welcome: &WelcomeMessage, ) -> Result<GroupInfo>
Joins a session from a Welcome message and synchronizes confirmation state.
Sourcepub fn manual_mls_process_welcome(
&mut self,
welcome: &WelcomeMessage,
) -> Result<GroupInfo>
pub fn manual_mls_process_welcome( &mut self, welcome: &WelcomeMessage, ) -> Result<GroupInfo>
Processes an MLS Welcome message and synchronizes confirmation state.
Sourcepub fn manual_mls_decrypt_from_user(
&mut self,
encrypted: &EncryptedMessage,
) -> Result<Option<Vec<u8>>>
pub fn manual_mls_decrypt_from_user( &mut self, encrypted: &EncryptedMessage, ) -> Result<Option<Vec<u8>>>
Decrypts a user-scoped MLS message and synchronizes confirmation state.
Sourcepub fn manual_mls_decrypt(
&mut self,
encrypted: &EncryptedMessage,
) -> Result<Option<Vec<u8>>>
pub fn manual_mls_decrypt( &mut self, encrypted: &EncryptedMessage, ) -> Result<Option<Vec<u8>>>
Decrypts any MLS message and synchronizes confirmation state for 1:1 flows.
Trait Implementations§
Source§impl Drop for OfflineProtocol
impl Drop for OfflineProtocol
Auto Trait Implementations§
impl !Freeze for OfflineProtocol
impl !RefUnwindSafe for OfflineProtocol
impl !UnwindSafe for OfflineProtocol
impl Send for OfflineProtocol
impl Sync for OfflineProtocol
impl Unpin for OfflineProtocol
impl UnsafeUnpin for OfflineProtocol
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Declassify for T
impl<T> Declassify for T
type Declassified = T
fn declassify(self) -> T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> Paint for Twhere
T: ?Sized,
impl<T> Paint for Twhere
T: ?Sized,
Source§fn fg(&self, value: Color) -> Painted<&T>
fn fg(&self, value: Color) -> Painted<&T>
Returns a styled value derived from self with the foreground set to
value.
This method should be used rarely. Instead, prefer to use color-specific
builder methods like red() and
green(), which have the same functionality but are
pithier.
§Example
Set foreground color to white using fg():
use yansi::{Paint, Color};
painted.fg(Color::White);Set foreground color to white using white().
use yansi::Paint;
painted.white();Source§fn bright_black(&self) -> Painted<&T>
fn bright_black(&self) -> Painted<&T>
Source§fn bright_red(&self) -> Painted<&T>
fn bright_red(&self) -> Painted<&T>
Source§fn bright_green(&self) -> Painted<&T>
fn bright_green(&self) -> Painted<&T>
Source§fn bright_yellow(&self) -> Painted<&T>
fn bright_yellow(&self) -> Painted<&T>
Source§fn bright_blue(&self) -> Painted<&T>
fn bright_blue(&self) -> Painted<&T>
Source§fn bright_magenta(&self) -> Painted<&T>
fn bright_magenta(&self) -> Painted<&T>
Source§fn bright_cyan(&self) -> Painted<&T>
fn bright_cyan(&self) -> Painted<&T>
Source§fn bright_white(&self) -> Painted<&T>
fn bright_white(&self) -> Painted<&T>
Source§fn bg(&self, value: Color) -> Painted<&T>
fn bg(&self, value: Color) -> Painted<&T>
Returns a styled value derived from self with the background set to
value.
This method should be used rarely. Instead, prefer to use color-specific
builder methods like on_red() and
on_green(), which have the same functionality but
are pithier.
§Example
Set background color to red using fg():
use yansi::{Paint, Color};
painted.bg(Color::Red);Set background color to red using on_red().
use yansi::Paint;
painted.on_red();Source§fn on_primary(&self) -> Painted<&T>
fn on_primary(&self) -> Painted<&T>
Source§fn on_magenta(&self) -> Painted<&T>
fn on_magenta(&self) -> Painted<&T>
Source§fn on_bright_black(&self) -> Painted<&T>
fn on_bright_black(&self) -> Painted<&T>
Source§fn on_bright_red(&self) -> Painted<&T>
fn on_bright_red(&self) -> Painted<&T>
Source§fn on_bright_green(&self) -> Painted<&T>
fn on_bright_green(&self) -> Painted<&T>
Source§fn on_bright_yellow(&self) -> Painted<&T>
fn on_bright_yellow(&self) -> Painted<&T>
Source§fn on_bright_blue(&self) -> Painted<&T>
fn on_bright_blue(&self) -> Painted<&T>
Source§fn on_bright_magenta(&self) -> Painted<&T>
fn on_bright_magenta(&self) -> Painted<&T>
Source§fn on_bright_cyan(&self) -> Painted<&T>
fn on_bright_cyan(&self) -> Painted<&T>
Source§fn on_bright_white(&self) -> Painted<&T>
fn on_bright_white(&self) -> Painted<&T>
Source§fn attr(&self, value: Attribute) -> Painted<&T>
fn attr(&self, value: Attribute) -> Painted<&T>
Enables the styling Attribute value.
This method should be used rarely. Instead, prefer to use
attribute-specific builder methods like bold() and
underline(), which have the same functionality
but are pithier.
§Example
Make text bold using attr():
use yansi::{Paint, Attribute};
painted.attr(Attribute::Bold);Make text bold using using bold().
use yansi::Paint;
painted.bold();Source§fn rapid_blink(&self) -> Painted<&T>
fn rapid_blink(&self) -> Painted<&T>
Source§fn quirk(&self, value: Quirk) -> Painted<&T>
fn quirk(&self, value: Quirk) -> Painted<&T>
Enables the yansi Quirk value.
This method should be used rarely. Instead, prefer to use quirk-specific
builder methods like mask() and
wrap(), which have the same functionality but are
pithier.
§Example
Enable wrapping using .quirk():
use yansi::{Paint, Quirk};
painted.quirk(Quirk::Wrap);Enable wrapping using wrap().
use yansi::Paint;
painted.wrap();Source§fn clear(&self) -> Painted<&T>
👎Deprecated since 1.0.1: renamed to resetting() due to conflicts with Vec::clear().
The clear() method will be removed in a future release.
fn clear(&self) -> Painted<&T>
renamed to resetting() due to conflicts with Vec::clear().
The clear() method will be removed in a future release.
Source§fn whenever(&self, value: Condition) -> Painted<&T>
fn whenever(&self, value: Condition) -> Painted<&T>
Conditionally enable styling based on whether the Condition value
applies. Replaces any previous condition.
See the crate level docs for more details.
§Example
Enable styling painted only when both stdout and stderr are TTYs:
use yansi::{Paint, Condition};
painted.red().on_yellow().whenever(Condition::STDOUTERR_ARE_TTY);