@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/being/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Being, type AskSpec } from './being.ts';
|
|
2
|
-
export { silence, isSilence, unreached, isUnreached } from './silence.ts';
|
|
2
|
+
export { silence, isSilence, unreached, isUnreached, word, isWord, wordOf, DOOR_WORDS, isDoorWord } from './silence.ts';
|
|
3
3
|
export { digest, canonical } from './digest.ts';
|
|
4
4
|
export { OWNER, PUBLIC, RESERVED_IDS } from './types.ts';
|
|
5
5
|
export type * from './types.ts';
|
package/dist/being/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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 } from './being.js';
|
|
4
|
-
export { silence, isSilence, unreached, isUnreached } from './silence.js';
|
|
4
|
+
export { silence, isSilence, unreached, isUnreached, word, isWord, wordOf, DOOR_WORDS, isDoorWord } from './silence.js';
|
|
5
5
|
export { digest, canonical } from './digest.js';
|
|
6
6
|
export { OWNER, PUBLIC, RESERVED_IDS } from './types.js';
|
package/dist/being/silence.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { type Unreached } from './types.ts';
|
|
1
|
+
import { type DoorWord, type Unreached, type Word, type WordName } from './types.ts';
|
|
2
2
|
export declare const silence: unique symbol;
|
|
3
3
|
export declare const isSilence: (x: unknown) => x is typeof silence;
|
|
4
|
+
export declare const DOOR_WORDS: readonly DoorWord[];
|
|
5
|
+
export declare const isDoorWord: (s: unknown) => s is DoorWord;
|
|
6
|
+
export declare const word: <W extends WordName>(name: W) => Word<W>;
|
|
7
|
+
export declare const isWord: (x: unknown) => x is Word;
|
|
8
|
+
export declare const wordOf: (x: Word) => WordName;
|
|
4
9
|
export declare const unreached: () => Unreached;
|
|
5
10
|
export declare const isUnreached: (x: unknown) => x is Unreached;
|
package/dist/being/silence.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
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 } from './types.js';
|
|
4
10
|
export const silence = Symbol.for('quo.silence');
|
|
5
11
|
export const isSilence = (x) => x === silence;
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
export const
|
|
12
|
+
// The words a door says to a key it has bound, and no other. They ride on
|
|
13
|
+
// the wire as `{ quo: word }`, so a reply is checked against this list.
|
|
14
|
+
export const DOOR_WORDS = ['removed', 'absent', 'unannounced', 'repeated', 'threw'];
|
|
15
|
+
export const isDoorWord = (s) => typeof s === 'string' && DOOR_WORDS.includes(s);
|
|
16
|
+
const WORDS = new Map();
|
|
17
|
+
export const word = (name) => {
|
|
18
|
+
let w = WORDS.get(name);
|
|
19
|
+
if (!w)
|
|
20
|
+
WORDS.set(name, (w = Object.freeze({ [WORD_KEY]: name })));
|
|
21
|
+
return w;
|
|
22
|
+
};
|
|
23
|
+
export const isWord = (x) => x !== null && typeof x === 'object' && typeof x[WORD_KEY] === 'string';
|
|
24
|
+
export const wordOf = (x) => x[WORD_KEY];
|
|
25
|
+
// Unreached: no far door was reached. Nothing is known to have been
|
|
26
|
+
// delivered, so asking again is safe. A being cannot produce it: a ward that
|
|
27
|
+
// sees a word come out of a being reads it as her having thrown.
|
|
28
|
+
export const unreached = () => word('unreached');
|
|
29
|
+
export const isUnreached = (x) => isWord(x) && wordOf(x) === 'unreached';
|
package/dist/being/types.d.ts
CHANGED
|
@@ -45,11 +45,15 @@ export type Cells = {
|
|
|
45
45
|
[hers: string]: Json;
|
|
46
46
|
};
|
|
47
47
|
export type Silence = typeof silence;
|
|
48
|
-
export type
|
|
49
|
-
|
|
48
|
+
export type DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
|
|
49
|
+
export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
|
|
50
|
+
export type WordName = DoorWord | WardWord;
|
|
51
|
+
export declare const WORD_KEY: unique symbol;
|
|
52
|
+
export type Word<W extends WordName = WordName> = {
|
|
53
|
+
readonly [K in typeof WORD_KEY]: W;
|
|
50
54
|
};
|
|
51
|
-
export
|
|
52
|
-
export type Answer = Json | Silence |
|
|
55
|
+
export type Unreached = Word<'unreached'>;
|
|
56
|
+
export type Answer = Json | Silence | Word;
|
|
53
57
|
export type Reply = Json | Silence;
|
|
54
58
|
export type Wanted = {
|
|
55
59
|
time?: number;
|
package/dist/being/types.js
CHANGED
|
@@ -85,7 +85,7 @@ export declare class Customer extends Being {
|
|
|
85
85
|
};
|
|
86
86
|
};
|
|
87
87
|
};
|
|
88
|
-
join(invitation: Invitation): Promise<typeof import("../being/silence.ts").silence | import("../being/types.ts").
|
|
88
|
+
join(invitation: Invitation): Promise<typeof import("../being/silence.ts").silence | import("../being/types.ts").Word | import("../being/types.ts").Json>;
|
|
89
89
|
buy(item: string): Promise<import("../being/types.ts").Answer>;
|
|
90
90
|
learn(): Promise<import("../being/types.ts").Answer>;
|
|
91
91
|
hi(_args: JsonObject, asker: Asker): {
|
|
@@ -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.js';
|
|
5
|
-
import { isSilence, isUnreached } from '../being/silence.js';
|
|
5
|
+
import { isSilence, isUnreached, isWord } from '../being/silence.js';
|
|
6
6
|
// A printer. Invites whom she is told to, prints for her occupants. Her
|
|
7
7
|
// blueprint lives in her cells so a test can change her shape.
|
|
8
8
|
export class Printer extends Being {
|
|
@@ -41,7 +41,7 @@ export class Shop extends Being {
|
|
|
41
41
|
}
|
|
42
42
|
async keepPrinter(invitation) {
|
|
43
43
|
const bp = await this.knock(invitation);
|
|
44
|
-
if (isSilence(bp) ||
|
|
44
|
+
if (isSilence(bp) || isWord(bp))
|
|
45
45
|
return false;
|
|
46
46
|
this.cells.printerId = await this.take('prn', invitation);
|
|
47
47
|
return true;
|
|
@@ -50,7 +50,7 @@ export class Shop extends Being {
|
|
|
50
50
|
const rec = this.occupant(asker);
|
|
51
51
|
if (rec?.notes.tier === 'gold' && invitation) {
|
|
52
52
|
const back = await this.knock(invitation, 'hi');
|
|
53
|
-
if (!isSilence(back) && !
|
|
53
|
+
if (!isSilence(back) && !isWord(back))
|
|
54
54
|
await this.take(`vip-${asker.id}`, invitation);
|
|
55
55
|
}
|
|
56
56
|
return { welcome: true };
|
|
@@ -65,7 +65,7 @@ export class Shop extends Being {
|
|
|
65
65
|
const out = await printer.ask('print', { doc: `receipt for ${item}` });
|
|
66
66
|
if (isUnreached(out))
|
|
67
67
|
return { error: 'printer unreachable', retry: true };
|
|
68
|
-
if (isSilence(out))
|
|
68
|
+
if (isSilence(out) || isWord(out))
|
|
69
69
|
return { error: 'printer refused' };
|
|
70
70
|
const st = this.cells.standings[id];
|
|
71
71
|
if (st.seen !== st.digest)
|
|
@@ -85,7 +85,7 @@ export class Customer extends Being {
|
|
|
85
85
|
async join(invitation) {
|
|
86
86
|
const mine = (await this.invite('shop-back'));
|
|
87
87
|
const out = await this.knock(invitation, 'hello', { invitation: mine });
|
|
88
|
-
if (isSilence(out) ||
|
|
88
|
+
if (isSilence(out) || isWord(out))
|
|
89
89
|
return out; // nothing was born
|
|
90
90
|
await this.take('shop', invitation); // now, and only now
|
|
91
91
|
return out;
|
|
@@ -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,9 +35,10 @@
|
|
|
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.js';
|
|
36
|
-
import { isSilence, isUnreached } from '../being/silence.js';
|
|
38
|
+
import { isSilence, isUnreached, isWord, wordOf } from '../being/silence.js';
|
|
37
39
|
import { Member } from './beings.js';
|
|
38
40
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
41
|
+
const said = (x, w) => isWord(x) && wordOf(x) === w;
|
|
39
42
|
// A pseudo-random source in the language alone: the estate must replay, on
|
|
40
43
|
// every terrain, from a number a failure can print.
|
|
41
44
|
const rolls = (seed) => () => {
|
|
@@ -44,17 +47,6 @@ const rolls = (seed) => () => {
|
|
|
44
47
|
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
45
48
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
46
49
|
};
|
|
47
|
-
// The graph, with nothing of the world in it: no key, no ward pk, no harbor.
|
|
48
|
-
// Two runs under two topologies are the same estate when these agree.
|
|
49
|
-
const abc = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
50
|
-
const fingerprint = (g) => JSON.stringify({
|
|
51
|
-
seats: g.seats.map((s) => JSON.stringify([s.host, s.id, s.guest, s.answered, s.live, s.taken])).sort(abc),
|
|
52
|
-
holds: g.holds.map((h) => JSON.stringify([h.owner, h.id, h.seat.host, h.seat.id, h.live])).sort(abc),
|
|
53
|
-
});
|
|
54
|
-
// Every run of one seed, under every topology, must land on the same graph.
|
|
55
|
-
// The topologies run in one process, so the first to finish records and the
|
|
56
|
-
// rest are held to it.
|
|
57
|
-
const landed = new Map();
|
|
58
50
|
// ---- the ledger is a graph
|
|
59
51
|
// Held after every move. Each line is a way the bookkeeping could stop being
|
|
60
52
|
// a graph: an arc with one end, an arc with two owners, a relation filed in
|
|
@@ -136,10 +128,11 @@ export function estate(label, make, { canDown, test }) {
|
|
|
136
128
|
const back = (await partner.being.invite('acme'));
|
|
137
129
|
assert.deepEqual(await acme.being.knock(back, 'ping'), { pong: 'acme' });
|
|
138
130
|
assert.equal(await acme.being.take('them', back), 'them');
|
|
139
|
-
// 5. acme kicks her. her standing is still hers to hold
|
|
140
|
-
// told
|
|
131
|
+
// 5. acme kicks her. her standing is still hers to hold, and she is not
|
|
132
|
+
// told until she asks: then the door, under the key it bound for her,
|
|
133
|
+
// says removed, and only to her.
|
|
141
134
|
acme.being.occupants.remove('partner-1');
|
|
142
|
-
assert.ok(
|
|
135
|
+
assert.ok(said(await partner.being.standings['the-client'].ask('ping'), 'removed'));
|
|
143
136
|
assert.deepEqual(Object.keys(partner.cells.standings), ['the-client']); // she never learns
|
|
144
137
|
assert.deepEqual(await acme.being.standings.them.ask('ping'), { pong: 'acme' }); // the other arc never moved
|
|
145
138
|
// 6. and back, as somebody else: a second invitation is a second relation,
|
|
@@ -148,7 +141,7 @@ export function estate(label, make, { canDown, test }) {
|
|
|
148
141
|
assert.notEqual(second.heir, first.heir);
|
|
149
142
|
assert.deepEqual(await partner.being.knock(second, 'ping'), { pong: 'partner-2' });
|
|
150
143
|
assert.equal(await partner.being.take('the-client-again', second), 'the-client-again');
|
|
151
|
-
assert.ok(
|
|
144
|
+
assert.ok(said(await partner.being.standings['the-client'].ask('ping'), 'removed'));
|
|
152
145
|
ledger(w.census(), undefined, 'the story');
|
|
153
146
|
});
|
|
154
147
|
for (const seed of [1, 2, 3]) {
|
|
@@ -193,6 +186,10 @@ export function estate(label, make, { canDown, test }) {
|
|
|
193
186
|
s.guest = guest;
|
|
194
187
|
s.answered = true;
|
|
195
188
|
}
|
|
189
|
+
else if (!s.live && (s.guest === null || (s.guest === guest && (hold === undefined || hold.live)))) {
|
|
190
|
+
// kicked, and she speaks under a key the door bound for this seat: she hears why
|
|
191
|
+
assert.ok(said(back, 'removed'), `${guest} knocking kicked ${s.host}/${s.id} should hear removed`);
|
|
192
|
+
}
|
|
196
193
|
else
|
|
197
194
|
assert.ok(isSilence(back), `${guest} knocking ${s.host}/${s.id} should be silence`);
|
|
198
195
|
return `${guest} knocks ${s.host}/${s.id}`;
|
|
@@ -274,7 +271,7 @@ export function estate(label, make, { canDown, test }) {
|
|
|
274
271
|
if (h.seat.live)
|
|
275
272
|
assert.deepEqual(out, { pong: h.seat.id }, `step ${step}: ${h.owner}/${h.id} -> ${h.seat.host}/${h.seat.id}`);
|
|
276
273
|
else
|
|
277
|
-
assert.ok(
|
|
274
|
+
assert.ok(said(out, 'removed'), `step ${step}: ${h.owner}/${h.id} is kicked and should hear removed`);
|
|
278
275
|
}
|
|
279
276
|
};
|
|
280
277
|
// The trace is the seed's own account of itself: a red run prints the
|
|
@@ -297,14 +294,6 @@ export function estate(label, make, { canDown, test }) {
|
|
|
297
294
|
throw new Error(`${at(step)}\n -> ${e.message}`, { cause: e });
|
|
298
295
|
});
|
|
299
296
|
}
|
|
300
|
-
// and the estate that came out is the estate every topology comes out with
|
|
301
|
-
const print = fingerprint(g);
|
|
302
|
-
const key = `seed ${seed}`;
|
|
303
|
-
const first = landed.get(key);
|
|
304
|
-
if (!first)
|
|
305
|
-
landed.set(key, { by: label, print });
|
|
306
|
-
else
|
|
307
|
-
assert.equal(print, first.print, `the estate under ${label} is not the estate under ${first.by}: a being learned the topology`);
|
|
308
297
|
});
|
|
309
298
|
}
|
|
310
299
|
}
|
|
@@ -19,12 +19,14 @@
|
|
|
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.js';
|
|
22
|
-
import { silence, isSilence, isUnreached } from '../being/silence.js';
|
|
22
|
+
import { silence, isSilence, isUnreached, isWord, wordOf } from '../being/silence.js';
|
|
23
23
|
import { digest } from '../being/digest.js';
|
|
24
24
|
import { Printer, Shop, Customer, Echo, Member } from './beings.js';
|
|
25
25
|
import { estate } from './estate.js';
|
|
26
26
|
export { Printer, Shop, Customer, Echo, Member };
|
|
27
27
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
28
|
+
// Her ward's word, as she receives it.
|
|
29
|
+
const said = (x, w) => isWord(x) && wordOf(x) === w;
|
|
28
30
|
export function conform(label, make, { canDown = true, test }) {
|
|
29
31
|
const t = (name, fn) => test(`[${label}] ${name}`, {}, fn);
|
|
30
32
|
const down = (name, fn) => test(`[${label}] ${name}`, { skip: !canDown && 'nothing to cut inside one ward' }, fn);
|
|
@@ -62,12 +64,12 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
62
64
|
assert.notEqual(i1.secret, i2.secret);
|
|
63
65
|
assert.equal(i1.ward, i2.ward);
|
|
64
66
|
});
|
|
65
|
-
t('remove before any knock: the heir is forgotten, a knock on it
|
|
67
|
+
t('remove before any knock: the heir is forgotten, and a knock on it by whoever holds it hears removed', async () => {
|
|
66
68
|
const { w, shop, alice } = await world();
|
|
67
69
|
const inv = await shop.being.invite();
|
|
68
70
|
shop.being.occupants.remove('cust1');
|
|
69
71
|
assert.equal(w.heir(inv), undefined);
|
|
70
|
-
assert.ok(
|
|
72
|
+
assert.ok(said(await alice.being.join(inv), 'removed'));
|
|
71
73
|
assert.deepEqual(alice.cells.standings, {});
|
|
72
74
|
});
|
|
73
75
|
// ---- consume, the knock
|
|
@@ -143,15 +145,15 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
143
145
|
assert.deepEqual(await alice.being.join(inv), { welcome: true }); // the same invitation still works
|
|
144
146
|
const inv2 = await shop.being.invite();
|
|
145
147
|
shop.being.occupants.remove('cust2'); // she changed her mind before the knock
|
|
146
|
-
assert.ok(
|
|
148
|
+
assert.ok(said(await alice.being.join(inv2), 'removed'));
|
|
147
149
|
assert.deepEqual(Object.keys(alice.cells.standings), ['shop']);
|
|
148
150
|
});
|
|
149
|
-
t('take without a knock, or after a
|
|
151
|
+
t('take without a knock, or after a knock that was not answered, births nothing', async () => {
|
|
150
152
|
const { shop, alice } = await world();
|
|
151
153
|
const inv = await shop.being.invite();
|
|
152
154
|
assert.equal(await alice.being.take('shop', inv), null);
|
|
153
155
|
shop.being.occupants.remove('cust1');
|
|
154
|
-
assert.ok(
|
|
156
|
+
assert.ok(said(await alice.being.knock(inv, 'hello'), 'removed'));
|
|
155
157
|
assert.equal(await alice.being.take('shop', inv), null);
|
|
156
158
|
assert.deepEqual(alice.cells.standings, {});
|
|
157
159
|
});
|
|
@@ -311,16 +313,16 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
311
313
|
printer.down = false;
|
|
312
314
|
assert.deepEqual(await alice.being.buy('hat'), { ok: true, receipt: 'receipt for hat' });
|
|
313
315
|
});
|
|
314
|
-
t('a removed occupant
|
|
316
|
+
t('a removed occupant hears removed under the key the door bound, her heir is forgotten; a dropped standing is gone', async () => {
|
|
315
317
|
const { w, alice, shop } = await world();
|
|
316
318
|
const inv = await shop.being.invite();
|
|
317
319
|
await alice.being.join(inv);
|
|
318
320
|
shop.being.occupants.remove('cust1');
|
|
319
321
|
assert.equal(w.heir(inv), undefined);
|
|
320
|
-
assert.ok(
|
|
322
|
+
assert.ok(said(await alice.being.buy('hat'), 'removed'));
|
|
321
323
|
alice.being.standings.remove('shop');
|
|
322
324
|
assert.equal(alice.being.standings.shop, undefined);
|
|
323
|
-
assert.ok(isSilence(await alice.being.knock(inv, 'hello'))); //
|
|
325
|
+
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
|
|
324
326
|
const again = await shop.being.invite(); // cust2. a new heir
|
|
325
327
|
assert.notEqual(again.heir, inv.heir);
|
|
326
328
|
assert.deepEqual(await alice.being.knock(again, 'hello'), { welcome: true });
|
|
@@ -333,7 +335,7 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
333
335
|
assert.deepEqual(await p, { ok: true, receipt: 'receipt for hat' });
|
|
334
336
|
assert.equal(shop.cells.sales.length, 1);
|
|
335
337
|
});
|
|
336
|
-
t('a being that throws is
|
|
338
|
+
t('a being that throws is the word threw to whom the door has bound; the ward stays up', async () => {
|
|
337
339
|
const w = await make();
|
|
338
340
|
class Bomb {
|
|
339
341
|
s;
|
|
@@ -349,7 +351,7 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
349
351
|
const x = await w.boot('X', Bomb);
|
|
350
352
|
const a = await w.boot('A', Customer);
|
|
351
353
|
const inv = (await x.being.s.occupants.invite('a'));
|
|
352
|
-
assert.ok(
|
|
354
|
+
assert.ok(said(await a.being.knock(inv, 'any'), 'threw'));
|
|
353
355
|
assert.deepEqual(await a.being.knock(inv), { asks: [], notes: {} });
|
|
354
356
|
});
|
|
355
357
|
t('null is an answer, silence is not', async () => {
|
|
@@ -407,7 +409,7 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
407
409
|
if (!w.oneWard)
|
|
408
410
|
assert.ok(isSilence(await a.being.knock(at(priv), 'time'))); // a ward with no public being: silence
|
|
409
411
|
assert.ok(isUnreached(await a.being.knock({ ward: 'a'.repeat(128) }, 'time'))); // no door anywhere: unreached
|
|
410
|
-
assert.ok(
|
|
412
|
+
assert.ok(said(await a.being.knock({ ward: 'not a pk' }, 'time'), 'invitation')); // not even an address
|
|
411
413
|
assert.equal(await a.being.take('clock', at(pub)), 'clock');
|
|
412
414
|
assert.deepEqual(await a.being.standings.clock.ask('time'), { time: 12, got: {} });
|
|
413
415
|
assert.equal(a.cells.standings.clock.seen, await digest((await a.being.standings.clock.ask())));
|
package/dist/harbor/core.d.ts
CHANGED
|
@@ -17,14 +17,20 @@ export type Bound = {
|
|
|
17
17
|
export type Loader = (record: WardRecord) => Promise<Record<string, BeingClass>>;
|
|
18
18
|
export declare const DEFAULT_CODE = "classes/index.ts";
|
|
19
19
|
export declare class Harbor {
|
|
20
|
+
#private;
|
|
20
21
|
readonly store: Store;
|
|
21
22
|
readonly loader: Loader;
|
|
22
23
|
readonly wards: Map<string, Hosted>;
|
|
23
|
-
readonly doors: Map<string, (bytes: Uint8Array) => Promise<
|
|
24
|
+
readonly doors: Map<string, (bytes: Uint8Array) => Promise<{
|
|
25
|
+
bytes: Uint8Array;
|
|
26
|
+
heard: boolean;
|
|
27
|
+
}>>;
|
|
24
28
|
readonly reaches: Map<string, Bound>;
|
|
25
29
|
readonly down: Set<string>;
|
|
30
|
+
readonly refused: Map<string, number>;
|
|
26
31
|
readonly classes: Record<string, BeingClass>;
|
|
27
32
|
readonly fallbacks: Reach[];
|
|
33
|
+
readonly announcers: Set<() => void>;
|
|
28
34
|
constructor(store: Store, loader?: Loader);
|
|
29
35
|
boot(): Promise<void>;
|
|
30
36
|
carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
|
package/dist/harbor/core.js
CHANGED
|
@@ -32,6 +32,7 @@ export class Harbor {
|
|
|
32
32
|
doors = new Map(); // ward pk -> door
|
|
33
33
|
reaches = new Map(); // the directory: foreign ward pk -> reach
|
|
34
34
|
down = new Set(); // pks this harbor will not reach right now: weather, for a test
|
|
35
|
+
refused = new Map(); // own ward pk -> arrivals its door would not admit. what a terrain does with it is its own
|
|
35
36
|
classes = {}; // classes handed in-process, beside the loader's
|
|
36
37
|
// The dialers' sockets, in the order they were dialed: where a pk nobody
|
|
37
38
|
// here knows is sent, because the listener a harbor dialed is a rendezvous
|
|
@@ -41,6 +42,18 @@ export class Harbor {
|
|
|
41
42
|
// carries it: with a single slot, a harbor holding two lines answered
|
|
42
43
|
// unreached for a pk the other line could have reached.
|
|
43
44
|
fallbacks = [];
|
|
45
|
+
// How a dialer says what this harbor holds now. An announce is a claim and
|
|
46
|
+
// a claim is worth nothing until the door behind it answers a probe, so
|
|
47
|
+
// saying it again costs a proof and buys nothing to an impostor. What is
|
|
48
|
+
// not free is never saying it again: a ward booted, adopted or dropped
|
|
49
|
+
// after a line opened would be announced only by the next line, and a line
|
|
50
|
+
// that does not drop never opens again, so it would be unreachable through
|
|
51
|
+
// that listener for as long as the socket stayed healthy.
|
|
52
|
+
announcers = new Set();
|
|
53
|
+
#announce() {
|
|
54
|
+
for (const say of this.announcers)
|
|
55
|
+
say();
|
|
56
|
+
}
|
|
44
57
|
constructor(store, loader = async () => ({})) {
|
|
45
58
|
this.store = store;
|
|
46
59
|
this.loader = loader;
|
|
@@ -56,12 +69,20 @@ export class Harbor {
|
|
|
56
69
|
this.hint(pk, url);
|
|
57
70
|
}
|
|
58
71
|
// ---- the directory
|
|
72
|
+
// Bytes to one of this harbor's own doors, and the one bit the door says
|
|
73
|
+
// beside its reply, counted.
|
|
74
|
+
async #own(pk, door, bytes) {
|
|
75
|
+
const r = await door(bytes);
|
|
76
|
+
if (!r.heard)
|
|
77
|
+
this.refused.set(pk, (this.refused.get(pk) ?? 0) + 1);
|
|
78
|
+
return new Uint8Array(r.bytes);
|
|
79
|
+
}
|
|
59
80
|
async carry(pk, bytes) {
|
|
60
81
|
if (this.down.has(pk))
|
|
61
82
|
return undefined;
|
|
62
83
|
const door = this.doors.get(pk);
|
|
63
84
|
if (door)
|
|
64
|
-
return
|
|
85
|
+
return this.#own(pk, door, bytes);
|
|
65
86
|
const bound = this.reaches.get(pk);
|
|
66
87
|
if (bound)
|
|
67
88
|
return bound.reach.carry(pk, bytes);
|
|
@@ -77,7 +98,7 @@ export class Harbor {
|
|
|
77
98
|
return undefined;
|
|
78
99
|
const door = this.doors.get(pk);
|
|
79
100
|
if (door)
|
|
80
|
-
return
|
|
101
|
+
return this.#own(pk, door, bytes);
|
|
81
102
|
const bound = this.reaches.get(pk);
|
|
82
103
|
return bound?.held ? bound.reach.carry(pk, bytes) : undefined;
|
|
83
104
|
}
|
|
@@ -155,6 +176,7 @@ export class Harbor {
|
|
|
155
176
|
await h.save();
|
|
156
177
|
this.wards.delete(name);
|
|
157
178
|
this.doors.delete(h.pk);
|
|
179
|
+
this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
|
|
158
180
|
}
|
|
159
181
|
return this.store.take(name);
|
|
160
182
|
}
|
|
@@ -204,6 +226,7 @@ export class Harbor {
|
|
|
204
226
|
this.wards.set(name, hosted);
|
|
205
227
|
this.doors.set(hosted.pk, door);
|
|
206
228
|
await save();
|
|
229
|
+
this.#announce(); // reachable through every line this harbor already holds, not only the next one
|
|
207
230
|
return hosted;
|
|
208
231
|
}
|
|
209
232
|
}
|
package/dist/harbor/dial.js
CHANGED
|
@@ -14,6 +14,7 @@ export function dial(harbor, url) {
|
|
|
14
14
|
wait = 1000;
|
|
15
15
|
void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
|
|
16
16
|
}, (why) => {
|
|
17
|
+
harbor.announcers.delete(say);
|
|
17
18
|
harbor.unbind(s);
|
|
18
19
|
if (d.socket === s)
|
|
19
20
|
d.socket = null;
|
|
@@ -30,9 +31,14 @@ export function dial(harbor, url) {
|
|
|
30
31
|
wait = Math.min(wait * 2, 30000);
|
|
31
32
|
});
|
|
32
33
|
harbor.fallbacks.push(s); // from the moment it is dialed: an ask made before the handshake waits on the socket, and is not unreached
|
|
34
|
+
// What this harbor holds, said when the line opens and again whenever it
|
|
35
|
+
// changes. A ward booted or adopted after the handshake is announced on
|
|
36
|
+
// the line already in hand, and a dropped one stops being claimed at once.
|
|
37
|
+
const say = () => s.announce([...harbor.doors.keys()]);
|
|
33
38
|
line.addEventListener('open', () => {
|
|
34
39
|
d.socket = s;
|
|
35
|
-
|
|
40
|
+
harbor.announcers.add(say);
|
|
41
|
+
say();
|
|
36
42
|
});
|
|
37
43
|
};
|
|
38
44
|
d.close = () => {
|
package/dist/harbor/memory.d.ts
CHANGED
|
@@ -5,9 +5,13 @@ export type Booted = WardPointers & {
|
|
|
5
5
|
};
|
|
6
6
|
export type WardFactory = (ground: Ground) => Promise<WardPointers>;
|
|
7
7
|
export declare class MemoryHarbor {
|
|
8
|
-
readonly doors: Map<string, (bytes: Uint8Array) => Promise<
|
|
8
|
+
readonly doors: Map<string, (bytes: Uint8Array) => Promise<{
|
|
9
|
+
bytes: Uint8Array;
|
|
10
|
+
heard: boolean;
|
|
11
|
+
}>>;
|
|
9
12
|
readonly down: Set<string>;
|
|
10
13
|
readonly wire: string[];
|
|
14
|
+
readonly refused: Map<string, number>;
|
|
11
15
|
readonly peers: Set<MemoryHarbor>;
|
|
12
16
|
cut: boolean;
|
|
13
17
|
readonly partitions: Map<string, Record<string, unknown>>;
|
package/dist/harbor/memory.js
CHANGED
|
@@ -3,6 +3,7 @@ export class MemoryHarbor {
|
|
|
3
3
|
doors = new Map(); // ward pk -> door
|
|
4
4
|
down = new Set(); // ward pks this harbor cannot reach right now
|
|
5
5
|
wire = []; // every byte string that ever crossed, as hex. for inspection
|
|
6
|
+
refused = new Map(); // ward pk -> arrivals its door would not admit: strangers, counted
|
|
6
7
|
peers = new Set();
|
|
7
8
|
cut = false;
|
|
8
9
|
partitions = new Map(); // seed -> memory. the harbor keeps it and reads nothing
|
|
@@ -13,8 +14,12 @@ export class MemoryHarbor {
|
|
|
13
14
|
if (this.down.has(farPk))
|
|
14
15
|
return undefined;
|
|
15
16
|
const door = this.doors.get(farPk);
|
|
16
|
-
if (door)
|
|
17
|
-
|
|
17
|
+
if (door) {
|
|
18
|
+
const r = await door(bytes);
|
|
19
|
+
if (!r.heard)
|
|
20
|
+
this.refused.set(farPk, (this.refused.get(farPk) ?? 0) + 1);
|
|
21
|
+
return r.bytes;
|
|
22
|
+
}
|
|
18
23
|
const peer = [...this.peers].find((h) => h.doors.has(farPk) && !h.down.has(farPk));
|
|
19
24
|
if (!peer || this.cut)
|
|
20
25
|
return undefined; // nothing. the harbor has no word silence
|
package/dist/ward/door.d.ts
CHANGED
|
@@ -8,5 +8,9 @@ export type Door = {
|
|
|
8
8
|
occupants: Record<string, unknown>;
|
|
9
9
|
};
|
|
10
10
|
};
|
|
11
|
-
export
|
|
12
|
-
|
|
11
|
+
export type Judged = {
|
|
12
|
+
bytes: Uint8Array;
|
|
13
|
+
heard: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
|
|
16
|
+
export declare function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, publicKey: () => string | null, random: (n: number) => Uint8Array): (bytes: Uint8Array) => Promise<Judged>;
|