nervur 0.19.2 → 0.20.1
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 +156 -104
- package/dist/being/being.d.ts +8 -15
- package/dist/being/being.js +20 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +10 -0
- package/dist/being/faculty.js +74 -0
- package/dist/being/index.d.ts +5 -5
- package/dist/being/index.js +6 -5
- package/dist/being/kind.d.ts +4 -0
- package/dist/being/kind.js +28 -0
- package/dist/being/types.d.ts +35 -63
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- package/dist/cli/command.d.ts +13 -0
- package/dist/cli/command.js +194 -0
- package/dist/cli/harbor.d.ts +21 -0
- package/dist/cli/harbor.js +141 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +14 -0
- package/dist/contract/index.d.ts +39 -0
- package/dist/contract/index.js +33 -0
- package/dist/crypto/aes.d.ts +3 -0
- package/dist/crypto/aes.js +24 -0
- package/dist/crypto/bytes.d.ts +6 -0
- package/dist/crypto/bytes.js +33 -0
- package/dist/crypto/ed25519.d.ts +3 -0
- package/dist/crypto/ed25519.js +85 -0
- package/dist/crypto/hash.d.ts +2 -0
- package/dist/crypto/hash.js +11 -0
- package/dist/crypto/index.d.ts +7 -0
- package/dist/crypto/index.js +10 -0
- package/dist/crypto/json.d.ts +8 -0
- package/dist/crypto/json.js +250 -0
- package/dist/crypto/mlkem.d.ts +12 -0
- package/dist/crypto/mlkem.js +36 -0
- package/dist/crypto/subtle.d.ts +10 -0
- package/dist/crypto/subtle.js +29 -0
- package/dist/crypto/x25519.d.ts +2 -0
- package/dist/crypto/x25519.js +21 -0
- package/dist/folder/index.d.ts +14 -0
- package/dist/folder/index.js +87 -0
- package/dist/harbor/catalogue.d.ts +25 -0
- package/dist/harbor/catalogue.js +93 -0
- package/dist/harbor/dock.d.ts +29 -0
- package/dist/harbor/dock.js +122 -0
- package/dist/harbor/harbor.d.ts +31 -0
- package/dist/harbor/harbor.js +146 -0
- package/dist/harbor/index.d.ts +7 -10
- package/dist/harbor/index.js +9 -18
- package/dist/harbor/package.d.ts +10 -0
- package/dist/harbor/package.js +115 -0
- package/dist/harbor/registry.d.ts +9 -0
- package/dist/harbor/registry.js +78 -0
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +36 -0
- package/dist/harbor/terrain.d.ts +9 -0
- package/dist/harbor/terrain.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/pointer/bodies.d.ts +26 -0
- package/dist/pointer/bodies.js +64 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +35 -0
- package/dist/pointer/world.js +85 -0
- package/dist/quo/door.d.ts +34 -0
- package/dist/quo/door.js +172 -0
- package/dist/quo/index.d.ts +8 -0
- package/dist/quo/index.js +11 -0
- package/dist/quo/invitation.d.ts +7 -0
- package/dist/quo/invitation.js +15 -0
- package/dist/quo/keys.d.ts +40 -0
- package/dist/quo/keys.js +79 -0
- package/dist/quo/payload.d.ts +15 -0
- package/dist/quo/payload.js +55 -0
- package/dist/quo/relations.d.ts +40 -0
- package/dist/quo/relations.js +33 -0
- package/dist/quo/reply.d.ts +13 -0
- package/dist/quo/reply.js +35 -0
- package/dist/quo/seal.d.ts +42 -0
- package/dist/quo/seal.js +78 -0
- package/dist/quo/standing.d.ts +39 -0
- package/dist/quo/standing.js +90 -0
- package/dist/tcp/frame.d.ts +29 -0
- package/dist/tcp/frame.js +74 -0
- package/dist/tcp/index.d.ts +18 -0
- package/dist/tcp/index.js +164 -0
- package/dist/ward/allowance.d.ts +13 -9
- package/dist/ward/allowance.js +28 -67
- package/dist/ward/cells.d.ts +6 -6
- package/dist/ward/cells.js +67 -179
- package/dist/ward/index.d.ts +6 -10
- package/dist/ward/index.js +7 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +27 -20
- package/dist/ward/stance.js +174 -406
- package/dist/ward/ward-being.d.ts +35 -0
- package/dist/ward/ward-being.js +82 -0
- package/dist/ward/ward.d.ts +45 -11
- package/dist/ward/ward.js +239 -357
- package/package.json +24 -31
- package/src/being/being.ts +35 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +73 -0
- package/src/being/index.ts +6 -6
- package/src/being/kind.ts +30 -0
- package/src/being/types.ts +50 -146
- package/src/being/words.ts +31 -0
- package/src/cli/command.ts +188 -0
- package/src/cli/harbor.ts +137 -0
- package/src/cli/main.ts +13 -0
- package/src/contract/index.ts +73 -0
- package/src/crypto/aes.ts +26 -0
- package/src/crypto/bytes.ts +37 -0
- package/src/crypto/ed25519.ts +84 -0
- package/src/crypto/hash.ts +14 -0
- package/src/crypto/index.ts +10 -0
- package/src/crypto/json.ts +241 -0
- package/src/crypto/mlkem.ts +38 -0
- package/src/crypto/subtle.ts +33 -0
- package/src/crypto/x25519.ts +21 -0
- package/src/folder/index.ts +92 -0
- package/src/harbor/catalogue.ts +110 -0
- package/src/harbor/dock.ts +137 -0
- package/src/harbor/harbor.ts +180 -0
- package/src/harbor/index.ts +9 -19
- package/src/harbor/package.ts +131 -0
- package/src/harbor/registry.ts +73 -0
- package/src/harbor/root-line.ts +36 -0
- package/src/harbor/terrain.ts +15 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +65 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +94 -0
- package/src/quo/door.ts +178 -0
- package/src/quo/index.ts +11 -0
- package/src/quo/invitation.ts +15 -0
- package/src/quo/keys.ts +91 -0
- package/src/quo/payload.ts +61 -0
- package/src/quo/relations.ts +62 -0
- package/src/quo/reply.ts +38 -0
- package/src/quo/seal.ts +101 -0
- package/src/quo/standing.ts +111 -0
- package/src/stand/main.ts +20 -0
- package/src/stand/stand.ts +220 -0
- package/src/tcp/frame.ts +78 -0
- package/src/tcp/index.ts +173 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +7 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +186 -420
- package/src/ward/ward-being.ts +103 -0
- package/src/ward/ward.ts +256 -359
- package/dist/being/lent.d.ts +0 -32
- package/dist/being/lent.js +0 -72
- package/dist/being/silence.d.ts +0 -12
- package/dist/being/silence.js +0 -41
- package/dist/conformance/assert.d.ts +0 -11
- package/dist/conformance/assert.js +0 -106
- package/dist/conformance/beings.d.ts +0 -199
- package/dist/conformance/beings.js +0 -188
- package/dist/conformance/estate.d.ts +0 -5
- package/dist/conformance/estate.js +0 -388
- package/dist/conformance/index.d.ts +0 -80
- package/dist/conformance/index.js +0 -819
- package/dist/conformance/reach.d.ts +0 -10
- package/dist/conformance/reach.js +0 -72
- package/dist/conformance/store.d.ts +0 -5
- package/dist/conformance/store.js +0 -113
- package/dist/harbor/box.d.ts +0 -121
- package/dist/harbor/box.js +0 -121
- package/dist/harbor/core.d.ts +0 -55
- package/dist/harbor/core.js +0 -662
- package/dist/harbor/dial.d.ts +0 -9
- package/dist/harbor/dial.js +0 -81
- package/dist/harbor/memory.d.ts +0 -29
- package/dist/harbor/memory.js +0 -104
- package/dist/harbor/reach.d.ts +0 -36
- package/dist/harbor/reach.js +0 -199
- package/dist/harbor/store.d.ts +0 -35
- package/dist/harbor/store.js +0 -62
- package/dist/vector/cases.d.ts +0 -42
- package/dist/vector/cases.js +0 -223
- package/dist/vector/index.d.ts +0 -6
- package/dist/vector/index.js +0 -8
- package/dist/vector/stand.d.ts +0 -9
- package/dist/vector/stand.js +0 -77
- package/dist/vector/world.d.ts +0 -144
- package/dist/vector/world.js +0 -209
- package/dist/ward/arithmetic.d.ts +0 -31
- package/dist/ward/arithmetic.js +0 -249
- package/dist/ward/door.d.ts +0 -18
- package/dist/ward/door.js +0 -186
- package/dist/ward/ground.d.ts +0 -29
- package/dist/ward/ground.js +0 -56
- package/dist/ward/heirs.d.ts +0 -13
- package/dist/ward/heirs.js +0 -117
- package/dist/ward/json.d.ts +0 -2
- package/dist/ward/json.js +0 -163
- package/dist/ward/owner.d.ts +0 -13
- package/dist/ward/owner.js +0 -220
- package/dist/ward/seal.d.ts +0 -52
- package/dist/ward/seal.js +0 -150
- package/src/being/lent.ts +0 -72
- package/src/being/silence.ts +0 -46
- package/src/conformance/assert.ts +0 -100
- package/src/conformance/beings.ts +0 -188
- package/src/conformance/estate.ts +0 -412
- package/src/conformance/index.ts +0 -965
- package/src/conformance/reach.ts +0 -83
- package/src/conformance/store.ts +0 -125
- package/src/harbor/box.ts +0 -131
- package/src/harbor/core.ts +0 -699
- package/src/harbor/dial.ts +0 -112
- package/src/harbor/memory.ts +0 -123
- package/src/harbor/reach.ts +0 -221
- package/src/harbor/store.ts +0 -91
- package/src/vector/cases.ts +0 -257
- package/src/vector/index.ts +0 -11
- package/src/vector/stand.ts +0 -76
- package/src/vector/world.ts +0 -232
- package/src/ward/arithmetic.ts +0 -251
- package/src/ward/door.ts +0 -186
- package/src/ward/ground.ts +0 -142
- package/src/ward/heirs.ts +0 -116
- package/src/ward/json.ts +0 -144
- package/src/ward/owner.ts +0 -214
- package/src/ward/seal.ts +0 -178
package/src/tcp/index.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
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 address its book holds for that pk
|
|
7
|
+
// TcpListener answers ask frames for the doors it is handed, a harbor
|
|
8
|
+
// among them
|
|
9
|
+
//
|
|
10
|
+
// Where a ward is reached is the carrier's own, and never on the wire. A
|
|
11
|
+
// connection is opened per address, carries many asks at once, and is
|
|
12
|
+
// dropped when either side closes it; the next ask dials again.
|
|
13
|
+
import { connect, createServer, type Server, type Socket } from 'node:net';
|
|
14
|
+
import { hex, isHex, unhex } from '../crypto/index.ts';
|
|
15
|
+
import { Carrier } from '../contract/index.ts';
|
|
16
|
+
import { askFrame, FrameReader, nothingFrame, parseAt, replyFrame } from './frame.ts';
|
|
17
|
+
|
|
18
|
+
export { parseAt } from './frame.ts';
|
|
19
|
+
|
|
20
|
+
// What a listener hands an ask to: bytes for a ward pk, or nothing.
|
|
21
|
+
export interface Doors {
|
|
22
|
+
arrive(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MAX_ID = 0xffff_ffff;
|
|
26
|
+
|
|
27
|
+
class Dialed {
|
|
28
|
+
readonly #socket: Socket;
|
|
29
|
+
readonly #reader = new FrameReader();
|
|
30
|
+
readonly #waiting = new Map<number, (box: Uint8Array | null) => void>();
|
|
31
|
+
#next = 1;
|
|
32
|
+
closed = false;
|
|
33
|
+
|
|
34
|
+
constructor(socket: Socket, onClose: () => void) {
|
|
35
|
+
this.#socket = socket;
|
|
36
|
+
socket.on('data', (chunk: Buffer) => {
|
|
37
|
+
for (const f of this.#reader.push(new Uint8Array(chunk))) {
|
|
38
|
+
if (f.kind === 'ask') continue;
|
|
39
|
+
const resolve = this.#waiting.get(f.id);
|
|
40
|
+
this.#waiting.delete(f.id);
|
|
41
|
+
resolve?.(f.kind === 'reply' ? f.box : null);
|
|
42
|
+
}
|
|
43
|
+
if (this.#reader.bad) socket.destroy();
|
|
44
|
+
});
|
|
45
|
+
socket.on('error', () => socket.destroy());
|
|
46
|
+
socket.on('close', () => {
|
|
47
|
+
this.closed = true;
|
|
48
|
+
onClose();
|
|
49
|
+
for (const resolve of this.#waiting.values()) resolve(null);
|
|
50
|
+
this.#waiting.clear();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static open(host: string, port: number, onClose: () => void): Promise<Dialed | null> {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const socket = connect({ host, port });
|
|
57
|
+
socket.once('connect', () => resolve(new Dialed(socket, onClose)));
|
|
58
|
+
socket.once('error', () => {
|
|
59
|
+
socket.destroy();
|
|
60
|
+
resolve(null);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ask(pk: Uint8Array, box: Uint8Array): Promise<Uint8Array | null> {
|
|
66
|
+
if (this.closed) return Promise.resolve(null);
|
|
67
|
+
const id = this.#next;
|
|
68
|
+
this.#next = this.#next === MAX_ID ? 1 : this.#next + 1;
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
this.#waiting.set(id, resolve);
|
|
71
|
+
this.#socket.write(askFrame(id, pk, box));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
close(): void {
|
|
76
|
+
this.#socket.destroy();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class TcpCarrier extends Carrier {
|
|
81
|
+
readonly #routes = new Map<string, string>();
|
|
82
|
+
readonly #lines = new Map<string, Promise<Dialed | null>>();
|
|
83
|
+
|
|
84
|
+
// The address a ward pk is reached at, replacing any before, or null to
|
|
85
|
+
// forget it.
|
|
86
|
+
override route(pk: string, at: string | null): boolean {
|
|
87
|
+
if (!isHex(pk, 64) || (at !== null && parseAt(at) === null)) return false;
|
|
88
|
+
if (at === null) this.#routes.delete(pk);
|
|
89
|
+
else this.#routes.set(pk, at);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Nothing where the pk has no route, the address is not reached, or the
|
|
94
|
+
// connection closed before an answer.
|
|
95
|
+
async carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
|
|
96
|
+
const at = this.#routes.get(pk);
|
|
97
|
+
if (at === undefined) return null;
|
|
98
|
+
const line = await this.#line(at);
|
|
99
|
+
return line ? line.ask(unhex(pk), bytes) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#line(at: string): Promise<Dialed | null> {
|
|
103
|
+
let line = this.#lines.get(at);
|
|
104
|
+
if (!line) {
|
|
105
|
+
const { host, port } = parseAt(at)!;
|
|
106
|
+
line = Dialed.open(host, port, () => this.#lines.delete(at)).then((dialed) => {
|
|
107
|
+
if (!dialed) this.#lines.delete(at);
|
|
108
|
+
return dialed;
|
|
109
|
+
});
|
|
110
|
+
this.#lines.set(at, line);
|
|
111
|
+
}
|
|
112
|
+
return line;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async close(): Promise<void> {
|
|
116
|
+
const lines = [...this.#lines.values()];
|
|
117
|
+
this.#lines.clear();
|
|
118
|
+
for (const line of await Promise.all(lines)) line?.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class TcpListener {
|
|
123
|
+
readonly #server: Server;
|
|
124
|
+
readonly #doors: Doors;
|
|
125
|
+
readonly #sockets = new Set<Socket>();
|
|
126
|
+
|
|
127
|
+
private constructor(doors: Doors) {
|
|
128
|
+
this.#doors = doors;
|
|
129
|
+
this.#server = createServer((socket) => this.#serve(socket));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// A listener on `host` and `port`, 127.0.0.1 and a free port unless named.
|
|
133
|
+
static listen(doors: Doors, host = '127.0.0.1', port = 0): Promise<TcpListener> {
|
|
134
|
+
const listener = new TcpListener(doors);
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
listener.#server.once('error', reject);
|
|
137
|
+
listener.#server.listen(port, host, () => resolve(listener));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get at(): string {
|
|
142
|
+
const address = this.#server.address() as { address: string; port: number; family: string };
|
|
143
|
+
return address.family === 'IPv6' ? `[${address.address}]:${address.port}` : `${address.address}:${address.port}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#serve(socket: Socket): void {
|
|
147
|
+
this.#sockets.add(socket);
|
|
148
|
+
const reader = new FrameReader();
|
|
149
|
+
socket.on('data', (chunk: Buffer) => {
|
|
150
|
+
for (const f of reader.push(new Uint8Array(chunk))) {
|
|
151
|
+
if (f.kind === 'ask') void this.#answer(socket, f.id, f.pk, f.box);
|
|
152
|
+
}
|
|
153
|
+
if (reader.bad) socket.destroy();
|
|
154
|
+
});
|
|
155
|
+
socket.on('error', () => socket.destroy());
|
|
156
|
+
socket.on('close', () => this.#sockets.delete(socket));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async #answer(socket: Socket, id: number, pk: Uint8Array, box: Uint8Array): Promise<void> {
|
|
160
|
+
let reply: Uint8Array | null = null;
|
|
161
|
+
try {
|
|
162
|
+
reply = await this.#doors.arrive(hex(pk), box);
|
|
163
|
+
} catch {
|
|
164
|
+
reply = null;
|
|
165
|
+
}
|
|
166
|
+
if (!socket.destroyed) socket.write(reply === null ? nothingFrame(id) : replyFrame(id, reply));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
close(): Promise<void> {
|
|
170
|
+
for (const socket of this.#sockets) socket.destroy();
|
|
171
|
+
return new Promise((resolve) => this.#server.close(() => resolve()));
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/ward/allowance.ts
CHANGED
|
@@ -1,85 +1,47 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The allowance
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// refusal there. The sender bounds the whole of her ask to the same number,
|
|
8
|
-
// and a wait that ran out is the word late, never silence and never unreached.
|
|
9
|
-
//
|
|
10
|
-
// Each ask is bounded on its own. The time an arriving call has left does not
|
|
11
|
-
// bound the asks a being makes while answering it: attributing her onward ask
|
|
12
|
-
// to the arrival that caused it needs a context the ward cannot get without
|
|
13
|
-
// naming a runtime, and the ward names none. Every wait still ends.
|
|
2
|
+
// The allowance: how long a being waits on one ask. It is on no wire. A
|
|
3
|
+
// being who says nothing gets the default, and nobody gets past the
|
|
4
|
+
// ceiling.
|
|
5
|
+
import type { Clock } from '../contract/index.ts';
|
|
6
|
+
import type { Wanted } from '../being/index.ts';
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import type { Wanted } from '../being/types.ts';
|
|
8
|
+
export const DEFAULT = 30_000;
|
|
9
|
+
export const CEILING = 300_000;
|
|
18
10
|
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
export const allow = (wanted: Wanted | undefined): number => {
|
|
12
|
+
const whole = typeof wanted?.time === 'number' ? Math.floor(wanted.time) : NaN;
|
|
13
|
+
return Math.min(Number.isFinite(whole) && whole > 0 ? whole : DEFAULT, CEILING);
|
|
14
|
+
};
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// refused. A being who never learns the third parameter exists writes correct
|
|
25
|
-
// code forever under these. They are two numbers because the third argument
|
|
26
|
-
// goes both ways: less for the ask she wants back quickly, more for the one
|
|
27
|
-
// she knows is slow.
|
|
28
|
-
export const DEFAULT: Allowance = { time: 30_000 };
|
|
29
|
-
export const CEILING: Allowance = { time: 300_000 };
|
|
16
|
+
export const LATE: unique symbol = Symbol('late');
|
|
17
|
+
export type Late = typeof LATE;
|
|
30
18
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// whole one.
|
|
39
|
-
const whole = (n: unknown, fallback: number) => {
|
|
40
|
-
const w = typeof n === 'number' ? Math.floor(n) : NaN;
|
|
41
|
-
return Number.isFinite(w) && w > 0 ? w : fallback;
|
|
42
|
-
};
|
|
43
|
-
return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
|
|
44
|
-
}
|
|
19
|
+
// One ask's bell. It bounds the wait of the being who asked and the send
|
|
20
|
+
// that holds the relation's line, so a far side that never answers holds
|
|
21
|
+
// neither for longer than the allowance.
|
|
22
|
+
export class Bell {
|
|
23
|
+
readonly #wait: { done: Promise<void>; cancel(): void };
|
|
24
|
+
readonly #late: Promise<Late>;
|
|
25
|
+
#rang = false;
|
|
45
26
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
27
|
+
constructor(clock: Clock, ms: number) {
|
|
28
|
+
this.#wait = clock.wait(ms);
|
|
29
|
+
this.#late = this.#wait.done.then((): Late => {
|
|
30
|
+
this.#rang = true;
|
|
31
|
+
return LATE;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
53
34
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
35
|
+
get rang(): boolean {
|
|
36
|
+
return this.#rang;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
race<T>(work: Promise<T>): Promise<T | Late> {
|
|
40
|
+
work.catch(() => {});
|
|
41
|
+
return Promise.race([work, this.#late]);
|
|
42
|
+
}
|
|
57
43
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// stopped at its own lane, before a single byte is sealed, and a bound that
|
|
61
|
-
// only watched the wire would never see it. A timer is the only way to stop
|
|
62
|
-
// waiting and every language that runs Quo has one; no runtime is named here.
|
|
63
|
-
//
|
|
64
|
-
// `rang` is called the moment the bell rings, before anyone waiting on the
|
|
65
|
-
// wait is told: whoever holds work that has not started yet reads it and does
|
|
66
|
-
// not start it. What comes back late is not read, and what has not left yet
|
|
67
|
-
// does not leave.
|
|
68
|
-
export async function within<T>(ms: number, work: Promise<T>, rang?: () => void): Promise<T | typeof LATE> {
|
|
69
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
-
const bell = new Promise<typeof LATE>((ring) => {
|
|
71
|
-
timer = setTimeout(() => {
|
|
72
|
-
rang?.();
|
|
73
|
-
ring(LATE);
|
|
74
|
-
}, ms);
|
|
75
|
-
});
|
|
76
|
-
try {
|
|
77
|
-
return await Promise.race([work, bell]);
|
|
78
|
-
} finally {
|
|
79
|
-
clearTimeout(timer);
|
|
80
|
-
void work.then(
|
|
81
|
-
() => {},
|
|
82
|
-
() => {},
|
|
83
|
-
); // whatever it was, it is nobody's answer now
|
|
44
|
+
cancel(): void {
|
|
45
|
+
this.#wait.cancel();
|
|
84
46
|
}
|
|
85
47
|
}
|
package/src/ward/cells.ts
CHANGED
|
@@ -1,190 +1,77 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// A being's cells, guarded. Her cells are kept by whatever memory her
|
|
3
|
+
// terrain gives, and a memory that writes JSON would hand back something
|
|
4
|
+
// other than what she wrote if she wrote anything JSON cannot hold. So a
|
|
5
|
+
// write that is not a value throws where she wrote it, in her own frame,
|
|
6
|
+
// and her answer becomes silence like any throw.
|
|
5
7
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
// So the cells refuse the write. Loud, where she wrote it, in her own frame:
|
|
14
|
-
// a being holds three answers and a throw is not one of them, so this throw
|
|
15
|
-
// lands inside her method and the door turns it into the silence it already
|
|
16
|
-
// turns every throw into. She has not answered, and nothing was written down
|
|
17
|
-
// that a harbor would have to lie about later.
|
|
18
|
-
import type { Cells, Json } from '../being/types.ts';
|
|
8
|
+
// What passes is stored as a fresh copy of its JSON text, so a reference
|
|
9
|
+
// she kept to what she wrote is not a way around the guard. Every key lands
|
|
10
|
+
// as an own key, `__proto__` included. A list grows at its end: a slot or a
|
|
11
|
+
// length past it would leave a hole.
|
|
12
|
+
import { writeValue, type Json } from '../crypto/index.ts';
|
|
13
|
+
import type { JsonObject } from '../being/index.ts';
|
|
19
14
|
|
|
20
|
-
// How deep a value may nest. Every walk over a value in the kit recurses,
|
|
21
|
-
// and a peer chooses the depth of what she answers; a bound keeps a hostile
|
|
22
|
-
// reply from ending a walk in a stack overflow instead of a refusal.
|
|
23
15
|
export const DEPTH = 64;
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// nesting that forty levels deep is inside the depth bound and is a trillion
|
|
28
|
-
// values once written down. What is walked here is the object graph, which is
|
|
29
|
-
// small; what a harbor keeps is the tree it expands to, which is not. So the
|
|
30
|
-
// walk counts the tree while it proves the graph, and refuses a value nobody
|
|
31
|
-
// could write down. This is a bound on what a being holds, so it is this
|
|
32
|
-
// kit's number and no word of the spec.
|
|
33
|
-
export const BREADTH = 1 << 20;
|
|
34
|
-
|
|
35
|
-
// Text a harbor can write down: every surrogate in a pair. A lone one is a
|
|
36
|
-
// code unit with no character, which JSON escapes and UTF-8 cannot encode at
|
|
37
|
-
// all, so a harbor that keeps strings as bytes loses it and one that keeps
|
|
38
|
-
// the object does not. I-JSON says a string is text, and this is that rule
|
|
39
|
-
// where the value is written rather than where it is read.
|
|
40
|
-
const WELL_FORMED = /^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/;
|
|
41
|
-
|
|
42
|
-
// I-JSON, all the way down. A number that JSON cannot write is not a number
|
|
43
|
-
// a harbor can keep, a key on the prototype is not a key she wrote, and a
|
|
44
|
-
// hole in a list, or a key named `__proto__`, is a thing JSON writes one way
|
|
45
|
-
// and a runtime reads another.
|
|
46
|
-
//
|
|
47
|
-
// A fault is a sentence; anything else is the count of values under this one,
|
|
48
|
-
// itself included. A value already counted is a value already proven, so a
|
|
49
|
-
// graph that shares is walked once per node rather than once per path.
|
|
50
|
-
function walk(v: unknown, path: string, seen: Set<object>, sized: Map<object, number>, depth: number): string | number {
|
|
51
|
-
if (v === null || typeof v === 'boolean') return 1;
|
|
52
|
-
if (typeof v === 'string') return WELL_FORMED.test(v) ? 1 : `${path} carries a lone surrogate, which is no text a harbor can write down`;
|
|
53
|
-
if (typeof v === 'number') {
|
|
54
|
-
if (!Number.isFinite(v)) return `${path} is ${String(v)}, which no harbor can write down`;
|
|
55
|
-
// Minus zero is a number JSON writes as `0` and reads back as `0`. A
|
|
56
|
-
// harbor that keeps values would hand her the sign she wrote and one that
|
|
57
|
-
// writes JSON would not, and she is never told which she is standing in.
|
|
58
|
-
return Object.is(v, -0) ? `${path} is minus zero, which comes back as zero from every harbor that writes JSON` : 1;
|
|
59
|
-
}
|
|
60
|
-
if (typeof v !== 'object') return `${path} is a ${typeof v}, which is not a value`;
|
|
61
|
-
if (seen.has(v)) return `${path} refers back to itself`;
|
|
62
|
-
const counted = sized.get(v);
|
|
63
|
-
if (counted !== undefined) return counted;
|
|
64
|
-
if (depth >= DEPTH) return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
|
|
65
|
-
seen.add(v);
|
|
66
|
-
try {
|
|
67
|
-
let size = 1;
|
|
68
|
-
const under = (where: string, x: unknown): string | null => {
|
|
69
|
-
const r = walk(x, where, seen, sized, depth + 1);
|
|
70
|
-
if (typeof r === 'string') return r;
|
|
71
|
-
size += r;
|
|
72
|
-
return size > BREADTH ? `${path} writes out to more than ${BREADTH} values, which no harbor can keep` : null;
|
|
73
|
-
};
|
|
74
|
-
if (Array.isArray(v)) {
|
|
75
|
-
for (let i = 0; i < v.length; i += 1) {
|
|
76
|
-
if (!(i in v)) return `${path}[${i}] is a hole, which no harbor can write down`;
|
|
77
|
-
const f = under(`${path}[${i}]`, v[i]);
|
|
78
|
-
if (f) return f;
|
|
79
|
-
}
|
|
80
|
-
sized.set(v, size);
|
|
81
|
-
return size;
|
|
82
|
-
}
|
|
83
|
-
const proto = Object.getPrototypeOf(v);
|
|
84
|
-
if (proto !== Object.prototype && proto !== null) return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
|
|
85
|
-
for (const k of Object.keys(v)) {
|
|
86
|
-
if (k === '__proto__') return `${path}.__proto__ is a key no harbor can keep`;
|
|
87
|
-
const f = under(`${path}.${k}`, (v as Record<string, unknown>)[k]);
|
|
88
|
-
if (f) return f;
|
|
89
|
-
}
|
|
90
|
-
sized.set(v, size);
|
|
91
|
-
return size;
|
|
92
|
-
} finally {
|
|
93
|
-
seen.delete(v);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export const cellFault = (v: unknown, path: string): string | null => {
|
|
98
|
-
const out = walk(v, path, new Set(), new Map(), 0);
|
|
99
|
-
return typeof out === 'string' ? out : null;
|
|
17
|
+
const refuse = (why: string): never => {
|
|
18
|
+
throw new TypeError(`cells hold values: ${why}`);
|
|
100
19
|
};
|
|
101
20
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
21
|
+
const copy = (value: unknown, room: number, where: string): Json => {
|
|
22
|
+
const text = writeValue(value, room);
|
|
23
|
+
return text === undefined ? refuse(`${where} is not a value JSON keeps, within ${room} levels`) : (JSON.parse(text) as Json);
|
|
24
|
+
};
|
|
105
25
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// object twice and a being may still compare what she holds.
|
|
110
|
-
const wrapped = new WeakMap<object, object>();
|
|
111
|
-
const targets = new WeakMap<object, object>();
|
|
112
|
-
const guards = new WeakSet();
|
|
26
|
+
export class Cells {
|
|
27
|
+
readonly #guards = new WeakMap<object, object>();
|
|
28
|
+
readonly #wrote: () => void;
|
|
113
29
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// which is half a relation and a throw where a null is promised. One rule,
|
|
118
|
-
// one place, read by the guard and by the stance.
|
|
119
|
-
export const unkeepable = (k: string): boolean => k === '__proto__';
|
|
30
|
+
constructor(wrote: () => void) {
|
|
31
|
+
this.#wrote = wrote;
|
|
32
|
+
}
|
|
120
33
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
throw new TypeError(`cells hold values: ${why}`);
|
|
125
|
-
};
|
|
126
|
-
function keyFault(t: object, k: string | symbol, v: unknown, path: string, root: boolean): void {
|
|
127
|
-
if (typeof k !== 'string') refuse(`${path} takes no symbol key`);
|
|
128
|
-
if (unkeepable(k)) refuse(`${path}.__proto__ is a key no harbor can keep`);
|
|
129
|
-
if (root && WARDS.has(k)) refuse(`${path}.${k} is the ward's to write`);
|
|
130
|
-
// A list grows by one at its end, or it has holes JSON cannot write. A
|
|
131
|
-
// push sets the slot at its length and then the length: both pass. A
|
|
132
|
-
// length set past what she wrote, or a slot beyond it, would leave holes.
|
|
133
|
-
if (Array.isArray(t)) {
|
|
134
|
-
if (k === 'length') {
|
|
135
|
-
if (typeof v !== 'number' || v > t.length) refuse(`${path}.length set past what she wrote would leave holes`);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const i = Number(k);
|
|
139
|
-
if (Number.isInteger(i) && i > t.length) refuse(`${path}[${i}] would leave a hole`);
|
|
34
|
+
// The guarded view of a being's cells.
|
|
35
|
+
guard(cells: JsonObject): JsonObject {
|
|
36
|
+
return this.#guard(cells, 'cells', 1);
|
|
140
37
|
}
|
|
141
|
-
}
|
|
142
38
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!('value' in d)) refuse(`${path}.${String(k)} is an accessor, which no harbor can keep`);
|
|
163
|
-
keyFault(t, k, d.value, path, root);
|
|
164
|
-
const f = cellFault(d.value, `${path}.${String(k)}`);
|
|
165
|
-
if (f) refuse(f);
|
|
166
|
-
const ok = Reflect.defineProperty(t, k, d);
|
|
167
|
-
wrote();
|
|
168
|
-
return ok;
|
|
169
|
-
},
|
|
170
|
-
deleteProperty(t, k) {
|
|
171
|
-
if (root && typeof k === 'string' && WARDS.has(k)) refuse(`${path}.${k} is the ward's to keep`);
|
|
172
|
-
const ok = Reflect.deleteProperty(t, k);
|
|
173
|
-
wrote();
|
|
39
|
+
#guard<T extends object>(target: T, path: string, level: number): T {
|
|
40
|
+
const had = this.#guards.get(target);
|
|
41
|
+
if (had) return had as T;
|
|
42
|
+
const room = DEPTH - level;
|
|
43
|
+
const place = (t: T, k: string | symbol, value: unknown): boolean => {
|
|
44
|
+
if (typeof k !== 'string') return refuse(`${path} takes no symbol key`);
|
|
45
|
+
const where = `${path}.${k}`;
|
|
46
|
+
if (Array.isArray(t)) {
|
|
47
|
+
if (k === 'length') {
|
|
48
|
+
if (typeof value !== 'number' || value > t.length) refuse(`${where} past its end would leave a hole`);
|
|
49
|
+
t.length = value as number;
|
|
50
|
+
this.#wrote();
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (!/^(0|[1-9]\d*)$/.test(k)) refuse(`${where} is not a slot of a list`);
|
|
54
|
+
if (Number(k) > t.length) refuse(`${where} would leave a hole`);
|
|
55
|
+
}
|
|
56
|
+
const ok = Reflect.defineProperty(t, k, { value: copy(value, room, where), enumerable: true, writable: true, configurable: true });
|
|
57
|
+
this.#wrote();
|
|
174
58
|
return ok;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
59
|
+
};
|
|
60
|
+
const proxy = new Proxy(target, {
|
|
61
|
+
get: (t, k, r) => {
|
|
62
|
+
const v: unknown = Reflect.get(t, k, r);
|
|
63
|
+
return typeof k === 'string' && Object.hasOwn(t, k) && typeof v === 'object' && v !== null ? this.#guard(v, `${path}.${k}`, level + 1) : v;
|
|
64
|
+
},
|
|
65
|
+
set: (t, k, v) => place(t, k, v),
|
|
66
|
+
defineProperty: (t, k, d) => ('value' in d ? place(t, k, d.value) : refuse(`${path}.${String(k)} is an accessor`)),
|
|
67
|
+
deleteProperty: (t, k) => {
|
|
68
|
+
const ok = Reflect.deleteProperty(t, k);
|
|
69
|
+
this.#wrote();
|
|
70
|
+
return ok;
|
|
71
|
+
},
|
|
72
|
+
setPrototypeOf: () => refuse(`${path} keeps its prototype`),
|
|
73
|
+
});
|
|
74
|
+
this.#guards.set(target, proxy);
|
|
75
|
+
return proxy;
|
|
76
|
+
}
|
|
181
77
|
}
|
|
182
|
-
|
|
183
|
-
// Her cells, guarded. Called once per being at boot; the guarded object is
|
|
184
|
-
// what goes into the partition and what the stance hands her, so there is no
|
|
185
|
-
// second door onto the same cells. Every write that passes says so to the
|
|
186
|
-
// ward, which says so to its harbor.
|
|
187
|
-
export const guardCells = (cells: Cells, wrote: () => void = () => {}): Cells => guard(cells as unknown as Record<string, Json>, 'cells', wrote, true) as unknown as Cells;
|
|
188
|
-
// The cells behind the guard, for the ward alone: the one writer of the
|
|
189
|
-
// three keys that are its own.
|
|
190
|
-
export const unguarded = (cells: Cells): Cells => (targets.get(cells) ?? cells) as Cells;
|
package/src/ward/index.ts
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
//
|
|
3
|
-
export {
|
|
4
|
-
export
|
|
5
|
-
export {
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export { GONE, MINTED, KNOCKS } from './partition.ts';
|
|
10
|
-
export type { AskPayload, ReplyPayload } from './seal.ts';
|
|
11
|
-
// The seal and the arithmetic, for a kit in another language to check its
|
|
12
|
-
// bytes against, and for tests that speak to a door directly.
|
|
13
|
-
export * as seal from './seal.ts';
|
|
14
|
-
export * as arithmetic from './arithmetic.ts';
|
|
15
|
-
// The allowance every ask carries, for a kit in another language and for
|
|
16
|
-
// tests that speak to a door directly.
|
|
17
|
-
export { allow, spent, DEFAULT, CEILING } from './allowance.ts';
|
|
18
|
-
export type { Allowance } from './allowance.ts';
|
|
2
|
+
// The ward: its partition, its stances, its ward-being, and the ward.
|
|
3
|
+
export { allow, CEILING, DEFAULT } from './allowance.ts';
|
|
4
|
+
export { HEAD_ROW, Partition, type BeingRow, type Head, type StandingRow } from './partition.ts';
|
|
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';
|
|
8
|
+
export { Pilot } from './pilot.ts';
|