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

Runtime Flow ​

Runtime flow explains what happens inside Softadastra Engine when a node starts, writes local data, persists operations, tracks sync work, communicates with peers, discovers peers, and exposes metadata.

The core rule is:

txt
Local work happens first.
Network work happens later.

Softadastra is designed so that local state can remain useful even when transport, discovery, or peers are unavailable.

Why runtime flow matters ​

Softadastra Engine is modular.

Each module has a clear responsibility:

txt
core        -> primitives and errors
wal         -> durable operation history
store       -> current local state
sync        -> propagation tracking
transport   -> peer communication
discovery   -> peer finding
metadata    -> node identity
cli         -> terminal interface

Runtime flow shows how these modules work together.

It answers:

  • what happens when the engine starts?
  • what happens when a value is written?
  • what happens when WAL is enabled?
  • what happens when sync runs?
  • what happens when transport fails?
  • what happens when discovery finds no peers?
  • what happens during recovery?

High-level flow ​

The full peer-aware runtime flow looks like this:

txt
Application / SDK / CLI
  ↓
Store operation
  ↓
WAL append, if enabled
  ↓
Store apply
  ↓
Sync tracking
  ↓
Sync tick
  ↓
Transport send, if available
  ↓
Remote node receives
  ↓
Remote store applies

Discovery and metadata support the flow:

txt
Discovery -> finds peers
Metadata  -> describes nodes
Transport -> connects peers
Sync      -> moves operations

Minimal local flow ​

The smallest runtime flow is local-only:

txt
Application
  ↓
Store
  ↓
Value stored locally

This flow does not require:

  • WAL
  • sync
  • transport
  • discovery
  • metadata
  • network
  • peer

It is useful for tests, demos, and temporary local state.

Persistent local flow ​

When WAL is enabled, the local flow becomes:

txt
Application
  ↓
Operation created
  ↓
WAL append
  ↓
WAL flush, if configured
  ↓
Store apply
  ↓
Value stored locally

The key difference is durability.

The operation is recorded so it can be replayed later.

txt
WAL   -> operation history
Store -> current local state

Sync-aware local flow ​

When sync is enabled, a write also creates propagation work:

txt
Application
  ↓
Store operation
  ↓
WAL append, if enabled
  ↓
Store apply
  ↓
Sync operation created
  ↓
Outbox entry created
  ↓
Operation queued

The local value is available immediately.

Sync work can move later.

txt
local write accepted
  does not mean
remote peer already has it

Peer-aware flow ​

When transport is enabled and a peer is available:

txt
Application
  ↓
Local write
  ↓
WAL append
  ↓
Store apply
  ↓
Sync outbox
  ↓
Sync tick creates batch
  ↓
Transport encodes message
  ↓
Transport sends to peer
  ↓
Remote transport receives
  ↓
Remote sync receives operation
  ↓
Remote store applies operation

This is the complete local-first peer synchronization path.

Engine startup flow ​

When a Softadastra runtime starts, the flow is:

txt
load configuration
  ↓
initialize core primitives
  ↓
open WAL, if enabled
  ↓
recover store from WAL, if enabled
  ↓
initialize sync context
  ↓
initialize transport, if configured
  ↓
initialize discovery, if configured
  ↓
initialize metadata
  ↓
runtime ready

The runtime should not require transport or discovery to become locally usable.

A local-first runtime can be ready even if no peer exists.

Configuration flow ​

Configuration defines how the runtime starts.

Important values include:

  • node id
  • WAL enabled
  • WAL path
  • auto flush
  • transport enabled
  • transport host
  • transport port
  • discovery enabled
  • discovery host
  • discovery port
  • discovery target
  • display name
  • version
  • retry policy
  • ACK timeout

The runtime reads these options before composing modules.

Example conceptual flow:

txt
ClientOptions
  ↓
StoreConfig
  ↓
SyncConfig
  ↓
TransportConfig
  ↓
DiscoveryConfig
  ↓
MetadataOptions

The SDK hides most of this wiring from application code.

The engine keeps it explicit.

Node identity flow ​

Every peer-aware runtime needs a node identity.

The node id flows into several modules:

txt
node id
  ↓
metadata
  ↓
sync operation origin
  ↓
transport messages
  ↓
discovery announcements
  ↓
CLI status

The node id should be stable.

Example:

txt
node-a

A sync operation from node-a can then be recognized as originating from that node.

Metadata flow ​

Metadata describes the local node.

