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

Use the JavaScript SDK with the Engine

This guide explains how the Softadastra JavaScript SDK maps to the internal Softadastra Engine model.

The goal is to understand what happens behind JavaScript SDK calls such as Client, ClientOptions, put, get, syncStateInfo, tick, startTransport, startDiscovery, and refreshNodeInfo.

The core rule is:

txt
The JavaScript SDK follows the same local-first engine model with an async JavaScript API.

Most JavaScript applications should use the SDK first. The engine documentation explains the lower-level runtime internals.

What you will learn

You will learn how the JavaScript SDK maps to these Softadastra concepts:

txt
ClientOptions  -> runtime configuration
Client         -> SDK facade
put/get        -> local store
persistent     -> WAL-backed persistence
syncStateInfo  -> sync engine state
tick           -> sync scheduler
transport      -> peer delivery
discovery      -> peer finding
node info      -> metadata
Result         -> explicit error handling

The basic model is:

txt
JavaScript app

Softadastra JS SDK

Softadastra runtime model

Why this mapping matters

The JavaScript SDK gives a small public API for local-first applications.

Instead of thinking about every internal layer separately, an app can use:

js
const client = new Client(
  ClientOptions.local("node-a"),
);

Then:

js
await client.open();
await client.put("app/name", "Softadastra");
await client.syncStateInfo();
await client.tick();
await client.close();

The SDK keeps the API simple while still following the same Softadastra runtime model used by the C++ engine.

SDK versus Engine

The SDK is the developer-facing layer.

The engine is the lower-level runtime layer.

txt
SDK JS
  -> Client
  -> ClientOptions
  -> Value
  -> Peer
  -> NodeInfo
  -> Result
  -> SoftadastraError

Engine model
  -> core
  -> WAL
  -> store
  -> sync
  -> transport
  -> discovery
  -> metadata

Use the SDK when you are building a JavaScript or Node.js application.

Use the engine documentation when you want to understand the internal modules, C++ implementation, or low-level runtime behavior.

Main SDK entry point

Most JavaScript applications should import:

js
import {
  Client,
  ClientOptions,
} from "@softadastra/sdk";

Other public types can include:

  • Value
  • Peer
  • NodeInfo
  • Result
  • SoftadastraError
  • SyncResult
  • TickResult

The JavaScript SDK uses camelCase for public fields and methods.

Examples:

  • enableWal
  • walPath
  • autoFlush
  • syncStateInfo
  • startTransport
  • startDiscovery
  • refreshNodeInfo
  • outboxSize
  • queuedCount
  • failedCount
  • batchSize

ClientOptions maps to runtime configuration

ClientOptions defines how the local runtime should behave.

Example:

js
const options = ClientOptions.persistent(
  "node-a",
  "data/node-a.wal",
);

options.autoFlush = true;

options.enableTransport = true;
options.transportHost = "127.0.0.1";
options.transportPort = 4041;

options.enableDiscovery = false;

options.displayName = "Node A";
options.version = "0.1.0";

Conceptually, these options map to runtime configuration:

txt
node id          -> metadata, sync, transport, discovery
wal path         -> WAL-backed persistence
auto flush       -> persistence durability behavior
transport host   -> transport bind host
transport port   -> transport bind port
discovery config -> discovery runtime
display name     -> metadata
version          -> metadata

The SDK hides the internal wiring.

Client maps to the runtime facade

Client is the main SDK object.

js
const client = new Client(options);

Conceptually, Client coordinates the runtime features selected by the options.

txt
Client

local store
persistence, if enabled
sync
transport, if enabled
discovery, if enabled
metadata

The application does not need to manually compose those pieces.

open maps to runtime initialization

Before using the SDK, call:

js
const opened = await client.open();

if (opened.isErr()) {
  console.error(opened.error().message);
  process.exit(1);
}

Conceptually, open() can initialize:

txt
validate options

initialize metadata

open persistence, if enabled

recover store from persistent history, if enabled

initialize store

initialize sync state

prepare transport config, if enabled

prepare discovery config, if enabled

client ready

Opening the client should not require a peer.

