nervur 0.22.2-8 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/GROUNDS.md CHANGED
@@ -14,24 +14,41 @@ kept by what the ground stands on: a file, a keychain, a Worker's
14
14
  secret. The part of a ground that answers it is its unlock. Everything
15
15
  else the ground keeps is sealed under that key.
16
16
 
17
- **It keeps its drawer in its one memory.** The drawer is the ground's
18
- own sealed places. It holds each house's entry and seed, each faculty's
19
- entry, every secret you set, and each house's ward. A memory that holds
20
- places the key does not open is refused, so a ground never opens on
21
- another's memory.
22
-
23
- **Its houses keep their places in the same memory.** Each house and each
24
- faculty sees a view of the ground's memory under a prefix of its own. A
25
- house seals its own rows, and the ground seals each faculty's. So what
26
- rests on the disk is ciphertext, and copying the key, the memory and the
27
- code moves the whole ground.
28
-
29
- **Every faculty has one lifecycle.** A registry holds faculties by name,
30
- each an object `{ up, install? }`. Standing a faculty raises it by its
31
- `up` into a body, the living instance houses and other bodies use.
32
- `install` does the slow work once for each entry. A body's `down` lets
33
- go of what its `up` opened. A foundation body says what it `serves`: a
34
- carry, the clock, or each house's memory or code.
17
+ **It unpacks in layers, each a faculty.** Every faculty has one
18
+ lifecycle. A registry holds faculties by name, each an object `{ takes?,
19
+ up, install? }`. Standing one raises it by its `up` into a body, the
20
+ living instance houses and other bodies use. `install` does the slow
21
+ work once for each entry, and a body's `down` lets go of what its `up`
22
+ opened.
23
+
24
+ | Layer | Its faculties | What they are |
25
+ | --- | --- | --- |
26
+ | primordial | the memory, the unlock, crypto, tools, the clock | the host's, read from the environment alone |
27
+ | the ground's work | `ground` | the library's, offered to the dock alone |
28
+ | the dock | a house of the library's beings | the drawer, and the hand's one road |
29
+ | the ladder | the terrain's defaults and your entries | carries, code, and every custom faculty |
30
+ | the hand | a socket, a key, a channel | the owner's reach, up last and down first |
31
+
32
+ **It keeps its drawer as cells of the dock's beings.** The dock is a
33
+ house every ground stands, and its beings are the library's. A twin
34
+ stands for each faculty entry, a being for each house, and one for each
35
+ secret. Their cells hold every entry, what each faculty installed, each
36
+ house's seed and ward, every secret, and the ground's bound on every
37
+ ask. No row of the ground's stands beside them.
38
+
39
+ **Every name an entry gives is a standing.** A house's code, its memory
40
+ and the faculties it uses, and a faculty's registry, its callees and its
41
+ secrets, are relations between those beings. The dock's steward
42
+ introduces each, and her notes name the grant. An entry that names a
43
+ faculty or a secret the dock does not hold is refused, and nothing
44
+ lands.
45
+
46
+ **Its houses keep their places in the same memory.** The dock, each
47
+ house and each faculty sees a view of the ground's memory under a prefix
48
+ of its own. A house seals its own rows, and the ground seals each
49
+ faculty's. So what rests on the disk is ciphertext, and copying the key,
50
+ the memory and the code moves the whole ground. A memory the key opens
51
+ no dock in is refused.
35
52
 
36
53
  **It stands its bodies on a ladder.** The ground's own registry is its
37
54
  terrain's. A body may carry a registry of its own, and the faculties an
@@ -39,12 +56,13 @@ entry raises `from` it stand after it. A body stands after each body its
39
56
  entry names in `faculties`, whose objects its `up` receives. A body that
40
57
  fails to stand stays down and says why, and the ground boots beside it.
41
58
 
42
- **Its terrain names default entries, and the drawer's win.** Each ground
43
- stands its clock, its carries and its bodies of code from entries of its
44
- own. An entry of the same name in the drawer stands in its place.
59
+ **Its terrain names default entries, and yours win.** Each ground stands
60
+ its carries and its bodies of code from entries of its own. Its one
61
+ clock is primordial, which the dock and every house receive.
62
+ An entry you land under the same name stands in its place.
45
63
 
46
64
  ```text
47
- recipe { make: 'module', args: { at: 'recipe.ts' } }
65
+ recipe { from: 'folder', make: 'module', args: { at: 'recipe.ts' } }
48
66
  payments { from: 'recipe', make: 'payments', secrets: ['stripe-key'], kinds: ['com.acme.order'] }
49
67
  ```
50
68
 
