Vix.cpp docs moved to vixcpp.com
Skip to content

Persistent Store ​

The Softadastra C++ SDK can persist local data with a WAL-backed store. A persistent store keeps local writes across application restarts. This is the recommended mode for real applications.

Include ​

cpp
#include <softadastra/sdk.hpp>

Why persistent storage matters ​

Local-first applications must be able to accept writes even when the network is unavailable. But accepting local writes is not enough. The application also needs to keep those writes after:

txt
process restart
application crash
device reboot
temporary network failure

Persistent mode solves this by storing local data through a WAL-backed store.

Use ClientOptions::persistent() for real applications.

cpp
auto options = softadastra::sdk::ClientOptions::persistent(
    "my-app",
    "data/my-app.wal"
);

This enables:

txt
WAL-backed local storage
automatic WAL flushing
restart recovery
local-first writes
sync operation tracking

Durable alias ​

ClientOptions::durable() is an explicit alias for persistent().

cpp
auto options = softadastra::sdk::ClientOptions::durable(
    "my-app",
    "data/my-app.wal"
);

Use persistent() in most documentation and application code.

Use durable() when you want the name to emphasize durability.

Basic persistent example ​

cpp
#include <softadastra/sdk.hpp>

#include <iostream>

int main()
{
    using namespace softadastra::sdk;

    Client client{
        ClientOptions::persistent(
            "example-persistent-store",
            "data/examples/persistent-store.wal"
        )
    };

    const auto opened = client.open();

    if (opened.is_err())
    {
        std::cerr << "open failed: " << opened.error().code_string() << ": " << opened.error().message() << "\n";
        return 1;
    }

    const auto stored_name = client.put("profile/name", "Ada");

    if (stored_name.is_err())
    {
        std::cerr << "put failed: " << stored_name.error().code_string() << ": " << stored_name.error().message() << "\n";
        client.close();
        return 1;
    }

    const auto stored_language = client.put("profile/language", "C++");

    if (stored_language.is_err())
    {
        std::cerr << "put failed: " << stored_language.error().code_string() << ": "
                  << stored_language.error().message() << "\n";
        client.close();
        return 1;
    }

    const auto name = client.get("profile/name");

    if (name.is_err())
    {
        std::cerr << "get failed: " << name.error().code_string() << ": "
                  << name.error().message() << "\n";
        client.close();
        return 1;
    }

    const auto language = client.get("profile/language");

    if (language.is_err())
    {
        std::cerr << "get failed: " << language.error().code_string() << ": "
                  << language.error().message() << "\n";
        client.close();
        return 1;
    }

    std::cout << "profile/name: " << name.value().to_string() << "\n";
    std::cout << "profile/language: " << language.value().to_string() << "\n";
    std::cout << "entries: " << client.size() << "\n";
    client.close();

    return 0;
}

Expected output:

txt
profile/name     : Ada
profile/language : C++
entries          : 2

Create the data directory ​

Before running a persistent example, create the parent directory for the WAL file.

bash
mkdir -p data/examples

If the parent directory does not exist, opening or writing through the persistent store can fail with an I/O error.

WAL path ​

The second argument to persistent() is the WAL path.

cpp
ClientOptions::persistent(
    "node-1",
    "data/sdk-store.wal"
)

The default WAL path is:

txt
data/sdk-store.wal

So this:

cpp
auto options = ClientOptions::persistent("node-1");

is equivalent to:

cpp
auto options = ClientOptions::persistent(
    "node-1",
    "data/sdk-store.wal"
);

Persistent mode behavior ​

Persistent mode configures the SDK with:

txt
store WAL enabled
auto flush enabled
initial store capacity = 1024
sync batch size = 64
max sync retries = 5
require ack = true
auto queue = true
transport backend = AsyncTcp

This mode is designed for application correctness and restart recovery.

Local writes are durable ​

When you call:

cpp
client.put("profile/name", "Ada");

the SDK accepts the write locally and submits it through the synchronization pipeline.

In persistent mode, the local store is backed by a WAL file, so the data can be recovered when the application opens the same WAL path again.