Startup can create metadata from:

  • node id
  • display name
  • hostname
  • operating system
  • version
  • capabilities
  • start time

Flow:

txt
MetadataOptions
  ↓
NodeMetadata
  ↓
MetadataService
  ↓
local metadata snapshot
  ↓
CLI / SDK / diagnostics

Metadata does not write application state.

It only describes the runtime.

Local write flow ​

A local write starts with an operation.

Example:

txt
put user:1 = Gaspard

Flow:

txt
operation created
  ↓
validate key and value
  ↓
append to WAL, if enabled
  ↓
apply to store
  ↓
create sync operation
  ↓
queue for sync
  ↓
return result

The local write should return an explicit success or error.

It should not hide WAL, store, or sync failures.

Write without WAL ​

If WAL is disabled:

txt
operation created
  ↓
validate key and value
  ↓
apply to memory store
  ↓
create sync operation, if sync enabled
  ↓
return result

This is faster and simpler, but not durable.

If the process restarts, memory-only state can be lost.

Write with WAL ​

If WAL is enabled:

txt
operation created
  ↓
validate key and value
  ↓
append operation to WAL
  ↓
flush WAL, if auto flush enabled
  ↓
apply operation to store
  ↓
create sync operation
  ↓
return result

The key rule is:

txt
do not report durable success if WAL append failed

WAL failure means the operation is not safely recorded.

WAL append flow ​

A WAL append follows this model:

txt
WalRecord created
  ↓
sequence assigned
  ↓
timestamp attached
  ↓
record encoded
  ↓
record written to file
  ↓
flush, if requested
  ↓
sequence returned

The WAL record can contain:

  • sequence
  • record type
  • status
  • timestamp
  • payload

The WAL is append-oriented.

It should not require rewriting the full state on every operation.

Store apply flow ​

After a valid operation is accepted, the store applies it.

For a put operation:

txt
Operation::Put
  ↓
key validated
  ↓
entry created or updated
  ↓
version incremented
  ↓
value stored

For a delete operation:

txt
Operation::Delete
  ↓
key validated
  ↓
entry removed
  ↓
delete result returned

The store exposes current state.

It does not expose the full historical sequence.

Read flow ​

A read is simple and local.

txt
get key
  ↓
store lookup
  ↓
entry found?
  ↓
return value or not_found error

A read should not require:

  • network
  • transport
  • discovery
  • peer
  • sync tick
  • remote server

Local reads must remain local.

Remove flow ​

A remove operation follows the same local-first model.

txt
remove key
  ↓
validate key
  ↓
append remove operation to WAL, if enabled
  ↓
apply remove to store
  ↓
track remove for sync
  ↓
return result

After removal:

txt
contains(key) -> false
get(key)      -> not_found

If the remove was recorded in WAL, replay should preserve the final removed state.

Recovery flow ​

Recovery happens when the runtime starts with WAL enabled.

txt
open WAL
  ↓
read records in sequence
  ↓
decode each record
  ↓
validate record
  ↓
apply operation to store
  ↓
rebuild local state
  ↓
runtime ready

Recovery should apply records in deterministic order.

The expected behavior is:

txt
same WAL
  ↓
same replay order
  ↓
same store state

Recovery example ​

Given this WAL sequence:

txt
1. put user:1 = Gaspard
2. put user:2 = Softadastra
3. put user:1 = Gaspard Kirira
4. delete user:2

After replay:

txt
user:1 -> Gaspard Kirira
user:2 -> not_found

The store exposes the final state.

The WAL keeps the history.

Corrupted WAL flow ​

If a WAL contains corrupted trailing data, the safer model is:

txt
read valid records
  ↓
stop at invalid record
  ↓
do not apply invalid bytes
  ↓
return clear recovery error or partial recovery result

The engine should never silently apply corrupted data.

Possible behavior:

  • recover valid prefix
  • report corruption
  • require repair or manual action

The exact policy can evolve, but invalid records must be visible.

Sync submit flow ​

After a local store operation, sync can create a sync operation.

txt
store operation
  ↓
SyncOperation created
  ↓
sync id assigned
  ↓
origin node id attached
  ↓
version attached
  ↓
operation inserted into outbox
  ↓
operation queued

The sync operation is the propagation unit.

It is separate from the raw store operation.

Outbox flow ​

The outbox keeps operations that need synchronization.

txt
new sync operation
  ↓
outbox entry created
  ↓
status = queued
  ↓
ready for tick

