Skip to main content

OfflineProtocol

Struct OfflineProtocol 

Source
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

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

pub fn list_groups(&self) -> Result<Vec<String>>

Lists all MLS groups (excluding 1:1 sessions).

Source

pub fn get_group_info(&self, group_id: &str) -> Result<Option<GroupInfo>>

Gets information about an MLS group.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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

Source

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.

Source

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.

Source

pub fn get_blocked_users(&self) -> Vec<String>

Returns the list of currently blocked user IDs, sorted for stable output.

Source

pub fn is_user_blocked(&self, user_id: &str) -> bool

Checks whether a user is currently blocked.

Source§

impl OfflineProtocol

Source

pub fn state(&self) -> ProtocolState

Gets the current protocol state.

Source

pub fn config(&self) -> &ProtocolConfig

Gets the configuration.

Source

pub fn mesh_services(&self) -> &MeshServices

Gets a reference to the mesh services registry.

Source

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.

Source

pub fn transport_manager(&self) -> &TransportManager

Gets a reference to the transport manager.

Source

pub fn file_transfer_manager_mut(&mut self) -> &mut FileTransferManager

Returns a mutable reference to the file transfer manager.

Source

pub fn file_transfer_manager(&self) -> &FileTransferManager

Returns a reference to the file transfer manager.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn relay_config(&self) -> &RelayConfig

Returns the current relay configuration.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn deduplicator_stats(&self) -> DeduplicatorStats

Gets deduplicator statistics for monitoring.

Source

pub fn pending_queue_metrics(&self) -> PendingQueueMetrics

Gets pending encrypted message queue counters and gauges.

Source

pub fn pending_ack_count(&self) -> usize

Gets the current ACK manager statistics.

Source

pub fn retry_queue_size(&self) -> usize

Gets the current retry queue statistics.

Source

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.

Source

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.

Source

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.

Source

pub fn register_service(&mut self, descriptor: ServiceDescriptor) -> Result<()>

Registers a local service that this node offers.

Source

pub fn unregister_service(&mut self, service_id: &str) -> Result<bool>

Unregisters a local service. Returns true if the service was found and removed.

Source

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

pub fn send_service_request( &mut self, provider: &str, service_id: &str, method: &str, body: &str, ) -> Result<String>

Sends a typed service request to a specific provider peer. Returns a request_id. The response arrives as a ServiceResponseReceived event.

Source

pub fn respond_to_service_request( &mut self, request_id: &str, requester: &str, service_id: &str, status: &str, body: &str, ) -> Result<MessageId>

Responds to a service request from another peer.

Source§

impl OfflineProtocol

Source

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.

Source

pub fn custody_config(&self) -> &CustodyConfig

The custody configuration in force.

Source

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

Source

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.

Source

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?
examples/replicated_notes.rs (line 95)
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

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.

Source

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.

Source

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.

Source

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.

Source

pub fn data_list_docs(&mut self, space: &str) -> Result<Vec<String>>

The documents in a space.

Examples found in repository?
examples/replicated_notes.rs (line 132)
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

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?
examples/replicated_notes.rs (line 131)
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

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?
examples/replicated_notes.rs (lines 98-106)
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

pub fn data_map_delete( &mut self, space: &str, doc: &str, collection: &str, key: &str, ) -> Result<()>

Remove a key from a map collection.

Source

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?
examples/replicated_notes.rs (line 161)
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

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?
examples/replicated_notes.rs (lines 110-117)
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

pub fn data_list_insert( &mut self, space: &str, doc: &str, collection: &str, index: u32, value: DataValue, ) -> Result<()>

Insert into a list collection.

Source

pub fn data_list_delete( &mut self, space: &str, doc: &str, collection: &str, index: u32, count: u32, ) -> Result<()>

Delete entries from a list collection.

Source

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?
examples/replicated_notes.rs (line 165)
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

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?
examples/replicated_notes.rs (line 122)
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

pub fn data_text_delete( &mut self, space: &str, doc: &str, collection: &str, position: u32, count: u32, ) -> Result<()>

Delete characters from a text collection.

Source

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?
examples/replicated_notes.rs (line 169)
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

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?
examples/replicated_notes.rs (line 125)
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

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?
examples/replicated_notes.rs (line 173)
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

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?
examples/replicated_notes.rs (line 141)
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

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?
examples/replicated_notes.rs (line 139)
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

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?
examples/replicated_notes.rs (line 129)
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

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.

