nervur 0.20.0 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +229 -20
  2. package/dist/being/being.d.ts +1 -0
  3. package/dist/being/being.js +5 -1
  4. package/dist/being/faculty.d.ts +1 -0
  5. package/dist/being/faculty.js +13 -7
  6. package/dist/being/index.d.ts +1 -0
  7. package/dist/being/index.js +1 -0
  8. package/dist/being/kind.d.ts +4 -0
  9. package/dist/being/kind.js +28 -0
  10. package/dist/being/types.d.ts +3 -4
  11. package/dist/browser/index.d.ts +35 -0
  12. package/dist/browser/index.js +188 -0
  13. package/dist/browser/worker.d.ts +12 -0
  14. package/dist/browser/worker.js +40 -0
  15. package/dist/browser.d.ts +1 -0
  16. package/dist/browser.js +7 -0
  17. package/dist/cli/command.d.ts +13 -0
  18. package/dist/cli/command.js +237 -0
  19. package/dist/cli/harbor.d.ts +19 -0
  20. package/dist/cli/harbor.js +99 -0
  21. package/dist/cli/main.d.ts +2 -0
  22. package/dist/cli/main.js +14 -0
  23. package/dist/contract/index.d.ts +16 -2
  24. package/dist/contract/index.js +13 -3
  25. package/dist/edge/index.d.ts +57 -0
  26. package/dist/edge/index.js +306 -0
  27. package/dist/edge.d.ts +1 -0
  28. package/dist/edge.js +8 -0
  29. package/dist/folder/index.d.ts +9 -4
  30. package/dist/folder/index.js +89 -84
  31. package/dist/harbor/carrying.d.ts +17 -0
  32. package/dist/harbor/carrying.js +38 -0
  33. package/dist/harbor/catalogue.d.ts +27 -0
  34. package/dist/harbor/catalogue.js +101 -0
  35. package/dist/harbor/dock.d.ts +32 -3
  36. package/dist/harbor/dock.js +98 -12
  37. package/dist/harbor/harbor.d.ts +27 -8
  38. package/dist/harbor/harbor.js +217 -26
  39. package/dist/harbor/index.d.ts +8 -2
  40. package/dist/harbor/index.js +8 -1
  41. package/dist/harbor/package.d.ts +12 -0
  42. package/dist/harbor/package.js +130 -0
  43. package/dist/harbor/probe.d.ts +18 -0
  44. package/dist/harbor/probe.js +15 -0
  45. package/dist/harbor/registry.d.ts +6 -3
  46. package/dist/harbor/registry.js +83 -7
  47. package/dist/harbor/relay.d.ts +15 -0
  48. package/dist/harbor/relay.js +387 -0
  49. package/dist/harbor/root-line.d.ts +9 -0
  50. package/dist/harbor/root-line.js +38 -0
  51. package/dist/harbor/terrain.d.ts +14 -1
  52. package/dist/harbor/terrain.js +19 -0
  53. package/dist/http/index.d.ts +4 -0
  54. package/dist/http/index.js +156 -0
  55. package/dist/http/websocket.d.ts +29 -0
  56. package/dist/http/websocket.js +133 -0
  57. package/dist/index.d.ts +5 -4
  58. package/dist/index.js +12 -6
  59. package/dist/line/answer.d.ts +8 -0
  60. package/dist/line/answer.js +60 -0
  61. package/dist/line/frame.d.ts +34 -0
  62. package/dist/line/frame.js +81 -0
  63. package/dist/line/ground.d.ts +3 -0
  64. package/dist/line/ground.js +9 -0
  65. package/dist/line/index.d.ts +5 -0
  66. package/dist/line/index.js +9 -0
  67. package/dist/line/push.d.ts +7 -0
  68. package/dist/line/push.js +92 -0
  69. package/dist/line/web.d.ts +30 -0
  70. package/dist/line/web.js +181 -0
  71. package/dist/node/index.d.ts +32 -0
  72. package/dist/node/index.js +164 -0
  73. package/dist/node.d.ts +1 -0
  74. package/dist/node.js +7 -0
  75. package/dist/pointer/bodies.d.ts +11 -3
  76. package/dist/pointer/bodies.js +47 -18
  77. package/dist/pointer/index.d.ts +1 -1
  78. package/dist/pointer/index.js +1 -1
  79. package/dist/pointer/world.d.ts +12 -5
  80. package/dist/pointer/world.js +29 -14
  81. package/dist/quo/address.d.ts +19 -0
  82. package/dist/quo/address.js +84 -0
  83. package/dist/quo/door.d.ts +1 -1
  84. package/dist/quo/door.js +6 -4
  85. package/dist/quo/index.d.ts +1 -0
  86. package/dist/quo/index.js +1 -0
  87. package/dist/quo/invitation.d.ts +1 -0
  88. package/dist/quo/invitation.js +9 -5
  89. package/dist/quo/keys.js +1 -1
  90. package/dist/tcp/frame.d.ts +25 -0
  91. package/dist/tcp/frame.js +64 -0
  92. package/dist/tcp/index.d.ts +24 -0
  93. package/dist/tcp/index.js +260 -0
  94. package/dist/ward/index.d.ts +3 -2
  95. package/dist/ward/index.js +2 -1
  96. package/dist/ward/pilot.d.ts +8 -0
  97. package/dist/ward/pilot.js +50 -0
  98. package/dist/ward/stance.d.ts +4 -4
  99. package/dist/ward/stance.js +10 -7
  100. package/dist/ward/ward-being.d.ts +4 -1
  101. package/dist/ward/ward-being.js +8 -2
  102. package/dist/ward/ward.d.ts +11 -2
  103. package/dist/ward/ward.js +41 -5
  104. package/package.json +28 -2
  105. package/src/being/being.ts +5 -1
  106. package/src/being/faculty.ts +14 -7
  107. package/src/being/index.ts +1 -0
  108. package/src/being/kind.ts +30 -0
  109. package/src/being/types.ts +8 -6
  110. package/src/browser/index.ts +207 -0
  111. package/src/browser/worker.ts +66 -0
  112. package/src/browser.ts +9 -0
  113. package/src/cli/command.ts +224 -0
  114. package/src/cli/harbor.ts +103 -0
  115. package/src/cli/main.ts +13 -0
  116. package/src/contract/index.ts +42 -10
  117. package/src/edge/index.ts +359 -0
  118. package/src/edge.ts +10 -0
  119. package/src/folder/index.ts +87 -84
  120. package/src/harbor/carrying.ts +56 -0
  121. package/src/harbor/catalogue.ts +125 -0
  122. package/src/harbor/dock.ts +122 -15
  123. package/src/harbor/harbor.ts +240 -36
  124. package/src/harbor/index.ts +10 -3
  125. package/src/harbor/package.ts +147 -0
  126. package/src/harbor/probe.ts +34 -0
  127. package/src/harbor/registry.ts +73 -9
  128. package/src/harbor/relay.ts +399 -0
  129. package/src/harbor/root-line.ts +39 -0
  130. package/src/harbor/terrain.ts +31 -3
  131. package/src/http/index.ts +150 -0
  132. package/src/http/websocket.ts +124 -0
  133. package/src/index.ts +39 -6
  134. package/src/line/answer.ts +59 -0
  135. package/src/line/frame.ts +88 -0
  136. package/src/line/ground.ts +19 -0
  137. package/src/line/index.ts +9 -0
  138. package/src/line/push.ts +112 -0
  139. package/src/line/web.ts +227 -0
  140. package/src/node/index.ts +170 -0
  141. package/src/node.ts +9 -0
  142. package/src/pointer/bodies.ts +45 -16
  143. package/src/pointer/index.ts +1 -1
  144. package/src/pointer/world.ts +38 -19
  145. package/src/quo/address.ts +87 -0
  146. package/src/quo/door.ts +6 -4
  147. package/src/quo/index.ts +1 -0
  148. package/src/quo/invitation.ts +10 -6
  149. package/src/quo/keys.ts +1 -1
  150. package/src/stand/main.ts +1 -0
  151. package/src/stand/stand.ts +87 -6
  152. package/src/tcp/index.ts +284 -0
  153. package/src/ward/index.ts +3 -2
  154. package/src/ward/pilot.ts +50 -0
  155. package/src/ward/stance.ts +14 -8
  156. package/src/ward/ward-being.ts +9 -3
  157. package/src/ward/ward.ts +53 -6
