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
@@ -1,787 +0,0 @@
1
- import { blueprintOf } from '../being/need.js';
2
- import { s } from '../being/schema.js';
3
- import { offered } from '../being/table.js';
4
- import { JoinedCarry } from '../bodies/joined-carry.js';
5
- import { NobleCrypto } from '../bodies/noble-crypto.js';
6
- import { SeedKeys } from '../bodies/seed-keys.js';
7
- import { StrictTools } from '../bodies/strict-tools.js';
8
- import { WebClock } from '../bodies/web-clock.js';
9
- import { openHouse } from '../house/house.js';
10
- import { DOCK, dockClasses, GroundNeed, IDS, KINDS, ListenerNeed } from './dock.js';
11
- import { HouseCarry, HouseClock, LadderCarry, SealedMemory, ViewMemory } from './views.js';
12
- const NONE = { args: s.object({}) };
13
- /** The faculties every terrain holds: the library's crypto, tools, and the clock of every web engine. */
14
- const libraryRegistry = {
15
- faculties: {
16
- noble: { takes: NONE, up: () => ({ serves: 'crypto', object: new NobleCrypto() }) },
17
- strict: { takes: NONE, up: () => ({ serves: 'tools', object: new StrictTools() }) },
18
- clock: { takes: NONE, up: () => ({ serves: 'clock', object: new WebClock() }) },
19
- },
20
- };
21
- /**
22
- * Registries joined as one rung, the first first. A name one holds is
23
- * refused to the next, never replaced. No entry exports it; every shipped
24
- * ground joins its first rung with it.
25
- */
26
- export const joinedRegistry = (...registries) => {
27
- const faculties = {};
28
- for (const registry of registries) {
29
- for (const [name, faculty] of Object.entries(registry.faculties ?? {})) {
30
- if (Object.hasOwn(faculties, name))
31
- throw new TypeError(`the registry names a faculty ${name}, which the ground holds already`);
32
- faculties[name] = faculty;
33
- }
34
- }
35
- return { faculties };
36
- };
37
- // What a house's entry names for its memory and its code, and what every house receives.
38
- const FOR_HOUSES = new Set(['memory', 'classes']);
39
- const EVERY_HOUSE = new Set(['carry']);
40
- const PRIMORDIAL = new Set(['unlock', 'crypto', 'tools', 'clock', 'ground', 'hand']);
41
- /** The library's own faculties: the ground's work, which the host never names, and the ground's one listener. */
42
- const GROUND = 'ground';
43
- const LISTENER = 'listener';
44
- // A foundation body's args for one house: everything its part of the entry names but the faculty.
45
- const argsOf = ({ faculty: _faculty, ...args }) => args;
46
- const message = (error) => (error instanceof Error ? error.message : String(error));
47
- // The memory a primordial faculty receives: none, since the dock is not open yet.
48
- const unopened = {
49
- read: () => Promise.reject(new Error('a primordial faculty keeps nothing')),
50
- list: () => Promise.reject(new Error('a primordial faculty keeps nothing')),
51
- write: () => Promise.reject(new Error('a primordial faculty keeps nothing')),
52
- };
53
- // One primordial body raised on its entry, filling the contract it names, or the reason the ground cannot open.
54
- const primordial = async (registry, role, entry, raised) => {
55
- const faculty = entry.make === undefined ? undefined : registry.faculties?.[entry.make];
56
- if (faculty === undefined)
57
- throw new Error(`no faculty ${entry.make} in the ground’s registry for its ${role}`);
58
- // The tools are not up yet, so the library's own check holds its args.
59
- const why = faculty.takes?.args === undefined ? null : new StrictTools().check(faculty.takes.args, entry.args ?? {});
60
- if (why !== null)
61
- throw new TypeError(`the faculty ${entry.make} is refused for its ${role}: ${why.replace(/^the value/, 'its args')}`);
62
- const faculties = {};
63
- for (const callee of entry.faculties ?? []) {
64
- const object = raised.get(callee)?.object;
65
- if (object === undefined)
66
- throw new Error(`the ${role} calls ${callee}, which stands before it nowhere`);
67
- faculties[callee] = object;
68
- }
69
- const made = { name: role, args: entry.args ?? {}, secrets: {}, memory: unopened, faculties };
70
- await faculty.install?.(made);
71
- const body = await faculty.up(made);
72
- if (body.serves !== role || (body.object === undefined && role !== 'hand')) {
73
- await body.down?.();
74
- throw new Error(`the faculty ${entry.make} serves no ${role}`);
75
- }
76
- return body;
77
- };
78
- export class Ground {
79
- #options;
80
- #registry;
81
- #memory;
82
- #crypto;
83
- #tools;
84
- #keys;
85
- /** The primordial bodies by role, in the order they went up. */
86
- #primordial = new Map();
87
- #dock;
88
- #houses = new Map();
89
- #stood = new Map();
90
- /** Each asleep house opening, so two boxes at once open it once. */
91
- #waking = new Map();
92
- constructor(options) {
93
- this.#options = options;
94
- // The library's own faculties first: the ground's work and its listener, then the terrain's and the host's.
95
- this.#registry = joinedRegistry({
96
- faculties: {
97
- [GROUND]: { takes: NONE, up: () => ({ serves: 'ground', blueprint: GroundNeed, object: this.#work() }) },
98
- [LISTENER]: { takes: NONE, up: () => ({ blueprint: ListenerNeed, object: this.handler }) },
99
- },
100
- }, libraryRegistry, options.registry);
101
- }
102
- /**
103
- * A ground opened on its host's registry and entries: the primordial
104
- * bodies go up, the dock opens on them and stands the ladder and every
105
- * house, and the hand goes up.
106
- */
107
- static async open(options) {
108
- const ground = new Ground(options);
109
- try {
110
- await ground.#boot();
111
- }
112
- catch (error) {
113
- await ground.close();
114
- throw error;
115
- }
116
- return ground;
117
- }
118
- async #boot() {
119
- const { primordial: host } = this.#options;
120
- // The memory first, so a lock it takes holds before the key is drawn.
121
- for (const [role, entry] of [
122
- ['memory', host.memory],
123
- ['unlock', host.unlock],
124
- ['crypto', host.crypto],
125
- ['tools', host.tools],
126
- ['clock', host.clock],
127
- ]) {
128
- this.#primordial.set(role, await primordial(this.#registry, role, entry, this.#primordial));
129
- }
130
- this.#memory = this.#primordial.get('memory').object;
131
- this.#crypto = this.#primordial.get('crypto').object;
132
- this.#tools = this.#primordial.get('tools').object;
133
- this.#keys = new SeedKeys(await this.#primordial.get('unlock').object.key(), this.#crypto);
134
- this.#primordial.set('ground', await primordial(this.#registry, 'ground', { make: GROUND }, this.#primordial));
135
- await this.#openDock();
136
- const booted = await this.#counted(this.#dock, this.#dock.opened.ask({ method: 'boot' }));
137
- if ('error' in booted)
138
- throw new Error(`the dock did not boot: ${booted.error.message}`);
139
- await this.#grantDock();
140
- if (host.hand !== undefined)
141
- this.#primordial.set('hand', await primordial(this.#registry, 'hand', host.hand, this.#primordial));
142
- }
143
- // ---- the dock ----
144
- // The dock opened on the primordial bodies alone: its seed derived from the ground's key, its places in a view of the ground's memory, and the ladder's carry and clock as they come to stand.
145
- async #openDock(granted = []) {
146
- const memory = new ViewMemory(this.#memory, await this.#prefix(DOCK, DOCK));
147
- // A memory that holds places and no dock this key opens holds another ground's, which this key does not open.
148
- if ((await memory.list()).length === 0 && (await this.#memory.list()).length > 0)
149
- throw new Error('this memory holds no drawer this key opens');
150
- const clock = new HouseClock(this.#clock(), DOCK);
151
- const carry = new HouseCarry(new LadderCarry(() => this.#carry()));
152
- const work = this.#primordial.get('ground').object;
153
- const offers = [{ blueprint: GroundNeed, object: work, kinds: [KINDS.steward, KINDS.faculty, KINDS.house] }, ...granted];
154
- const opened = await openHouse({
155
- keys: new SeedKeys(this.#tools.hex(await this.#keys.derive('dock', 32)), this.#crypto),
156
- memory,
157
- classes: dockClasses(),
158
- carry,
159
- clock,
160
- crypto: this.#crypto,
161
- tools: this.#tools,
162
- }, offers);
163
- this.#dock = { opened, clock, carry, flight: new Set() };
164
- }
165
- /**
166
- * The dock opened again on each body its steward stands on, offered as
167
- * any body is offered to a house, with the kinds its twin's entry grants:
168
- * the terrain's shell, where it stands. The being that holds the offer is
169
- * borne then.
170
- */
171
- async #grantDock() {
172
- const dock = this.#dock;
173
- const read = await this.#counted(dock, dock.opened.ask({ method: 'grants' }));
174
- const names = 'result' in read ? read.result : [];
175
- const granted = this.#offers({ classes: { faculty: '' }, faculties: names.filter((name) => this.#stood.get(name)?.body !== undefined) });
176
- if (typeof granted === 'string' || granted.length === 0)
177
- return;
178
- await this.#counted(dock, dock.opened.ask({ method: 'housesList' }));
179
- await this.#closeDock();
180
- await this.#openDock(granted);
181
- // The dock opened again hooks its door to the carries the ladder stands, as the first did.
182
- await this.#hookDock();
183
- const born = await this.#counted(this.#dock, this.#dock.opened.ask({ method: 'granted' }));
184
- if ('error' in born)
185
- throw new Error(`the dock did not take its grants: ${born.error.message}`);
186
- }
187
- // The dock's door hooked to the carries standing now, and unhooked from those it held.
188
- async #hookDock() {
189
- const dock = this.#dock;
190
- if (dock === undefined)
191
- return;
192
- dock.hooked?.unlisten({ ward: dock.opened.ward });
193
- dock.hooked = undefined;
194
- const carry = this.#carry();
195
- if (typeof carry === 'string')
196
- return;
197
- await carry.listen({ ward: dock.opened.ward, door: (box) => this.#dockDoor(box) });
198
- dock.hooked = carry;
199
- }
200
- #dockDoor(box) {
201
- const dock = this.#dock;
202
- return dock === undefined ? Promise.resolve(null) : this.#counted(dock, dock.opened.door(box));
203
- }
204
- async #closeDock() {
205
- const dock = this.#dock;
206
- if (dock === undefined)
207
- return;
208
- this.#dock = undefined;
209
- dock.hooked?.unlisten({ ward: dock.opened.ward });
210
- dock.carry.close();
211
- dock.clock.close();
212
- await Promise.allSettled([...dock.flight]);
213
- }
214
- // A prefix of the ground's memory for one house, one faculty or the dock: a digest of its name, which says nothing of it.
215
- async #prefix(of, name) {
216
- const names = await this.#keys.derive('view-names', 32);
217
- const digest = await this.#crypto.sha256(new Uint8Array([...names, ...this.#tools.utf8(`${of}\n${name}`)]));
218
- return `${this.#tools.hex(digest.subarray(0, 8))}/`;
219
- }
220
- // A secret's value, read from its being's cells as the dock's own code, or nothing where none is kept.
221
- async #secret(name) {
222
- const dock = this.#dock;
223
- if (dock === undefined)
224
- return undefined;
225
- const read = await dock.opened.ask({ id: IDS.secret + name, cells: true });
226
- return 'result' in read && typeof read.result.value === 'string' ? read.result.value : undefined;
227
- }
228
- // The bound every house opens on where the steward names none: the one in her cells, or the terrain's.
229
- async #bound() {
230
- const read = this.#dock === undefined ? null : await this.#dock.opened.ask({ cells: true });
231
- const wait = read !== null && 'result' in read ? read.result.wait : undefined;
232
- return typeof wait === 'number' ? wait : this.#options.wait;
233
- }
234
- // ---- the ground's work, as its faculty offers it to the dock ----
235
- #work() {
236
- const answered = (run) => async (args) => {
237
- try {
238
- return { result: ((await run(args)) ?? null) };
239
- }
240
- catch (error) {
241
- return { error: { message: message(error) } };
242
- }
243
- };
244
- return {
245
- terrain: answered(() => ({
246
- entries: (this.#options.entries ?? {}),
247
- primordial: {
248
- ...Object.fromEntries(Object.entries(this.#options.primordial).map(([role, entry]) => [role, entry.make ?? ''])),
249
- [GROUND]: GROUND,
250
- },
251
- lazy: this.#options.lazy === true,
252
- ...(this.#options.wait === undefined ? {} : { wait: this.#options.wait }),
253
- })),
254
- check: answered(({ entry }) => {
255
- const registry = entry.from === undefined ? this.#registry : this.#stood.get(entry.from)?.body?.registry;
256
- const faculty = entry.make === undefined ? undefined : registry?.faculties?.[entry.make];
257
- const why = faculty === undefined ? undefined : this.#untaken(faculty, entry);
258
- return why === undefined ? {} : { why };
259
- }),
260
- raise: answered(({ name, entry, installed }) => this.#raise(name, entry, installed)),
261
- lower: answered(({ name, why }) => this.#lower(name, why)),
262
- open: answered(({ name, entry, seed, bound }) => this.#openHouse(name, entry, seed, bound)),
263
- close: answered(({ name, why }) => this.#closeHouse(name, why)),
264
- sleep: answered(({ name, entry, ward }) => this.#sleep(name, entry, ward)),
265
- catalog: answered(() => this.#catalog()),
266
- call: answered((call) => this.#callFaculty(call)),
267
- placesOut: answered(({ name, entry }) => this.#placesOut(name, entry)),
268
- placesIn: answered(({ name, entry, places }) => this.#placesIn(name, entry, places)),
269
- /** The hand, as a primordial faculty serves it to the owner. It is no method of the blueprint, so no being reaches it. */
270
- hand: (request) => this.hand(request),
271
- };
272
- }
273
- // ---- the ladder ----
274
- // A body standing under a name, or why there is none.
275
- #standing(name) {
276
- const stood = this.#stood.get(name);
277
- if (stood === undefined)
278
- return `no faculty ${name} stands here`;
279
- return stood.body ?? `the faculty ${name} is down: ${stood.why}`;
280
- }
281
- // Why an entry fails what its faculty takes: its args against the schema, or a secret it must name and does not.
282
- #untaken(faculty, entry) {
283
- const { args, secrets = {} } = faculty.takes ?? {};
284
- if (args !== undefined) {
285
- const why = this.#tools.check(args, entry.args ?? {});
286
- if (why !== null)
287
- return why.replace(/^the value/, 'its args');
288
- }
289
- for (const [secret, what] of Object.entries(secrets)) {
290
- if (!(entry.secrets ?? []).includes(secret))
291
- return `its entry names no secret ${secret}, which ${entry.make} takes: ${what}`;
292
- }
293
- return undefined;
294
- }
295
- // One body raised on its entry, where one stands under the name taken down first: installed where the entry moved, then up, or down and why.
296
- async #raise(name, entry, installed) {
297
- await this.#lower(name);
298
- const now = this.#tools.canonical(entry);
299
- let done = installed;
300
- const stood = await (async () => {
301
- const down = (why) => ({ entry, why });
302
- let registry = this.#registry;
303
- if (entry.from !== undefined) {
304
- const below = this.#standing(entry.from);
305
- if (typeof below === 'string')
306
- return down(`its registry: ${below}`);
307
- if (below.registry === undefined)
308
- return down(`the faculty ${entry.from} carries no registry`);
309
- registry = below.registry;
310
- }
311
- const faculty = entry.make === undefined ? undefined : registry.faculties?.[entry.make];
312
- if (faculty === undefined)
313
- return down(`no faculty ${entry.make} in ${entry.from ?? 'the ground’s registry'}`);
314
- const refused = this.#untaken(faculty, entry);
315
- if (refused !== undefined)
316
- return down(refused);
317
- const secrets = {};
318
- for (const secret of entry.secrets ?? []) {
319
- const value = await this.#secret(secret);
320
- if (value === undefined)
321
- return down(`no secret ${secret} is kept`);
322
- secrets[secret] = value;
323
- }
324
- const faculties = {};
325
- for (const callee of entry.faculties ?? []) {
326
- const called = this.#standing(callee);
327
- if (typeof called === 'string')
328
- return down(called);
329
- if (called.object === undefined)
330
- return down(`the faculty ${callee} offers no object to call`);
331
- faculties[callee] = called.object;
332
- }
333
- let raised;
334
- try {
335
- const memory = new SealedMemory(new ViewMemory(this.#memory, await this.#prefix('faculty', name)), await this.#keys.derive(`faculty:${name}`, 32), this.#crypto, this.#tools);
336
- const made = { name, args: entry.args ?? {}, secrets, memory, faculties };
337
- if (installed !== now) {
338
- await faculty.install?.(made);
339
- done = now;
340
- }
341
- raised = await faculty.up(made);
342
- const why = this.#refused(name, raised);
343
- if (why !== undefined) {
344
- await raised.down?.();
345
- return down(why);
346
- }
347
- return { entry, body: raised };
348
- }
349
- catch (error) {
350
- // A body raised and then refused lets go of what its `up` opened.
351
- await raised?.down?.();
352
- return down(`it did not stand: ${message(error)}`);
353
- }
354
- })();
355
- this.#stood.set(name, stood);
356
- await this.#hookDock();
357
- return { ...(done === undefined ? {} : { installed: done }), ...(stood.why === undefined ? {} : { why: stood.why }), ...(stood.body?.serves === undefined ? {} : { serves: stood.body.serves }), ...(stood.body?.port === undefined ? {} : { port: stood.body.port }) };
358
- }
359
- // A body taken down: it stands down with why where one is given, and is gone where none is.
360
- async #lower(name, why) {
361
- const stood = this.#stood.get(name);
362
- if (why === undefined)
363
- this.#stood.delete(name);
364
- else if (stood !== undefined)
365
- this.#stood.set(name, { entry: stood.entry, why });
366
- await stood?.body?.down?.();
367
- if (stood?.body?.serves === 'carry')
368
- await this.#hookDock();
369
- }
370
- // What a raised body may not be: an offer half made, a contract filled wrong, or a primordial one.
371
- #refused(name, raised) {
372
- if ((raised.blueprint === undefined) !== (raised.object === undefined) && raised.serves === undefined)
373
- return 'a body offers a blueprint and an object together, or neither';
374
- if (raised.blueprint !== undefined)
375
- offered(raised.blueprint, `the faculty ${name}`);
376
- if (raised.serves === undefined)
377
- return undefined;
378
- if (raised.blueprint !== undefined)
379
- return 'a body serves the house or offers beings, never both';
380
- if (PRIMORDIAL.has(raised.serves))
381
- return `the ${raised.serves} is primordial, and the drawer names none`;
382
- if (FOR_HOUSES.has(raised.serves) && typeof raised.house !== 'function')
383
- return `a body serving ${raised.serves} serves each house through house()`;
384
- if (raised.serves === 'memory' && raised.object !== undefined)
385
- return 'the ground’s memory is primordial, and the drawer names none';
386
- if (EVERY_HOUSE.has(raised.serves) && raised.object === undefined)
387
- return `a body serving the ${raised.serves} holds it as its object`;
388
- if (raised.serves === 'carry' && (raised.schemes === undefined || raised.schemes.length === 0))
389
- return 'a carry names the schemes it speaks';
390
- return undefined;
391
- }
392
- // ---- houses ----
393
- // The ground's carries joined by scheme, or why there is none.
394
- #carry() {
395
- const bySchemes = {};
396
- for (const stood of this.#stood.values()) {
397
- if (stood.body?.serves !== 'carry')
398
- continue;
399
- for (const scheme of stood.body.schemes ?? [])
400
- bySchemes[scheme] = stood.body.object;
401
- }
402
- if (Object.keys(bySchemes).length === 0)
403
- return 'no body serves the carry';
404
- return new JoinedCarry(bySchemes);
405
- }
406
- // The ground's one clock, a primordial body.
407
- #clock() {
408
- return this.#primordial.get('clock').object;
409
- }
410
- // What a house is offered: the faculties its entry names, each with the kinds its own entry grants.
411
- #offers(entry) {
412
- const offers = [];
413
- for (const name of entry.faculties ?? []) {
414
- const stood = this.#standing(name);
415
- if (typeof stood === 'string')
416
- return stood;
417
- const { blueprint, object, window } = stood;
418
- if (blueprint === undefined || object === undefined)
419
- return `the faculty ${name} offers beings nothing`;
420
- const kinds = this.#stood.get(name)?.entry.kinds;
421
- offers.push({
422
- blueprint,
423
- object,
424
- ...(window === undefined ? {} : { window }),
425
- ...(kinds === undefined ? {} : { kinds }),
426
- ...(stood.opened === undefined ? {} : { opened: stood.opened.bind(stood) }),
427
- });
428
- }
429
- return offers;
430
- }
431
- // What a foundation body serves one house, or why it serves it nothing.
432
- async #served(named, serves, house) {
433
- const stood = this.#standing(named.faculty);
434
- if (typeof stood === 'string')
435
- throw new Error(stood);
436
- if (stood.serves !== serves || stood.house === undefined)
437
- throw new Error(`the faculty ${named.faculty} serves no ${serves}`);
438
- return (await stood.house({ house, args: argsOf(named) }));
439
- }
440
- // The memory a house keeps its places in: what the body its entry names serves it, or its view of the ground's.
441
- async #houseMemory(name, entry) {
442
- if (entry.memory === undefined)
443
- return new ViewMemory(this.#memory, await this.#prefix('house', name));
444
- return this.#served(entry.memory, 'memory', name);
445
- }
446
- // One house opened on the bodies its entry names, or closed and why.
447
- async #open(name, entry, seed, bound) {
448
- try {
449
- const offers = this.#offers(entry);
450
- if (typeof offers === 'string')
451
- return { entry, why: offers };
452
- const hooked = this.#carry();
453
- if (typeof hooked === 'string')
454
- return { entry, why: hooked };
455
- const ticking = this.#clock();
456
- const memory = await this.#houseMemory(name, entry);
457
- const classes = await this.#served(entry.classes, 'classes', name);
458
- const clock = new HouseClock(ticking, name);
459
- const carry = new HouseCarry(hooked);
460
- const wait = entry.wait === undefined ? bound : bound === undefined ? entry.wait : Math.min(entry.wait, bound);
461
- const opened = await openHouse({ keys: new SeedKeys(seed, this.#crypto), memory, classes, carry, clock, crypto: this.#crypto, tools: this.#tools }, offers, wait === undefined ? {} : { wait });
462
- await hooked.listen({ ward: opened.ward, door: (box) => this.#door(name, box) });
463
- return { entry, opened, clock, carry, hooked, flight: new Set(), ...(bound === undefined ? {} : { bound }) };
464
- }
465
- catch (error) {
466
- return { entry, why: message(error) };
467
- }
468
- }
469
- // A house opened on its entry where it is not open on it and its bound: what it held closed first.
470
- async #openHouse(name, entry, seed, given) {
471
- const bound = given ?? (await this.#bound());
472
- const held = this.#houses.get(name);
473
- if (held?.opened !== undefined && this.#tools.canonical(held.entry) === this.#tools.canonical(entry) && held.bound === bound)
474
- return { ward: held.opened.ward };
475
- if (held !== undefined) {
476
- this.#houses.set(name, { entry: held.entry, why: 'it is opening again' });
477
- await this.#close(held);
478
- }
479
- const opened = await this.#open(name, entry, seed, bound);
480
- this.#houses.set(name, opened);
481
- return opened.opened === undefined ? { why: opened.why ?? 'it did not open' } : { ward: opened.opened.ward };
482
- }
483
- // A house closed through its views: it stands closed with why where one is given, and is gone where none is.
484
- async #closeHouse(name, why) {
485
- const held = this.#houses.get(name);
486
- if (held === undefined)
487
- return;
488
- if (why === undefined)
489
- this.#houses.delete(name);
490
- else
491
- this.#houses.set(name, { entry: held.entry, why });
492
- await this.#close(held);
493
- }
494
- // A house held closed until something reaches it, its door hooked unopened: the first box opens it, then goes in.
495
- async #sleep(name, entry, ward) {
496
- const carry = this.#carry();
497
- if (typeof carry === 'string') {
498
- this.#houses.set(name, { entry, why: carry });
499
- return;
500
- }
501
- this.#houses.set(name, { entry, asleep: ward, hooked: carry });
502
- await carry.listen({ ward, door: (box) => this.#door(name, box) });
503
- }
504
- // A house that stands closed until something reaches it, opened once through its being in the dock.
505
- async #woken(name) {
506
- const held = this.#houses.get(name);
507
- if (held?.asleep === undefined || this.#dock === undefined)
508
- return held;
509
- let waking = this.#waking.get(name);
510
- if (waking === undefined) {
511
- waking = this.#counted(this.#dock, this.#dock.opened.ask({ id: IDS.house + name, method: 'open', args: {} }));
512
- this.#waking.set(name, waking);
513
- void waking.finally(() => this.#waking.delete(name));
514
- }
515
- await waking;
516
- return this.#houses.get(name);
517
- }
518
- /** Every house that stands closed until reached, opened now, as a wake by alarm needs: each arms its own times again. */
519
- async wake() {
520
- await Promise.all([...this.#houses.keys()].map((name) => this.#woken(name)));
521
- // A clock that keeps its waits in a platform's alarm ends each one due.
522
- await this.#clock().alarm?.();
523
- }
524
- // A box for a house the ground still holds open, and nothing for one it closed.
525
- async #door(name, box) {
526
- const held = await this.#woken(name);
527
- return held?.opened === undefined ? null : this.#counted(held, held.opened.door(box));
528
- }
529
- // A call on a house, in flight until it ends, so closing the house waits for it.
530
- #counted(held, call) {
531
- held.flight?.add(call);
532
- const done = () => void held.flight?.delete(call);
533
- call.then(done, done);
534
- return call;
535
- }
536
- // Its door unhooked, its sends stopped and its waits run out, then every call in flight on it ended.
537
- async #close(held) {
538
- const ward = held.opened?.ward ?? held.asleep;
539
- if (ward !== undefined)
540
- held.hooked?.unlisten({ ward });
541
- held.carry?.close();
542
- held.clock?.close();
543
- await Promise.allSettled([...(held.flight ?? [])]);
544
- }
545
- // Every place of a house's memory, each entry's bytes as hex.
546
- async #placesOut(name, entry) {
547
- const store = await this.#houseMemory(name, entry);
548
- const places = {};
549
- for (const place of await store.list()) {
550
- const { entries } = await store.read({ place });
551
- places[place] = Object.fromEntries(Object.entries(entries).map(([key, bytes]) => [key, this.#tools.hex(bytes)]));
552
- }
553
- return { places };
554
- }
555
- // Places written into a house's memory that holds none: with none given, only whether it holds none.
556
- async #placesIn(name, entry, places) {
557
- const store = await this.#houseMemory(name, entry);
558
- if ((await store.list()).length > 0)
559
- throw new Error(`the memory for ${name} holds places already`);
560
- const writes = {};
561
- for (const [place, entries] of Object.entries(places)) {
562
- writes[place] = Object.fromEntries(Object.entries(entries).map(([key, text]) => {
563
- const bytes = this.#tools.bytes(text);
564
- if (bytes === null)
565
- throw new TypeError(`the place ${place} holds no hex under ${key}`);
566
- return [key, bytes];
567
- }));
568
- }
569
- if (Object.keys(writes).length === 0)
570
- return;
571
- if ((await store.write({ writes, expect: Object.fromEntries(Object.keys(writes).map((place) => [place, null])) })) === null)
572
- throw new Error(`the memory for ${name} refused the places`);
573
- }
574
- /** Every house of the dock, open with its ward, or closed and why. The dock is none of them. */
575
- list() {
576
- return [...this.#houses].map(([name, held]) => ({
577
- name,
578
- entry: held.entry,
579
- ...(held.opened === undefined ? (held.asleep === undefined ? {} : { ward: held.asleep }) : { ward: held.opened.ward }),
580
- ...(held.why === undefined ? {} : { why: held.why }),
581
- }));
582
- }
583
- // One ask of the dock's steward by the hand, answered, or thrown where it refused.
584
- async #steward(method, args) {
585
- const answer = await this.hand({ method, args });
586
- if ('error' in answer)
587
- throw new Error(answer.error.message);
588
- return 'result' in answer ? answer.result : null;
589
- }
590
- #standingOf(name) {
591
- const found = this.list().find((standing) => standing.name === name);
592
- if (found === undefined)
593
- throw new Error(`no house ${name} is here`);
594
- return found;
595
- }
596
- /** A house added through the dock and opened, as the hand adds one. */
597
- async add(name, entry) {
598
- await this.#steward('housesAdd', { name, ...entry });
599
- return this.#standingOf(name);
600
- }
601
- /** A new entry landed over a house's through the dock, and the house opened again on it. */
602
- async update(name, entry) {
603
- await this.#steward('housesUpdate', { name, ...entry });
604
- return this.#standingOf(name);
605
- }
606
- /** A house closed and its entry dropped through the dock. Its seed and its places stay. */
607
- async remove(name) {
608
- await this.#steward('housesRemove', { name });
609
- }
610
- /** A body raised through the dock on its entry, answering why it is down where it is. */
611
- async stand(name, entry) {
612
- return (await this.#steward('facultiesAdd', { name, ...entry }));
613
- }
614
- /** A new entry landed over a faculty's through the dock. */
615
- async restand(name, entry) {
616
- return (await this.#steward('facultiesUpdate', { name, ...entry }));
617
- }
618
- /** A body taken down and up again through the dock. */
619
- async restart(name) {
620
- return (await this.#steward('facultiesRestart', { name }));
621
- }
622
- /** A body taken down and its entry dropped through the dock, where no house and no body uses it. */
623
- async unstand(name) {
624
- await this.#steward('facultiesRemove', { name });
625
- }
626
- /** A being in a house asked as `root`, by her id, or the house's steward where none is named. */
627
- async ask({ house, ...request }) {
628
- const held = await this.#woken(house);
629
- if (held?.opened === undefined)
630
- return { error: { message: `no house ${house} is open here` } };
631
- return this.#counted(held, held.opened.ask(request));
632
- }
633
- /**
634
- * The ground's hand, one request at a time, as every terrain serves it:
635
- * `describe`, or an ask as `root` of a being in the house it names, or of
636
- * a being of the dock where it names none. It is the one road into the
637
- * ground: every change is an ask of the dock. A secret's cells are read
638
- * by no one through it.
639
- */
640
- async hand({ describe, house, ...request }) {
641
- if (describe === true)
642
- return { result: (await this.describe()) };
643
- if (house !== undefined)
644
- return this.ask({ house, ...request });
645
- const dock = this.#dock;
646
- if (dock === undefined)
647
- return { error: { message: 'the ground is closed' } };
648
- if (request.cells === true && request.id?.startsWith(IDS.secret) === true)
649
- return { error: { message: 'a secret’s cells are shown to no one' } };
650
- return this.#counted(dock, dock.opened.ask(request));
651
- }
652
- /**
653
- * What the ground's hand shows its owner: the asks the dock's steward
654
- * shows `root`, every body of the ladder with its methods, what it serves,
655
- * or why it is down, each method's description, args, result and hints,
656
- * and every house. A face reads it and needs no code of its own for any
657
- * of them.
658
- */
659
- async describe() {
660
- const shown = (value) => {
661
- const blueprint = blueprintOf(value) ?? value;
662
- return {
663
- blueprint: blueprint.name,
664
- methods: Object.fromEntries(Object.entries(blueprint.methods).map(([method, spec]) => [method, JSON.parse(JSON.stringify({ description: spec.description, args: spec.args, result: spec.result, hints: spec.hints }))])),
665
- };
666
- };
667
- const faculties = {};
668
- for (const [name, stood] of this.#stood) {
669
- if (stood.body === undefined)
670
- faculties[name] = { why: stood.why ?? 'down' };
671
- else if (stood.body.serves !== undefined)
672
- faculties[name] = { serves: stood.body.serves, methods: {} };
673
- else
674
- faculties[name] = stood.body.blueprint === undefined ? { methods: {} } : shown(stood.body.blueprint);
675
- }
676
- const read = this.#dock === undefined ? null : await this.#dock.opened.ask({});
677
- return { dock: read !== null && 'describe' in read ? read.describe : null, faculties, houses: this.list() };
678
- }
679
- // A method of a body of the ladder called by name, its args held to the method's schema. It holds no token of any house, so a call it makes back answers an error.
680
- async #callFaculty({ faculty: name, method, args = {} }) {
681
- const stood = this.#standing(name);
682
- if (typeof stood === 'string')
683
- throw new Error(stood);
684
- if (stood.blueprint === undefined || stood.object === undefined)
685
- throw new Error(`${name} offers no methods`);
686
- const blueprint = blueprintOf(stood.blueprint) ?? stood.blueprint;
687
- const spec = blueprint.methods[method];
688
- const run = stood.object[method];
689
- if (spec === undefined || typeof run !== 'function')
690
- throw new Error(`${name} has no method ${method}`);
691
- const why = this.#tools.check(spec.args, args);
692
- if (why !== null)
693
- throw new Error(why);
694
- const context = {
695
- id: this.#tools.hex(this.#crypto.random(16)),
696
- call: () => Promise.resolve({ error: { message: 'the hand holds no token' } }),
697
- describe: () => Promise.resolve({ error: { message: 'the hand holds no token' } }),
698
- };
699
- const answered = (await run.call(stood.object, args, context));
700
- if (typeof answered !== 'object' || answered === null || !('result' in answered || 'error' in answered))
701
- throw new Error(`${name} answered no answer`);
702
- if ('error' in answered)
703
- throw new Error(answered.error.message);
704
- return answered.result;
705
- }
706
- /**
707
- * The one handler the ground's listener chains: the hand's where it
708
- * serves one, then each standing body's in the ladder's order, read as
709
- * each request arrives, so a body raised while the ground runs is served
710
- * at once.
711
- */
712
- handler = {
713
- fetch: async (request) => {
714
- for (const handler of this.#handlers()) {
715
- const response = await handler.fetch(request);
716
- if (response !== null)
717
- return response;
718
- }
719
- return null;
720
- },
721
- upgrade: (request) => {
722
- for (const handler of this.#handlers()) {
723
- const taken = handler.upgrade?.(request);
724
- if (taken !== null && taken !== undefined)
725
- return taken;
726
- }
727
- return null;
728
- },
729
- };
730
- // The hand's handler, then each standing body's, which opens every house naming the body before it takes a request, so its tokens answer on a ground woken per event.
731
- #handlers() {
732
- const hand = this.#primordial.get('hand')?.handler;
733
- return [
734
- ...(hand === undefined ? [] : [hand]),
735
- ...[...this.#stood].flatMap(([name, stood]) => {
736
- const handler = stood.body?.handler;
737
- if (handler === undefined)
738
- return [];
739
- const naming = () => Promise.all([...this.#houses].flatMap(([house, held]) => ((held.entry.faculties ?? []).includes(name) ? [this.#woken(house)] : [])));
740
- return [
741
- {
742
- fetch: async (request) => {
743
- await naming();
744
- return handler.fetch(request);
745
- },
746
- upgrade: (request) => {
747
- void naming();
748
- return handler.upgrade?.(request) ?? null;
749
- },
750
- },
751
- ];
752
- }),
753
- ];
754
- }
755
- /** The hand down first, then every house closed through its views, the dock, every body in the reverse of the ladder, and the primordial last. */
756
- async close() {
757
- const hand = this.#primordial.get('hand');
758
- this.#primordial.delete('hand');
759
- await hand?.down?.();
760
- // Every being of the dock is read once as her steward reads her, so each first ask a change began has run before the dock's clock closes.
761
- const dock = this.#dock;
762
- if (dock !== undefined)
763
- await this.#counted(dock, dock.opened.ask({ method: 'housesList' }));
764
- const held = [...this.#houses.values()].reverse();
765
- this.#houses.clear();
766
- for (const one of held)
767
- await this.#close(one);
768
- await this.#closeDock();
769
- const stood = [...this.#stood.values()].reverse();
770
- this.#stood.clear();
771
- for (const one of stood)
772
- await one.body?.down?.();
773
- const primordials = [...this.#primordial.values()].reverse();
774
- this.#primordial.clear();
775
- for (const one of primordials)
776
- await one.down?.();
777
- }
778
- // ---- the catalogue ----
779
- // Every registry of the ladder, the ground's first, then each standing body's that carries one, with each faculty it holds and what it takes.
780
- #catalog() {
781
- const shown = (registry) => Object.entries(registry.faculties ?? {})
782
- .sort(([a], [b]) => (a < b ? -1 : 1))
783
- .map(([make, faculty]) => ({ make, ...(faculty.takes === undefined ? {} : { takes: JSON.parse(JSON.stringify(faculty.takes)) }) }));
784
- const bodies = [...this.#stood].flatMap(([name, stood]) => (stood.body?.registry === undefined ? [] : [{ from: name, faculties: shown(stood.body.registry) }]));
785
- return [{ faculties: shown(this.#registry) }, ...bodies];
786
- }
787
- }