Source

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.

Source

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?
examples/replicated_notes.rs (line 135)
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

Source

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.

Source

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.

Source

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.

Source

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.

Source

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

Source

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::InvalidArgument if the string is not a claimable username.
  • Error::InvalidConfiguration if username discovery is off. It also requires cold contact, so this covers a claim that could only dead-end one hop later.
  • Error::InvalidState if too many lookups are already in flight. Transient: retry once earlier ones drain.
  • Error::NotStarted if the protocol is not running. Nothing pumps relay traffic and nothing sweeps deadlines outside Running, so a lookup accepted here would be a promise with no machinery behind it. Transient: retry after start().
Source

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.

Source

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.

Source

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

Source

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

Source

pub fn receive_message(&mut self) -> Option<Message>

Receives the next available message.

Source§

impl OfflineProtocol

Source

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.

Source

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.

Source

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).
Source

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.

Source

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.

Source

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

Source

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 ID
  • content - Message content
  • priority - 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.

Source

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).

Source

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 forward
  • new_recipient - Recipient’s user ID
  • priority - Message priority (optional, defaults to Medium)
§Returns

Returns the new message ID if successful.

Source

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).

Source

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.

Source

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.

Source

pub fn on_transport_send_confirmed(&mut self, message_id: &str) -> Result<()>

Confirms that a message was successfully sent by the transport layer.

Source

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.

Source

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.

Source

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.

Source

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_message optimization); REPLACE with relay-native CreateGroup+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-native LeaveGroup so 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.)

Source

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 recipient
  • sender_name - Display name of the sender
  • key_package - Optional MLS key package for encrypted session setup
  • initial_message - Optional first message shown with the request (surfaced verbatim in the recipient’s ConnectionRequestReceived event; 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 with Error::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.

Source

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 requester
  • accepter_name - Display name of the accepting party
  • key_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).

Source

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).

Source

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).

Source

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 notify
  • status - Presence status
Source

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 notify
  • conversation_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
Source

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 messages
  • message_ids - IDs of the messages that were read (max 256)
Source§

impl OfflineProtocol

Source

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.

Source

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.

Source

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

Source

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.

Source

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.

Source

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

Source

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?
examples/replicated_notes.rs (line 84)
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

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:

  • OfflineProtocol is the single authoritative owner of MlsManager
  • initialization is idempotent per protocol instance
  • subsequent calls return without replacing the existing manager
  • manager publication is transactional: restore must succeed before mls_manager becomes 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?
examples/replicated_notes.rs (line 88)
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

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.

Source

pub fn is_mls_initialized(&self) -> bool

Checks if MLS encryption is initialized.

Source

pub fn set_mls_event_emitter(&mut self, emitter: Arc<dyn MlsEventEmitter>)

Configures the MLS lifecycle event emitter.

Source

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.

Source

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.

Source

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.

Source

pub fn telemetry_pipe(&self) -> Option<Arc<TelemetryPipe>>

The running telemetry pipe, if any.

Source

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.

Source

pub fn flush_telemetry(&self)

Asks the uploader to flush now. Returns immediately.

Source

pub fn flush_telemetry_blocking(&self, budget: Duration) -> bool

Flushes and waits up to budget for the uploader to finish.

Source

pub fn telemetry_stats(&self) -> Option<TelemetryStats>

The pipe’s counters, or None while telemetry is not enabled.

Source

pub fn end_telemetry_session(&self)

Closes the current telemetry session: summary, a blocking flush, then a fresh session id.

Source

pub fn notify_app_state(&self, state: AppState)

Reports an application lifecycle transition to the pipe.

Source

pub fn start(&mut self) -> Result<()>

Starts the protocol.

§Returns

Returns Ok(()) if started successfully, Err if already started.

Source

pub fn stop(&mut self) -> Result<()>

Stops the protocol gracefully.

§Returns

Returns Ok(()) if stopped successfully, Err if not started.

Source

pub fn pause(&mut self) -> Result<()>

Pauses the protocol (for background mode).

Source

pub fn resume(&mut self) -> Result<()>

Resumes the protocol from pause.

Source

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
Source

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.

Source

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.

Source

pub fn clear_gateway_capabilities(&mut self)

Forgets the attached gateway’s advertisement, on carrier drop.

Source

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.

