Skip to content

Store

What It Is

The store surface provides shared mutable state outside point-to-point DAG state mappings: StoreInterface, BaseStore, MemoryStore, TypedStore, snapshots, update semantics, remote-store shape, and StoreError taxonomy.

Use this page when nodes need a shared memory graph, cache, typed key space, remote replicated state, or checkpoint-restorable structure that multiple placements can read and write.

How It Works

Stores are passed into node constructors and survive scatter clone boundaries within a run. Checkpoints can snapshot named stores alongside parent state for deterministic resume.

Use state mapping for point-to-point field transfer. Use stores when multiple placements accumulate into the same structure or when state needs a named persistence boundary.

Diagrams, Examples, and Outputs

Stores are runtime objects, not DAG placements. These pages show when to choose stores and how checkpoint snapshots interact with them:

What It Lets You Do

The store reference lets applications share and checkpoint mutable data outside point-to-point DAG state mappings.

@studnicky/dagonizer/store

The store module provides the shared key-value store contract and its implementations. Stores are passed into node constructors and survive scatter clone boundaries within a run. Checkpoint integration snapshots named stores alongside parent state for deterministic resume.

Code Samples

The code below covers store contracts, snapshots, base implementation hooks, memory stores, typed stores, update semantics, remote-store behavior, and store errors.

Import

ts
import { 
BaseStore
,
MemoryStore
,
StoreError
} from '@studnicky/dagonizer/store';
import type { SnapshottableInterface, StoreInterface,
StoreSnapshotType
,
StoreSnapshotEntryType
} from '@studnicky/dagonizer/contracts';

Interface: SnapshottableInterface

@studnicky/dagonizer/contracts

The capability checkpointing depends on: a named container that serializes itself to a StoreSnapshotType and rehydrates from one. It declares only two methods.

ts
interface SnapshottableInterface {
  
snapshot
():
Promise
<
StoreSnapshotType
>;
restore
(
snapshot
:
StoreSnapshotType
):
Promise
<void>;
}
MethodReturnsDescription
snapshot()Promise<StoreSnapshotType>Capture the entire state as a typed envelope.
restore(snapshot)Promise<void>Repopulate from a snapshot. Implementations validate type and version before applying entries.

SnapshottableInterface is decoupled from the key-value surface on purpose. Checkpoint.capture(dag, result, { stores }) and Checkpoint.restoreStores(map) take Record<string, SnapshottableInterface>, so a non-KV backing (an RDF triple store, a vector index, an append-only projection) can ride along in a checkpoint without implementing get/set/has/delete/update. StoreInterface extends SnapshottableInterface, so every StoreInterface is also SnapshottableInterface. The StoreSnapshotType / StoreSnapshotEntryType envelopes live with this capability.


Interface: StoreInterface

@studnicky/dagonizer/contracts

Shared key-value store contract, extending SnapshottableInterface. Every method returns a Promise. There is no sync variant; always await store calls.

Values are typed per-call via the method's <T> parameter. There is no class-level value generic. A StoreInterface instance can hold heterogeneous values under different keys; type narrowing happens at the call site.

ts
interface SnapshottableInterface {
  
snapshot
():
Promise
<
StoreSnapshotType
>;
restore
(
snapshot
:
StoreSnapshotType
):
Promise
<void>;
} interface StoreInterface extends SnapshottableInterface {
get
(
key
: string):
Promise
<
JsonValueType
| null>;
set
(
key
: string,
value
:
JsonValueType
):
Promise
<void>;
has
(
key
: string):
Promise
<boolean>;
delete
(
key
: string):
Promise
<boolean>;
update
(
key
: string,
fn
: (
current
:
JsonValueType
| undefined) =>
JsonValueType
):
Promise
<
JsonValueType
>;
// snapshot() / restore() inherited from SnapshottableInterface.
connect
():
Promise
<void>;
disconnect
():
Promise
<void>;
}
MethodReturnsDescription
get(key)Promise<T | null>Return the value at key, or null when absent.
set(key, value)Promise<void>Write value at key. Last-write-wins.
has(key)Promise<boolean>Return true when the key exists.
delete(key)Promise<boolean>Remove the key. Returns true when the key existed.
update(key, fn)Promise<T>Atomic read-modify-write. fn receives the current value (or undefined when absent) and returns the new value. Implementations are responsible for atomicity.
snapshot() / restore(snapshot)inheritedFrom SnapshottableInterface: capture / repopulate the whole store.
connect?()Promise<void>Optional lifecycle hook for stores that hold a connection.
disconnect?()Promise<void>Optional lifecycle hook for stores that hold a connection.

