@quo-systems/quo 0.2.9 → 0.2.11

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 (78) hide show
  1. package/README.md +19 -10
  2. package/SPEC.md +301 -110
  3. package/dist/being/being.d.ts +2 -2
  4. package/dist/being/being.js +34 -12
  5. package/dist/being/digest.js +26 -8
  6. package/dist/being/index.d.ts +2 -2
  7. package/dist/being/index.js +2 -2
  8. package/dist/being/silence.d.ts +2 -0
  9. package/dist/being/silence.js +12 -0
  10. package/dist/being/types.d.ts +4 -2
  11. package/dist/being/types.js +25 -0
  12. package/dist/conformance/assert.js +40 -6
  13. package/dist/conformance/beings.d.ts +48 -5
  14. package/dist/conformance/beings.js +39 -8
  15. package/dist/conformance/estate.js +110 -21
  16. package/dist/conformance/index.d.ts +5 -2
  17. package/dist/conformance/index.js +165 -7
  18. package/dist/harbor/core.d.ts +5 -2
  19. package/dist/harbor/core.js +195 -45
  20. package/dist/harbor/dial.js +32 -15
  21. package/dist/harbor/index.d.ts +1 -0
  22. package/dist/harbor/index.js +3 -0
  23. package/dist/harbor/memory.d.ts +3 -3
  24. package/dist/harbor/memory.js +7 -12
  25. package/dist/harbor/reach.js +42 -17
  26. package/dist/ward/allowance.js +15 -4
  27. package/dist/ward/arithmetic.d.ts +1 -0
  28. package/dist/ward/arithmetic.js +22 -6
  29. package/dist/ward/cells.d.ts +3 -1
  30. package/dist/ward/cells.js +79 -21
  31. package/dist/ward/door.d.ts +3 -2
  32. package/dist/ward/door.js +38 -10
  33. package/dist/ward/ground.d.ts +5 -1
  34. package/dist/ward/ground.js +38 -1
  35. package/dist/ward/heirs.d.ts +2 -3
  36. package/dist/ward/heirs.js +19 -13
  37. package/dist/ward/index.d.ts +1 -0
  38. package/dist/ward/index.js +3 -0
  39. package/dist/ward/owner.d.ts +6 -27
  40. package/dist/ward/owner.js +59 -33
  41. package/dist/ward/partition.d.ts +3 -0
  42. package/dist/ward/partition.js +109 -4
  43. package/dist/ward/seal.d.ts +1 -0
  44. package/dist/ward/seal.js +41 -13
  45. package/dist/ward/stance.d.ts +7 -3
  46. package/dist/ward/stance.js +156 -66
  47. package/dist/ward/ward.d.ts +10 -0
  48. package/dist/ward/ward.js +123 -51
  49. package/package.json +4 -2
  50. package/src/being/being.ts +33 -11
  51. package/src/being/digest.ts +28 -13
  52. package/src/being/index.ts +2 -2
  53. package/src/being/silence.ts +14 -0
  54. package/src/being/types.ts +39 -5
  55. package/src/conformance/assert.ts +37 -4
  56. package/src/conformance/beings.ts +41 -10
  57. package/src/conformance/estate.ts +107 -20
  58. package/src/conformance/index.ts +188 -13
  59. package/src/harbor/core.ts +203 -46
  60. package/src/harbor/dial.ts +46 -17
  61. package/src/harbor/index.ts +3 -0
  62. package/src/harbor/memory.ts +8 -13
  63. package/src/harbor/reach.ts +47 -21
  64. package/src/ward/allowance.ts +15 -4
  65. package/src/ward/arithmetic.ts +25 -8
  66. package/src/ward/cells.ts +76 -25
  67. package/src/ward/door.ts +38 -12
  68. package/src/ward/ground.ts +52 -3
  69. package/src/ward/heirs.ts +19 -13
  70. package/src/ward/index.ts +3 -0
  71. package/src/ward/owner.ts +65 -46
  72. package/src/ward/partition.ts +109 -5
  73. package/src/ward/seal.ts +41 -12
  74. package/src/ward/stance.ts +163 -64
  75. package/src/ward/ward.ts +124 -52
  76. package/vectors/arithmetic.json +7 -0
  77. package/vectors/framing.json +30 -15
  78. package/vectors/wire.json +4 -4
@@ -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
- import { RESERVED_IDS } from '../being/types.js';
7
- import { at, put, drop } from './partition.js';
6
+ import { RESERVED_IDS, isBlueprint } from '../being/types.js';
7
+ import { at, put, drop, prune } from './partition.js';
8
8
  import { allow, within, LATE } from './allowance.js';
