@quo-systems/quo 0.2.9 → 0.2.10

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 (52) hide show
  1. package/README.md +19 -10
  2. package/SPEC.md +161 -89
  3. package/dist/being/being.d.ts +2 -2
  4. package/dist/being/being.js +27 -11
  5. package/dist/being/digest.js +11 -7
  6. package/dist/being/types.d.ts +2 -2
  7. package/dist/conformance/estate.js +1 -3
  8. package/dist/conformance/index.js +1 -1
  9. package/dist/harbor/core.d.ts +2 -0
  10. package/dist/harbor/core.js +114 -25
  11. package/dist/harbor/dial.js +5 -3
  12. package/dist/harbor/memory.d.ts +1 -1
  13. package/dist/harbor/memory.js +3 -2
  14. package/dist/harbor/reach.js +32 -7
  15. package/dist/ward/allowance.js +8 -2
  16. package/dist/ward/arithmetic.js +11 -3
  17. package/dist/ward/cells.d.ts +3 -1
  18. package/dist/ward/cells.js +79 -21
  19. package/dist/ward/door.d.ts +1 -0
  20. package/dist/ward/door.js +24 -5
  21. package/dist/ward/ground.d.ts +1 -0
  22. package/dist/ward/heirs.d.ts +1 -1
  23. package/dist/ward/heirs.js +15 -8
  24. package/dist/ward/owner.d.ts +5 -26
  25. package/dist/ward/owner.js +24 -15
  26. package/dist/ward/seal.js +3 -0
  27. package/dist/ward/stance.d.ts +4 -1
  28. package/dist/ward/stance.js +116 -54
  29. package/dist/ward/ward.d.ts +10 -0
  30. package/dist/ward/ward.js +87 -36
  31. package/package.json +4 -2
  32. package/src/being/being.ts +26 -10
  33. package/src/being/digest.ts +13 -11
  34. package/src/being/types.ts +10 -4
  35. package/src/conformance/estate.ts +1 -3
  36. package/src/conformance/index.ts +1 -1
  37. package/src/harbor/core.ts +110 -26
  38. package/src/harbor/dial.ts +6 -4
  39. package/src/harbor/memory.ts +3 -2
  40. package/src/harbor/reach.ts +38 -11
  41. package/src/ward/allowance.ts +8 -2
  42. package/src/ward/arithmetic.ts +11 -3
  43. package/src/ward/cells.ts +76 -25
  44. package/src/ward/door.ts +22 -5
  45. package/src/ward/ground.ts +9 -2
  46. package/src/ward/heirs.ts +13 -6
  47. package/src/ward/owner.ts +24 -24
  48. package/src/ward/seal.ts +2 -0
  49. package/src/ward/stance.ts +117 -48
  50. package/src/ward/ward.ts +86 -35
  51. package/vectors/arithmetic.json +7 -0
  52. package/vectors/framing.json +14 -5
@@ -1,12 +1,13 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // The one stance builder. Used for every being the ward boots. Nothing
3
3
  // outer is in what she holds: ids in, values out, keys in the bind table.
4
- import { silence, isSilence, isUnreached, isWord, word } from '../being/silence.js';
4
+ import { silence, isSilence, isWord, word, wordOf } from '../being/silence.js';
5
5
  import { digest } from '../being/digest.js';
6
6
  import { RESERVED_IDS } from '../being/types.js';
7
7
  import { at, put, drop } from './partition.js';
8
8
  import { allow, within, LATE } from './allowance.js';
9
9
  import { isWardPk } from './seal.js';
10
+ import { cellFault } from './cells.js';
10
11
  // The three calls on `standings` share the object with the ids she takes, so
11
12
  // a standing named after one of them would be unreachable: `standings.knock`
12
13
  // is the call, whatever record sits under that name. One namespace means one
@@ -51,9 +52,9 @@ export function buildStance(inside, key, cells, bind) {
51
52
  // Every ask a being makes, bounded. The bound is outside the lane on
52
53
  // purpose: an ask that waits its turn behind a relation that comes back
53
54
  // round never reaches a send, and a bound that only watched the wire would
54
- // never see it hang. A bound that ran out is silence, never unreached:
55
- // unreached promises nothing was delivered and is safe to ask again, and a
56
- // wait that gave up knows no such thing.
55
+ // never see it hang. A bound that ran out is the word late, never
56
+ // unreached: unreached promises nothing was delivered and is safe to ask
57
+ // again, and a wait that gave up knows no such thing.
57
58
  //
58
59
  // The bell rings before the work is reached, too: an ask that was still
59
60
  // waiting its turn when the wait ran out is never sent. Sent then, it would
@@ -67,56 +68,88 @@ export function buildStance(inside, key, cells, bind) {
67
68
  });
