@quo-systems/quo 0.2.0 → 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 CHANGED
@@ -539,7 +539,8 @@ the standing is.
539
539
  ### The public being
540
540
 
541
541
  A ward may have one public being, and no more. She is an ordinary being,
542
- booted by the owner and marked public. Arrivals for no heir reach her. Her
542
+ booted by the owner and then marked public by the owner's `public` ask.
543
+ Arrivals for no heir reach her. Her
543
544
  asker is `{}`: she is asked by anyone, and she can tell. She may hold
544
545
  standings. She may invite, and an occupant of hers arrives named while
545
546
  strangers still arrive as `{}`. A ward without a public being answers
@@ -560,6 +561,14 @@ So her answer must be safe to repeat. This is her obligation, not a gap at
560
561
  the door. Anything that must happen once lives behind an invitation, where
561
562
  there is a heir and a count.
562
563
 
564
+ Nothing else about her is different. Her cells, her standings and what she
565
+ keeps are hers, exactly as they are for every being, and Quo has no say in
566
+ them: she may hold a board every stranger plays on, a catalogue, a queue, or
567
+ nothing at all. She is an ordinary being who chose to describe herself to
568
+ strangers and to answer them, and that choice is the whole of what makes her
569
+ public. A stranger who keeps knocking is the harbor's, never hers and never
570
+ the door's.
571
+
563
572
  ### How a relation crosses wards
564
573
 
565
574
  1. She mints an id. Her ward mints a heir for it, keeps the pk, and hands
@@ -818,6 +827,14 @@ silent. It keeps every being it booted, builds every stance, mints every
818
827
  key, seals every ask that leaves, and unseals every one that arrives. Its
819
828
  beings trust it blindly. It trusts its harbor the same way.
820
829
 
830
+ Only a ward moves. A being has no address of her own and never leaves the
831
+ ward that booted her: her peers hold a standing at a ward pk, and what they
832
+ trust is that ward's word about an id, never the being under it. So which
833
+ beings share a ward is settled when they are booted, and a ward migrates
834
+ whole, with everyone in it, or not at all. To move one being would be to ask
835
+ every peer to trust a ward they never accepted, and asking that is what an
836
+ invitation is.
837
+
821
838
  Its ward is itself. It boots itself as the first being in its own map, under
822
839
  its own pk, and its own stance is built by the same code that builds every
823
840
  being's. Its cells are its partition, all the way down.
@@ -945,19 +962,25 @@ become one.
945
962
  The root is not an occupant. It holds no heir, no key, no invitation. The
946
963
  ward names every arrival on the ask `{ id: 'OWNER' }`, the third asker of the
947
964
  ward-to-being edge, and answers it as such; an owner at the door arrives as
948
- her id. What an owner can do is exactly what a being cannot: boot, place a
949
- relation into a being of the ward, and take one out of her. The root's asks
950
- carry no allowance of their own.
965
+ her id. What an owner can do is exactly what a being cannot: boot, mark the
966
+ public being, place a relation into a being of the ward, take one out of
967
+ her, and take her out of the ward. The root's asks carry no allowance of
968
+ their own. The ward is a being to her owner: the empty ask is her describe,
969
+ and each ask in it carries a description and an input naming its fields, as
970
+ a being's asks do, so that a side renders the owner's asks the way it
971
+ renders anyone's and holds no list of its own.
951
972
 
952
973
  ```
953
- ask() -> { asks: [boot, invite, knock, remove],
974
+ ask() -> { asks: [boot, public, invite, knock, remove, unboot],
954
975
  notes: { pk, beings: { key: { class, public, digest } } } }
955
- ask('boot', { key, class, public? }) -> { booted: key } | { error }
976
+ ask('boot', { key, class }) -> { booted: key } | { error }
977
+ ask('public', { key }) -> { public: key } | { error }
956
978
  ask('invite', { being, id }) -> invitation | null | { error }
957
979
  ask('knock', { being | { boot: class, key },
958
980
  id, invitation, method?, args?, wanted? })
959
981
  -> { taken: id | null, answer } | { error: 'silence' | 'unreached' | ... }
960
982
  ask('remove', { being, id }) -> { removed: id } | { error }
983
+ ask('unboot', { being }) -> { unbooted: key, removed: [id, ...] } | { error }
961
984
  anything else -> { error: 'unknown ask' }
962
985
  ```