9
- import { isWardPk } from './seal.js';
9
+ import { isHex, 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
@@ -28,7 +29,7 @@ export function buildStance(inside, key, cells, bind) {
28
29
  return false;
29
30
  if (i.heir === undefined)
30
31
  return i.secret === undefined;
31
- return typeof i.heir === 'string' && typeof i.secret === 'string' && /^[0-9a-f]{64}$/.test(i.heir) && /^[0-9a-f]{64}$/.test(i.secret);
32
+ return isHex(i.heir) && isHex(i.secret);
32
33
  };
33
34
  // One relation, one ask at a time. The rotation is a conversation: a send
34
35
  // reads the key that speaks now, announces the next, and moves to it once
@@ -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,95 @@ 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
78
- if (rec && method !== undefined && r.seen !== null)
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
+ // The ask is bounded twice with one budget, once around the lane and once
86
+ // around the wire, and the two bells are microseconds apart. A reply that
87
+ // lands between them is a reply she was already told was late: its keys are
88
+ // rotated, since the far door rotated too and nothing may un-rotate that,
89
+ // but nothing of it is written into her cells. What she was handed is the
90
+ // last word on what she saw.
91
+ const send = async (keys, method, args, wanted, rec, live = () => true) => {
92
+ const r = read(await inside.send(bind, keys, method, args, wanted));
93
+ if (rec && method !== undefined && r.seen !== null && live())
79
94
  rec.seen = r.seen; // the digest rode along
80
- return r.object;
95
+ return r.answer;
81
96
  };
82
- const knocking = async (inv, method, args, wanted) => {
97
+ const knocking = async (inv, method, args, wanted, live = () => true) => {
83
98
  const name = nameOf(inv);
99
+ // Taken while this knock waited its turn: the relation lives in the
100
+ // standing now and speaks on its lane, under its keys and its count.
101
+ const taken = takenAs(name);
102
+ if (taken !== undefined) {
103
+ const keys = at(bind.standings, taken);
104
+ if (!keys)
105
+ return word('dropped');
106
+ return lane(`ask:${taken}`, () => asking(taken, keys, at(cells.standings, taken), method, args, wanted, live));
107
+ }
84
108
  // her own key for this relation, minted once: rotation two. reused if she knocks again.
85
109
  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.
110
+ prune(bind.knocks, bind.answered); // this one is the newest, so it stays and the oldest never answered goes
111
+ // The first knock on a heir speaks as the heir, the key the inviter
112
+ // handed out, and announces her own. Once the door has honoured it the
113
+ // heir is spent, whatever she then said, and her own key speaks; a heir
114
+ // spent once stays spent.
115
+ //
116
+ // She sent once as the heir and the door did not speak: nothing came
117
+ // back, or bytes that were not Quo's, or the wait ran out. Either the
118
+ // door heard, spent the heir and rotated to the key she announced, or
119
+ // nothing arrived and the heir still stands. Only the far door knows,
120
+ // and it will not say.
91
121
  //
92
122
  // 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.
123
+ // heard, that key is the one it admits, and she is answered, or told the
124
+ // door's word. If it did not, the key means nothing there, the ask is
125
+ // refused, and a refusal at that door writes nothing: the heir is
126
+ // untouched and still speaks. Then she knocks as the heir, as she would
127
+ // have. One extra round trip in the one case where a reply was lost, and
128
+ // the relation is not stranded.
98
129
  //
99
130
  // No stranger gains anything by it: whoever holds the invitation could
100
131
  // always knock as the heir, and her own key is admitted only where the
101
132
  // door already bound it to her.
102
133
  if (inv.heir !== undefined && !k.spoke && k.sent) {
103
134
  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);
135
+ const r = read(await inside.send(bind, own, method, args, wanted));
105
136
  k.seq = own.seq;
106
137
  k.current = own.current;
107
138
  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;
139
+ inside.wrote();
140
+ if (!r.door) {
141
+ // Nothing the door said. Unreached and late are hers to hear now; bytes
142
+ // that were not Quo's leave her where she was, and the heir is tried.
143
+ if (!isSilence(r.answer))
144
+ return r.answer;
145
+ }
146
+ else if (!isSilence(r.answer)) {
147
+ // The door spoke to her own key, so it heard the first knock: this is
148
+ // the answer, or the door's word for her. Its silence alone cannot be
149
+ // told from a refusal, so silence is followed by the knock as the heir,
150
+ // which the door then refuses if it had heard, and she hears silence.
151
+ k.spoke = true;
152
+ if (answered(r))
153
+ bind.answered[name] = true;
154
+ return r.answer;
114
155
  }
115
- // Refused. The door never heard her, so the heir is still hers to spend.
116
156
  }
117
157
  const first = inv.heir !== undefined && !k.spoke;
118
158
  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);
159
+ const r = read(await inside.send(bind, keys, method, args, wanted));
120
160
  k.sent = true; // bytes went out. whether they arrived is the far door's to know.
121
161
  // The number she spoke under, and the key she announced, kept whoever
122
162
  // answered and kept when nobody did. A reply lost on the way back is a
@@ -127,28 +167,28 @@ export function buildStance(inside, key, cells, bind) {
127
167
  k.current = keys.current;
128
168
  k.next = keys.next;
129
169
  }
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;
170
+ if (honoured(r))
171
+ k.spoke = true;
172
+ if (answered(r))
173
+ bind.answered[name] = true;
174
+ inside.wrote();
175
+ return r.answer;
144
176
  };