Read after write ​

You can read the value immediately after writing it.

cpp
const auto saved = client.put("profile/name", "Ada");

if (saved.is_err())
{
    return 1;
}

const auto value = client.get("profile/name");

if (value.is_ok())
{
    std::cout << value.value().to_string() << "\n";
}

Expected output:

txt
Ada

Check stored entries ​

Use size() to inspect the number of entries in the local store.

cpp
std::cout << "entries: " << client.size() << "\n";

Use empty() to check whether the local store is empty.

cpp
if (client.empty())
{
    std::cout << "store is empty\n";
}

Remove persistent data ​

Use remove() to remove a value.

cpp
const auto removed = client.remove("profile/name");

if (removed.is_err())
{
    std::cerr << removed.error().code_string() << ": "
              << removed.error().message() << "\n";
    return 1;
}

After removal, the operation is also tracked by the synchronization pipeline.

Persistent store and sync state ​

Persistent writes are also visible through sync_state().

cpp
const auto state = client.sync_state();

if (state.is_ok())
{
    std::cout << "outbox: " << state.value().outbox_size() << "\n";
    std::cout << "queued: " << state.value().queued_count() << "\n";
}

This helps applications observe pending local work.

Persistent store vs memory-only store ​

ModeWALSurvives restartBest for
memory_only()nonoexamples and tests
local()nonoexamples and tests
persistent()yesyesreal applications
durable()yesyesreal applications
fast()yesyesbenchmarks and controlled environments

Persistent mode vs fast mode ​

persistent() enables automatic flushing after local writes.

cpp
auto options = ClientOptions::persistent(
    "node-1",
    "data/app.wal"
);

fast() keeps the WAL enabled but disables automatic flushing after every write.

cpp
auto options = ClientOptions::fast(
    "node-1",
    "data/app.wal"
);

Use persistent() when correctness and durability matter.

Use fast() only for benchmarks or controlled environments where throughput matters more than maximum durability.

Complete example with error handling ​

cpp
#include <softadastra/sdk.hpp>
#include <iostream>

namespace
{
    template <typename ResultType>
    bool failed(
        const ResultType &result,
        const char *operation)
    {
        if (result.is_ok())
        {
            return false;
        }

        const auto &error = result.error();

        std::cerr << operation << " failed: " << error.code_string() << ": " << error.message();
        if (error.has_context())
        {
            std::cerr << " (" << error.context() << ")";
        }

        std::cerr << "\n";
        return true;
    }
}

int main()
{
    using namespace softadastra::sdk;

    Client client{
        ClientOptions::persistent(
            "persistent-example",
            "data/persistent-example.wal"
        )
    };

    const auto opened = client.open();
    if (failed(opened, "open"))
    {
        return 1;
    }

    const auto saved = client.put(
        "settings/theme",
        "dark"
    );

    if (failed(saved, "put"))
    {
        client.close();
        return 1;
    }

    const auto value = client.get("settings/theme");
    if (failed(value, "get"))
    {
        client.close();
        return 1;
    }

    std::cout << "settings/theme: " << value.value().to_string() << "\n";
    std::cout << "entries: " << client.size() << "\n";
    client.close();

    return 0;
}

Common issues ​

The WAL directory does not exist ​

Create the parent directory before running the application.

bash
mkdir -p data

The WAL path is empty ​

This is invalid when WAL persistence is enabled.

cpp
auto options = ClientOptions::persistent("node-1", "");

if (!options.is_valid())
{
    // Invalid.
}

The client was not opened ​

Most operations require client.open() first.

txt
invalid_state: SDK client is not open

Fix:

cpp
const auto opened = client.open();

if (opened.is_err())
{
    return 1;
}

Summary ​

Use persistent storage when your application needs local data to survive restarts.

The recommended configuration is:

cpp
ClientOptions::persistent(
    "my-app",
    "data/my-app.wal"
)

Persistent mode gives the application:

txt
local-first writes
WAL-backed storage
automatic flushing
restart recovery
sync operation tracking

Next, continue with Restart Recovery.

Released under the Apache License 2.0.