68
69
  return out === LATE ? word('late') : out;
69
70
  };
70
- const send = async (keys, method, args, wanted, rec) => {
71
- const r = await inside.send(bind, keys, true, method, args, wanted);
71
+ const read = (r) => {
72
72
  if (isSilence(r) || isWord(r))
73
- return r;
73
+ return { answer: r, door: false, seen: null };
74
74
  if ('silence' in r)
75
- return silence;
75
+ return { answer: silence, door: true, seen: null };
76
76
  if ('quo' in r)
77
- return word(r.quo); // the far door's word for her, sealed to her lid
77
+ return { answer: word(r.quo), door: true, seen: null }; // the far door's word for her, sealed to her lid
78
+ return { answer: r.object, door: true, seen: r.seen };
79
+ };
80
+ // Whether the door spent the heir this send spoke under. An object, the
81
+ // door's silence and threw all mean it was honoured. A refusal word wrote
82
+ // nothing and says so. Anything that is not the door speaking says nothing.
83
+ const honoured = (r) => r.door && !(isWord(r.answer) && wordOf(r.answer) !== 'threw');
84
+ const answered = (r) => r.door && !isSilence(r.answer) && !isWord(r.answer);
85
+ const send = async (keys, method, args, wanted, rec) => {
86
+ const r = read(await inside.send(bind, keys, true, method, args, wanted));
78
87
  if (rec && method !== undefined && r.seen !== null)
79
88
  rec.seen = r.seen; // the digest rode along
80
- return r.object;
89
+ return r.answer;
81
90
  };
82
91
  const knocking = async (inv, method, args, wanted) => {
83
92
  const name = nameOf(inv);
93
+ // Taken while this knock waited its turn: the relation lives in the
94
+ // standing now and speaks on its lane, under its keys and its count.
95
+ const taken = takenAs(name);
96
+ if (taken !== undefined) {
97
+ const keys = at(bind.standings, taken);
98
+ if (!keys)
99
+ return word('dropped');
100
+ return lane(`ask:${taken}`, () => asking(taken, keys, at(cells.standings, taken), method, args, wanted));
101
+ }
84
102
  // her own key for this relation, minted once: rotation two. reused if she knocks again.
85
103
  const k = (bind.knocks[name] ??= { current: (await inside.mintKey(bind)).seed, next: null, spoke: false, sent: false, seq: 0 });
86
- // the first knock on a heir speaks as the heir, the key the inviter handed out, and announces her own.
87
- // once bytes have come back, the heir is spent whatever they said, and her own key speaks.
88
- // She sent once as the heir and nothing came back. Either the door heard,
89
- // spent the heir and rotated to the key she announced, or nothing arrived
90
- // and the heir still stands. Only the far door knows, and it will not say.
104
+ // The first knock on a heir speaks as the heir, the key the inviter
105
+ // handed out, and announces her own. Once the door has honoured it the
106
+ // heir is spent, whatever she then said, and her own key speaks; a heir
107
+ // spent once stays spent.
108
+ //
109
+ // She sent once as the heir and the door did not speak: nothing came
110
+ // back, or bytes that were not Quo's, or the wait ran out. Either the
111
+ // door heard, spent the heir and rotated to the key she announced, or
112
+ // nothing arrived and the heir still stands. Only the far door knows,
113
+ // and it will not say.
91
114
  //
92
115
  // So she asks, and the asking tells her. Under her own key: if the door
93
- // heard, that key is the one it admits, and she is answered. If it did
94
- // not, the key means nothing there, the ask is refused, and a refusal at
95
- // that door writes nothing the heir is untouched and still speaks. Then
96
- // she knocks as the heir, as she would have. One extra round trip in the
97
- // one case where a reply was lost, and the relation is not stranded.
116
+ // heard, that key is the one it admits, and she is answered, or told the
117
+ // door's word. If it did not, the key means nothing there, the ask is
118
+ // refused, and a refusal at that door writes nothing: the heir is
119
+ // untouched and still speaks. Then she knocks as the heir, as she would
120
+ // have. One extra round trip in the one case where a reply was lost, and
121
+ // the relation is not stranded.
98
122
  //
99
123
  // No stranger gains anything by it: whoever holds the invitation could
100
124
  // always knock as the heir, and her own key is admitted only where the
101
125
  // door already bound it to her.
102
126
  if (inv.heir !== undefined && !k.spoke && k.sent) {
103
127
  const own = { ward: inv.ward, heir: inv.heir, current: k.current, next: k.next, seq: k.seq };
104
- const r = await inside.send(bind, own, true, method, args, wanted);
128
+ const r = read(await inside.send(bind, own, true, method, args, wanted));
105
129
  k.seq = own.seq;
106
130
  k.current = own.current;
107
131
  k.next = own.next;
108
- if (isUnreached(r))
109
- return r; // still nothing. she knows no more than before.
110
- if (!isSilence(r) && !isWord(r) && 'object' in r) {
111
- k.spoke = true; // it was heard the first time, and this is the answer
112
- bind.answered[name] = true;
113
- return r.object;
132
+ inside.wrote();
133
+ if (!r.door) {
134
+ // Nothing the door said. Unreached and late are hers to hear now; bytes
135
+ // that were not Quo's leave her where she was, and the heir is tried.
136
+ if (!isSilence(r.answer))
137
+ return r.answer;
138
+ }
139
+ else if (!isSilence(r.answer)) {
140
+ // The door spoke to her own key, so it heard the first knock: this is
141
+ // the answer, or the door's word for her. Its silence alone cannot be
142
+ // told from a refusal, so silence is followed by the knock as the heir,
143
+ // which the door then refuses if it had heard, and she hears silence.
144
+ k.spoke = true;
145
+ if (answered(r))
146
+ bind.answered[name] = true;
147
+ return r.answer;
114
148
  }
115
- // Refused. The door never heard her, so the heir is still hers to spend.
116
149
  }
117
150
  const first = inv.heir !== undefined && !k.spoke;
118
151
  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 };
119
- const r = await inside.send(bind, keys, true, method, args, wanted);
152
+ const r = read(await inside.send(bind, keys, true, method, args, wanted));
120
153
  k.sent = true; // bytes went out. whether they arrived is the far door's to know.
121
154
  // The number she spoke under, and the key she announced, kept whoever
122
155
  // answered and kept when nobody did. A reply lost on the way back is a
@@ -127,20 +160,12 @@ export function buildStance(inside, key, cells, bind) {
127
160
  k.current = keys.current;
128
161
  k.next = keys.next;
129
162
  }
130
- if (isUnreached(r))
131
- return r; // nothing came back. she does not know whether it was heard.
132
- // The heir is spent once the door honoured it, whatever she then said: an
133
- // object, silence, or that she threw. A refusal wrote nothing, and says
134
- // so by its word, so the heir still speaks and the next knock is as it.
135
- k.spoke = !(!isSilence(r) && !isWord(r) && 'quo' in r && r.quo !== 'threw');
136
- if (isSilence(r) || isWord(r))
137
- return r;
138
- if ('silence' in r)
139
- return silence;
140
- if ('quo' in r)
141
- return word(r.quo);
142
- bind.answered[name] = true;
143
- return r.object;
163
+ if (honoured(r))
164
+ k.spoke = true;
165
+ if (answered(r))
166
+ bind.answered[name] = true;
167
+ inside.wrote();
168
+ return r.answer;
144
169
  };
145
170
  // The id she took this relation under, if she took it. One relation is one
146
171
  // ward and one heir whichever door it is reached by, so the standing and the
@@ -157,7 +182,7 @@ export function buildStance(inside, key, cells, bind) {
157
182
  // same relation, and the two would refuse each other.
158
183
  const id = takenAs(nameOf(inv));
159
184
  if (id !== undefined)
160
- return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, bind.standings[id], cells.standings[id], method, args, wanted) : Promise.resolve(word('late')))));
185
+ return standingAt(id).ask(method, args, wanted);
161
186
  return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(word('late')))));
