@quo-systems/quo 0.2.8 → 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,10 +1,23 @@
1
1
  // Names a subclass may not use for an ask, because they are the base's own.
2
2
  const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'constructor']);
3
+ // Whether she has a method of that name, written on her own prototype chain
4
+ // below Object's. A name Object lends every object, `hasOwnProperty` or
5
+ // `toString`, is not a method she wrote; and a field she assigns in her own
6
+ // constructor is not there yet when the base checks, so an ask is a method
7
+ // on the prototype and nothing else.
8
+ const method = (self, name) => {
9
+ for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
10
+ if (Object.hasOwn(p, name))
11
+ return typeof p[name] === 'function';
12
+ }
13
+ return false;
14
+ };
3
15
  export class Being {
4
16
  // Her cells' defaults. Merged in at birth, only where a key is missing, so
5
17
  // a restart keeps what she wrote.
6
18
  static cells = {};
7
- // What she can be asked. Declaration order is blueprint order.
19
+ // What she can be asked. Declaration order is blueprint order, except a
20
+ // name that reads as an array index, which the language lists first.
8
21
  static asks = {};
9
22
  stance;
10
23
  constructor(stance) {
@@ -13,11 +26,13 @@ export class Being {
13
26
  for (const name of Object.keys(C.asks)) {
14
27
  if (RESERVED.has(name))
15
28
  throw new Error(`ask '${name}' is a reserved name`);
16
- if (typeof this[name] !== 'function')
17
- throw new Error(`ask '${name}' has no method`);
29
+ if (!method(this, name))
30
+ throw new Error(`ask '${name}' has no method on the prototype`);
18
31
  }
32
+ // Own keys only: a default named after a member of Object's prototype is
33
+ // still hers, and still missing until she writes it.
19
34
  for (const [k, v] of Object.entries(C.cells))
20
- if (!(k in stance.cells))
35
+ if (!Object.hasOwn(stance.cells, k))
21
36
  stance.cells[k] = structuredClone(v);
22
37
  }
23
38
  get cells() {
@@ -29,8 +44,8 @@ export class Being {
29
44
  get occupants() {
30
45
  return this.stance.occupants;
31
46
  }
32
- invite(id) {
33
- return this.stance.occupants.invite(id);
47
+ invite(id, notes) {
48
+ return this.stance.occupants.invite(id, notes);
34
49
  }
35
50
  knock(invitation, method, args, wanted) {
36
51
  return this.stance.standings.knock(invitation, method, args, wanted);
@@ -38,13 +53,14 @@ export class Being {
38
53
  take(id, invitation) {
39
54
  return this.stance.standings.take(id, invitation);
40
55
  }
41
- // A new being of her ward, by class name, under a key she chooses.
42
- boot(className, key) {
43
- return this.stance.boot(className, key);
56
+ // A new being of her ward, by class name, under a key she chooses. With an
57
+ // id, she holds a standing to the being she made, who knows her by her key.
58
+ boot(className, key, id) {
59
+ return this.stance.boot(className, key, id);
44
60
  }
45
61
  // The occupant record for whoever is at the door. Undefined at a public being.
46
62
  occupant(asker) {
47
- return asker.id === undefined ? undefined : this.cells.occupants[asker.id];
63
+ return asker.id !== undefined && Object.hasOwn(this.cells.occupants, asker.id) ? this.cells.occupants[asker.id] : undefined;
48
64
  }
49
65
  // Her blueprint for this asker. Override to shape it by hand.
50
66
  describe(asker) {
@@ -72,7 +88,7 @@ export class Being {
72
88
  // lookup would also find every name on Object's prototype: `valueOf`
73
89
  // would answer with her stance, `toString` with a string, and neither is
74
90
  // an ask she wrote. Only her own keys are asks, which is what describe
75
- // has always shown. What she shows is what she can be asked.
91
+ // shows. What she shows is what she can be asked.
76
92
  const spec = typeof method === 'string' && Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
77
93
  if (!spec || (spec.for && !spec.for(this.occupant(asker), asker)))
78
94
  return { error: 'unknown ask' };
@@ -5,15 +5,19 @@
5
5
  // happened to be holding.
6
6
  const absent = (v) => v === undefined || typeof v === 'function' || typeof v === 'symbol';
7
7
  // JCS for I-JSON values: sorted keys, no whitespace, JSON escaping. Numbers
8
- // are serialized as ES does, which is what RFC 8785 specifies.
9
- export const canonical = (v) => Array.isArray(v)
10
- ? `[${v.map((slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`
11
- : v !== null && typeof v === 'object'
12
- ? `{${Object.keys(v)
8
+ // are serialized as ES does, which is what RFC 8785 specifies. A hole in a
9
+ // list is null, as JSON writes it.
10
+ export const canonical = (v) => {
11
+ if (Array.isArray(v))
12
+ return `[${Array.from(v, (slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`;
13
+ if (v !== null && typeof v === 'object') {
14
+ return `{${Object.keys(v)
13
15
  .filter((k) => !absent(v[k]))
14
16
  .sort()
15
17
  .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
16
- .join(',')}}`
17
- : JSON.stringify(v);
18
+ .join(',')}}`;
19
+ }
20
+ return JSON.stringify(v);
21
+ };
18
22
  const hex = (bytes) => Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
19
23
  export const digest = async (blueprint) => hex(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonical(blueprint))));
@@ -70,14 +70,14 @@ export type Standings = {
70
70
  readonly [id: string]: Standing | undefined;
71
71
  };
72
72
  export type Occupants = {
73
- invite(id: string): Promise<Invitation | null>;
73
+ invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
74
74
  remove(id: string): void;
75
75
  };
76
76
  export type Stance = {
77
77
  readonly cells: Cells;
78
78
  readonly occupants: Occupants;
79
79
  readonly standings: Standings;
80
- boot(className: string, key: string): string | null;
80
+ boot(className: string, key: string, id?: string): Promise<string | null>;
81
81
  };
82
82
  export interface BeingLike {
83
83
  answer(asker: Asker, method?: string, args?: JsonObject): Reply | Promise<Reply>;
@@ -22,7 +22,7 @@
22
22
  // harbors, and that is where the promise of Quo is held: a being never learns
23
23
  // where the other one is, so every answer, after every move, under every
24
24
  // topology, is the answer the model wrote without knowing the topology
25
- // either. Held forty times a run, it is the promise at a scale a scene
25
+ // either. Held after every one of sixty moves, it is the promise at a scale a scene
26
26
  // cannot reach, and it is what makes a migration honest: if no answer can
27
27
  // tell the topology apart, moving a ward between harbors cannot be felt.
28
28
  // The model itself is the script's own bookkeeping and takes nothing from a
@@ -241,8 +241,6 @@ export function estate(label, make, { canDown, test }) {
241
241
  return `${who}'s ward moves harbor`;
242
242
  });
243
243
  // weather: the host's door is gone. unreached, never silence, and the
244
- // relation is exactly where it was when it comes back.
245
- // weather: the host's door is gone. unreached, never silence, and the
246
244
  // relation is exactly where it was when it comes back. The move is on
247
245
  // the list under every topology even where there is nothing to cut,
248
246
  // because the list is the script: a move missing in one world would
@@ -12,7 +12,7 @@
12
12
  // handle.being the object, as the test drives it
13
13
  // handle.cells her cells, as the ward keeps them
14
14
  // handle.down = true | false her door is unreachable (needs canDown)
15
- // handle.minted.has(pk) every pk her side ever minted
15
+ // handle.minted.has(pk) the last eight pks her side minted
16
16
  // handle.forgeKnock(invitation) make her side knock with a key of its own, unannounced
17
17
  // world.census() every partition in the world, as values
18
18
  // world.migrate(key) her ward moves to another harbor, same seed, same pk
@@ -28,6 +28,8 @@ export declare class Harbor {
28
28
  readonly reaches: Map<string, Bound>;
29
29
  readonly down: Set<string>;
30
30
  readonly refused: Map<string, number>;
31
+ readonly faults: Map<string, number>;
32
+ readonly unbooted: string[];
31
33
  readonly classes: Record<string, BeingClass>;
32
34
  readonly fallbacks: Reach[];
33
35
  readonly announcers: Set<() => void>;
@@ -17,6 +17,7 @@
17
17
  // that arrive from the wire go to an own door or a held socket and never
18
18
  // onward by request or by fallback; that one rule is the rendezvous.
19
19
  import { Ward } from '../ward/ward.js';
20
+ import { silence } from '../being/silence.js';
20
21
  import { request } from './reach.js';
21
22
  import { openReply, wardSignPk } from '../ward/seal.js';
22
23
  import { concat, sealingPair, KEY } from '../ward/arithmetic.js';
@@ -25,6 +26,12 @@ export const DEFAULT_CODE = 'classes/index.ts';
25
26
  // How long a probe waits for the door behind a claim. A claim that answers
26
27
  // nothing in this time is not bound; the next announce is another chance.
27
28
  const PROBE = 10_000;
29
+ // How many pks one announce may claim, and how many are proven at once.
30
+ const ANNOUNCE = 64;
31
+ const PROVING = 8;
32
+ // How many times each reach has announced: a proof from an older announce
33
+ // binds nothing when it lands.
34
+ const announces = new WeakMap();
28
35
  export class Harbor {
29
36
  store;
30
37
  loader;
@@ -33,7 +40,10 @@ export class Harbor {
33
40
  reaches = new Map(); // the directory: foreign ward pk -> reach
34
41
  down = new Set(); // pks this harbor will not reach right now: weather, for a test
35
42
  refused = new Map(); // own ward pk -> arrivals its door would not admit. what a terrain does with it is its own
43
+ faults = new Map(); // ward name -> saves the store refused. what a terrain does with it is its own
44
+ unbooted = []; // the wards the store keeps that would not host this run, by name
36
45
  classes = {}; // classes handed in-process, beside the loader's
46
+ #saving = new Map();
37
47
  // The dialers' sockets, in the order they were dialed: where a pk nobody
38
48
  // here knows is sent, because the listener a harbor dialed is a rendezvous
39
49
  // and may hold that pk for someone else. It is a list because a harbor may
@@ -58,16 +68,62 @@ export class Harbor {
58
68
  this.store = store;
59
69
  this.loader = loader;
60
70
  }
61
- // Boot every ward the store keeps, and learn the hints.
71
+ // Boot every ward the store keeps, and learn the hints. One ward that will
72
+ // not host, a partition this kit cannot read or a loader that throws, is
73
+ // named in `unbooted` and takes nobody else down with it.
62
74
  async boot() {
63
75
  for (const name of await this.store.list()) {
64
- const kept = await this.store.load(name);
65
- if (kept)
66
- await this.host(name, kept);
76
+ try {
77
+ const kept = await this.store.load(name);
78
+ if (kept)
79
+ await this.host(name, kept);
80
+ }
81
+ catch {
82
+ this.unbooted.push(name);
83
+ }
67
84
  }
68
85
  for (const [pk, url] of Object.entries(await this.store.hints()))
69
86
  this.hint(pk, url);
70
87
  }
88
+ // ---- saving
89
+ // The ward wrote. Its partition is saved once the line is free, and once
90
+ // for every burst of writes, in the order the writes came.
91
+ #wrote(name) {
92
+ const s = this.#saving.get(name);
93
+ if (!s || s.gone)
94
+ return;
95
+ s.dirty = true;
96
+ if (s.timer === undefined)
97
+ s.timer = setTimeout(() => void this.#flush(name), 0);
98
+ }
99
+ async #flush(name) {
100
+ const s = this.#saving.get(name);
101
+ if (!s)
102
+ return;
103
+ if (s.timer !== undefined) {
104
+ clearTimeout(s.timer);
105
+ s.timer = undefined;
106
+ }
107
+ if (s.running)
108
+ return s.running;
109
+ s.running = (async () => {
110
+ while (s.dirty && !s.gone) {
111
+ s.dirty = false;
112
+ try {
113
+ await this.store.save(name, s.memory);
114
+ }
115
+ catch {
116
+ this.faults.set(name, (this.faults.get(name) ?? 0) + 1);
117
+ }
118
+ }
119
+ })();
120
+ try {
121
+ await s.running;
122
+ }
123
+ finally {
124
+ s.running = null;
125
+ }
126
+ }
71
127
  // ---- the directory
72
128
  // Bytes to one of this harbor's own doors, and the one bit the door says
73
129
  // beside its reply, counted.
@@ -103,8 +159,8 @@ export class Harbor {
103
159
  return bound?.held ? bound.reach.carry(pk, bytes) : undefined;
104
160
  }
105
161
  hint(pk, url) {
106
- if (this.reaches.get(pk)?.held)
107
- return; // a socket in hand beats a hint
162
+ if (this.reaches.has(pk))
163
+ return; // a reach proven at a door beats a hint, held or reached through a dialed line
108
164
  this.reaches.set(pk, { reach: request(url), held: false });
109
165
  }
110
166
  async remember(pk, url) {
@@ -119,8 +175,8 @@ export class Harbor {
119
175
  // signed by the ward key. Only the holder of that ward's seed can write
120
176
  // that reply, the lid is fresh so nothing replays, and a box that does not
121
177
  // open writes nothing at the ward. This is the one box a harbor ever opens,
122
- // the one it sealed itself, and it reads nothing from it but that the
123
- // signature is the claimed key's.
178
+ // the one it sealed itself, and it reads nothing from it but that it is
179
+ // the silence a door owes such a box, signed by the claimed key.
124
180
  async prove(pk, reach) {
125
181
  if (!/^[0-9a-f]{128}$/.test(pk))
126
182
  return false;
@@ -139,19 +195,32 @@ export class Harbor {
139
195
  }
140
196
  // Bind what the far side claims, each pk proven at its door first. Both
141
197
  // sides bind this way: the listener the dialer's claims, the dialer the
142
- // listener's, since a claim is a claim whichever end made it.
198
+ // listener's, since a claim is a claim whichever end made it. An announce
199
+ // names at most this many pks, and they are proven a few at a time: a
200
+ // side that announced a thousand would otherwise cost a thousand keys, a
201
+ // thousand frames and a thousand waits at once, on its word alone.
143
202
  async bind(pks, reach, held) {
144
203
  // An announce is the whole of what that side holds now, so a pk this
145
- // reach was bound for and no longer claims is unbound at once: a ward
204
+ // reach was bound for and does not claim now is unbound at once: a ward
146
205
  // that left a dialer is not reachable through it, proof or no proof.
206
+ // And it is the newest word: a proof still in flight from an earlier
207
+ // announce on this reach binds nothing when it lands, since the side
208
+ // has spoken again since.
209
+ const said = (announces.get(reach) ?? 0) + 1;
210
+ announces.set(reach, said);
147
211
  for (const [pk, b] of this.reaches)
148
212
  if (b.reach === reach && !pks.includes(pk))
149
213
  this.reaches.delete(pk);
150
- await Promise.all(pks.map(async (pk) => {
214
+ const claimed = [...new Set(pks)].slice(0, ANNOUNCE);
215
+ const prove = async (pk) => {
151
216
  if (this.doors.has(pk) || !(await this.prove(pk, reach)))
152
217
  return;
218
+ if (announces.get(reach) !== said)
219
+ return; // a newer announce has spoken since
153
220
  this.reaches.set(pk, { reach, held });
154
- }));
221
+ };
222
+ for (let i = 0; i < claimed.length; i += PROVING)
223
+ await Promise.all(claimed.slice(i, i + PROVING).map(prove));
155
224
  }
156
225
  unbind(reach) {
157
226
  for (const [pk, b] of this.reaches)
@@ -168,12 +237,16 @@ export class Harbor {
168
237
  return hosted;
169
238
  }
170
239
  // Stop serving a ward and take it out of the store: what was kept comes
171
- // back, for another harbor to put. The partition is written first, because
172
- // a being driven in-process changes it without passing a door.
240
+ // back, for another harbor to put. The partition is written first, and
241
+ // never again under this name: whoever still holds the old pointers holds
242
+ // a ward that saves nothing and answers its owner silence.
173
243
  async drop(name) {
174
244
  const h = this.wards.get(name);
175
245
  if (h) {
176
246
  await h.save();
247
+ const s = this.#saving.get(name);
248
+ if (s)
249
+ s.gone = true;
177
250
  this.wards.delete(name);
178
251
  this.doors.delete(h.pk);
179
252
  this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
@@ -189,12 +262,13 @@ export class Harbor {
189
262
  async host(name, kept) {
190
263
  const { seed, partition: memory } = kept;
191
264
  const classes = await this.loader(kept.record);
192
- const objects = new Map(); // cells -> the object instantiate made. a side's hand, never the ward's
265
+ const objects = new WeakMap(); // cells -> the object instantiate made. a side's hand, never the ward's; it follows the cells out when she is unbooted
193
266
  const ground = {
194
267
  seed,
195
268
  memory,
196
269
  instantiate: (className, stance) => {
197
- const C = classes[className] ?? this.classes[className];
270
+ // Own keys only: `constructor` is a name Object lends every registry.
271
+ const C = Object.hasOwn(classes, className) ? classes[className] : Object.hasOwn(this.classes, className) ? this.classes[className] : undefined;
198
272
  if (!C)
199
273
  return null;
200
274
  const obj = new C(stance);
@@ -203,19 +277,34 @@ export class Harbor {
203
277
  },
204
278
  carry: (pk, bytes) => this.carry(pk, new Uint8Array(bytes)),
205
279
  random: (n) => globalThis.crypto.getRandomValues(new Uint8Array(n)),
280
+ wrote: () => this.#wrote(name),
206
281
  };
282
+ const saving = { memory, dirty: false, running: null, timer: undefined, gone: false };
283
+ this.#saving.set(name, saving);
207
284
  const w = await Ward(ground);
208
- const save = () => this.store.save(name, memory);
209
- const after = (f) => async (...a) => {
210
- try {
211
- return await f(...a);
212
- }
213
- finally {
285
+ // A save the caller may wait for: what the ward wrote so far is in the
286
+ // store when this resolves, or the fault is counted. After a call through
287
+ // a pointer the store is current, so a restart right after it finds
288
+ // everything; a refusal at the door wrote nothing and saves nothing.
289
+ const save = async () => {
290
+ if (saving.gone)
291
+ return;
292
+ saving.dirty = true;
293
+ await this.#flush(name);
294
+ };
295
+ const door = async (bytes) => {
296
+ const r = await w.door(bytes);
297
+ if (r.heard)
214
298
  await save();
215
- }
299
+ return r;
300
+ };
301
+ const ask = async (method, args) => {
302
+ if (saving.gone)
303
+ return silence;
304
+ const out = await w.ask(method, args);
305
+ await save();
306
+ return out;
216
307
  };
217
- const door = after(w.door);
218
- const ask = after(w.ask);
219
308
  const bp = (await w.ask()); // learned by asking, as anyone learns anything
220
309
  const beings = memory.beings;
221
310
  const being = (key) => {
@@ -1,12 +1,12 @@
1
1
  import { REFUSED, Socket } from './reach.js';
2
2
  export function dial(harbor, url) {
3
3
  const d = { url, socket: null, wake: () => { }, close: () => { } };
4
- let stopped = false, wait = 1000, timer;
4
+ let stopped = false, wait = 1000, timer, line; // the line in hand, open or still opening: close() closes it either way
5
5
  const connect = () => {
6
6
  timer = undefined; // no wait pending: this is the dial it was waiting for
7
7
  if (stopped)
8
8
  return;
9
- const line = new WebSocket(url.replace(/\/$/, '').replace(/^http/, 'ws'));
9
+ line = new WebSocket(url.replace(/\/$/, '').replace(/^http/, 'ws'));
10
10
  const s = new Socket(line, (pk, bytes) => harbor.deliver(pk, bytes), (far) => {
11
11
  // A line that opened is not a line that works. The wait goes back to
12
12
  // a second here, where the far side has answered and been taken, and
@@ -37,6 +37,8 @@ export function dial(harbor, url) {
37
37
  // the line already in hand, and a dropped one stops being claimed at once.
38
38
  const say = () => s.announce([...harbor.doors.keys()]);
39
39
  line.addEventListener('open', () => {
40
+ if (stopped)
41
+ return s.close(); // closed while it was opening: it opens into nothing
40
42
  d.socket = s;
41
43
  harbor.announcers.add(say);
42
44
  say();
@@ -57,7 +59,7 @@ export function dial(harbor, url) {
57
59
  d.close = () => {
58
60
  stopped = true;
59
61
  clearTimeout(timer);
60
- d.socket?.close();
62
+ line?.close(); // open or opening: a line closed while connecting fires close and never open
61
63
  };
62
64
  connect();
63
65
  return d;
@@ -16,7 +16,7 @@ export declare class MemoryHarbor {
16
16
  cut: boolean;
17
17
  readonly partitions: Map<string, Record<string, unknown>>;
18
18
  readonly wards: Map<string, Booted>;
19
- readonly objects: Map<object, unknown>;
19
+ readonly objects: WeakMap<object, unknown>;
20
20
  route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
21
21
  link(harbor: MemoryHarbor): void;
22
22
  boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>): Promise<Booted>;
@@ -8,7 +8,7 @@ export class MemoryHarbor {
8
8
  cut = false;
9
9
  partitions = new Map(); // seed -> memory. the harbor keeps it and reads nothing
10
10
  wards = new Map(); // seed -> pointers
11
- objects = new Map(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's
11
+ objects = new WeakMap(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
12
12
  // the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
13
13
  async route(farPk, bytes) {
14
14
  if (this.down.has(farPk))
@@ -43,7 +43,8 @@ export class MemoryHarbor {
43
43
  seed,
44
44
  memory,
45
45
  instantiate: (name, stance) => {
46
- const C = classes[name];
46
+ // Own keys only: `constructor` is a name Object lends every registry.
47
+ const C = Object.hasOwn(classes, name) ? classes[name] : undefined;
47
48
  if (!C)
48
49
  return null;
49
50
  const obj = new C(stance);
@@ -1,9 +1,9 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Reach: carrying bytes to a pk off the device. One interface, two
3
- // implementations, as quo-harbor.md names them:
3
+ // implementations:
4
4
  //
5
- // request a URL, the quo. route of a world. POST the bytes to
6
- // `<url>/<pk>`, get bytes back. Listener to listener.
5
+ // request a URL. POST the bytes to `<url>/<pk>`, get bytes back.
6
+ // Listener to listener.
7
7
  // socket a held WebSocket at `<url>`, opened by whichever side can
8
8
  // dial, used in both directions with a frame id. Each side
9
9
  // announces the ward pks it holds when the socket opens, and the
@@ -34,8 +34,12 @@ export function request(url) {
34
34
  catch {
35
35
  return undefined; // the connection never opened: nothing was delivered
36
36
  }
37
- if (res.status === 404)
38
- return undefined; // the listener holds no reach for that pk
37
+ // A status that says the listener did not take the bytes is nothing
38
+ // delivered: no reach for that pk, a request it refused on its face, a
39
+ // gateway with nobody behind it. Any other failure is a listener that
40
+ // took them and failed after, which is sent and no word since.
41
+ if (res.status === 404 || (res.status >= 400 && res.status < 500) || res.status === 502 || res.status === 503 || res.status === 504)
42
+ return undefined;
39
43
  if (!res.ok)
40
44
  return never();
41
45
  return new Uint8Array(await res.arrayBuffer());
@@ -68,6 +72,11 @@ export const SUITE = 1;
68
72
  // the refusal names itself on the way out, and the far side may read it.
69
73
  export const REFUSED = 4001;
70
74
  const OPEN = 1; // WebSocket.OPEN, on every terrain
75
+ // How many asks a socket keeps a resolver for. An ask the far side never
76
+ // answers is ended by the ward's bound, and its resolver stays until the
77
+ // line closes; a far side that answers nothing for long enough would grow
78
+ // the map for as long as this side asks, so the oldest goes when it is full.
79
+ const PENDING = 4096;
71
80
  // One held socket, either side of it. `deliver` is what this side does with
72
81
  // an ask that arrives for a pk: its own door, or a socket it holds for that
73
82
  // pk, and nothing else. `onAnnounce` learns the far side's pks; `onClose`
@@ -81,7 +90,17 @@ export class Socket {
81
90
  constructor(line, deliver, onAnnounce, onClose) {
82
91
  this.line = line;
83
92
  line.binaryType = 'arraybuffer';
84
- this.#open = line.readyState === OPEN ? Promise.resolve() : new Promise((ok, no) => (line.addEventListener('open', ok), line.addEventListener('error', no)));
93
+ // A line that opens is a reach; one that errors, or closes before it
94
+ // opened, is a reach that failed, and an ask waiting on the handshake is
95
+ // nothing delivered.
96
+ this.#open =
97
+ line.readyState === OPEN
98
+ ? Promise.resolve()
99
+ : new Promise((ok, no) => {
100
+ line.addEventListener('open', ok);
101
+ line.addEventListener('error', no);
102
+ line.addEventListener('close', () => no(new Error('closed before it opened')));
103
+ });
85
104
  this.#open.catch(() => { }); // a handshake refused is a reach that failed, not a process that dies
86
105
  line.addEventListener('message', (e) => {
87
106
  if (typeof e.data === 'string') {
@@ -109,7 +128,11 @@ export class Socket {
109
128
  const kind = buf[0], id = new DataView(buf.buffer, buf.byteOffset, buf.byteLength).getUint32(1);
110
129
  if (kind === ASK) {
111
130
  const pk = hex(buf.subarray(5, 69));
112
- void deliver(pk, new Uint8Array(buf.subarray(69))).then((back) => {
131
+ // A deliver that rejects has delivered nothing this side can vouch
132
+ // for, and a listener does not die of it: the far side hears none.
133
+ void deliver(pk, new Uint8Array(buf.subarray(69)))
134
+ .then((back) => back, () => undefined)
135
+ .then((back) => {
113
136
  const head = new Uint8Array(5);
114
137
  head[0] = back === undefined ? NONE : REPLY;
115
138
  new DataView(head.buffer).setUint32(1, id);
@@ -150,6 +173,8 @@ export class Socket {
150
173
  new DataView(head.buffer).setUint32(1, id);
151
174
  head.set(unhex(pk), 5);
152
175
  return new Promise((ok) => {
176
+ if (this.pending.size >= PENDING)
177
+ this.pending.delete(this.pending.keys().next().value); // the oldest, which the ward's bound ended long ago
153
178
  this.pending.set(id, ok);
154
179
  this.line.send(concat(head, bytes));
155
180
  });
@@ -5,7 +5,7 @@
5
5
  // is held to the ward's ceiling. The door reads it before anything is done
6
6
  // under it and refuses a budget already gone, as one silence like every other
7
7
  // refusal there. The sender bounds the whole of her ask to the same number,
8
- // and a wait that ran out is silence, never unreached.
8
+ // and a wait that ran out is the word late, never silence and never unreached.
9
9
  //
10
10
  // Each ask is bounded on its own. The time an arriving call has left does not
11
11
  // bound the asks a being makes while answering it: attributing her onward ask
@@ -24,7 +24,13 @@ export const CEILING = { time: 300_000 };
24
24
  // is thirty seconds, silently, because the ceiling is not hers to know.
25
25
  // Asking for nothing at all is the default, which is the whole point.
26
26
  export function allow(wanted, ceiling = CEILING, base = DEFAULT) {
27
- const whole = (n, fallback) => (typeof n === 'number' && Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback);
27
+ // Floored first, then tested: a fraction of a millisecond is not a budget,
28
+ // and falls to the default like every other number that is not a positive
29
+ // whole one.
30
+ const whole = (n, fallback) => {
31
+ const w = typeof n === 'number' ? Math.floor(n) : NaN;
32
+ return Number.isFinite(w) && w > 0 ? w : fallback;
33
+ };
28
34
  return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
29
35
  }
30
36
  // Whether a budget has anything left to spend. A door reads this on arrival.
@@ -8,7 +8,6 @@
8
8
  // runs on. `test/floor.test.ts` names the floor and probes for it. Subtle is
9
9
  // asynchronous, so everything here is.
10
10
  //
11
- // Ported from an earlier kit's arithmetic. Same bytes, same vectors.
12
11
  // `crypto.subtle` is read at every use and never captured at load. A browser
13
12
  // on a plain http:// origin has `crypto` without `subtle`, and a terrain may
14
13
  // install one after this module is first imported; a reference taken here
@@ -60,7 +59,12 @@ export function sameBytes(a, b) {
60
59
  diff |= a[at] ^ b[at];
61
60
  return diff === 0;
62
61
  }
63
- // The eight small-order points. A public key among them verifies nothing.
62
+ // The eight small-order points of Ed25519, in their canonical encoding. A
63
+ // public key among them verifies nothing. An encoding whose y coordinate is
64
+ // not reduced, y at or above the field's prime p, names one of the same
65
+ // points under another spelling, and a terrain's verify may accept a zero
66
+ // signature under it; the field has room for the spelling, so it is refused
67
+ // before the list is read.
64
68
  const SMALL_ORDER = [
65
69
  '0100000000000000000000000000000000000000000000000000000000000000',
66
70
  'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
@@ -71,7 +75,11 @@ const SMALL_ORDER = [
71
75
  '26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85',
72
76
  'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
73
77
  ].map(unhex);
74
- export const smallOrder = (pk) => SMALL_ORDER.some((p) => sameBytes(p, pk));
78
+ // y >= p, read little-endian with the sign bit masked off: the top byte is
79
+ // 0x7f under the mask, every middle byte is 0xff, and the low byte is at
80
+ // least 0xed, which is p's.
81
+ const unreduced = (pk) => (pk[31] & 0x7f) === 0x7f && pk[0] >= 0xed && pk.subarray(1, 31).every((b) => b === 0xff);
82
+ export const smallOrder = (pk) => unreduced(pk) || SMALL_ORDER.some((p) => sameBytes(p, pk));
75
83
  const key32 = (value, what) => {
76
84
  if (!(value instanceof Uint8Array) || value.length !== KEY)
77
85
  throw new Error(`${what} is not a 32-byte key`);
@@ -1,3 +1,5 @@
1
1
  import type { Cells } from '../being/types.ts';
2
+ export declare const DEPTH = 64;
2
3
  export declare const cellFault: (v: unknown, path: string) => string | null;
3
- export declare const guardCells: (cells: Cells) => Cells;
4
+ export declare const guardCells: (cells: Cells, wrote?: () => void) => Cells;
5
+ export declare const unguarded: (cells: Cells) => Cells;