Concurrency: update(key, fn) is atomic within a single store instance. Implementations are responsible for delivering this. See the update note on BaseStore for the requirement. set + get is not atomic.


Interface: StoreSnapshotType

@studnicky/dagonizer/contracts

Versioned snapshot envelope returned by SnapshottableInterface.snapshot() and consumed by SnapshottableInterface.restore().

ts
interface StoreSnapshotType {
  readonly 
version
: number;
readonly
type
: string;
readonly
entries
: readonly
StoreSnapshotEntryType
[];
}
FieldDescription
versionSnapshot schema version. Plugin authors increment this when the storage shape changes. BaseStore.restore rejects mismatches with StoreError(INCOMPATIBLE_SNAPSHOT).
typeStable identifier for the store implementation (e.g. 'memory-store'). Set via BaseStore.snapshotType.
entriesOrdered list of key-value pairs at capture time.

Interface: StoreSnapshotEntryType

@studnicky/dagonizer/contracts

A single entry in a StoreSnapshotType.

ts
interface StoreSnapshotEntryType {
  readonly 
key
: string;
readonly
value
:
JsonValueType
;
}

Keys in the snapshot carry the namespace prefix when a namespace is configured. Restore feeds entries directly back through performRestoreEntries; no prefix stripping is applied. Restore into a store with the same namespace used at capture time.


Class: BaseStore

@studnicky/dagonizer/store

Abstract base class every concrete store extends. Owns the snapshot envelope, the update default, optional namespace prefix, and lifecycle no-ops. Concrete stores implement the protected abstract hooks listed below.

ts
// BaseStore is an abstract class — extend it:
abstract class 
MyStore
extends
BaseStore
{
protected constructor(
options
?:
BaseStoreOptionsType
) { super(
options
); }
}

BaseStoreOptionsType

ts
interface BaseStoreOptionsType {
  readonly 
namespace
?: string;
}

namespace is an optional key prefix. When set, every key passed to public methods is prefixed with ${namespace}:${key} before reaching the perform* hooks. Two stores with different namespaces can share the same physical backing without collisions.

Public methods

All public methods delegate to the perform* hooks after qualifying the key.

MethodDescription
get(key)Delegates to performGet(qualifiedKey).
set(key, value)Delegates to performSet(qualifiedKey, value).
has(key)Delegates to performHas(qualifiedKey).
delete(key)Delegates to performDelete(qualifiedKey).
update(key, fn)Default: performGetfn(current)performSet. Two await points, not atomic on its own. Subclasses must override when backing supports a single-step RMW (in-memory direct access, SQL transactions, Redis WATCH/MULTI, etc.).
snapshot()Calls performSnapshotEntries(), then wraps in { version: snapshotVersion, type: snapshotType, entries }.
restore(snapshot)Validates snapshot.type and snapshot.version; throws StoreError(INCOMPATIBLE_SNAPSHOT) on mismatch. On match, calls performRestoreEntries(entries).
connect()No-op default. Override for connection lifecycle.
disconnect()No-op default. Override for connection lifecycle.

Protected abstract hooks

Plugin authors implement these six methods and two accessors. All keyed arguments receive the qualified key (namespace prefix already applied).

HookSignatureDescription
snapshotTypeget snapshotType(): stringStable identifier written into every snapshot envelope.
snapshotVersionget snapshotVersion(): numberSchema version. Increment when the shape changes.
performGet(qualifiedKey: string) → Promise<T | undefined>Read a single value.
performSet(qualifiedKey: string, value: T) → Promise<void>Write a single value.
performHas(qualifiedKey: string) → Promise<boolean>Check existence.
performDelete(qualifiedKey: string) → Promise<boolean>Remove key; return true when it existed.
performSnapshotEntries() → Promise<readonly StoreSnapshotEntryType[]>Return all entries for the snapshot.
performRestoreEntries(entries: readonly StoreSnapshotEntryType[]) → Promise<void>Repopulate from entries (clear first, then apply).

Protected utility

MemberDescription
qualifyKey(key)Apply the namespace prefix. Call this in update overrides that bypass the default RMW path.

Class: MemoryStore

@studnicky/dagonizer/store

Reference implementation of BaseStore backed by a Map.