145
177
  // The id she took this relation under, if she took it. One relation is one
146
178
  // ward and one heir whichever door it is reached by, so the standing and the
147
179
  // invitation answer to the same name.
148
180
  const keyName = (k) => (k.heir === null ? `public:${k.ward}` : `${k.ward}:${k.heir}`);
149
181
  const takenAs = (name) => Object.keys(bind.standings).find((id) => keyName(bind.standings[id]) === name);
182
+ // A being the ward has taken out reaches nothing through the stance she
183
+ // still holds. Her relations went with her, so every one of them reads as
184
+ // dropped, and nothing she asks for is made: a heir written here would name
185
+ // a being no door holds, and an id minted here would be handed to whoever
186
+ // is booted under her key next.
187
+ const gone = () => !inside.live();
150
188
  const calls = {
151
189
  knock: async (inv, method, args = {}, wanted) => {
190
+ if (gone())
191
+ return word('dropped');
152
192
  if (!valid(inv))
153
193
  return word('invitation'); // S1. nothing is sent
154
194
  // She may knock again, and after take that knock is an ask: the relation
@@ -157,8 +197,8 @@ export function buildStance(inside, key, cells, bind) {
157
197
  // same relation, and the two would refuse each other.
158
198
  const id = takenAs(nameOf(inv));
159
199
  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')))));
161
- return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(word('late')))));
200
+ return standingAt(id).ask(method, args, wanted);
201
+ return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted, live) : Promise.resolve(word('late')))));
162
202
  },
163
203
  // Take consumes. Until now the relation lived in the knock record, under
164
204
  // the ward and the heir; from now it lives in the standing, under her id.
@@ -178,7 +218,7 @@ export function buildStance(inside, key, cells, bind) {
178
218
  // ask on it is refused. The relation she took correctly would be dead
179
219
  // because a second knock on the same invitation was careless.
180
220
  take: async (id, inv) => {
181
- if (!valid(inv))
221
+ if (gone() || !valid(inv))
182
222
  return null;
183
223
  const name = nameOf(inv);
184
224
  return lane(`knock:${name}`, () => {
@@ -194,12 +234,16 @@ export function buildStance(inside, key, cells, bind) {
194
234
  put(cells.standings, id, { id, digest: null, blueprint: null, seen: null });
195
235
  delete bind.knocks[name];
196
236
  delete bind.answered[name];
237
+ inside.wrote();
197
238
  return Promise.resolve(id);
198
239
  });
199
240
  },
200
241
  remove: (id) => {
242
+ if (gone())
243
+ return;
201
244
  drop(cells.standings, id);
202
245
  drop(bind.standings, id);
246
+ inside.wrote();
203
247
  },
204
248
  };
205
249
  const standingAt = (id) => ({
@@ -208,20 +252,33 @@ export function buildStance(inside, key, cells, bind) {
208
252
  // issued while the standing stood is answered even if she drops it in
209
253
  // the next line. Waiting for the lane is not a reason to lose it.
210
254
  ask: (method, args = {}, wanted) => {
255
+ if (gone())
256
+ return Promise.resolve(word('dropped'));
211
257
  const keys = at(bind.standings, id);
212
258
  if (!keys)
213
259
  return Promise.resolve(word('dropped')); // S2. she dropped it between one line and the next
214
260
  const rec = at(cells.standings, id);
215
- return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted) : Promise.resolve(word('late')))));
261
+ return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted, live) : Promise.resolve(word('late')))));
216
262
  },
