Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Once

Once<T> is a replicated write-once register. It holds at most one value — once a value has been set, all subsequent writes are silently ignored by the state machine. This is the distributed equivalent of a tokio::sync::OnceCell.

Unlike Register<T>, which allows overwriting the stored value, Once<T> guarantees that the first accepted write is permanent. This makes it ideal for one-time initialization patterns where you need exactly one value to win across a distributed cluster.

Internally the state is an Option<T>, identical to Register, but the apply logic rejects writes when the value is already Some.

Construction

use mosaik::collections::{Once, StoreId, SyncConfig};

// Writer — can read and write
let once = Once::<String>::writer(&network, StoreId::from("config-seed"));

// Writer with custom sync config
let once = Once::<String>::writer_with_config(&network, store_id, config);

// Reader — read-only, deprioritized for leadership
let once = Once::<String>::reader(&network, store_id);

// Reader with custom sync config
let once = Once::<String>::reader_with_config(&network, store_id, config);

// Aliases: new() == writer(), new_with_config() == writer_with_config()
let once = Once::<String>::new(&network, store_id);

Read operations

Available on both writers and readers. Reads operate on the local committed state and never touch the network.

MethodTimeDescription
read() -> Option<T>O(1)Get the current value
get() -> Option<T>O(1)Alias for read()
is_empty() -> boolO(1)Whether the register has been set
is_none() -> boolO(1)Alias for is_empty()
is_some() -> boolO(1)Whether the register holds a value
version() -> VersionO(1)Current committed state version
when() -> &WhenO(1)Access the state observer
// Read the current value
if let Some(seed) = once.read() {
    println!("Config seed: {seed}");
}

// Check if a value has been written
if once.is_none() {
    println!("No seed set yet");
}

// Equivalent check
assert_eq!(once.is_empty(), once.is_none());
assert_eq!(once.is_some(), !once.is_empty());

Write operations

Only available on OnceWriter<T>.

MethodTimeDescription
write(T) -> Result<Version, Error<T>>O(1)Set the value (ignored if already set)
set(T) -> Result<Version, Error<T>>O(1)Alias for write()

The key behavior: only the first write takes effect. If a value has already been set, subsequent writes are silently ignored by the state machine. The returned Version still advances (the command is committed to the Raft log), but the stored value does not change.

// First write — this value is stored permanently
let v = once.write("genesis".into()).await?;
once.when().reaches(v).await;
assert_eq!(once.read(), Some("genesis".into()));

// Second write — silently ignored
let v2 = once.write("overwrite-attempt".into()).await?;
once.when().reaches(v2).await;
assert_eq!(once.read(), Some("genesis".into())); // still "genesis"

// Using the alias
once.set("another-attempt".into()).await?;
assert_eq!(once.read(), Some("genesis".into())); // unchanged

Error handling

Writes return the failed value on Error::Offline so you can retry:

match once.write(value).await {
    Ok(version) => {
        once.when().reaches(version).await;
    }
    Err(Error::Offline(value)) => {
        // Retry with the same value later
    }
    Err(Error::Encoding(value, err)) => {
        // Serialization failed
    }
    Err(Error::NetworkDown) => {
        // Permanent failure
    }
}

Status & observation

// Wait until online
once.when().online().await;

// Wait for a specific committed version
let v = once.write("init".into()).await?;
once.when().reaches(v).await;

// Wait for any update
once.when().updated().await;

// React to going offline
once.when().offline().await;

Group identity

The group key for a Once<T> is derived from:

UniqueId::from("mosaik_collections_once")
    .derive(store_id)
    .derive(type_name::<T>())

A Once<T> and a Register<T> with the same StoreId and type parameter will be in separate consensus groups because the prefix differs ("mosaik_collections_once" vs "mosaik_collections_register").

Once vs Register

BehaviorOnce<T>Register<T>
Write semanticsFirst write wins; subsequent ignoredEvery write replaces the stored value
clear()Not supportedSupported
compare_exchangeNot supportedSupported
Distributed analogtokio::sync::OnceCelltokio::sync::watch
State machine sig"mosaik_collections_once""mosaik_collections_register"

When to use Once

Once is ideal for:

  • One-time initialization — a genesis config, cluster seed, or bootstrap value that should only be set once
  • Distributed elections — the first node to write a value claims a slot
  • Immutable assignments — assigning a resource or identity permanently
  • Configuration anchors — values that must not change after initial deployment

If you need to update the value after setting it, use Register instead.