963
986
 
@@ -965,9 +988,15 @@ anything else -> { error: 'unknown ask' }
965
988
  spend, and saying nothing is the ward's default, exactly as for a being.
966
989
  - The describe runs every being's own describe as `OWNER` and hashes it; a
967
990
  being that throws or falls silent there shows a null digest.
968
- - Boot refuses a key already booted, a class the harbor does not know, and a
969
- second public being; a boot that made nobody leaves the partition as it
970
- found it. A throw at birth is that boot's error, and nothing half-lives.
991
+ - Boot refuses a key already booted and a class the harbor does not know; a
992
+ boot that made nobody leaves the partition as it found it. A throw at
993
+ birth is that boot's error, and nothing half-lives.
994
+ - Public marks a being already booted as the ward's one public being. It
995
+ refuses a key not booted, the ward's own pk, and a second public being
996
+ while one stands, since marking a second would leave the first holding
997
+ every relation she had, reachable by nobody at the bare pk and told by
998
+ nobody she was replaced. Marking the one already public again is
999
+ answered, and changes nothing.
971
1000
  - Invite on the ward's own pk mints an owner, and only the root may ask it:
972
1001
  from the door it is answered as an invite on nobody. The ward has
973
1002
  occupants, its owners, and no standings: a knock for the ward itself is a
@@ -978,6 +1007,17 @@ anything else -> { error: 'unknown ask' }
978
1007
  chose her class and her cells at boot; her owner may choose a relation for
979
1008
  her too. The boot form names a being new or existing: a key already booted
980
1009
  is a being of theirs, not a class that failed.
1010
+ - **Unboot is the inverse of boot, and the only way a being leaves a ward.**
1011
+ Every relation she holds goes with her, by the same calls she would have
1012
+ used herself, so an occupant of hers hears `removed` from the keys her door
1013
+ kept rather than meeting a being who is not there; the owner is told which
1014
+ ids went. Her cells and her bind table go too, because a row naming a being
1015
+ no door holds would boot her again on the next restart. If she was the
1016
+ public being the mark goes with her, and the ward answers arrivals for no
1017
+ heir with silence as one that never had a public being does. The ward
1018
+ itself is refused, for the reason knock is: it would be a ward deleting
1019
+ itself from inside its own map, leaving its owners bound to a door that is
1020
+ gone.
981
1021
  - The owner's remove is the mirror of its knock: a relation out of a being,
982
1022
  by id, and the id may be an occupant or a standing, since the two share
983
1023
  one namespace. The being's own remove says nothing; the owner hears
@@ -1188,7 +1228,10 @@ socket this harbor holds for that pk, and never onward by request. So a
1188
1228
  harbor that cannot be dialed is reached through the one it dialed, by
1189
1229
  anyone who holds a hint that its pk is there. The **dialer**,
1190
1230
  `src/harbor/dial.ts`, is that harbor's half: one held socket to a URL, its
1191
- own pks announced when the line opens, the listener's bound, dialed again
1231
+ own pks announced when the line opens and again whenever what this harbor
1232
+ holds changes, so a ward booted, adopted or dropped on a line already in hand
1233
+ is reachable through it without waiting for that line to fall; the listener's
1234
+ bound, dialed again
1192
1235
  with a wait that doubles from a second to thirty when the line drops, and
1193
1236
  while it is dialed it is one of the harbor's fallbacks, where a pk nobody
1194
1237
  here knows is sent. Fallbacks are a list and a pk is tried down it in
@@ -1464,11 +1507,14 @@ passes the same suite. Those tests are the checklist, not the mock.
1464
1507
  on Quo actually has -- scattered wards on scattered harbors, partners and
1465
1508
  employees invited, knocking, taken, kicked, and moving to harbors of their
1466
1509
  own. It keeps a model of the graph, plays legal moves against it from three