The outbox can contain:

  • queued operations
  • in-flight operations
  • acknowledged operations
  • failed operations

The outbox makes sync state observable.

Sync tick flow ​

A sync tick moves sync forward once.

txt
tick
  ↓
retry expired operations
  ↓
prune acknowledged operations, if requested
  ↓
select queued operations
  ↓
create batch
  ↓
return TickResult

A tick can report:

  • retried count
  • pruned count
  • batch size

Manual ticks are useful because they are deterministic and testable.

Sync batch flow ​

A sync batch is a group of operations ready to send.

txt
queued operations
  ↓
selected by scheduler
  ↓
wrapped in envelopes
  ↓
returned as batch
  ↓
transport can send

A batch can exist even if transport is disabled.

Transport availability controls delivery, not batch creation.

ACK flow ​

If acknowledgements are required:

txt
operation sent
  ↓
operation marked in-flight
  ↓
AckTracker tracks sync id
  ↓
remote peer replies ACK
  ↓
operation marked acknowledged
  ↓
operation can be pruned later

If ACK does not arrive:

txt
ACK timeout
  ↓
operation becomes retry candidate
  ↓
retry policy decides next action

Retry flow ​

Retry handles operations that were sent but not confirmed.

txt
operation in-flight
  ↓
timeout reached
  ↓
retry count checked
  ↓
operation re-queued
  ↓
next tick can send again

If retry limit is exceeded:

txt
max retries reached
  ↓
operation marked failed
  ↓
failed count increases

Failed sync work does not automatically mean local data is lost.

It means propagation failed.

Conflict flow ​

When a remote operation targets a key that also changed locally, conflict policy decides what happens.

Conceptual flow:

txt
remote operation received
  ↓
local entry exists?
  ↓
versions or timestamps compared
  ↓
conflict policy applied
  ↓
apply remote or keep local
  ↓
return resolution

Possible policies can include:

  • last write wins
  • keep local
  • apply remote
  • custom policy later

The conflict resolver should make the decision explicit.

Remote receive flow ​

When a remote node sends an operation:

txt
transport receives message
  ↓
message decoded
  ↓
dispatcher identifies sync message
  ↓
sync operation decoded
  ↓
SyncEngine receives remote operation
  ↓
conflict policy checked
  ↓
store operation applied if accepted
  ↓
result returned

The remote apply path should preserve local correctness.

Invalid remote operations should be rejected clearly.

Transport startup flow ​

Transport startup happens after the client or app has opened the runtime.

txt
TransportConfig
  ↓
backend created
  ↓
bind host and port
  ↓
start listener
  ↓
transport running

Transport should return explicit errors for:

  • invalid host
  • invalid port
  • port already in use
  • permission denied
  • socket failure

A transport start failure should not destroy local store state.

Transport connect flow ​

Connecting to a peer follows this path:

txt
PeerInfo / Peer
  ↓
validate host and port
  ↓
open connection
  ↓
handshake or hello message
  ↓
mark peer connected

If the peer is unavailable:

txt
connection refused
  ↓
return transport error
  ↓
local store remains usable

Transport send flow ​

Sending a sync batch follows this path:

txt
sync batch
  ↓
encode sync operation
  ↓
wrap in transport message
  ↓
encode frame
  ↓
send through backend
  ↓
wait for response or ACK, if required

Transport should not decide whether the operation is valid application data.

Sync and store own that meaning.

Transport receive flow ​

Receiving a message follows this path:

txt
network frame received
  ↓
decode frame
  ↓
decode transport message
  ↓
dispatch by message type
  ↓
sync message goes to SyncEngine
  ↓
ping message returns pong
  ↓
hello message updates peer state

The dispatcher connects transport messages to the correct engine behavior.

Discovery startup flow ​

Discovery startup prepares peer discovery.

txt
DiscoveryConfig
  ↓
UDP backend created
  ↓
bind discovery host and port
  ↓
discovery server starts
  ↓
discovery running

Discovery should return explicit errors for:

  • port already in use
  • invalid host
  • socket failure
  • permission denied

Discovery failure should not prevent local store operations.

Discovery announce flow ​

A node can announce itself:

txt
local node id
  ↓
announcement created
  ↓
payload encoded
  ↓
datagram created
  ↓
UDP message sent to discovery target

Announcement data can include:

  • node id
  • host
  • port
  • capabilities later

Discovery announces existence.

Transport handles connection.

Discovery listen flow ​

A node listening for announcements follows this flow:

txt
UDP datagram received
  ↓
