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/ward/stance.ts
CHANGED
|
@@ -1,433 +1,198 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
9
|
-
import { allow,
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
instantiate(className: string, key: string): string | null;
|
|
30
|
-
// mint an occupant on a being of this ward, for the being who made her.
|
|
31
|
-
relate(key: string, id: string): Promise<Invitation | null>;
|
|
32
|
-
// an invitation to one of the harbor's own beings, by the harbor's name for
|
|
33
|
-
// it, handed to the taker and never returned. false where this box lends
|
|
34
|
-
// nothing of that name to this ward, or where the taker did not take it, in
|
|
35
|
-
// which case the harbor has already removed what it minted.
|
|
36
|
-
lend(name: string, take: (invitation: Invitation) => Promise<boolean>): Promise<boolean>;
|
|
37
|
-
// take a being of this ward out again, with every relation she holds.
|
|
38
|
-
unmake(key: string): void;
|
|
39
|
-
// the bind table was written: a record put, dropped or moved on.
|
|
40
|
-
wrote(): void;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// An invitation as it travels: the far ward's pk, the heir pk, the heir's secret seed. Or just the ward, for its public being.
|
|
44
|
-
type SealedInvitation ={ ward: string; heir?: string; secret?: string };
|
|
2
|
+
// One being's stance: her cells, the ids she invited, the relations she
|
|
3
|
+
// holds, and boot. Every relation she holds speaks through `quo/`'s
|
|
4
|
+
// standing, one send at a time, inside her allowance.
|
|
5
|
+
import { readValue, writeValue } from '../crypto/index.ts';
|
|
6
|
+
import { readInvitation, Standing as Wire, freshStanding, type Door, type Draw, type Invitation, type Read } from '../quo/index.ts';
|
|
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';
|
|
9
|
+
import { allow, Bell, LATE } from './allowance.ts';
|
|
10
|
+
import type { BeingRow, Partition } from './partition.ts';
|
|
11
|
+
import { Cells, DEPTH } from './cells.ts';
|
|
12
|
+
|
|
13
|
+
export { DEPTH };
|
|
14
|
+
|
|
15
|
+
// What a stance needs from its ward.
|
|
16
|
+
export interface Inside {
|
|
17
|
+
readonly partition: Partition;
|
|
18
|
+
readonly door: Door;
|
|
19
|
+
readonly draw: Draw;
|
|
20
|
+
readonly clock: Clock;
|
|
21
|
+
live(key: string, row: BeingRow): boolean;
|
|
22
|
+
send(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
23
|
+
boot(maker: string, className: string, key: string, id?: string): Promise<string | null>;
|
|
24
|
+
// An offer from the faculty the dock opened for a contract, and its
|
|
25
|
+
// retraction when the offer was not taken.
|
|
26
|
+
offer(contract: string): Promise<Invitation | null>;
|
|
27
|
+
retract(contract: string, heir: string): Promise<void>;
|
|
28
|
+
}
|
|
45
29
|
|
|
46
|
-
|
|
47
|
-
// a standing named after one of them would be unreachable: `standings.knock`
|
|
48
|
-
// is the call, whatever record sits under that name. One namespace means one
|
|
49
|
-
// list: these are refused at invite and at take, beside the ward's own words.
|
|
50
|
-
const CALLS = ['knock', 'take', 'remove'];
|
|
30
|
+
const RESERVED = new Set([OWNER]);
|
|
51
31
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// protocol names, and the ones this kit adds because its own spelling took
|
|
55
|
-
// them: the calls a standing already answers to, and a name no partition
|
|
56
|
-
// can hold. That last one is refused here rather than by the cells guard,
|
|
57
|
-
// because the guard throws and this table promises a null.
|
|
58
|
-
// A method is a word or it is nothing at all. The seal writes a payload as
|
|
59
|
-
// JSON, which drops a key whose value it cannot write, so a symbol or a
|
|
60
|
-
// function here would leave as an ask with no method: the empty ask. She
|
|
61
|
-
// would have asked for work, been handed a blueprint, and spent a number on
|
|
62
|
-
// it. Nothing that cannot be sent is sent, and what never left is unreached,
|
|
63
|
-
// which is the one answer that says asking again is safe. This is the same
|
|
64
|
-
// rule args are already held to.
|
|
65
|
-
const named = (method: unknown): boolean => method === undefined || typeof method === 'string';
|
|
66
|
-
const reserved = (id: string): boolean => RESERVED_IDS.includes(id) || CALLS.includes(id) || unkeepable(id);
|
|
67
|
-
// The name of one relation, as her side files it. A relation is a ward and
|
|
68
|
-
// a heir, never a heir alone: the heir pk is outer, it rides in the clear on
|
|
69
|
-
// every lid, and anyone who reads one can quote it back inside an invitation
|
|
70
|
-
// naming a ward of their own. Filed under the heir alone, that invitation
|
|
71
|
-
// would share her knock record with the relation the heir really belongs to:
|
|
72
|
-
// it would sign with that relation's key, and an answer to it would set the
|
|
73
|
-
// answered flag the other relation is taken on. Both halves, always.
|
|
74
|
-
const nameOf = (inv: SealedInvitation) => (inv.heir === undefined ? `public:${inv.ward}` : `${inv.ward}:${inv.heir}`);
|
|
75
|
-
const valid = (inv: unknown): inv is SealedInvitation => {
|
|
76
|
-
const i = inv as SealedInvitation;
|
|
77
|
-
if (!i || typeof i !== 'object' || !isWardPk(i.ward)) return false;
|
|
78
|
-
if (i.heir === undefined) return i.secret === undefined;
|
|
79
|
-
return isHex(i.heir) && isHex(i.secret);
|
|
80
|
-
};
|
|
32
|
+
// How many times a take asks again after its knock brought no object.
|
|
33
|
+
const TAKES = 2;
|
|
81
34
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const mine = (lanes.get(name) ?? Promise.resolve()).then(run, run);
|
|
92
|
-
const quiet = mine.then(
|
|
93
|
-
() => {},
|
|
94
|
-
() => {},
|
|
95
|
-
);
|
|
96
|
-
lanes.set(name, quiet);
|
|
97
|
-
void quiet.then(() => {
|
|
98
|
-
if (lanes.get(name) === quiet) lanes.delete(name); // the lane is idle. nothing to remember.
|
|
35
|
+
// One relation, one send at a time.
|
|
36
|
+
class Lanes {
|
|
37
|
+
readonly #tails = new Map<string, Promise<unknown>>();
|
|
38
|
+
run<T>(name: string, work: () => Promise<T>): Promise<T> {
|
|
39
|
+
const mine = (this.#tails.get(name) ?? Promise.resolve()).then(work, work);
|
|
40
|
+
const tail = mine.catch(() => {});
|
|
41
|
+
this.#tails.set(name, tail);
|
|
42
|
+
void tail.then(() => {
|
|
43
|
+
if (this.#tails.get(name) === tail) this.#tails.delete(name);
|
|
99
44
|
});
|
|
100
45
|
return mine;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Every ask a being makes, bounded. The bound is outside the lane on
|
|
104
|
-
// purpose: an ask that waits its turn behind a relation that comes back
|
|
105
|
-
// round never reaches a send, and a bound that only watched the wire would
|
|
106
|
-
// never see it hang. A bound that ran out is the word late, never
|
|
107
|
-
// unreached: unreached promises nothing was delivered and is safe to ask
|
|
108
|
-
// again, and a wait that gave up knows no such thing.
|
|
109
|
-
//
|
|
110
|
-
// The bell rings before the work is reached, too: an ask that was still
|
|
111
|
-
// waiting its turn when the wait ran out is never sent. Sent then, it would
|
|
112
|
-
// be a fresh ask nobody is waiting on, the far being would do the work, and
|
|
113
|
-
// what came back would be read into her cells behind a silence she was
|
|
114
|
-
// already handed. So the work is given the bell, and reads it at the lane.
|
|
115
|
-
const bounded = async (wanted: Wanted | undefined, work: (live: () => boolean) => Promise<Answer>): Promise<Answer> => {
|
|
116
|
-
let late = false;
|
|
117
|
-
const out = await within(allow(wanted).time, work(() => !late), () => {
|
|
118
|
-
late = true;
|
|
119
|
-
});
|
|
120
|
-
return out === LATE ? word('late') : out;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// What one send came back as, read in one place for every path that sends.
|
|
124
|
-
// A reply that opened and was signed by the far ward is the door speaking,
|
|
125
|
-
// whatever it said: an object, its silence, or a word. Bytes that were not
|
|
126
|
-
// Quo's, late and unreached are not the door speaking, and say nothing
|
|
127
|
-
// about whether it heard.
|
|
128
|
-
type Read = { answer: Answer; door: boolean; seen: string | null };
|
|
129
|
-
const read = (r: ReplyPayload | Silence | Word): Read => {
|
|
130
|
-
if (isSilence(r) || isWord(r)) return { answer: r, door: false, seen: null };
|
|
131
|
-
if ('silence' in r) return { answer: silence, door: true, seen: null };
|
|
132
|
-
if ('quo' in r) return { answer: word(r.quo), door: true, seen: null }; // the far door's word for her, sealed to her lid
|
|
133
|
-
return { answer: r.object, door: true, seen: r.seen };
|
|
134
|
-
};
|
|
135
|
-
// Whether the door spent the heir this send spoke under. An object, the
|
|
136
|
-
// door's silence and threw all mean it was honoured. A refusal word wrote
|
|
137
|
-
// nothing and says so. Anything that is not the door speaking says nothing.
|
|
138
|
-
const honoured = (r: Read): boolean => r.door && !(isWord(r.answer) && wordOf(r.answer) !== 'threw');
|
|
139
|
-
const answered = (r: Read): boolean => r.door && !isSilence(r.answer) && !isWord(r.answer);
|
|
140
|
-
|
|
141
|
-
// The ask is bounded twice with one budget, once around the lane and once
|
|
142
|
-
// around the wire, and the two bells are microseconds apart. A reply that
|
|
143
|
-
// lands between them is a reply she was already told was late: its keys are
|
|
144
|
-
// rotated, since the far door rotated too and nothing may un-rotate that,
|
|
145
|
-
// but nothing of it is written into her cells. What she was handed is the
|
|
146
|
-
// last word on what she saw.
|
|
147
|
-
const send = async (keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, rec?: Cells['standings'][string], live: () => boolean = () => true): Promise<Answer> => {
|
|
148
|
-
const r = read(await inside.send(bind, keys, method, args, wanted));
|
|
149
|
-
if (rec && method !== undefined && r.seen !== null && live()) rec.seen = r.seen; // the digest rode along
|
|
150
|
-
return r.answer;
|
|
151
|
-
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
152
48
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const keys = at(bind.standings, taken);
|
|
160
|
-
if (!keys) return word('dropped');
|
|
161
|
-
return lane(`ask:${taken}`, () => asking(taken, keys, at(cells.standings, taken), method, args, wanted, live));
|
|
162
|
-
}
|
|
163
|
-
// her own key for this relation, minted once: rotation two. reused if she knocks again.
|
|
164
|
-
const k = (bind.knocks[name] ??= { current: (await inside.mintKey(bind)).seed, next: null, spoke: false, sent: false, seq: 0 });
|
|
165
|
-
prune(bind.knocks, bind.answered); // this one is the newest, so it stays and the oldest never answered goes
|
|
166
|
-
// The first knock on a heir speaks as the heir, the key the inviter
|
|
167
|
-
// handed out, and announces her own. Once the door has honoured it the
|
|
168
|
-
// heir is spent, whatever she then said, and her own key speaks; a heir
|
|
169
|
-
// spent once stays spent.
|
|
170
|
-
//
|
|
171
|
-
// She sent once as the heir and the door did not speak: nothing came
|
|
172
|
-
// back, or bytes that were not Quo's, or the wait ran out. Either the
|
|
173
|
-
// door heard, spent the heir and rotated to the key she announced, or
|
|
174
|
-
// nothing arrived and the heir still stands. Only the far door knows,
|
|
175
|
-
// and it will not say.
|
|
176
|
-
//
|
|
177
|
-
// So she asks, and the asking tells her. Under her own key: if the door
|
|
178
|
-
// heard, that key is the one it admits, and she is answered, or told the
|
|
179
|
-
// door's word. If it did not, the key means nothing there, the ask is
|
|
180
|
-
// refused, and a refusal at that door writes nothing: the heir is
|
|
181
|
-
// untouched and still speaks. Then she knocks as the heir, as she would
|
|
182
|
-
// have. One extra round trip in the one case where a reply was lost, and
|
|
183
|
-
// the relation is not stranded.
|
|
184
|
-
//
|
|
185
|
-
// No stranger gains anything by it: whoever holds the invitation could
|
|
186
|
-
// always knock as the heir, and her own key is admitted only where the
|
|
187
|
-
// door already bound it to her.
|
|
188
|
-
if (inv.heir !== undefined && !k.spoke && k.sent) {
|
|
189
|
-
const own: StandingKeys = { ward: inv.ward, heir: inv.heir, current: k.current, next: k.next, seq: k.seq };
|
|
190
|
-
const r = read(await inside.send(bind, own, method, args, wanted));
|
|
191
|
-
k.seq = own.seq;
|
|
192
|
-
k.current = own.current;
|
|
193
|
-
k.next = own.next;
|
|
194
|
-
inside.wrote();
|
|
195
|
-
if (!r.door) {
|
|
196
|
-
// Nothing the door said. Unreached and late are hers to hear now; bytes
|
|
197
|
-
// that were not Quo's leave her where she was, and the heir is tried.
|
|
198
|
-
if (!isSilence(r.answer)) return r.answer;
|
|
199
|
-
} else if (!isSilence(r.answer)) {
|
|
200
|
-
// The door spoke to her own key, so it heard the first knock: this is
|
|
201
|
-
// the answer, or the door's word for her. Its silence alone cannot be
|
|
202
|
-
// told from a refusal, so silence is followed by the knock as the heir,
|
|
203
|
-
// which the door then refuses if it had heard, and she hears silence.
|
|
204
|
-
k.spoke = true;
|
|
205
|
-
if (answered(r)) bind.answered[name] = true;
|
|
206
|
-
return r.answer;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
49
|
+
const answerOf = (read: Read): Answer => {
|
|
50
|
+
if ('nothing' in read) return word('unreached');
|
|
51
|
+
if ('silence' in read) return silence;
|
|
52
|
+
if ('quo' in read) return word(read.quo);
|
|
53
|
+
return readValue(read.object, DEPTH) ?? silence;
|
|
54
|
+
};
|
|
209
55
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
56
|
+
export class WardStance implements Stance {
|
|
57
|
+
readonly key: string;
|
|
58
|
+
readonly occupants: Occupants;
|
|
59
|
+
readonly standings: Standings;
|
|
60
|
+
readonly #inside: Inside;
|
|
61
|
+
readonly #row: BeingRow;
|
|
62
|
+
readonly #lanes = new Lanes();
|
|
63
|
+
readonly #cells = new Cells(() => this.#told());
|
|
64
|
+
|
|
65
|
+
constructor(inside: Inside, key: string, row: BeingRow) {
|
|
66
|
+
this.key = key;
|
|
67
|
+
this.#inside = inside;
|
|
68
|
+
this.#row = row;
|
|
69
|
+
this.occupants = {
|
|
70
|
+
invite: (id, notes) => this.#invite(id, notes),
|
|
71
|
+
notes: (id) => (Object.hasOwn(row.occupants, id) ? row.occupants[id]!.notes : undefined),
|
|
72
|
+
ids: () => Object.keys(row.occupants),
|
|
73
|
+
remove: (id) => this.#removeOccupant(id),
|
|
74
|
+
};
|
|
75
|
+
this.standings = {
|
|
76
|
+
take: (id, invitation) => this.#take(id, invitation),
|
|
77
|
+
get: (id) => this.#standing(id),
|
|
78
|
+
ids: () => Object.keys(row.standings),
|
|
79
|
+
remove: (id) => this.#removeStanding(id),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get cells(): JsonObject {
|
|
84
|
+
return this.#cells.guard(this.#row.cells);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get #live(): boolean {
|
|
88
|
+
return this.#inside.live(this.key, this.#row);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
boot(className: string, key: string, id?: string): Promise<string | null> {
|
|
92
|
+
return this.#live ? this.#inside.boot(this.key, className, key, id) : Promise.resolve(null);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async lend(contract: { readonly name: string }, id: string): Promise<string | null> {
|
|
96
|
+
if (!this.#free(id, this.#row.standings)) return null;
|
|
97
|
+
const invitation = await this.#inside.offer(contract.name);
|
|
98
|
+
if (invitation === null) return null;
|
|
99
|
+
const taken = await this.#take(id, invitation);
|
|
100
|
+
if (taken === null) await this.#inside.retract(contract.name, invitation.heir);
|
|
101
|
+
return taken;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#free(id: string, of: Record<string, unknown>): boolean {
|
|
105
|
+
return this.#live && typeof id === 'string' && !RESERVED.has(id) && !Object.hasOwn(of, id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async #invite(id: string, notes: JsonObject = {}): Promise<Invitation | null> {
|
|
109
|
+
const kept = writeValue(notes, DEPTH);
|
|
110
|
+
if (!this.#free(id, this.#row.occupants) || kept === undefined) return null;
|
|
111
|
+
const { heir, invitation } = await this.#inside.door.invite();
|
|
112
|
+
if (!this.#free(id, this.#row.occupants)) {
|
|
113
|
+
this.#inside.door.remove(heir);
|
|
114
|
+
return null;
|
|
222
115
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
drop(bind.standings, id);
|
|
294
|
-
inside.wrote();
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
const standingAt = (id: string): Standing => ({
|
|
299
|
-
id,
|
|
300
|
-
// The keys are read now, not when the lane reaches this ask: an ask
|
|
301
|
-
// issued while the standing stood is answered even if she drops it in
|
|
302
|
-
// the next line. Waiting for the lane is not a reason to lose it.
|
|
303
|
-
ask: (method?: string, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> => {
|
|
304
|
-
if (gone()) return Promise.resolve(word('dropped') as Answer);
|
|
305
|
-
if (!named(method)) return Promise.resolve(unreached() as Answer);
|
|
306
|
-
const keys = at(bind.standings, id);
|
|
307
|
-
if (!keys) return Promise.resolve(word('dropped') as Answer); // S2. she dropped it between one line and the next
|
|
308
|
-
const rec = at(cells.standings, id);
|
|
309
|
-
return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted, live) : Promise.resolve(word('late')))));
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// The record an ask writes into is the one it was issued on. A standing
|
|
314
|
-
// dropped and re-taken under the same id while the ask was in flight is
|
|
315
|
-
// another relation, and the blueprint that came back is not hers.
|
|
316
|
-
const asking = async (id: string, keys: StandingKeys, rec: Cells['standings'][string] | undefined, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, live: () => boolean = () => true): Promise<Answer> => {
|
|
317
|
-
const out = await send(keys, method, args, wanted, rec, live);
|
|
318
|
-
if (method === undefined && !isSilence(out) && !isWord(out)) {
|
|
319
|
-
// A blueprint her cells cannot keep, nested past the bound or carrying
|
|
320
|
-
// a key no harbor writes, is not Quo's bytes: silence, and nothing written.
|
|
321
|
-
if (cellFault(out, 'blueprint') !== null) return silence;
|
|
322
|
-
if (rec && live() && at(cells.standings, id) === rec) {
|
|
323
|
-
// Read as a blueprint before it is kept as one. A far describe is
|
|
324
|
-
// somebody else's code and may answer anything; what is not a
|
|
325
|
-
// blueprint is not written as one, and she holds no blueprint for
|
|
326
|
-
// that relation rather than one a side would walk and break on. The
|
|
327
|
-
// answer itself is hers either way: it is returned below, unread.
|
|
328
|
-
rec.blueprint = isBlueprint(out) ? out : null;
|
|
329
|
-
rec.digest = rec.seen = await digest(out);
|
|
330
|
-
}
|
|
116
|
+
this.#row.occupants[id] = { heir, notes: JSON.parse(kept) as JsonObject };
|
|
117
|
+
this.#inside.partition.head.bind[heir] = { being: this.key, id };
|
|
118
|
+
this.#told();
|
|
119
|
+
this.#inside.partition.toldHead();
|
|
120
|
+
return invitation;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#removeOccupant(id: string): boolean {
|
|
124
|
+
if (!this.#live || !Object.hasOwn(this.#row.occupants, id)) return false;
|
|
125
|
+
const { heir } = this.#row.occupants[id]!;
|
|
126
|
+
this.#inside.door.remove(heir);
|
|
127
|
+
delete this.#row.occupants[id];
|
|
128
|
+
delete this.#inside.partition.head.bind[heir];
|
|
129
|
+
this.#told();
|
|
130
|
+
this.#inside.partition.toldHead();
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async #take(id: string, value: Invitation): Promise<string | null> {
|
|
135
|
+
const invitation = readInvitation(value);
|
|
136
|
+
if (!invitation || !this.#free(id, this.#row.standings)) return null;
|
|
137
|
+
const wire = new Wire(invitation, freshStanding(), this.#inside.draw);
|
|
138
|
+
const read = await this.#lanes.run(`take:${id}`, async () => {
|
|
139
|
+
// A knock that brought nothing back may have bound: the standing asks
|
|
140
|
+
// under its own key, then knocks again as the same bytes.
|
|
141
|
+
let out = await this.#send(wire);
|
|
142
|
+
for (let tries = 0; tries < TAKES && ('nothing' in out || 'silence' in out); tries += 1) out = await this.#send(wire);
|
|
143
|
+
return out;
|
|
144
|
+
});
|
|
145
|
+
if (!('object' in read) || !this.#free(id, this.#row.standings)) return null;
|
|
146
|
+
this.#row.standings[id] = { invitation, state: wire.state, seen: read.seen };
|
|
147
|
+
this.#told();
|
|
148
|
+
return id;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#removeStanding(id: string): boolean {
|
|
152
|
+
if (!this.#live || !Object.hasOwn(this.#row.standings, id)) return false;
|
|
153
|
+
delete this.#row.standings[id];
|
|
154
|
+
this.#told();
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#standing(id: string): Standing | undefined {
|
|
159
|
+
if (!Object.hasOwn(this.#row.standings, id)) return undefined;
|
|
160
|
+
return { id, ask: (method, args, wanted) => this.#ask(id, method, args, wanted) };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async #ask(id: string, method: unknown, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> {
|
|
164
|
+
const text = writeValue(args, DEPTH + 1);
|
|
165
|
+
if ((method !== undefined && typeof method !== 'string') || text === undefined || Array.isArray(args)) return word('unreached');
|
|
166
|
+
const bell = new Bell(this.#inside.clock, allow(wanted));
|
|
167
|
+
try {
|
|
168
|
+
const out = await bell.race(
|
|
169
|
+
this.#lanes.run(`ask:${id}`, async (): Promise<Answer> => {
|
|
170
|
+
const row = Object.hasOwn(this.#row.standings, id) ? this.#row.standings[id] : undefined;
|
|
171
|
+
if (!this.#live || row === undefined) return word('dropped');
|
|
172
|
+
if (bell.rang) return word('late');
|
|
173
|
+
const wire = new Wire(row.invitation, row.state, this.#inside.draw);
|
|
174
|
+
const read = await bell.race(this.#send(wire, method as string | undefined, text));
|
|
175
|
+
row.state = wire.state;
|
|
176
|
+
if (read === LATE) return word('late');
|
|
177
|
+
if ('object' in read) row.seen = read.seen;
|
|
178
|
+
this.#told();
|
|
179
|
+
await this.#inside.partition.kept();
|
|
180
|
+
return answerOf(read);
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
return out === LATE ? word('late') : out;
|
|
184
|
+
} finally {
|
|
185
|
+
bell.cancel();
|
|
331
186
|
}
|
|
332
|
-
|
|
333
|
-
};
|
|
187
|
+
}
|
|
334
188
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}) as unknown as Standings;
|
|
189
|
+
async #send(wire: Wire, method?: string, args?: string): Promise<Read> {
|
|
190
|
+
const sent = await wire.ask(method, args);
|
|
191
|
+
if (!sent) return { nothing: true };
|
|
192
|
+
return wire.read(sent, await this.#inside.send(wire.invitation.ward, sent.bytes));
|
|
193
|
+
}
|
|
341
194
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
// already booted, a class the harbor does not hold, and a throw at birth
|
|
346
|
-
// leaves the partition as it was. Reaching into another is still the
|
|
347
|
-
// owner's alone: what a maker may have is a relation to what she made,
|
|
348
|
-
// by naming an id, and it is made the way every relation is. The being
|
|
349
|
-
// made mints an occupant for her maker under the maker's own key, so she
|
|
350
|
-
// knows who made her by that name and by nothing else; her maker knocks,
|
|
351
|
-
// takes it under the id she gave, and holds one standing like any other.
|
|
352
|
-
// Nothing half-lives: a relation that could not be made unmakes the
|
|
353
|
-
// being, who is a moment old and known to nobody.
|
|
354
|
-
boot: async (className: string, made: string, id?: string): Promise<string | null> => {
|
|
355
|
-
if (gone()) return null;
|
|
356
|
-
if (id !== undefined && (reserved(id) || at(cells.standings, id) || at(cells.occupants, id))) return null;
|
|
357
|
-
const born = inside.instantiate(className, made);
|
|
358
|
-
if (born === null || id === undefined) return born;
|
|
359
|
-
const inv = await inside.relate(born, key);
|
|
360
|
-
if (inv) {
|
|
361
|
-
const out = await calls.knock(inv);
|
|
362
|
-
if (!isSilence(out) && !isWord(out) && (await calls.take(id, inv)) === id) return born;
|
|
363
|
-
}
|
|
364
|
-
inside.unmake(born);
|
|
365
|
-
return null;
|
|
366
|
-
},
|
|
367
|
-
occupants: {
|
|
368
|
-
// rotation one: the ward mints the occupant's first key and gives the secret away.
|
|
369
|
-
// it keeps the pk beside the id and nothing else. the invitation IS the key.
|
|
370
|
-
invite: async (id: string, notes?: JsonObject): Promise<Invitation | null> => {
|
|
371
|
-
if (gone() || reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null;
|
|
372
|
-
// Notes are values, like everything else the partition keeps, and a
|
|
373
|
-
// spread copies one level: an object inside them would stay the
|
|
374
|
-
// caller's, a handle into her cells that writes past the guard and
|
|
375
|
-
// never says `wrote`, so what a restart brought back would not be
|
|
376
|
-
// what she read. Held to the rule and copied whole, or no invitation.
|
|
377
|
-
if (notes !== undefined && cellFault(notes, 'notes') !== null) return null;
|
|
378
|
-
// The key first, and nothing written until it exists: a record put
|
|
379
|
-
// before the mint would be a record a remove in the meantime drops
|
|
380
|
-
// with no heir to close, and the heir opened after it would name an
|
|
381
|
-
// id nobody holds, for a later invite under that id to resurrect.
|
|
382
|
-
const k = await inside.mintKey(bind);
|
|
383
|
-
if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null; // taken while the key was minted
|
|
384
|
-
// The notes are the terms the inviter minted under, hers to read on her
|
|
385
|
-
// gate. She may write more later; nobody outside ever writes them.
|
|
386
|
-
put(cells.occupants, id, { id, notes: notes ? (JSON.parse(JSON.stringify(notes)) as JsonObject) : {} });
|
|
387
|
-
inside.openHeir(k.pk, key, id);
|
|
388
|
-
put(bind.occupants, id, k.pk);
|
|
389
|
-
inside.wrote();
|
|
390
|
-
return { ward: inside.pk, heir: k.pk, secret: k.seed };
|
|
391
|
-
},
|
|
392
|
-
remove: (id: string): void => {
|
|
393
|
-
if (gone()) return;
|
|
394
|
-
drop(cells.occupants, id);
|
|
395
|
-
const heir = at(bind.occupants, id);
|
|
396
|
-
drop(bind.occupants, id);
|
|
397
|
-
if (heir) inside.closeHeir(heir); // the heir dies with the id
|
|
398
|
-
inside.wrote();
|
|
399
|
-
},
|
|
400
|
-
},
|
|
401
|
-
standings,
|
|
402
|
-
// A standing at one of the things this device can do. The same three
|
|
403
|
-
// moves her boot makes, with the harbor's root doing the inviting instead
|
|
404
|
-
// of a being of this ward: ask the ground for the name, knock with what
|
|
405
|
-
// comes back, take it under the id she gave. She is handed the id.
|
|
406
|
-
//
|
|
407
|
-
// The invitation never reaches her, and that is the difference between
|
|
408
|
-
// this relation and every other she holds. Her own invitations are hers
|
|
409
|
-
// to give away, because giving one away is giving away her own relation.
|
|
410
|
-
// This one is the device's, minted for this ward alone, and a value she
|
|
411
|
-
// could copy is a capability she could hand to anyone. A being who wants
|
|
412
|
-
// to lend her device access to another does it in the open, by an ask of
|
|
413
|
-
// her own that forwards to this standing, where her gate reads who is
|
|
414
|
-
// asking and she can stop.
|
|
415
|
-
lend: async (name: string, id: string): Promise<string | null> => {
|
|
416
|
-
if (gone() || typeof name !== 'string' || typeof id !== 'string') return null;
|
|
417
|
-
if (reserved(id) || at(cells.standings, id) || at(cells.occupants, id)) return null;
|
|
418
|
-
// Her half runs inside the harbor's call, so a knock that was refused
|
|
419
|
-
// and a take that lost the id both end with the harbor removing what it
|
|
420
|
-
// minted. Nothing half-lives here either.
|
|
421
|
-
const took = await inside.lend(name, async (inv) => {
|
|
422
|
-
// Read again: the ground was awaited, and a line of hers in between
|
|
423
|
-
// may have taken the id. Nothing is written for a relation she cannot
|
|
424
|
-
// hold, and the invitation goes back unspent.
|
|
425
|
-
if (gone() || reserved(id) || at(cells.standings, id) || at(cells.occupants, id)) return false;
|
|
426
|
-
const out = await calls.knock(inv);
|
|
427
|
-
if (isSilence(out) || isWord(out)) return false;
|
|
428
|
-
return (await calls.take(id, inv)) === id;
|
|
429
|
-
});
|
|
430
|
-
return took ? id : null;
|
|
431
|
-
},
|
|
432
|
-
};
|
|
195
|
+
#told(): void {
|
|
196
|
+
this.#inside.partition.told(this.key);
|
|
197
|
+
}
|
|
433
198
|
}
|