1467
- fixed seeds, and after every move holds three things: that the ledger every
1468
- ward keeps closes on both ends, that every arc in it answers when asked
1469
- exactly what being that arc means, and that the same seed played under all
1470
- three topologies lands on the same graph. The last is the promise of Quo at
1471
- a scale a scene cannot reach. A relation is two arcs and never one edge:
1510
+ fixed seeds, and after every move holds two things: that the ledger every
1511
+ ward keeps closes on both ends, and that every arc in it answers when asked
1512
+ exactly what being that arc means. The second is held under all three
1513
+ topologies, and that is the promise of Quo at a scale a scene cannot
1514
+ reach: the model never learns the topology, so neither may any answer. The
1515
+ model is the script's own bookkeeping and is never compared with itself
1516
+ across topologies, which would hold by construction and prove nothing. A
1517
+ relation is two arcs and never one edge:
1472
1518
  the occupant is the host's, the standing is the guest's, and the model
1473
1519
  keeps them apart because the ward does. A red run prints its seed and the
1474
1520
  moves that got there. It is read beside one hand-written story that says in
@@ -1645,6 +1691,19 @@ Decided here, so that the answer is not rediscovered:
1645
1691
  the default says so with `wanted` on the ask, which is the whole of what a
1646
1692
  being needs. An owner who wants another ceiling is asking for a different
1647
1693
  ward.
1694
+ - **A leaked seed is a taken ward, and there is no succession.** The seed has
1695
+ one custodian, the harbor, which vouches that it stays secret; a being
1696
+ never touches it and no byte on the wire carries it. So it is lost only
1697
+ where that vouch broke, and a broken vouch is total and silent. It is not
1698
+ lost alone: a harbor keeps the seed and the partition under one name, and
1699
+ the partition holds every relation's keys, so there is no event where the
1700
+ address is taken and the relations are not. Nothing is left to succeed to.
1701
+ A succession signed by the key that speaks now is signed as well by
1702
+ whoever took it, and one that could not be forged would have to be
1703
+ committed to in advance, in the invitation, which carries nothing. The
1704
+ answer is a new ward, and every peer invited again as anyone is invited.
1705
+ Lose the seed, lose the ward; there is no recovery inside Quo, and that is
1706
+ the same sentence the root owner already hears.
1648
1707
 
1649
1708
  ## Glossary
1650
1709
 
@@ -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 three things:
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 third is the one that matters most here. A being never learns where the
25
- // other one is, so a script that is topology-invariant at forty moves is the
26
- // promise of Quo held at a scale a scene cannot reach -- and it is what makes
27
- // a migration honest: if nobody can tell the topology apart, moving a ward
28
- // between harbors cannot be felt.
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
@@ -45,17 +47,6 @@ const rolls = (seed) => () => {
45
47
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
46
48
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
47
49
  };
48
- // The graph, with nothing of the world in it: no key, no ward pk, no harbor.
49
- // Two runs under two topologies are the same estate when these agree.
50
- const abc = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
51
- const fingerprint = (g) => JSON.stringify({
52
- seats: g.seats.map((s) => JSON.stringify([s.host, s.id, s.guest, s.answered, s.live, s.taken])).sort(abc),
53
- holds: g.holds.map((h) => JSON.stringify([h.owner, h.id, h.seat.host, h.seat.id, h.live])).sort(abc),
54
- });
55
- // Every run of one seed, under every topology, must land on the same graph.
56
- // The topologies run in one process, so the first to finish records and the
57
- // rest are held to it.
58
- const landed = new Map();
59
50
  // ---- the ledger is a graph
60
51
  // Held after every move. Each line is a way the bookkeeping could stop being
61
52
  // a graph: an arc with one end, an arc with two owners, a relation filed in
@@ -303,14 +294,6 @@ export function estate(label, make, { canDown, test }) {
303
294
  throw new Error(`${at(step)}\n -> ${e.message}`, { cause: e });
304
295
  });
305
296
  }
306
- // and the estate that came out is the estate every topology comes out with
307
- const print = fingerprint(g);
308
- const key = `seed ${seed}`;
309
- const first = landed.get(key);
310
- if (!first)
311
- landed.set(key, { by: label, print });
312
- else
313
- assert.equal(print, first.print, `the estate under ${label} is not the estate under ${first.by}: a being learned the topology`);
314
297
  });
315
298
  }
316
299
  }
