Database¶
CommitDatabase is the commit-based persistence layer: an immutable,
append-only mutation DAG with full history. A state is never stored on its
own — it is rebuilt on demand from the trace of mutations leading to a commit.
Concept |
Role |
|---|---|
|
The store — the mutation DAG and its blobs |
|
Stages writes, then sealed into a commit |
|
A read-only snapshot reconstructed at a commit |
|
Navigates / edits a portion of a nested value |
This page is the Node twin of the Python Database and its
companion Commit overview, which explain the reconstruction
pipeline (the DAG, linearization, last-writer-wins) in depth. Here we cover the
Node lifecycle: create, define, mutate, commit, read — and the Path primitive
that the Structures and Enumerations page relies on for deep mutation.
const {
Definitions, NameSpace, ValueUUId, Type, TypeStructureDescriptor,
CommitDatabase, CommitMutableState, CommitStateBuilder, Path,
} = require('@digitalsubstrate/dsviper');
Creating a database¶
const db = CommitDatabase.createInMemory();
db.path(); // 'InMemory'
db.codecName(); // 'StreamBinary'
db.documentation(); // 'In Memory'
db.uuid().isValid(); // true
Setting up definitions¶
A database is empty until you extend it with a set of definitions — the schema
your documents conform to. extendDefinitions takes a const (frozen) view:
const defs = new Definitions();
const ns = new NameSpace(ValueUUId.create(), 'Test');
// Types
const t_A = defs.createConcept(ns, 'A');
const ds_S = new TypeStructureDescriptor('S');
ds_S.addField('f_i', Type.INT64);
ds_S.addField('f_s', Type.STRING);
const t_S = defs.createStructure(ns, ds_S);
// Attachments — a key type and a document type
const a_A = defs.createAttachment(ns, 'A_I', t_A, Type.INT64);
const a_S = defs.createAttachment(ns, 'A_S', Type.ANY_CONCEPT, t_S);
db.extendDefinitions(defs.const());
defs.const().isEqual(db.definitions()); // true
In your own code the definitions usually come from a parsed DSM model rather than being assembled by hand — see DSM Processing.
Writing — stage, then commit¶
All writes go through a CommitMutableState built from the database’s initial
state. You stage documents on its mutating handle, then seal the batch into a
single commit:
const state = new CommitMutableState(CommitStateBuilder.initialState(db));
const m = state.attachmentMutating();
const key = a_A.createKey().toParentKey();
m.set(a_A, key, 42);
m.set(a_S, key.toAnyConceptKey(), a_S.createStructure({ f_i: 42, f_s: 'a string' }));
const commitId = db.commitMutations('Initial import', state);
commitMutations returns the ValueCommitId of the sealed commit. The same id
is available afterwards as db.lastCommitId().
The staged values are readable back from the mutable state before the commit —
its attachmentMutating() exposes get, keys and has, and its
attachmentGetting() gives the same read-only view. keys(att) is an iterable
set; get(att, key) is a ValueOptional:
[...m.keys(a_A)][0].equals(key); // true
m.get(a_A, key).unwrap(); // 42n (INT64 unwraps to bigint)
Reading — reconstruct a snapshot¶
A read is a reconstruction, not a retrieval: CommitStateBuilder.state
replays the linearized mutation trace onto the initial state up to a commit and
hands you an immutable CommitState.
const s = CommitStateBuilder.state(db, db.lastCommitId());
const g = s.attachmentGetting();
[...g.keys(a_A)][0].equals(key); // true
g.get(a_A, key).unwrap(); // 42n
get returns a ValueOptional — .unwrap() peels it to the document. Value
semantics are Java-like: compare with .equals() (which accepts a native
argument), never with == between two Values (that is reference equality).
How a document unwraps depends on its type:
// INT64 -> bigint
g.get(a_A, key).unwrap(); // 42n
// structure -> ValueStructure; read fields via .at(), compare via .equals()
const doc = g.get(a_S, key.toAnyConceptKey()).unwrap();
doc.at('f_i'); // 42n
doc.equals(a_S.createStructure({ f_i: 42, f_s: 'a string' })); // true
A scalar stored under an ANY attachment comes back boxed in a ValueAny, so
it needs a second .unwrap() to reach the native; a container (e.g. a
ValueTuple) under ANY comes back directly.
Lifecycle and closed-database errors¶
Close a database when you are done with it:
db.close();
db.isClosed(); // true
A thrown Viper runtime error is an ordinary JS Error whose .name is
'ViperError'. Calling into a closed database throws it:
const isViperError = (e) => e.name === 'ViperError';
const closed = CommitDatabase.createInMemory();
closed.close();
assert.throws(() => closed.definitions(), isViperError);
assert.throws(() => closed.uuid(), isViperError);
assert.throws(() => closed.codecName(), isViperError);
assert.throws(() => closed.headCommitIds(), isViperError);
Deep mutation through a path¶
The mutating handle uses a path to update inside a staged document without
re-setting it whole. update rewrites a single location; collection fields have
their own operations — unionInSet / subtractInSet, unionInMap /
subtractInMap, and insertInXarray / updateInXarray / removeInXarray:
const state = new CommitMutableState(CommitStateBuilder.initialState(db));
const m = state.attachmentMutating();
const acKey = a_A.createKey().toAnyConceptKey();
m.set(a_S, acKey, a_S.createStructure());
m.update(a_S, acKey, new Path().field('f_i').const(), 42);
m.get(a_S, acKey).unwrap().at('f_i'); // 42n
See Structures and Enumerations for the full set of collection-field operations and the
value handles (ValueSet, ValueMap, ValueXArray) they take.