nervur 0.19.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -101
- package/dist/being/being.d.ts +7 -15
- package/dist/being/being.js +16 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +9 -0
- package/dist/being/faculty.js +68 -0
- package/dist/being/index.d.ts +4 -5
- package/dist/being/index.js +5 -5
- package/dist/being/types.d.ts +35 -62
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- package/dist/contract/index.d.ts +26 -0
- package/dist/contract/index.js +19 -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 +13 -0
- package/dist/folder/index.js +125 -0
- package/dist/harbor/dock.d.ts +19 -0
- package/dist/harbor/dock.js +92 -0
- package/dist/harbor/harbor.d.ts +22 -0
- package/dist/harbor/harbor.js +84 -0
- package/dist/harbor/index.d.ts +4 -10
- package/dist/harbor/index.js +5 -18
- package/dist/harbor/registry.d.ts +7 -0
- package/dist/harbor/registry.js +13 -0
- package/dist/harbor/terrain.d.ts +8 -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 +20 -0
- package/dist/pointer/bodies.js +45 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +34 -0
- package/dist/pointer/world.js +76 -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/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 +5 -10
- package/dist/ward/index.js +6 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/stance.d.ts +29 -20
- package/dist/ward/stance.js +173 -406
- package/dist/ward/ward-being.d.ts +33 -0
- package/dist/ward/ward-being.js +76 -0
- package/dist/ward/ward.d.ts +43 -11
- package/dist/ward/ward.js +215 -363
- package/package.json +19 -33
- package/src/being/being.ts +31 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +66 -0
- package/src/being/index.ts +5 -6
- package/src/being/types.ts +47 -145
- package/src/being/words.ts +31 -0
- package/src/contract/index.ts +44 -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 +134 -0
- package/src/harbor/dock.ts +101 -0
- package/src/harbor/harbor.ts +105 -0
- package/src/harbor/index.ts +5 -19
- package/src/harbor/registry.ts +20 -0
- package/src/harbor/terrain.ts +13 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +47 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +91 -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 +19 -0
- package/src/stand/stand.ts +178 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +6 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/stance.ts +185 -420
- package/src/ward/ward-being.ts +97 -0
- package/src/ward/ward.ts +229 -363
- 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/harbor/dial.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// A harbor as a dialer: one held socket to a listener's URL, its own
|
|
3
|
-
// pks announced, the listener's bound, and a reconnect with backoff from a
|
|
4
|
-
// second to thirty when the line drops. Seed, partition and keys are here;
|
|
5
|
-
// only the reach comes and goes. While the socket is open it is also the
|
|
6
|
-
// harbor's fallback: a pk nobody here knows is sent to the listener, which
|
|
7
|
-
// is the rendezvous, and it is the fallback from the moment it is dialed,
|
|
8
|
-
// because a ward asks the instant it boots and a socket's carry waits for
|
|
9
|
-
// the handshake itself. Terrain-free: a daemon on a Mac and a tab dial alike,
|
|
10
|
-
// on the WebSocket every terrain has.
|
|
11
|
-
import type { Harbor } from './core.ts';
|
|
12
|
-
import { REFUSED, Socket, type Line } from './reach.ts';
|
|
13
|
-
|
|
14
|
-
// `wake` is the device saying it is back: a phone out of a pocket, a
|
|
15
|
-
// laptop's lid opened. A device that slept comes back with its timer frozen
|
|
16
|
-
// and, often, its socket closed under it by the system without a word. So
|
|
17
|
-
// nothing in hand is trusted: whatever line there is, open or still opening,
|
|
18
|
-
// is put down and a new one is dialed now.
|
|
19
|
-
//
|
|
20
|
-
// Nothing here can tell a line the system dropped from a live one, and
|
|
21
|
-
// asking is worse than dialing. An announce written into a dead line is
|
|
22
|
-
// taken into the send buffer and the error comes when TCP gives up
|
|
23
|
-
// retransmitting, one to two minutes on a Mac and up to fifteen on Linux,
|
|
24
|
-
// and for all of it every ask to a pk bound behind that line hears `late`.
|
|
25
|
-
// A probe with a bound would want a round trip the wire does not have: four
|
|
26
|
-
// words, none of them a ping, and making one out of an ask would give a
|
|
27
|
-
// second kit's listener a meaning to reproduce. Dialing costs one handshake
|
|
28
|
-
// when the line was fine, and an ask made meanwhile waits on the new socket
|
|
29
|
-
// rather than being lost, because a socket is a fallback from the moment it
|
|
30
|
-
// is dialed.
|
|
31
|
-
export type Dialer = { url: string; socket: Socket | null; wake(): void; close(): void };
|
|
32
|
-
|
|
33
|
-
export function dial(harbor: Harbor, url: string): Dialer {
|
|
34
|
-
const d: Dialer = { url, socket: null, wake: () => {}, close: () => {} };
|
|
35
|
-
let stopped = false,
|
|
36
|
-
wait = 1000,
|
|
37
|
-
timer: ReturnType<typeof setTimeout> | undefined,
|
|
38
|
-
line: Line | undefined, // the line in hand, open or still opening: close() closes it either way
|
|
39
|
-
held: { socket: Socket; shed: () => void } | undefined; // the one this dialer owns now, and how it gives back what the line took
|
|
40
|
-
const connect = () => {
|
|
41
|
-
timer = undefined; // no wait pending: this is the dial it was waiting for
|
|
42
|
-
if (stopped) return;
|
|
43
|
-
line = new WebSocket(url.replace(/\/$/, '').replace(/^http/, 'ws')) as unknown as Line;
|
|
44
|
-
const s: Socket = new Socket(
|
|
45
|
-
line,
|
|
46
|
-
(pk, bytes) => harbor.deliver(pk, bytes),
|
|
47
|
-
(far) => {
|
|
48
|
-
// A line that opened is not a line that works. The wait goes back to
|
|
49
|
-
// a second here, where the far side has answered and been taken, and
|
|
50
|
-
// not at the handshake: a listener that opens and then refuses every
|
|
51
|
-
// time would otherwise be dialed at a fixed two seconds for good.
|
|
52
|
-
wait = 1000;
|
|
53
|
-
void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
|
|
54
|
-
},
|
|
55
|
-
(why) => {
|
|
56
|
-
shed();
|
|
57
|
-
// A line this dialer already put down. Its close arrives whenever the
|
|
58
|
-
// system gets round to noticing, which on a socket that died under a
|
|
59
|
-
// sleeping device is minutes; by then the line dialed in its place is
|
|
60
|
-
// the one in hand, so this one gives back what it took and dials
|
|
61
|
-
// nothing. Only the line the dialer still holds dials again.
|
|
62
|
-
if (held?.socket !== s) return;
|
|
63
|
-
held = undefined;
|
|
64
|
-
// A suite this listener will not speak is not a line that dropped. It
|
|
65
|
-
// will not become speakable by asking again sooner, so the wait goes
|
|
66
|
-
// straight to the ceiling and stays there until it changes its mind.
|
|
67
|
-
if (why.code === REFUSED) wait = 30000;
|
|
68
|
-
if (!stopped) timer = setTimeout(connect, wait);
|
|
69
|
-
wait = Math.min(wait * 2, 30000);
|
|
70
|
-
},
|
|
71
|
-
);
|
|
72
|
-
// Everything this line took from the harbor, given back. Said when it
|
|
73
|
-
// closes, and when the dialer puts it down without waiting for that.
|
|
74
|
-
const shed = () => {
|
|
75
|
-
harbor.announcers.delete(say);
|
|
76
|
-
harbor.unbind(s);
|
|
77
|
-
if (d.socket === s) d.socket = null;
|
|
78
|
-
const at = harbor.fallbacks.indexOf(s);
|
|
79
|
-
if (at !== -1) harbor.fallbacks.splice(at, 1); // this line only: another dialer's stays
|
|
80
|
-
};
|
|
81
|
-
held = { socket: s, shed };
|
|
82
|
-
harbor.fallbacks.push(s); // from the moment it is dialed: an ask made before the handshake waits on the socket, and is not unreached
|
|
83
|
-
// What this harbor holds, said when the line opens and again whenever it
|
|
84
|
-
// changes. A ward booted or adopted after the handshake is announced on
|
|
85
|
-
// the line already in hand, and a dropped one stops being claimed at once.
|
|
86
|
-
const say = () => s.announce([...harbor.doors.keys()]);
|
|
87
|
-
line.addEventListener('open', () => {
|
|
88
|
-
if (stopped) return s.close(); // closed while it was opening: it opens into nothing
|
|
89
|
-
d.socket = s;
|
|
90
|
-
harbor.announcers.add(say);
|
|
91
|
-
say();
|
|
92
|
-
});
|
|
93
|
-
};
|
|
94
|
-
d.wake = () => {
|
|
95
|
-
if (stopped) return;
|
|
96
|
-
clearTimeout(timer);
|
|
97
|
-
timer = undefined;
|
|
98
|
-
wait = 1000; // the device is back, and the wait it froze belongs to the sleep, not to this line
|
|
99
|
-
const old = held;
|
|
100
|
-
held = undefined; // put down before the new one is dialed, so a fallback list never holds the dead line ahead of the live one
|
|
101
|
-
old?.shed();
|
|
102
|
-
old?.socket.close();
|
|
103
|
-
connect();
|
|
104
|
-
};
|
|
105
|
-
d.close = () => {
|
|
106
|
-
stopped = true;
|
|
107
|
-
clearTimeout(timer);
|
|
108
|
-
line?.close(); // open or opening: a line closed while connecting fires close and never open
|
|
109
|
-
};
|
|
110
|
-
connect();
|
|
111
|
-
return d;
|
|
112
|
-
}
|
package/src/harbor/memory.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// A memory harbor. One process, no wire. Owns what a device owns: the code
|
|
3
|
-
// half, the memory half, the routing. Boots a ward by passing ONE object and
|
|
4
|
-
// keeping TWO pointers. Routes one ward pk to one door. Knows no being,
|
|
5
|
-
// reads no partition, opens no byte. Several memory harbors may be linked,
|
|
6
|
-
// which stands in for a wire, and cut, which stands in for weather.
|
|
7
|
-
//
|
|
8
|
-
// It works in acts, as every harbor does. `up` is act one: the box's own
|
|
9
|
-
// ward on the plain ground, the box's being and the lent beings in it.
|
|
10
|
-
// `host` is act two for one ward: on the roll, and handed one invitation
|
|
11
|
-
// on the box's being. A
|
|
12
|
-
// ward booted with `boot` alone is handed no box and lends nothing, which is
|
|
13
|
-
// what a suite of the ward wants.
|
|
14
|
-
import type { BeingClass, BeingLike, Invitation } from '../being/types.ts';
|
|
15
|
-
import { maker, entropy, learnPk, volatile, type Ground, type WardPointers } from '../ward/ground.ts';
|
|
16
|
-
import { hex } from '../ward/arithmetic.ts';
|
|
17
|
-
import { Box, BOX_SEED, stand, offerTo, roll } from './box.ts';
|
|
18
|
-
|
|
19
|
-
export type Booted = WardPointers & { pk: string };
|
|
20
|
-
export type WardFactory = (ground: Ground) => Promise<WardPointers>;
|
|
21
|
-
|
|
22
|
-
export class MemoryHarbor {
|
|
23
|
-
readonly doors = new Map<string, WardPointers['door']>(); // ward pk -> door
|
|
24
|
-
readonly down = new Set<string>(); // ward pks this harbor cannot reach right now
|
|
25
|
-
readonly wire: string[] = []; // every byte string that ever crossed, as hex. for inspection
|
|
26
|
-
readonly refused = new Map<string, number>(); // ward pk -> arrivals its door would not admit: strangers, counted
|
|
27
|
-
readonly peers = new Set<MemoryHarbor>();
|
|
28
|
-
cut = false;
|
|
29
|
-
readonly partitions = new Map<string, Record<string, unknown>>(); // seed -> memory. the harbor keeps it and reads nothing
|
|
30
|
-
readonly wards = new Map<string, Booted>(); // seed -> pointers
|
|
31
|
-
readonly objects = new WeakMap<object, BeingLike>(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
|
|
32
|
-
// Where the wards this harbor boots draw their entropy. The device's own
|
|
33
|
-
// by default. A harbor may hand a fixed stream instead, which is what pins
|
|
34
|
-
// bytes: every key a ward mints and every nonce it seals under comes from
|
|
35
|
-
// here, so a ward on a known seed with a known stream answers the same
|
|
36
|
-
// bytes every run, and a corpus of them is a corpus and not a sample.
|
|
37
|
-
random: Ground['random'] = entropy;
|
|
38
|
-
|
|
39
|
-
// the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
|
|
40
|
-
async route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
|
|
41
|
-
if (this.down.has(farPk)) return undefined;
|
|
42
|
-
const door = this.doors.get(farPk);
|
|
43
|
-
if (door) {
|
|
44
|
-
const r = await door(bytes);
|
|
45
|
-
if (!r.heard) this.refused.set(farPk, (this.refused.get(farPk) ?? 0) + 1);
|
|
46
|
-
return r.bytes;
|
|
47
|
-
}
|
|
48
|
-
const peer = [...this.peers].find((h) => h.doors.has(farPk) && !h.down.has(farPk));
|
|
49
|
-
if (!peer || this.cut) return undefined; // nothing. the harbor has no word silence
|
|
50
|
-
return peer.route(farPk, bytes);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
link(harbor: MemoryHarbor): void {
|
|
54
|
-
this.peers.add(harbor);
|
|
55
|
-
harbor.peers.add(this);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// boot by seed. the harbor gets a door and an ask, and learns the pk by
|
|
59
|
-
// asking. `box` is the invitation on this device's box being, for this
|
|
60
|
-
// ward: left out, the ward holds no box and every lend is null.
|
|
61
|
-
async boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, box?: Invitation): Promise<Booted> {
|
|
62
|
-
this.partitions.set(seed, {});
|
|
63
|
-
return this.reboot(seed, Ward, classes, box);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// a restart: same seed, same partition, fresh ward. the ward's files are the harbor's to keep.
|
|
67
|
-
async reboot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, box?: Invitation): Promise<Booted> {
|
|
68
|
-
const rows = this.partitions.get(seed);
|
|
69
|
-
if (!rows) throw new Error(`no partition for ${seed}`);
|
|
70
|
-
const ground: Ground = {
|
|
71
|
-
seed,
|
|
72
|
-
memory: volatile(rows),
|
|
73
|
-
...(box ? { box } : {}),
|
|
74
|
-
// One objects map for the whole harbor, not one per ward: every ward
|
|
75
|
-
// here is in this process and a test reaches any being of any of them.
|
|
76
|
-
instantiate: maker(this.objects, classes),
|
|
77
|
-
// bytes cross, copied, never a reference. both ways.
|
|
78
|
-
carry: async (farPk, bytes) => {
|
|
79
|
-
this.wire.push(hex(bytes));
|
|
80
|
-
const back = await this.route(farPk, new Uint8Array(bytes));
|
|
81
|
-
return back === undefined ? undefined : new Uint8Array(back);
|
|
82
|
-
},
|
|
83
|
-
random: (n) => this.random(n),
|
|
84
|
-
};
|
|
85
|
-
const w = await Ward(ground);
|
|
86
|
-
const booted: Booted = { ...w, pk: await learnPk(w) };
|
|
87
|
-
this.doors.set(booted.pk, booted.door);
|
|
88
|
-
this.wards.set(seed, booted);
|
|
89
|
-
return booted;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Act one. The box's own ward, the box's being and the lent beings named,
|
|
93
|
-
// each booted under its name and held by her under it. Run again on a
|
|
94
|
-
// harbor whose box ward stands, it changes nothing.
|
|
95
|
-
async up(Ward: WardFactory, lent: Record<string, BeingClass> = {}): Promise<Booted> {
|
|
96
|
-
const classes: Record<string, BeingClass> = { Box };
|
|
97
|
-
for (const C of Object.values(lent)) classes[C.name] = C;
|
|
98
|
-
const box = this.wards.get(BOX_SEED) ?? (await this.boot(BOX_SEED, Ward, classes));
|
|
99
|
-
await stand(box, lent);
|
|
100
|
-
return box;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
get box(): Booted | undefined {
|
|
104
|
-
return this.wards.get(BOX_SEED);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Act two, one ward: on the roll and handed one invitation on the box's being.
|
|
108
|
-
// A harbor with no box ward up hosts as `boot` does, with nothing.
|
|
109
|
-
async host(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>): Promise<Booted> {
|
|
110
|
-
const box = this.box;
|
|
111
|
-
const inv = box ? await offerTo(box, seed) : undefined;
|
|
112
|
-
return this.partitions.has(seed) ? this.reboot(seed, Ward, classes, inv) : this.boot(seed, Ward, classes, inv);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Act two, whole: the box's being says who is hosted, and each comes up.
|
|
116
|
-
async hosted(Ward: WardFactory, classes: Record<string, BeingClass>): Promise<string[]> {
|
|
117
|
-
const box = this.box;
|
|
118
|
-
if (!box) return [];
|
|
119
|
-
const seeds = await roll(box);
|
|
120
|
-
for (const seed of seeds) await this.host(seed, Ward, classes);
|
|
121
|
-
return seeds;
|
|
122
|
-
}
|
|
123
|
-
}
|
package/src/harbor/reach.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Reach: carrying bytes to a pk off the device. One interface, two
|
|
3
|
-
// implementations:
|
|
4
|
-
//
|
|
5
|
-
// request a URL. POST the bytes to `<url>/<pk>`, get bytes back.
|
|
6
|
-
// Listener to listener.
|
|
7
|
-
// socket a held WebSocket at `<url>`, opened by whichever side can
|
|
8
|
-
// dial, used in both directions with a frame id. Each side
|
|
9
|
-
// announces the ward pks it holds when the socket opens, and the
|
|
10
|
-
// other binds them.
|
|
11
|
-
//
|
|
12
|
-
// Both are written on what every terrain has: fetch, and the standard
|
|
13
|
-
// WebSocket surface, which a browser, Node, an edge worker's pair and the
|
|
14
|
-
// `ws` server all offer. The listener half of a socket is the terrain's
|
|
15
|
-
// own and stays outside this tree; what is here is the framing, either end
|
|
16
|
-
// of a line, and the request. Nothing here names a runtime, and
|
|
17
|
-
// `src/conformance/reach.ts` is what every reach passes. A reach carries and returns; it reads
|
|
18
|
-
// nothing. `undefined` is a promise that nothing was delivered: no such pk
|
|
19
|
-
// at the far end, a connection that would not open, a socket that is gone.
|
|
20
|
-
// A reach that sent the bytes and lost the line afterwards answers nothing
|
|
21
|
-
// at all, and the ward's own bound ends the ask.
|
|
22
|
-
import { concat, hex, unhex } from '../ward/arithmetic.ts';
|
|
23
|
-
import { readJson } from '../ward/json.ts';
|
|
24
|
-
|
|
25
|
-
export type Carry = (pk: string, bytes: Uint8Array) => Promise<Uint8Array | undefined>;
|
|
26
|
-
export type Reach = { carry: Carry; close(): void };
|
|
27
|
-
|
|
28
|
-
const never = () => new Promise<undefined>(() => {}); // sent, and no word since: the ward's bound decides
|
|
29
|
-
|
|
30
|
-
// A request reach to one listener.
|
|
31
|
-
export function request(url: string): Reach {
|
|
32
|
-
const base = url.replace(/\/$/, '');
|
|
33
|
-
return {
|
|
34
|
-
close: () => {},
|
|
35
|
-
carry: async (pk, bytes) => {
|
|
36
|
-
let res: Response;
|
|
37
|
-
// A body of its own, not the caller's view: fetch reads it after this
|
|
38
|
-
// line returns, and a buffer somebody else may write is not a body. The
|
|
39
|
-
// copy is its own ArrayBuffer, which is what a body is; the bytes handed
|
|
40
|
-
// in are typed as backed by anything, shared memory included.
|
|
41
|
-
const body = new Uint8Array(bytes.length);
|
|
42
|
-
body.set(bytes);
|
|
43
|
-
try {
|
|
44
|
-
res = await fetch(`${base}/${pk}`, { method: 'POST', headers: { 'content-type': 'application/octet-stream', 'quo-suite': String(SUITE) }, body });
|
|
45
|
-
} catch {
|
|
46
|
-
return undefined; // the connection never opened: nothing was delivered
|
|
47
|
-
}
|
|
48
|
-
// A status that says the listener did not take the bytes is nothing
|
|
49
|
-
// delivered: no reach for that pk, a request it refused on its face, a
|
|
50
|
-
// gateway with nobody behind it. Any other failure is a listener that
|
|
51
|
-
// took them and failed after, which is sent and no word since.
|
|
52
|
-
if (res.status === 404 || (res.status >= 400 && res.status < 500) || res.status === 502 || res.status === 503 || res.status === 504) return undefined;
|
|
53
|
-
if (!res.ok) return never();
|
|
54
|
-
return new Uint8Array(await res.arrayBuffer());
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// The frames on a socket. Binary, one byte of kind first.
|
|
60
|
-
// ask [0][id: 4 bytes][pk: 64 bytes][bytes]
|
|
61
|
-
// reply [1][id: 4 bytes][bytes]
|
|
62
|
-
// none [2][id: 4 bytes] nothing was delivered
|
|
63
|
-
// And one text frame, JSON: { announce: [pk, ...], suite }, the ward pks this
|
|
64
|
-
// side holds and the wire suite it speaks, sent when the socket opens and
|
|
65
|
-
// whenever the pks change.
|
|
66
|
-
const ASK = 0,
|
|
67
|
-
REPLY = 1,
|
|
68
|
-
NONE = 2;
|
|
69
|
-
|
|
70
|
-
// The wire suite: which frames these are and which five algorithms seal what
|
|
71
|
-
// they carry. One value, and it is not negotiated. It rides in the handshake
|
|
72
|
-
// and never on an ask, so an ask stays bytes nobody can tell from noise, and
|
|
73
|
-
// a line still learns before it carries anything whether the far side speaks
|
|
74
|
-
// what this side speaks. A side meeting a suite it does not know carries
|
|
75
|
-
// nothing for it and closes: the alternative is bytes that will never open,
|
|
76
|
-
// answered by a silence that says nothing about why, forever. Nothing
|
|
77
|
-
// changes this number before 1.0.0. After it, this is the one place a second
|
|
78
|
-
// suite can be told from a first without guessing.
|
|
79
|
-
export const SUITE = 1;
|
|
80
|
-
|
|
81
|
-
// The close a side sends when it refuses a suite, in the range a protocol
|
|
82
|
-
// may spend on itself. A line that just closes tells the far side nothing,
|
|
83
|
-
// and a dialer that learns nothing dials again on its backoff for good: the
|
|
84
|
-
// whole reason this number exists is that a kit which cannot be understood
|
|
85
|
-
// should be told so instead of meeting a silence that names no reason. So
|
|
86
|
-
// the refusal names itself on the way out, and the far side may read it.
|
|
87
|
-
export const REFUSED = 4001;
|
|
88
|
-
const OPEN = 1; // WebSocket.OPEN, on every terrain
|
|
89
|
-
// How many asks a socket keeps a resolver for. An ask the far side never
|
|
90
|
-
// answers is ended by the ward's bound, and its resolver stays until the
|
|
91
|
-
// line closes; a far side that answers nothing for long enough would grow
|
|
92
|
-
// the map for as long as this side asks, so the oldest goes when it is full.
|
|
93
|
-
const PENDING = 4096;
|
|
94
|
-
|
|
95
|
-
// The standard surface, and no more of it: what a browser WebSocket, Node's
|
|
96
|
-
// WebSocket and a `ws` server socket all have.
|
|
97
|
-
export type Line = {
|
|
98
|
-
readyState: number;
|
|
99
|
-
binaryType: string;
|
|
100
|
-
send(data: string | Uint8Array): void;
|
|
101
|
-
close(code?: number, reason?: string): void;
|
|
102
|
-
addEventListener(type: 'open' | 'error', fn: () => void): void;
|
|
103
|
-
// A close carries why, where the terrain gives it: the code and the reason
|
|
104
|
-
// the far side named. A line that just ends names neither.
|
|
105
|
-
addEventListener(type: 'close', fn: (e?: { code?: number; reason?: string }) => void): void;
|
|
106
|
-
addEventListener(type: 'message', fn: (e: { data: unknown }) => void): void;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
export type Announce = (pks: string[]) => void;
|
|
110
|
-
|
|
111
|
-
// One held socket, either side of it. `deliver` is what this side does with
|
|
112
|
-
// an ask that arrives for a pk: its own door, or a socket it holds for that
|
|
113
|
-
// pk, and nothing else. `onAnnounce` learns the far side's pks; `onClose`
|
|
114
|
-
// forgets them.
|
|
115
|
-
export class Socket implements Reach {
|
|
116
|
-
readonly line: Line;
|
|
117
|
-
readonly pending = new Map<number, (bytes: Uint8Array | undefined) => void>();
|
|
118
|
-
far: string[] = []; // the pks the far side announced
|
|
119
|
-
#id = 1;
|
|
120
|
-
#open: Promise<void>;
|
|
121
|
-
|
|
122
|
-
constructor(line: Line, deliver: Carry, onAnnounce: Announce, onClose: (why: { code?: number; reason?: string }) => void) {
|
|
123
|
-
this.line = line;
|
|
124
|
-
line.binaryType = 'arraybuffer';
|
|
125
|
-
// A line that opens is a reach; one that errors, or closes before it
|
|
126
|
-
// opened, is a reach that failed, and an ask waiting on the handshake is
|
|
127
|
-
// nothing delivered.
|
|
128
|
-
this.#open =
|
|
129
|
-
line.readyState === OPEN
|
|
130
|
-
? Promise.resolve()
|
|
131
|
-
: new Promise((ok, no) => {
|
|
132
|
-
line.addEventListener('open', ok);
|
|
133
|
-
line.addEventListener('error', no);
|
|
134
|
-
line.addEventListener('close', () => no(new Error('closed before it opened')));
|
|
135
|
-
});
|
|
136
|
-
this.#open.catch(() => {}); // a handshake refused is a reach that failed, not a process that dies
|
|
137
|
-
line.addEventListener('message', (e) => {
|
|
138
|
-
if (typeof e.data === 'string') {
|
|
139
|
-
try {
|
|
140
|
-
const m = readJson(e.data) as { announce?: unknown; suite?: unknown };
|
|
141
|
-
if (m === null || typeof m !== 'object') return;
|
|
142
|
-
// Absent is this suite: a side that says nothing says one.
|
|
143
|
-
if (m.suite !== undefined && m.suite !== SUITE) {
|
|
144
|
-
this.far = [];
|
|
145
|
-
line.close(REFUSED, 'suite');
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (Array.isArray(m.announce)) {
|
|
149
|
-
this.far = m.announce.filter((x): x is string => typeof x === 'string');
|
|
150
|
-
onAnnounce(this.far);
|
|
151
|
-
}
|
|
152
|
-
} catch {
|
|
153
|
-
/* not for us */
|
|
154
|
-
}
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
const buf = e.data instanceof ArrayBuffer ? new Uint8Array(e.data) : ArrayBuffer.isView(e.data) ? new Uint8Array(e.data.buffer, e.data.byteOffset, e.data.byteLength) : undefined;
|
|
158
|
-
// A frame that is none of the three is not a frame: an unknown kind, an
|
|
159
|
-
// ask too short to hold a pk, a nothing with bytes after its id. It is
|
|
160
|
-
// dropped, and tells no ask waiting on its id that nothing was delivered.
|
|
161
|
-
if (!buf || buf.length < 5) return;
|
|
162
|
-
const kind = buf[0],
|
|
163
|
-
id = new DataView(buf.buffer, buf.byteOffset, buf.byteLength).getUint32(1);
|
|
164
|
-
if (kind > NONE || (kind === ASK && buf.length < 69) || (kind === NONE && buf.length !== 5)) return;
|
|
165
|
-
if (kind === ASK) {
|
|
166
|
-
const pk = hex(buf.subarray(5, 69));
|
|
167
|
-
// A deliver that rejects has delivered nothing this side can vouch
|
|
168
|
-
// for, and a listener does not die of it: the far side hears none.
|
|
169
|
-
void deliver(pk, new Uint8Array(buf.subarray(69)))
|
|
170
|
-
.then(
|
|
171
|
-
(back) => back,
|
|
172
|
-
() => undefined,
|
|
173
|
-
)
|
|
174
|
-
.then((back) => {
|
|
175
|
-
const head = new Uint8Array(5);
|
|
176
|
-
head[0] = back === undefined ? NONE : REPLY;
|
|
177
|
-
new DataView(head.buffer).setUint32(1, id);
|
|
178
|
-
if (line.readyState === OPEN) line.send(back === undefined ? head : concat([head, back]));
|
|
179
|
-
});
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
const waiting = this.pending.get(id);
|
|
183
|
-
if (!waiting) return;
|
|
184
|
-
this.pending.delete(id);
|
|
185
|
-
waiting(kind === REPLY ? new Uint8Array(buf.subarray(5)) : undefined);
|
|
186
|
-
});
|
|
187
|
-
line.addEventListener('error', () => {});
|
|
188
|
-
line.addEventListener('close', (e) => {
|
|
189
|
-
this.pending.clear(); // in flight and no word since: nothing is answered
|
|
190
|
-
onClose({ code: e?.code, reason: e?.reason });
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Say which ward pks this side holds.
|
|
195
|
-
announce(pks: string[]): void {
|
|
196
|
-
if (this.line.readyState === OPEN) this.line.send(JSON.stringify({ announce: pks, suite: SUITE }));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
|
|
200
|
-
try {
|
|
201
|
-
await this.#open;
|
|
202
|
-
} catch {
|
|
203
|
-
return undefined;
|
|
204
|
-
}
|
|
205
|
-
if (this.line.readyState !== OPEN) return undefined;
|
|
206
|
-
const id = this.#id++;
|
|
207
|
-
const head = new Uint8Array(69);
|
|
208
|
-
head[0] = ASK;
|
|
209
|
-
new DataView(head.buffer).setUint32(1, id);
|
|
210
|
-
head.set(unhex(pk), 5);
|
|
211
|
-
return new Promise((ok) => {
|
|
212
|
-
if (this.pending.size >= PENDING) this.pending.delete(this.pending.keys().next().value!); // the oldest, which the ward's bound ended long ago
|
|
213
|
-
this.pending.set(id, ok);
|
|
214
|
-
this.line.send(concat([head, bytes]));
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
close(): void {
|
|
219
|
-
this.line.close();
|
|
220
|
-
}
|
|
221
|
-
}
|
package/src/harbor/store.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The store: keeping wards by name. A ward is three things and the store
|
|
3
|
-
// keeps all three under one name: the seed, the partition and the ward
|
|
4
|
-
// record, which says where the class bodies come from and which being is
|
|
5
|
-
// the user's. The store reads nothing it keeps; the partition is
|
|
6
|
-
// values the ward wrote, and the seed is bytes only the ward derives from.
|
|
7
|
-
// Every terrain has one: files on a disk, IndexedDB in a tab, an object's
|
|
8
|
-
// storage on the edge; the memory store below is the library's own, for a
|
|
9
|
-
// harbor core with no device under it. `src/conformance/store.ts` is what
|
|
10
|
-
// every one of them passes.
|
|
11
|
-
import { fromRows, rowOf, rowsIn, type Partition } from '../ward/partition.ts';
|
|
12
|
-
|
|
13
|
-
export type WardRecord = { pk: string; code: string; user: string };
|
|
14
|
-
export type Kept = { seed: Uint8Array; partition: Record<string, unknown>; record: WardRecord };
|
|
15
|
-
|
|
16
|
-
export type Store = {
|
|
17
|
-
list(): Promise<string[]>;
|
|
18
|
-
load(name: string): Promise<Kept | undefined>;
|
|
19
|
-
// Keep all three under the name; a name already kept is refused.
|
|
20
|
-
put(name: string, kept: Kept): Promise<void>;
|
|
21
|
-
// The partition back, after a call; the record back, after a boot learned
|
|
22
|
-
// the pk. A name not kept is nothing, on both.
|
|
23
|
-
//
|
|
24
|
-
// `rows` names what the ward wrote since the last save: a being by her key,
|
|
25
|
-
// or `HEAD`. A store may write only those, which is what `rowsOf` is for,
|
|
26
|
-
// and a store that writes the whole partition every time is correct and
|
|
27
|
-
// slower. A being's cells are the one part of a partition that grows
|
|
28
|
-
// without limit, so a store that keeps her apart lets her neighbour be
|
|
29
|
-
// asked without her being written down again.
|
|
30
|
-
save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
|
|
31
|
-
record(name: string, record: WardRecord): Promise<void>;
|
|
32
|
-
// Take a ward out: the first move of a migration. What was kept comes back.
|
|
33
|
-
take(name: string): Promise<Kept | undefined>;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// The partition is values only, and the ward hands it out through a guard,
|
|
37
|
-
// so a store takes a copy of the values the way a file would: through JSON.
|
|
38
|
-
export const values = (p: Record<string, unknown>): Record<string, unknown> => JSON.parse(JSON.stringify(p)) as Record<string, unknown>;
|
|
39
|
-
|
|
40
|
-
// The rows a store was told about, as values, ready to be written down. A row
|
|
41
|
-
// whose key names nobody is a being who left, and comes back undefined, which
|
|
42
|
-
// is the store dropping what it holds for her.
|
|
43
|
-
export const rowsOf = (partition: Record<string, unknown>, rows: readonly string[]): [string, Record<string, unknown> | undefined][] =>
|
|
44
|
-
rows.map((row) => {
|
|
45
|
-
const value = rowOf(partition as unknown as Partition, row);
|
|
46
|
-
return [row, value === undefined ? undefined : (values(value))];
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// One store's copy, brought up to date with the rows it was told about.
|
|
50
|
-
const keep = (row: Row, partition: Record<string, unknown>, rows: readonly string[]): void => {
|
|
51
|
-
for (const [id, value] of rowsOf(partition, rows)) {
|
|
52
|
-
if (value === undefined) row.parts.delete(id);
|
|
53
|
-
else row.parts.set(id, value);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// The store as a map, one process, nothing kept past it. It keeps a ward as
|
|
58
|
-
// rows, the way a store on a device does, so what it costs to save is what a
|
|
59
|
-
// device pays and the library's own measurements mean something.
|
|
60
|
-
type Row = { seed: Uint8Array; parts: Map<string, Record<string, unknown>>; record: WardRecord };
|
|
61
|
-
|
|
62
|
-
export class MemoryStore implements Store {
|
|
63
|
-
readonly rows = new Map<string, Row>();
|
|
64
|
-
|
|
65
|
-
async list(): Promise<string[]> {
|
|
66
|
-
return [...this.rows.keys()];
|
|
67
|
-
}
|
|
68
|
-
async load(name: string): Promise<Kept | undefined> {
|
|
69
|
-
const row = this.rows.get(name);
|
|
70
|
-
return row && { seed: new Uint8Array(row.seed), partition: fromRows(Object.fromEntries(row.parts)), record: { ...row.record } };
|
|
71
|
-
}
|
|
72
|
-
async put(name: string, kept: Kept): Promise<void> {
|
|
73
|
-
if (this.rows.has(name)) throw new Error(`ward ${name} already exists here`);
|
|
74
|
-
const row: Row = { seed: new Uint8Array(kept.seed), parts: new Map(), record: { ...kept.record } };
|
|
75
|
-
this.rows.set(name, row);
|
|
76
|
-
keep(row, kept.partition, rowsIn(kept.partition));
|
|
77
|
-
}
|
|
78
|
-
async save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void> {
|
|
79
|
-
const row = this.rows.get(name);
|
|
80
|
-
if (row) keep(row, partition, rows);
|
|
81
|
-
}
|
|
82
|
-
async record(name: string, record: WardRecord): Promise<void> {
|
|
83
|
-
const row = this.rows.get(name);
|
|
84
|
-
if (row) row.record = { ...record };
|
|
85
|
-
}
|
|
86
|
-
async take(name: string): Promise<Kept | undefined> {
|
|
87
|
-
const kept = await this.load(name);
|
|
88
|
-
this.rows.delete(name);
|
|
89
|
-
return kept;
|
|
90
|
-
}
|
|
91
|
-
}
|