nervur 0.23.0 → 0.23.1-2
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/AUTHORING.md +614 -636
- package/COMMAND.md +2 -2
- package/FACES.md +25 -25
- package/FACULTIES.md +232 -49
- package/GROUNDS.md +72 -42
- package/KIT-SPEC.md +26 -14
- package/README.md +15 -10
- package/dist/app/app-ground.d.ts +4 -2
- package/dist/app/app-ground.js +2 -1
- package/dist/being/being.d.ts +47 -36
- package/dist/being/being.js +1 -1
- package/dist/being/covers.js +1 -1
- package/dist/being/dock-pilot.d.ts +305 -0
- package/dist/being/dock-pilot.js +69 -0
- package/dist/being/index.d.ts +2 -1
- package/dist/being/index.js +1 -0
- package/dist/being/need.d.ts +11 -6
- package/dist/being/need.js +4 -8
- package/dist/being/table.d.ts +9 -3
- package/dist/being/table.js +29 -13
- package/dist/bench/bench-ground.d.ts +19 -13
- package/dist/bench/bench-ground.js +87 -34
- package/dist/bench/bench.d.ts +86 -13
- package/dist/bench/bench.js +694 -98
- package/dist/bench/settle.d.ts +9 -1
- package/dist/bench/settle.js +81 -2
- package/dist/bench/stand-in.d.ts +17 -0
- package/dist/bench/stand-in.js +56 -0
- package/dist/bench/stewards.d.ts +26 -28
- package/dist/bench/stewards.js +58 -29
- package/dist/bodies/class-list.d.ts +13 -1
- package/dist/bodies/class-list.js +59 -5
- package/dist/bodies/noble-crypto.d.ts +7 -2
- package/dist/bodies/noble-crypto.js +6 -1
- package/dist/bodies/strict-tools.d.ts +7 -2
- package/dist/bodies/strict-tools.js +9 -1
- package/dist/bodies/web-clock.d.ts +7 -2
- package/dist/bodies/web-clock.js +8 -1
- package/dist/browser/browser-ground.d.ts +13 -5
- package/dist/browser/browser-ground.js +49 -61
- package/dist/browser/origin-classes.d.ts +6 -4
- package/dist/browser/origin-classes.js +11 -11
- package/dist/edge/edge-ground.d.ts +7 -5
- package/dist/edge/edge-ground.js +51 -55
- package/dist/faculty.d.ts +146 -0
- package/dist/faculty.js +71 -0
- package/dist/foundation.d.ts +32 -0
- package/dist/foundation.js +13 -1
- package/dist/ground/dock.d.ts +400 -442
- package/dist/ground/dock.js +531 -157
- package/dist/ground/entry.d.ts +1 -0
- package/dist/ground/entry.js +31 -0
- package/dist/ground/forwarding.d.ts +28 -0
- package/dist/ground/forwarding.js +60 -0
- package/dist/ground/ground.d.ts +51 -68
- package/dist/ground/ground.js +619 -191
- package/dist/ground/inside.d.ts +25 -0
- package/dist/ground/inside.js +265 -0
- package/dist/ground/runner.d.ts +32 -0
- package/dist/ground/runner.js +418 -0
- package/dist/house/house.d.ts +19 -2
- package/dist/house/house.js +376 -112
- package/dist/house/rows-shape.d.ts +15 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +5 -2
- package/dist/node/bridge.d.ts +2 -2
- package/dist/node/folder-classes.d.ts +4 -2
- package/dist/node/folder-classes.js +9 -23
- package/dist/node/node-ground.d.ts +2 -2
- package/dist/node/node-ground.js +75 -98
- package/dist/node/shell.d.ts +2 -2
- package/package.json +27 -18
- package/source.mjs +56 -0
- package/test.mjs +151 -0
- package/tsconfig.base.json +29 -0
package/AUTHORING.md
CHANGED
|
@@ -1,45 +1,46 @@
|
|
|
1
|
-
# Writing
|
|
1
|
+
# Writing a species
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
3
|
+
A species is a class of beings you write. This guide teaches it whole:
|
|
4
|
+
how to declare one, how she calls others, how beings compose, and how you
|
|
5
|
+
prove her. An author who finishes it writes and proves a species alone.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
You meet the library at three moments. Writing a species is this guide.
|
|
8
|
+
Writing a faculty, which reaches the world outside, is
|
|
9
|
+
[Writing a faculty](FACULTIES.md). Running a ground, the process houses
|
|
10
|
+
run in, is [Grounds](GROUNDS.md) and [the command](COMMAND.md).
|
|
11
|
+
[Reading a world](WORLD.md) says which pieces a situation needs.
|
|
12
|
+
|
|
13
|
+
The examples build one small shop, and every file here is one the
|
|
14
|
+
package's own tests run.
|
|
10
15
|
|
|
11
16
|
- `Order` is one order, from its first item to its shipping.
|
|
12
|
-
- `Shop` is the steward, the being
|
|
13
|
-
- `Lobby` is the public being,
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
- `Shop` is the steward, the being who runs the house.
|
|
18
|
+
- `Lobby` is the public being, whom strangers may ask.
|
|
19
|
+
|
|
20
|
+
## A being is five things
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
A being is an instance of a species. She is five things, and nothing
|
|
23
|
+
else reaches her.
|
|
18
24
|
|
|
19
|
-
|
|
|
25
|
+
| Part | What it is |
|
|
20
26
|
| --- | --- |
|
|
21
|
-
| being | an instance of a class, born for one ask and dropped after it |
|
|
22
27
|
| cells | her state, JSON values, kept when an ask lands |
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
##
|
|
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.
|
|
28
|
+
| asks | the methods others may call on her, each with its entry |
|
|
29
|
+
| occupants | edges in: who may ask her, each by id |
|
|
30
|
+
| standings | edges out: other beings she may ask, each by id |
|
|
31
|
+
| bodies | edges out: faculties she calls through her needs |
|
|
32
|
+
|
|
33
|
+
Two edges every being holds. `house` is a default body: the time, random
|
|
34
|
+
bytes and alarms. `root` is a default occupant: the owner, through the
|
|
35
|
+
house's hand. A normal being also holds her steward, as a standing and as
|
|
36
|
+
an occupant.
|
|
37
|
+
|
|
38
|
+
A position adds one edge. A steward is a being plus the body `powers`. A
|
|
39
|
+
public being is a being plus the occupant `stranger`, who stands for
|
|
40
|
+
every asker with no relation. Every other rule is the same, so a species
|
|
41
|
+
never asks where she was placed.
|
|
42
|
+
|
|
43
|
+
## Declaring a species
|
|
43
44
|
|
|
44
45
|
```ts
|
|
45
46
|
// classes/order.ts
|
|
@@ -72,7 +73,7 @@ export class Order extends Being.of({
|
|
|
72
73
|
asks: {
|
|
73
74
|
hire: {
|
|
74
75
|
for: 'steward',
|
|
75
|
-
|
|
76
|
+
idempotent: true,
|
|
76
77
|
args: s.object({ courier: s.handle() }),
|
|
77
78
|
result: s.string(),
|
|
78
79
|
},
|
|
@@ -84,7 +85,7 @@ export class Order extends Being.of({
|
|
|
84
85
|
result: s.object({ total: s.number() }),
|
|
85
86
|
},
|
|
86
87
|
checkout: { in: 'open', for: 'owner', to: 'paying' },
|
|
87
|
-
charged: { in: 'paying', args: s.reply(Payments.charge), to: ['paying', 'open'] },
|
|
88
|
+
charged: { in: 'paying', for: 'pay', args: s.reply(Payments.charge), to: ['paying', 'open'] },
|
|
88
89
|
settled: { in: 'paying', for: 'handle', to: 'paid' },
|
|
89
90
|
ship: { in: 'paid', for: 'owner', to: 'shipped' },
|
|
90
91
|
},
|
|
@@ -129,194 +130,199 @@ The order wrote no retry, no catch, no key and no address. `charge` is
|
|
|
129
130
|
an effect, so it leaves only once `checkout` has landed. Its answer comes
|
|
130
131
|
back to `charged`, and a refused charge opens the order again. The
|
|
131
132
|
provider calls `settled` through the handle once the money arrives.
|
|
132
|
-
`
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
### The declaration
|
|
133
|
+
`ship` asks the courier through the standing `hire` took. `shipped` is a
|
|
134
|
+
terminal state, and nothing leaves it.
|
|
136
135
|
|
|
137
|
-
A
|
|
138
|
-
|
|
136
|
+
A species extends `Being.of({ … })`, and TypeScript reads her types from
|
|
137
|
+
that one object. Every field is optional but `kind` and `asks`.
|
|
139
138
|
|
|
140
139
|
| Field | What it is |
|
|
141
140
|
| --- | --- |
|
|
142
|
-
| `kind` | the
|
|
141
|
+
| `kind` | her name for the house: a reversed domain you own, then a name |
|
|
143
142
|
| `description` | one line for readers and agents |
|
|
144
|
-
| `cells` |
|
|
143
|
+
| `cells` | her state and its defaults |
|
|
145
144
|
| `needs` | blueprints she calls, each under a member name she chooses |
|
|
146
145
|
| `roles` | named tests over the asker and her cells |
|
|
147
|
-
| `state` | a function
|
|
146
|
+
| `state` | a function naming her current state; omitted, `ready` alone |
|
|
148
147
|
| `asks` | one entry per method she answers |
|
|
149
|
-
| `view` | markup as text over her asks, which a screen renders |
|
|
148
|
+
| `view` | markup as text over her asks, at most 64 KiB, which a screen renders |
|
|
150
149
|
|
|
151
|
-
A
|
|
152
|
-
|
|
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.
|
|
150
|
+
A method in TypeScript names its args with `Args<Class, 'method'>`. A
|
|
151
|
+
method in JavaScript writes nothing.
|
|
163
152
|
|
|
164
153
|
### An ask's entry
|
|
165
154
|
|
|
166
155
|
| Field | What it says |
|
|
167
156
|
| --- | --- |
|
|
157
|
+
| `for` | the roles that may call it |
|
|
168
158
|
| `in` | the states where the ask exists; omitted, every state |
|
|
169
|
-
| `for` | the roles that may call it; omitted, every occupant but handles |
|
|
170
159
|
| `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
|
|
160
|
+
| `args` | the schema of what it takes; omitted, the empty object |
|
|
172
161
|
| `result` | the schema of what it answers; omitted, nothing |
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
162
|
+
| `readOnly` | it writes nothing, and implies `idempotent`; the house holds it |
|
|
163
|
+
| `idempotent` | it is safe to repeat, so its caller awaits it |
|
|
164
|
+
| `hints` | `{ destructive }`, the one hint, which changes nothing in the house |
|
|
165
|
+
| `replayable` | your word that a stranger may repeat it, though it is not idempotent |
|
|
166
|
+
| `wait` | milliseconds she may run; omitted, thirty seconds, and never past five minutes |
|
|
167
|
+
| `examples` | histories the bench runs, each ending in one ask and what it gives |
|
|
175
168
|
| `description` | one line for readers and agents |
|
|
176
|
-
| `wait` | milliseconds she may run; omitted, thirty seconds |
|
|
177
169
|
|
|
178
|
-
`in`, `for` and `to` each take one name or a list
|
|
179
|
-
|
|
180
|
-
|
|
170
|
+
`in`, `for` and `to` each take one name or a list. A method with no entry
|
|
171
|
+
is never reached, and an entry with no method refuses the species.
|
|
172
|
+
|
|
173
|
+
### Roles
|
|
174
|
+
|
|
175
|
+
A role names who may ask. An omitted `for` means every occupant, and
|
|
176
|
+
never an edge out of her. Five roles are the house's, for occupants.
|
|
177
|
+
|
|
178
|
+
- `root` is the owner, through the hand.
|
|
179
|
+
- `steward` is her steward.
|
|
180
|
+
- `being` is a being her steward introduced to her.
|
|
181
|
+
- `stranger` is any asker of a public being with no relation.
|
|
182
|
+
- `handle` is whoever holds a handle she minted to this ask.
|
|
183
|
+
|
|
184
|
+
The rest are edges out of her, which answer her and ask nothing else.
|
|
185
|
+
Each need's member is a role, held by the body answering her effect on
|
|
186
|
+
it. `standing` is held by a standing answering hers. `powers` is held by
|
|
187
|
+
a being her steward's powers asked, and `house` by the house. So the
|
|
188
|
+
order's `charged` says `for: 'pay'`, and no occupant fakes the payment's
|
|
189
|
+
answer.
|
|
190
|
+
|
|
191
|
+
Every other role is yours, a function of `(asker, me)`. The order's
|
|
192
|
+
`owner` reads the steward's notes on the asker, so only the steward
|
|
193
|
+
decides who owns an order.
|
|
194
|
+
|
|
195
|
+
The house checks the table when it first loads the species. It refuses a
|
|
196
|
+
state no ask reaches, an ask no role reaches, and a role no ask names. A
|
|
197
|
+
method that lands in a state outside its `to` fails, and nothing lands.
|
|
198
|
+
|
|
199
|
+
### Schemas and needs
|
|
200
|
+
|
|
201
|
+
One builder gives the schema and the TypeScript type: `s.object`,
|
|
202
|
+
`s.string`, `s.number`, `s.integer`, `s.boolean`, `s.array`, `s.enum`,
|
|
203
|
+
`s.const`, `s.bytes`, `s.optional`, `s.handle`, `s.invitation` and
|
|
204
|
+
`s.reply`. The house checks every ask against them.
|
|
205
|
+
|
|
206
|
+
- `s.bytes` is a `Uint8Array` in the process and hex across a door.
|
|
207
|
+
- `s.handle` carries a relation. A handle leaves as an invitation, and
|
|
208
|
+
an invitation arrives as a new standing id.
|
|
209
|
+
- `s.invitation` carries an invitation unopened, which she passes on.
|
|
210
|
+
- `s.reply(method)` is the args of an effect's reply.
|
|
211
|
+
|
|
212
|
+
A need is a blueprint at its minimum: what she calls, never what a
|
|
213
|
+
faculty offers. `need(name, methods)` writes one, and `needs` binds it to
|
|
214
|
+
a member of her own. A faculty covers it where the names match, every
|
|
215
|
+
method is offered, and each is `idempotent` in both or in neither. Every
|
|
216
|
+
need is covered, or she is absent: she answers silence and keeps her
|
|
217
|
+
cells until one is.
|
|
218
|
+
|
|
219
|
+
## How she calls
|
|
181
220
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
function of `(asker, me)`.
|
|
221
|
+
The callee decides how it is called, by one flag its describe shows. A
|
|
222
|
+
method marked `idempotent` or `readOnly` is harmless to repeat, so she
|
|
223
|
+
awaits it during her ask. Everything else changes the world, so she
|
|
224
|
+
commits it as an effect.
|
|
187
225
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
226
|
+
### Awaited calls answer as data
|
|
227
|
+
|
|
228
|
+
An awaited call never throws into her code. It answers a
|
|
229
|
+
`Reply<T>`, which is `{ result }` or `{ error: { message } }`. Silence
|
|
230
|
+
reads as an error saying the call answered nothing. She decides what an
|
|
231
|
+
error means.
|
|
232
|
+
|
|
233
|
+
- `this.must(reply)` gives the result, or fails her ask with the error.
|
|
234
|
+
- `this.fail(message)` refuses her ask with a message the asker can act
|
|
235
|
+
on. Nothing of her ask lands.
|
|
236
|
+
- `this.silence()` answers nothing, as an absent being does, and nothing
|
|
237
|
+
lands. A prober learns nothing from it.
|
|
238
|
+
|
|
239
|
+
A failed ask changed nothing she owns, so asking again is safe.
|
|
240
|
+
|
|
241
|
+
### Effects commit after she lands
|
|
242
|
+
|
|
243
|
+
An effect returns nothing. The house writes it in the same write as her
|
|
244
|
+
cells, then sends it with one call id until it is answered. So it acts
|
|
245
|
+
at most once, and never leaves where her ask failed. Its answer asks the
|
|
246
|
+
method `reply` names, as `{ result }` or `{ error }`. It gives up at its
|
|
247
|
+
deadline, seven days for a standing, and its reply hears why. Effects to
|
|
248
|
+
one receiver leave one at a time, in order.
|
|
249
|
+
|
|
250
|
+
### Who asks
|
|
251
|
+
|
|
252
|
+
Every ask arrives from one true id on her graph, never a disguise.
|
|
253
|
+
|
|
254
|
+
- An occupant asks as itself, and `this.asker` holds its id and notes.
|
|
255
|
+
- A reply comes from the edge that answered: a need's member, `standing`
|
|
256
|
+
or `powers`.
|
|
257
|
+
- An alarm comes from `house`, so its ask names `house` in its `for`.
|
|
258
|
+
- An effect to a stranger's door answers as `house`, since that door is
|
|
259
|
+
no edge of hers.
|
|
192
260
|
|
|
193
261
|
### What she reaches
|
|
194
262
|
|
|
195
263
|
| Member | What it is |
|
|
196
264
|
| --- | --- |
|
|
197
|
-
| `this.id` | her
|
|
198
|
-
| `this.
|
|
199
|
-
| `this.
|
|
200
|
-
| `this
|
|
201
|
-
| `this.
|
|
202
|
-
| `this.
|
|
203
|
-
| `this.
|
|
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 |
|
|
265
|
+
| `this.id`, `this.cells` | her id and her values |
|
|
266
|
+
| `this.asker` | `{ id, notes, steward }` of who asks now, and `signer` for a stranger |
|
|
267
|
+
| `this.house` | `now()`, `random({ length })`, `alarm({ at, ask, args, key })`, `cancelAlarm({ key })` |
|
|
268
|
+
| `this.<need>.<method>(args, { reply? })` | a call on a body |
|
|
269
|
+
| `this.held(id, Need).<ask>(args, { reply?, after? })` | a call on a standing matched to a need |
|
|
270
|
+
| `this.held(id).describe()`, `.ask(method, args)` | a standing she holds no need for, read first |
|
|
271
|
+
| `this.stranger({ ward, at }, Need).<ask>(args)` | a far house's public being, asked as a stranger |
|
|
210
272
|
| `this.standings` | `list()`, `note(id, notes)` and `drop(id)` |
|
|
273
|
+
| `this.occupants` | `list()`, `note(id, notes)` and `dismiss(id)` |
|
|
211
274
|
| `this.handle(ask, { bind?, notes?, once?, expires? })` | a handle to one of her asks |
|
|
212
275
|
| `this.invite(id, { notes?, expires? })` | a new occupant, as a handle |
|
|
213
|
-
| `this.occupants` | `list()`, `note(id, notes)` and `dismiss(id)` |
|
|
214
276
|
| `this.steward` | her steward, where she is not one |
|
|
215
|
-
| `this.powers` | the
|
|
216
|
-
| `this.fail
|
|
277
|
+
| `this.powers` | the steward's six powers |
|
|
278
|
+
| `this.fail`, `this.silence`, `this.must` | how an ask refuses or reads a reply |
|
|
217
279
|
|
|
218
280
|
An alarm lives in the house's rows and survives every restart. A second
|
|
219
|
-
alarm under one key replaces the first.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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.
|
|
281
|
+
alarm under one key replaces the first.
|
|
282
|
+
|
|
283
|
+
### Watching
|
|
284
|
+
|
|
285
|
+
A watch is a `readOnly` ask or body method asked with `after`, the
|
|
286
|
+
answer she holds. The house answers at once where the answer differs.
|
|
287
|
+
Otherwise it answers the first answer that differs, or the same one when
|
|
288
|
+
the wait runs out. So a chat or an order's status is a loop of watches,
|
|
289
|
+
and nothing polls.
|
|
290
|
+
|
|
291
|
+
From a being, a watch sits inside a `readOnly` ask of her own:
|
|
292
|
+
`this.held(room, Messages).messages({}, { after: seen })`. Given `reply`
|
|
293
|
+
beside `after`, it leaves as an effect, and its answer asks her `reply`,
|
|
294
|
+
where she watches again. A watch across a door holds its relation until
|
|
295
|
+
it answers. So watch a far ask through a handle to it alone.
|
|
296
|
+
|
|
297
|
+
## The graph
|
|
298
|
+
|
|
299
|
+
A relation is born in one of two ways. She mints an invitation, and
|
|
300
|
+
another takes it. Or her steward's `introduce` joins two beings of one
|
|
301
|
+
house. A being is born holding her steward and occupied by no one else.
|
|
302
|
+
|
|
303
|
+
Each end cuts its own side. `this.standings.drop` lets a standing go.
|
|
304
|
+
`this.occupants.dismiss` ends an occupant, and the far standing fails at
|
|
305
|
+
its next call with `the standing was removed`. A far side that answers
|
|
306
|
+
nothing is never read as gone.
|
|
307
|
+
|
|
308
|
+
A handle is the only way a relation leaves her. `this.handle(ask)` admits
|
|
309
|
+
its holder to that one ask. `once` dismisses it after its first ask, and
|
|
310
|
+
`bind` fixes args the holder cannot change. `this.invite(id)` mints a
|
|
311
|
+
whole occupant. `expires` gives each invitation that long to be taken. A
|
|
312
|
+
handle never enters her cells.
|
|
313
|
+
|
|
314
|
+
A standing arrives where an ask's args carry an invitation under
|
|
315
|
+
`s.handle`. Taking one is safe to repeat: the same invitation gives the
|
|
316
|
+
same standing. So the order's `hire` is `idempotent`. She asks a far
|
|
317
|
+
standing from the ask after the one that took it.
|
|
296
318
|
|
|
297
319
|
Every relation carries two sets of notes. `notes` are hers alone.
|
|
298
|
-
`steward` are her steward's,
|
|
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.
|
|
320
|
+
`steward` are her steward's, which she reads and never writes.
|
|
314
321
|
|
|
315
322
|
### The steward
|
|
316
323
|
|
|
317
|
-
Every house has one steward, with the id `steward`. The
|
|
318
|
-
|
|
319
|
-
forge. She alone holds `this.powers`.
|
|
324
|
+
Every house has one steward, with the id `steward`. The owner asks her as
|
|
325
|
+
`root`, and she alone holds `this.powers`.
|
|
320
326
|
|
|
321
327
|
```ts
|
|
322
328
|
// classes/shop.ts
|
|
@@ -334,17 +340,17 @@ export class Shop extends Being.of({
|
|
|
334
340
|
args: s.object({ id: s.string() }),
|
|
335
341
|
result: s.object({ owner: s.invitation() }),
|
|
336
342
|
},
|
|
337
|
-
orders: { for: 'pilot',
|
|
343
|
+
orders: { for: 'pilot', readOnly: true, result: s.array(s.string()) },
|
|
338
344
|
hire: {
|
|
339
345
|
for: 'pilot',
|
|
340
346
|
description: 'Hands a courier’s invitation to an order, which takes it.',
|
|
341
|
-
|
|
347
|
+
idempotent: true,
|
|
342
348
|
args: s.object({ order: s.string(), courier: s.invitation() }),
|
|
343
349
|
result: s.string(),
|
|
344
350
|
},
|
|
345
351
|
enroll: {
|
|
346
352
|
for: 'being',
|
|
347
|
-
|
|
353
|
+
idempotent: true,
|
|
348
354
|
args: s.object({ signer: s.bytes() }),
|
|
349
355
|
result: s.object({ invitation: s.invitation() }),
|
|
350
356
|
},
|
|
@@ -362,7 +368,7 @@ export class Shop extends Being.of({
|
|
|
362
368
|
|
|
363
369
|
// She carries the invitation unopened, and the order she names takes it.
|
|
364
370
|
async hire({ order, courier }: Args<Shop, 'hire'>) {
|
|
365
|
-
return (await this.powers!.ask({ id: order, method: 'hire', args: { courier } })) as string;
|
|
371
|
+
return this.must(await this.powers!.ask({ id: order, method: 'hire', args: { courier } })) as string;
|
|
366
372
|
}
|
|
367
373
|
|
|
368
374
|
// One order a signer: asked twice, the same id is borne once.
|
|
@@ -380,36 +386,21 @@ export class Shop extends Being.of({
|
|
|
380
386
|
| `bear({ kind, id, args })` | places a new being; the same id twice answers the first |
|
|
381
387
|
| `remove({ id })` | removes a being and everything of hers |
|
|
382
388
|
| `list()` | every being, her kind, whether she is absent, and her dead letters |
|
|
383
|
-
| `ask({ id, method, args }, { reply? })` | asks
|
|
389
|
+
| `ask({ id, method, args }, { reply? })` | asks a being as her occupant `steward` |
|
|
384
390
|
| `introduce({ from, to, notes })` | gives `from` a standing on `to`, and `to` an occupant |
|
|
385
391
|
| `invite({ id, occupant, notes, expires? })` | gives a being a new occupant, and the steward its handle |
|
|
386
392
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
`
|
|
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.
|
|
393
|
+
The powers land in the steward's own write, so a failed ask did none of
|
|
394
|
+
them. A being borne runs her `born` ask first, where she declares one,
|
|
395
|
+
asked by `steward`. The steward's `hire` declares the courier
|
|
396
|
+
`s.invitation`, so she carries it unopened. The order's declares it
|
|
397
|
+
`s.handle`, so the order holds the standing.
|
|
401
398
|
|
|
402
|
-
|
|
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.
|
|
399
|
+
### Strangers
|
|
407
400
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
strangers: every asker with no relation is the occupant `stranger`, and
|
|
412
|
-
`this.asker.signer` is the key the ask was signed with.
|
|
401
|
+
A house may name one public being, with the id `public`. She is
|
|
402
|
+
sovereign: each ask follows its own flag, and she answers what her
|
|
403
|
+
author wrote. `this.asker.signer` is the key a stranger signed with.
|
|
413
404
|
|
|
414
405
|
```ts
|
|
415
406
|
// classes/lobby.ts
|
|
@@ -418,7 +409,7 @@ import { Being, need, s } from 'nervur/being';
|
|
|
418
409
|
/** What the lobby asks of her steward. */
|
|
419
410
|
const Signup = need('signup', {
|
|
420
411
|
enroll: {
|
|
421
|
-
|
|
412
|
+
idempotent: true,
|
|
422
413
|
args: s.object({ signer: s.bytes() }),
|
|
423
414
|
result: s.object({ invitation: s.invitation() }),
|
|
424
415
|
},
|
|
@@ -428,240 +419,298 @@ export class Lobby extends Being.of({
|
|
|
428
419
|
kind: 'com.acme.lobby',
|
|
429
420
|
description: 'The shop’s front door: a stranger signs up and receives an order of their own.',
|
|
430
421
|
asks: {
|
|
431
|
-
signup: { for: 'stranger', result: s.object({ invitation: s.invitation() }) },
|
|
422
|
+
signup: { for: 'stranger', idempotent: true, result: s.object({ invitation: s.invitation() }) },
|
|
432
423
|
},
|
|
433
424
|
}) {
|
|
434
|
-
signup() {
|
|
435
|
-
return this.held('steward', Signup).enroll({ signer: this.asker.signer! });
|
|
425
|
+
async signup() {
|
|
426
|
+
return this.must(await this.held('steward', Signup).enroll({ signer: this.asker.signer! }));
|
|
436
427
|
}
|
|
437
428
|
}
|
|
438
429
|
```
|
|
439
430
|
|
|
440
|
-
A
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
`
|
|
431
|
+
A stranger holds no relation, so a stranger's box may arrive twice. The
|
|
432
|
+
house answers a replayed box from a cache for ten minutes. Beyond that,
|
|
433
|
+
make every ask a stranger reaches `idempotent`, or mark it
|
|
434
|
+
`replayable: true`. A stranger who signs up twice here holds one order.
|
|
444
435
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
436
|
+
She asks a far public being with `this.stranger({ ward, at }, Need)`. She
|
|
437
|
+
awaits its idempotent asks and commits any other as an effect. That
|
|
438
|
+
effect's answer reaches her `reply` as `house`.
|
|
448
439
|
|
|
449
|
-
|
|
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.
|
|
440
|
+
## What the house holds
|
|
453
441
|
|
|
454
|
-
|
|
442
|
+
- **Cells stay small.** Cells past one mebibyte fail her ask, and
|
|
443
|
+
nothing lands.
|
|
444
|
+
- **Some ids are the house's.** `root`, `stranger`, `steward`, `house`,
|
|
445
|
+
`powers` and `standing` are reserved, with every id beginning
|
|
446
|
+
`handle:`, `being:` or `standing:`.
|
|
447
|
+
- **A member name never clashes.** An ask named as a member of `Being`,
|
|
448
|
+
such as `invite` or `held`, refuses the species.
|
|
449
|
+
- **A species is pure.** It imports `nervur/being`, other species and its
|
|
450
|
+
own files, and nothing that reaches a machine. It reaches time and
|
|
451
|
+
randomness through `this.house` alone.
|
|
452
|
+
- **A species runs contained.** Each house runs in a runner of its own.
|
|
453
|
+
A loop without end, or a rejection she left unawaited, fails her ask
|
|
454
|
+
at its wait. Every other house and being goes on.
|
|
455
455
|
|
|
456
|
-
|
|
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.
|
|
456
|
+
## Composition
|
|
461
457
|
|
|
462
|
-
|
|
463
|
-
|
|
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 } }`.
|
|
458
|
+
Beings compose by asking each other. Four patterns cover what one being
|
|
459
|
+
alone cannot do.
|
|
472
460
|
|
|
473
|
-
###
|
|
461
|
+
### A saga, since nothing is atomic across beings
|
|
462
|
+
|
|
463
|
+
Her ask lands whole, but two beings never land together. So a step that
|
|
464
|
+
spans beings is a saga: one effect at a time, each answered through its
|
|
465
|
+
reply, and a step that fails undoes the ones before it.
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
// patterns/trip.ts
|
|
469
|
+
import { Being, need, s, type Args } from 'nervur/being';
|
|
474
470
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
471
|
+
export const Hotel = need('hotel', {
|
|
472
|
+
hold: { args: s.object({ trip: s.string() }) },
|
|
473
|
+
release: { args: s.object({ trip: s.string() }) },
|
|
474
|
+
});
|
|
479
475
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
4. Each method is `idempotent` in both, or in neither.
|
|
476
|
+
export const Flight = need('flight', {
|
|
477
|
+
book: { args: s.object({ trip: s.string(), seats: s.integer() }) },
|
|
478
|
+
});
|
|
484
479
|
|
|
485
|
-
|
|
486
|
-
|
|
480
|
+
// A saga: each step is an effect, and a step that fails undoes the ones before it.
|
|
481
|
+
export class Trip extends Being.of({
|
|
482
|
+
kind: 'org.example.trip',
|
|
483
|
+
cells: { state: 'planning', seats: 0 },
|
|
484
|
+
state: (me) => me.cells.state,
|
|
485
|
+
asks: {
|
|
486
|
+
book: { in: 'planning', for: 'root', to: 'holding', args: s.object({ seats: s.integer() }) },
|
|
487
|
+
roomHeld: { in: 'holding', for: 'standing', args: s.reply(Hotel.hold), to: ['booking', 'planning'] },
|
|
488
|
+
flown: { in: 'booking', for: 'standing', args: s.reply(Flight.book), to: ['booked', 'planning'] },
|
|
489
|
+
},
|
|
490
|
+
}) {
|
|
491
|
+
book({ seats }: Args<Trip, 'book'>) {
|
|
492
|
+
this.cells.seats = seats;
|
|
493
|
+
this.held('hotel', Hotel).hold({ trip: this.id }, { reply: 'roomHeld' });
|
|
494
|
+
this.cells.state = 'holding';
|
|
495
|
+
}
|
|
487
496
|
|
|
488
|
-
|
|
489
|
-
|
|
497
|
+
roomHeld({ error }: Args<Trip, 'roomHeld'>) {
|
|
498
|
+
if (error) {
|
|
499
|
+
this.cells.state = 'planning';
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this.held('flight', Flight).book({ trip: this.id, seats: this.cells.seats }, { reply: 'flown' });
|
|
503
|
+
this.cells.state = 'booking';
|
|
504
|
+
}
|
|
490
505
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
506
|
+
flown({ error }: Args<Trip, 'flown'>) {
|
|
507
|
+
if (error) {
|
|
508
|
+
// The room is held and the flight is not: the saga lets the room go.
|
|
509
|
+
this.held('hotel', Hotel).release({ trip: this.id });
|
|
510
|
+
this.cells.state = 'planning';
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
this.cells.state = 'booked';
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
```
|
|
495
517
|
|
|
496
|
-
###
|
|
518
|
+
### Awaited edges form no cycle
|
|
497
519
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
520
|
+
While she awaits, she holds her queue. A call back to her waits behind
|
|
521
|
+
the ask that waits for it, until the wait runs out. So a being answering
|
|
522
|
+
her caller calls back with an effect, which leaves once her ask lands.
|
|
501
523
|
|
|
502
524
|
```ts
|
|
503
|
-
//
|
|
504
|
-
import
|
|
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';
|
|
525
|
+
// patterns/team.ts
|
|
526
|
+
import { Being, need, s, type Args } from 'nervur/being';
|
|
509
527
|
|
|
510
|
-
|
|
511
|
-
|
|
528
|
+
const Work = need('work', {
|
|
529
|
+
work: { idempotent: true, args: s.object({ job: s.string() }), result: s.string() },
|
|
512
530
|
});
|
|
513
531
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const order = await bench.place(Order, { id: 'first' });
|
|
532
|
+
const Report = need('report', {
|
|
533
|
+
done: { args: s.object({ job: s.string() }) },
|
|
534
|
+
});
|
|
518
535
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
536
|
+
export class Manager extends Being.of({
|
|
537
|
+
kind: 'org.example.manager',
|
|
538
|
+
cells: { done: [] as string[] },
|
|
539
|
+
asks: {
|
|
540
|
+
assign: { for: 'root', idempotent: true, args: s.object({ job: s.string() }), result: s.string() },
|
|
541
|
+
done: { for: 'being', args: s.object({ job: s.string() }) },
|
|
542
|
+
report: { for: 'root', readOnly: true, result: s.array(s.string()) },
|
|
543
|
+
},
|
|
544
|
+
}) {
|
|
545
|
+
async assign({ job }: Args<Manager, 'assign'>) {
|
|
546
|
+
return this.must(await this.held('clerk', Work).work({ job }));
|
|
547
|
+
}
|
|
524
548
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
549
|
+
done({ job }: Args<Manager, 'done'>) {
|
|
550
|
+
if (!this.cells.done.includes(job)) this.cells.done = [...this.cells.done, job];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
report() {
|
|
554
|
+
return this.cells.done;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export class Clerk extends Being.of({
|
|
559
|
+
kind: 'org.example.clerk',
|
|
560
|
+
asks: {
|
|
561
|
+
work: { for: 'being', idempotent: true, args: s.object({ job: s.string() }), result: s.string() },
|
|
562
|
+
},
|
|
563
|
+
}) {
|
|
564
|
+
work({ job }: Args<Clerk, 'work'>) {
|
|
565
|
+
// The manager awaits this ask, so she answers back with an effect.
|
|
566
|
+
this.held('manager', Report).done({ job });
|
|
567
|
+
return `on it: ${job}`;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export const beings = [Manager, Clerk];
|
|
530
572
|
```
|
|
531
573
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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.
|
|
574
|
+
### An index being answers questions across beings
|
|
575
|
+
|
|
576
|
+
A being knows only her own cells. A question across many, such as the
|
|
577
|
+
cheapest listings, needs an index being that each one tells. The index
|
|
578
|
+
keys what it hears by the true asker, so no listing writes another's
|
|
579
|
+
entry.
|
|
572
580
|
|
|
573
581
|
```ts
|
|
574
|
-
//
|
|
575
|
-
import
|
|
576
|
-
import { need, s } from 'nervur/being';
|
|
582
|
+
// patterns/search.ts
|
|
583
|
+
import { Being, need, s, type Args } from 'nervur/being';
|
|
577
584
|
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
const Index = need('index', {
|
|
586
|
+
put: { idempotent: true, args: s.object({ price: s.number() }) },
|
|
585
587
|
});
|
|
586
588
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
589
|
+
export class Listing extends Being.of({
|
|
590
|
+
kind: 'org.example.listing',
|
|
591
|
+
cells: { price: 0 },
|
|
592
|
+
asks: {
|
|
593
|
+
price: { for: 'root', args: s.object({ price: s.number() }) },
|
|
594
|
+
},
|
|
595
|
+
}) {
|
|
596
|
+
async price({ price }: Args<Listing, 'price'>) {
|
|
597
|
+
this.must(await this.held('search', Index).put({ price }));
|
|
598
|
+
this.cells.price = price;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// The index answers the question no listing can: which are cheapest.
|
|
603
|
+
export class Search extends Being.of({
|
|
604
|
+
kind: 'org.example.search',
|
|
605
|
+
cells: { prices: {} },
|
|
606
|
+
asks: {
|
|
607
|
+
put: { for: 'being', idempotent: true, args: s.object({ price: s.number() }) },
|
|
608
|
+
cheapest: { for: 'root', readOnly: true, result: s.array(s.string()) },
|
|
609
|
+
},
|
|
610
|
+
}) {
|
|
611
|
+
// Keyed by the true asker, so no listing writes another's price.
|
|
612
|
+
put({ price }: Args<Search, 'put'>) {
|
|
613
|
+
this.cells.prices = { ...this.cells.prices, [this.asker.id]: price };
|
|
606
614
|
}
|
|
607
615
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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}` });
|
|
616
|
+
cheapest() {
|
|
617
|
+
const sorted = Object.entries<number>(this.cells.prices).sort(([, a], [, b]) => a - b);
|
|
618
|
+
return sorted.slice(0, 3).map(([id]) => id);
|
|
614
619
|
}
|
|
615
620
|
}
|
|
616
621
|
|
|
617
|
-
|
|
618
|
-
export const paymentsOffer = (payments: Payments): Offer => ({ blueprint: PaymentsBlueprint, object: payments, window: 7 * 86_400_000 });
|
|
622
|
+
export const beings = [Listing, Search];
|
|
619
623
|
```
|
|
620
624
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
625
|
+
### Verifying who accepted an invitation
|
|
626
|
+
|
|
627
|
+
An invitation admits whoever takes it first. Where that must be one
|
|
628
|
+
person, the being checks it herself. Here the club keeps a pin in her
|
|
629
|
+
notes on the new occupant, and the pin travels by another road. A role
|
|
630
|
+
reads her notes, so the asks a member reaches stay hidden until the pin
|
|
631
|
+
is shown.
|
|
628
632
|
|
|
629
633
|
```ts
|
|
630
|
-
//
|
|
631
|
-
import {
|
|
634
|
+
// patterns/club.ts
|
|
635
|
+
import { Being, need, s, type Args } from 'nervur/being';
|
|
632
636
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
637
|
+
const hex = (bytes: Uint8Array) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
638
|
+
|
|
639
|
+
export class Club extends Being.of({
|
|
640
|
+
kind: 'org.example.club',
|
|
641
|
+
roles: {
|
|
642
|
+
guest: (asker) => typeof asker.notes.pin === 'string',
|
|
643
|
+
member: (asker) => asker.notes.member === true,
|
|
644
|
+
},
|
|
645
|
+
asks: {
|
|
646
|
+
admit: {
|
|
647
|
+
for: 'root',
|
|
648
|
+
args: s.object({ name: s.string() }),
|
|
649
|
+
result: s.object({ invitation: s.handle(), pin: s.string() }),
|
|
650
|
+
},
|
|
651
|
+
claim: { for: 'guest', idempotent: true, args: s.object({ pin: s.string() }) },
|
|
652
|
+
enter: { for: 'member', readOnly: true, result: s.string() },
|
|
653
|
+
},
|
|
654
|
+
}) {
|
|
655
|
+
// The invitation travels by one road and the pin by another.
|
|
656
|
+
admit({ name }: Args<Club, 'admit'>) {
|
|
657
|
+
const pin = hex(this.house.random({ length: 4 }));
|
|
658
|
+
const invitation = this.invite(`guest-${name}`, { notes: { pin, name }, expires: 86_400_000 });
|
|
659
|
+
return { invitation, pin };
|
|
660
|
+
}
|
|
638
661
|
|
|
639
|
-
|
|
662
|
+
// Whoever took the invitation proves they are the one it was meant for.
|
|
663
|
+
claim({ pin }: Args<Club, 'claim'>) {
|
|
664
|
+
if (pin !== this.asker.notes.pin) this.fail('That is not the pin.');
|
|
665
|
+
this.occupants.note(this.asker.id, { member: true, name: this.asker.notes.name ?? '' });
|
|
666
|
+
}
|
|
640
667
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
which houses open on which bodies.
|
|
668
|
+
enter() {
|
|
669
|
+
return `Welcome, ${String(this.asker.notes.name)}.`;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
646
672
|
|
|
647
|
-
|
|
673
|
+
const Claim = need('claim', {
|
|
674
|
+
claim: { idempotent: true, args: s.object({ pin: s.string() }) },
|
|
675
|
+
});
|
|
648
676
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
payments.ts
|
|
653
|
-
classes/index.ts, order.ts, shop.ts, lobby.ts
|
|
654
|
-
state/ made by the ground, its owner's alone
|
|
655
|
-
```
|
|
677
|
+
const Entry = need('entry', {
|
|
678
|
+
enter: { readOnly: true, result: s.string() },
|
|
679
|
+
});
|
|
656
680
|
|
|
657
|
-
|
|
658
|
-
|
|
681
|
+
export class Guest extends Being.of({
|
|
682
|
+
kind: 'org.example.guest',
|
|
683
|
+
cells: { club: '' },
|
|
684
|
+
asks: {
|
|
685
|
+
join: { for: 'root', idempotent: true, args: s.object({ invitation: s.handle() }) },
|
|
686
|
+
claim: { for: 'root', idempotent: true, args: s.object({ pin: s.string() }) },
|
|
687
|
+
enter: { for: 'root', idempotent: true, result: s.string() },
|
|
688
|
+
},
|
|
689
|
+
}) {
|
|
690
|
+
join({ invitation }: Args<Guest, 'join'>) {
|
|
691
|
+
this.cells.club = invitation;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async claim({ pin }: Args<Guest, 'claim'>) {
|
|
695
|
+
this.must(await this.held(this.cells.club, Claim).claim({ pin }));
|
|
696
|
+
}
|
|
659
697
|
|
|
660
|
-
|
|
661
|
-
|
|
698
|
+
async enter() {
|
|
699
|
+
return this.must(await this.held(this.cells.club, Entry).enter());
|
|
700
|
+
}
|
|
701
|
+
}
|
|
662
702
|
```
|
|
663
703
|
|
|
664
|
-
A
|
|
704
|
+
A code the steward writes in her own notes serves the same end, as the
|
|
705
|
+
shop's `owner` does.
|
|
706
|
+
|
|
707
|
+
## Proving a species
|
|
708
|
+
|
|
709
|
+
`nervur/bench` proves a species at three levels: alone, as a household,
|
|
710
|
+
and as a world. The bench runs contained, as every ground does, so it
|
|
711
|
+
takes her module by its URL and loads her classes in the house's runner.
|
|
712
|
+
|
|
713
|
+
A house's folder names what the house holds, and the household reads it.
|
|
665
714
|
|
|
666
715
|
```ts
|
|
667
716
|
// classes/index.ts
|
|
@@ -672,264 +721,193 @@ import { Order } from './order.ts';
|
|
|
672
721
|
export const beings = [Order];
|
|
673
722
|
```
|
|
674
723
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
```
|
|
724
|
+
```ts
|
|
725
|
+
// order.test.ts
|
|
726
|
+
import assert from 'node:assert/strict';
|
|
727
|
+
import { test } from 'node:test';
|
|
728
|
+
import { Bench } from 'nervur/bench';
|
|
729
|
+
import { Lobby } from './classes/lobby.ts';
|
|
730
|
+
import { Courier, Order } from './classes/order.ts';
|
|
731
|
+
import { Shop } from './classes/shop.ts';
|
|
732
|
+
import { Payments, paymentsOffer } from './payments.ts';
|
|
722
733
|
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
```
|
|
734
|
+
// Her module, which the bench loads in the house's own runner.
|
|
735
|
+
const module = new URL('./classes/order.ts', import.meta.url);
|
|
739
736
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
737
|
+
test('Order keeps her table: every state and role shows what it owes', async () => {
|
|
738
|
+
await Bench.check(Order, { module });
|
|
739
|
+
});
|
|
743
740
|
|
|
744
|
-
|
|
741
|
+
test('An order is paid once the provider calls the handle it was given', async () => {
|
|
742
|
+
// The provider stands in memory.
|
|
743
|
+
const payments = new Payments();
|
|
744
|
+
const bench = await Bench.open({ module, offers: [paymentsOffer(payments)] });
|
|
745
|
+
const order = await bench.place(Order, { id: 'first' });
|
|
745
746
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
747
|
+
assert.deepEqual(await order.ask('checkout'), { error: { message: 'Add an item first.' } });
|
|
748
|
+
assert.deepEqual(await order.ask('add', { sku: 'tea', price: 4 }), { result: { total: 4 } });
|
|
749
|
+
assert.deepEqual(await order.ask('checkout'), { result: null });
|
|
750
|
+
await bench.settle();
|
|
751
|
+
assert.equal((await order.describe())?.state, 'paying', 'the charge left once checkout landed, and answered pending');
|
|
749
752
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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.
|
|
753
|
+
assert.deepEqual(await payments.settle('first'), { result: null });
|
|
754
|
+
await bench.settle();
|
|
755
|
+
assert.equal((await order.describe())?.state, 'paid');
|
|
756
|
+
assert.deepEqual(await order.ask('add', { sku: 'jam', price: 1 }), { error: { message: 'not in this state' } });
|
|
757
|
+
});
|
|
757
758
|
|
|
758
|
-
A
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
759
|
+
test('A paid order ships through the courier she hired, whose stand-in hears her', async (t) => {
|
|
760
|
+
const payments = new Payments();
|
|
761
|
+
const bench = await Bench.open({
|
|
762
|
+
module,
|
|
763
|
+
offers: [paymentsOffer(payments)],
|
|
764
|
+
// The courier's side of her standing, answered by the handlers in courier.ts.
|
|
765
|
+
standIns: { courier: { need: Courier, module: new URL('./courier.ts', import.meta.url) } },
|
|
766
|
+
});
|
|
767
|
+
t.after(() => bench.close());
|
|
768
|
+
const order = await bench.place(Order, { id: 'second' });
|
|
769
|
+
|
|
770
|
+
// Her steward carries the invitation, and she takes it as a standing of her own.
|
|
771
|
+
await order.ask('hire', { courier: await bench.invitation('courier') });
|
|
772
|
+
await order.ask('add', { sku: 'tea', price: 4 });
|
|
773
|
+
await order.ask('checkout');
|
|
774
|
+
await payments.settle('second');
|
|
775
|
+
await bench.settle();
|
|
776
|
+
assert.deepEqual(await order.ask('ship'), { result: null });
|
|
777
|
+
await bench.settle();
|
|
778
|
+
assert.deepEqual(await bench.heard('courier'), [{ method: 'pickup', args: { items: ['tea'] } }]);
|
|
779
|
+
});
|
|
762
780
|
|
|
763
|
-
|
|
781
|
+
// The house module: its steward, its public being and its beings.
|
|
782
|
+
const house = new URL('./classes/index.ts', import.meta.url);
|
|
764
783
|
|
|
765
|
-
The
|
|
766
|
-
|
|
767
|
-
|
|
784
|
+
test('The steward and the lobby keep their tables where the house places them', async () => {
|
|
785
|
+
await Bench.check(Shop, { module: house, position: 'steward' });
|
|
786
|
+
await Bench.check(Lobby, { module: house, position: 'public' });
|
|
787
|
+
});
|
|
768
788
|
|
|
769
|
-
|
|
770
|
-
|
|
789
|
+
test('Her house keeps its table as one household: each being walked beside the others', async () => {
|
|
790
|
+
await Bench.household({ module: house });
|
|
791
|
+
});
|
|
771
792
|
```
|
|
772
793
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
`
|
|
794
|
+
### Alone
|
|
795
|
+
|
|
796
|
+
`Bench.check(Class, { module, position?, steward? })` proves her alone,
|
|
797
|
+
where the house would place her: `normal`, `steward` or `public`. It runs
|
|
798
|
+
every example and walks every state. An owner's dock steward is checked
|
|
799
|
+
at `dock`, as GROUNDS.md says. It throws an error naming every
|
|
800
|
+
finding that failed.
|
|
801
|
+
|
|
802
|
+
An example is a history, then one ask. `given` lists the asks that
|
|
803
|
+
bring her from her `born` to where it starts. `role` asks, `args` are
|
|
804
|
+
the ask's, `fakes` answer her needs, and `gives` is the answer owed.
|
|
805
|
+
|
|
806
|
+
The walk describes every state she reaches to every role the bench can
|
|
807
|
+
play. It then plays each ask by each role its `for` names. The bench
|
|
808
|
+
plays a role as an owner could: `root` through the hand, `steward` as her
|
|
809
|
+
steward, `being` as a being introduced to her, and `stranger` at the
|
|
810
|
+
public being. A role of yours is an occupant whose steward notes hold it
|
|
811
|
+
`true`. A role read from her own notes is hers to grant, and the bench
|
|
812
|
+
plays it only where her asks grant it.
|
|
813
|
+
|
|
814
|
+
The checker flags each of these.
|
|
815
|
+
|
|
816
|
+
- An example whose answer differs from its `gives`.
|
|
817
|
+
- A re-run of one history that answers differently, or leaves her cells
|
|
818
|
+
differently.
|
|
819
|
+
- A state that shows a role other asks than her table owes it.
|
|
820
|
+
- An ask she refuses to a role her table gives it.
|
|
821
|
+
- A `readOnly` ask that writes.
|
|
822
|
+
- An ask that lands in a state its `to` does not name.
|
|
823
|
+
- An ask that throws past `fail`.
|
|
824
|
+
- A reply her table refused, played where her own asks reach its edge.
|
|
825
|
+
- An ask a stranger reaches that is neither `idempotent` nor
|
|
826
|
+
`replayable`.
|
|
827
|
+
|
|
828
|
+
### Her bodies and standings
|
|
829
|
+
|
|
830
|
+
`Bench.open({ module, offers, standIns, steward, public })` opens a
|
|
831
|
+
bench to drive her by hand. `place(Class, { id, born })` places her.
|
|
832
|
+
`ask(method, args, { role })` and `describe({ role })` meet her as a role
|
|
833
|
+
would. `cells()` reads her cells as her owner reads them. `settle()` lets
|
|
834
|
+
every effect and reply run, and `advance(ms)` moves the clock.
|
|
835
|
+
|
|
836
|
+
A body stands beside her as an offer: `{ blueprint, object }`, an object
|
|
837
|
+
your test writes, which keeps its own calls. The order's payments are
|
|
838
|
+
one, and [Writing a faculty](FACULTIES.md) teaches how to write them.
|
|
839
|
+
|
|
840
|
+
A standing stands beside her as a stand-in. `standIns` names each by id,
|
|
841
|
+
with her need and a module of handlers, one function for each method. A
|
|
842
|
+
throw refuses the ask with its message. The bench introduces each to
|
|
843
|
+
every being it places, under that id. `bench.invitation(id)` answers an
|
|
844
|
+
invitation to one, which her ask takes as a standing of her own.
|
|
845
|
+
`bench.heard(id)` answers what it heard, in order.
|
|
846
|
+
|
|
847
|
+
A standing on another species of yours is an introduction.
|
|
848
|
+
`bench.introduce(from, to)` makes one as a steward does, each side a
|
|
849
|
+
placed being or an id. `{ notes }` writes her steward's notes on the
|
|
850
|
+
relation, where she finds a standing by them. So a manager and her
|
|
851
|
+
clerk are proven on one bench, before their household. The bench
|
|
852
|
+
introduces beside its own steward alone. A steward you bring holds the
|
|
853
|
+
powers, and the bench bears nothing in her house, neither a being nor
|
|
854
|
+
a stand-in.
|
|
777
855
|
|
|
778
|
-
```
|
|
779
|
-
|
|
856
|
+
```ts
|
|
857
|
+
// courier.ts
|
|
858
|
+
// The courier's side of her standing, one function for each method of
|
|
859
|
+
// her need. On the bench a stand-in runs them in the house's own runner.
|
|
860
|
+
export const pickup = ({ items }: { items: string[] }): void => {
|
|
861
|
+
if (items.length === 0) throw new Error('Nothing to pick up.');
|
|
862
|
+
};
|
|
780
863
|
```
|
|
781
864
|
|
|
782
|
-
|
|
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
|
-
```
|
|
865
|
+
### A household
|
|
790
866
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
867
|
+
`Bench.household({ module, beings?, introduce? })` stands a house module
|
|
868
|
+
whole. The bench's steward bears every being it lists, places its public
|
|
869
|
+
being, and makes each introduction `introduce` names. Each being is
|
|
870
|
+
walked in place, beside the others. An awaited call that answered nothing
|
|
871
|
+
between two of them is a finding naming both, since it came back into
|
|
872
|
+
its own chain.
|
|
794
873
|
|
|
795
|
-
|
|
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.
|
|
874
|
+
### A world
|
|
799
875
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
names.
|
|
876
|
+
A world is proven on a `BenchGround`: a ground in memory, on a
|
|
877
|
+
`FakeNetwork` whose one clock the test moves. The test stands faculties
|
|
878
|
+
and houses through the ground's hand, as an owner does. Several grounds
|
|
879
|
+
join one network, so one test proves the same species at every distance.
|
|
805
880
|
|
|
806
|
-
|
|
881
|
+
```ts
|
|
882
|
+
// world.test.ts
|
|
883
|
+
import assert from 'node:assert/strict';
|
|
884
|
+
import { test } from 'node:test';
|
|
885
|
+
import { BenchGround, FakeNetwork } from 'nervur/bench';
|
|
886
|
+
import { faculties } from './recipe.ts';
|
|
807
887
|
|
|
808
|
-
|
|
809
|
-
on Linux, started again after any exit, and a launchd agent on macOS.
|
|
888
|
+
const shop = new URL('./classes/index.ts', import.meta.url);
|
|
810
889
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
890
|
+
test('The shop stands as a world: its owner opens an order through the hand', async (t) => {
|
|
891
|
+
const ground = await BenchGround.open({ network: new FakeNetwork(), host: 'shop', modules: { shop }, registry: { faculties } });
|
|
892
|
+
t.after(() => ground.down());
|
|
893
|
+
await ground.hand({ method: 'facultiesAdd', args: { name: 'payments', make: 'payments' } });
|
|
894
|
+
await ground.add('shop', 'shop', { faculties: ['payments'] });
|
|
814
895
|
|
|
815
|
-
|
|
816
|
-
|
|
896
|
+
assert.ok('result' in (await ground.ask({ house: 'shop', method: 'open', args: { id: 'first' } })));
|
|
897
|
+
assert.deepEqual(await ground.ask({ house: 'shop', method: 'orders' }), { result: ['first'] });
|
|
898
|
+
const order = await ground.ask({ house: 'shop', id: 'first' });
|
|
899
|
+
assert.equal('describe' in order && (order.describe as { state: string }).state, 'open');
|
|
900
|
+
});
|
|
817
901
|
```
|
|
818
902
|
|
|
819
|
-
|
|
820
|
-
|
|
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.
|
|
903
|
+
`recipe.ts` is the registry of the shop's faculties, and
|
|
904
|
+
[Grounds](GROUNDS.md) says how a ground stands one.
|
|
886
905
|
|
|
887
|
-
|
|
906
|
+
### In your gate and in CI
|
|
888
907
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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.
|
|
908
|
+
The same checker runs in your gate and in CI. `@nervur-org/proof`, an
|
|
909
|
+
open package beside the library, proves a folder with `nervur test`. It
|
|
910
|
+
checks each species where its ground places her, with `Bench.check`, and
|
|
911
|
+
walks each house folder with `Bench.household`. Run your own suites with
|
|
912
|
+
`node --test`, and check your types with `npx tsc --noEmit`, since a need
|
|
913
|
+
you forgot to declare fails only inside her ask.
|