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.
- package/README.md +229 -20
- package/dist/being/being.d.ts +1 -0
- package/dist/being/being.js +5 -1
- package/dist/being/faculty.d.ts +1 -0
- package/dist/being/faculty.js +13 -7
- package/dist/being/index.d.ts +1 -0
- package/dist/being/index.js +1 -0
- package/dist/being/kind.d.ts +4 -0
- package/dist/being/kind.js +28 -0
- package/dist/being/types.d.ts +3 -4
- package/dist/browser/index.d.ts +35 -0
- package/dist/browser/index.js +188 -0
- package/dist/browser/worker.d.ts +12 -0
- package/dist/browser/worker.js +40 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +7 -0
- package/dist/cli/command.d.ts +13 -0
- package/dist/cli/command.js +237 -0
- package/dist/cli/harbor.d.ts +19 -0
- package/dist/cli/harbor.js +99 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +14 -0
- package/dist/contract/index.d.ts +16 -2
- package/dist/contract/index.js +13 -3
- package/dist/edge/index.d.ts +57 -0
- package/dist/edge/index.js +306 -0
- package/dist/edge.d.ts +1 -0
- package/dist/edge.js +8 -0
- package/dist/folder/index.d.ts +9 -4
- package/dist/folder/index.js +89 -84
- package/dist/harbor/carrying.d.ts +17 -0
- package/dist/harbor/carrying.js +38 -0
- package/dist/harbor/catalogue.d.ts +27 -0
- package/dist/harbor/catalogue.js +101 -0
- package/dist/harbor/dock.d.ts +32 -3
- package/dist/harbor/dock.js +98 -12
- package/dist/harbor/harbor.d.ts +27 -8
- package/dist/harbor/harbor.js +217 -26
- package/dist/harbor/index.d.ts +8 -2
- package/dist/harbor/index.js +8 -1
- package/dist/harbor/package.d.ts +12 -0
- package/dist/harbor/package.js +130 -0
- package/dist/harbor/probe.d.ts +18 -0
- package/dist/harbor/probe.js +15 -0
- package/dist/harbor/registry.d.ts +6 -3
- package/dist/harbor/registry.js +83 -7
- package/dist/harbor/relay.d.ts +15 -0
- package/dist/harbor/relay.js +387 -0
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +38 -0
- package/dist/harbor/terrain.d.ts +14 -1
- package/dist/harbor/terrain.js +19 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.js +156 -0
- package/dist/http/websocket.d.ts +29 -0
- package/dist/http/websocket.js +133 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +12 -6
- package/dist/line/answer.d.ts +8 -0
- package/dist/line/answer.js +60 -0
- package/dist/line/frame.d.ts +34 -0
- package/dist/line/frame.js +81 -0
- package/dist/line/ground.d.ts +3 -0
- package/dist/line/ground.js +9 -0
- package/dist/line/index.d.ts +5 -0
- package/dist/line/index.js +9 -0
- package/dist/line/push.d.ts +7 -0
- package/dist/line/push.js +92 -0
- package/dist/line/web.d.ts +30 -0
- package/dist/line/web.js +181 -0
- package/dist/node/index.d.ts +32 -0
- package/dist/node/index.js +164 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +7 -0
- package/dist/pointer/bodies.d.ts +11 -3
- package/dist/pointer/bodies.js +47 -18
- package/dist/pointer/index.d.ts +1 -1
- package/dist/pointer/index.js +1 -1
- package/dist/pointer/world.d.ts +12 -5
- package/dist/pointer/world.js +29 -14
- package/dist/quo/address.d.ts +19 -0
- package/dist/quo/address.js +84 -0
- package/dist/quo/door.d.ts +1 -1
- package/dist/quo/door.js +6 -4
- package/dist/quo/index.d.ts +1 -0
- package/dist/quo/index.js +1 -0
- package/dist/quo/invitation.d.ts +1 -0
- package/dist/quo/invitation.js +9 -5
- package/dist/quo/keys.js +1 -1
- package/dist/tcp/frame.d.ts +25 -0
- package/dist/tcp/frame.js +64 -0
- package/dist/tcp/index.d.ts +24 -0
- package/dist/tcp/index.js +260 -0
- package/dist/ward/index.d.ts +3 -2
- package/dist/ward/index.js +2 -1
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +4 -4
- package/dist/ward/stance.js +10 -7
- package/dist/ward/ward-being.d.ts +4 -1
- package/dist/ward/ward-being.js +8 -2
- package/dist/ward/ward.d.ts +11 -2
- package/dist/ward/ward.js +41 -5
- package/package.json +28 -2
- package/src/being/being.ts +5 -1
- package/src/being/faculty.ts +14 -7
- package/src/being/index.ts +1 -0
- package/src/being/kind.ts +30 -0
- package/src/being/types.ts +8 -6
- package/src/browser/index.ts +207 -0
- package/src/browser/worker.ts +66 -0
- package/src/browser.ts +9 -0
- package/src/cli/command.ts +224 -0
- package/src/cli/harbor.ts +103 -0
- package/src/cli/main.ts +13 -0
- package/src/contract/index.ts +42 -10
- package/src/edge/index.ts +359 -0
- package/src/edge.ts +10 -0
- package/src/folder/index.ts +87 -84
- package/src/harbor/carrying.ts +56 -0
- package/src/harbor/catalogue.ts +125 -0
- package/src/harbor/dock.ts +122 -15
- package/src/harbor/harbor.ts +240 -36
- package/src/harbor/index.ts +10 -3
- package/src/harbor/package.ts +147 -0
- package/src/harbor/probe.ts +34 -0
- package/src/harbor/registry.ts +73 -9
- package/src/harbor/relay.ts +399 -0
- package/src/harbor/root-line.ts +39 -0
- package/src/harbor/terrain.ts +31 -3
- package/src/http/index.ts +150 -0
- package/src/http/websocket.ts +124 -0
- package/src/index.ts +39 -6
- package/src/line/answer.ts +59 -0
- package/src/line/frame.ts +88 -0
- package/src/line/ground.ts +19 -0
- package/src/line/index.ts +9 -0
- package/src/line/push.ts +112 -0
- package/src/line/web.ts +227 -0
- package/src/node/index.ts +170 -0
- package/src/node.ts +9 -0
- package/src/pointer/bodies.ts +45 -16
- package/src/pointer/index.ts +1 -1
- package/src/pointer/world.ts +38 -19
- package/src/quo/address.ts +87 -0
- package/src/quo/door.ts +6 -4
- package/src/quo/index.ts +1 -0
- package/src/quo/invitation.ts +10 -6
- package/src/quo/keys.ts +1 -1
- package/src/stand/main.ts +1 -0
- package/src/stand/stand.ts +87 -6
- package/src/tcp/index.ts +284 -0
- package/src/ward/index.ts +3 -2
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +14 -8
- package/src/ward/ward-being.ts +9 -3
- package/src/ward/ward.ts +53 -6
package/src/tcp/index.ts
ADDED
|
@@ -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
|
+
}
|
package/src/ward/stance.ts
CHANGED
|
@@ -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(
|
|
92
|
-
return this.#live ? this.#inside.boot(this.key,
|
|
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:
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
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
|
package/src/ward/ward-being.ts
CHANGED
|
@@ -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
|
|
14
|
+
// Where a ward finds a class by the kind its row keeps.
|
|
15
15
|
export interface Catalogue {
|
|
16
|
-
classOf(
|
|
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
|
|
92
|
+
own.class = kindOf(being.constructor);
|
|
85
93
|
ward.#residents.set(WARD, { being, row: own, stance });
|
|
86
|
-
|
|
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,
|
|
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
|
}
|