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.
Files changed (157) hide show
  1. package/CONTRIBUTING.md +13 -0
  2. package/LICENSE +55 -202
  3. package/NOTICE +3 -8
  4. package/README.md +14 -156
  5. package/RELEASING.md +15 -0
  6. package/SECURITY.md +9 -0
  7. package/bin/nervur.js +6 -0
  8. package/package.json +26 -81
  9. package/AUTHORING.md +0 -935
  10. package/COMMAND.md +0 -179
  11. package/FACES.md +0 -280
  12. package/FACULTIES.md +0 -578
  13. package/GROUNDS.md +0 -315
  14. package/KIT-SPEC.md +0 -304
  15. package/WORLD.md +0 -195
  16. package/dist/app/app-ground.d.ts +0 -18
  17. package/dist/app/app-ground.js +0 -23
  18. package/dist/app/index.d.ts +0 -2
  19. package/dist/app/index.js +0 -5
  20. package/dist/app/native.d.ts +0 -11
  21. package/dist/app/native.js +0 -13
  22. package/dist/being/being.d.ts +0 -306
  23. package/dist/being/being.js +0 -20
  24. package/dist/being/covers.d.ts +0 -9
  25. package/dist/being/covers.js +0 -21
  26. package/dist/being/index.d.ts +0 -4
  27. package/dist/being/index.js +0 -6
  28. package/dist/being/need.d.ts +0 -49
  29. package/dist/being/need.js +0 -33
  30. package/dist/being/schema.d.ts +0 -100
  31. package/dist/being/schema.js +0 -132
  32. package/dist/being/table.d.ts +0 -48
  33. package/dist/being/table.js +0 -344
  34. package/dist/bench/bench-ground.d.ts +0 -73
  35. package/dist/bench/bench-ground.js +0 -153
  36. package/dist/bench/bench.d.ts +0 -101
  37. package/dist/bench/bench.js +0 -368
  38. package/dist/bench/fake-clock.d.ts +0 -17
  39. package/dist/bench/fake-clock.js +0 -34
  40. package/dist/bench/fake-memory.d.ts +0 -16
  41. package/dist/bench/fake-memory.js +0 -46
  42. package/dist/bench/fake-network.d.ts +0 -66
  43. package/dist/bench/fake-network.js +0 -222
  44. package/dist/bench/fake-unlock.d.ts +0 -6
  45. package/dist/bench/fake-unlock.js +0 -10
  46. package/dist/bench/index.d.ts +0 -3
  47. package/dist/bench/index.js +0 -5
  48. package/dist/bench/seeded.d.ts +0 -11
  49. package/dist/bench/seeded.js +0 -37
  50. package/dist/bench/settle.d.ts +0 -2
  51. package/dist/bench/settle.js +0 -49
  52. package/dist/bench/stewards.d.ts +0 -169
  53. package/dist/bench/stewards.js +0 -128
  54. package/dist/bodies/class-list.d.ts +0 -14
  55. package/dist/bodies/class-list.js +0 -33
  56. package/dist/bodies/joined-carry.d.ts +0 -29
  57. package/dist/bodies/joined-carry.js +0 -48
  58. package/dist/bodies/kept.d.ts +0 -44
  59. package/dist/bodies/kept.js +0 -91
  60. package/dist/bodies/noble-crypto.d.ts +0 -22
  61. package/dist/bodies/noble-crypto.js +0 -88
  62. package/dist/bodies/seed-keys.d.ts +0 -10
  63. package/dist/bodies/seed-keys.js +0 -42
  64. package/dist/bodies/strict-tools.d.ts +0 -10
  65. package/dist/bodies/strict-tools.js +0 -306
  66. package/dist/bodies/vouch.d.ts +0 -20
  67. package/dist/bodies/vouch.js +0 -71
  68. package/dist/bodies/web-carry.d.ts +0 -72
  69. package/dist/bodies/web-carry.js +0 -245
  70. package/dist/bodies/web-clock.d.ts +0 -12
  71. package/dist/bodies/web-clock.js +0 -33
  72. package/dist/browser/browser-ground.d.ts +0 -76
  73. package/dist/browser/browser-ground.js +0 -294
  74. package/dist/browser/index.d.ts +0 -4
  75. package/dist/browser/index.js +0 -7
  76. package/dist/browser/indexeddb-memory.d.ts +0 -17
  77. package/dist/browser/indexeddb-memory.js +0 -97
  78. package/dist/browser/locked-unlock.d.ts +0 -24
  79. package/dist/browser/locked-unlock.js +0 -85
  80. package/dist/browser/origin-classes.d.ts +0 -7
  81. package/dist/browser/origin-classes.js +0 -31
  82. package/dist/edge/durable-clock.d.ts +0 -19
  83. package/dist/edge/durable-clock.js +0 -61
  84. package/dist/edge/durable-memory.d.ts +0 -5
  85. package/dist/edge/durable-memory.js +0 -11
  86. package/dist/edge/durable.d.ts +0 -18
  87. package/dist/edge/durable.js +0 -24
  88. package/dist/edge/edge-ground.d.ts +0 -49
  89. package/dist/edge/edge-ground.js +0 -223
  90. package/dist/edge/index.d.ts +0 -7
  91. package/dist/edge/index.js +0 -9
  92. package/dist/edge/native-crypto.d.ts +0 -8
  93. package/dist/edge/native-crypto.js +0 -102
  94. package/dist/edge/secret-unlock.d.ts +0 -7
  95. package/dist/edge/secret-unlock.js +0 -13
  96. package/dist/edge/socket-carry.d.ts +0 -39
  97. package/dist/edge/socket-carry.js +0 -111
  98. package/dist/foundation.d.ts +0 -162
  99. package/dist/foundation.js +0 -4
  100. package/dist/ground/dock.d.ts +0 -1460
  101. package/dist/ground/dock.js +0 -1159
  102. package/dist/ground/ground.d.ts +0 -239
  103. package/dist/ground/ground.js +0 -787
  104. package/dist/ground/views.d.ts +0 -76
  105. package/dist/ground/views.js +0 -208
  106. package/dist/house/crossing.d.ts +0 -50
  107. package/dist/house/crossing.js +0 -153
  108. package/dist/house/house.d.ts +0 -88
  109. package/dist/house/house.js +0 -2069
  110. package/dist/house/rows-shape.d.ts +0 -110
  111. package/dist/house/rows-shape.js +0 -12
  112. package/dist/house/rows.d.ts +0 -27
  113. package/dist/house/rows.js +0 -185
  114. package/dist/index.d.ts +0 -33
  115. package/dist/index.js +0 -29
  116. package/dist/node/bridge.d.ts +0 -20
  117. package/dist/node/bridge.js +0 -284
  118. package/dist/node/cli.d.ts +0 -2
  119. package/dist/node/cli.js +0 -225
  120. package/dist/node/file-memory.d.ts +0 -13
  121. package/dist/node/file-memory.js +0 -86
  122. package/dist/node/folder-classes.d.ts +0 -5
  123. package/dist/node/folder-classes.js +0 -40
  124. package/dist/node/gone.d.ts +0 -2
  125. package/dist/node/gone.js +0 -7
  126. package/dist/node/hand.d.ts +0 -39
  127. package/dist/node/hand.js +0 -107
  128. package/dist/node/http.d.ts +0 -11
  129. package/dist/node/http.js +0 -90
  130. package/dist/node/index.d.ts +0 -11
  131. package/dist/node/index.js +0 -13
  132. package/dist/node/ledger-memory.d.ts +0 -17
  133. package/dist/node/ledger-memory.js +0 -152
  134. package/dist/node/lock.d.ts +0 -10
  135. package/dist/node/lock.js +0 -78
  136. package/dist/node/node-ground.d.ts +0 -26
  137. package/dist/node/node-ground.js +0 -228
  138. package/dist/node/notify.d.ts +0 -1
  139. package/dist/node/notify.js +0 -18
  140. package/dist/node/shell.d.ts +0 -11
  141. package/dist/node/shell.js +0 -35
  142. package/dist/node/sync-folder.d.ts +0 -6
  143. package/dist/node/sync-folder.js +0 -18
  144. package/dist/node/tcp-carry.d.ts +0 -42
  145. package/dist/node/tcp-carry.js +0 -193
  146. package/dist/node/unlock.d.ts +0 -17
  147. package/dist/node/unlock.js +0 -86
  148. package/dist/node/websocket.d.ts +0 -12
  149. package/dist/node/websocket.js +0 -134
  150. package/dist/quo/frame.d.ts +0 -33
  151. package/dist/quo/frame.js +0 -92
  152. package/dist/quo/index.d.ts +0 -1
  153. package/dist/quo/index.js +0 -5
  154. package/dist/quo/room.d.ts +0 -154
  155. package/dist/quo/room.js +0 -362
  156. package/dist/serve/index.d.ts +0 -45
  157. package/dist/serve/index.js +0 -71