Opening the client should not require discovery.

Opening the client should not require a server.

The local runtime should be useful first.

put maps to store, persistence, and sync

A normal write looks like this:

js
const result = await client.put(
  "app/name",
  "Softadastra",
);

if (result.isErr()) {
  console.error(result.error().message);
  await client.close();
  process.exit(1);
}

Conceptually:

txt
client.put()

validate key and value

create store operation

append to persistent log, if enabled

apply to local store

create sync operation, if sync tracking is enabled

return Result

The store makes the value readable locally.

Persistence makes the operation recoverable when enabled.

Sync tracks the operation for later propagation.

get maps to the local store

A read looks like this:

js
const result = await client.get("app/name");

if (result.isOk()) {
  console.log(result.value().toString());
}

Conceptually:

txt
client.get()

local store lookup

return Value or Error

get() is local.

It should not require a server, peer, transport, discovery, or cloud access.

If the key is missing, the result should be an explicit error.

js
const result = await client.get("missing/key");

if (result.isErr()) {
  console.log(result.error().codeString());
}

Expected output style:

txt
not_found

A missing key is a normal store result. It should not crash the app.

remove maps to a store operation

A remove operation looks like this:

js
const result = await client.remove("app/name");

if (result.isErr()) {
  console.error(result.error().message);
}

Conceptually:

txt
client.remove()

create delete operation

append to persistent log, if enabled

apply delete to store

track sync operation, if enabled

return Result

A remove is still local-first.

Remote peers can learn about the delete later through sync.

contains, size, and empty map to store state

These methods inspect current local state:

js
client.contains("app/name");
client.size();
client.empty();

They map to the local store.

txt
contains -> does this key exist locally?
size     -> how many local entries exist?
empty    -> is the local store empty?

They do not require transport or discovery.

Persistent options map to WAL-backed behavior

Use persistent mode when local data should survive restart:

js
const options = ClientOptions.persistent(
  "node-persistent",
  "data/node-persistent.wal",
);

options.autoFlush = true;

Conceptually:

txt
ClientOptions.persistent

enable persistence

set WAL path

open store with recovery

The persistent write path is:

txt
put

persistent log append

store apply

sync tracking

The recovery path is:

txt
client.open()

read persistent history

replay valid operations

restore local store

syncStateInfo maps to the sync engine

Use syncStateInfo() to inspect pending synchronization work:

js
const state = await client.syncStateInfo();

if (state.isOk()) {
  console.log(`outbox: ${state.value().outboxSize}`);
  console.log(`queued: ${state.value().queuedCount}`);
  console.log(`failed: ${state.value().failedCount}`);
}

Conceptually:

txt
client.syncStateInfo()

sync engine state

outbox count

queued count

in-flight count

acknowledged count

failed count

retry count

The sync state does not mean remote delivery has completed.

It shows what the local runtime knows about propagation work.

tick maps to the sync scheduler

Use tick() to move the sync pipeline forward once:

js
const tick = await client.tick();

if (tick.isOk()) {
  console.log(`retried: ${tick.value().retriedCount}`);
  console.log(`pruned: ${tick.value().prunedCount}`);
  console.log(`batch: ${tick.value().batchSize}`);
}

Conceptually:

txt
client.tick()

retry expired work

select queued operations

produce next batch

prepare delivery through transport, if enabled

return TickResult

A tick is explicit.

This makes synchronization easier to test, debug, and control.

tick with pruning

If supported:

js
const tick = await client.tick({
  prune: true,
});

Conceptually:

txt
tick

move sync pipeline

remove completed entries, if safe

Pruning should only remove completed sync work.

It should not remove local store values.

startTransport maps to transport

Transport is optional.

Enable it through options:

js
options.enableTransport = true;
options.transportHost = "127.0.0.1";
options.transportPort = 4041;

Start it explicitly:

js
const result = await client.startTransport();

if (result.isErr()) {
  console.error(result.error().message);
}

Conceptually:

txt
client.startTransport()

transport config

bind host and port

start transport backend

ready to connect peers

Transport is the delivery layer.

It does not decide what a sync operation means.