Source

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.

Source

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
Source

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
Source

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:

  1. Checks if a session already exists (returns Ok(None) if so)
  2. Checks for a pending key package from the peer
  3. If found, imports the key package, creates the session, and sends the Welcome message
  4. 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 exists
  • Err(SessionNotReady(state)) - Establishment in progress; caller can retry or show “Establishing…”
Source

pub fn has_pending_key_package(&self, peer_id: &str) -> bool

Checks if a pending key package is available for a peer.

This can be used to check if session establishment is possible before calling establish_secure_session.

§Arguments
  • peer_id - The ID of the peer to check
§Returns

true if a key package is available, false otherwise

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn manual_mls_delete_session(&mut self, peer_id: &str) -> Result<()>

Deletes a 1:1 session and clears protocol-level lifecycle state.

Source

pub fn manual_mls_join_session( &mut self, welcome: &WelcomeMessage, ) -> Result<GroupInfo>

Joins a session from a Welcome message and synchronizes confirmation state.

Source

pub fn manual_mls_process_welcome( &mut self, welcome: &WelcomeMessage, ) -> Result<GroupInfo>

Processes an MLS Welcome message and synchronizes confirmation state.

Source

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.

Source

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.

Source

pub fn on_event<F>(&mut self, handler: F)
where F: Fn(Event) + Send + Sync + 'static,

Registers an event handler.

§Arguments
  • handler - Callback function that will be called for each event
Source

pub fn process(&mut self) -> Result<()>

Processes pending operations (retries, timeouts, etc.).

This should be called periodically to handle background tasks.

Trait Implementations§

Source§

impl Drop for OfflineProtocol

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> Classify for T

Source§

type Classified = T

Source§

fn classify(self) -> T

Source§

impl<T> Declassify for T

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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 more
Source§

impl<T> Paint for T
where T: ?Sized,

Source§

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 primary(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: Primary].

§Example
println!("{}", value.primary());
Source§

fn fixed(&self, color: u8) -> Painted<&T>

Returns self with the fg() set to [Color :: Fixed].

§Example
println!("{}", value.fixed(color));
Source§

fn rgb(&self, r: u8, g: u8, b: u8) -> Painted<&T>

Returns self with the fg() set to [Color :: Rgb].

§Example
println!("{}", value.rgb(r, g, b));
Source§

fn black(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: Black].

§Example
println!("{}", value.black());
Source§

fn red(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: Red].

§Example
println!("{}", value.red());
Source§

fn green(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: Green].

§Example
println!("{}", value.green());
Source§

fn yellow(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: Yellow].

§Example
println!("{}", value.yellow());
Source§

fn blue(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: Blue].

§Example
println!("{}", value.blue());
Source§

fn magenta(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: Magenta].

§Example
println!("{}", value.magenta());
Source§

fn cyan(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: Cyan].

§Example
println!("{}", value.cyan());
Source§

fn white(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: White].

§Example
println!("{}", value.white());
Source§

fn bright_black(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: BrightBlack].

§Example
println!("{}", value.bright_black());
Source§

fn bright_red(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: BrightRed].

§Example
println!("{}", value.bright_red());
Source§

fn bright_green(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: BrightGreen].

§Example
println!("{}", value.bright_green());
Source§

fn bright_yellow(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: BrightYellow].

§Example
println!("{}", value.bright_yellow());
Source§

fn bright_blue(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: BrightBlue].

§Example
println!("{}", value.bright_blue());
Source§

fn bright_magenta(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: BrightMagenta].

§Example
println!("{}", value.bright_magenta());
Source§

fn bright_cyan(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: BrightCyan].

§Example
println!("{}", value.bright_cyan());
Source§

fn bright_white(&self) -> Painted<&T>

Returns self with the fg() set to [Color :: BrightWhite].

§Example
println!("{}", value.bright_white());
Source§

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>

Returns self with the bg() set to [Color :: Primary].

§Example
println!("{}", value.on_primary());
Source§

fn on_fixed(&self, color: u8) -> Painted<&T>

Returns self with the bg() set to [Color :: Fixed].

§Example
println!("{}", value.on_fixed(color));
Source§

fn on_rgb(&self, r: u8, g: u8, b: u8) -> Painted<&T>

Returns self with the bg() set to [Color :: Rgb].

§Example
println!("{}", value.on_rgb(r, g, b));
Source§