@@ -30,6 +30,7 @@ export declare class Harbor {
30
30
  readonly refused: Map<string, number>;
31
31
  readonly classes: Record<string, BeingClass>;
32
32
  readonly fallbacks: Reach[];
33
+ readonly announcers: Set<() => void>;
33
34
  constructor(store: Store, loader?: Loader);
34
35
  boot(): Promise<void>;
35
36
  carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
@@ -42,6 +42,18 @@ export class Harbor {
42
42
  // carries it: with a single slot, a harbor holding two lines answered
43
43
  // unreached for a pk the other line could have reached.
44
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
+ }
45
57
  constructor(store, loader = async () => ({})) {
46
58
  this.store = store;
47
59
  this.loader = loader;
@@ -164,6 +176,7 @@ export class Harbor {
164
176
  await h.save();
165
177
  this.wards.delete(name);
166
178
  this.doors.delete(h.pk);
179
+ this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
167
180
  }
168
181
  return this.store.take(name);
169
182
  }
@@ -213,6 +226,7 @@ export class Harbor {
213
226
  this.wards.set(name, hosted);
214
227
  this.doors.set(hosted.pk, door);
215
228
  await save();
229
+ this.#announce(); // reachable through every line this harbor already holds, not only the next one
216
230
  return hosted;
217
231
  }
218
232
  }
@@ -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
- s.announce([...harbor.doors.keys()]);
40
+ harbor.announcers.add(say);
41
+ say();
36
42
  });
37
43
  };
