Node¶
You defined a data model in DSM and ran Kibo over it. What you got back — and what
you work against every day — is a typed TypeScript package named after your model.
You almost never call the @digitalsubstrate/dsviper binding directly: you import a
couple of its modules and let them delegate to the runtime for you.
This page is that daily surface, end to end. For why the generated code exists (the adapter/Dual Reality pattern), see kibo-template-viper; for the raw runtime this layer sits on, see the dsviper for Node chapter — starting at Types and Values.
Anatomy of a generated package¶
A generated package — say model/ — compiles from a handful of TypeScript modules
under src/:
model/
├── src/
│ ├── data.ts ← the types: one class per concept, struct, enum, container
│ ├── attachments.ts ← the verbs on a commit state (one object per attachment)
│ ├── database_attachments.ts ← the same verbs for a standard (non-commit) Database
│ ├── definitions.ts ← the runtime type catalog (plumbing)
│ ├── value_type.ts ← Viper type handles used by data / attachments (plumbing)
│ ├── path.ts ← field paths used by the fine-grained setters (plumbing)
│ ├── resources.ts ← the packed definitions blob (plumbing)
│ └── index.ts ← the barrel: re-exports everything
├── package.json
└── tsconfig.json
This guide uses the versioned commit database throughout;
database_attachments.ts is the twin of attachments.ts for the plain,
non-versioned Database store — the same verb objects, called directly on a
Database instead of a commit state (see Database).
Every file carries this header — it is generated, and regenerated on every model change:
// Generated by kibo from <namespace>. Do not edit by hand.
Treat the package as a build artifact: never hand-edit it, never commit a local tweak — the next generation overwrites you. If something in the generated code is wrong, the fix belongs in the DSM model or the template, not the output.
The two things you import¶
index.ts re-exports the types at the top level and the verb modules as
namespaces, so one import line gets you both:
import {
Tuto_UserKey, Tuto_Login, Set_Tuto_UserKey, // the types
attachments, // the verbs (a namespace)
} from "model";
import { CommitDatabase, CommitStateBuilder, CommitMutableState }
from "@digitalsubstrate/dsviper";
A first program touches nothing but those, plus a CommitDatabase:
const db = CommitDatabase.open("model.cdb");
// a typed key + a typed struct — from `data`, never a raw dsviper.Value
const user = Tuto_UserKey.create();
const login = new Tuto_Login();
login.nickname = "alice";
// a mutation verb — the attachment's object, applied to the mutating state
const state = new CommitMutableState(CommitStateBuilder.initialState(db));
attachments.user_Login.set(state.attachmentMutating(), user, login);
db.commitMutations("Add user", state);
That is the whole shape of daily work: types are classes you new, changes go
through an attachment’s verb object, the commit loop lives on CommitDatabase.
The rest of this page is those three ideas in detail.
Where each name comes from¶
The generated names are mechanical — which is what makes a stack trace legible (see Reading a repro):
You see |
It came from |
DSM |
|---|---|---|
|
a concept |
|
|
a struct and its field |
|
|
an enum case |
|
|
an attachment |
|
Types are prefixed <Namespace>_; an attachment’s object is
<key-concept>_<attachment> (lower-first, so user_Login) — so a symbol in an
error points straight back to a line of DSM.
The proxy posture¶
The generated classes — Tuto_UserKey, Tuto_Login, Set_Tuto_UserKey — are
proxies. It is tempting to treat them as ordinary objects and drop them into
ordinary JS collections. Don’t. Understanding why keeps the runtime boundary
clean.
One value, two realities¶
Viper is a strongly-typed data model. The type catalog, built from your DSM at runtime, is the single source of truth — a value either conforms to its type or it is rejected, at the moment it is built. That safety lives in the runtime.
A proxy is how that runtime value is surfaced to TypeScript — the same value seen two ways (the Dual Reality):
Runtime Reality — the actual typed value the engine manipulates and persists.
Developer Reality — the proxy your editor autocompletes and
tsctype-checks.
Unlike a dynamically-typed binding, where the proxy’s job is to add enforcement
the language lacks, here tsc already checks the proxy surface at compile time. The
proxy’s job is therefore twofold: give the compiler a typed, idiomatic surface, and
carry the underlying runtime value — with its reference semantics, identity, and
serialization — so it can cross into the database.
What vprValue is¶
Every proxy holds exactly one runtime value in a vprValue field:
const user = Tuto_UserKey.create();
user.vprValue; // the underlying dsviper.ValueKey — the runtime reality
vprValue is the seam between the two realities. The generated code reaches
through it to delegate every operation to the runtime — that is its job. It is an
implementation detail, not an interchange format for your code. Writing
.vprValue in your own logic is a signal that you dropped the proxy posture
upstream and are patching the boundary by hand.
Native collection vs. proxy collection¶
They look interchangeable and are not — but in TypeScript the difference bites at compile time, not at run time:
// A native JS Set, typed by the compiler — but it is a JS collection of proxies,
// not a runtime set<User> the engine can store in an attachment:
const raw = new Set<Tuto_UserKey>();
raw.add(user);
// The proxy collection IS the runtime value:
const strict = new Set_Tuto_UserKey();
strict.add(user);
// strict.add("nope"); // ✗ compile error — add(value: Tuto_UserKey)
Because the proxy surface is typed, tsc rejects a wrong element before you run —
there is no deferred, dynamically-typed bug to defend against here. The reason to
reach for Set_Tuto_UserKey over a native Set<Tuto_UserKey> is not enforcement,
it is that the former is a runtime set<User> you can hand straight to an
attachment; the latter is a JS collection you would have to convert at the boundary.
The rule: produce the runtime value at creation¶
Don’t build a native collection of proxies and convert it at the edge; produce the proxy collection where the value is created:
function generate(): Set_Tuto_UserKey {
const result = new Set_Tuto_UserKey();
result.add(Tuto_UserKey.create());
result.add(Tuto_UserKey.create());
return result; // ready for an attachment — no conversion
}
Stay in the proxy posture: the generated SDK expresses the runtime’s types and carries its values. A native JS structure of proxies gives you neither the runtime value nor a shorter path — only a conversion to write at the boundary.
Living in the proxies¶
Everything in data follows a few shapes. Once you know them, a new model reads
itself.
Keys — concepts and clubs¶
A concept or club becomes a …Key class. A key is an identity, not data:
const user = Tuto_UserKey.create(); // a fresh identity
user.instanceId(); // its UUID
user.description(); // "<uuid>:Tuto::UserKey"
user.equals(other); // identity comparison, via the runtime
Use .equals(), not ===: two proxies wrapping the same runtime value are equal by
value, not by reference.
Structs — fields as accessors¶
A struct becomes a class you new, with a typed getter/setter per field:
const login = new Tuto_Login();
login.nickname = "alice";
login.password = "s3cret";
// login.nickname = 42; // ✗ compile error — nickname is a string
login.copy(); // a deep, independent copy
Enums — cases as static members¶
const account = new Tuto_Account();
account.state = Tuto_Status.ACTIVE;
account.state.name(); // "active"
Tuto_Status.fromStr("pending"); // parse from a name
Containers — idiomatic, but still runtime values¶
vector / set / map / xarray / optional become classes with the idiomatic
surface — for..of, .size, set operators — all delegating to the runtime:
const keys = new Set_Tuto_UserKey();
keys.add(Tuto_UserKey.create());
keys.size; // number of elements
keys.union(other); // typed set algebra
for (const k of keys) { // yields Tuto_UserKey proxies, not raw values
// …
}
The rule from the proxy posture applies throughout:
elements are proxies too. Build and pass the typed container; don’t assemble a
native Set/Array of proxies and convert at the boundary.
Mutating: calling an attachment verb¶
Data does not live in a key — it hangs off it as attachments. Each attachment generates one object whose methods are the verbs; you call them against a state. A state is a point in the commit DAG (see Commit for the model); here is the loop.
Open a state¶
const db = CommitDatabase.open("model.cdb");
// a fresh, empty state:
const state = new CommitMutableState(CommitStateBuilder.initialState(db));
// …or the latest committed state of an existing database:
const latest = new CommitMutableState(
CommitStateBuilder.state(db, db.lastCommitId()));
Write — through attachmentMutating(), then commit¶
The verb object exposes set, per-field set<Field>, union<Field>, … taking an
AttachmentMutating. Nothing is persisted until you commit. A full loop against a
fresh in-memory database — a real app opens a .cdb, but the shape is identical (the
generated definitions module seeds the schema):
import { CommitDatabase, CommitStateBuilder, CommitMutableState }
from "@digitalsubstrate/dsviper";
import { Tuto_UserKey, Tuto_Login, attachments, definitions } from "model";
const db = CommitDatabase.createInMemory();
db.extendDefinitions(definitions.definitions());
const user = Tuto_UserKey.create();
const login = new Tuto_Login();
login.nickname = "alice";
const state = new CommitMutableState(CommitStateBuilder.initialState(db));
attachments.user_Login.set(state.attachmentMutating(), user, login);
attachments.user_Login.setNickname(state.attachmentMutating(), user, "alice2"); // fine-grained
db.commitMutations("Register alice", state);
Read — through attachmentGetting()¶
Each verb object also exposes get, keys, enumerate, taking an
AttachmentGetting drawn from a state:
const read = new CommitMutableState(
CommitStateBuilder.state(db, db.lastCommitId()));
const getting = read.attachmentGetting();
const users = attachments.user_Login.keys(getting); // Set_Tuto_UserKey
const maybe = attachments.user_Login.get(getting, user); // Optional_Tuto_Login
if (!maybe.isNil()) {
console.log(maybe.unwrap().nickname); // "alice2"
}
for (const [key, login] of attachments.user_Login.enumerate(getting)) {
// …
}
The mental model is uniform: a verb is a method on the attachment’s object,
taking a getting/mutating handle, a proxy key, and (for setters) a proxy value.
Collection-valued attachments add the expected verbs — union<Field>,
subtract<Field>, insert<Field>, remove<Field>. The commit itself, and
everything about heads, history and merges, is the Commit
subsystem’s job, not the SDK’s.
Reading a repro¶
Because every generated name is mechanical, an error or bug report written in generated symbols maps straight back to your model.
Given a stack trace that mentions attachments.user_Login.set or Tuto_Login,
read the name:
Tuto_Login→ typeLogininnamespace Tuto→data.ts, and the DSM linestruct Login { … }.attachments.user_Login.set→ namespaceTuto, key conceptUser, attachmentlogin, verbset→ the DSM lineattachment<User, Login> login;, and theuser_Loginobject inattachments.ts.
From there:
The generated file says “Do not edit by hand.” The defect is upstream — in the DSM model, or in the template that produced the file — never a patch to the output.
Regenerate after any model change (
dsm_util.py create_node_package, see dsm_util.py); the package is a build artifact and a stale one will lie about the model.
That legibility is the payoff of the proxy layer: the names you debug are the names you declared.