fn on_black(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: Black].

§Example
println!("{}", value.on_black());
Source§

fn on_red(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: Red].

§Example
println!("{}", value.on_red());
Source§

fn on_green(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: Green].

§Example
println!("{}", value.on_green());
Source§

fn on_yellow(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: Yellow].

§Example
println!("{}", value.on_yellow());
Source§

fn on_blue(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: Blue].

§Example
println!("{}", value.on_blue());
Source§

fn on_magenta(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: Magenta].

§Example
println!("{}", value.on_magenta());
Source§

fn on_cyan(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: Cyan].

§Example
println!("{}", value.on_cyan());
Source§

fn on_white(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: White].

§Example
println!("{}", value.on_white());
Source§

fn on_bright_black(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: BrightBlack].

§Example
println!("{}", value.on_bright_black());
Source§

fn on_bright_red(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: BrightRed].

§Example
println!("{}", value.on_bright_red());
Source§

fn on_bright_green(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: BrightGreen].

§Example
println!("{}", value.on_bright_green());
Source§

fn on_bright_yellow(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: BrightYellow].

§Example
println!("{}", value.on_bright_yellow());
Source§

fn on_bright_blue(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: BrightBlue].

§Example
println!("{}", value.on_bright_blue());
Source§

fn on_bright_magenta(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: BrightMagenta].

§Example
println!("{}", value.on_bright_magenta());
Source§

fn on_bright_cyan(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: BrightCyan].

§Example
println!("{}", value.on_bright_cyan());
Source§

fn on_bright_white(&self) -> Painted<&T>

Returns self with the bg() set to [Color :: BrightWhite].

§Example
println!("{}", value.on_bright_white());
Source§

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 bold(&self) -> Painted<&T>

Returns self with the attr() set to [Attribute :: Bold].

§Example
println!("{}", value.bold());
Source§

fn dim(&self) -> Painted<&T>

Returns self with the attr() set to [Attribute :: Dim].

§Example
println!("{}", value.dim());
Source§

fn italic(&self) -> Painted<&T>

Returns self with the attr() set to [Attribute :: Italic].

§Example
println!("{}", value.italic());
Source§

fn underline(&self) -> Painted<&T>

Returns self with the attr() set to [Attribute :: Underline].

§Example
println!("{}", value.underline());

Returns self with the attr() set to [Attribute :: Blink].

§Example
println!("{}", value.blink());

Returns self with the attr() set to [Attribute :: RapidBlink].

§Example
println!("{}", value.rapid_blink());
Source§

fn invert(&self) -> Painted<&T>

Returns self with the attr() set to [Attribute :: Invert].

§Example
println!("{}", value.invert());
Source§

fn conceal(&self) -> Painted<&T>

Returns self with the attr() set to [Attribute :: Conceal].

§Example
println!("{}", value.conceal());
Source§

fn strike(&self) -> Painted<&T>

Returns self with the attr() set to [Attribute :: Strike].

§Example
println!("{}", value.strike());
Source§

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 mask(&self) -> Painted<&T>

Returns self with the quirk() set to [Quirk :: Mask].

§Example
println!("{}", value.mask());
Source§

fn wrap(&self) -> Painted<&T>

Returns self with the quirk() set to [Quirk :: Wrap].

§Example
println!("{}", value.wrap());
Source§

fn linger(&self) -> Painted<&T>

Returns self with the quirk() set to [Quirk :: Linger].

§Example
println!("{}", value.linger());
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.

Returns self with the quirk() set to [Quirk :: Clear].

§Example
println!("{}", value.clear());
Source§

fn resetting(&self) -> Painted<&T>

Returns self with the quirk() set to [Quirk :: Resetting].

§Example
println!("{}", value.resetting());
Source§

fn bright(&self) -> Painted<&T>

Returns self with the quirk() set to [Quirk :: Bright].

§Example
println!("{}", value.bright());
Source§

fn on_bright(&self) -> Painted<&T>

Returns self with the quirk() set to [Quirk :: OnBright].

§Example
println!("{}", value.on_bright());
Source§

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);
Source§

fn new(self) -> Painted<Self>
where Self: Sized,

Create a new Painted with a default Style. Read more
Source§

fn paint<S>(&self, style: S) -> Painted<&Self>
where S: Into<Style>,

Apply a style wholesale to self. Any previous style is replaced. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more