38
44
  d.close = () => {
@@ -1,4 +1,4 @@
1
- import type { Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
1
+ import type { Ask, Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
2
2
  export type OwnerSide = {
3
3
  pk: string;
4
4
  doors: Map<string, {
@@ -24,9 +24,11 @@ export type OwnerSide = {
24
24
  };
25
25
  }>;
26
26
  publicKey(): string | null;
27
- instantiate(key: string, className: string, isPublic?: boolean): {
27
+ instantiate(key: string, className: string): {
28
28
  key: string;
29
29
  } | null;
30
+ unboot(key: string): string[] | null;
31
+ setPublic(key: string): void;
30
32
  };
31
- export declare const OWNER_ASKS: readonly ['boot', 'invite', 'knock', 'remove'];
33
+ export declare const OWNER_ASKS: Ask[];
32
34
  export declare function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json>;
@@ -1,13 +1,29 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // The owner's asks. The root owner holds the ask pointer; every other owner
3
- // is an occupant of the ward itself, invited by the root. Five asks:
4
- // describe, boot, invite, knock, remove. The knock is a being's knock made
5
- // for her, and the standing is written into her cells under the id the
6
- // owner gave; remove takes a relation out of her by id, the mirror of it.
3
+ // is an occupant of the ward itself, invited by the root. The ward is a
4
+ // being to her owner: the empty ask is her describe, and it carries her asks
5
+ // with a description and an input each, so a side renders them as it
6
+ // renders any being's. Six asks: boot, public, invite, knock, remove and
7
+ // unboot, which is boot's inverse and takes her relations with her. The
8
+ // knock is a being's knock made for her, and the standing is written into
9
+ // her cells under the id the owner gave; remove takes a relation out of her
10
+ // by id, the mirror of it.
7
11
  import { isSilence, isWord, wordOf } from '../being/silence.js';
8
12
  import { digest } from '../being/digest.js';
9
13
  import { OWNER } from '../being/types.js';
10
- export const OWNER_ASKS = ['boot', 'invite', 'knock', 'remove'];
14
+ const str = { type: 'string' };
15
+ export const OWNER_ASKS = [
16
+ { name: 'boot', description: 'boot a being by class name, under a key the owner chooses', input: { type: 'object', properties: { key: str, class: str }, required: ['key', 'class'] } },
17
+ { name: 'public', description: 'mark a booted being as the one public being of the ward, reached by anyone at the bare pk', input: { type: 'object', properties: { key: str }, required: ['key'] } },
18
+ { name: 'invite', description: 'mint an invitation on a being of the ward, under the id she will know the occupant by; on the ward pk it mints an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str }, required: ['being', 'id'] } },
19
+ {
20
+ name: 'knock',
21
+ description: 'knock for a being of the ward with an invitation, and take the standing under id if answered; being is a key booted, or { boot: class, key } to boot her first',
22
+ input: { type: 'object', properties: { being: { description: 'a key, or { boot, key }' }, id: str, invitation: { type: 'object' }, method: str, args: { type: 'object' }, wanted: { type: 'object', properties: { time: { type: 'number' } } } }, required: ['being', 'id', 'invitation'] },
23
+ },
24
+ { name: 'remove', description: 'take a relation out of a being of the ward by id, occupant or standing; on the ward pk it unseats an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str }, required: ['being', 'id'] } },
25
+ { name: 'unboot', description: 'take a being out of the ward, with every relation she holds; her occupants hear removed, and the ward itself is refused', input: { type: 'object', properties: { being: str }, required: ['being'] } },
26
+ ];
11
27
  export async function ownerAnswer(w, asker, method, args) {
12
28
  // Every name the owner gives is a word. Coerced instead of checked, an
13
29
  // object passed where a name belongs becomes the string "[object Object]",
@@ -16,17 +32,24 @@ export async function ownerAnswer(w, asker, method, args) {
16
32
  if (method === undefined)
17
33
  return describe(w);
18
34
  if (method === 'boot') {
19
- // A ward may have one public being, and no more. Marking a second would
20
- // leave the first holding every relation she had, reachable by nobody at
21
- // the bare pk, and told by nobody that she had been replaced.
22
- if (args.public === true && w.publicKey() !== null)
23
- return { error: 'a ward has one public being' };
24
35
  const key = word(args.key), className = word(args.class);
25
36
  if (key === null || className === null)
26
37
  return { error: 'no such class, or key taken' };
27
- const door = w.instantiate(key, className, args.public === true);
38
+ const door = w.instantiate(key, className);
28
39
  return door ? { booted: door.key } : { error: 'no such class, or key taken' };
29
40
  }
41
+ if (method === 'public') {
42
+ // A ward may have one public being, and no more. Marking a second would
43
+ // leave the first holding every relation she had, reachable by nobody at
44
+ // the bare pk, and told by nobody that she had been replaced.
45
+ const key = word(args.key);
46
+ if (key === null || key === w.pk || !w.doors.has(key))
47
+ return { error: 'no such being' };
48
+ if (w.publicKey() !== null && w.publicKey() !== key)
49
+ return { error: 'a ward has one public being' };
50
+ w.setPublic(key);
51
+ return { public: key };
52
+ }
30
53
  if (method === 'invite') {
31
54
  // On the ward itself, only the root may invite: an occupant of the ward
32
55
  // is an owner, and ownership is minted by the ask pointer alone. An owner
@@ -90,6 +113,20 @@ export async function ownerAnswer(w, asker, method, args) {
90
113
  return { error: 'nothing to remove' };
91
114
  return { removed: id };
92
115
  }
116
+ if (method === 'unboot') {
117
+ // The inverse of boot, and the only way a being leaves a ward. Her
118
+ // relations go with her, so an occupant of hers hears `removed` from the
119
+ // keys the door kept, exactly as it would had the owner removed each id
120
+ // by hand; the owner hears which ids those were. The ward itself is
121
+ // refused, for the reason knock is: it is a being to nobody outside, and
122
+ // unbooting it would be a ward deleting itself from inside its own map,
123
+ // leaving its owners bound to a door that is gone.
124
+ const being = word(args.being);
125
+ if (being === null || being === w.pk)
126
+ return { error: 'no such being' };
127
+ const removed = w.unboot(being);
128
+ return removed === null ? { error: 'no such being' } : { unbooted: being, removed };
129
+ }
93
130
  return { error: 'unknown ask' };
94
131
  }
95
132
  // The ward's describe for its owner: its beings, their classes, a digest each,
@@ -110,7 +147,7 @@ async function describe(w) {
110
147
  beings[key] = { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d };
111
148
  }
112
149
  return {
113
- asks: OWNER_ASKS.map((name) => ({ name, input: { type: 'object' } })),
150
+ asks: OWNER_ASKS,
114
151
  notes: { pk: w.pk, beings },
115
152
  };
116
153
  }
package/dist/ward/ward.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // leaves, opens every one that arrives.
7
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';
@@ -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, isPublic) => this.#instantiate(key, className, isPublic),
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, isPublic = false) {
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quo-systems/quo",
3
- "version": "0.2.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",
@@ -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 three things:
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 third is the one that matters most here. A being never learns where the
25
- // other one is, so a script that is topology-invariant at forty moves is the
26
- // promise of Quo held at a scale a scene cannot reach -- and it is what makes
27
- // a migration honest: if nobody can tell the topology apart, moving a ward
28
- // between harbors cannot be felt.
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
@@ -60,20 +62,6 @@ const rolls = (seed: number) => () => {
60
62
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
61
63
  };
62
64
 
63
- // The graph, with nothing of the world in it: no key, no ward pk, no harbor.
64
- // Two runs under two topologies are the same estate when these agree.
65
- const abc = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
66
- const fingerprint = (g: Graph): string =>
67
- JSON.stringify({
68
- seats: g.seats.map((s) => JSON.stringify([s.host, s.id, s.guest, s.answered, s.live, s.taken])).sort(abc),
69
- holds: g.holds.map((h) => JSON.stringify([h.owner, h.id, h.seat.host, h.seat.id, h.live])).sort(abc),
70
- });
71
-
72
- // Every run of one seed, under every topology, must land on the same graph.
73
- // The topologies run in one process, so the first to finish records and the
74
- // rest are held to it.
75
- const landed = new Map<string, { by: string; print: string }>();
76
-
77
65
  // ---- the ledger is a graph
78
66
 
79
67
  // Held after every move. Each line is a way the bookkeeping could stop being
@@ -332,13 +320,6 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
332
320
  throw new Error(`${at(step)}\n -> ${e.message}`, { cause: e });
333
321
  });
334
322
  }
335
-
336
- // and the estate that came out is the estate every topology comes out with
337
- const print = fingerprint(g);
338
- const key = `seed ${seed}`;
339
- const first = landed.get(key);
340
- if (!first) landed.set(key, { by: label, print });
341
- else assert.equal(print, first.print, `the estate under ${label} is not the estate under ${first.by}: a being learned the topology`);
342
323
  });
343
324
  }
