nervur 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +13 -0
- package/LICENSE +55 -202
- package/NOTICE +3 -8
- package/README.md +14 -156
- package/RELEASING.md +15 -0
- package/SECURITY.md +9 -0
- package/bin/nervur.js +6 -0
- package/package.json +26 -81
- package/AUTHORING.md +0 -935
- package/COMMAND.md +0 -179
- package/FACES.md +0 -280
- package/FACULTIES.md +0 -578
- package/GROUNDS.md +0 -315
- package/KIT-SPEC.md +0 -304
- package/WORLD.md +0 -195
- package/dist/app/app-ground.d.ts +0 -18
- package/dist/app/app-ground.js +0 -23
- package/dist/app/index.d.ts +0 -2
- package/dist/app/index.js +0 -5
- package/dist/app/native.d.ts +0 -11
- package/dist/app/native.js +0 -13
- package/dist/being/being.d.ts +0 -306
- package/dist/being/being.js +0 -20
- package/dist/being/covers.d.ts +0 -9
- package/dist/being/covers.js +0 -21
- package/dist/being/index.d.ts +0 -4
- package/dist/being/index.js +0 -6
- package/dist/being/need.d.ts +0 -49
- package/dist/being/need.js +0 -33
- package/dist/being/schema.d.ts +0 -100
- package/dist/being/schema.js +0 -132
- package/dist/being/table.d.ts +0 -48
- package/dist/being/table.js +0 -344
- package/dist/bench/bench-ground.d.ts +0 -73
- package/dist/bench/bench-ground.js +0 -153
- package/dist/bench/bench.d.ts +0 -101
- package/dist/bench/bench.js +0 -368
- package/dist/bench/fake-clock.d.ts +0 -17
- package/dist/bench/fake-clock.js +0 -34
- package/dist/bench/fake-memory.d.ts +0 -16
- package/dist/bench/fake-memory.js +0 -46
- package/dist/bench/fake-network.d.ts +0 -66
- package/dist/bench/fake-network.js +0 -222
- package/dist/bench/fake-unlock.d.ts +0 -6
- package/dist/bench/fake-unlock.js +0 -10
- package/dist/bench/index.d.ts +0 -3
- package/dist/bench/index.js +0 -5
- package/dist/bench/seeded.d.ts +0 -11
- package/dist/bench/seeded.js +0 -37
- package/dist/bench/settle.d.ts +0 -2
- package/dist/bench/settle.js +0 -49
- package/dist/bench/stewards.d.ts +0 -169
- package/dist/bench/stewards.js +0 -128
- package/dist/bodies/class-list.d.ts +0 -14
- package/dist/bodies/class-list.js +0 -33
- package/dist/bodies/joined-carry.d.ts +0 -29
- package/dist/bodies/joined-carry.js +0 -48
- package/dist/bodies/kept.d.ts +0 -44
- package/dist/bodies/kept.js +0 -91
- package/dist/bodies/noble-crypto.d.ts +0 -22
- package/dist/bodies/noble-crypto.js +0 -88
- package/dist/bodies/seed-keys.d.ts +0 -10
- package/dist/bodies/seed-keys.js +0 -42
- package/dist/bodies/strict-tools.d.ts +0 -10
- package/dist/bodies/strict-tools.js +0 -306
- package/dist/bodies/vouch.d.ts +0 -20
- package/dist/bodies/vouch.js +0 -71
- package/dist/bodies/web-carry.d.ts +0 -72
- package/dist/bodies/web-carry.js +0 -245
- package/dist/bodies/web-clock.d.ts +0 -12
- package/dist/bodies/web-clock.js +0 -33
- package/dist/browser/browser-ground.d.ts +0 -76
- package/dist/browser/browser-ground.js +0 -294
- package/dist/browser/index.d.ts +0 -4
- package/dist/browser/index.js +0 -7
- package/dist/browser/indexeddb-memory.d.ts +0 -17
- package/dist/browser/indexeddb-memory.js +0 -97
- package/dist/browser/locked-unlock.d.ts +0 -24
- package/dist/browser/locked-unlock.js +0 -85
- package/dist/browser/origin-classes.d.ts +0 -7
- package/dist/browser/origin-classes.js +0 -31
- package/dist/edge/durable-clock.d.ts +0 -19
- package/dist/edge/durable-clock.js +0 -61
- package/dist/edge/durable-memory.d.ts +0 -5
- package/dist/edge/durable-memory.js +0 -11
- package/dist/edge/durable.d.ts +0 -18
- package/dist/edge/durable.js +0 -24
- package/dist/edge/edge-ground.d.ts +0 -49
- package/dist/edge/edge-ground.js +0 -223
- package/dist/edge/index.d.ts +0 -7
- package/dist/edge/index.js +0 -9
- package/dist/edge/native-crypto.d.ts +0 -8
- package/dist/edge/native-crypto.js +0 -102
- package/dist/edge/secret-unlock.d.ts +0 -7
- package/dist/edge/secret-unlock.js +0 -13
- package/dist/edge/socket-carry.d.ts +0 -39
- package/dist/edge/socket-carry.js +0 -111
- package/dist/foundation.d.ts +0 -162
- package/dist/foundation.js +0 -4
- package/dist/ground/dock.d.ts +0 -1460
- package/dist/ground/dock.js +0 -1159
- package/dist/ground/ground.d.ts +0 -239
- package/dist/ground/ground.js +0 -787
- package/dist/ground/views.d.ts +0 -76
- package/dist/ground/views.js +0 -208
- package/dist/house/crossing.d.ts +0 -50
- package/dist/house/crossing.js +0 -153
- package/dist/house/house.d.ts +0 -88
- package/dist/house/house.js +0 -2069
- package/dist/house/rows-shape.d.ts +0 -110
- package/dist/house/rows-shape.js +0 -12
- package/dist/house/rows.d.ts +0 -27
- package/dist/house/rows.js +0 -185
- package/dist/index.d.ts +0 -33
- package/dist/index.js +0 -29
- package/dist/node/bridge.d.ts +0 -20
- package/dist/node/bridge.js +0 -284
- package/dist/node/cli.d.ts +0 -2
- package/dist/node/cli.js +0 -225
- package/dist/node/file-memory.d.ts +0 -13
- package/dist/node/file-memory.js +0 -86
- package/dist/node/folder-classes.d.ts +0 -5
- package/dist/node/folder-classes.js +0 -40
- package/dist/node/gone.d.ts +0 -2
- package/dist/node/gone.js +0 -7
- package/dist/node/hand.d.ts +0 -39
- package/dist/node/hand.js +0 -107
- package/dist/node/http.d.ts +0 -11
- package/dist/node/http.js +0 -90
- package/dist/node/index.d.ts +0 -11
- package/dist/node/index.js +0 -13
- package/dist/node/ledger-memory.d.ts +0 -17
- package/dist/node/ledger-memory.js +0 -152
- package/dist/node/lock.d.ts +0 -10
- package/dist/node/lock.js +0 -78
- package/dist/node/node-ground.d.ts +0 -26
- package/dist/node/node-ground.js +0 -228
- package/dist/node/notify.d.ts +0 -1
- package/dist/node/notify.js +0 -18
- package/dist/node/shell.d.ts +0 -11
- package/dist/node/shell.js +0 -35
- package/dist/node/sync-folder.d.ts +0 -6
- package/dist/node/sync-folder.js +0 -18
- package/dist/node/tcp-carry.d.ts +0 -42
- package/dist/node/tcp-carry.js +0 -193
- package/dist/node/unlock.d.ts +0 -17
- package/dist/node/unlock.js +0 -86
- package/dist/node/websocket.d.ts +0 -12
- package/dist/node/websocket.js +0 -134
- package/dist/quo/frame.d.ts +0 -33
- package/dist/quo/frame.js +0 -92
- package/dist/quo/index.d.ts +0 -1
- package/dist/quo/index.js +0 -5
- package/dist/quo/room.d.ts +0 -154
- package/dist/quo/room.js +0 -362
- package/dist/serve/index.d.ts +0 -45
- package/dist/serve/index.js +0 -71
package/AUTHORING.md
DELETED
|
@@ -1,935 +0,0 @@
|
|
|
1
|
-
# Writing for nervur
|
|
2
|
-
|
|
3
|
-
This guide teaches the two things you write with `nervur`. A **being**
|
|
4
|
-
holds logic and state. A **faculty** reaches the world outside, and
|
|
5
|
-
[Writing a faculty](FACULTIES.md) teaches it whole. A **ground** runs them on
|
|
6
|
-
a machine, and the library ships it. The examples build one small shop,
|
|
7
|
-
and every file here is a file the package's own tests run.
|
|
8
|
-
|
|
9
|
-
The shop has three classes and one faculty.
|
|
10
|
-
|
|
11
|
-
- `Order` is one order, from its first item to its shipping.
|
|
12
|
-
- `Shop` is the steward, the being that runs the house.
|
|
13
|
-
- `Lobby` is the public being, which strangers may ask.
|
|
14
|
-
- `Payments` is a faculty that charges money, which `recipe.ts` holds
|
|
15
|
-
by name.
|
|
16
|
-
|
|
17
|
-
## Words
|
|
18
|
-
|
|
19
|
-
| Word | What it is |
|
|
20
|
-
| --- | --- |
|
|
21
|
-
| being | an instance of a class, born for one ask and dropped after it |
|
|
22
|
-
| cells | her state, JSON values, kept when an ask lands |
|
|
23
|
-
| ask | a method others may call on her, with its entry |
|
|
24
|
-
| asker | who calls her now: an id and notes |
|
|
25
|
-
| occupant | someone who may ask her, by id |
|
|
26
|
-
| standing | someone she may ask, by id |
|
|
27
|
-
| blueprint | the shape of what can be called: a name and its methods |
|
|
28
|
-
| need | the blueprint she calls, at its minimum, under a member of her own |
|
|
29
|
-
| faculty | anything outside the house that answers a blueprint |
|
|
30
|
-
| offer | a faculty the ground hands the house: its blueprint and its object |
|
|
31
|
-
| role | a named test over the asker and her cells |
|
|
32
|
-
| state | a name read from her cells that decides which asks exist |
|
|
33
|
-
| house | what holds beings, keeps their cells, and seals every ask |
|
|
34
|
-
| ground | the process houses run in, holding one key and sealing their seeds, faculties and secrets under it |
|
|
35
|
-
| registry | code that holds faculties by name, for the ground to stand |
|
|
36
|
-
| body | a faculty stood: the living instance houses and bodies use |
|
|
37
|
-
|
|
38
|
-
## A being
|
|
39
|
-
|
|
40
|
-
A being is pure logic over her cells. She never knows where she runs,
|
|
41
|
-
who carries her asks, or how her cells are kept. The house does all of
|
|
42
|
-
it, and she trusts it.
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
// classes/order.ts
|
|
46
|
-
import { Being, s, need, type Args } from 'nervur/being';
|
|
47
|
-
|
|
48
|
-
export const Payments = need('payments', {
|
|
49
|
-
charge: {
|
|
50
|
-
args: s.object({ order: s.string(), amount: s.number(), notify: s.handle() }),
|
|
51
|
-
result: s.object({ pending: s.boolean() }),
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
export const Courier = need('courier', {
|
|
56
|
-
pickup: { args: s.object({ items: s.array(s.string()) }) },
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
export class Order extends Being.of({
|
|
60
|
-
kind: 'com.acme.order',
|
|
61
|
-
description: 'One order, from its first item to its shipping.',
|
|
62
|
-
needs: { pay: Payments },
|
|
63
|
-
cells: {
|
|
64
|
-
items: [] as string[],
|
|
65
|
-
total: 0,
|
|
66
|
-
state: 'open',
|
|
67
|
-
paidAt: 0,
|
|
68
|
-
courier: '',
|
|
69
|
-
},
|
|
70
|
-
roles: { owner: (asker) => asker.steward.owner === true },
|
|
71
|
-
state: (me) => me.cells.state,
|
|
72
|
-
asks: {
|
|
73
|
-
hire: {
|
|
74
|
-
for: 'steward',
|
|
75
|
-
hints: { idempotent: true },
|
|
76
|
-
args: s.object({ courier: s.handle() }),
|
|
77
|
-
result: s.string(),
|
|
78
|
-
},
|
|
79
|
-
add: {
|
|
80
|
-
in: 'open',
|
|
81
|
-
for: 'owner',
|
|
82
|
-
to: 'open',
|
|
83
|
-
args: s.object({ sku: s.string(), price: s.number() }),
|
|
84
|
-
result: s.object({ total: s.number() }),
|
|
85
|
-
},
|
|
86
|
-
checkout: { in: 'open', for: 'owner', to: 'paying' },
|
|
87
|
-
charged: { in: 'paying', args: s.reply(Payments.charge), to: ['paying', 'open'] },
|
|
88
|
-
settled: { in: 'paying', for: 'handle', to: 'paid' },
|
|
89
|
-
ship: { in: 'paid', for: 'owner', to: 'shipped' },
|
|
90
|
-
},
|
|
91
|
-
}) {
|
|
92
|
-
// The courier's invitation arrives as her standing. The same one twice is the same standing.
|
|
93
|
-
hire({ courier }: Args<Order, 'hire'>) {
|
|
94
|
-
this.cells.courier = courier;
|
|
95
|
-
return courier;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
add({ sku, price }: Args<Order, 'add'>) {
|
|
99
|
-
this.cells.items = [...this.cells.items, sku];
|
|
100
|
-
this.cells.total += price;
|
|
101
|
-
return { total: this.cells.total };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
checkout() {
|
|
105
|
-
if (this.cells.items.length === 0) this.fail('Add an item first.');
|
|
106
|
-
const notify = this.handle('settled', { once: true });
|
|
107
|
-
this.pay.charge({ order: this.id, amount: this.cells.total, notify }, { reply: 'charged' });
|
|
108
|
-
this.cells.state = 'paying';
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
charged({ error }: Args<Order, 'charged'>) {
|
|
112
|
-
if (error) this.cells.state = 'open';
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
settled() {
|
|
116
|
-
this.cells.state = 'paid';
|
|
117
|
-
this.cells.paidAt = this.house.now();
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
ship() {
|
|
121
|
-
if (this.cells.courier === '') this.fail('Hire a courier first.');
|
|
122
|
-
this.held(this.cells.courier, Courier).pickup({ items: this.cells.items });
|
|
123
|
-
this.cells.state = 'shipped';
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
The order wrote no retry, no catch, no key and no address. `charge` is
|
|
129
|
-
an effect, so it leaves only once `checkout` has landed. Its answer comes
|
|
130
|
-
back to `charged`, and a refused charge opens the order again. The
|
|
131
|
-
provider calls `settled` through the handle once the money arrives.
|
|
132
|
-
`hire` takes a courier's invitation as her standing, and `ship` asks the
|
|
133
|
-
courier through it. `shipped` is a terminal state, and nothing leaves it.
|
|
134
|
-
|
|
135
|
-
### The declaration
|
|
136
|
-
|
|
137
|
-
A class extends `Being.of({ … })`. That one object declares the class,
|
|
138
|
-
and TypeScript reads from it the types of her cells and her needs.
|
|
139
|
-
|
|
140
|
-
| Field | What it is |
|
|
141
|
-
| --- | --- |
|
|
142
|
-
| `kind` | the code's name: a reversed domain you own, then a name |
|
|
143
|
-
| `description` | one line for readers and agents |
|
|
144
|
-
| `cells` | the state and its defaults, written where a key is missing |
|
|
145
|
-
| `needs` | blueprints she calls, each under a member name she chooses |
|
|
146
|
-
| `roles` | named tests over the asker and her cells |
|
|
147
|
-
| `state` | a function of the being that names her current state |
|
|
148
|
-
| `asks` | one entry per method she answers |
|
|
149
|
-
| `view` | markup as text over her asks, which a screen renders |
|
|
150
|
-
|
|
151
|
-
A view is data, at most 64 KiB, carried in her describe to every asker.
|
|
152
|
-
It holds no script, and a renderer loads nothing it names, so a view
|
|
153
|
-
neither acts nor tracks. The library defines no markup: a renderer
|
|
154
|
-
speaks its own.
|
|
155
|
-
|
|
156
|
-
Every field is optional but `kind` and `asks`. A class with no `state`
|
|
157
|
-
has one state, named `ready`. The kind is how the house finds her code
|
|
158
|
-
again, so a bundler that renames classes changes nothing.
|
|
159
|
-
|
|
160
|
-
A method in TypeScript names its args with `Args<Class, 'method'>`,
|
|
161
|
-
since a subclass's method takes no type from its base. A method in
|
|
162
|
-
JavaScript writes nothing.
|
|
163
|
-
|
|
164
|
-
### An ask's entry
|
|
165
|
-
|
|
166
|
-
| Field | What it says |
|
|
167
|
-
| --- | --- |
|
|
168
|
-
| `in` | the states where the ask exists; omitted, every state |
|
|
169
|
-
| `for` | the roles that may call it; omitted, every occupant but handles |
|
|
170
|
-
| `to` | the states it may land in; omitted, the state it began in |
|
|
171
|
-
| `args` | the schema of what it takes; omitted, the empty object alone |
|
|
172
|
-
| `result` | the schema of what it answers; omitted, nothing |
|
|
173
|
-
| `hints` | `readOnly`, `idempotent`, `destructive` |
|
|
174
|
-
| `examples` | a history of her asks, a role, args, fakes, and what it gives |
|
|
175
|
-
| `description` | one line for readers and agents |
|
|
176
|
-
| `wait` | milliseconds she may run; omitted, thirty seconds |
|
|
177
|
-
|
|
178
|
-
`in`, `for` and `to` each take one name or a list of names. A method
|
|
179
|
-
with no entry is never reached. An entry with no method is refused when
|
|
180
|
-
the house first loads the class.
|
|
181
|
-
|
|
182
|
-
Five roles are the house's. `handle` is whoever holds a handle to this
|
|
183
|
-
ask. `stranger` is the asker of a public being. `steward` is her
|
|
184
|
-
steward. `being` is another being her steward introduced to her. `root`
|
|
185
|
-
is the owner, through the house's hand. Every other role is yours, a
|
|
186
|
-
function of `(asker, me)`.
|
|
187
|
-
|
|
188
|
-
The house checks the table when it first loads the class. It refuses a
|
|
189
|
-
state no ask reaches, an ask no role reaches, and a role no ask names.
|
|
190
|
-
After a method runs, a state outside the entry's `to` fails the ask, and
|
|
191
|
-
nothing lands.
|
|
192
|
-
|
|
193
|
-
### What she reaches
|
|
194
|
-
|
|
195
|
-
| Member | What it is |
|
|
196
|
-
| --- | --- |
|
|
197
|
-
| `this.id` | her own id |
|
|
198
|
-
| `this.position` | `steward`, `public` or `normal` |
|
|
199
|
-
| `this.asker` | `{ id, notes, steward }` of who asks now; `signer` for a stranger |
|
|
200
|
-
| `this.cells` | her values |
|
|
201
|
-
| `this.house.now()` | the time, in milliseconds since the epoch |
|
|
202
|
-
| `this.house.random({ length })` | random bytes |
|
|
203
|
-
| `this.house.alarm({ at, ask, args, key })` | one of her asks, at that time |
|
|
204
|
-
| `this.house.cancelAlarm({ key })` | an alarm removed |
|
|
205
|
-
| `this.<need>.<method>({…}, { reply? })` | an awaited call, or an effect |
|
|
206
|
-
| `this.held(id, Need).<ask>({…}, { reply?, after? })` | the same, on a standing, and a watch where `after` is given |
|
|
207
|
-
| `this.held(id).describe()`, `.ask(method, {…}, options)` | a standing with no need: what it shows her, then any ask it showed |
|
|
208
|
-
| `this.stranger({ ward, at }, Need).<ask>({…})` | a far house's public being, asked as a stranger, every ask awaited |
|
|
209
|
-
| `this.stranger({ ward, at }).describe()`, `.ask(method, {…})` | the same with no need: what she shows a stranger, then any ask it showed |
|
|
210
|
-
| `this.standings` | `list()`, `note(id, notes)` and `drop(id)` |
|
|
211
|
-
| `this.handle(ask, { bind?, notes?, once?, expires? })` | a handle to one of her asks |
|
|
212
|
-
| `this.invite(id, { notes?, expires? })` | a new occupant, as a handle |
|
|
213
|
-
| `this.occupants` | `list()`, `note(id, notes)` and `dismiss(id)` |
|
|
214
|
-
| `this.steward` | her steward, where she is not one |
|
|
215
|
-
| `this.powers` | the house's powers, where she is the steward |
|
|
216
|
-
| `this.fail(message)` | an error the asker can act on |
|
|
217
|
-
|
|
218
|
-
An alarm lives in the house's rows and survives every restart. A second
|
|
219
|
-
alarm under one key replaces the first. When its time comes, the house
|
|
220
|
-
asks her named ask as her steward would.
|
|
221
|
-
|
|
222
|
-
### Awaited calls and effects
|
|
223
|
-
|
|
224
|
-
The callee decides how it is called, with one flag. A method or ask
|
|
225
|
-
marked `idempotent` is safe to repeat, so the caller awaits it during
|
|
226
|
-
her ask. Everything else is an effect. `readOnly` is stricter: it writes
|
|
227
|
-
nothing, it implies `idempotent`, and the house holds it.
|
|
228
|
-
|
|
229
|
-
An awaited call answers during her ask. `await this.fx.rate({…})`
|
|
230
|
-
returns the answer, and a failure throws an error she may catch. It
|
|
231
|
-
waits thirty seconds unless its entry says otherwise, and never more
|
|
232
|
-
than five minutes.
|
|
233
|
-
|
|
234
|
-
An awaited call never comes back to a being in its own chain. While she
|
|
235
|
-
awaits, she holds her queue. A call back to her, other than a
|
|
236
|
-
`readOnly` one, waits behind the ask that waits for it, until the wait
|
|
237
|
-
runs out and the call fails as `answered nothing`. Call back with an
|
|
238
|
-
effect instead.
|
|
239
|
-
|
|
240
|
-
An effect leaves after her ask lands. She calls it and it returns
|
|
241
|
-
nothing. The house writes it in the same write as her cells, then sends
|
|
242
|
-
it with one call id until it is answered. So an effect acts at most
|
|
243
|
-
once. Its answer comes back to the ask `reply` names, as `{ result }` or
|
|
244
|
-
`{ error: { message } }`.
|
|
245
|
-
|
|
246
|
-
An effect gives up at its deadline, seven days for a standing and the
|
|
247
|
-
offer's window for a faculty. The reply then hears an error. Effects to
|
|
248
|
-
one receiver leave one at a time, in the order she called them.
|
|
249
|
-
|
|
250
|
-
Asks to one being run one at a time, in the order they arrive. A
|
|
251
|
-
`readOnly` ask runs beside that queue, on the cells last landed.
|
|
252
|
-
|
|
253
|
-
### Watching an answer
|
|
254
|
-
|
|
255
|
-
A watch is a `readOnly` ask asked with `after`, the answer you already
|
|
256
|
-
hold. The house answers at once where the answer differs. Where it is
|
|
257
|
-
the same, the house holds the ask and runs it again each time an ask of
|
|
258
|
-
that being lands. It answers the first answer that differs, or the same
|
|
259
|
-
one when the wait runs out. So a chat, an order's status or a dashboard
|
|
260
|
-
is a loop of watches, and nothing polls.
|
|
261
|
-
|
|
262
|
-
Through the hand, pass `after` beside the method, as the answer you
|
|
263
|
-
hold: `{ result: [...] }`. From a being, pass the result she holds:
|
|
264
|
-
`this.held(room, Messages).messages({}, { after: seen })`, inside a
|
|
265
|
-
`readOnly` ask of her own, so she stays free while she waits. Only a
|
|
266
|
-
`readOnly` ask is watched, and a watch on anything else is refused where
|
|
267
|
-
she makes it.
|
|
268
|
-
|
|
269
|
-
A watch may land as a reply instead: `{ after: seen, reply: 'heard' }`.
|
|
270
|
-
It leaves once her ask lands, as an effect does, and its answer asks her
|
|
271
|
-
`heard`, where she writes what she heard and watches again. So a device
|
|
272
|
-
that only dials, a phone or a Pi behind a router, hears its station the
|
|
273
|
-
moment something changes there, and opens no port.
|
|
274
|
-
|
|
275
|
-
A watch moves only on what its asker could read, since it runs as that
|
|
276
|
-
asker. One asker holds one watch on one ask with the same args, and a
|
|
277
|
-
second answers the first at once. A watch across a door holds its
|
|
278
|
-
relation until it answers, since Quo moves a relation one ask at a time.
|
|
279
|
-
So watch a far being through a handle to the watched ask alone, a
|
|
280
|
-
relation of its own, and ask everything else on your other standing.
|
|
281
|
-
|
|
282
|
-
### Relations
|
|
283
|
-
|
|
284
|
-
An **occupant** is someone who may ask her. She mints one with
|
|
285
|
-
`this.invite(id, { notes })`, which answers a handle, and lets one go with
|
|
286
|
-
`dismiss`.
|
|
287
|
-
|
|
288
|
-
A **standing** is someone she may ask. She receives one where an ask's
|
|
289
|
-
args carry an invitation under `s.handle`, or where her steward
|
|
290
|
-
introduces one. She asks through it with `this.held(id, Need)`, which
|
|
291
|
-
checks the standing's describe covers the need. A standing from an
|
|
292
|
-
invitation is named `standing:` and sixteen hex digits, and the id is
|
|
293
|
-
what her ask receives. One her steward introduced is named by the being
|
|
294
|
-
it reaches. She asks a far standing from the ask after the one that took
|
|
295
|
-
it.
|
|
296
|
-
|
|
297
|
-
Every relation carries two sets of notes. `notes` are hers alone.
|
|
298
|
-
`steward` are her steward's, written when the steward made the relation,
|
|
299
|
-
and she reads them and never writes them. The order's `owner` role reads
|
|
300
|
-
the steward's notes, so only the steward decides who owns an order.
|
|
301
|
-
|
|
302
|
-
A **handle** is the only way a relation leaves her. `this.handle(ask)`
|
|
303
|
-
admits its holder to that one ask. `once` dismisses it after its first
|
|
304
|
-
ask lands, and `bind` fixes args the holder cannot change. The house
|
|
305
|
-
turns a handle into an invitation for a far house, or a token for a
|
|
306
|
-
faculty. A handle never enters her cells.
|
|
307
|
-
|
|
308
|
-
`expires`, on `this.handle`, `this.invite` and the steward's `invite`,
|
|
309
|
-
gives each invitation minted from the handle that many milliseconds to
|
|
310
|
-
be taken. Its first knock lands within that time, or it hears silence.
|
|
311
|
-
One taken in time never expires, and one without `expires` waits for
|
|
312
|
-
ever. Give it to every invitation a being mints on each answer, so the
|
|
313
|
-
unspent ones go.
|
|
314
|
-
|
|
315
|
-
### The steward
|
|
316
|
-
|
|
317
|
-
Every house has one steward, with the id `steward`. The holder of the
|
|
318
|
-
house's hand asks her as the occupant `root`, which no sealed box can
|
|
319
|
-
forge. She alone holds `this.powers`.
|
|
320
|
-
|
|
321
|
-
```ts
|
|
322
|
-
// classes/shop.ts
|
|
323
|
-
import { Being, s, type Args } from 'nervur/being';
|
|
324
|
-
|
|
325
|
-
export class Shop extends Being.of({
|
|
326
|
-
kind: 'com.acme.shop',
|
|
327
|
-
description: 'The shop’s steward: opens orders, and enrols whoever signs up.',
|
|
328
|
-
cells: { opened: 0 },
|
|
329
|
-
roles: { pilot: (asker) => asker.id === 'root' },
|
|
330
|
-
asks: {
|
|
331
|
-
open: {
|
|
332
|
-
for: 'pilot',
|
|
333
|
-
description: 'Opens an order, and hands back an invitation for its owner.',
|
|
334
|
-
args: s.object({ id: s.string() }),
|
|
335
|
-
result: s.object({ owner: s.invitation() }),
|
|
336
|
-
},
|
|
337
|
-
orders: { for: 'pilot', hints: { readOnly: true }, result: s.array(s.string()) },
|
|
338
|
-
hire: {
|
|
339
|
-
for: 'pilot',
|
|
340
|
-
description: 'Hands a courier’s invitation to an order, which takes it.',
|
|
341
|
-
hints: { idempotent: true },
|
|
342
|
-
args: s.object({ order: s.string(), courier: s.invitation() }),
|
|
343
|
-
result: s.string(),
|
|
344
|
-
},
|
|
345
|
-
enroll: {
|
|
346
|
-
for: 'being',
|
|
347
|
-
hints: { idempotent: true },
|
|
348
|
-
args: s.object({ signer: s.bytes() }),
|
|
349
|
-
result: s.object({ invitation: s.invitation() }),
|
|
350
|
-
},
|
|
351
|
-
},
|
|
352
|
-
}) {
|
|
353
|
-
open({ id }: Args<Shop, 'open'>) {
|
|
354
|
-
this.powers!.bear({ kind: 'com.acme.order', id });
|
|
355
|
-
this.cells.opened += 1;
|
|
356
|
-
return { owner: this.powers!.invite({ id, occupant: 'owner', notes: { owner: true } }) };
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async orders() {
|
|
360
|
-
return (await this.powers!.list()).filter((being) => being.kind === 'com.acme.order').map((being) => being.id);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// She carries the invitation unopened, and the order she names takes it.
|
|
364
|
-
async hire({ order, courier }: Args<Shop, 'hire'>) {
|
|
365
|
-
return (await this.powers!.ask({ id: order, method: 'hire', args: { courier } })) as string;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// One order a signer: asked twice, the same id is borne once.
|
|
369
|
-
enroll({ signer }: Args<Shop, 'enroll'>) {
|
|
370
|
-
const id = `order-${Array.from(signer.subarray(0, 8), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
371
|
-
this.powers!.bear({ kind: 'com.acme.order', id });
|
|
372
|
-
const occupant = `owner-${Array.from(this.house.random({ length: 4 }), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
373
|
-
return { invitation: this.powers!.invite({ id, occupant, notes: { owner: true } }) };
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
| Power | What it does |
|
|
379
|
-
| --- | --- |
|
|
380
|
-
| `bear({ kind, id, args })` | places a new being; the same id twice answers the first |
|
|
381
|
-
| `remove({ id })` | removes a being and everything of hers |
|
|
382
|
-
| `list()` | every being, her kind, whether she is absent, and her dead letters |
|
|
383
|
-
| `ask({ id, method, args }, { reply? })` | asks any being as her occupant `steward`; with no method, reads what she shows the steward |
|
|
384
|
-
| `introduce({ from, to, notes })` | gives `from` a standing on `to`, and `to` an occupant |
|
|
385
|
-
| `invite({ id, occupant, notes, expires? })` | gives a being a new occupant, and the steward its handle |
|
|
386
|
-
|
|
387
|
-
`bear`, `remove`, `introduce` and `invite` land in the steward's own
|
|
388
|
-
write. If her ask fails, none of them happened. A being borne runs her
|
|
389
|
-
`born` ask first, where her class declares one, asked as the occupant
|
|
390
|
-
`steward`, so its entry says `for: 'steward'`.
|
|
391
|
-
|
|
392
|
-
Every other being is placed as `normal`. She holds the standing
|
|
393
|
-
`steward` and the occupant `steward`, and can drop neither.
|
|
394
|
-
|
|
395
|
-
An invitation from outside lands where the owner says. The courier's
|
|
396
|
-
invitation reaches the owner by any road, a mail or a link. The owner
|
|
397
|
-
hands it to the steward's `hire` with the order it is for. `hire`
|
|
398
|
-
declares it `s.invitation`, so the steward carries it unopened and never
|
|
399
|
-
holds it. The order's `hire` declares it `s.handle`, so the order takes
|
|
400
|
-
it, and the standing is hers.
|
|
401
|
-
|
|
402
|
-
Taking an invitation is safe to repeat. The same invitation taken again
|
|
403
|
-
gives the same standing, so the order's `hire` is `idempotent`, and the
|
|
404
|
-
steward awaits it. The owner hears the standing, or why it was refused.
|
|
405
|
-
A steward that bears a being for an invitation bears her first, then
|
|
406
|
-
hands it to her the same way.
|
|
407
|
-
|
|
408
|
-
### A public being
|
|
409
|
-
|
|
410
|
-
A house may name one public being, with the id `public`. She answers
|
|
411
|
-
strangers: every asker with no relation is the occupant `stranger`, and
|
|
412
|
-
`this.asker.signer` is the key the ask was signed with.
|
|
413
|
-
|
|
414
|
-
```ts
|
|
415
|
-
// classes/lobby.ts
|
|
416
|
-
import { Being, need, s } from 'nervur/being';
|
|
417
|
-
|
|
418
|
-
/** What the lobby asks of her steward. */
|
|
419
|
-
const Signup = need('signup', {
|
|
420
|
-
enroll: {
|
|
421
|
-
hints: { idempotent: true },
|
|
422
|
-
args: s.object({ signer: s.bytes() }),
|
|
423
|
-
result: s.object({ invitation: s.invitation() }),
|
|
424
|
-
},
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
export class Lobby extends Being.of({
|
|
428
|
-
kind: 'com.acme.lobby',
|
|
429
|
-
description: 'The shop’s front door: a stranger signs up and receives an order of their own.',
|
|
430
|
-
asks: {
|
|
431
|
-
signup: { for: 'stranger', result: s.object({ invitation: s.invitation() }) },
|
|
432
|
-
},
|
|
433
|
-
}) {
|
|
434
|
-
signup() {
|
|
435
|
-
return this.held('steward', Signup).enroll({ signer: this.asker.signer! });
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
A public being's asks are idempotent by default, since a stranger's box
|
|
441
|
-
may arrive twice. The house answers a replayed stranger's box from a
|
|
442
|
-
cache for ten minutes, and nothing runs twice. An ask marked
|
|
443
|
-
`hints: { idempotent: false }` is refused to strangers.
|
|
444
|
-
|
|
445
|
-
Signup awaits the steward. The lobby asks `enroll`, which is
|
|
446
|
-
`idempotent`, so the lobby awaits its answer and returns the invitation
|
|
447
|
-
unopened. A stranger who signs up twice holds one order.
|
|
448
|
-
|
|
449
|
-
Signup is this shop's choice, not the house's. A house is its owner's,
|
|
450
|
-
and a public being does only what its owner wrote. One may answer who
|
|
451
|
-
the house is and nothing more. A house with no public being answers
|
|
452
|
-
strangers silence.
|
|
453
|
-
|
|
454
|
-
### Schemas
|
|
455
|
-
|
|
456
|
-
One builder gives the schema and the TypeScript type: `s.object`,
|
|
457
|
-
`s.string`, `s.number`, `s.integer`, `s.boolean`, `s.array`, `s.enum`,
|
|
458
|
-
`s.const`, `s.bytes`, `s.optional`, `s.handle`, `s.invitation` and
|
|
459
|
-
`s.reply`. They write JSON Schema 2020-12, in a subset the house checks
|
|
460
|
-
on every ask.
|
|
461
|
-
|
|
462
|
-
- `s.bytes` is a `Uint8Array` in the process and lowercase hex across a
|
|
463
|
-
door.
|
|
464
|
-
- `s.handle` carries a relation. A handle leaves as an invitation, and
|
|
465
|
-
an invitation arrives as a new standing id.
|
|
466
|
-
- `s.invitation` carries an invitation unopened, as the lobby does.
|
|
467
|
-
Passed on under `s.handle`, it is taken by the being that receives
|
|
468
|
-
it. Taking one is safe to repeat: the same invitation gives the same
|
|
469
|
-
standing.
|
|
470
|
-
- `s.reply(method)` is the args of an effect's reply: `{ result }` or
|
|
471
|
-
`{ error: { message } }`.
|
|
472
|
-
|
|
473
|
-
### Needs
|
|
474
|
-
|
|
475
|
-
A need is a blueprint at its minimum: what she will call, never what a
|
|
476
|
-
faculty offers. `need(name, methods)` writes one, and she binds it to a
|
|
477
|
-
member of her own in `needs`. An offer covers a need when four things
|
|
478
|
-
hold.
|
|
479
|
-
|
|
480
|
-
1. The blueprint's name is the same.
|
|
481
|
-
2. Every method the need names is offered. More are allowed.
|
|
482
|
-
3. The offer requires no property the need does not require.
|
|
483
|
-
4. Each method is `idempotent` in both, or in neither.
|
|
484
|
-
|
|
485
|
-
Every need is covered, or she is absent. An absent being answers silence
|
|
486
|
-
and keeps her cells, and answers again once an offer covers her needs.
|
|
487
|
-
|
|
488
|
-
A standing is matched to a need by rules two to four alone. A describe
|
|
489
|
-
names no blueprint, so the need's name is hers to choose there.
|
|
490
|
-
|
|
491
|
-
A need she forgot to declare is a member she does not hold, and the call
|
|
492
|
-
throws inside her ask, which answers only `the ask failed`. So check your
|
|
493
|
-
classes with `npx tsc --noEmit` beside your tests, which run with
|
|
494
|
-
`node --test`.
|
|
495
|
-
|
|
496
|
-
### Testing on the bench
|
|
497
|
-
|
|
498
|
-
The bench opens two houses on one ground in memory, on fake memory,
|
|
499
|
-
keys and clock. A being of the bench's own house asks hers through a
|
|
500
|
-
door, so every ask crosses as it would in production.
|
|
501
|
-
|
|
502
|
-
```ts
|
|
503
|
-
// order.test.ts
|
|
504
|
-
import assert from 'node:assert/strict';
|
|
505
|
-
import { test } from 'node:test';
|
|
506
|
-
import { Bench } from 'nervur/bench';
|
|
507
|
-
import { Order } from './classes/order.ts';
|
|
508
|
-
import { Payments, paymentsOffer } from './payments.ts';
|
|
509
|
-
|
|
510
|
-
test('Order keeps her table: every state and role shows what it owes', async () => {
|
|
511
|
-
await Bench.check(Order);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
test('An order is paid once the provider calls the handle it was given', async () => {
|
|
515
|
-
const payments = new Payments();
|
|
516
|
-
const bench = await Bench.open({ classes: [Order], offers: [paymentsOffer(payments)] });
|
|
517
|
-
const order = await bench.place(Order, { id: 'first' });
|
|
518
|
-
|
|
519
|
-
assert.deepEqual(await order.ask('checkout'), { error: { message: 'Add an item first.' } });
|
|
520
|
-
assert.deepEqual(await order.ask('add', { sku: 'tea', price: 4 }), { result: { total: 4 } });
|
|
521
|
-
assert.deepEqual(await order.ask('checkout'), { result: null });
|
|
522
|
-
await bench.settle();
|
|
523
|
-
assert.equal((await order.describe())?.state, 'paying', 'the charge left once checkout landed, and answered pending');
|
|
524
|
-
|
|
525
|
-
assert.deepEqual(await payments.settle('first'), { result: null });
|
|
526
|
-
await bench.settle();
|
|
527
|
-
assert.equal((await order.describe())?.state, 'paid');
|
|
528
|
-
assert.deepEqual(await order.ask('add', { sku: 'jam', price: 1 }), { error: { message: 'not in this state' } });
|
|
529
|
-
});
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
`place(Class, { id, born })` has the bench's steward bear a being. A
|
|
533
|
-
placed being is asked with `ask(method, args, { role })` and described
|
|
534
|
-
with `describe({ role })`. Test her through her asks first, as every
|
|
535
|
-
asker meets her. `cells()` reads her cells through the hand, as her owner
|
|
536
|
-
inspects them. An effect answers with the reply it brings once it lands.
|
|
537
|
-
`settle()` lets every effect and reply run, and `advance(ms)` moves the
|
|
538
|
-
fake clock.
|
|
539
|
-
|
|
540
|
-
The bench plays a role as an owner could, judged on her cells as they
|
|
541
|
-
stand. `root`, and a role root holds, is the hand. `steward` is the
|
|
542
|
-
bench's steward, and `being` a being it introduces to her. Any other
|
|
543
|
-
role is an occupant the bench's steward invites, with the role `true` in
|
|
544
|
-
its steward notes. A role read from her own notes is hers to grant, and
|
|
545
|
-
a handle only her own ask mints.
|
|
546
|
-
|
|
547
|
-
A steward and a public being are placed where the house places them.
|
|
548
|
-
`Bench.open({ steward: Shop, public: Lobby, classes: [Order] })` opens the
|
|
549
|
-
author's house with them there, and `place(Shop)` and `place(Lobby)` find
|
|
550
|
-
them. On the public being, a role a stranger holds is played by a being
|
|
551
|
-
of the bench's own house, asking as a stranger. Beside a steward the test
|
|
552
|
-
brings, the bench plays `root` and `stranger` alone, and places no other
|
|
553
|
-
being. A world of your steward and the beings she bears is tested on a
|
|
554
|
-
BenchGround, as [Faces](FACES.md) tests its desk.
|
|
555
|
-
`Bench.check(Shop, { position: 'steward' })` and `Bench.check(Lobby, {
|
|
556
|
-
position: 'public' })` check them.
|
|
557
|
-
|
|
558
|
-
An entry's `examples` are tests the bench runs. Each is a history, then
|
|
559
|
-
one ask, on a fresh bench. `given` lists the asks of hers that bring her
|
|
560
|
-
from her `born` to where the example starts. `role` asks, `args` are the
|
|
561
|
-
ask's, `fakes` answer her needs, and `gives` is the answer owed.
|
|
562
|
-
`Bench.check` runs every example twice from one seed and flags a class
|
|
563
|
-
that answers differently, or leaves her cells differently. It then
|
|
564
|
-
describes every state to every role it plays, and names every finding
|
|
565
|
-
that failed.
|
|
566
|
-
|
|
567
|
-
## A faculty
|
|
568
|
-
|
|
569
|
-
A faculty is anything a being may call that is not a being: a payment
|
|
570
|
-
provider, a mail sender, a model, a sensor. The ground hands it to the
|
|
571
|
-
house as an offer, and the house matches it to every need it covers.
|
|
572
|
-
|
|
573
|
-
```ts
|
|
574
|
-
// payments.ts
|
|
575
|
-
import type { FacultyContext, Offer } from 'nervur';
|
|
576
|
-
import { need, s } from 'nervur/being';
|
|
577
|
-
|
|
578
|
-
/** What the faculty offers. An order's need is covered by it. */
|
|
579
|
-
export const PaymentsBlueprint = need('payments', {
|
|
580
|
-
charge: {
|
|
581
|
-
description: 'Charges an order, and calls notify once the money arrives.',
|
|
582
|
-
args: s.object({ order: s.string(), amount: s.number(), notify: s.handle() }),
|
|
583
|
-
result: s.object({ pending: s.boolean() }),
|
|
584
|
-
},
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
type Answer = { result: { pending: boolean } } | { error: { message: string } };
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* A payment provider in the ground's process. It answers a call id it has
|
|
591
|
-
* seen with the answer it gave, so an effect sent twice charges once. A
|
|
592
|
-
* provider that changes the world keeps these in the memory its faculty's
|
|
593
|
-
* `up` receives, where a restart and a move keep them.
|
|
594
|
-
*/
|
|
595
|
-
export class Payments {
|
|
596
|
-
readonly #answered = new Map<string, Answer>();
|
|
597
|
-
readonly #waiting = new Map<string, { notify: string; context: FacultyContext }>();
|
|
598
|
-
|
|
599
|
-
async charge({ order, amount, notify }: { order: string; amount: number; notify: string }, context: FacultyContext): Promise<Answer> {
|
|
600
|
-
const seen = this.#answered.get(context.id);
|
|
601
|
-
if (seen !== undefined) return seen;
|
|
602
|
-
const answer: Answer = amount > 0 ? { result: { pending: true } } : { error: { message: 'Nothing to charge.' } };
|
|
603
|
-
if (amount > 0) this.#waiting.set(order, { notify, context });
|
|
604
|
-
this.#answered.set(context.id, answer);
|
|
605
|
-
return answer;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/** The money for an order arrived: the provider calls the handle it was given. */
|
|
609
|
-
settle(order: string) {
|
|
610
|
-
const waiting = this.#waiting.get(order);
|
|
611
|
-
if (waiting === undefined) throw new Error(`no charge waits for ${order}`);
|
|
612
|
-
this.#waiting.delete(order);
|
|
613
|
-
return waiting.context.call({ token: waiting.notify, id: `settle:${order}` });
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/** The offer a ground hands: the blueprint, the object, and a week's memory of call ids. */
|
|
618
|
-
export const paymentsOffer = (payments: Payments): Offer => ({ blueprint: PaymentsBlueprint, object: payments, window: 7 * 86_400_000 });
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
An offer is `{ blueprint, object, kinds?, window? }`. The object answers
|
|
622
|
-
each call with its call id, and one that changes the world answers a
|
|
623
|
-
call id it has seen with the answer it gave. A registry holds each
|
|
624
|
-
faculty by name, as `{ up, install? }`. The ground raises a faculty by
|
|
625
|
-
its `up` when an entry names it, and the entry's `kinds` grants it to
|
|
626
|
-
the classes it names. [Writing a faculty](FACULTIES.md) teaches the
|
|
627
|
-
craft whole, with a faculty written in Python.
|
|
628
|
-
|
|
629
|
-
```ts
|
|
630
|
-
// recipe.ts
|
|
631
|
-
import { Payments, paymentsOffer } from './payments.ts';
|
|
632
|
-
|
|
633
|
-
// A registry: the ground raises each faculty an entry names by its `up` here.
|
|
634
|
-
export const faculties = {
|
|
635
|
-
payments: { up: () => paymentsOffer(new Payments()) },
|
|
636
|
-
};
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
## A ground
|
|
640
|
-
|
|
641
|
-
A ground is the process houses run in, and you write none. `nervur up`
|
|
642
|
-
runs one on a folder of code: a folder for each house, and each module
|
|
643
|
-
your entries name. It holds one key in `state/`, and keeps every entry
|
|
644
|
-
sealed as the cells of its dock's beings: which faculties stand, and
|
|
645
|
-
which houses open on which bodies.
|
|
646
|
-
|
|
647
|
-
### The shop's folder
|
|
648
|
-
|
|
649
|
-
```text
|
|
650
|
-
nervur-ground/
|
|
651
|
-
recipe.ts
|
|
652
|
-
payments.ts
|
|
653
|
-
classes/index.ts, order.ts, shop.ts, lobby.ts
|
|
654
|
-
state/ made by the ground, its owner's alone
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
The folder is a package of ECMAScript modules, so Node reads its
|
|
658
|
-
TypeScript as it is written, with no build step.
|
|
659
|
-
|
|
660
|
-
```bash
|
|
661
|
-
npm init -y && npm pkg set type=module && npm install nervur
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
A house's folder names what the house holds.
|
|
665
|
-
|
|
666
|
-
```ts
|
|
667
|
-
// classes/index.ts
|
|
668
|
-
export { Shop as steward } from './shop.ts';
|
|
669
|
-
export { Lobby as public } from './lobby.ts';
|
|
670
|
-
import { Order } from './order.ts';
|
|
671
|
-
|
|
672
|
-
export const beings = [Order];
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
### Running it
|
|
676
|
-
|
|
677
|
-
```bash
|
|
678
|
-
npx nervur up .
|
|
679
|
-
```
|
|
680
|
-
|
|
681
|
-
The ground boots in one order, and a stop is that order reversed, on an
|
|
682
|
-
interrupt and on `SIGTERM`.
|
|
683
|
-
|
|
684
|
-
1. **Primordial.** Its ledger goes up and takes the lock on its state,
|
|
685
|
-
so one ground runs on it. Its unlock, crypto and tools go up. Its key
|
|
686
|
-
is read from `state/key`, drawn there on the first start.
|
|
687
|
-
2. **The ground's work.** The library's `ground` faculty goes up, which
|
|
688
|
-
the dock alone reaches.
|
|
689
|
-
3. **Dock.** The ground's own house opens in the ledger, under a seed
|
|
690
|
-
its key derives. Its beings hold every entry, seed, secret and
|
|
691
|
-
setting as their cells.
|
|
692
|
-
4. **Ladder.** The dock's steward joins your entries to the defaults its
|
|
693
|
-
code fixes, yours winning by name. Each entry is held to what its
|
|
694
|
-
faculty takes. Each body is installed where its entry is new, and
|
|
695
|
-
goes up. One that fails stays down, and says why.
|
|
696
|
-
5. **Houses.** Each house opens on its bodies, and its door joins the
|
|
697
|
-
carry.
|
|
698
|
-
6. **Hand.** The hand takes its socket, and the ground tells systemd it
|
|
699
|
-
is up.
|
|
700
|
-
|
|
701
|
-
Its environment names what opens its memory, its key and its hand, and
|
|
702
|
-
nothing else.
|
|
703
|
-
|
|
704
|
-
| Variable | What it names |
|
|
705
|
-
| --- | --- |
|
|
706
|
-
| `NERVUR_STATE` | its state, `state/` in its folder where unset |
|
|
707
|
-
| `NERVUR_UNLOCK` | `keychain:<account>` keeps its key in the macOS keychain, in place of `state/key` |
|
|
708
|
-
| `NERVUR_HAND` | its hand's socket, `state/hand` where unset |
|
|
709
|
-
|
|
710
|
-
Every other setting is an arg of an entry its dock keeps, set through
|
|
711
|
-
the hand. Its `tcp` entry listens on every address at 9110. Its `web`
|
|
712
|
-
entry serves the ground's one listener over HTTP, for Quo over the web
|
|
713
|
-
and every handler, once its args name a `port`. It names that listener
|
|
714
|
-
in its `faculties`. Both take `bind`, the public `addresses` written
|
|
715
|
-
into invitations, and `allowPrivate` to dial loopback addresses, as two
|
|
716
|
-
grounds on one machine do. `web` also takes the `origins` of pages it
|
|
717
|
-
answers. `wait set` keeps its bound on every ask.
|
|
718
|
-
|
|
719
|
-
```bash
|
|
720
|
-
npx nervur faculties update name=web make=web faculties='["listener"]' args='{"port":8080}'
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
The state holds the key in a file its owner alone reads, and the
|
|
724
|
-
ground's ledger. Every house keeps its rows in that ledger, sealed. A
|
|
725
|
-
ledger appends every write and never rewrites one. Its witness keeps
|
|
726
|
-
where it last stood, and a ledger behind its witness is refused. So a
|
|
727
|
-
ledger restored alone cannot replay what a house already answered,
|
|
728
|
-
though a whole state folder restored with its witness is not seen. A
|
|
729
|
-
lost key is a lost ground.
|
|
730
|
-
|
|
731
|
-
The ladder stands once, and the ground stands it again at every start.
|
|
732
|
-
The first entry stands the folder's `recipe.ts` as a registry, by the
|
|
733
|
-
faculty `module` the folder's registry holds. The second raises the
|
|
734
|
-
payments from it. An entry names only faculties that stand already.
|
|
735
|
-
|
|
736
|
-
```bash
|
|
737
|
-
npx nervur faculties add name=recipe from=folder make=module args='{"at":"recipe.ts"}'
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
```bash
|
|
741
|
-
npx nervur faculties add name=payments from=recipe make=payments
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
A house is added once, and the ground opens it again at every start.
|
|
745
|
-
|
|
746
|
-
```bash
|
|
747
|
-
npx nervur houses add name=shop classes='{"faculty":"folder","at":"classes"}' faculties='["payments"]'
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
`classes` names the faculty whose body serves the house's code. `folder`
|
|
751
|
-
is the ground's own, and a registry may add more, a git repository among
|
|
752
|
-
them. A house keeps its rows in the ground's ledger unless its entry
|
|
753
|
-
names a `memory` faculty. `faculties` names what the house receives, and
|
|
754
|
-
within it each faculty's entry names in `kinds` the classes that hold
|
|
755
|
-
it. `houses update` and `faculties update` land a new entry in one
|
|
756
|
-
write, and `faculties restart` takes a body down and up again.
|
|
757
|
-
|
|
758
|
-
A secret reaches a faculty the same way, and never through the
|
|
759
|
-
environment. `npx nervur secrets set -` reads `{ name, value }` from
|
|
760
|
-
standard input into a being of the dock, whose cells no one reads. A
|
|
761
|
-
faculty's entry names the secrets its `up` receives in `secrets`.
|
|
762
|
-
|
|
763
|
-
### The hand
|
|
764
|
-
|
|
765
|
-
The command is a face on the ground's hand. It holds no word of its own
|
|
766
|
-
beyond `up` and `service`, and reads every other from what the ground
|
|
767
|
-
describes.
|
|
768
|
-
|
|
769
|
-
```bash
|
|
770
|
-
npx nervur help
|
|
771
|
-
```
|
|
772
|
-
|
|
773
|
-
Two words ask the dock, the ground's own steward, so `houses list` asks
|
|
774
|
-
her `housesList`. A faculty named alone shows its methods, and a method
|
|
775
|
-
after it is called through the dock. Args are one JSON object, or words
|
|
776
|
-
`key=value`.
|
|
777
|
-
|
|
778
|
-
```bash
|
|
779
|
-
npx nervur houses list
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
`ask` asks a being in a house as `root`: the steward, or the being
|
|
783
|
-
`--id` names. The owner opens an order through the steward, and hands a
|
|
784
|
-
courier's paper to it the same way. The steward's `hire` carries the
|
|
785
|
-
paper unopened, and the order takes it.
|
|
786
|
-
|
|
787
|
-
```bash
|
|
788
|
-
npx nervur ask shop open id=first
|
|
789
|
-
```
|
|
790
|
-
|
|
791
|
-
```bash
|
|
792
|
-
npx nervur ask shop hire order=first courier=7b22…
|
|
793
|
-
```
|
|
794
|
-
|
|
795
|
-
`--id` names the being asked in the steward's place, as `npx nervur ask
|
|
796
|
-
shop --id first` shows the order's describe. `--cells` reads her cells
|
|
797
|
-
and asks nothing, as `npx nervur ask shop --id first --cells`. No one
|
|
798
|
-
but the owner reads them, and only her own asks write them.
|
|
799
|
-
|
|
800
|
-
Each prints one JSON value. It exits 0 on a result and 1 on an error
|
|
801
|
-
answered. It exits 2 where nothing was asked, so a script tells a
|
|
802
|
-
refusal from a ground that is down. The hand is `state/hand` in the
|
|
803
|
-
folder where the command runs, or the socket `--at` or `NERVUR_HAND`
|
|
804
|
-
names.
|
|
805
|
-
|
|
806
|
-
### Running it as a service
|
|
807
|
-
|
|
808
|
-
`nervur service` writes the unit that runs a folder: a systemd user unit
|
|
809
|
-
on Linux, started again after any exit, and a launchd agent on macOS.
|
|
810
|
-
|
|
811
|
-
```bash
|
|
812
|
-
npx nervur service . > ~/.config/systemd/user/nervur-ground.service
|
|
813
|
-
```
|
|
814
|
-
|
|
815
|
-
```bash
|
|
816
|
-
systemctl --user enable --now nervur-ground
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
A user unit stops when its user logs out, unless lingering is enabled
|
|
820
|
-
with `loginctl enable-linger`. On macOS, the agent goes to
|
|
821
|
-
`~/Library/LaunchAgents/` and is started with `launchctl bootstrap`.
|
|
822
|
-
|
|
823
|
-
### In a page
|
|
824
|
-
|
|
825
|
-
`BrowserGround.open({ registry })` from `nervur/browser` runs the same
|
|
826
|
-
houses in a page. Every tab and the service worker of one origin share
|
|
827
|
-
one ground: the one holding the Web Lock runs it, and the others reach
|
|
828
|
-
its hand over a `BroadcastChannel`. When it closes, the next opens the
|
|
829
|
-
ground from the same storage. `hand` takes the same requests the
|
|
830
|
-
command sends: `describe`, and an ask of a being in a named house. A
|
|
831
|
-
request that names no house asks the dock's steward, as
|
|
832
|
-
`{ method: 'housesList' }`.
|
|
833
|
-
|
|
834
|
-
Its bodies are the browser's. The ground's key is sealed under an AES
|
|
835
|
-
key that IndexedDB holds unextractable, and its memory is IndexedDB.
|
|
836
|
-
Classes load from the page's own origin through the `origin` faculty, `{
|
|
837
|
-
faculty: 'origin', at: '/house/index.js' }`, and a path off the origin
|
|
838
|
-
is refused. The registry is the object you pass: faculties by name,
|
|
839
|
-
foundation and custom alike, beside the terrain's own.
|
|
840
|
-
|
|
841
|
-
Any script on the origin can use the ground's keys, so the ground is
|
|
842
|
-
whoever serves the origin's script. Give it an origin of its own, serve
|
|
843
|
-
nothing a stranger wrote there, and set a strict content security
|
|
844
|
-
policy. The page and the house's module must share one copy of
|
|
845
|
-
`nervur/being`, since a house knows a class by a mark that module gives.
|
|
846
|
-
A bundler's shared chunk or an import map gives the one copy.
|
|
847
|
-
|
|
848
|
-
A service worker opens the ground the same way, on a push, where no tab
|
|
849
|
-
runs it. It may not `import()`, so hand it the modules it imported
|
|
850
|
-
itself: `platform: { load: (href) => modules[new URL(href).pathname] }`.
|
|
851
|
-
|
|
852
|
-
### In an app
|
|
853
|
-
|
|
854
|
-
`AppGround.open({ shell })` from `nervur/app` is a BrowserGround in the
|
|
855
|
-
app's web view, on two interfaces the shell fills in native code:
|
|
856
|
-
|
|
857
|
-
- `NativeSecrets`: `get(name)` and `set(name, value)`, text kept by the
|
|
858
|
-
iOS Keychain or the Android Keystore on this device alone.
|
|
859
|
-
- `NativeStore`: `get(key)`, `keys(prefix)`, and `swap(writes, expect)`,
|
|
860
|
-
which lands every write only where each key in `expect` still holds
|
|
861
|
-
what it names, and answers whether it landed.
|
|
862
|
-
|
|
863
|
-
The library holds the rest. The ground's key goes into the secret store,
|
|
864
|
-
and its memory into the native store, which the system never evicts as
|
|
865
|
-
it may a web view's storage. A push token reaches the ground through a
|
|
866
|
-
faculty your registry holds.
|
|
867
|
-
|
|
868
|
-
## What the house guarantees
|
|
869
|
-
|
|
870
|
-
1. **Every need is covered, or she is absent.** An absent being answers
|
|
871
|
-
silence and keeps her cells.
|
|
872
|
-
2. **The asker's id is true.** Wherever the asker lives, the id she reads
|
|
873
|
-
is who asked.
|
|
874
|
-
3. **Her notes are hers.** No one else writes them, and she never writes
|
|
875
|
-
her steward's.
|
|
876
|
-
4. **She never holds an invitation's bytes.** Handles leave, standing ids
|
|
877
|
-
arrive, and `s.invitation` passes through unopened.
|
|
878
|
-
5. **One ask at a time.** Asks to one being run in order, and `readOnly`
|
|
879
|
-
asks run beside them.
|
|
880
|
-
6. **An ask lands whole or not at all.** Her cells, her relations, her
|
|
881
|
-
effects and her call ids land in one write.
|
|
882
|
-
7. **An effect acts at most once, and its outcome is known.** Its answer,
|
|
883
|
-
or its giving up, reaches her through `reply`.
|
|
884
|
-
8. **Her class is checked before her first ask.** A class that fails
|
|
885
|
-
leaves her absent, and the refusal says why.
|
|
886
|
-
|
|
887
|
-
A failed ask changed nothing she owns, so asking again is safe.
|
|
888
|
-
|
|
889
|
-
## What the house refuses
|
|
890
|
-
|
|
891
|
-
Each refusal answers why, where it is met: at her class's first resolve,
|
|
892
|
-
at her call, or where an ask arrives.
|
|
893
|
-
|
|
894
|
-
- A being reaching anything her position, her needs and `this.house` do
|
|
895
|
-
not give.
|
|
896
|
-
- A ground's own references handed to a being, or offered to her as a
|
|
897
|
-
faculty.
|
|
898
|
-
- A handle or an invitation in her cells.
|
|
899
|
-
- Her cells read by anyone but the owner's hand.
|
|
900
|
-
- A standing she mints herself. Standings arrive from the house.
|
|
901
|
-
- A far standing asked in the ask that took it. She asks it from her next
|
|
902
|
-
ask.
|
|
903
|
-
- A write by anyone else to her notes, and by her to her steward's.
|
|
904
|
-
- An occupant id the house reserves, and one she already holds.
|
|
905
|
-
- Two member names that clash.
|
|
906
|
-
- An ask with no entry.
|
|
907
|
-
- A state no ask reaches, an ask no role reaches, and a role unused.
|
|
908
|
-
- A method landing in a state its `to` does not name.
|
|
909
|
-
- A `readOnly` ask that writes.
|
|
910
|
-
- A need and an offer that disagree on `idempotent`.
|
|
911
|
-
- A schema keyword outside the subset `s` writes.
|
|
912
|
-
- Two offers covering one need for one kind, and a kind two sources
|
|
913
|
-
claim.
|
|
914
|
-
- An effect sent before its ask landed, or sent again while a call for
|
|
915
|
-
it is pending.
|
|
916
|
-
- An ask a stranger reaches that is not idempotent.
|
|
917
|
-
- An invite from a public being.
|
|
918
|
-
- A seed inside the house, and a seed that is not sixty-four hex digits.
|
|
919
|
-
- A memory opened with keys that derive another bound.
|
|
920
|
-
- A send to a private address, unless its ground allows it.
|
|
921
|
-
- A call on the house beside `House.open`, `door` and `ask`.
|
|
922
|
-
- A faculty in a house's code. Faculties are the ground's.
|
|
923
|
-
- A drawer opened with a key that did not seal it.
|
|
924
|
-
- A secret read by a being, a describe or anyone but a faculty whose
|
|
925
|
-
entry names it.
|
|
926
|
-
- A faculty raised any way but its `up`, foundation or custom.
|
|
927
|
-
- An entry named `dock`, for a house or a faculty. The dock is the
|
|
928
|
-
library's.
|
|
929
|
-
- A secret or a move asked by anyone but the hand.
|
|
930
|
-
- A grant naming a faculty or a secret the ground does not hold.
|
|
931
|
-
- A secret's cells read through the hand.
|
|
932
|
-
- An entry naming, making or granting `ground` or `shell`. Both are the
|
|
933
|
-
dock's alone.
|
|
934
|
-
- A second ground on the state one runs on.
|
|
935
|
-
- A ground an owner must write. Each terrain's ships.
|