decode discovery message
  ↓
extract announcement
  ↓
upsert peer in discovery registry
  ↓
mark peer available

This adds peers to the list returned by discovery.

It does not automatically connect to them.

Discovery probe flow ​

A probe asks whether peers are available.

txt
probe message sent
  ↓
remote node receives probe
  ↓
remote node replies or announces
  ↓
registry updates peer state

This is useful when the local node wants to refresh peer availability.

Discovery registry flow ​

The discovery registry stores known peers.

txt
announcement received
  ↓
peer inserted or updated
  ↓
last seen timestamp updated
  ↓
peer marked available

Over time:

txt
peer not seen
  ↓
peer marked stale
  ↓
peer marked expired
  ↓
expired peer pruned

This prevents old peers from staying forever.

Metadata startup flow ​

Metadata starts from node options.

txt
node id
display name
version
hostname
platform info
capabilities
  ↓
NodeMetadata created
  ↓
runtime fields refreshed
  ↓
metadata exposed to SDK / CLI

Metadata can be refreshed later to update fields like uptime.

Metadata refresh flow ​

Refreshing metadata follows this model:

txt
read current hostname
  ↓
read platform info
  ↓
calculate uptime
  ↓
update capabilities
  ↓
return NodeMetadata / NodeInfo

Metadata is diagnostic.

It does not modify application store data.

CLI command flow ​

A CLI command goes through the CLI engine.

txt
terminal input
  ↓
Tokenizer
  ↓
CommandLine
  ↓
ArgParser
  ↓
ParsedCommand
  ↓
CommandRegistry
  ↓
ICommandHandler
  ↓
engine module call
  ↓
formatted output

Example:

txt
store put user:1 Gaspard
  ↓
parse command
  ↓
call store command handler
  ↓
write local value
  ↓
print result

Interactive CLI flow ​

Interactive mode keeps the CLI running.

txt
start CLI
  ↓
show banner
  ↓
read line
  ↓
parse command
  ↓
execute handler
  ↓
print result
  ↓
read next line

The loop stops on commands such as:

  • exit
  • quit

Single-command CLI flow ​

Single-command mode runs one command and exits.

txt
start CLI
  ↓
parse command
  ↓
execute command
  ↓
print output
  ↓
return exit code

This is useful for scripts and automation.

SDK C++ flow ​

The C++ SDK wraps engine composition into a simpler client API.

txt
ClientOptions
  ↓
Client
  ↓
open
  ↓
put / get / remove
  ↓
sync_state / tick
  ↓
start_transport / start_discovery
  ↓
close

The SDK hides manual engine wiring.

Application code uses Client.

SDK JS flow ​

The JavaScript SDK mirrors the public C++ SDK shape.

txt
ClientOptions
  ↓
Client
  ↓
open
  ↓
put / get / remove
  ↓
syncStateInfo / tick
  ↓
startTransport / startDiscovery
  ↓
close

The names follow JavaScript conventions, but the model remains the same.

Full local-first flow ​

A complete local-first write with persistence and sync looks like this:

txt
client.put("profile/name", "Ada")
  ↓
validate key and value
  ↓
create store operation
  ↓
append to WAL
  ↓
flush WAL
  ↓
apply to store
  ↓
create sync operation
  ↓
insert into outbox
  ↓
queue operation
  ↓
return success

Then:

txt
client.get("profile/name")
  ↓
read local store
  ↓
return "Ada"

No network was required.

Full peer-sync flow ​

A complete peer-sync flow looks like this:

txt
node-a writes locally
  ↓
node-a WAL append
  ↓
node-a store apply
  ↓
node-a sync outbox
  ↓
node-a tick creates batch
  ↓
node-a transport sends batch to node-b
  ↓
node-b transport receives batch
  ↓
node-b dispatcher decodes batch
  ↓
node-b sync receives remote operation
  ↓
node-b conflict resolver checks operation
  ↓
node-b store applies operation
  ↓
node-b may ACK
  ↓
node-a marks operation acknowledged

This is the full architecture working together.

Flow when transport fails ​

If transport fails:

txt
local write
  ↓
WAL append succeeds
  ↓
store apply succeeds
  ↓
sync operation queued
  ↓
transport connection fails
  ↓
operation remains pending
  ↓
retry later

The correct behavior is:

  • local value remains available
  • sync is delayed
  • error is visible

Transport failure should not delete local state.

Flow when discovery finds no peers ​

If discovery finds no peers:

