@quo-systems/quo 0.2.0 → 0.2.2
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 +87 -29
- package/dist/conformance/estate.js +11 -28
- package/dist/harbor/core.d.ts +1 -0
- package/dist/harbor/core.js +14 -0
- package/dist/harbor/dial.js +7 -1
- package/dist/ward/owner.d.ts +5 -3
- package/dist/ward/owner.js +49 -12
- package/dist/ward/ward.js +30 -5
- package/package.json +1 -1
- package/src/conformance/estate.ts +11 -30
- package/src/harbor/core.ts +13 -0
- package/src/harbor/dial.ts +7 -1
- package/src/ward/owner.ts +50 -13
- package/src/ward/ward.ts +28 -4
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
|
|
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.
|
|
@@ -834,10 +851,14 @@ One ward is one runtime and one language, and every being in it shares both.
|
|
|
834
851
|
A harbor that wants two languages starts two wards at least. Under
|
|
835
852
|
`src/being/`, `src/ward/`, `src/harbor/` and `src/conformance/` no runtime is
|
|
836
853
|
named. The whole
|
|
837
|
-
platform surface is the language plus
|
|
838
|
-
`TextDecoder`, `structuredClone`, `setTimeout
|
|
839
|
-
|
|
840
|
-
|
|
854
|
+
platform surface is the language plus ten globals. Six are the ward's own:
|
|
855
|
+
`crypto`, `TextEncoder`, `TextDecoder`, `structuredClone`, `setTimeout` and
|
|
856
|
+
`atob`. Four more came with the harbor, and are the whole of what a reach
|
|
857
|
+
costs: `fetch` and `WebSocket`, the two kinds of reach; `DataView`, the frame
|
|
858
|
+
id on a socket; and `clearTimeout`, the dialer's reconnect called off. That
|
|
859
|
+
list is a promise, not an accident: `test/terrain.test.ts` fails the build
|
|
860
|
+
both when a file names a platform and when it reaches for a global outside
|
|
861
|
+
the list, and when the list names one the tree has stopped using.
|
|
841
862
|
|
|
842
863
|
The package exports raw TypeScript, extension and all, on every specifier, so
|
|
843
864
|
Node loads it directly and every other terrain reaches a ward through a
|
|
@@ -874,7 +895,8 @@ partition
|
|
|
874
895
|
bind key -> her bind table
|
|
875
896
|
standings id -> { ward, heir | null, current, next, seq } her keys for a standing
|
|
876
897
|
occupants id -> heir pk
|
|
877
|
-
knocks <ward>:<heir> | public:<ward> -> { current, next, spoke, seq } before take
|
|
898
|
+
knocks <ward>:<heir> | public:<ward> -> { current, next, spoke, sent, seq } before take
|
|
899
|
+
spoke: bytes came back once. sent: bytes went out once, answered or not
|
|
878
900
|
answered <ward>:<heir> | public:<ward> -> true knocked and answered
|
|
879
901
|
minted the last eight pks her side minted
|
|
880
902
|
heirs heir pk -> { being, id, current, announced, fresh, mark, spent } the door's view of every occupant
|
|
@@ -945,19 +967,25 @@ become one.
|
|
|
945
967
|
The root is not an occupant. It holds no heir, no key, no invitation. The
|
|
946
968
|
ward names every arrival on the ask `{ id: 'OWNER' }`, the third asker of the
|
|
947
969
|
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,
|
|
949
|
-
relation into a being of the ward,
|
|
950
|
-
carry no allowance of
|
|
970
|
+
her id. What an owner can do is exactly what a being cannot: boot, mark the
|
|
971
|
+
public being, place a relation into a being of the ward, take one out of
|
|
972
|
+
her, and take her out of the ward. The root's asks carry no allowance of
|
|
973
|
+
their own. The ward is a being to her owner: the empty ask is her describe,
|
|
974
|
+
and each ask in it carries a description and an input naming its fields, as
|
|
975
|
+
a being's asks do, so that a side renders the owner's asks the way it
|
|
976
|
+
renders anyone's and holds no list of its own.
|
|
951
977
|
|
|
952
978
|
```
|
|
953
|
-
ask() -> { asks: [boot, invite, knock, remove],
|
|
979
|
+
ask() -> { asks: [boot, public, invite, knock, remove, unboot],
|
|
954
980
|
notes: { pk, beings: { key: { class, public, digest } } } }
|
|
955
|
-
ask('boot', { key, class
|
|
981
|
+
ask('boot', { key, class }) -> { booted: key } | { error }
|
|
982
|
+
ask('public', { key }) -> { public: key } | { error }
|
|
956
983
|
ask('invite', { being, id }) -> invitation | null | { error }
|
|
957
984
|
ask('knock', { being | { boot: class, key },
|
|
958
985
|
id, invitation, method?, args?, wanted? })
|
|
959
986
|
-> { taken: id | null, answer } | { error: 'silence' | 'unreached' | ... }
|
|
960
987
|
ask('remove', { being, id }) -> { removed: id } | { error }
|
|
988
|
+
ask('unboot', { being }) -> { unbooted: key, removed: [id, ...] } | { error }
|
|
961
989
|
anything else -> { error: 'unknown ask' }
|
|
962
990
|
```
|
|
963
991
|
|
|
@@ -965,9 +993,15 @@ anything else -> { error: 'unknown ask' }
|
|
|
965
993
|
spend, and saying nothing is the ward's default, exactly as for a being.
|
|
966
994
|
- The describe runs every being's own describe as `OWNER` and hashes it; a
|
|
967
995
|
being that throws or falls silent there shows a null digest.
|
|
968
|
-
- Boot refuses a key already booted
|
|
969
|
-
|
|
970
|
-
|
|
996
|
+
- Boot refuses a key already booted and a class the harbor does not know; a
|
|
997
|
+
boot that made nobody leaves the partition as it found it. A throw at
|
|
998
|
+
birth is that boot's error, and nothing half-lives.
|
|
999
|
+
- Public marks a being already booted as the ward's one public being. It
|
|
1000
|
+
refuses a key not booted, the ward's own pk, and a second public being
|
|
1001
|
+
while one stands, since marking a second would leave the first holding
|
|
1002
|
+
every relation she had, reachable by nobody at the bare pk and told by
|
|
1003
|
+
nobody she was replaced. Marking the one already public again is
|
|
1004
|
+
answered, and changes nothing.
|
|
971
1005
|
- Invite on the ward's own pk mints an owner, and only the root may ask it:
|
|
972
1006
|
from the door it is answered as an invite on nobody. The ward has
|
|
973
1007
|
occupants, its owners, and no standings: a knock for the ward itself is a
|
|
@@ -978,6 +1012,17 @@ anything else -> { error: 'unknown ask' }
|
|
|
978
1012
|
chose her class and her cells at boot; her owner may choose a relation for
|
|
979
1013
|
her too. The boot form names a being new or existing: a key already booted
|
|
980
1014
|
is a being of theirs, not a class that failed.
|
|
1015
|
+
- **Unboot is the inverse of boot, and the only way a being leaves a ward.**
|
|
1016
|
+
Every relation she holds goes with her, by the same calls she would have
|
|
1017
|
+
used herself, so an occupant of hers hears `removed` from the keys her door
|
|
1018
|
+
kept rather than meeting a being who is not there; the owner is told which
|
|
1019
|
+
ids went. Her cells and her bind table go too, because a row naming a being
|
|
1020
|
+
no door holds would boot her again on the next restart. If she was the
|
|
1021
|
+
public being the mark goes with her, and the ward answers arrivals for no
|
|
1022
|
+
heir with silence as one that never had a public being does. The ward
|
|
1023
|
+
itself is refused, for the reason knock is: it would be a ward deleting
|
|
1024
|
+
itself from inside its own map, leaving its owners bound to a door that is
|
|
1025
|
+
gone.
|
|
981
1026
|
- The owner's remove is the mirror of its knock: a relation out of a being,
|
|
982
1027
|
by id, and the id may be an occupant or a standing, since the two share
|
|
983
1028
|
one namespace. The being's own remove says nothing; the owner hears
|
|
@@ -1145,8 +1190,7 @@ way waits out the ceiling before asking again, because a suite does not
|
|
|
1145
1190
|
become speakable by asking sooner. Its wait returns to the first second
|
|
1146
1191
|
where the far side announced and was taken, and not where the line merely
|
|
1147
1192
|
opened: a listener that opens and then refuses would otherwise be dialed at
|
|
1148
|
-
a fixed two seconds for good
|
|
1149
|
-
code was read. Absent is this suite,
|
|
1193
|
+
a fixed two seconds for good. Absent is this suite,
|
|
1150
1194
|
because a caller older than the header is this one. Present and different is
|
|
1151
1195
|
refused as it is written: the number is compared as it was sent, so a suite
|
|
1152
1196
|
is one spelling and not a family of them.
|
|
@@ -1188,7 +1232,10 @@ socket this harbor holds for that pk, and never onward by request. So a
|
|
|
1188
1232
|
harbor that cannot be dialed is reached through the one it dialed, by
|
|
1189
1233
|
anyone who holds a hint that its pk is there. The **dialer**,
|
|
1190
1234
|
`src/harbor/dial.ts`, is that harbor's half: one held socket to a URL, its
|
|
1191
|
-
own pks announced when the line opens
|
|
1235
|
+
own pks announced when the line opens and again whenever what this harbor
|
|
1236
|
+
holds changes, so a ward booted, adopted or dropped on a line already in hand
|
|
1237
|
+
is reachable through it without waiting for that line to fall; the listener's
|
|
1238
|
+
bound, dialed again
|
|
1192
1239
|
with a wait that doubles from a second to thirty when the line drops, and
|
|
1193
1240
|
while it is dialed it is one of the harbor's fallbacks, where a pk nobody
|
|
1194
1241
|
here knows is sent. Fallbacks are a list and a pk is tried down it in
|
|
@@ -1393,8 +1440,8 @@ alone, one JavaScript file and one declaration file per source file with
|
|
|
1393
1440
|
every relative import rewritten, and the exports map points there, types
|
|
1394
1441
|
beside default. The
|
|
1395
1442
|
build runs before every gate and every publish and is never tracked;
|
|
1396
|
-
nothing
|
|
1397
|
-
|
|
1443
|
+
nothing in this repository imports it except through the package name,
|
|
1444
|
+
which is how a consumer does. `test/package.test.ts` is the one
|
|
1398
1445
|
suite that meets the package as a stranger: it packs the tarball,
|
|
1399
1446
|
installs it into an empty folder, and imports every entry point under
|
|
1400
1447
|
plain Node. `npm pack --dry-run` shows what ships: `dist/`, the four
|
|
@@ -1413,11 +1460,6 @@ test/ the suites.
|
|
|
1413
1460
|
vectors/ fixed inputs and outputs: the arithmetic, Quo's own framing, the frames on the wire.
|
|
1414
1461
|
```
|
|
1415
1462
|
|
|
1416
|
-
`dock/` sits beside `src` and is not Quo: the beings, screens, model sides
|
|
1417
|
-
and real harbors built on it, each folder with its own document. This
|
|
1418
|
-
document does not read them, and they may not add a word to it. Its one
|
|
1419
|
-
claim on them is that every harbor there passes `src/conformance/` untouched.
|
|
1420
|
-
|
|
1421
1463
|
The hand to a kit in another language is two things, and they are of two
|
|
1422
1464
|
kinds. `vectors/` is the byte-level hand: fixed inputs and outputs for
|
|
1423
1465
|
everything a stranger can observe, the arithmetic, the ward pk, the digest,
|
|
@@ -1464,11 +1506,14 @@ passes the same suite. Those tests are the checklist, not the mock.
|
|
|
1464
1506
|
on Quo actually has -- scattered wards on scattered harbors, partners and
|
|
1465
1507
|
employees invited, knocking, taken, kicked, and moving to harbors of their
|
|
1466
1508
|
own. It keeps a model of the graph, plays legal moves against it from three
|
|
1467
|
-
fixed seeds, and after every move holds
|
|
1468
|
-
ward keeps closes on both ends, that every arc in it answers when asked
|
|
1469
|
-
exactly what being that arc means
|
|
1470
|
-
|
|
1471
|
-
|
|
1509
|
+
fixed seeds, and after every move holds two things: that the ledger every
|
|
1510
|
+
ward keeps closes on both ends, and that every arc in it answers when asked
|
|
1511
|
+
exactly what being that arc means. The second is held under all three
|
|
1512
|
+
topologies, and that is the promise of Quo at a scale a scene cannot
|
|
1513
|
+
reach: the model never learns the topology, so neither may any answer. The
|
|
1514
|
+
model is the script's own bookkeeping and is never compared with itself
|
|
1515
|
+
across topologies, which would hold by construction and prove nothing. A
|
|
1516
|
+
relation is two arcs and never one edge:
|
|
1472
1517
|
the occupant is the host's, the standing is the guest's, and the model
|
|
1473
1518
|
keeps them apart because the ward does. A red run prints its seed and the
|
|
1474
1519
|
moves that got there. It is read beside one hand-written story that says in
|
|
@@ -1645,6 +1690,19 @@ Decided here, so that the answer is not rediscovered:
|
|
|
1645
1690
|
the default says so with `wanted` on the ask, which is the whole of what a
|
|
1646
1691
|
being needs. An owner who wants another ceiling is asking for a different
|
|
1647
1692
|
ward.
|
|
1693
|
+
- **A leaked seed is a taken ward, and there is no succession.** The seed has
|
|
1694
|
+
one custodian, the harbor, which vouches that it stays secret; a being
|
|
1695
|
+
never touches it and no byte on the wire carries it. So it is lost only
|
|
1696
|
+
where that vouch broke, and a broken vouch is total and silent. It is not
|
|
1697
|
+
lost alone: a harbor keeps the seed and the partition under one name, and
|
|
1698
|
+
the partition holds every relation's keys, so there is no event where the
|
|
1699
|
+
address is taken and the relations are not. Nothing is left to succeed to.
|
|
1700
|
+
A succession signed by the key that speaks now is signed as well by
|
|
1701
|
+
whoever took it, and one that could not be forged would have to be
|
|
1702
|
+
committed to in advance, in the invitation, which carries nothing. The
|
|
1703
|
+
answer is a new ward, and every peer invited again as anyone is invited.
|
|
1704
|
+
Lose the seed, lose the ward; there is no recovery inside Quo, and that is
|
|
1705
|
+
the same sentence the root owner already hears.
|
|
1648
1706
|
|
|
1649
1707
|
## Glossary
|
|
1650
1708
|
|
|
@@ -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
|
|
@@ -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
|
}
|
package/dist/harbor/core.d.ts
CHANGED
|
@@ -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>;
|
package/dist/harbor/core.js
CHANGED
|
@@ -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
|
}
|
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/ward/owner.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
33
|
+
export declare const OWNER_ASKS: Ask[];
|
|
32
34
|
export declare function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json>;
|
package/dist/ward/owner.js
CHANGED
|
@@ -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.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/package.json
CHANGED
|
@@ -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
|
|
@@ -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
|
}
|
package/src/harbor/core.ts
CHANGED
|
@@ -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
|
}
|
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/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.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|