@quo-systems/quo 0.1.1 → 0.2.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/SPEC.md +259 -129
- package/dist/being/index.d.ts +1 -1
- package/dist/being/index.js +1 -1
- package/dist/being/silence.d.ts +6 -1
- package/dist/being/silence.js +25 -7
- package/dist/being/types.d.ts +8 -4
- package/dist/being/types.js +1 -1
- package/dist/conformance/beings.d.ts +1 -1
- package/dist/conformance/beings.js +5 -5
- package/dist/conformance/estate.js +23 -34
- package/dist/conformance/index.js +14 -12
- package/dist/harbor/core.d.ts +7 -1
- package/dist/harbor/core.js +25 -2
- package/dist/harbor/dial.js +7 -1
- package/dist/harbor/memory.d.ts +5 -1
- package/dist/harbor/memory.js +7 -2
- package/dist/ward/door.d.ts +6 -2
- package/dist/ward/door.js +55 -35
- package/dist/ward/ground.d.ts +4 -1
- package/dist/ward/heirs.d.ts +4 -2
- package/dist/ward/heirs.js +20 -3
- package/dist/ward/owner.d.ts +5 -3
- package/dist/ward/owner.js +55 -16
- package/dist/ward/partition.d.ts +6 -0
- package/dist/ward/partition.js +2 -0
- package/dist/ward/seal.d.ts +3 -1
- package/dist/ward/seal.js +6 -2
- package/dist/ward/stance.d.ts +2 -2
- package/dist/ward/stance.js +21 -12
- package/dist/ward/ward.js +37 -12
- package/package.json +19 -31
- package/src/being/index.ts +1 -1
- package/src/being/silence.ts +26 -8
- package/src/being/types.ts +13 -3
- package/src/conformance/beings.ts +5 -5
- package/src/conformance/estate.ts +22 -36
- package/src/conformance/index.ts +14 -12
- package/src/harbor/core.ts +24 -2
- package/src/harbor/dial.ts +7 -1
- package/src/harbor/memory.ts +6 -1
- package/src/ward/door.ts +54 -32
- package/src/ward/ground.ts +6 -2
- package/src/ward/heirs.ts +20 -5
- package/src/ward/owner.ts +54 -16
- package/src/ward/partition.ts +10 -0
- package/src/ward/seal.ts +8 -5
- package/src/ward/stance.ts +20 -14
- package/src/ward/ward.ts +37 -13
- package/vectors/framing.json +7 -1
package/dist/ward/ward.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// the first being in its own map, and every being it later instantiates gets
|
|
5
5
|
// a stance built by the same code. Mints every key, seals every ask that
|
|
6
6
|
// leaves, opens every one that arrives.
|
|
7
|
-
import { silence, isSilence, unreached,
|
|
7
|
+
import { silence, isSilence, unreached, isWord, word } from '../being/silence.js';
|
|
8
8
|
import { OWNER } from '../being/types.js';
|
|
9
|
-
import { at, open, put, emptyBind, emptyCells } from './partition.js';
|
|
9
|
+
import { at, drop, open, put, emptyBind, emptyCells } from './partition.js';
|
|
10
10
|
import { Heirs } from './heirs.js';
|
|
11
11
|
import { makeDoor } from './door.js';
|
|
12
12
|
import { buildStance } from './stance.js';
|
|
@@ -65,7 +65,7 @@ class Self {
|
|
|
65
65
|
this.ask = async (method, args = {}) => {
|
|
66
66
|
try {
|
|
67
67
|
const out = await this.answer({ id: OWNER }, method, args);
|
|
68
|
-
return isSilence(out) ||
|
|
68
|
+
return isSilence(out) || isWord(out) ? silence : out;
|
|
69
69
|
}
|
|
70
70
|
catch {
|
|
71
71
|
return silence;
|
|
@@ -83,21 +83,46 @@ class Self {
|
|
|
83
83
|
pk: this.pk,
|
|
84
84
|
doors: this.doors,
|
|
85
85
|
publicKey: () => this.p.public,
|
|
86
|
-
instantiate: (key, className
|
|
86
|
+
instantiate: (key, className) => this.#instantiate(key, className),
|
|
87
|
+
unboot: (key) => this.#unboot(key),
|
|
88
|
+
setPublic: (key) => void (this.p.public = key),
|
|
87
89
|
}, asker, method, args);
|
|
88
90
|
}
|
|
89
91
|
// ---- ward functions. on the object. no stance reaches them.
|
|
90
|
-
#instantiate(key, className
|
|
92
|
+
#instantiate(key, className) {
|
|
91
93
|
if (!key || this.doors.has(key))
|
|
92
94
|
return null;
|
|
93
95
|
const door = this.#boot(key, className);
|
|
94
96
|
if (!door)
|
|
95
97
|
return null;
|
|
96
98
|
door.cells.class = className; // so a restart finds her
|
|
97
|
-
if (isPublic)
|
|
98
|
-
this.p.public = key;
|
|
99
99
|
return door;
|
|
100
100
|
}
|
|
101
|
+
// The inverse of boot, and the only way a being leaves a ward. Her
|
|
102
|
+
// relations go first and by the same calls she would have used herself,
|
|
103
|
+
// so every occupant hears `removed` at the door from the keys it kept,
|
|
104
|
+
// rather than meeting a being who is simply not there any more. What is
|
|
105
|
+
// left after that is her cells and her bind table, and they go with her:
|
|
106
|
+
// a row naming a being no door holds would be read on the next restart
|
|
107
|
+
// and boot her again.
|
|
108
|
+
#unboot(key) {
|
|
109
|
+
const door = this.doors.get(key);
|
|
110
|
+
if (!door || key === this.pk)
|
|
111
|
+
return null;
|
|
112
|
+
const occupants = Object.keys(door.cells.occupants), standings = Object.keys(door.cells.standings);
|
|
113
|
+
for (const id of occupants)
|
|
114
|
+
door.stance.occupants.remove(id);
|
|
115
|
+
for (const id of standings)
|
|
116
|
+
door.stance.standings.remove(id);
|
|
117
|
+
// A ward whose public being is gone answers arrivals for no heir with
|
|
118
|
+
// silence, as a ward that never had one does.
|
|
119
|
+
if (this.p.public === key)
|
|
120
|
+
this.p.public = null;
|
|
121
|
+
drop(this.p.beings, key);
|
|
122
|
+
drop(this.p.bind, key);
|
|
123
|
+
this.doors.delete(key);
|
|
124
|
+
return [...occupants, ...standings];
|
|
125
|
+
}
|
|
101
126
|
// Nothing is written until there is somebody to write it for: a class the
|
|
102
127
|
// harbor does not know makes no being, and a boot that made nobody must
|
|
103
128
|
// leave the partition as it found it.
|
|
@@ -173,22 +198,22 @@ class Self {
|
|
|
173
198
|
// relation that comes back round holds a lane the answer needs, and only a
|
|
174
199
|
// bound on the wait can break that. What comes back late is not read.
|
|
175
200
|
//
|
|
176
|
-
// A wait that ran out is
|
|
201
|
+
// A wait that ran out is `late`, never unreached. Unreached promises
|
|
177
202
|
// nothing was delivered and is safe to retry; a bound that expired knows
|
|
178
203
|
// no such thing, because the far door may have heard and be working still.
|
|
179
204
|
//
|
|
180
205
|
// A harbor answers bytes or nothing. One that throws instead has answered
|
|
181
206
|
// nothing in a louder voice, and is read as nothing: no door was reached.
|
|
182
|
-
const carried = (keys.ward === this.pk ? this.door(new Uint8Array(bytes)) : this.g.carry(keys.ward, bytes)).then((b) => b, () => undefined);
|
|
207
|
+
const carried = (keys.ward === this.pk ? this.door(new Uint8Array(bytes)).then((r) => r.bytes) : this.g.carry(keys.ward, bytes)).then((b) => b, () => undefined);
|
|
183
208
|
const out = await within(budget.time, carried);
|
|
184
209
|
if (out === LATE)
|
|
185
|
-
return
|
|
210
|
+
return word('late');
|
|
186
211
|
if (out === undefined)
|
|
187
212
|
return unreached(); // nothing came back. no door was reached.
|
|
188
213
|
const reply = await openReply(out, ephemeral, wardSignPk(keys.ward));
|
|
189
214
|
if (!reply)
|
|
190
|
-
return silence; // did not open, or not from that ward
|
|
191
|
-
if (
|
|
215
|
+
return silence; // did not open, or not from that ward: not Quo's bytes
|
|
216
|
+
if ('object' in reply && keys.next !== null) {
|
|
192
217
|
keys.current = keys.next; // the far door holds `next` as announced. move to it.
|
|
193
218
|
keys.next = null;
|
|
194
219
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quo-systems/quo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
@@ -18,40 +18,28 @@
|
|
|
18
18
|
"node": ">=22.18"
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
|
21
|
-
".": {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/being/index.d.ts",
|
|
23
|
+
"default": "./dist/being/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./ward": {
|
|
26
|
+
"types": "./dist/ward/index.d.ts",
|
|
27
|
+
"default": "./dist/ward/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./harbor": {
|
|
30
|
+
"types": "./dist/harbor/index.d.ts",
|
|
31
|
+
"default": "./dist/harbor/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./conformance": {
|
|
34
|
+
"types": "./dist/conformance/index.d.ts",
|
|
35
|
+
"default": "./dist/conformance/index.js"
|
|
36
|
+
}
|
|
25
37
|
},
|
|
26
38
|
"scripts": {
|
|
27
39
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
28
|
-
"check": "npm run build && npm run typecheck && npm run lint && npm test && npm run check:estate",
|
|
29
|
-
"check:estate": "node --test \"estate/test/*.test.ts\"",
|
|
30
|
-
"pretypecheck": "npm run build",
|
|
31
|
-
"typecheck": "tsc",
|
|
32
|
-
"typecheck:watch": "tsc --watch",
|
|
33
|
-
"lint": "npm run lint:ts && npm run lint:md",
|
|
34
|
-
"lint:ts": "oxlint --type-aware",
|
|
35
|
-
"lint:md": "markdownlint-cli2",
|
|
36
|
-
"lint:fix": "oxlint --type-aware --fix && markdownlint-cli2 --fix",
|
|
37
|
-
"pretest": "npm run build",
|
|
38
40
|
"test": "node --test \"test/*.test.ts\"",
|
|
39
|
-
"check:terrain": "node --test \"test/terrain/*.test.ts\"
|
|
40
|
-
"
|
|
41
|
-
"test:ward": "node --test test/ward.test.ts",
|
|
42
|
-
"prepublishOnly": "npm run check && npm run check:terrain"
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"@types/node": "^26.4.1",
|
|
46
|
-
"bun": "^1.4.0",
|
|
47
|
-
"deno": "^2.9.6",
|
|
48
|
-
"esbuild": "^0.28.2",
|
|
49
|
-
"markdownlint-cli2": "^0.23.2",
|
|
50
|
-
"oxlint": "^1.81.0",
|
|
51
|
-
"oxlint-tsgolint": "^7.0.2001",
|
|
52
|
-
"playwright": "^1.62.1",
|
|
53
|
-
"typescript": "^7.0.2",
|
|
54
|
-
"workerd": "^1.20260904.1"
|
|
41
|
+
"check:terrain": "node --test \"test/terrain/*.test.ts\"",
|
|
42
|
+
"prepublishOnly": "cd ../.. && npm run check && npm run check:terrain"
|
|
55
43
|
},
|
|
56
44
|
"publishConfig": {
|
|
57
45
|
"access": "public"
|
package/src/being/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// @quo-systems/quo — the Being side. What a being author imports, if anything.
|
|
3
3
|
export { Being, type AskSpec } from './being.ts';
|
|
4
|
-
export { silence, isSilence, unreached, isUnreached } from './silence.ts';
|
|
4
|
+
export { silence, isSilence, unreached, isUnreached, word, isWord, wordOf, DOOR_WORDS, isDoorWord } from './silence.ts';
|
|
5
5
|
export { digest, canonical } from './digest.ts';
|
|
6
6
|
export { OWNER, PUBLIC, RESERVED_IDS } from './types.ts';
|
|
7
7
|
export type * from './types.ts';
|
package/src/being/silence.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Silence is one distinguished value. Null is an answer. Silence is no answer.
|
|
3
|
-
|
|
3
|
+
//
|
|
4
|
+
// The ward's words are the other distinguished values: what her ward tells
|
|
5
|
+
// her when no object came back and it knows why. Each is one frozen object
|
|
6
|
+
// under one symbol key, so a being cannot make one by accident, none of them
|
|
7
|
+
// crosses an edge as a value, and a being who compares against them needs
|
|
8
|
+
// no fourth shape. `unreached` is one of them.
|
|
9
|
+
import { WORD_KEY, type DoorWord, type Unreached, type Word, type WordName } from './types.ts';
|
|
4
10
|
|
|
5
11
|
export const silence: unique symbol = Symbol.for('quo.silence');
|
|
6
12
|
export const isSilence = (x: unknown): x is typeof silence => x === silence;
|
|
7
13
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// The words a door says to a key it has bound, and no other. They ride on
|
|
15
|
+
// the wire as `{ quo: word }`, so a reply is checked against this list.
|
|
16
|
+
export const DOOR_WORDS: readonly DoorWord[] = ['removed', 'absent', 'unannounced', 'repeated', 'threw'];
|
|
17
|
+
export const isDoorWord = (s: unknown): s is DoorWord => typeof s === 'string' && (DOOR_WORDS as readonly string[]).includes(s);
|
|
18
|
+
|
|
19
|
+
const WORDS = new Map<WordName, Word>();
|
|
20
|
+
export const word = <W extends WordName>(name: W): Word<W> => {
|
|
21
|
+
let w = WORDS.get(name);
|
|
22
|
+
if (!w) WORDS.set(name, (w = Object.freeze({ [WORD_KEY]: name })));
|
|
23
|
+
return w as Word<W>;
|
|
24
|
+
};
|
|
25
|
+
export const isWord = (x: unknown): x is Word => x !== null && typeof x === 'object' && typeof (x as Record<symbol, unknown>)[WORD_KEY] === 'string';
|
|
26
|
+
export const wordOf = (x: Word): WordName => x[WORD_KEY];
|
|
27
|
+
|
|
28
|
+
// Unreached: no far door was reached. Nothing is known to have been
|
|
29
|
+
// delivered, so asking again is safe. A being cannot produce it: a ward that
|
|
30
|
+
// sees a word come out of a being reads it as her having thrown.
|
|
31
|
+
export const unreached = (): Unreached => word('unreached');
|
|
32
|
+
export const isUnreached = (x: unknown): x is Unreached => isWord(x) && wordOf(x) === 'unreached';
|
package/src/being/types.ts
CHANGED
|
@@ -44,11 +44,21 @@ export type Cells = {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
export type Silence = typeof silence;
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
|
|
48
|
+
// The ward's words: what her ward says when no object came back and it knows
|
|
49
|
+
// why. A door says the first five to a key it has bound, and rides them on
|
|
50
|
+
// the wire as `{ quo: word }`; her own ward says the last four to her and
|
|
51
|
+
// they never leave the ward. Each is one frozen object under one symbol key,
|
|
52
|
+
// so no being makes one and none crosses an edge as a value.
|
|
53
|
+
export type DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
|
|
54
|
+
export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
|
|
55
|
+
export type WordName = DoorWord | WardWord;
|
|
56
|
+
export const WORD_KEY: unique symbol = Symbol.for('quo.word');
|
|
57
|
+
export type Word<W extends WordName = WordName> = { readonly [K in typeof WORD_KEY]: W };
|
|
58
|
+
export type Unreached = Word<'unreached'>;
|
|
49
59
|
|
|
50
60
|
// What comes back from an ask or a knock.
|
|
51
|
-
export type Answer = Json | Silence |
|
|
61
|
+
export type Answer = Json | Silence | Word;
|
|
52
62
|
// What a being answers.
|
|
53
63
|
export type Reply = Json | Silence;
|
|
54
64
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The fixture beings. Fixed, so that the ward is what varies. A printer, a
|
|
3
3
|
// shop, a customer on the base class, and one raw being with no base at all.
|
|
4
4
|
import { Being } from '../being/being.ts';
|
|
5
|
-
import { isSilence, isUnreached } from '../being/silence.ts';
|
|
5
|
+
import { isSilence, isUnreached, isWord } from '../being/silence.ts';
|
|
6
6
|
import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
|
|
7
7
|
|
|
8
8
|
// A printer. Invites whom she is told to, prints for her occupants. Her
|
|
@@ -46,7 +46,7 @@ export class Shop extends Being {
|
|
|
46
46
|
|
|
47
47
|
async keepPrinter(invitation: Invitation): Promise<boolean> {
|
|
48
48
|
const bp = await this.knock(invitation);
|
|
49
|
-
if (isSilence(bp) ||
|
|
49
|
+
if (isSilence(bp) || isWord(bp)) return false;
|
|
50
50
|
this.cells.printerId = await this.take('prn', invitation);
|
|
51
51
|
return true;
|
|
52
52
|
}
|
|
@@ -55,7 +55,7 @@ export class Shop extends Being {
|
|
|
55
55
|
const rec = this.occupant(asker);
|
|
56
56
|
if (rec?.notes.tier === 'gold' && invitation) {
|
|
57
57
|
const back = await this.knock(invitation as Invitation, 'hi');
|
|
58
|
-
if (!isSilence(back) && !
|
|
58
|
+
if (!isSilence(back) && !isWord(back)) await this.take(`vip-${asker.id}`, invitation as Invitation);
|
|
59
59
|
}
|
|
60
60
|
return { welcome: true };
|
|
61
61
|
}
|
|
@@ -67,7 +67,7 @@ export class Shop extends Being {
|
|
|
67
67
|
if (!printer) return { error: 'no printer' };
|
|
68
68
|
const out = await printer.ask('print', { doc: `receipt for ${item}` });
|
|
69
69
|
if (isUnreached(out)) return { error: 'printer unreachable', retry: true };
|
|
70
|
-
if (isSilence(out)) return { error: 'printer refused' };
|
|
70
|
+
if (isSilence(out) || isWord(out)) return { error: 'printer refused' };
|
|
71
71
|
const st = this.cells.standings[id!];
|
|
72
72
|
if (st.seen !== st.digest) this.occupant(asker)!.notes.printerChanged = true;
|
|
73
73
|
(this.cells.sales as JsonObject[]).push({ item: item ?? null, by: asker.id ?? null });
|
|
@@ -88,7 +88,7 @@ export class Customer extends Being {
|
|
|
88
88
|
async join(invitation: Invitation) {
|
|
89
89
|
const mine = (await this.invite('shop-back'))!;
|
|
90
90
|
const out = await this.knock(invitation, 'hello', { invitation: mine as never });
|
|
91
|
-
if (isSilence(out) ||
|
|
91
|
+
if (isSilence(out) || isWord(out)) return out; // nothing was born
|
|
92
92
|
await this.take('shop', invitation); // now, and only now
|
|
93
93
|
return out;
|
|
94
94
|
}
|
|
@@ -12,20 +12,22 @@
|
|
|
12
12
|
//
|
|
13
13
|
// So this chapter does not write the sequence. It keeps a model of the graph,
|
|
14
14
|
// plays legal moves against it from three fixed seeds, and after every move
|
|
15
|
-
// asks
|
|
15
|
+
// asks two things:
|
|
16
16
|
//
|
|
17
17
|
// the ledger is a graph every arc the wards hold closes on both ends
|
|
18
18
|
// the ledger is what it can do every arc in the model is asked, and answers
|
|
19
19
|
// exactly what being that arc means it answers
|
|
20
|
-
// the graph is not the topology the same seed, played under one ward, under
|
|
21
|
-
// a ward per being, and under two harbors,
|
|
22
|
-
// ends in the same graph
|
|
23
20
|
//
|
|
24
|
-
// The
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
21
|
+
// The second is asked under one ward, under a ward per being, and under two
|
|
22
|
+
// harbors, and that is where the promise of Quo is held: a being never learns
|
|
23
|
+
// where the other one is, so every answer, after every move, under every
|
|
24
|
+
// topology, is the answer the model wrote without knowing the topology
|
|
25
|
+
// either. Held forty times a run, it is the promise at a scale a scene
|
|
26
|
+
// cannot reach, and it is what makes a migration honest: if no answer can
|
|
27
|
+
// tell the topology apart, moving a ward between harbors cannot be felt.
|
|
28
|
+
// The model itself is the script's own bookkeeping and takes nothing from a
|
|
29
|
+
// ward, so it is never compared with itself across topologies: that would
|
|
30
|
+
// hold by construction and prove nothing.
|
|
29
31
|
//
|
|
30
32
|
// A relation is two arcs and never one edge. The occupant arc is the host's,
|
|
31
33
|
// born at invite; the standing arc is the guest's, born at take, and only
|
|
@@ -33,12 +35,13 @@
|
|
|
33
35
|
// A model that kept one symmetric edge would be a wrong model, and would
|
|
34
36
|
// agree happily with a ward that had drifted.
|
|
35
37
|
import { assert } from './assert.ts';
|
|
36
|
-
import { isSilence, isUnreached } from '../being/silence.ts';
|
|
38
|
+
import { isSilence, isUnreached, isWord, wordOf } from '../being/silence.ts';
|
|
37
39
|
import type { Invitation } from '../being/types.ts';
|
|
38
40
|
import { Member } from './beings.ts';
|
|
39
41
|
import type { Census, Handle, Runner, World } from './index.ts';
|
|
40
42
|
|
|
41
43
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
44
|
+
const said = (x: unknown, w: string): boolean => isWord(x) && wordOf(x) === w;
|
|
42
45
|
|
|
43
46
|
// One occupant arc, as the host holds it. `guest` is null until somebody
|
|
44
47
|
// knocks; `live` goes false when she is kicked. Both are the host's business
|
|
@@ -59,20 +62,6 @@ const rolls = (seed: number) => () => {
|
|
|
59
62
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
60
63
|
};
|
|
61
64
|
|
|
62
|
-
// The graph, with nothing of the world in it: no key, no ward pk, no harbor.
|
|
63
|
-
// Two runs under two topologies are the same estate when these agree.
|
|
64
|
-
const abc = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
|
|
65
|
-
const fingerprint = (g: Graph): string =>
|
|
66
|
-
JSON.stringify({
|
|
67
|
-
seats: g.seats.map((s) => JSON.stringify([s.host, s.id, s.guest, s.answered, s.live, s.taken])).sort(abc),
|
|
68
|
-
holds: g.holds.map((h) => JSON.stringify([h.owner, h.id, h.seat.host, h.seat.id, h.live])).sort(abc),
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Every run of one seed, under every topology, must land on the same graph.
|
|
72
|
-
// The topologies run in one process, so the first to finish records and the
|
|
73
|
-
// rest are held to it.
|
|
74
|
-
const landed = new Map<string, { by: string; print: string }>();
|
|
75
|
-
|
|
76
65
|
// ---- the ledger is a graph
|
|
77
66
|
|
|
78
67
|
// Held after every move. Each line is a way the bookkeeping could stop being
|
|
@@ -161,10 +150,11 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
161
150
|
assert.deepEqual(await acme.being.knock(back, 'ping'), { pong: 'acme' });
|
|
162
151
|
assert.equal(await acme.being.take('them', back), 'them');
|
|
163
152
|
|
|
164
|
-
// 5. acme kicks her. her standing is still hers to hold
|
|
165
|
-
// told
|
|
153
|
+
// 5. acme kicks her. her standing is still hers to hold, and she is not
|
|
154
|
+
// told until she asks: then the door, under the key it bound for her,
|
|
155
|
+
// says removed, and only to her.
|
|
166
156
|
acme.being.occupants.remove('partner-1');
|
|
167
|
-
assert.ok(
|
|
157
|
+
assert.ok(said(await partner.being.standings['the-client']!.ask('ping'), 'removed'));
|
|
168
158
|
assert.deepEqual(Object.keys(partner.cells.standings), ['the-client']); // she never learns
|
|
169
159
|
assert.deepEqual(await acme.being.standings.them!.ask('ping'), { pong: 'acme' }); // the other arc never moved
|
|
170
160
|
|
|
@@ -174,7 +164,7 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
174
164
|
assert.notEqual(second.heir, first.heir);
|
|
175
165
|
assert.deepEqual(await partner.being.knock(second, 'ping'), { pong: 'partner-2' });
|
|
176
166
|
assert.equal(await partner.being.take('the-client-again', second), 'the-client-again');
|
|
177
|
-
assert.ok(
|
|
167
|
+
assert.ok(said(await partner.being.standings['the-client']!.ask('ping'), 'removed'));
|
|
178
168
|
ledger(w.census(), undefined, 'the story');
|
|
179
169
|
});
|
|
180
170
|
|
|
@@ -221,6 +211,9 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
221
211
|
assert.deepEqual(back, { pong: s.id }, `${guest} knocking ${s.host}/${s.id}`);
|
|
222
212
|
s.guest = guest;
|
|
223
213
|
s.answered = true;
|
|
214
|
+
} else if (!s.live && (s.guest === null || (s.guest === guest && (hold === undefined || hold.live)))) {
|
|
215
|
+
// kicked, and she speaks under a key the door bound for this seat: she hears why
|
|
216
|
+
assert.ok(said(back, 'removed'), `${guest} knocking kicked ${s.host}/${s.id} should hear removed`);
|
|
224
217
|
} else assert.ok(isSilence(back), `${guest} knocking ${s.host}/${s.id} should be silence`);
|
|
225
218
|
return `${guest} knocks ${s.host}/${s.id}`;
|
|
226
219
|
});
|
|
@@ -304,7 +297,7 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
304
297
|
}
|
|
305
298
|
const out = await standing!.ask('ping');
|
|
306
299
|
if (h.seat.live) assert.deepEqual(out, { pong: h.seat.id }, `step ${step}: ${h.owner}/${h.id} -> ${h.seat.host}/${h.seat.id}`);
|
|
307
|
-
else assert.ok(
|
|
300
|
+
else assert.ok(said(out, 'removed'), `step ${step}: ${h.owner}/${h.id} is kicked and should hear removed`);
|
|
308
301
|
}
|
|
309
302
|
};
|
|
310
303
|
|
|
@@ -327,13 +320,6 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
327
320
|
throw new Error(`${at(step)}\n -> ${e.message}`, { cause: e });
|
|
328
321
|
});
|
|
329
322
|
}
|
|
330
|
-
|
|
331
|
-
// and the estate that came out is the estate every topology comes out with
|
|
332
|
-
const print = fingerprint(g);
|
|
333
|
-
const key = `seed ${seed}`;
|
|
334
|
-
const first = landed.get(key);
|
|
335
|
-
if (!first) landed.set(key, { by: label, print });
|
|
336
|
-
else assert.equal(print, first.print, `the estate under ${label} is not the estate under ${first.by}: a being learned the topology`);
|
|
337
323
|
});
|
|
338
324
|
}
|
|
339
325
|
}
|
package/src/conformance/index.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Every assertion here is the truth's. Nothing here knows how a ward is built.
|
|
21
21
|
import { assert } from './assert.ts';
|
|
22
|
-
import { silence, isSilence, isUnreached } from '../being/silence.ts';
|
|
22
|
+
import { silence, isSilence, isUnreached, isWord, wordOf } from '../being/silence.ts';
|
|
23
23
|
import { digest } from '../being/digest.ts';
|
|
24
24
|
import type { Asker, BeingClass, Cells, Invitation, Json, JsonObject } from '../being/types.ts';
|
|
25
25
|
import { Printer, Shop, Customer, Echo, Member } from './beings.ts';
|
|
@@ -54,6 +54,8 @@ export type World = {
|
|
|
54
54
|
export { Printer, Shop, Customer, Echo, Member };
|
|
55
55
|
|
|
56
56
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
57
|
+
// Her ward's word, as she receives it.
|
|
58
|
+
const said = (x: unknown, w: string): boolean => isWord(x) && wordOf(x) === w;
|
|
57
59
|
|
|
58
60
|
// The runner is handed in. Node passes `node:test`; a browser and an edge
|
|
59
61
|
// worker pass a shim of their own. The suite is the ward's truth, and the
|
|
@@ -100,12 +102,12 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
100
102
|
assert.equal(i1.ward, i2.ward);
|
|
101
103
|
});
|
|
102
104
|
|
|
103
|
-
t('remove before any knock: the heir is forgotten, a knock on it
|
|
105
|
+
t('remove before any knock: the heir is forgotten, and a knock on it by whoever holds it hears removed', async () => {
|
|
104
106
|
const { w, shop, alice } = await world();
|
|
105
107
|
const inv = await shop.being.invite();
|
|
106
108
|
shop.being.occupants.remove('cust1');
|
|
107
109
|
assert.equal(w.heir(inv), undefined);
|
|
108
|
-
assert.ok(
|
|
110
|
+
assert.ok(said(await alice.being.join(inv), 'removed'));
|
|
109
111
|
assert.deepEqual(alice.cells.standings, {});
|
|
110
112
|
});
|
|
111
113
|
|
|
@@ -189,16 +191,16 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
189
191
|
assert.deepEqual(await alice.being.join(inv), { welcome: true }); // the same invitation still works
|
|
190
192
|
const inv2 = await shop.being.invite();
|
|
191
193
|
shop.being.occupants.remove('cust2'); // she changed her mind before the knock
|
|
192
|
-
assert.ok(
|
|
194
|
+
assert.ok(said(await alice.being.join(inv2), 'removed'));
|
|
193
195
|
assert.deepEqual(Object.keys(alice.cells.standings), ['shop']);
|
|
194
196
|
});
|
|
195
197
|
|
|
196
|
-
t('take without a knock, or after a
|
|
198
|
+
t('take without a knock, or after a knock that was not answered, births nothing', async () => {
|
|
197
199
|
const { shop, alice } = await world();
|
|
198
200
|
const inv = await shop.being.invite();
|
|
199
201
|
assert.equal(await alice.being.take('shop', inv), null);
|
|
200
202
|
shop.being.occupants.remove('cust1');
|
|
201
|
-
assert.ok(
|
|
203
|
+
assert.ok(said(await alice.being.knock(inv, 'hello'), 'removed'));
|
|
202
204
|
assert.equal(await alice.being.take('shop', inv), null);
|
|
203
205
|
assert.deepEqual(alice.cells.standings, {});
|
|
204
206
|
});
|
|
@@ -374,16 +376,16 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
374
376
|
assert.deepEqual(await alice.being.buy('hat'), { ok: true, receipt: 'receipt for hat' });
|
|
375
377
|
});
|
|
376
378
|
|
|
377
|
-
t('a removed occupant
|
|
379
|
+
t('a removed occupant hears removed under the key the door bound, her heir is forgotten; a dropped standing is gone', async () => {
|
|
378
380
|
const { w, alice, shop } = await world();
|
|
379
381
|
const inv = await shop.being.invite();
|
|
380
382
|
await alice.being.join(inv);
|
|
381
383
|
shop.being.occupants.remove('cust1');
|
|
382
384
|
assert.equal(w.heir(inv), undefined);
|
|
383
|
-
assert.ok(
|
|
385
|
+
assert.ok(said(await alice.being.buy('hat'), 'removed'));
|
|
384
386
|
alice.being.standings.remove('shop');
|
|
385
387
|
assert.equal(alice.being.standings.shop, undefined);
|
|
386
|
-
assert.ok(isSilence(await alice.being.knock(inv, 'hello'))); //
|
|
388
|
+
assert.ok(isSilence(await alice.being.knock(inv, 'hello'))); // her bound key went with the standing: a re-knock is as the heir, which is spent, a stranger
|
|
387
389
|
const again = await shop.being.invite(); // cust2. a new heir
|
|
388
390
|
assert.notEqual(again.heir, inv.heir);
|
|
389
391
|
assert.deepEqual(await alice.being.knock(again, 'hello'), { welcome: true });
|
|
@@ -398,7 +400,7 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
398
400
|
assert.equal((shop.cells.sales as unknown[]).length, 1);
|
|
399
401
|
});
|
|
400
402
|
|
|
401
|
-
t('a being that throws is
|
|
403
|
+
t('a being that throws is the word threw to whom the door has bound; the ward stays up', async () => {
|
|
402
404
|
const w = await make();
|
|
403
405
|
class Bomb {
|
|
404
406
|
s: unknown;
|
|
@@ -413,7 +415,7 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
413
415
|
const x = await w.boot<Bomb>('X', Bomb);
|
|
414
416
|
const a = await w.boot<Customer>('A', Customer);
|
|
415
417
|
const inv = (await (x.being.s as Customer['stance']).occupants.invite('a'))!;
|
|
416
|
-
assert.ok(
|
|
418
|
+
assert.ok(said(await a.being.knock(inv, 'any'), 'threw'));
|
|
417
419
|
assert.deepEqual(await a.being.knock(inv), { asks: [], notes: {} });
|
|
418
420
|
});
|
|
419
421
|
|
|
@@ -473,7 +475,7 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
473
475
|
assert.deepEqual(pub.cells.occupants, {}); // she never invited anyone
|
|
474
476
|
if (!w.oneWard) assert.ok(isSilence(await a.being.knock(at(priv), 'time'))); // a ward with no public being: silence
|
|
475
477
|
assert.ok(isUnreached(await a.being.knock({ ward: 'a'.repeat(128) }, 'time'))); // no door anywhere: unreached
|
|
476
|
-
assert.ok(
|
|
478
|
+
assert.ok(said(await a.being.knock({ ward: 'not a pk' }, 'time'), 'invitation')); // not even an address
|
|
477
479
|
assert.equal(await a.being.take('clock', at(pub)), 'clock');
|
|
478
480
|
assert.deepEqual(await a.being.standings.clock!.ask('time'), { time: 12, got: {} });
|
|
479
481
|
assert.equal(a.cells.standings.clock.seen, await digest((await a.being.standings.clock!.ask()) as Json));
|
package/src/harbor/core.ts
CHANGED
|
@@ -45,6 +45,7 @@ export class Harbor {
|
|
|
45
45
|
readonly doors = new Map<string, WardPointers['door']>(); // ward pk -> door
|
|
46
46
|
readonly reaches = new Map<string, Bound>(); // the directory: foreign ward pk -> reach
|
|
47
47
|
readonly down = new Set<string>(); // pks this harbor will not reach right now: weather, for a test
|
|
48
|
+
readonly refused = new Map<string, number>(); // own ward pk -> arrivals its door would not admit. what a terrain does with it is its own
|
|
48
49
|
readonly classes: Record<string, BeingClass> = {}; // classes handed in-process, beside the loader's
|
|
49
50
|
// The dialers' sockets, in the order they were dialed: where a pk nobody
|
|
50
51
|
// here knows is sent, because the listener a harbor dialed is a rendezvous
|
|
@@ -54,6 +55,17 @@ export class Harbor {
|
|
|
54
55
|
// carries it: with a single slot, a harbor holding two lines answered
|
|
55
56
|
// unreached for a pk the other line could have reached.
|
|
56
57
|
readonly fallbacks: Reach[] = [];
|
|
58
|
+
// How a dialer says what this harbor holds now. An announce is a claim and
|
|
59
|
+
// a claim is worth nothing until the door behind it answers a probe, so
|
|
60
|
+
// saying it again costs a proof and buys nothing to an impostor. What is
|
|
61
|
+
// not free is never saying it again: a ward booted, adopted or dropped
|
|
62
|
+
// after a line opened would be announced only by the next line, and a line
|
|
63
|
+
// that does not drop never opens again, so it would be unreachable through
|
|
64
|
+
// that listener for as long as the socket stayed healthy.
|
|
65
|
+
readonly announcers = new Set<() => void>();
|
|
66
|
+
#announce(): void {
|
|
67
|
+
for (const say of this.announcers) say();
|
|
68
|
+
}
|
|
57
69
|
|
|
58
70
|
constructor(store: Store, loader: Loader = async () => ({})) {
|
|
59
71
|
this.store = store;
|
|
@@ -71,10 +83,18 @@ export class Harbor {
|
|
|
71
83
|
|
|
72
84
|
// ---- the directory
|
|
73
85
|
|
|
86
|
+
// Bytes to one of this harbor's own doors, and the one bit the door says
|
|
87
|
+
// beside its reply, counted.
|
|
88
|
+
async #own(pk: string, door: WardPointers['door'], bytes: Uint8Array): Promise<Uint8Array> {
|
|
89
|
+
const r = await door(bytes);
|
|
90
|
+
if (!r.heard) this.refused.set(pk, (this.refused.get(pk) ?? 0) + 1);
|
|
91
|
+
return new Uint8Array(r.bytes);
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
async carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
|
|
75
95
|
if (this.down.has(pk)) return undefined;
|
|
76
96
|
const door = this.doors.get(pk);
|
|
77
|
-
if (door) return
|
|
97
|
+
if (door) return this.#own(pk, door, bytes);
|
|
78
98
|
const bound = this.reaches.get(pk);
|
|
79
99
|
if (bound) return bound.reach.carry(pk, bytes);
|
|
80
100
|
for (const reach of this.fallbacks) {
|
|
@@ -87,7 +107,7 @@ export class Harbor {
|
|
|
87
107
|
async deliver(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
|
|
88
108
|
if (this.down.has(pk)) return undefined;
|
|
89
109
|
const door = this.doors.get(pk);
|
|
90
|
-
if (door) return
|
|
110
|
+
if (door) return this.#own(pk, door, bytes);
|
|
91
111
|
const bound = this.reaches.get(pk);
|
|
92
112
|
return bound?.held ? bound.reach.carry(pk, bytes) : undefined;
|
|
93
113
|
}
|
|
@@ -163,6 +183,7 @@ export class Harbor {
|
|
|
163
183
|
await h.save();
|
|
164
184
|
this.wards.delete(name);
|
|
165
185
|
this.doors.delete(h.pk);
|
|
186
|
+
this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
|
|
166
187
|
}
|
|
167
188
|
return this.store.take(name);
|
|
168
189
|
}
|
|
@@ -214,6 +235,7 @@ export class Harbor {
|
|
|
214
235
|
this.wards.set(name, hosted);
|
|
215
236
|
this.doors.set(hosted.pk, door);
|
|
216
237
|
await save();
|
|
238
|
+
this.#announce(); // reachable through every line this harbor already holds, not only the next one
|
|
217
239
|
return hosted;
|
|
218
240
|
}
|
|
219
241
|
}
|
package/src/harbor/dial.ts
CHANGED
|
@@ -33,6 +33,7 @@ export function dial(harbor: Harbor, url: string): Dialer {
|
|
|
33
33
|
void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
|
|
34
34
|
},
|
|
35
35
|
(why) => {
|
|
36
|
+
harbor.announcers.delete(say);
|
|
36
37
|
harbor.unbind(s);
|
|
37
38
|
if (d.socket === s) d.socket = null;
|
|
38
39
|
const at = harbor.fallbacks.indexOf(s);
|
|
@@ -46,9 +47,14 @@ export function dial(harbor: Harbor, url: string): Dialer {
|
|
|
46
47
|
},
|
|
47
48
|
);
|
|
48
49
|
harbor.fallbacks.push(s); // from the moment it is dialed: an ask made before the handshake waits on the socket, and is not unreached
|
|
50
|
+
// What this harbor holds, said when the line opens and again whenever it
|
|
51
|
+
// changes. A ward booted or adopted after the handshake is announced on
|
|
52
|
+
// the line already in hand, and a dropped one stops being claimed at once.
|
|
53
|
+
const say = () => s.announce([...harbor.doors.keys()]);
|
|
49
54
|
line.addEventListener('open', () => {
|
|
50
55
|
d.socket = s;
|
|
51
|
-
|
|
56
|
+
harbor.announcers.add(say);
|
|
57
|
+
say();
|
|
52
58
|
});
|
|
53
59
|
};
|
|
54
60
|
d.close = () => {
|
package/src/harbor/memory.ts
CHANGED
|
@@ -15,6 +15,7 @@ export class MemoryHarbor {
|
|
|
15
15
|
readonly doors = new Map<string, WardPointers['door']>(); // ward pk -> door
|
|
16
16
|
readonly down = new Set<string>(); // ward pks this harbor cannot reach right now
|
|
17
17
|
readonly wire: string[] = []; // every byte string that ever crossed, as hex. for inspection
|
|
18
|
+
readonly refused = new Map<string, number>(); // ward pk -> arrivals its door would not admit: strangers, counted
|
|
18
19
|
readonly peers = new Set<MemoryHarbor>();
|
|
19
20
|
cut = false;
|
|
20
21
|
readonly partitions = new Map<string, Record<string, unknown>>(); // seed -> memory. the harbor keeps it and reads nothing
|
|
@@ -25,7 +26,11 @@ export class MemoryHarbor {
|
|
|
25
26
|
async route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
|
|
26
27
|
if (this.down.has(farPk)) return undefined;
|
|
27
28
|
const door = this.doors.get(farPk);
|
|
28
|
-
if (door)
|
|
29
|
+
if (door) {
|
|
30
|
+
const r = await door(bytes);
|
|
31
|
+
if (!r.heard) this.refused.set(farPk, (this.refused.get(farPk) ?? 0) + 1);
|
|
32
|
+
return r.bytes;
|
|
33
|
+
}
|
|
29
34
|
const peer = [...this.peers].find((h) => h.doors.has(farPk) && !h.down.has(farPk));
|
|
30
35
|
if (!peer || this.cut) return undefined; // nothing. the harbor has no word silence
|
|
31
36
|
return peer.route(farPk, bytes);
|