@@ -0,0 +1,284 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Quo over TCP: `nervur/tcp`, an entry that names a platform, Node's
3
+ // sockets.
4
+ //
5
+ // TcpCarrier a Carrier that dials: bytes to a ward pk go in an ask
6
+ // frame to the `tcp://host:port` addresses its book holds
7
+ // for that pk, in their order
8
+ // TcpListener answers ask frames for the doors it is handed, a harbor
9
+ // among them, at a `tcp://` address
10
+ //
11
+ // A connection is opened per address, carries many asks at once, and is
12
+ // dropped when either side closes it; the next ask dials again. An ask
13
+ // goes to the next address only when the one before could not be dialed,
14
+ // since an ask a line took may have been heard.
15
+ import { connect, createServer, type Server, type Socket } from 'node:net';
16
+ import { hex, isHex, unhex } from '../crypto/index.ts';
17
+ import { Carrier } from '../contract/index.ts';
18
+ import { CarrierFaculty, UNDIALED, type Dialed, type Ground, type GroundFaculty } from '../harbor/index.ts';
19
+ import { tcpAddress, tcpAt, type TcpAddress } from '../quo/index.ts';
20
+ import { ownerAsk } from '../ward/index.ts';
21
+ import type { AskSpec, JsonObject } from '../being/index.ts';
22
+ import { askFrame, FrameReader, nothingFrame, replyFrame, TCP, TCP_OPENING } from '../line/index.ts';
23
+
24
+ // What a listener hands an ask to: bytes for a ward pk, or nothing.
25
+ export interface Doors {
26
+ arrive(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
27
+ }
28
+
29
+ const MAX_ID = 0xffff_ffff;
30
+
31
+ // The `tcp` addresses among some, in their order.
32
+ const tcpAddresses = (at: readonly string[]): TcpAddress[] => at.map(tcpAddress).filter((a): a is TcpAddress => a !== null);
33
+
34
+ class Line {
35
+ readonly #socket: Socket;
36
+ readonly #reader = new FrameReader();
37
+ readonly #waiting = new Map<number, (box: Uint8Array | null) => void>();
38
+ #next = 1;
39
+ closed = false;
40
+
41
+ constructor(socket: Socket, onClose: () => void) {
42
+ this.#socket = socket;
43
+ socket.on('data', (chunk: Buffer) => {
44
+ for (const f of this.#reader.push(new Uint8Array(chunk))) {
45
+ if (f.kind === 'ask') continue;
46
+ const resolve = this.#waiting.get(f.id);
47
+ this.#waiting.delete(f.id);
48
+ resolve?.(f.kind === 'reply' ? f.box : null);
49
+ }
50
+ if (this.#reader.bad) socket.destroy();
51
+ });
52
+ socket.on('error', () => socket.destroy());
53
+ socket.on('close', () => {
54
+ this.closed = true;
55
+ onClose();
56
+ for (const resolve of this.#waiting.values()) resolve(null);
57
+ this.#waiting.clear();
58
+ });
59
+ }
60
+
61
+ static open(host: string, port: number, onClose: () => void): Promise<Line | null> {
62
+ return new Promise((resolve) => {
63
+ const socket = connect({ host, port });
64
+ socket.once('connect', () => resolve(new Line(socket, onClose)));
65
+ socket.once('error', () => {
66
+ socket.destroy();
67
+ resolve(null);
68
+ });
69
+ });
70
+ }
71
+
72
+ ask(pk: Uint8Array, box: Uint8Array): Promise<Uint8Array | null> {
73
+ if (this.closed) return Promise.resolve(null);
74
+ const id = this.#next;
75
+ this.#next = this.#next === MAX_ID ? 1 : this.#next + 1;
76
+ return new Promise((resolve) => {
77
+ this.#waiting.set(id, resolve);
78
+ this.#socket.write(askFrame(id, pk, box));
79
+ });
80
+ }
81
+
82
+ close(): void {
83
+ this.#socket.destroy();
84
+ }
85
+ }
86
+
87
+ export class TcpCarrier extends Carrier {
88
+ readonly #routes = new Map<string, TcpAddress[]>();
89
+ readonly #lines = new Map<string, Promise<Line | null>>();
90
+
91
+ // Where a ward pk is reached, replacing any route before: its `tcp`
92
+ // addresses in order, the others skipped. A list with none is not
93
+ // taken; null forgets the route.
94
+ route(pk: string, at: readonly string[] | null): boolean {
95
+ if (!isHex(pk, 64)) return false;
96
+ if (at === null) return this.#routes.delete(pk) || true;
97
+ const mine = tcpAddresses(at);
98
+ if (mine.length === 0) return false;
99
+ this.#routes.set(pk, mine);
100
+ return true;
101
+ }
102
+
103
+ // Whether a ward pk has a route.
104
+ routed(pk: string): boolean {
105
+ return this.#routes.has(pk);
106
+ }
107
+
108
+ // Nothing where the pk has no route, no address is reached, or the
109
+ // connection closed before an answer.
110
+ carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
111
+ return this.#carry(this.#routes.get(pk) ?? [], pk, bytes);
112
+ }
113
+
114
+ // Bytes to a ward pk at the addresses named, as a route would carry them.
115
+ carryAt(at: readonly string[], pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
116
+ return isHex(pk, 64) ? this.#carry(tcpAddresses(at), pk, bytes) : Promise.resolve(null);
117
+ }
118
+
119
+ async #carry(addresses: readonly TcpAddress[], pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
120
+ for (const address of addresses) {
121
+ const out = await this.#dial(address, pk, bytes);
122
+ if (out !== UNDIALED) return out;
123
+ }
124
+ return null;
125
+ }
126
+
127
+ // Bytes to a ward pk at one address, or `UNDIALED` where no line opens.
128
+ dial(at: string, pk: string, bytes: Uint8Array): Promise<Dialed> {
129
+ const address = tcpAddress(at);
130
+ return address && isHex(pk, 64) ? this.#dial(address, pk, bytes) : Promise.resolve(UNDIALED);
131
+ }
132
+
133
+ async #dial(address: TcpAddress, pk: string, bytes: Uint8Array): Promise<Dialed> {
134
+ const line = await this.#line(address);
135
+ return line ? line.ask(unhex(pk), bytes) : UNDIALED;
136
+ }
137
+
138
+ #line({ host, port }: TcpAddress): Promise<Line | null> {
139
+ const at = tcpAt(host, port);
140
+ let line = this.#lines.get(at);
141
+ if (!line) {
142
+ line = Line.open(host, port, () => this.#lines.delete(at)).then((dialed) => {
143
+ if (!dialed) this.#lines.delete(at);
144
+ return dialed;
145
+ });
146
+ this.#lines.set(at, line);
147
+ }
148
+ return line;
149
+ }
150
+
151
+ async close(): Promise<void> {
152
+ const lines = [...this.#lines.values()];
153
+ this.#lines.clear();
154
+ for (const line of await Promise.all(lines)) line?.close();
155
+ }
156
+ }
157
+
158
+ export class TcpListener {
159
+ readonly #server: Server;
160
+ readonly #doors: Doors;
161
+ readonly #sockets = new Set<Socket>();
162
+
163
+ private constructor(doors: Doors) {
164
+ this.#doors = doors;
165
+ this.#server = createServer((socket) => this.#serve(socket));
166
+ }
167
+
168
+ // A listener on `host` and `port`, 127.0.0.1 and a free port unless named.
169
+ static listen(doors: Doors, host = '127.0.0.1', port = 0): Promise<TcpListener> {
170
+ const listener = new TcpListener(doors);
171
+ return new Promise((resolve, reject) => {
172
+ listener.#server.once('error', reject);
173
+ listener.#server.listen(port, host, () => resolve(listener));
174
+ });
175
+ }
176
+
177
+ // The `tcp://host:port` address this listener answers at.
178
+ get at(): string {
179
+ const address = this.#server.address() as { address: string; port: number };
180
+ return tcpAt(address.address, address.port);
181
+ }
182
+
183
+ #serve(socket: Socket): void {
184
+ this.#sockets.add(socket);
185
+ const reader = new FrameReader();
186
+ socket.on('data', (chunk: Buffer) => {
187
+ for (const f of reader.push(new Uint8Array(chunk))) {
188
+ if (f.kind === 'ask') void this.#answer(socket, f.id, f.pk, f.box);
189
+ }
190
+ if (reader.bad) socket.destroy();
191
+ });
192
+ socket.on('error', () => socket.destroy());
193
+ socket.on('close', () => this.#sockets.delete(socket));
194
+ }
195
+
196
+ async #answer(socket: Socket, id: number, pk: Uint8Array, box: Uint8Array): Promise<void> {
197
+ let reply: Uint8Array | null = null;
198
+ try {
199
+ reply = await this.#doors.arrive(hex(pk), box);
200
+ } catch {
201
+ reply = null;
202
+ }
203
+ if (!socket.destroyed) socket.write(reply === null ? nothingFrame(id) : replyFrame(id, reply));
204
+ }
205
+
206
+ close(): Promise<void> {
207
+ for (const socket of this.#sockets) socket.destroy();
208
+ return new Promise((resolve) => this.#server.close(() => resolve()));
209
+ }
210
+ }
211
+
212
+ export { TCP, TCP_OPENING };
213
+
214
+ type Listen = { host: string; port: number };
215
+ const listenOf = (value: unknown): Listen | null => {
216
+ if (typeof value !== 'object' || value === null) return null;
217
+ const { host, port } = value as Record<string, unknown>;
218
+ return typeof host === 'string' && Number.isInteger(port) && (port as number) >= 0 && (port as number) <= 65_535 ? { host, port: port as number } : null;
219
+ };
220
+
221
+ // Quo over TCP as a carrier faculty of one harbor. It dials every `tcp://`
222
+ // address the harbor routes, and listens where its cells say:
223
+ //
224
+ // listen { host?, port? } listen there from now on, 127.0.0.1 and a
225
+ // free port unless named, and on every wake
226
+ // at the port it bound
227
+ // deaf listen nowhere from now on
228
+ export const tcpFaculty: GroundFaculty = (ground: Ground) =>
229
+ class TcpFaculty extends CarrierFaculty {
230
+ static override readonly kind: string = TCP;
231
+ static override asks: Record<string, AskSpec> = {
232
+ listen: ownerAsk('listen for Quo over TCP, and on every wake', { properties: { host: { type: 'string' }, port: { type: 'integer' } } }),
233
+ deaf: ownerAsk('listen nowhere'),
234
+ };
235
+
236
+ readonly #dialer = new TcpCarrier();
237
+ #listener: TcpListener | undefined;
238
+
239
+ accepts(address: string): boolean {
240
+ return tcpAddress(address) !== null;
241
+ }
242
+
243
+ dial(address: string, pk: string, bytes: Uint8Array): Promise<Dialed> {
244
+ return this.#dialer.dial(address, pk, bytes);
245
+ }
246
+
247
+ override listening(): string[] {
248
+ return this.#listener ? [this.#listener.at] : [];
249
+ }
250
+
251
+ override async awake(): Promise<void> {
252
+ const at = listenOf(this.cells.listen);
253
+ if (at && !this.#listener) this.#listener = await TcpListener.listen(ground, at.host, at.port);
254
+ }
255
+
256
+ override async sleep(): Promise<void> {
257
+ await this.#dialer.close();
258
+ await this.#listener?.close();
259
+ this.#listener = undefined;
260
+ }
261
+
262
+ async listen(args: JsonObject): Promise<JsonObject> {
263
+ const at = listenOf({ host: args.host ?? '127.0.0.1', port: args.port ?? 0 });
264
+ if (at === null) return { error: 'host is text and port a port' };
265
+ await this.#listener?.close();
266
+ this.#listener = undefined;
267
+ try {
268
+ this.#listener = await TcpListener.listen(ground, at.host, at.port);
269
+ } catch (e) {
270
+ return { error: (e as Error).message };
271
+ }
272
+ // The port it bound, so a wake listens where every route and every
273
+ // invitation already points.
274
+ this.cells.listen = { host: at.host, port: tcpAddress(this.#listener.at)!.port };
275
+ return { at: this.#listener.at };
276
+ }
277
+
278
+ async deaf(): Promise<JsonObject> {
279
+ await this.#listener?.close();
280
+ this.#listener = undefined;
281
+ delete this.cells.listen;
282
+ return { deaf: true };
283
+ }
284
+ };
package/src/ward/index.ts CHANGED
@@ -3,5 +3,6 @@
3
3
  export { allow, CEILING, DEFAULT } from './allowance.ts';
4
4
  export { HEAD_ROW, Partition, type BeingRow, type Head, type StandingRow } from './partition.ts';
5
5
  export { DEPTH } from './stance.ts';
6
- export { DOCK, Ward, type Catalogue, type Lender, type WardParts } from './ward.ts';
7
- export { ownerAsk, pilots, WardBeing, type Steward, type WardBeingStance } from './ward-being.ts';
6
+ export { DOCK, Ward, type Catalogue, type Lender, type Routes, type WardParts } from './ward.ts';
7
+ export { ownerAsk, PILOT, pilots, rootAsk, WardBeing, type Steward, type WardBeingStance } from './ward-being.ts';
8
+ export { Pilot } from './pilot.ts';
@@ -0,0 +1,50 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The pilot: a kit being that holds an owner's relation on another ward's
3
+ // ward-being, so whoever pilots her ward pilots that ward from afar. The
4
+ // relation is an ordinary standing, kept in her ward's partition, so it
5
+ // moves its keys and survives a restart like any other.
6
+ //
7
+ // hold { invitation } the owner invitation taken, under `far`
8
+ // pilot { method?, args? } the far ward-being asked, as its owner
9
+ // drop the relation let go
10
+ //
11
+ // She is the kit's, of kind `org.nervur.pilot`, and answers whoever pilots
12
+ // her own ward alone.
13
+ import { Being, isSilence, isWord, told, type AskSpec, type JsonObject } from '../being/index.ts';
14
+ import { readInvitation } from '../quo/index.ts';
15
+ import { ownerAsk, PILOT } from './ward-being.ts';
16
+
17
+ // The standing id the far ward-being is held under.
18
+ const FAR = 'far';
19
+
20
+ const object = (v: unknown): v is JsonObject => typeof v === 'object' && v !== null && !Array.isArray(v);
21
+
22
+ export class Pilot extends Being {
23
+ static override readonly kind: string = PILOT;
24
+ static override asks: Record<string, AskSpec> = {
25
+ hold: ownerAsk('an owner invitation on a far ward, held from now on', { required: ['invitation'] }),
26
+ pilot: ownerAsk('the far ward-being, asked as its owner'),
27
+ drop: ownerAsk('the far ward let go'),
28
+ };
29
+
30
+ async hold(args: JsonObject): Promise<JsonObject> {
31
+ const invitation = readInvitation(args.invitation);
32
+ if (invitation === null) return { error: 'no invitation' };
33
+ if (this.stance.standings.get(FAR)) return { error: 'a ward is held' };
34
+ return (await this.stance.standings.take(FAR, invitation)) === null ? { error: 'not taken' } : { held: invitation.ward };
35
+ }
36
+
37
+ async pilot(args: JsonObject): Promise<JsonObject> {
38
+ if ((args.method !== undefined && typeof args.method !== 'string') || (args.args !== undefined && !object(args.args))) return { error: 'method is text, args an object' };
39
+ const standing = this.stance.standings.get(FAR);
40
+ if (!standing) return { error: 'no ward is held' };
41
+ const out = await standing.ask(args.method, args.args ?? {});
42
+ if (isSilence(out)) return { error: 'silence' };
43
+ if (isWord(out)) return { error: told(out) as string };
44
+ return { answer: out };
45
+ }
46
+
47
+ drop(): JsonObject {
48
+ return this.stance.standings.remove(FAR) ? { dropped: true } : { error: 'no ward is held' };
49
+ }
50
+ }
@@ -5,7 +5,7 @@
5
5
  import { readValue, writeValue } from '../crypto/index.ts';
6
6
  import { readInvitation, Standing as Wire, freshStanding, type Door, type Draw, type Invitation, type Read } from '../quo/index.ts';
7
7
  import type { Clock } from '../contract/index.ts';
8
- import { OWNER, silence, word, type Answer, type JsonObject, type Occupants, type Stance, type Standing, type Standings, type Wanted } from '../being/index.ts';
8
+ import { OWNER, ownKind, silence, word, type Answer, type JsonObject, type Occupants, type Stance, type Standing, type Standings, type Wanted } from '../being/index.ts';
9
9
  import { allow, Bell, LATE } from './allowance.ts';
10
10
  import type { BeingRow, Partition } from './partition.ts';
11
11
  import { Cells, DEPTH } from './cells.ts';
@@ -25,6 +25,10 @@ export interface Inside {
25
25
  // retraction when the offer was not taken.
26
26
  offer(contract: string): Promise<Invitation | null>;
27
27
  retract(contract: string, heir: string): Promise<void>;
28
+ // Where this ward is reached, for the invitations it gives, and where a
29
+ // ward it takes an invitation of is reached, learned from that `at`.
30
+ reach(): readonly string[];
31
+ learn(ward: string, at: readonly string[]): Promise<void>;
28
32
  }
29
33
 
30
34
  const RESERVED = new Set([OWNER]);
@@ -88,16 +92,17 @@ export class WardStance implements Stance {
88
92
  return this.#inside.live(this.key, this.#row);
89
93
  }
90
94
 
91
- boot(className: string, key: string, id?: string): Promise<string | null> {
92
- return this.#live ? this.#inside.boot(this.key, className, key, id) : Promise.resolve(null);
95
+ boot(kind: string, key: string, id?: string): Promise<string | null> {
96
+ return this.#live ? this.#inside.boot(this.key, kind, key, id) : Promise.resolve(null);
93
97
  }
94
98
 
95
- async lend(contract: { readonly name: string }, id: string): Promise<string | null> {
96
- if (!this.#free(id, this.#row.standings)) return null;
97
- const invitation = await this.#inside.offer(contract.name);
99
+ async lend(contract: abstract new (...args: never[]) => unknown, id: string): Promise<string | null> {
100
+ const kind = ownKind(contract);
101
+ if (kind === undefined || !this.#free(id, this.#row.standings)) return null;
102
+ const invitation = await this.#inside.offer(kind);
98
103
  if (invitation === null) return null;
99
104
  const taken = await this.#take(id, invitation);
100
- if (taken === null) await this.#inside.retract(contract.name, invitation.heir);
105
+ if (taken === null) await this.#inside.retract(kind, invitation.heir);
101
106
  return taken;
102
107
  }
103
108
 
@@ -108,7 +113,7 @@ export class WardStance implements Stance {
108
113
  async #invite(id: string, notes: JsonObject = {}): Promise<Invitation | null> {
109
114
  const kept = writeValue(notes, DEPTH);
110
115
  if (!this.#free(id, this.#row.occupants) || kept === undefined) return null;
111
- const { heir, invitation } = await this.#inside.door.invite();
116
+ const { heir, invitation } = await this.#inside.door.invite(this.#inside.reach());
112
117
  if (!this.#free(id, this.#row.occupants)) {
113
118
  this.#inside.door.remove(heir);
114
119
  return null;
@@ -134,6 +139,7 @@ export class WardStance implements Stance {
134
139
  async #take(id: string, value: Invitation): Promise<string | null> {
135
140
  const invitation = readInvitation(value);
136
141
  if (!invitation || !this.#free(id, this.#row.standings)) return null;
142
+ if (invitation.at) await this.#inside.learn(invitation.ward, invitation.at);
137
143
  const wire = new Wire(invitation, freshStanding(), this.#inside.draw);
138
144
  const read = await this.#lanes.run(`take:${id}`, async () => {
139
145
  // A knock that brought nothing back may have bound: the standing asks
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // The ward-being: the first being of every ward, the ward itself. Her asks
3
3
  // are the root's, shown and answered to `OWNER` alone.
4
- import { Being, isSilence, isWord, OWNER, told, type Answer, type Asker, type AskSpec, type BeingClass, type Blueprint, type Invitation, type JsonObject, type Stance } from '../being/index.ts';
4
+ import { Being, isSilence, isWord, KIT, OWNER, told, type Answer, type Asker, type AskSpec, type BeingClass, type Blueprint, type Invitation, type JsonObject, type Stance } from '../being/index.ts';
5
5
 
6
6
  // What the ward lends its ward-being, and no other being.
7
7
  export interface Steward {
@@ -26,11 +26,16 @@ export const ownerAsk = (description: string, input: JsonObject = {}): AskSpec =
26
26
 
27
27
  // An ask the root alone may make, so a carried key pilots but never hands
28
28
  // piloting on.
29
- const rootAsk = (description: string, input: JsonObject = {}): AskSpec => ({ description, input: { type: 'object', ...input }, for: (asker: Asker) => asker.id === OWNER });
29
+ export const rootAsk = (description: string, input: JsonObject = {}): AskSpec => ({ description, input: { type: 'object', ...input }, for: (asker: Asker) => asker.id === OWNER });
30
+ // The one kit kind the root boots: every other kit class stands where the
31
+ // kit puts it.
32
+ export const PILOT = 'org.nervur.pilot';
33
+
30
34
  const text = (v: unknown): v is string => typeof v === 'string';
31
35
  const object = (v: unknown): v is JsonObject => typeof v === 'object' && v !== null && !Array.isArray(v);
32
36
 
33
37
  export class WardBeing extends Being {
38
+ static override readonly kind: string = 'org.nervur.ward-being';
34
39
  static override asks: Record<string, AskSpec> = {
35
40
  boot: ownerAsk('a new being of this ward', { required: ['key', 'class'] }),
36
41
  unboot: ownerAsk('a being out, with every relation she holds', { required: ['being'] }),
@@ -66,11 +71,12 @@ export class WardBeing extends Being {
66
71
 
67
72
  async boot(args: JsonObject): Promise<JsonObject> {
68
73
  if (!text(args.key) || !text(args.class)) return { error: 'key and class are text' };
74
+ if (args.class.startsWith(KIT) && args.class !== PILOT) return { error: 'no such class' };
69
75
  const out = await this.steward.boot(args.key, args.class);
70
76
  return out === 'booted' ? { booted: args.key } : { error: out };
71
77
  }
72
78
 
73
- unboot(args: JsonObject): JsonObject {
79
+ unboot(args: JsonObject): JsonObject | Promise<JsonObject> {
74
80
  return text(args.being) && this.steward.unboot(args.being) ? { unbooted: args.being } : { error: 'no such being' };
75
81
  }
76
82
 
package/src/ward/ward.ts CHANGED
@@ -6,14 +6,14 @@
6
6
  import { readValue, writeValue } from '../crypto/index.ts';
7
7
  import { Door, readInvitation, WardKey, type Behind, type Choice, type Invitation, type Payload } from '../quo/index.ts';
8
8
  import type { Carrier, Clock, Entropy, Rows } from '../contract/index.ts';
9
- import { digest, isSilence, isWord, OWNER, silence, WARD, word, type Answer, type Asker, type BeingClass, type BeingLike, type JsonObject, type Reply } from '../being/index.ts';
9
+ import { digest, isSilence, isWord, kindOf, OWNER, silence, WARD, word, type Answer, type Asker, type BeingClass, type BeingLike, type JsonObject, type Reply } from '../being/index.ts';
10
10
  import { Partition, PartitionRelations, type BeingRow } from './partition.ts';
11
11
  import { DEPTH, WardStance, type Inside } from './stance.ts';
12
12
  import { WardBeing, type Steward, type WardBeingStance } from './ward-being.ts';
13
13
 
14
- // Where a ward finds a class by the name its row keeps.
14
+ // Where a ward finds a class by the kind its row keeps.
15
15
  export interface Catalogue {
16
- classOf(name: string): BeingClass | undefined;
16
+ classOf(kind: string): BeingClass | undefined;
17
17
  }
18
18
 
19
19
  export type WardParts = {
@@ -28,8 +28,16 @@ export type WardParts = {
28
28
  // An invitation on the dock, taken by the ward-being under `dock` when
29
29
  // she holds no such standing yet.
30
30
  readonly dock?: Invitation;
31
+ // Where the ward is reached, and where a ward it takes an invitation of
32
+ // is reached. A ward alone is reached nowhere and learns nothing.
33
+ readonly routes?: Routes;
31
34
  };
32
35
 
36
+ export interface Routes {
37
+ reach(): readonly string[];
38
+ learn(ward: string, at: readonly string[]): Promise<void>;
39
+ }
40
+
33
41
  // The standing every hosted ward's ward-being holds on the dock.
34
42
  export const DOCK = 'dock';
35
43
 
@@ -81,9 +89,13 @@ export class Ward implements Behind, Inside {
81
89
  const own = ward.partition.being(WARD)!;
82
90
  const stance = new RootStance(ward, own, ward.#steward);
83
91
  const being = parts.root ? parts.root(stance) : new WardBeing(stance);
84
- own.class = being.constructor.name;
92
+ own.class = kindOf(being.constructor);
85
93
  ward.#residents.set(WARD, { being, row: own, stance });
86
- if (parts.dock && !Object.hasOwn(own.standings, DOCK)) await stance.standings.take(DOCK, parts.dock);
94
+ // The standing on the dock is kept at once: its heir is spent, so a
95
+ // restart before the ward's first keep would find no way back.
96
+ if (parts.dock && !Object.hasOwn(own.standings, DOCK) && (await stance.standings.take(DOCK, parts.dock)) !== null && !(await ward.partition.kept())) {
97
+ throw new Error('the standing on the dock was not kept');
98
+ }
87
99
  for (const [key, row] of Object.entries(ward.partition.beings)) {
88
100
  const C = key === WARD ? undefined : parts.catalogue.classOf(row.class);
89
101
  if (C) ward.#bear(key, row, C);
@@ -103,6 +115,33 @@ export class Ward implements Behind, Inside {
103
115
  return this.#residents.get(WARD)!.being as WardBeing;
104
116
  }
105
117
 
118
+ // The being standing under a key, for the harbor's own faculties.
119
+ being(key: string): BeingLike | undefined {
120
+ return this.#residents.get(key)?.being;
121
+ }
122
+
123
+ // Every being her row names, of the class the catalogue now resolves for
124
+ // her kind: an absent being is born, and one whose class a module
125
+ // replaced is born again of the new class on the stance she holds, so
126
+ // her relations and their lanes stay hers. A new class that throws at
127
+ // birth leaves her as she was.
128
+ bearAnew(): void {
129
+ for (const [key, row] of Object.entries(this.partition.beings)) {
130
+ const C = key === WARD ? undefined : this.#parts.catalogue.classOf(row.class);
131
+ const resident = this.#residents.get(key);
132
+ if (!C || resident?.being.constructor === C) continue;
133
+ if (!resident) {
134
+ this.#bear(key, row, C);
135
+ continue;
136
+ }
137
+ try {
138
+ resident.being = new C(resident.stance);
139
+ } catch {
140
+ continue;
141
+ }
142
+ }
143
+ }
144
+
106
145
  // The unsealed ask: the ward-being, asked by whoever holds this ward.
107
146
  async root(method?: string, args: JsonObject = {}): Promise<Answer> {
108
147
  return this.#kept(WARD, () => this.#answer(WARD, { id: OWNER }, method, args));
@@ -124,7 +163,7 @@ export class Ward implements Behind, Inside {
124
163
  return silence;
125
164
  }
126
165
 
127
- // Behind: the zero head reaches the public being, a heir the being that
166
+ // Behind: the zero head reaches the public being, an heir the being that
128
167
  // invited it.
129
168
  get zero(): boolean {
130
169
  const key = this.partition.head.public;
@@ -182,6 +221,14 @@ export class Ward implements Behind, Inside {
182
221
  await own.stance.standings.get(DOCK)?.ask('retract', { contract, heir });
183
222
  }
184
223
 
224
+ reach(): readonly string[] {
225
+ return this.#parts.routes?.reach() ?? [];
226
+ }
227
+
228
+ learn(ward: string, at: readonly string[]): Promise<void> {
229
+ return this.#parts.routes?.learn(ward, at) ?? Promise.resolve();
230
+ }
231
+
185
232
  live(key: string, row: BeingRow): boolean {
186
233
  return this.#residents.get(key)?.row === row;
187
234
  }