ts
import { 
MemoryStore
} from '@studnicky/dagonizer/store';
const
store
= new
MemoryStore
();
await
store
.
set
('greeting', 'hello');
const
v
= await
store
.
get
('greeting'); // 'hello' (JsonValueType | null; narrow with typeof)

Constructor

ts
const 
opts
:
BaseStoreOptionsType
= {
namespace
: 'my-ns' };
const
store
= new
MemoryStore
(
opts
);

Accepts the same BaseStoreOptionsType as BaseStore (namespace prefix).

Snapshot type and version

FieldValue
snapshotType'memory-store'
snapshotVersion1

Atomic update

MemoryStore overrides update to access #data directly without any intermediate await. Because the body contains no yield point, no concurrent microtask can interleave between the read and the write; the read-modify-write is atomic within the store instance.

ts
// Concurrent updates produce no lost writes.
await 
Promise
.
all
([
store
.
update
('counter', (
n
) => (typeof
n
=== 'number' ?
n
: 0) + 1),
store
.
update
('counter', (
n
) => (typeof
n
=== 'number' ?
n
: 0) + 1),
]); const
raw
= await
store
.
get
('counter');
const
v
= typeof
raw
=== 'number' ?
raw
: 0; // → 2

Class: StoreError

@studnicky/dagonizer/store

Error class for store operations. Carries a structured classification object so callers discriminate by reason without instanceof chains.