package/FACULTIES.md DELETED
@@ -1,578 +0,0 @@
1
- # Writing a faculty
2
-
3
- A faculty is anything a being calls that is not a being: a payment
4
- provider, a mail sender, a model, a sensor, a relay on a Pi. This guide
5
- teaches the craft whole. [Writing for nervur](AUTHORING.md) teaches beings and
6
- grounds, and the words both guides use.
7
-
8
- The example is a garage door. A relay on a Raspberry Pi pulses once to
9
- toggle the door, so a pulse sent twice opens the door and closes it
10
- again. The relay is a page of Python. The ground brings the rest: one
11
- call id across a crash and a lost reply, one twin granted the relay, and
12
- a phone that reaches the twin from anywhere. Every file here is a file
13
- the package's own tests run.
14
-
15
- ## What a faculty owes
16
-
17
- A faculty answers a blueprint: a name, and methods with their schemas.
18
- A registry holds it by name as `{ takes?, up, install? }`. The ground
19
- raises it by its `up` into a body, and hands the body to its houses as
20
- an offer, with the kinds its entry grants.
21
-
22
- ```text
23
- faculty = { takes?, install?, up }
24
- takes = { args?: schema, secrets?: { [name]: what it holds } }
25
- body = { blueprint?, object?, window?, handler?, registry?, down? }
26
- offer = { blueprint, object, kinds?, window? }
27
- ```
28
-
29
- - **It declares what it takes.** `takes.args` is a schema built with
30
- `s` from `nervur/being`, which its entry's args meet. `takes.secrets`
31
- names each secret its entry must name, with a line saying what it
32
- holds. The hand refuses an entry that fails either, before it lands,
33
- and `facultiesCatalog` shows it to whoever writes the entry.
34
- - **The object has the blueprint's methods.** Each takes one args object
35
- and a context, and answers `{ result }` or `{ error: { message } }`.
36
- - **An error is final, and a throw is not.** An error reaches the being
37
- as the answer. A throw, a crash or silence is a failure to answer, and
38
- the house asks again.
39
- - **The context holds the call id**, and `call({ token, args, id })`,
40
- which calls a handle the faculty was handed.
41
-
42
- The house checks the args against the offer's schema at each call, and
43
- the answer against the being's need when it arrives. A being sees only
44
- the methods her need names, and nothing else of the object.
45
-
46
- ### It acts once for each call id
47
-
48
- A method marked `idempotent` is safe to repeat, so a being awaits it
49
- during her ask. Every other method is an effect. The house writes an
50
- effect in the same write as her cells, then sends it until it is
51
- answered, with the same call id every time.
52
-
53
- So one call arrives more than once. A reply lost on its way back brings
54
- it again, and so does a program that died before it answered. A faculty
55
- that changes the world keeps each call id with the answer it gave, in
56
- the memory its `up` receives, where a restart and a move keep it. It
57
- answers a call id it has seen with that answer, and acts once.
58
-
59
- It keeps them for its `window`, seven days where the offer names none.
60
- The house gives up on an effect at the window and tells the being, so a
61
- retry never outlives the faculty's memory.
62
-
63
- ### It calls back by token
64
-
65
- A being may hand a faculty a handle to one of her asks, under
66
- `s.handle` in the args. It arrives as a token, a string of at least 128
67
- random bits that answers this faculty alone. The faculty calls it with
68
- `context.call({ token, args, id })` whenever the world moves. Its own
69
- `id` makes that call run once, however often it is sent.
70
-
71
- A handle may reach a whole occupant rather than one ask, as a door a
72
- steward's `invite` minted does. A face holds such tokens, one for each
73
- person it serves. `context.call({ token, method, args, id, after, home
74
- })` asks as that occupant, and `after` makes a `readOnly` ask a watch.
75
- `home` carries the answer's handles home as invitation bytes, which the
76
- person's own house takes.
77
- `context.describe({ token })` reads what that occupant may ask now. The
78
- being behind the token declares a need the face covers, so the ground's
79
- grant decides which classes a face reaches.
80
-
81
- ### The ground grants it
82
-
83
- A faculty is the ground's, never a house's. Its grant has two steps,
84
- both the ground's. A house receives the faculties its entry names.
85
- Within it, the faculty's own entry lists in `kinds` the classes that may
86
- hold the offer, and without it, every class whose need it covers holds
87
- it.
88
-
89
- So a raw faculty reaches one class. Limits, approvals and quotas are not
90
- the faculty's. One being holds the raw faculty, and every other being
91
- reaches her through a standing, so policy is written as a being.
92
-
93
- ### It lives in the ground
94
-
95
- The object arrives living. The ground raises each faculty its entries
96
- name, awaiting its `up`, and the house never starts, stops or restarts
97
- it. Every being whose need it covers holds the same object. Its `up`
98
- receives four things beside the faculty's name.
99
-
100
- - **`args`** are its entry's, as the owner wrote them.
101
- - **`secrets`** are the secrets its entry names, set through the hand
102
- and kept sealed in a being of the ground's dock. No other code reads
103
- them.
104
- - **`memory`** is the faculty's own, a view of the ground's memory
105
- sealed under a key the ground derives for it. Its call ids live there,
106
- so they move with the ground.
107
- - **`faculties`** holds the object of each body its entry names in
108
- `faculties`, so one body calls another directly. It goes up after
109
- each of them.
110
-
111
- `install` receives the same, and does the slow work once for each entry.
112
- A program's packages and a repository's checkout are installed there, so
113
- going up again installs nothing.
114
-
115
- A body answers four things beside its methods, each where it has one.
116
-
117
- - **`handler`** answers HTTP on the ground's one listener. It takes a
118
- `Request` and answers a `Response`, or `null` where the request is not
119
- its own. A site, an API or an MCP server is a body with a handler.
120
- - **`down`** lets go of what its `up` opened. The ground calls it when
121
- the body is updated, restarted or removed, and when the ground stops,
122
- bodies in the reverse of the order they stood.
123
- - **`registry`** holds more faculties, for every entry that names this
124
- body in `from`. A module of code, a git repository and a program's
125
- catalogue are each a registry.
126
- - **`window`** is how long it remembers a call id.
127
-
128
- The hand changes a body. `faculties update` lands a new entry, and the
129
- body goes down and up on it. `faculties restart` takes it down and up on
130
- the same entry. Each house that names it opens again.
131
-
132
- ## Three ways to wrap a program
133
-
134
- | Way | What the object is | What a crash takes |
135
- | --- | --- | --- |
136
- | in the ground | a JavaScript object, an addon or a WASM module | the ground and every house |
137
- | beside it | a program over the bridge | the calls in flight to it |
138
- | far away | a client of a socket or an HTTP service | the calls in flight to it |
139
-
140
- ## The bridge
141
-
142
- `bridge({ command, args, env, cwd, memory })` from `nervur/node` starts
143
- a program and answers a body. A NodeGround raises one by its faculty
144
- `bridge`, from its entry alone. The program may be written in any
145
- language. It speaks one JSON value a line on its standard streams.
146
-
147
- ```text
148
- in { id, method, args, call } a call to the program
149
- out { id, result } | { id, error } its answer
150
- out { id, token, args, call } the program calls a handle
151
- in { id, result } | { id, error } the house's answer to that
152
- out { id, memory, args } the program reads or writes its memory
153
- in { id, result } | { id, error } the ground's answer to that
154
- ```
155
-
156
- - **It describes itself first.** Its first call is the method
157
- `describe`, and it answers `{ blueprint, window }`. It cannot offer
158
- what it does not declare.
159
- - **Calls overlap.** Answers come in any order and are matched by `id`,
160
- and each direction keeps ids of its own.
161
- - **`call` is the call id.** On a call in, it is the house's. On a
162
- handle the program calls, it is the program's own, and it must stay
163
- the same every time that call is sent, in every life of the program.
164
- A handle called with no `call` is refused.
165
- - **It starts with an empty environment.** It holds only what `env`
166
- names, so it never reads the ground's key. The faculty `bridge` names
167
- there the secrets of its entry, each under its name.
168
- - **It keeps its state in the faculty's memory.** A memory line names
169
- `read`, `list` or `write` and the memory's args, every entry's bytes
170
- in lowercase hex. A write names the versions it read in `expect`, and
171
- answers `null` where another write moved them. Nothing it keeps rests
172
- in its folder.
173
- - **It is started again.** A program that exits fails every awaited call
174
- in flight to it, and the house sends each effect again with its call
175
- id. A program that keeps failing is started more slowly, up to a
176
- minute apart.
177
- - **Its blueprint is fixed while its faculty stands.** A program started
178
- again that describes another blueprint is stopped, and the ground says
179
- so. Every call to it then answers why, until its faculty stands again.
180
- - **Its standard error goes to the ground's log**, and a line is at most
181
- a mebibyte.
182
-
183
- The bridge is not a sandbox. A program runs as the ground's user, and
184
- `kinds` decides which classes reach it. A program you do not trust runs
185
- as a user of its own. On Linux the entry's command becomes `sudo`, with
186
- `-n -u relay` before the program, and one rule allows it and nothing
187
- else:
188
-
189
- ```text
190
- nervur ALL=(relay) NOPASSWD: /usr/bin/python3 /srv/garage/relay.py
191
- ```
192
-
193
- ## The relay, in Python
194
-
195
- ```python
196
- #!/usr/bin/env python3
197
- # The relay on a garage's Pi, as a faculty over the bridge. One pulse
198
- # toggles the door, so a pulse sent twice would open it and close it
199
- # again. It keeps each call id in the faculty's memory before it pulses,
200
- # and answers an id it has seen with the answer it gave, so a pulse sent
201
- # again never toggles twice. Its memory is sealed by the ground and
202
- # travels with it; its folder holds only what the Pi's pin would show.
203
- import json
204
- import os
205
- import sys
206
-
207
- BLUEPRINT = {
208
- 'name': 'relay',
209
- 'methods': {
210
- 'pulse': {
211
- 'description': 'One pulse of the relay, which toggles the door.',
212
- 'args': {'type': 'object', 'properties': {}, 'required': [], 'additionalProperties': False},
213
- 'result': {'type': 'integer'},
214
- },
215
- },
216
- }
217
-
218
- waiting = []
219
- asked = 0
220
-
221
-
222
- def send(message):
223
- sys.stdout.write(json.dumps(message) + '\n')
224
- sys.stdout.flush()
225
-
226
-
227
- def remember(op, args):
228
- # One memory line, and the ground's answer to it; calls that arrive meanwhile wait their turn.
229
- global asked
230
- asked += 1
231
- mine = 'memory-' + str(asked)
232
- send({'id': mine, 'memory': op, 'args': args})
233
- while True:
234
- line = sys.stdin.readline()
235
- if line == '':
236
- sys.exit(0)
237
- message = json.loads(line)
238
- if message.get('id') != mine or 'method' in message:
239
- waiting.append(message)
240
- continue
241
- if 'error' in message:
242
- raise RuntimeError(message['error']['message'])
243
- return message['result']
244
-
245
-
246
- def seen():
247
- read = remember('read', {'place': 'seen'})
248
- return {call: int(bytes.fromhex(count).decode()) for call, count in read['entries'].items()}, read['version']
249
-
250
-
251
- def keep(call, count, version):
252
- landed = remember('write', {'writes': {'seen': {call: str(count).encode().hex()}}, 'expect': {'seen': version}})
253
- if landed is None:
254
- # Another life wrote first: this one ends, and the ground starts it again.
255
- sys.exit(1)
256
-
257
-
258
- def pulse(call):
259
- # The Pi drives its pin here. This relay writes a line, so a test counts pulses.
260
- with open('pulses', 'a') as relay:
261
- relay.write(call + '\n')
262
-
263
-
264
- def messages():
265
- while True:
266
- if waiting:
267
- yield waiting.pop(0)
268
- continue
269
- line = sys.stdin.readline()
270
- if line == '':
271
- return
272
- yield json.loads(line)
273
-
274
-
275
- # Each life writes a line, so a test knows when the program started again.
276
- with open('lives', 'a') as lives:
277
- lives.write(str(os.getpid()) + '\n')
278
- for message in messages():
279
- if message['method'] == 'describe':
280
- send({'id': message['id'], 'result': {'blueprint': BLUEPRINT, 'window': 7 * 86_400_000}})
281
- continue
282
- if message['method'] != 'pulse':
283
- send({'id': message['id'], 'error': {'message': 'no such method'}})
284
- continue
285
- counts, version = seen()
286
- if message['call'] in counts:
287
- send({'id': message['id'], 'result': counts[message['call']]})
288
- continue
289
- count = len(counts) + 1
290
- # The call id is kept before the pulse, so a crash between them never pulses twice.
291
- keep(message['call'], count, version)
292
- pulse(message['call'])
293
- # A test writes `crash-after`: the program dies once after that pulse, before it answers.
294
- if os.path.exists('crash-after') and count == int(open('crash-after').read()):
295
- os.remove('crash-after')
296
- os._exit(1)
297
- send({'id': message['id'], 'result': count})
298
- ```
299
-
300
- The call id is written to disk before the pin moves. A crash between the
301
- write and the pulse loses one opening, and the call answers as if it
302
- pulsed. The other order would pulse twice, and the door would close
303
- again. For a door, a missed opening is the safe failure.
304
- The blueprint is JSON Schema, in the subset `s` writes, so a need in
305
- TypeScript and a program in any language describe one shape.
306
-
307
- ## The twin, and the remote
308
-
309
- ```ts
310
- // The garage door's twin, on the Pi's ground, and the remote on a phone.
311
- // The twin alone holds the relay, and keeps who opened the door and which
312
- // pulse did it. The remote holds a standing on the twin and taps it by an
313
- // effect, so a tap is sent until it is answered, and pulses once.
314
- import { Being, need, s, type Args } from 'nervur/being';
315
-
316
- /** The relay, as the Python program offers it: one pulse, answered with its count. */
317
- export const Relay = need('relay', { pulse: { result: s.integer() } });
318
-
319
- /** The twin, as the remote asks it. */
320
- export const Door = need('garage', { open: {} });
321
-
322
- export class Garage extends Being.of({
323
- kind: 'org.example.garage',
324
- description: 'The twin of a garage door: who opened it, and which pulse did.',
325
- needs: { relay: Relay },
326
- cells: { openers: [] as string[], pulses: [] as number[], failed: [] as string[] },
327
- asks: {
328
- open: {},
329
- pulsed: { args: s.reply(Relay.pulse) },
330
- log: { hints: { readOnly: true }, result: s.object({ openers: s.array(s.string()), pulses: s.array(s.integer()), failed: s.array(s.string()) }) },
331
- },
332
- }) {
333
- open() {
334
- this.cells.openers = [...this.cells.openers, this.asker.id];
335
- this.relay.pulse({}, { reply: 'pulsed' });
336
- }
337
-
338
- pulsed({ result, error }: Args<Garage, 'pulsed'>) {
339
- if (error === undefined) this.cells.pulses = [...this.cells.pulses, result];
340
- else this.cells.failed = [...this.cells.failed, error.message];
341
- }
342
-
343
- log() {
344
- return this.cells;
345
- }
346
- }
347
-
348
- export class Remote extends Being.of({
349
- kind: 'org.example.remote',
350
- description: 'A garage remote on a phone: one tap, one opening.',
351
- cells: { garage: '', heard: 0, failed: [] as string[] },
352
- asks: {
353
- accept: { args: s.object({ invitation: s.handle() }), hints: { idempotent: true } },
354
- tap: {},
355
- opened: { args: s.reply(Door.open) },
356
- heard: { hints: { readOnly: true }, result: s.integer() },
357
- },
358
- }) {
359
- accept({ invitation }: Args<Remote, 'accept'>) {
360
- this.cells.garage = invitation;
361
- }
362
-
363
- tap() {
364
- this.held(this.cells.garage, Door).open({}, { reply: 'opened' });
365
- }
366
-
367
- opened({ error }: Args<Remote, 'opened'>) {
368
- if (error === undefined) this.cells.heard += 1;
369
- else this.cells.failed = [...this.cells.failed, error.message];
370
- }
371
-
372
- heard() {
373
- return this.cells.heard;
374
- }
375
- }
376
- ```
377
-
378
- `pulse` is not `idempotent`, so it is an effect. It leaves once `open`
379
- has landed, and its answer comes back to `pulsed`. The twin never waits
380
- on the relay, and the relay never waits on the twin. The remote asks the
381
- twin by an effect too, so a tap made while the Pi is unreachable reaches
382
- it later, once.
383
-
384
- ## The entry
385
-
386
- The Pi's folder holds the program and a folder of classes for its
387
- house, and no code of the ground's. The relay stands from its entry,
388
- raised by the faculty `bridge` of the NodeGround's folder, and granted
389
- to the twin's class alone.
390
-
391
- ```bash
392
- npx nervur faculties add name=relay from=folder make=bridge args='{"command":"python3","args":["relay.py"]}' kinds='["org.example.garage"]'
393
- ```
394
-
395
- The house is added once, and names the relay among the faculties it
396
- receives.
397
-
398
- ```bash
399
- npx nervur houses add name=garage classes='{"faculty":"folder","at":"classes"}' faculties='["relay"]'
400
- ```
401
-
402
- Both entries rest sealed as cells of the ground's dock, so the ground
403
- stands the relay again at every start.
404
-
405
- ## In JavaScript
406
-
407
- `serve(need, methods)` from `nervur/serve` is the program's side of the
408
- bridge in JavaScript. It answers `describe` from the need, and every
409
- call with the method it names. A method receives its args and a context:
410
- the call id, `call(token, args, id?)` for a handle it was handed, and
411
- `memory`, the faculty's own, whose `read`, `list` and `write` speak the
412
- memory lines. Where no `id` is given, the handle's call id is the call's
413
- own id and its count within the call. A retry of the call calls back with the same
414
- ids, so the handle acts once. A method that throws answers an error,
415
- which is final. A program that must be asked again exits instead, and
416
- the ground starts it again.
417
-
418
- ```ts
419
- // A doorbell, as a faculty written in JavaScript and run over the bridge
420
- // by `nervur/serve`. A being hands it the handle of an ask to call when
421
- // the bell is pressed; a press calls that handle back through the house.
422
- import { need, s } from 'nervur/being';
423
- import { serve } from 'nervur/serve';
424
-
425
- export const Doorbell = need('doorbell', {
426
- watch: { args: s.object({ inbox: s.handle() }), hints: { idempotent: true } },
427
- press: {},
428
- });
429
-
430
- let inbox: string | undefined;
431
-
432
- serve(Doorbell, {
433
- watch: ({ inbox: token }) => {
434
- inbox = token as string;
435
- return null;
436
- },
437
- press: async (_args, context) => {
438
- if (inbox === undefined) throw new Error('no one watches the bell');
439
- const answered = await context.call(inbox, {});
440
- if ('error' in answered) throw new Error(answered.error.message);
441
- return null;
442
- },
443
- });
444
- ```
445
-
446
- Its entry raises it with the faculty `bridge`, its command `node` and
447
- its args `["doorbell.js"]`. A faculty that needs no process of its own
448
- is a plain object its `up` answers instead, as the shop's payments
449
- are in [Writing for nervur](AUTHORING.md).
450
-
451
- ## Testing a faculty
452
-
453
- A faculty is tested on BenchGround, the ground in memory from
454
- `nervur/bench`, with the program itself behind the bridge. The test
455
- hands the bench a registry whose `bridge` faculty bridges the program, as a
456
- NodeGround's own does, and stands the relay through the ground's hand,
457
- as an owner does. A `FakeNetwork` joins
458
- grounds and loses what the test tells it to lose, and its one clock
459
- moves them all. A test waits on what it can hear: a watch on a being's
460
- answer, or a line the program writes. The clock moves only as far as a
461
- retry is due.
462
-
463
- The Pi's house and the phone's each have a steward the owner pilots
464
- through the hand. It answers `bear`, and `offerFor`, which mints a paper
465
- on one of its beings for an occupant, as the shop's steward does in
466
- [Writing for nervur](AUTHORING.md). The phone's remote takes that paper
467
- as its standing on the twin.
468
-
469
- ```ts
470
- // garage.test.ts
471
- import assert from 'node:assert/strict';
472
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, watch, writeFileSync } from 'node:fs';
473
- import { tmpdir } from 'node:os';
474
- import { join } from 'node:path';
475
- import { test } from 'node:test';
476
- import { fileURLToPath } from 'node:url';
477
- import { BenchGround, FakeNetwork } from 'nervur/bench';
478
- import { bridge } from 'nervur/node';
479
- import * as phone from './phone.ts';
480
- import * as pi from './pi.ts';
481
-
482
- test('Each tap on the phone pulses the relay once, whatever fails between', { timeout: 30_000 }, async (t) => {
483
- const state = mkdtempSync(join(tmpdir(), 'garage-'));
484
- t.after(() => rmSync(state, { recursive: true, force: true }));
485
- // Where the program runs, and where the Pi's pin shows each pulse.
486
- const pin = join(state, 'relay');
487
- mkdirSync(pin);
488
- // The program dies once, after its second pulse and before it answers.
489
- writeFileSync(join(pin, 'crash-after'), '2');
490
- const network = new FakeNetwork();
491
-
492
- // The Pi's ground, whose registry bridges the program as a NodeGround's does, and the door's twin.
493
- const relay = fileURLToPath(new URL('./relay.py', import.meta.url));
494
- const garage = await BenchGround.open({
495
- network,
496
- host: 'pi',
497
- names: ['garage.local'],
498
- modules: { pi },
499
- registry: { faculties: { bridge: { up: ({ memory }) => bridge({ command: 'python3', args: [relay], cwd: pin, memory }) } } },
500
- });
501
- t.after(() => garage.down());
502
- // The relay, granted to the twin's class alone.
503
- await garage.hand({ method: 'facultiesAdd', args: { name: 'relay', make: 'bridge', kinds: ['org.example.garage'] } });
504
- await garage.add('garage', 'pi', { faculties: ['relay'] });
505
- await garage.ask({ house: 'garage', method: 'bear', args: { kind: 'org.example.garage', id: 'door' } });
506
-
507
- // Alice's phone: a device, holding a standing on the twin.
508
- const alice = await BenchGround.open({ network, host: 'phone', modules: { phone } });
509
- t.after(() => alice.down());
510
- await alice.add('phone', 'phone');
511
- await alice.ask({ house: 'phone', method: 'bear', args: { kind: 'org.example.remote', id: 'remote' } });
512
- const paper = await garage.ask({ house: 'garage', method: 'offerFor', args: { being: 'door', occupant: 'alice' } });
513
- await alice.ask({ house: 'phone', id: 'remote', method: 'accept', args: { invitation: (paper as { result: { handle: string } }).result.handle } });
514
-
515
- // A being's answer, watched until it is what the test waits for: each ask answers once it moves.
516
- const watched = async <T>(ground: BenchGround, house: string, id: string, method: string, done: (seen: T) => boolean): Promise<T> => {
517
- let seen = ((await ground.ask({ house, id, method })) as { result: T }).result;
518
- while (!done(seen)) seen = ((await ground.ask({ house, id, method, after: { result: seen as never } })) as { result: T }).result;
519
- return seen;
520
- };
521
- const pulsed = (count: number) => watched<{ pulses: number[] }>(garage, 'garage', 'door', 'log', (log) => log.pulses.length >= count);
522
- const heard = (count: number) => watched<number>(alice, 'phone', 'remote', 'heard', (seen) => seen >= count);
523
- // Resolves once the relay's program has started `count` times.
524
- const lives = (count: number) =>
525
- new Promise<void>((resolve) => {
526
- const started = () => existsSync(join(pin, 'lives')) && readFileSync(join(pin, 'lives'), 'utf8').trim().split('\n').length >= count;
527
- const watcher = watch(pin, () => {
528
- if (!started()) return;
529
- watcher.close();
530
- resolve();
531
- });
532
- t.after(() => watcher.close());
533
- if (!started()) return;
534
- watcher.close();
535
- resolve();
536
- });
537
- const tap = async () => assert.ok('result' in (await alice.ask({ house: 'phone', id: 'remote', method: 'tap' })));
538
-
539
- await tap();
540
- await pulsed(1);
541
- await heard(1);
542
-
543
- // The second pulse kills the program before it answers. Once it runs
544
- // again, a second on the bench's clock sends the pulse again, with its
545
- // call id, and the program answers the pulse it gave.
546
- await tap();
547
- await lives(2);
548
- await network.elapse(1_000);
549
- await pulsed(2);
550
- await heard(2);
551
- assert.ok(!existsSync(join(pin, 'crash-after')), 'the program died between its second pulse and its answer');
552
-
553
- // The reply to the phone is lost, so a second later it asks again, and
554
- // the twin answers from its call id.
555
- network.loseNext();
556
- await tap();
557
- await pulsed(3);
558
- await network.elapse(1_000);
559
- await heard(3);
560
-
561
- assert.ok(network.crossings.some(({ outcome }) => outcome === 'lost'), 'a reply to the phone was lost');
562
- assert.equal(readFileSync(join(pin, 'pulses'), 'utf8').trim().split('\n').length, 3, 'three pulses, never four');
563
- assert.deepEqual(await garage.ask({ house: 'garage', id: 'door', method: 'log' }), { result: { openers: ['alice', 'alice', 'alice'], pulses: [1, 2, 3], failed: [] } });
564
- });
565
- ```
566
-
567
- Three taps open the door three times. The second pulse's program dies
568
- before it answers, and the house sends that call again to the program
569
- started anew, which answers from what it kept. The third tap's reply to
570
- the phone is lost, and the phone's house sends it again to the twin,
571
- which answers from its own call ids. The relay pulses three times, never
572
- four.
573
-
574
- `pi.ts` and `phone.ts` are the two houses' folders, each a steward and
575
- the class it holds. A faculty a test does not run is an object the test
576
- writes, whose methods answer, throw, refuse or never answer as the test
577
- needs. Where no offer covers a need of a class, bearing a being of it
578
- fails with `no offer covers her need <member>`.