344
325
  }
@@ -55,6 +55,17 @@ export class Harbor {
55
55
  // carries it: with a single slot, a harbor holding two lines answered
56
56
  // unreached for a pk the other line could have reached.
57
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
+ }
58
69
 
59
70
  constructor(store: Store, loader: Loader = async () => ({})) {
60
71
  this.store = store;
@@ -172,6 +183,7 @@ export class Harbor {
172
183
  await h.save();
173
184
  this.wards.delete(name);
174
185
  this.doors.delete(h.pk);
186
+ this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
175
187
  }
176
188
  return this.store.take(name);
177
189
  }
@@ -223,6 +235,7 @@ export class Harbor {
223
235
  this.wards.set(name, hosted);
224
236
  this.doors.set(hosted.pk, door);
225
237
  await save();
238
+ this.#announce(); // reachable through every line this harbor already holds, not only the next one
226
239
  return hosted;
227
240
  }
228
241
  }
@@ -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
- s.announce([...harbor.doors.keys()]);
56
+ harbor.announcers.add(say);
57
+ say();
52
58
  });
53
59
  };
54
60
  d.close = () => {
package/src/ward/owner.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // The owner's asks. The root owner holds the ask pointer; every other owner
3
- // is an occupant of the ward itself, invited by the root. Five asks:
4
- // describe, boot, invite, knock, remove. The knock is a being's knock made
5
- // for her, and the standing is written into her cells under the id the
6
- // owner gave; remove takes a relation out of her by id, the mirror of it.
3
+ // is an occupant of the ward itself, invited by the root. The ward is a
4
+ // being to her owner: the empty ask is her describe, and it carries her asks
5
+ // with a description and an input each, so a side renders them as it
6
+ // renders any being's. Six asks: boot, public, invite, knock, remove and
7
+ // unboot, which is boot's inverse and takes her relations with her. The
8
+ // knock is a being's knock made for her, and the standing is written into
9
+ // her cells under the id the owner gave; remove takes a relation out of her
10
+ // by id, the mirror of it.
7
11
  import { isSilence, isWord, wordOf } from '../being/silence.ts';
8
12
  import { digest } from '../being/digest.ts';
9
13
  import { OWNER } from '../being/types.ts';
10
- import type { Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
14
+ import type { Ask, Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
11
15
 
12
16
  export type OwnerSide = {
13
17
  pk: string;
@@ -21,10 +25,24 @@ export type OwnerSide = {
21
25
  }
22
26
  >;
23
27
  publicKey(): string | null;
24
- instantiate(key: string, className: string, isPublic?: boolean): { key: string } | null;
28
+ instantiate(key: string, className: string): { key: string } | null;
29
+ unboot(key: string): string[] | null;
30
+ setPublic(key: string): void;
25
31
  };
26
32
 
27
- export const OWNER_ASKS = ['boot', 'invite', 'knock', 'remove'] as const;
33
+ const str = { type: 'string' };
34
+ export const OWNER_ASKS: Ask[] = [
35
+ { name: 'boot', description: 'boot a being by class name, under a key the owner chooses', input: { type: 'object', properties: { key: str, class: str }, required: ['key', 'class'] } },
36
+ { name: 'public', description: 'mark a booted being as the one public being of the ward, reached by anyone at the bare pk', input: { type: 'object', properties: { key: str }, required: ['key'] } },
37
+ { name: 'invite', description: 'mint an invitation on a being of the ward, under the id she will know the occupant by; on the ward pk it mints an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str }, required: ['being', 'id'] } },
38
+ {
39
+ name: 'knock',
40
+ description: 'knock for a being of the ward with an invitation, and take the standing under id if answered; being is a key booted, or { boot: class, key } to boot her first',
41
+ input: { type: 'object', properties: { being: { description: 'a key, or { boot, key }' }, id: str, invitation: { type: 'object' }, method: str, args: { type: 'object' }, wanted: { type: 'object', properties: { time: { type: 'number' } } } }, required: ['being', 'id', 'invitation'] },
42
+ },
43
+ { name: 'remove', description: 'take a relation out of a being of the ward by id, occupant or standing; on the ward pk it unseats an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str }, required: ['being', 'id'] } },
44
+ { name: 'unboot', description: 'take a being out of the ward, with every relation she holds; her occupants hear removed, and the ward itself is refused', input: { type: 'object', properties: { being: str }, required: ['being'] } },
45
+ ];
28
46
 
29
47
  export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json> {
30
48
  // Every name the owner gives is a word. Coerced instead of checked, an
@@ -34,16 +52,22 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
34
52
 
35
53
  if (method === undefined) return describe(w);
36
54
  if (method === 'boot') {
37
- // A ward may have one public being, and no more. Marking a second would
38
- // leave the first holding every relation she had, reachable by nobody at
39
- // the bare pk, and told by nobody that she had been replaced.
40
- if (args.public === true && w.publicKey() !== null) return { error: 'a ward has one public being' };
41
55
  const key = word(args.key),
42
56
  className = word(args.class);
43
57
  if (key === null || className === null) return { error: 'no such class, or key taken' };
44
- const door = w.instantiate(key, className, args.public === true);
58
+ const door = w.instantiate(key, className);
45
59
  return door ? { booted: door.key } : { error: 'no such class, or key taken' };
46
60
  }
61
+ if (method === 'public') {
62
+ // A ward may have one public being, and no more. Marking a second would
63
+ // leave the first holding every relation she had, reachable by nobody at
64
+ // the bare pk, and told by nobody that she had been replaced.
65
+ const key = word(args.key);
66
+ if (key === null || key === w.pk || !w.doors.has(key)) return { error: 'no such being' };
67
+ if (w.publicKey() !== null && w.publicKey() !== key) return { error: 'a ward has one public being' };
68
+ w.setPublic(key);
69
+ return { public: key };
70
+ }
47
71
  if (method === 'invite') {
48
72
  // On the ward itself, only the root may invite: an occupant of the ward
49
73
  // is an owner, and ownership is minted by the ask pointer alone. An owner
@@ -98,6 +122,19 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
98
122
  else return { error: 'nothing to remove' };
99
123
  return { removed: id };
100
124
  }
125
+ if (method === 'unboot') {
126
+ // The inverse of boot, and the only way a being leaves a ward. Her
127
+ // relations go with her, so an occupant of hers hears `removed` from the
128
+ // keys the door kept, exactly as it would had the owner removed each id
129
+ // by hand; the owner hears which ids those were. The ward itself is
130
+ // refused, for the reason knock is: it is a being to nobody outside, and
131
+ // unbooting it would be a ward deleting itself from inside its own map,
132
+ // leaving its owners bound to a door that is gone.
133
+ const being = word(args.being);
134
+ if (being === null || being === w.pk) return { error: 'no such being' };
135
+ const removed = w.unboot(being);
136
+ return removed === null ? { error: 'no such being' } : { unbooted: being, removed };
137
+ }
101
138
  return { error: 'unknown ask' };
102
139
  }
103
140
 
@@ -117,7 +154,7 @@ async function describe(w: OwnerSide): Promise<Json> {
117
154
  beings[key] = { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d };
118
155
  }
119
156
  return {
120
- asks: OWNER_ASKS.map((name) => ({ name, input: { type: 'object' } })),
157
+ asks: OWNER_ASKS,
121
158
  notes: { pk: w.pk, beings },
122
159
  };
123
160
  }
package/src/ward/ward.ts CHANGED
@@ -8,7 +8,7 @@ import { silence, isSilence, unreached, isWord, word } from '../being/silence.ts
8
8
  import { OWNER } from '../being/types.ts';
9
9
  import type { Asker, BeingLike, Cells, JsonObject, Reply, Stance, Wanted, Word } from '../being/types.ts';
10
10
  import type { Ground, WardPointers } from './ground.ts';
11
- import { at, open, put, emptyBind, emptyCells, type Bind, type Partition, type StandingKeys } from './partition.ts';
11
+ import { at, drop, open, put, emptyBind, emptyCells, type Bind, type Partition, type StandingKeys } from './partition.ts';
12
12
  import { Heirs } from './heirs.ts';
13
13
  import { makeDoor, type Door } from './door.ts';
14
14
  import { buildStance } from './stance.ts';
@@ -90,7 +90,9 @@ class Self implements BeingLike {
90
90
  pk: this.pk,
91
91
  doors: this.doors,
92
92
  publicKey: () => this.p.public,
93
- instantiate: (key, className, isPublic) => this.#instantiate(key, className, isPublic),
93
+ instantiate: (key, className) => this.#instantiate(key, className),
94
+ unboot: (key) => this.#unboot(key),
95
+ setPublic: (key) => void (this.p.public = key),
94
96
  },
95
97
  asker,
96
98
  method,
@@ -99,15 +101,37 @@ class Self implements BeingLike {
99
101
  }
100
102
 
101
103
  // ---- ward functions. on the object. no stance reaches them.
102
- #instantiate(key: string, className: string, isPublic = false): Booted | null {
104
+ #instantiate(key: string, className: string): Booted | null {
103
105
  if (!key || this.doors.has(key)) return null;
104
106
  const door = this.#boot(key, className);
105
107
  if (!door) return null;
106
108
  door.cells.class = className; // so a restart finds her
107
- if (isPublic) this.p.public = key;
108
109
  return door;
109
110
  }
110
111
 
112
+ // The inverse of boot, and the only way a being leaves a ward. Her
113
+ // relations go first and by the same calls she would have used herself,
114
+ // so every occupant hears `removed` at the door from the keys it kept,
115
+ // rather than meeting a being who is simply not there any more. What is
116
+ // left after that is her cells and her bind table, and they go with her:
117
+ // a row naming a being no door holds would be read on the next restart
118
+ // and boot her again.
119
+ #unboot(key: string): string[] | null {
120
+ const door = this.doors.get(key);
121
+ if (!door || key === this.pk) return null;
122
+ const occupants = Object.keys(door.cells.occupants),
123
+ standings = Object.keys(door.cells.standings);
124
+ for (const id of occupants) door.stance.occupants.remove(id);
125
+ for (const id of standings) door.stance.standings.remove(id);
126
+ // A ward whose public being is gone answers arrivals for no heir with
127
+ // silence, as a ward that never had one does.
128
+ if (this.p.public === key) this.p.public = null;
129
+ drop(this.p.beings, key);
130
+ drop(this.p.bind, key);
131
+ this.doors.delete(key);
132
+ return [...occupants, ...standings];
133
+ }
134
+
111
135
  // Nothing is written until there is somebody to write it for: a class the
112
136
  // harbor does not know makes no being, and a boot that made nobody must
113
137
  // leave the partition as it found it.