ts
try {
  await 
store
.
restore
(
incompatibleSnapshot
);
} catch (
err
) {
if (
err
instanceof
StoreError
&&
err
.
classification
.
reason
=== 'INCOMPATIBLE_SNAPSHOT') {
// err.classification.expectedType, .actualType, .expectedVersion, .actualVersion } }

StoreErrorClassification

ts
type 
StoreErrorClassification
=
| { readonly
reason
: 'INCOMPATIBLE_SNAPSHOT';
readonly
expectedType
: string;
readonly
actualType
: string;
readonly
expectedVersion
: number;
readonly
actualVersion
: number;
} | { readonly
reason
: 'KEY_NOT_FOUND';
readonly
key
: string;
} | { readonly
reason
: 'BACKING_ERROR';
readonly
cause
: Error;
} | { readonly
reason
: 'LEASE_DENIED';
readonly
subject
: string;
readonly
holder
: string;
} | { readonly
reason
: 'LEASE_EXPIRED';
readonly
subject
: string;
readonly
token
: string;
} | { readonly
reason
: 'UNREACHABLE';
readonly
endpoint
: string;
readonly
cause
: Error;
};
ReasonWhenExtra fields
INCOMPATIBLE_SNAPSHOTrestore() called with wrong type or versionexpectedType, actualType, expectedVersion, actualVersion
KEY_NOT_FOUNDPlugin author throws when a required key is absentkey
BACKING_ERRORPlugin author wraps a backing-level failurecause
LEASE_DENIEDacquireLease finds an active holder and maxWaitMs expires before it releasessubject, holder
LEASE_EXPIREDA write or release is attempted with a token that has already expiredsubject, token
UNREACHABLETransport failure (endpoint does not respond within the health budget)endpoint, cause

BaseStore throws INCOMPATIBLE_SNAPSHOT automatically on type/version mismatch. KEY_NOT_FOUND and BACKING_ERROR are available for plugin authors to classify errors from their backing stores. LEASE_DENIED, LEASE_EXPIRED, and UNREACHABLE are for RemoteStoreInterface implementations.


Interface: RemoteStoreInterface

@studnicky/dagonizer/contracts

Extension of StoreInterface for distributed or network-backed implementations. Plugins that talk over HTTP, gRPC, or WebSocket, or that replicate state across processes, implement RemoteStoreInterface rather than StoreInterface directly. Single-process and single-node-durable stores implement StoreInterface directly.

ts
import type { RemoteStoreInterface, 
RemoteStoreEndpointType
,
RemoteStoreLeaseType
} from '@studnicky/dagonizer/contracts';
ts
interface RemoteStoreInterface extends StoreInterface {
  readonly 
endpoint
:
RemoteStoreEndpointType
;
acquireLease
(
subject
: string,
ttlMs
: number,
maxWaitMs
: number):
Promise
<
RemoteStoreLeaseType
>;
releaseLease
(
lease
:
RemoteStoreLeaseType
):
Promise
<void>;
health
(
timeoutMs
: number):
Promise
<boolean>;
}

The engine consumes a RemoteStoreInterface through the StoreInterface surface. The extra methods are observability and coordination primitives the dispatcher uses when distributed execution is wired in.

Interface: RemoteStoreEndpointType

ts
interface RemoteStoreEndpointType {
  readonly 
url
: string;
readonly
region
: string;
}
FieldDescription
urlStable identifier for the remote endpoint (URL, gRPC target, etc.).
regionRegion/zone hint for placement decisions. Default at construction: '' (no region constraint).

region is required. Implementations that have no region concept supply ''.

Interface: RemoteStoreLeaseType

ts
interface RemoteStoreLeaseType {
  readonly 
token
: string;
readonly
expiresAt
: number;
readonly
subject
: string;
}

Opaque lease token returned by acquireLease. Applications treat token as opaque; the store validates it on releaseLease and on writes when leasing is enforced.

FieldDescription
tokenOpaque string the store recognises on releaseLease and write checks.
expiresAtMonotonic ms timestamp the lease expires at (exclusive).
subjectScope of the lease (e.g. a key namespace or DAG run id).

Methods

MethodReturnsDescription
endpointRemoteStoreEndpointTypeEndpoint descriptor; surfaces in observability and placement decisions.
acquireLease(subject, ttlMs, maxWaitMs)Promise<RemoteStoreLeaseType>Acquire exclusive write authority for subject with a lifetime of ttlMs ms. Waits up to maxWaitMs for an active holder to release before throwing StoreError(LEASE_DENIED).
releaseLease(lease)Promise<void>Release a previously-acquired lease. Idempotent: releasing an already-expired lease is a no-op.
health(timeoutMs)Promise<boolean>Health probe. Returns true when the endpoint is reachable and the backing responds within timeoutMs. Implementations must not throw on transport failure: return false so the dispatcher can route around an unhealthy store.

Implementing RemoteStoreInterface

Extend BaseStore and implement the three additional methods plus the endpoint property:

ts
<<< @/../examples/dags/store-remote.ts#remote-store

Class: TypedStore<Schema>

@studnicky/dagonizer/store

Schema-narrowed wrapper over any StoreInterface. Constrains keys to the declared Schema and infers the value type from Schema[K]. Callers never specify <T> at the call site.

TypedStore does not implement the StoreInterface contract (its set signature is narrower). Use .inner to access the underlying StoreInterface when you need the wider, heterogeneous contract.

ts
<<< @/../examples/the-archivist/memory/TypedRunStore.ts#typed-store

Constructor

ts
interface MySchema { 
count
: number;
label
: string; }
const
CountSchema
= { '$id': 'urn:docs:MySchema/count', 'type': 'number' } as
const
;
const
LabelSchema
= { '$id': 'urn:docs:MySchema/label', 'type': 'string' } as
const
;
const
store
= new
TypedStore
<MySchema>(new
MemoryStore
(), {
count
:
Validator
.
compile
<MySchema['count']>(
CountSchema
),
label
:
Validator
.
compile
<MySchema['label']>(
LabelSchema
),
});

Schema must be a Record<string, JsonValueType>: every value type must be JSON-serializable.

Methods

MethodReturnsDescription
get(key)Promise<Schema[K] | null>Return the value at key, type inferred from Schema[K], or null when absent.
set(key, value)Promise<void>Write value at key. value must be Schema[K].
has(key)Promise<boolean>Return true when the key exists.
delete(key)Promise<boolean>Remove the key. Returns true when the key existed.
update(key, fn)Promise<Schema[K]>Atomic read-modify-write. fn receives Schema[K] | undefined, returns Schema[K].
snapshot()Promise<StoreSnapshotType>Pass-through to the underlying StoreInterface.
restore(snapshot)Promise<void>Pass-through to the underlying StoreInterface.
connect()Promise<void>Pass-through to the underlying StoreInterface.
disconnect()Promise<void>Pass-through to the underlying StoreInterface.
.innerStoreInterfaceThe underlying StoreInterface instance for un-narrowed operations.

All key parameters are constrained to keyof Schema & string. TypeScript rejects keys absent from the schema and values of the wrong type at compile time.


Details for Nerds

Stores are runtime dependencies, not graph topology. The DAG can stay portable while nodes share a store instance injected by the host.

Snapshot support is explicit. A store that implements SnapshottableInterface can participate in checkpoint capture and restore; a store that does not implement it remains runtime-only.

Watched over by the Order of Dagon.