162
187
  },
163
188
  // Take consumes. Until now the relation lived in the knock record, under
@@ -194,12 +219,14 @@ export function buildStance(inside, key, cells, bind) {
194
219
  put(cells.standings, id, { id, digest: null, blueprint: null, seen: null });
195
220
  delete bind.knocks[name];
196
221
  delete bind.answered[name];
222
+ inside.wrote();
197
223
  return Promise.resolve(id);
198
224
  });
199
225
  },
200
226
  remove: (id) => {
201
227
  drop(cells.standings, id);
202
228
  drop(bind.standings, id);
229
+ inside.wrote();
203
230
  },
204
231
  };
205
232
  const standingAt = (id) => ({
@@ -215,13 +242,19 @@ export function buildStance(inside, key, cells, bind) {
215
242
  return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted) : Promise.resolve(word('late')))));
216
243
  },
217
244
  });
245
+ // The record an ask writes into is the one it was issued on. A standing
246
+ // dropped and re-taken under the same id while the ask was in flight is
247
+ // another relation, and the blueprint that came back is not hers.
218
248
  const asking = async (id, keys, rec, method, args, wanted) => {
219
249
  const out = await send(keys, method, args, wanted, rec);
220
250
  if (method === undefined && !isSilence(out) && !isWord(out)) {
221
- const here = at(cells.standings, id);
222
- if (here) {
223
- here.blueprint = out;
224
- here.digest = here.seen = await digest(out);
251
+ // A blueprint her cells cannot keep, nested past the bound or carrying
252
+ // a key no harbor writes, is not Quo's bytes: silence, and nothing written.
253
+ if (cellFault(out, 'blueprint') !== null)
254
+ return silence;
255
+ if (rec && at(cells.standings, id) === rec) {
256
+ rec.blueprint = out;
257
+ rec.digest = rec.seen = await digest(out);
225
258
  }
226
259
  }
227
260
  return out;
@@ -236,20 +269,48 @@ export function buildStance(inside, key, cells, bind) {
236
269
  cells,
237
270
  // A being may make. The ward refuses what it refuses the owner: a key
238
271
  // already booted, a class the harbor does not hold, and a throw at birth
239
- // leaves the partition as it was. Reaching into the being made is not
240
- // offered here: that is the owner's, and she has no relation to her
241
- // until one is invited and taken like any other.
242
- boot: (className, key) => inside.instantiate(key, className),
272
+ // leaves the partition as it was. Reaching into another is still the
273
+ // owner's alone: what a maker may have is a relation to what she made,
274
+ // by naming an id, and it is made the way every relation is. The being
275
+ // made mints an occupant for her maker under the maker's own key, so she
276
+ // knows who made her by that name and by nothing else; her maker knocks,
277
+ // takes it under the id she gave, and holds one standing like any other.
278
+ // Nothing half-lives: a relation that could not be made unmakes the
279
+ // being, who is a moment old and known to nobody.
280
+ boot: async (className, made, id) => {
281
+ if (id !== undefined && (reserved(id) || at(cells.standings, id) || at(cells.occupants, id)))
282
+ return null;
283
+ const born = inside.instantiate(made, className);
284
+ if (born === null || id === undefined)
285
+ return born;
286
+ const inv = await inside.relate(born, key);
287
+ if (inv) {
288
+ const out = await calls.knock(inv);
289
+ if (!isSilence(out) && !isWord(out) && (await calls.take(id, inv)) === id)
290
+ return born;
291
+ }
292
+ inside.unmake(born);
293
+ return null;
294
+ },
243
295
  occupants: {
244
296
  // rotation one: the ward mints the occupant's first key and gives the secret away.
245
297
  // it keeps the pk beside the id and nothing else. the invitation IS the key.
246
- invite: async (id) => {
298
+ invite: async (id, notes) => {
247
299
  if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
248
300
  return null;
249
- put(cells.occupants, id, { id, notes: {} });
301
+ // The key first, and nothing written until it exists: a record put
302
+ // before the mint would be a record a remove in the meantime drops
303
+ // with no heir to close, and the heir opened after it would name an
304
+ // id nobody holds, for a later invite under that id to resurrect.
250
305
  const k = await inside.mintKey(bind);
306
+ if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
307
+ return null; // taken while the key was minted
308
+ // The notes are the terms the inviter minted under, hers to read on her
309
+ // gate. She may write more later; nobody outside ever writes them.
310
+ put(cells.occupants, id, { id, notes: notes ? { ...notes } : {} });
251
311
  inside.openHeir(k.pk, key, id);
252
312
  put(bind.occupants, id, k.pk);
313
+ inside.wrote();
253
314
  return { ward: inside.pk, heir: k.pk, secret: k.seed };
254
315
  },
255
316
  remove: (id) => {
@@ -258,6 +319,7 @@ export function buildStance(inside, key, cells, bind) {
258
319
  drop(bind.occupants, id);
259
320
  if (heir)
260
321
  inside.closeHeir(heir); // the heir dies with the id
322
+ inside.wrote();
261
323
  },
262
324
  },
263
325
  standings,
@@ -1,2 +1,12 @@
1
+ import type { Cells, Stance } from '../being/types.ts';
1
2
  import type { Ground, WardPointers } from './ground.ts';
3
+ import { type Bind } from './partition.ts';
4
+ import { type Door } from './door.ts';
5
+ export type Booted = Door & {
6
+ bind: Bind;
7
+ stance: Stance;
8
+ cells: Cells & {
9
+ class?: string;
10
+ };
11
+ };
2
12
  export declare function Ward(ground: Ground): Promise<WardPointers>;
package/dist/ward/ward.js CHANGED
@@ -14,7 +14,7 @@ import { ownerAnswer } from './owner.js';
14
14
  import { allow, within, LATE } from './allowance.js';
15
15
  import { beingKey, openReply, sealAsk, wardKey, wardPadlock, wardSignPk } from './seal.js';
16
16
  import { unhex } from './arithmetic.js';
17
- import { guardCells, cellFault } from './cells.js';
17
+ import { guardCells, unguarded, cellFault } from './cells.js';
18
18
  // How many minted pks she keeps behind her. Enough to name the keys in play.
19
19
  const MINTED = 8;
20
20
  // The ward. One call in, two pointers out. Async because keys are derived.
@@ -38,24 +38,25 @@ class Self {
38
38
  this.g = ground;
39
39
  this.key = key;
40
40
  this.pk = key.pk;
41
- this.p = open(ground.memory); // the partition IS the ward's cells
42
- this.heirs = new Heirs(this.p);
41
+ this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
42
+ this.heirs = new Heirs(this.p, () => this.#wrote());
43
43
  this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n)); // pointer one
44
44
  this.#boot(this.pk, this); // the ward's ward is itself
45
45
  // a restart is silent: every being in the cells is constructed again, unasked.
46
- // A throw on a boot the owner asked for is that boot's answer: silence,
47
- // and nothing half-lives. This boot nobody asked for, and there is nobody
48
- // to hand a silence to. Any constructor may throw a default that is not
49
- // a value, an author's mistake, a shape she no longer recognises after an
50
- // upgrade — and let through here it is one being taking down the ward and
51
- // every other being in it, on a restart. She is absent instead: no door,
52
- // so silence at the door, and her cells sit untouched, waiting for the
53
- // run that can read them.
46
+ // A throw on a boot the owner asked for is that boot's error, and nothing
47
+ // half-lives. This boot nobody asked for, and there is nobody to hand an
48
+ // error to. Any constructor may throw, a default that is not a value, an
49
+ // author's mistake, a shape she does not recognise after an upgrade, and
50
+ // let through here it is one being taking down the ward and every other
51
+ // being in it, on a restart. She is absent instead: no door, so silence
52
+ // at the door, and her cells sit untouched, waiting for the run that can
53
+ // read them. Her row stays hers: nobody boots another class over it, the
54
+ // owner sees her as absent, and the owner may unboot her.
54
55
  for (const [k, c] of Object.entries(this.p.beings)) {
55
56
  if (k === this.pk || !c.class)
56
57
  continue;
57
58
  try {
58
- this.#instantiate(k, c.class);
59
+ this.#boot(k, c.class);
59
60
  }
60
61
  catch {
61
62
  /* she is not here this run. the ward is, and so is everyone else. */
@@ -82,20 +83,50 @@ class Self {
82
83
  return ownerAnswer({
83
84
  pk: this.pk,
84
85
  doors: this.doors,
86
+ absent: () => this.#absent(),
85
87
  publicKey: () => this.p.public,
86
88
  instantiate: (key, className) => this.#instantiate(key, className),
87
89
  unboot: (key) => this.#unboot(key),
88
- setPublic: (key) => void (this.p.public = key),
90
+ setPublic: (key) => {
91
+ this.p.public = key;
92
+ this.#wrote();
93
+ },
89
94
  }, asker, method, args);
90
95
  }
91
96
  // ---- ward functions. on the object. no stance reaches them.
97
+ // The partition was written. The harbor is told, and nothing more: what it
98
+ // does with the word is its own, and the ward never learns.
99
+ #wrote() {
100
+ this.g.wrote?.();
101
+ }
102
+ // The rows no door holds this run: beings whose class threw at birth or
103
+ // is not the harbor's to give. Key to class.
104
+ #absent() {
105
+ const out = {};
106
+ for (const [k, c] of Object.entries(this.p.beings))
107
+ if (k !== this.pk && !this.doors.has(k))
108
+ put(out, k, c.class ?? null);
109
+ return out;
110
+ }
111
+ // A new being under a key nobody holds. A key with a row in the partition
112
+ // is booted, whether a door holds it this run or not: an absent being's
113
+ // row is hers, with every relation in it, and no class is booted over it.
114
+ // A throw at birth is that boot's refusal and nothing half-lives; the
115
+ // ward says which, since the owner hears objects.
92
116
  #instantiate(key, className) {
93
- if (!key || this.doors.has(key))
117
+ if (typeof key !== 'string' || typeof className !== 'string' || !key || this.doors.has(key) || at(this.p.beings, key))
94
118
  return null;
95
- const door = this.#boot(key, className);
119
+ let door;
120
+ try {
121
+ door = this.#boot(key, className);
122
+ }
123
+ catch {
124
+ return 'threw';
125
+ }
96
126
  if (!door)
97
127
  return null;
98
- door.cells.class = className; // so a restart finds her
128
+ unguarded(door.cells).class = className; // so a restart finds her. the ward's key, written behind her guard
129
+ this.#wrote();
99
130
  return door;
100
131
  }
101
132
  // The inverse of boot, and the only way a being leaves a ward. Her
@@ -104,16 +135,28 @@ class Self {
104
135
  // rather than meeting a being who is simply not there any more. What is
105
136
  // left after that is her cells and her bind table, and they go with her:
106
137
  // a row naming a being no door holds would be read on the next restart
107
- // and boot her again.
138
+ // and boot her again. An absent being leaves the same way: she has no
139
+ // stance to speak for her, so the ward closes her heirs itself.
108
140
  #unboot(key) {
141
+ if (key === this.pk)
142
+ return null;
109
143
  const door = this.doors.get(key);
110
- if (!door || key === this.pk)
144
+ const cells = door?.cells ?? at(this.p.beings, key);
145
+ if (!cells)
111
146
  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);
147
+ const ids = (r) => (r !== null && typeof r === 'object' ? Object.keys(r) : []);
148
+ const occupants = ids(cells.occupants), standings = ids(cells.standings);
149
+ if (door) {
150
+ for (const id of occupants)
151
+ door.stance.occupants.remove(id);
152
+ for (const id of standings)
153
+ door.stance.standings.remove(id);
154
+ }
155
+ else {
156
+ const bind = at(this.p.bind, key);
157
+ for (const heir of Object.values(bind?.occupants ?? {}))
158
+ this.heirs.close(heir);
159
+ }
117
160
  // A ward whose public being is gone answers arrivals for no heir with
118
161
  // silence, as a ward that never had one does.
119
162
  if (this.p.public === key)
@@ -121,13 +164,14 @@ class Self {
121
164
  drop(this.p.beings, key);
122
165
  drop(this.p.bind, key);
123
166
  this.doors.delete(key);
167
+ this.#wrote();
124
168
  return [...occupants, ...standings];
125
169
  }
126
170
  // Nothing is written until there is somebody to write it for: a class the
127
171
  // harbor does not know makes no being, and a boot that made nobody must
128
172
  // leave the partition as it found it.
129
173
  #boot(key, classNameOrSelf) {
130
- const cells = guardCells(at(this.p.beings, key) ?? emptyCells());
174
+ const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
131
175
  const bind = at(this.p.bind, key) ?? emptyBind();
132
176
  const stance = buildStance({
133
177
  pk: this.pk,
@@ -138,21 +182,24 @@ class Self {
138
182
  // A throw at birth is null to her, not a throw in her method: she asked
139
183
  // for a being and got none, and her own answer is still hers to give.
140
184
  instantiate: (k, className) => {
141
- try {
142
- return this.#instantiate(k, className)?.key ?? null;
143
- }
144
- catch {
145
- return null;
146
- }
185
+ const made = this.#instantiate(k, className);
186
+ return made && made !== 'threw' ? made.key : null;
147
187
  },
188
+ // The two halves of a maker's way back, each the ward's own call: the
189
+ // being made mints an occupant for her maker, and a relation that
190
+ // never came about takes her out again.
191
+ relate: async (k, id) => (await this.doors.get(k)?.stance.occupants.invite(id)) ?? null,
192
+ unmake: (k) => void this.#unboot(k),
193
+ wrote: () => this.#wrote(),
148
194
  }, key, cells, bind);
149
195
  const being = classNameOrSelf === this ? this : this.g.instantiate(classNameOrSelf, stance);
150
- if (!being)
151
- return null;
196
+ if (!being || typeof being.answer !== 'function')
197
+ return null; // a being is anything with an answer, and nothing without one
152
198
  put(this.p.beings, key, cells);
153
199
  put(this.p.bind, key, bind);
154
200
  const door = { key, cells, bind, stance, being };
155
201
  this.doors.set(key, door);
202
+ this.#wrote();
156
203
  return door;
157
204
  }
158
205
  async #mintKey(bind) {
@@ -164,6 +211,7 @@ class Self {
164
211
  bind.minted.push(k.pk);
165
212
  if (bind.minted.length > MINTED)
166
213
  bind.minted.splice(0, bind.minted.length - MINTED);
214
+ this.#wrote();
167
215
  return k;
168
216
  }
169
217
  // One send for every door, the ward's own included. Mine: never leaves.
@@ -190,10 +238,12 @@ class Self {
190
238
  // Args that will not go through the seal are args that never left. A
191
239
  // being holds three answers and a throw is not one of them, so an ask
192
240
  // that cannot be sealed is told what is true of it: no door was reached.
193
- // Values only, by the same rule her cells keep: a Date would arrive as a
194
- // string, a function would not arrive at all, and she would never know
195
- // the far being was asked something other than what she said.
196
- if (cellFault(args, 'args') !== null)
241
+ // One object of values, by the rule her cells keep and the far door
242
+ // reads: a Date would arrive as a string, a function would not arrive at
243
+ // all, a list or a string would be refused as a stranger's bytes, and
244
+ // she would never know the far being was asked something other than
245
+ // what she said.
246
+ if (args === null || typeof args !== 'object' || Array.isArray(args) || cellFault(args, 'args') !== null)
197
247
  return unreached();
198
248
  let sealed;
199
249
  try {
@@ -227,6 +277,7 @@ class Self {
227
277
  keys.current = keys.next; // the far door holds `next` as announced. move to it.
228
278
  keys.next = null;
229
279
  }
280
+ this.#wrote(); // the count moved, and the keys may have
230
281
  return reply;
231
282
  }
232
283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quo-systems/quo",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
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",
@@ -33,7 +33,9 @@
33
33
  "./conformance": {
34
34
  "types": "./dist/conformance/index.d.ts",
35
35
  "default": "./dist/conformance/index.js"
36
- }
36
+ },
37
+ "./vectors/*": "./vectors/*",
38
+ "./package.json": "./package.json"
37
39
  },
38
40
  "scripts": {
39
41
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
@@ -18,11 +18,24 @@ export type AskSpec = {
18
18
  // Names a subclass may not use for an ask, because they are the base's own.
19
19
  const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'constructor']);
20
20
 
21
+ // Whether she has a method of that name, written on her own prototype chain
22
+ // below Object's. A name Object lends every object, `hasOwnProperty` or
23
+ // `toString`, is not a method she wrote; and a field she assigns in her own
24
+ // constructor is not there yet when the base checks, so an ask is a method
25
+ // on the prototype and nothing else.
26
+ const method = (self: object, name: string): boolean => {
27
+ for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
28
+ if (Object.hasOwn(p, name)) return typeof (p as Record<string, unknown>)[name] === 'function';
29
+ }
30
+ return false;
31
+ };
32
+
21
33
  export class Being {
22
34
  // Her cells' defaults. Merged in at birth, only where a key is missing, so
23
35
  // a restart keeps what she wrote.
24
36
  static cells: JsonObject = {};
25
- // What she can be asked. Declaration order is blueprint order.
37
+ // What she can be asked. Declaration order is blueprint order, except a
38
+ // name that reads as an array index, which the language lists first.
26
39
  static asks: Record<string, AskSpec> = {};
27
40
 
28
41
  readonly stance: Stance;
@@ -32,9 +45,11 @@ export class Being {
32
45
  const C = this.constructor as typeof Being;
33
46
  for (const name of Object.keys(C.asks)) {
34
47
  if (RESERVED.has(name)) throw new Error(`ask '${name}' is a reserved name`);
35
- if (typeof (this as unknown as Record<string, unknown>)[name] !== 'function') throw new Error(`ask '${name}' has no method`);
48
+ if (!method(this, name)) throw new Error(`ask '${name}' has no method on the prototype`);
36
49
  }
37
- for (const [k, v] of Object.entries(C.cells)) if (!(k in stance.cells)) stance.cells[k] = structuredClone(v);
50
+ // Own keys only: a default named after a member of Object's prototype is
51
+ // still hers, and still missing until she writes it.
52
+ for (const [k, v] of Object.entries(C.cells)) if (!Object.hasOwn(stance.cells, k)) stance.cells[k] = structuredClone(v);
38
53
  }
39
54
 
40
55
  get cells(): Cells {
@@ -46,8 +61,8 @@ export class Being {
46
61
  get occupants(): Occupants {
47
62
  return this.stance.occupants;
48
63
  }
49
- invite(id: string): Promise<Invitation | null> {
50
- return this.stance.occupants.invite(id);
64
+ invite(id: string, notes?: JsonObject): Promise<Invitation | null> {
65
+ return this.stance.occupants.invite(id, notes);
51
66
  }
52
67
  knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer> {
53
68
  return this.stance.standings.knock(invitation, method, args, wanted);
@@ -55,13 +70,14 @@ export class Being {
55
70
  take(id: string, invitation: Invitation): Promise<string | null> {
56
71
  return this.stance.standings.take(id, invitation);
57
72
  }
58
- // A new being of her ward, by class name, under a key she chooses.
59
- boot(className: string, key: string): string | null {
60
- return this.stance.boot(className, key);
73
+ // A new being of her ward, by class name, under a key she chooses. With an
74
+ // id, she holds a standing to the being she made, who knows her by her key.
75
+ boot(className: string, key: string, id?: string): Promise<string | null> {
76
+ return this.stance.boot(className, key, id);
61
77
  }
62
78
  // The occupant record for whoever is at the door. Undefined at a public being.
63
79
  occupant(asker: Asker): OccupantRecord | undefined {
64
- return asker.id === undefined ? undefined : this.cells.occupants[asker.id];
80
+ return asker.id !== undefined && Object.hasOwn(this.cells.occupants, asker.id) ? this.cells.occupants[asker.id] : undefined;
65
81
  }
66
82
 
67
83
  // Her blueprint for this asker. Override to shape it by hand.
@@ -87,7 +103,7 @@ export class Being {
87
103
  // lookup would also find every name on Object's prototype: `valueOf`
88
104
  // would answer with her stance, `toString` with a string, and neither is
89
105
  // an ask she wrote. Only her own keys are asks, which is what describe
90
- // has always shown. What she shows is what she can be asked.
106
+ // shows. What she shows is what she can be asked.
91
107
  const spec = typeof method === 'string' && Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
92
108
  if (!spec || (spec.for && !spec.for(this.occupant(asker), asker))) return { error: 'unknown ask' };
93
109
  const fn = (this as unknown as Record<string, (args: JsonObject, asker: Asker) => Reply | Promise<Reply>>)[method];
@@ -12,17 +12,19 @@ import type { Json } from './types.ts';
12
12
  const absent = (v: unknown): boolean => v === undefined || typeof v === 'function' || typeof v === 'symbol';
13
13
 
14
14
  // JCS for I-JSON values: sorted keys, no whitespace, JSON escaping. Numbers
15
- // are serialized as ES does, which is what RFC 8785 specifies.
16
- export const canonical = (v: Json): string =>
17
- Array.isArray(v)
18
- ? `[${v.map((slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`
19
- : v !== null && typeof v === 'object'
20
- ? `{${Object.keys(v)
21
- .filter((k) => !absent(v[k]))
22
- .sort()
23
- .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
24
- .join(',')}}`
25
- : JSON.stringify(v);
15
+ // are serialized as ES does, which is what RFC 8785 specifies. A hole in a
16
+ // list is null, as JSON writes it.
17
+ export const canonical = (v: Json): string => {
18
+ if (Array.isArray(v)) return `[${Array.from(v, (slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`;
19
+ if (v !== null && typeof v === 'object') {
20
+ return `{${Object.keys(v)
21
+ .filter((k) => !absent(v[k]))
22
+ .sort()
23
+ .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
24
+ .join(',')}}`;
25
+ }
26
+ return JSON.stringify(v);
27
+ };
26
28
 
27
29
  const hex = (bytes: ArrayBuffer): string =>
28
30
  Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');