217
263
  });
218
- const asking = async (id, keys, rec, method, args, wanted) => {
219
- const out = await send(keys, method, args, wanted, rec);
264
+ // The record an ask writes into is the one it was issued on. A standing
265
+ // dropped and re-taken under the same id while the ask was in flight is
266
+ // another relation, and the blueprint that came back is not hers.
267
+ const asking = async (id, keys, rec, method, args, wanted, live = () => true) => {
268
+ const out = await send(keys, method, args, wanted, rec, live);
220
269
  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);
270
+ // A blueprint her cells cannot keep, nested past the bound or carrying
271
+ // a key no harbor writes, is not Quo's bytes: silence, and nothing written.
272
+ if (cellFault(out, 'blueprint') !== null)
273
+ return silence;
274
+ if (rec && live() && at(cells.standings, id) === rec) {
275
+ // Read as a blueprint before it is kept as one. A far describe is
276
+ // somebody else's code and may answer anything; what is not a
277
+ // blueprint is not written as one, and she holds no blueprint for
278
+ // that relation rather than one a side would walk and break on. The
279
+ // answer itself is hers either way: it is returned below, unread.
280
+ rec.blueprint = isBlueprint(out) ? out : null;
281
+ rec.digest = rec.seen = await digest(out);
225
282
  }
226
283
  }
227
284
  return out;
@@ -236,28 +293,61 @@ export function buildStance(inside, key, cells, bind) {
236
293
  cells,
237
294
  // A being may make. The ward refuses what it refuses the owner: a key
238
295
  // 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),
296
+ // leaves the partition as it was. Reaching into another is still the
297
+ // owner's alone: what a maker may have is a relation to what she made,
298
+ // by naming an id, and it is made the way every relation is. The being
299
+ // made mints an occupant for her maker under the maker's own key, so she
300
+ // knows who made her by that name and by nothing else; her maker knocks,
301
+ // takes it under the id she gave, and holds one standing like any other.
302
+ // Nothing half-lives: a relation that could not be made unmakes the
303
+ // being, who is a moment old and known to nobody.
304
+ boot: async (className, made, id) => {
305
+ if (gone())
306
+ return null;
307
+ if (id !== undefined && (reserved(id) || at(cells.standings, id) || at(cells.occupants, id)))
308
+ return null;
309
+ const born = inside.instantiate(className, made);
310
+ if (born === null || id === undefined)
311
+ return born;
312
+ const inv = await inside.relate(born, key);
313
+ if (inv) {
314
+ const out = await calls.knock(inv);
315
+ if (!isSilence(out) && !isWord(out) && (await calls.take(id, inv)) === id)
316
+ return born;
317
+ }
318
+ inside.unmake(born);
319
+ return null;
320
+ },
243
321
  occupants: {
244
322
  // rotation one: the ward mints the occupant's first key and gives the secret away.
245
323
  // it keeps the pk beside the id and nothing else. the invitation IS the key.
246
- invite: async (id) => {
247
- if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
324
+ invite: async (id, notes) => {
325
+ if (gone() || reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
248
326
  return null;
249
- put(cells.occupants, id, { id, notes: {} });
327
+ // The key first, and nothing written until it exists: a record put
328
+ // before the mint would be a record a remove in the meantime drops
329
+ // with no heir to close, and the heir opened after it would name an
330
+ // id nobody holds, for a later invite under that id to resurrect.
250
331
  const k = await inside.mintKey(bind);
332
+ if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
333
+ return null; // taken while the key was minted
334
+ // The notes are the terms the inviter minted under, hers to read on her
335
+ // gate. She may write more later; nobody outside ever writes them.
336
+ put(cells.occupants, id, { id, notes: notes ? { ...notes } : {} });
251
337
  inside.openHeir(k.pk, key, id);
252
338
  put(bind.occupants, id, k.pk);
339
+ inside.wrote();
253
340
  return { ward: inside.pk, heir: k.pk, secret: k.seed };
254
341
  },
255
342
  remove: (id) => {
343
+ if (gone())
344
+ return;
256
345
  drop(cells.occupants, id);
257
346
  const heir = at(bind.occupants, id);
258
347
  drop(bind.occupants, id);
259
348
  if (heir)
260
349
  inside.closeHeir(heir); // the heir dies with the id
350
+ inside.wrote();
261
351
  },
262
352
  },
263
353
  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 Resident = 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>;