connect maps to peer transport

A peer describes another node:

js
const peer = new Peer(
  "node-b",
  "127.0.0.1",
  4042,
);

Connect:

js
const connected = await client.connect(peer);

if (connected.isErr()) {
  console.log(`connection failed: ${connected.error().message}`);
}

Conceptually:

txt
client.connect(peer)

transport connect

peer registry update

messages can be delivered, if connection succeeds

A connection failure should not invalidate local state.

txt
transport failure

sync work remains tracked

local value remains readable

startDiscovery maps to discovery

Discovery is optional.

Enable it through options:

js
options.enableDiscovery = true;
options.discoveryHost = "127.0.0.1";
options.discoveryPort = 5051;
options.discoveryBroadcastHost = "127.0.0.1";
options.discoveryBroadcastPort = 5052;

Start it explicitly:

js
const result = await client.startDiscovery();

if (result.isErr()) {
  console.error(result.error().message);
}

Conceptually:

txt
client.startDiscovery()

discovery config

listen for discovery messages

announce local node

track discovered peers

Discovery finds peers.

Transport connects peers.

Sync sends operations.

peers maps to discovery and peer registry

Use:

js
const peers = await client.peers();

if (peers.isOk()) {
  for (const peer of peers.value()) {
    console.log(`${peer.nodeId} ${peer.host}:${peer.port}`);
  }
}

Conceptually:

txt
client.peers()

known peer registry

discovered or configured peers

No peers is a valid state.

A node can still read and write local data when no peer is known.

refreshNodeInfo maps to metadata

Use:

js
const info = await client.refreshNodeInfo();

if (info.isOk()) {
  const node = info.value();

  console.log(node.nodeId);
  console.log(node.displayName);
  console.log(node.hostname);
  console.log(node.osName);
  console.log(node.version);
}

Conceptually:

txt
client.refreshNodeInfo()

metadata service

node id

display name

hostname

operating system

version

capabilities

uptime

Metadata describes the node.

It does not store application data.

Result maps to explicit error handling

Most SDK operations return explicit results.

js
const result = await client.get("app/name");

if (result.isErr()) {
  console.error(result.error().message);
  await client.close();
  process.exit(1);
}

console.log(result.value().toString());

Conceptually:

txt
operation

Result

ok(value) or err(error)

The SDK uses explicit errors because failure is normal in local-first systems.

Examples:

  • missing key
  • invalid WAL path
  • transport failed
  • peer unavailable
  • discovery disabled
  • sync failed
  • metadata unavailable

The rule is:

txt
Check the result before using the value.

Complete JavaScript SDK flow

This example uses several SDK features and shows how they map to the engine model.

js
import {
  Client,
  ClientOptions,
} from "@softadastra/sdk";

const options = ClientOptions.persistent(
  "node-a",
  "data/node-a.wal",
);

options.autoFlush = true;

options.enableTransport = true;
options.transportHost = "127.0.0.1";
options.transportPort = 4041;

options.enableDiscovery = false;

options.displayName = "Node A";
options.version = "0.1.0";

const client = new Client(options);

const opened = await client.open();

if (opened.isErr()) {
  console.error(`open failed: ${opened.error().message}`);
  process.exit(1);
}

const written = await client.put(
  "app/name",
  "Softadastra",
);

if (written.isErr()) {
  console.error(`write failed: ${written.error().message}`);
  await client.close();
  process.exit(1);
}

const value = await client.get("app/name");

if (value.isOk()) {
  console.log(`value: ${value.value().toString()}`);
}

const state = await client.syncStateInfo();

if (state.isOk()) {
  console.log(`outbox: ${state.value().outboxSize}`);
}

const tick = await client.tick();

if (tick.isOk()) {
  console.log(`batch: ${tick.value().batchSize}`);
}

const node = await client.refreshNodeInfo();

if (node.isOk()) {
  console.log(`node: ${node.value().nodeId}`);
}

await client.close();

Expected output style:

txt
value: Softadastra
outbox: 1
batch: 1
node: node-a

The exact sync numbers can differ depending on runtime configuration.

Internal flow behind the example

