nervur 0.19.2 → 0.20.0

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