@@ -58,6 +76,11 @@ each body's name, it holds the args that body hands this house.
58
76
  shop { classes: { faculty: 'folder', at: 'shop' }, faculties: ['payments'], wait: 60000 }
59
77
  ```
60
78
 
79
+ **A change answers what stands, and an error means nothing landed.**
80
+ Adding or updating a faculty answers why its body is down, where it is.
81
+ Adding or updating a house answers its ward, or why it stays closed.
82
+ The entry lands either way, and the hand mends it later.
83
+
61
84
  **It updates an entry in one write.** `update` lands the new entry over
62
85
  the old. A house closes and opens again on it, as the same ward. A body
63
86
  goes down, installs where its entry moved, and goes up, and each house
@@ -65,13 +88,21 @@ naming it opens again. `restart` takes a body down and up on its entry.
65
88
 
66
89
  **It grants a faculty in two steps.** A house uses only the faculties its
67
90
  entry names. Inside it, a being holds one only where the faculty's entry
68
- names her class in `kinds`, or where it names none. So a raw shell
91
+ names her class in `kinds`, or where it names none. So a raw program
69
92
  reaches one house and one class inside it.
70
93
 
71
94
  **It hands each faculty its secrets.** A secret is set through the hand
72
- and kept in the drawer. A faculty's entry names the secrets its `up`
73
- receives, and no other code reads one. The hand lists their names, and
74
- never a value. A changed secret reaches a body when it goes up again.
95
+ and kept in a being's cells. A faculty's entry names the secrets its
96
+ `up` receives, and no other code reads one. The hand lists their names
97
+ and the entries that name each, and never a value. A secret's cells are
98
+ shown to no one, and a changed secret reaches a body when it goes up
99
+ again.
100
+
101
+ **It holds each entry to what its faculty takes.** A faculty declares
102
+ `takes`: a schema its args meet, and the secrets its entry must name.
103
+ The hand refuses an entry that fails it, or that names a secret not
104
+ kept, and says what failed. `facultiesCatalog` shows every faculty the
105
+ ground can raise, and what each takes.
75
106
 
76
107
  **It hooks every door and runs one listener.** Each house's door goes to
77
108
  the ground's carry, which also delivers between the ground's own houses
@@ -79,36 +110,64 @@ without touching the network. Everything that speaks HTTP, Quo over the
79
110
  web and every face, is a handler on one listener. A faculty stood while
80
111
  the ground runs is served at once.
81
112
 
82
- **It serves its owner's hand.** The hand answers three things: what the
83
- ground holds, a faculty's method, and an ask of any being of any house,
84
- as the house's owner. Four faculties are the ground's own.
85
-
86
- | Faculty | Its methods | Who reaches it |
113
+ **The dock's steward reaches the ground through two faculties.** The
114
+ `ground` faculty raises and lowers bodies, opens and closes houses,
115
+ shows the catalogue and moves a house's places. It is offered to the
116
+ dock's beings alone. The `shell` runs a command on the ground's machine,
117
+ where the terrain has one. The dock is offered it as any house is
118
+ offered a body. Its steward stands on the shell's twin, and the twin's
119
+ entry grants it to the dock's shell being alone. No entry names, makes
120
+ or grants either.
121
+
122
+ **A boot writes only what moved.** Each twin raises its body by a read,
123
+ and her cells are written only where what came of it differs. So a wake
124
+ that finds nothing changed writes nothing.
125
+
126
+ **It serves its owner's hand, and the hand has one road.** The hand
127
+ answers `describe`, and otherwise asks a being of the dock or of a house
128
+ as `root`. A request that names a house asks a being there. One that
129
+ names none asks the dock's steward. `describe` shows her asks, every
130
+ faculty with its methods or why it is down, and every house.
131
+
132
+ | The dock's asks | What they do | Who asks them |
87
133
  | --- | --- | --- |
88
- | `houses` | `add`, `update`, `remove`, `list` | the hand, and the kinds its entry grants |
89
- | `faculties` | `add`, `update`, `restart`, `remove`, `list` | the hand, and the kinds its entry grants |
90
- | `secrets` | `set`, `remove`, `list` | the hand alone |
91
- | `moves` | `out`, `in` | the hand alone |
92
-
93
- **It lets a pilot possess it from afar.** An entry for `houses` or
94
- `faculties` holds `kinds` alone, and grants it to one pilot being. A
95
- standing on that being then pilots the ground. `secrets` and `moves`
96
- take no entry, so no being ever holds a seed or a secret.
97
-
98
- **It moves a house by its seed and its memory.** `moves` takes a house
99
- out as its seed and every place of its memory, and another ground takes
100
- them in. The house keeps its identity, so every relation still answers
101
- and no far house notices.
134
+ | `housesAdd`, `housesUpdate`, `housesRemove`, `housesList` | the houses | `root`, and a pilot |
135
+ | `facultiesAdd`, `facultiesUpdate`, `facultiesRestart`, `facultiesRemove`, `facultiesList` | the faculties | `root`, and a pilot |
136
+ | `facultiesCatalog` | every faculty the ground can raise, and what it takes | `root`, and a pilot |
137
+ | `waitSet`, `waitShow` | the ground's bound on every ask | `root`, and a pilot |
138
+ | `secretsSet`, `secretsRemove`, `secretsList` | the secrets | `root` alone |
139
+ | `movesOut`, `movesIn` | a house moved between grounds | `root` alone |
140
+ | `callFaculty` | a faculty's method, called by name | `root` alone |
141
+ | `shellRun` | a command on the ground's machine | `root` alone |
142
+ | `pilotsInvite`, `pilotsDismiss`, `pilotsList` | who pilots the ground | `root` alone |
143
+ | `boot` | the ladder stood and every house opened, as each boot asks | `root` alone |
144
+
145
+ **An ask of the dock is safe to send twice.** A request may carry
146
+ `call`, its call id. The same id sent again answers what the first
147
+ answered, and runs nothing twice.
148
+
149
+ **It lets a pilot possess it from afar.** `pilotsInvite` mints an
150
+ invitation to the dock's steward, and the owner hands it to one being.
151
+ That being then holds a standing on the dock, and asks the houses and
152
+ faculties asks through it. Her class reaches them typed with
153
+ `this.held(id, DockPilot)`, the need `nervur` exports. Secrets, moves,
154
+ the shell and faculties' methods stay the hand's alone. A secret passes
155
+ through `secretsSet`, which answers nothing, so no answer holds one.
156
+
157
+ **It moves a house by its seed and its memory.** `movesOut` takes a
158
+ house out as its seed and every place of its memory, and another ground
159
+ takes them in with `movesIn`. The house keeps its identity, so every
160
+ relation still answers and no far house notices.
102
161
 
103
162
  ## The five grounds
104
163
 
105
- | Ground | Where it runs | When it wakes | Its unlock | Its memory |
106
- | --- | --- | --- | --- | --- |
107
- | NodeGround | a server, a desktop, a Pi | always on | a key file, or the macOS keychain | a ledger in its folder |
108
- | EdgeGround | a Cloudflare Worker and its Durable Object | per request, per message, per alarm | the Worker's secret | the object's storage |
109
- | BrowserGround | a page or its service worker | while a tab is open, and by push | a key sealed under one the browser never hands out | IndexedDB |
110
- | AppGround | an iOS or Android app's web view | per launch, and by push | the Keychain or the Keystore | the app's native store |
111
- | BenchGround | memory, in a test | as the test moves its clock | a key drawn from the bench's seed | memory in the process |
164
+ | Ground | Where it runs | When it wakes | Its unlock | Its memory | Its hand |
165
+ | --- | --- | --- | --- | --- | --- |
166
+ | NodeGround | a server, a desktop, a Pi | always on | a key file, or the macOS keychain | a ledger in its folder | a socket in its folder |
167
+ | EdgeGround | a Cloudflare Worker and its Durable Object | per request, per message, per alarm | the Worker's secret | the object's storage | a key the Worker holds |
168
+ | BrowserGround | a page or its service worker | while a tab is open, and by push | a key sealed under one the browser never hands out | IndexedDB | a channel between the origin's tabs |
169
+ | AppGround | an iOS or Android app's web view | per launch, and by push | the Keychain or the Keystore | the app's native store | a channel, as in a page |
170
+ | BenchGround | memory, in a test | as the test moves its clock | a key drawn from the bench's seed | memory in the process | the test's own |
112
171
 
113
172
  The same class runs unchanged on every row. What differs is the bodies
114
173
  each ground hands its houses.
@@ -126,17 +185,29 @@ port is set, on the web. [The command](COMMAND.md) lists its settings
126
185
  and runs its hand.
127
186
 
128
187
  **Its registry holds its terrain's faculties.** `folder` serves each
129
- house its classes from a folder inside it. `module` imports a module
130
- inside it as a registry, whose `faculties` export holds `{ up, install? }`
131
- by name. `bridge` starts a program, in any language, and hands it the
132
- secrets its entry names as its environment. `tcp` and `web` are its
133
- carries, and `clock` its clock.
134
-
135
- **Its environment gives the default entries.** `clock`, `folder`, `tcp`
136
- and `web` stand from entries its settings write. An entry of the same
137
- name in the drawer stands in their place, so a port changes through the
138
- hand. A `tcp` entry whose args name no port only dials, and its
139
- invitations name no TCP address.
188
+ house its classes from a folder inside the code folder its args name.
189
+ Its body carries a registry of two more, rooted there. `module` imports
190
+ a module as a registry, whose `faculties` export holds `{ up, install?
191
+ }` by name. `bridge` starts a program, in any language, and hands it the
192
+ secrets its entry names as its environment. An entry stands either with
193
+ `from: 'folder'`. `tcp` and `web` are its carries, `listener` its one
194
+ listener, and `shell` its shell. Its clock is the library's, and
195
+ primordial.
196
+
197
+ **Its code gives the default entries.** `folder`, `listener`, `tcp`,
198
+ `web` and `shell` stand from entries the library fixes. TCP
199
+ listens on the loopback at 9110, so nothing beyond the machine reaches
200
+ a new ground. A server names its bind through the hand. The web names
201
+ `listener` in its `faculties` and listens on no port. An entry of the
202
+ same name you land stands in their place, so a port changes through the
203
+ hand. Name
204
+ `listener` there too, since the web serves the listener it calls.
205
+
206
+ **A port another process holds keeps that carry down with why.** The
207
+ ground boots beside it until the hand moves the port. A `tcp` entry
208
+ whose args name no port only dials, and its invitations name no TCP
209
+ address. Its environment names only `NERVUR_STATE`, `NERVUR_UNLOCK` and
210
+ `NERVUR_HAND`.
140
211
 
141
212
  **Its key rests in `state/key`, its owner's alone.** A key file others
142
213
  may read is refused, as ssh refuses a key. On macOS,
@@ -144,10 +215,17 @@ may read is refused, as ssh refuses a key. On macOS,
144
215
  instead. Keep `state/` as you keep an ssh key: whoever holds the key and
145
216
  the ledger holds the ground.
146
217
 
147
- **It keeps its memory as a ledger.** Every write is one line, chained to
148
- the line before it and synced before it counts. A ledger edited anywhere
149
- but its end is refused. A witness file kept on another disk refuses a
150
- restored copy that is behind.
218
+ **It keeps its memory as a ledger, which holds the folder's lock.** Every
219
+ write is one line, chained to the line before it and synced before it
220
+ counts. A ledger edited anywhere but its end is refused. A witness file
221
+ kept on another disk refuses a restored copy that is behind. While the
222
+ ledger stands, a second ground on the same state is refused.
223
+
224
+ **Its shell runs a command for the hand alone.** `nervur shell run --
225
+ <command>` runs it in the code folder, and answers its exit code and
226
+ what it printed. Its environment holds `PATH`, `HOME` and a few more of
227
+ the process's, and never a secret or a `NERVUR_` variable. No pilot and
228
+ no being reaches it.
151
229
 
152
230
  **It runs a program as a faculty, in any language.** The bridge starts a
153
231
  program and speaks one JSON value a line on its standard streams. The
@@ -179,21 +257,20 @@ The Worker binds the object's class as `GROUND`, with SQLite storage.
179
257
  no `from`. A house's entry names its code with `classes: { faculty:
180
258
  'bundle', at: 'shop' }`.
181
259
 
182
- **Its settings are the Worker's.**
260
+ **Its Worker holds two secrets and nothing else.**
183
261
 
184
- | Setting | What it sets |
262
+ | Variable | What it names |
185
263
  | --- | --- |
186
264
  | `NERVUR_SECRET` | the ground's key, sixty-four hex digits; set as a secret |
187
265
  | `NERVUR_HAND` | the key its hand answers; set as a secret |
188
- | `NERVUR_ADDRESSES` | the addresses it is reached at, by commas, the held line first |
189
- | `NERVUR_ORIGINS` | the page origins it answers |
190
- | `NERVUR_ALLOW_PRIVATE` | `1` to let it dial a private address |
191
- | `NERVUR_WAIT` | the longest any ask may run, a minute where unset |
192
266
 
193
267
  A Worker learns no name it is reached by, so it writes only the
194
- addresses `NERVUR_ADDRESSES` names. These settings give the default
195
- entry of its web carry, and an entry in the drawer stands in its place.
196
- Its faculties take their secrets from its drawer, as on every ground.
268
+ addresses its `web` entry names, the held line first. Name them through
269
+ the hand with `facultiesUpdate`, as `addresses` in the args of `web`,
270
+ beside its `origins` and `allowPrivate`, and the dock keeps them.
271
+ Every ask runs a minute at most, and `waitSet` keeps a shorter bound.
272
+ Its faculties take their secrets from the dock, as on every ground. It
273
+ has no shell.
197
274
 
198
275
  **Its hand answers its key alone.** The hand is `POST /nervur/hand`, with
199
276
  the key as a bearer token and one request as the body. Every other
@@ -215,18 +292,19 @@ the object. A watch held on that line answers the moment the change lands.
215
292
 
216
293
  **A BrowserGround is one ground for its origin.** Every tab and the
217
294
  service worker of an origin share it. The tab holding a Web Lock runs
218
- the ground, and every other tab reaches its hand. It only dials, so it
219
- answers no face. Whoever serves the origin's script possesses the
220
- ground, so its origin serves nothing else.
295
+ the ground, and its hand answers every other tab over a channel. It
296
+ only dials, so it answers no face. Whoever serves the origin's script
297
+ possesses the ground, so its origin serves nothing else.
221
298
  [The package's start](README.md) opens one in a page.
222
299
 
223
300
  **Its registry is the one the page hands.** `BrowserGround.open({
224
301
  registry })` joins it to the terrain's own, whose faculty `origin`
225
302
  serves each house its classes from the origin.
226
303
 
227
- **An AppGround is a BrowserGround with the shell's bodies.** The shell
228
- hands in two small interfaces, one for secrets and one for a store, and
229
- the library holds the rest. The ground's key rests in the secret store.
304
+ **An AppGround is a BrowserGround with the shell's bodies.** The app's
305
+ native shell hands in two small interfaces, one for secrets and one for
306
+ a store, and the library holds the rest. The ground's key rests in the
307
+ secret store.
230
308
 
231
309
  ## BenchGround
232
310
 
package/README.md CHANGED
@@ -96,8 +96,8 @@ Run the ground in the folder, and leave it running.
96
96
  npx nervur up .
97
97
  ```
