The data an orbital owns, where it is stored, and who may read or change it.
An entity is the data an orbital owns. Every orbital declares exactly one primary entity; a second or third entity block becomes an auxiliary entity of the same orbital. Traits bind to an entity with ->, and read and write it through @entity.
1entity Invoice [persistent: invoices] {
2 id : string!
3 client : string!
4 amount : money = 0
5 status : "draft" | "sent" | "paid" = draft
6 dueOn : date
7 paidAmount : number = derived (if (= @entity.status "paid") @entity.amount 0)
8}! marks a required field, ? a nullable one.= value sets a default. Defaults are expressions; = now stamps the creation time."a" | "b" | "c" is a closed vocabulary: setting any other value is a validation error.derived (…) is computed from other fields whenever they change; it is never stored.The types are the usual primitives (string, number, int, boolean, date, timestamp) plus refinements that render and validate better: money, email, url, phone, file, duration, icon.
The tag in brackets says where rows are kept:
| Tag | Meaning |
|---|---|
[runtime] | In memory, for the life of the page. View state, counters, wizards. |
[persistent: invoices] | Stored rows in the named collection, read with fetch and written with persist. |
[persistent: invoices, local] | Stored rows that live in the viewer's browser (IndexedDB). The app needs no server. |
[runtime, shared] | One frame shared by every trait bound to the entity, within one viewer's session. |
[persistent: people, identity] | This entity is the shape of the signed-in viewer, @user. |
A trait's @entity is its entity's defaults, then the stored row it is bound to (if any), then its own frame, the view state that (set @entity.x v) writes. Two traits on the same non-shared entity never see each other's writes; mark the entity shared when they should.
A browser-stored entity can start with rows. instances writes them out, and each row is type-checked against the fields:
trait NoteList -> Note [interaction, collection] {
state loading {
INIT -> loading
(fetch Note {
emit: { failure: NotesFailed, success: NotesLoaded }
})
(render-ui main <Skeleton rows={2} variant={list} />)
NotesLoaded -> ready
(emit MOVED { event: NotesLoaded })
(render-ui main <DataList emptyDescription={"Add one with the form."} emptyIcon={file-text} emptyTitle={"No notes yet"} entity={?data} fields={[{
label: Title
name: title
variant: h4
}, {
label: Note
name: body
variant: body
}]} />)
NotesFailed -> ready
(emit MOVED { event: NotesFailed })
(render-ui main <Alert message={?error} variant={error} />)
}
state ready {
RELOAD -> loading
(emit MOVED { event: RELOAD })
(fetch Note {
emit: { failure: NotesFailed, success: NotesLoaded }
})
}
emits {
NotesLoaded {
data : [Note]
}
NotesFailed {
error : string
}
MOVED -> external {
event : string!
}
@description "Each move names the event it took, so the diagram beside the demo can follow it."
}
listens {
NoteForm.NOTE_SAVED -> RELOAD
}
}trait NoteForm -> Note [interaction, instance] {
state editing {
INIT -> editing
(render-ui main <FormSection entity={{}} fields={(title body)} mode={create} submitEvent={SAVE} />)
SAVE -> editing
(emit MOVED { event: SAVE })
(persist create Note ?data)
(emit NOTE_SAVED)
(render-ui main <FormSection entity={{}} fields={(title body)} mode={create} submitEvent={SAVE} />)
}
emits {
NOTE_SAVED
@description "A note was stored."
MOVED -> external {
event : string!
}
@description "Each move names the event it took, so the diagram beside the demo can follow it."
}
listens {
SAVE {
data : Note!
}
}
}The program above as state machines, one per trait: each box is a state, each arrow an event that moves it, and the icons on an arrow are the effects that event runs. A ◇ on an arrow is a guard; hover it to read the check. How to read AVL
1app std-orb-docex-notes "1.0.0"
2"std-orb-docex-notes — a stored list with a form: the list loads on mount, the form saves a new note, and a listens route reloads the list."
3orbital NotesOrbital {
4 entity Note [persistent: docex_notes, local] {
5 id : string!
6 title : string!
7 body : string = ""
8 instances [
9 { id: "N-1", title: "Read about the closed circuit", body: "Event, guard, transition, effects, UI." },
10 { id: "N-2", title: "Run orb validate", body: "Zero errors and zero warnings is the bar." }
11 ]
12 }
13
14 trait NoteList -> Note [interaction, collection] {
15 initial: loading
16
17 state loading {
18 INIT -> loading
19 (fetch Note { emit: { success: NotesLoaded, failure: NotesFailed } })
20 (render-ui main { type: skeleton, variant: list, rows: 2 })
21 NotesLoaded -> ready
22 (emit MOVED { event: "NotesLoaded" })
23 (render-ui main { type: data-list, entity: ?data, fields: [{ name: "title", label: "Title", variant: h4 }, { name: "body", label: "Note", variant: body }], emptyTitle: "No notes yet", emptyDescription: "Add one with the form.", emptyIcon: "file-text" })
24 NotesFailed -> ready
25 (emit MOVED { event: "NotesFailed" })
26 (render-ui main { type: alert, variant: error, message: ?error })
27 }
28
29 state ready {
30 RELOAD -> loading
31 (emit MOVED { event: "RELOAD" })
32 (fetch Note { emit: { success: NotesLoaded, failure: NotesFailed } })
33 }
34
35 emits {
36 NotesLoaded {
37 data : [Note]
38 }
39 NotesFailed {
40 error : string
41 }
42 MOVED -> external {
43 event : string!
44 }
45 @description "Each move names the event it took, so the diagram beside the demo can follow it."
46 }
47
48 listens {
49 NoteForm.NOTE_SAVED -> RELOAD
50 }
51 }
52
53 trait NoteForm -> Note [interaction, instance] {
54 initial: editing
55
56 state editing {
57 INIT -> editing
58 (render-ui main { type: form-section, fields: ["title", "body"], submitEvent: SAVE, mode: create, entity: {} })
59 SAVE -> editing
60 (emit MOVED { event: "SAVE" })
61 (persist create Note ?data)
62 (emit NOTE_SAVED)
63 (render-ui main { type: form-section, fields: ["title", "body"], submitEvent: SAVE, mode: create, entity: {} })
64 }
65
66 emits {
67 NOTE_SAVED
68 @description "A note was stored."
69 MOVED -> external {
70 event : string!
71 }
72 @description "Each move names the event it took, so the diagram beside the demo can follow it."
73 }
74
75 listens {
76 SAVE {
77 data : @entity!
78 }
79 }
80 }
81
82 page "/notes" -> NoteList, NoteForm
83}What happens when SAVE fires:
(persist "create" "Note" @payload.data)(emit "NOTE_SAVED")How the traits talk to each other, in order:
Rows are seeded once, into an empty store, and never over rows the viewer already has. A program that declares locales [en, ar, sl] can seed each language separately with instances ar [ … ] and instances sl [ … ]; see localization.
1(fetch Note { emit: { success: NotesLoaded, failure: NotesFailed } })
2(fetch Note { id: ?id })
3(fetch Note { filter: ["=", (object/get @entity status), "open"], limit: 20 })
4(persist create Note ?data)
5(persist update Note (object/merge ?row { done: true }))
6(persist delete Note ?id)fetch loads rows and reports through the events in emit; without them nothing observes the load. Both result events are declared in the trait's emits. Inside a filter, (object/get @entity field) is the row being tested; a bare @entity.field is the trait's own frame.
Typing a field as another entity creates a relation: customerId : Customer (one) or attendees : [Person] (many). (fetch Order { include: ["customerId"] }) hydrates it. T.f names the type of a field of another entity, so tagId : Tag.id! is a string that is known to hold a tag's id.
Access policies sit at the top of the entity body and are enforced once, at the data layer, for every trait bound to the entity:
1entity Session [persistent: sessions] {
2 @read (or ["=", @user.role, "manager"] ["=", (object/get @entity trainerId), @user.id])
3 @update ["=", (object/get @entity trainerId), @user.id]
4 @delete none "sessions are archived, never deleted"
5 trainerId : Person
6 title : string!
7}@user is the viewer; @entity is the row in question. No policy means everyone may perform that operation; none "reason" records a deliberate omission. Policies protect data. What a screen shows or hides is written explicitly in the program, with guards.
Traits are the state machines that read and change entities.
A programming language of composable state machines.