txt
discovery starts
  ↓
no announcement received
  ↓
peer list empty
  ↓
local writes still work
  ↓
sync work remains pending

No peers is not a local store failure.

It only means the node does not currently know a remote peer.

Flow when WAL fails ​

If WAL append fails:

txt
local write requested
  ↓
WAL append fails
  ↓
operation is not durably recorded
  ↓
return error

The engine should not pretend the operation was safely accepted.

This is different from transport failure.

txt
WAL failure       -> local durability problem
transport failure -> remote delivery problem

Flow when key is missing ​

A missing key is a normal read result.

txt
get missing/key
  ↓
store lookup
  ↓
entry not found
  ↓
return not_found error

This should not crash.

It should be explicit.

Flow when remote operation conflicts ​

A conflict flow looks like this:

txt
remote operation received
  ↓
local key exists with different version
  ↓
conflict resolver called
  ↓
policy decides result
  ↓
apply remote or keep local
  ↓
return conflict resolution

The important point is:

txt
conflicts should be visible and deterministic

Runtime state visibility ​

The runtime should expose useful state.

Examples:

  • store size
  • WAL path
  • last WAL sequence
  • outbox size
  • queued count
  • in-flight count
  • acknowledged count
  • failed count
  • transport running
  • discovery running
  • known peers
  • node metadata

This visibility is important for debugging.

Runtime flow by command ​

status ​

Conceptual flow:

txt
status command
  ↓
read runtime state
  ↓
read store state
  ↓
read sync state
  ↓
read transport state
  ↓
read discovery state
  ↓
print summary

node info ​

Conceptual flow:

txt
node info command
  ↓
refresh metadata
  ↓
print node id
  ↓
print hostname
  ↓
print OS
  ↓
print version
  ↓
print capabilities

store put ​

Conceptual flow:

txt
store put key value
  ↓
create store operation
  ↓
WAL append, if enabled
  ↓
store apply
  ↓
sync tracking
  ↓
print result

store get ​

Conceptual flow:

txt
store get key
  ↓
store lookup
  ↓
print value or not_found

sync tick ​

Conceptual flow:

txt
sync tick
  ↓
retry expired work
  ↓
prune completed work, if requested
  ↓
select queued operations
  ↓
print tick result

peers ​

Conceptual flow:

txt
peers command
  ↓
read discovery registry
  ↓
read transport peer registry
  ↓
print known peers

Runtime flow by module ​

ModuleMain runtime flow
coreCreate safe primitives and structured errors
walAppend, read, stream, replay records
storeApply operations and expose current state
syncTrack operations and produce batches
transportConnect peers and move messages
discoveryAnnounce, listen, probe, track peers
metadataBuild and refresh node identity
cliParse commands and call modules

Runtime flow rules ​

Softadastra runtime flow should follow these rules:

  1. Local writes must not require the network.
  2. WAL-backed writes must not ignore WAL failures.
  3. Store must expose current state.
  4. WAL must preserve operation history.
  5. Sync must be observable.
  6. Transport failure must not delete local state.
  7. Discovery failure must not block local writes.
  8. Metadata must describe nodes, not application data.
  9. CLI must expose state clearly.
  10. Errors must be explicit.

Common wrong flows ​

Avoid this:

txt
local write
  ↓
wait for remote server
  ↓
only then update local store

This is not local-first.

Avoid this:

txt
WAL append failed
  ↓
store still reports durable success

This breaks durability.

Avoid this:

txt
transport connection failed
  ↓
delete local operation

This loses local work.

Avoid this:

txt
discovery returned no peers
  ↓
disable local store

This breaks offline-first behavior.

Use this mental model:

txt
Write locally.
Persist locally.
Track sync.
Send when possible.
Retry when needed.
Converge later.

Expanded:

txt
Write locally
  -> store accepts state

Persist locally
  -> WAL records history

Track sync
  -> outbox remembers propagation work

Send when possible
  -> transport uses available peers

Retry when needed
  -> sync handles failure

Converge later
  -> remote nodes eventually receive operations

Summary ​

Runtime flow explains how Softadastra Engine behaves during real work.

The key separation is:

txt
WAL records operation history.
Store exposes current local state.
Sync tracks propagation work.
Transport sends messages.
Discovery finds peers.
Metadata describes nodes.
CLI exposes the runtime.

The most important rule is:

txt
Local work must remain valid even when the network is unavailable.

Next step ​

Continue with modules:

Go to Modules

Released under the Apache License 2.0.