98
98
 
99
- In a second shell, add the house once. The ground keeps its entry in its
100
- sealed drawer, and opens it again at every start.
99
+ In a second shell, add the house once. The ground keeps its entry as a
100
+ sealed cell of its dock, and opens it again at every start.
101
101
 
102
102
  ```bash
103
103
  npx nervur houses add name=main classes='{"faculty":"folder","at":"house"}'
@@ -130,8 +130,7 @@ const ground = await BrowserGround.open();
130
130
 
131
131
  // The house's code is a module on this origin, and its rows rest sealed in IndexedDB.
132
132
  await ground.hand({
133
- faculty: 'houses',
134
- method: 'add',
133
+ method: 'housesAdd',
135
134
  args: { name: 'main', classes: { faculty: 'origin', at: '/house/index.js' } },
136
135
  });
137
136
 
@@ -154,10 +153,11 @@ keeps. [Writing for nervur](AUTHORING.md) shows both.
154
153
  | Entry | For |
155
154
  | --- | --- |
156
155
  | `nervur/being` | writing a being: `Being`, `s`, `need`, `tableOf`, `Args`, `Result`, `Json`, `Table` |
157
- | `nervur` | any engine: `Ground`, `House` and the bodies they take |
156
+ | `nervur` | any engine: `Ground`, `House`, `DockPilot`, `vouchesOf` and the bodies they take |
158
157
  | `nervur/node` | a ground on Node: `NodeGround`, the `nervur` command, and its bodies |
159
158
  | `nervur/browser` | a ground in a page or its service worker: `BrowserGround` and its bodies |
160
- | `nervur/app` | a ground in a phone's app: `AppGround`, and the two interfaces its shell fills |
159
+ | `nervur/edge` | a ground on the edge, a Worker and its Durable Object: `EdgeGround` and its bodies |
160
+ | `nervur/app` | a ground in a phone's app: `AppGround`, and the three interfaces its shell fills: `NativeShell`, `NativeSecrets` and `NativeStore` |
161
161
  | `nervur/serve` | a faculty's program in JavaScript: `serve` |
162
162
  | `nervur/bench` | tests: `Bench`, `BenchGround` and `FakeNetwork` |
163
163
 
@@ -5,7 +5,6 @@ import { FakeUnlock } from './fake-unlock.ts';
5
5
  import { SeededCrypto } from './seeded.ts';
6
6
  type BeingClass = ConstructorParameters<typeof ClassList>[0]['steward'];
7
7
  type Entry = Parameters<Ground['add']>[1];
8
- type HandAsk = Parameters<Ground['ask']>[0];
9
8
  type Standing = Awaited<ReturnType<Ground['add']>>;
10
9
  /** A house module: what a folder's index exports. */
11
10
  export interface HouseModule {
@@ -61,8 +60,9 @@ export declare class BenchGround {
61
60
  add(name: string, from?: string | Entry, rest?: Omit<Entry, 'classes'>): Promise<Standing>;
62
61
  remove(name: string): Promise<void>;
63
62
  list(): readonly Standing[];
64
- ask(request: HandAsk): ReturnType<Ground['ask']>;
65
- /** The ground's hand, as its owner holds it: `describe`, a faculty's method, or an ask of a being. */
63
+ /** A being in a house asked as `root`, the house's steward where no id is named. */
64
+ ask(request: Parameters<Ground['ask']>[0]): ReturnType<Ground['ask']>;
65
+ /** The ground's hand, as its owner holds it: `describe`, or an ask of a being in a house, or of the dock's steward where no house is named. */
66
66
  hand(request: Parameters<Ground['hand']>[0]): ReturnType<Ground['hand']>;
67
67
  /**
68
68
  * A request to the ground's one listener, with no socket: each faculty's
@@ -3,12 +3,14 @@
3
3
  // and its memory in a machine that outlives it, and opens house modules by
4
4
  // name. So a test turns it off, opens it again on the same machine, or
5
5
  // moves the machine to another host.
6
+ import { s } from '../being/index.js';
6
7
  import { ClassList, Ground } from '../index.js';
7
8
  import { FakeMemory } from './fake-memory.js';
8
9
  import { clockOf } from './fake-network.js';
9
10
  import { FakeUnlock } from './fake-unlock.js';
10
11
  import { SeededCrypto } from './seeded.js';
11
12
  // Registries' faculties as one, the first first: a name one holds is refused to the next, never replaced.
13
+ const NONE = { args: s.object({}) };
12
14
  const joined = (...each) => {
13
15
  const all = {};
14
16
  for (const faculties of each) {
@@ -67,13 +69,24 @@ export class BenchGround {
67
69
  const living = Object.fromEntries(Object.entries(faculties).map(([name, { kinds: _kinds, ...body }]) => [name, { up: () => body }]));
68
70
  const own = {
69
71
  faculties: {
70
- 'bench-unlock': { up: () => ({ serves: 'unlock', object: machine.unlock }) },
71
- 'bench-memory': { up: () => ({ serves: 'memory', object: machine.memory }) },
72
- seeded: { up: () => ({ serves: 'crypto', object: machine.crypto }) },
73
- 'bench-clock': { up: () => ({ serves: 'clock', object: clockOf(network) }) },
74
- 'bench-carry': { up: () => ({ serves: 'carry', schemes: ['bench'], object: network.join(host, { names, listens: names.length > 0, serve: (request) => this.fetch(request) }) }) },
75
- fake: { up: () => ({ serves: 'memory', house: ({ house }) => machine.memoryOf(house) }) },
72
+ 'bench-unlock': { takes: NONE, up: () => ({ serves: 'unlock', object: machine.unlock }) },
73
+ 'bench-memory': { takes: NONE, up: () => ({ serves: 'memory', object: machine.memory }) },
74
+ seeded: { takes: NONE, up: () => ({ serves: 'crypto', object: machine.crypto }) },
75
+ // The test holds the hand in its own process, so this hand listens on nothing.
76
+ 'bench-hand': { takes: NONE, up: () => ({ serves: 'hand' }) },
77
+ 'bench-clock': { takes: NONE, up: () => ({ serves: 'clock', object: clockOf(network) }) },
78
+ // Its listener on the network is the ground's one, which its entry names in `faculties`.
79
+ 'bench-carry': {
80
+ takes: NONE,
81
+ up: ({ faculties: called }) => {
82
+ const listener = called.listener;
83
+ const serve = async (request) => (await listener.fetch(request)) ?? new Response(null, { status: 404 });
84
+ return { serves: 'carry', schemes: ['bench'], object: network.join(host, { names, listens: names.length > 0, serve }) };
85
+ },
86
+ },
87
+ fake: { takes: NONE, up: () => ({ serves: 'memory', house: ({ house }) => machine.memoryOf(house) }) },
76
88
  module: {
89
+ takes: NONE,
77
90
  up: () => ({
78
91
  serves: 'classes',
79
92
  house: ({ args }) => {
@@ -89,8 +102,8 @@ export class BenchGround {
89
102
  this.#ground = await Ground.open({
90
103
  // The bench's own faculties, then the test's beside them, never in their place.
91
104
  registry: { faculties: joined(own.faculties ?? {}, living, registry.faculties ?? {}) },
92
- primordial: { unlock: { make: 'bench-unlock' }, memory: { make: 'bench-memory' }, crypto: { make: 'seeded' }, tools: { make: 'strict' } },
93
- entries: { clock: { make: 'bench-clock' }, carry: { make: 'bench-carry' }, module: { make: 'module' }, fake: { make: 'fake' } },
105
+ primordial: { unlock: { make: 'bench-unlock' }, memory: { make: 'bench-memory' }, crypto: { make: 'seeded' }, tools: { make: 'strict' }, clock: { make: 'bench-clock' }, hand: { make: 'bench-hand' } },
106
+ entries: { listener: { make: 'listener' }, carry: { make: 'bench-carry', faculties: ['listener'] }, module: { make: 'module' }, fake: { make: 'fake' } },
94
107
  });
95
108
  // Each living faculty stands on its first up, as its owner would stand it through the hand.
96
109
  for (const [name, { kinds }] of Object.entries(faculties)) {
@@ -122,10 +135,11 @@ export class BenchGround {
122
135
  list() {
123
136
  return this.#live().list();
124
137
  }
138
+ /** A being in a house asked as `root`, the house's steward where no id is named. */
125
139
  ask(request) {
126
140
  return this.#live().ask(request);
127
141
  }
128
- /** The ground's hand, as its owner holds it: `describe`, a faculty's method, or an ask of a being. */
142
+ /** The ground's hand, as its owner holds it: `describe`, or an ask of a being in a house, or of the dock's steward where no house is named. */
129
143
  hand(request) {
130
144
  return this.#live().hand(request);
131
145
  }
@@ -39,16 +39,16 @@ export interface BrowserGroundOptions {
39
39
  /** Whether it dials private and loopback addresses, as a page on localhost does. */
40
40
  readonly allowPrivate?: boolean;
41
41
  }
42
- /** One request of the hand, as every terrain serves it. */
42
+ /** One request of the hand, as every terrain serves it: `describe`, or an ask of a being in the house it names, or of the dock's steward where it names none. */
43
43
  export interface BrowserHandRequest {
44
44
  readonly describe?: true;
45
- readonly faculty?: string;
46
45
  readonly house?: string;
47
46
  readonly id?: string;
48
47
  readonly method?: string;
49
48
  readonly args?: Json;
50
49
  readonly after?: Answer;
51
50
  readonly cells?: true;
51
+ readonly call?: string;
52
52
  }
53
53
  export declare class BrowserGround {
54
54
  #private;
@@ -65,8 +65,8 @@ export declare class BrowserGround {
65
65
  /** Resolves once this page runs the ground, and rejects where its boot failed. */
66
66
  led(): Promise<void>;
67
67
  /**
68
- * The hand: `describe`, a faculty's method, or an ask of a being in a
69
- * named house, answered by whichever page runs the ground.
68
+ * The hand: `describe`, or an ask of a being in a named house or of the
69
+ * dock's steward, answered by whichever page runs the ground.
70
70
  */
71
71
  hand(request: BrowserHandRequest): Promise<Answer | {
72
72
  readonly describe: Json;
@@ -6,6 +6,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
+ import { s } from '../being/schema.js';
9
10
  import { WebCarry } from '../bodies/web-carry.js';
10
11
  import { Ground, joinedRegistry } from '../ground/ground.js';
11
12
  import { IndexedDbMemory } from './indexeddb-memory.js';
@@ -19,6 +20,7 @@ let joinOn;
19
20
  /** The origin's ground on stores of the terrain's own: an AppGround's, or a proof's in memory. No entry exports it. */
20
21
  export const openOn = (options, stores) => joinOn(options, stores);
21
22
  const NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
23
+ const NONE = { args: s.object({}) };
22
24
  const random = () => Array.from(crypto.getRandomValues(new Uint8Array(8)), (byte) => byte.toString(16).padStart(2, '0')).join('');
23
25
  const defaults = (name) => ({
24
26
  locks: navigator.locks,
@@ -92,8 +94,8 @@ export class BrowserGround {
92
94
  return this.#leading;
93
95
  }
94
96
  /**
95
- * The hand: `describe`, a faculty's method, or an ask of a being in a
96
- * named house, answered by whichever page runs the ground.
97
+ * The hand: `describe`, or an ask of a being in a named house or of the
98
+ * dock's steward, answered by whichever page runs the ground.
97
99
  */
98
100
  async hand(request) {
99
101
  if (this.#closed)
@@ -127,8 +129,6 @@ export class BrowserGround {
127
129
  this.#say({ kind: 'up', leader: this.#self });
128
130
  else if (said.kind === 'up')
129
131
  this.#heard(said.leader);
130
- else if (said.kind === 'ask' && said.to === this.#self)
131
- void this.#answer(said.id, said.request);
132
132
  else if (said.kind === 'answer') {
133
133
  const pending = this.#pending.get(said.id);
134
134
  this.#pending.delete(said.id);
@@ -136,10 +136,6 @@ export class BrowserGround {
136
136
  }
137
137
  };
138
138
  }
139
- async #answer(id, request) {
140
- const answer = this.#ground === undefined ? { error: { message: 'the ground moved to another page; ask again' } } : await this.#local(request);
141
- this.#say({ kind: 'answer', id, answer });
142
- }
143
139
  // A new page runs the ground: asks sent to the one before may never be answered, so they fail, and whoever waited goes on.
144
140
  #heard(leader) {
145
141
  if (leader !== this.#leader)
@@ -162,13 +158,20 @@ export class BrowserGround {
162
158
  #queue() {
163
159
  this.#platform.locks
164
160
  .request(`${this.#name}-ground`, { signal: this.#abort.signal }, async () => {
161
+ let booted;
165
162
  try {
166
- this.#ground = await this.#boot();
163
+ booted = await this.#boot();
167
164
  }
168
165
  catch (error) {
169
166
  this.#failed(error);
170
167
  return;
171
168
  }
169
+ // A page that closed while its boot ran lets the ground go at once, and the lock with it.
170
+ if (this.#closed) {
171
+ await booted.close();
172
+ return;
173
+ }
174
+ this.#ground = booted;
172
175
  this.#heard(this.#self);
173
176
  this.#say({ kind: 'up', leader: this.#self });
174
177
  this.#led();
@@ -189,25 +192,60 @@ export class BrowserGround {
189
192
  const own = {
190
193
  faculties: {
191
194
  'store-unlock': {
195
+ takes: NONE,
192
196
  up: async () => {
193
197
  const unlock = await stores.unlock(`${name}-unlock`);
194
198
  return { serves: 'unlock', object: unlock, down: closing(unlock) };
195
199
  },
196
200
  },
197
201
  'store-memory': {
202
+ takes: NONE,
198
203
  up: async () => {
199
204
  const memory = await stores.memory(`${name}-ground`);
200
205
  return { serves: 'memory', object: memory, down: closing(memory) };
201
206
  },
202
207
  },
208
+ // The hand, for every other page of the origin: the asks sent to this page on the channel, each answered there.
209
+ 'channel-hand': {
210
+ takes: { args: s.object({ channel: s.string(), self: s.string(), persisted: s.boolean() }) },
211
+ up: ({ args, faculties }) => {
212
+ const ground = faculties.ground;
213
+ const channel = platform.channel(args.channel);
214
+ let open = true;
215
+ channel.onmessage = (event) => {
216
+ const said = event.data;
217
+ if (said.kind !== 'ask' || said.to !== args.self)
218
+ return;
219
+ // An ask this hand's close ended is answered by no one here: the page that runs the ground next is asked again.
220
+ void ground.hand(said.request).then((answer) => {
221
+ if (!open)
222
+ return;
223
+ channel.postMessage({
224
+ kind: 'answer',
225
+ id: said.id,
226
+ answer: said.request.describe === true && 'result' in answer ? { result: { ...answer.result, persisted: args.persisted === true } } : answer,
227
+ });
228
+ });
229
+ };
230
+ return {
231
+ serves: 'hand',
232
+ down: () => {
233
+ open = false;
234
+ channel.close();
235
+ },
236
+ };
237
+ },
238
+ },
203
239
  // It only dials, so it writes no address into an invitation.
204
240
  web: {
241
+ takes: { args: s.object({ allowPrivate: s.optional(s.boolean()) }) },
205
242
  up: ({ args }) => {
206
243
  const web = new WebCarry({ allowPrivate: args.allowPrivate === true });
207
244
  return { serves: 'carry', schemes: ['https', 'http', 'wss', 'ws'], object: web };
208
245
  },
209
246
  },
210
247
  origin: {
248
+ takes: NONE,
211
249
  up: () => ({
212
250
  serves: 'classes',
213
251
  house: ({ args }) => {
@@ -222,9 +260,15 @@ export class BrowserGround {
222
260
  const wait = this.#options.wait;
223
261
  return Ground.open({
224
262
  registry: joinedRegistry(own, this.#options.registry ?? {}),
225
- primordial: { unlock: { make: 'store-unlock' }, memory: { make: 'store-memory' }, crypto: { make: 'noble' }, tools: { make: 'strict' } },
226
- entries: {
263
+ primordial: {
264
+ unlock: { make: 'store-unlock' },
265
+ memory: { make: 'store-memory' },
266
+ crypto: { make: 'noble' },
267
+ tools: { make: 'strict' },
227
268
  clock: { make: 'clock' },
269
+ hand: { make: 'channel-hand', args: { channel: `${name}-hand`, self: this.#self, persisted: this.#persisted }, faculties: ['ground'] },
270
+ },
271
+ entries: {
228
272
  origin: { make: 'origin' },
229
273
  web: { make: 'web', args: this.#options.allowPrivate === true ? { allowPrivate: true } : {} },
230
274
  },