The previous example maps to this runtime flow:

txt
ClientOptions.persistent

WAL path configured

client.open()

metadata initialized

persistent history opened

store recovered

sync initialized

transport prepared

client.put()

store operation

persistent log append

store apply

sync operation created

client.syncStateInfo()

sync state read

client.tick()

sync scheduler moves pipeline

client.refreshNodeInfo()

metadata refreshed

client.close()

runtime cleanup

The SDK gives one clean async API over that flow.

When to use the JavaScript SDK

Use the JavaScript SDK when you want to:

  • build a Node.js application
  • build a local-first JavaScript tool
  • store local data
  • add offline-first behavior
  • persist local operations
  • inspect sync state
  • connect peers
  • use Softadastra without manual engine wiring

For JavaScript applications, the SDK is the right level.

When to read the engine docs

Read the engine docs when you want to:

  • understand the C++ runtime internals
  • extend a low-level module
  • debug WAL behavior
  • debug sync behavior
  • understand transport
  • understand discovery
  • understand metadata
  • compare SDK behavior with engine design

The engine section is lower-level and more technical.

SDK and CLI relationship

The SDK and CLI both sit above the same Softadastra runtime model.

txt
JS SDK

runtime model

CLI

runtime model

They expose similar concepts in different forms.

SDK:

js
await client.put("app/name", "Softadastra");
await client.syncStateInfo();
await client.tick();

CLI:

bash
softadastra store put app/name Softadastra
softadastra sync status
softadastra sync tick

Both follow the same model.

SDK and local-first behavior

The SDK should preserve Softadastra's local-first rules.

A local write should not require:

  • remote server
  • connected peer
  • transport
  • discovery
  • cloud access

A transport failure should not delete local state.

A discovery failure should not block store access.

A sync failure should not make a local value unreadable.

This is the practical meaning of local-first behavior in the JavaScript SDK.

Common mistakes

Forgetting ESM setup

The JavaScript SDK uses modern JavaScript modules.

Your package.json should include:

json
{
  "type": "module"
}

Then you can use:

js
import { Client, ClientOptions } from "@softadastra/sdk";

Forgetting to open the client

Wrong:

js
const client = new Client(options);
await client.put("app/name", "Softadastra");

Correct:

js
const client = new Client(options);

const opened = await client.open();

if (opened.isErr()) {
  console.error(opened.error().message);
  process.exit(1);
}

await client.put("app/name", "Softadastra");

Forgetting await

Wrong:

js
const result = client.put("app/name", "Softadastra");

Correct:

js
const result = await client.put("app/name", "Softadastra");

Most runtime operations are async.

Assuming put means remote delivery

put() writes locally.

It can create sync work, but it does not guarantee another node already received the operation.

Inspect sync:

js
const state = await client.syncStateInfo();

Run a tick:

js
const tick = await client.tick();

Assuming transport is required for local store

Transport is optional.

This should still work:

js
options.enableTransport = false;

await client.put("local/key", "value");
await client.get("local/key");

Sharing a WAL path between nodes

Each node should have its own WAL path.

Good:

txt
node-a -> data/node-a.wal
node-b -> data/node-b.wal

Bad:

txt
node-a -> data/shared.wal
node-b -> data/shared.wal

Debugging with CLI

Use the CLI to inspect the same runtime concepts:

bash
softadastra status
softadastra node info
softadastra store put app/name Softadastra
softadastra store get app/name
softadastra sync status
softadastra sync tick
softadastra peers

This helps confirm that JavaScript SDK behavior matches the runtime model.

Summary

The JavaScript SDK is a stable async API over the Softadastra runtime model.

The mapping is:

txt
ClientOptions  -> runtime configuration
Client         -> SDK facade
put/get        -> store
persistent     -> WAL-backed recovery
syncStateInfo  -> sync state
tick           -> sync scheduler
transport      -> peer delivery
discovery      -> peer finding
metadata       -> node identity
Result         -> explicit error handling

The SDK lets you build local-first JavaScript applications without manually wiring every engine module.

Next step

Continue with:

Production

Released under the Apache License 2.0.