@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
@@ -17,15 +17,38 @@
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.ts';
20
- import type { Ground, WardPointers } from '../ward/ground.ts';
20
+ import { maker, entropy as random, learnPk, type Ground, type WardPointers } from '../ward/ground.ts';
21
21
  import type { BeingClass, BeingLike } from '../being/types.ts';
22
+ import { silence } from '../being/silence.ts';
22
23
  import { request, type Reach } from './reach.ts';
23
- import { openReply, wardSignPk } from '../ward/seal.ts';
24
+ import { isWardPk, openReply, wardSignPk } from '../ward/seal.ts';
24
25
  import { concat, sealingPair, KEY } from '../ward/arithmetic.ts';
25
26
  import { within, LATE } from '../ward/allowance.ts';
26
27
  import type { Kept, Store, WardRecord } from './store.ts';
27
28
 
28
- export type Hosted = WardPointers & { pk: string; name: string; record: WardRecord; partition: Record<string, unknown>; being(key: string): BeingLike | undefined; save(): Promise<void> };
29
+ // What a harbor hands its own device for one ward it serves. The two
30
+ // pointers, and beside them what the device needs to be a device: the pk it
31
+ // routes by, the name it is kept under, the record, a save the caller may
32
+ // wait for.
33
+ //
34
+ // And the beings the harbor made. `instantiate` is in the ground, so the
35
+ // harbor is the one that constructs every being of every ward it serves: it
36
+ // holds those objects because it made them, and `being` and `keys` say what
37
+ // it made. This is not a third path to a being. It is the device's own code
38
+ // reaching its own object in its own process, the same reach a being has on
39
+ // one she booted herself, and nothing of it crosses an edge. What a harbor
40
+ // never does is read the partition to decide anything, and the partition is
41
+ // not here: it holds every seed the ward has, and a side that wants to know
42
+ // which beings there are or which one is public asks the ward, which is
43
+ // what the ask pointer is for.
44
+ export type Hosted = WardPointers & { pk: string; name: string; record: WardRecord; being(key: string): BeingLike | undefined; keys(): string[]; save(): Promise<void> };
45
+
46
+ // One ward's saves, in a line: the ward says it wrote, the harbor writes the
47
+ // partition once the line is free, and never twice at once, so an older
48
+ // snapshot cannot land after a newer one. A save that fails is the harbor's
49
+ // to count and never a door's to answer: the door answered bytes, and a
50
+ // disk that is full is not a reason the far side may hear.
51
+ type Saving = { memory: Record<string, unknown>; dirty: boolean; running: Promise<void> | null; timer: ReturnType<typeof setTimeout> | undefined; gone: boolean };
29
52
  // A reach in the directory, and whether this harbor holds it as a socket a
30
53
  // dialer opened: only those, and its own doors, take bytes from the wire.
31
54
  export type Bound = { reach: Reach; held: boolean };
@@ -34,9 +57,18 @@ export type Bound = { reach: Reach; held: boolean };
34
57
  export type Loader = (record: WardRecord) => Promise<Record<string, BeingClass>>;
35
58
 
36
59
  export const DEFAULT_CODE = 'classes/index.ts';
60
+ // The device's entropy, spelled once. A harbor draws it for a ward's seed at
61
+ // birth, for the lid of every probe it sends, and it hands the same function
62
+ // to every ward it boots as the ground's `random`.
37
63
  // How long a probe waits for the door behind a claim. A claim that answers
38
64
  // nothing in this time is not bound; the next announce is another chance.
39
65
  const PROBE = 10_000;
66
+ // How many pks one announce may claim, and how many are proven at once.
67
+ const ANNOUNCE = 64;
68
+ const PROVING = 8;
69
+ // How many times each reach has announced: a proof from an older announce
70
+ // binds nothing when it lands.
71
+ const announces = new WeakMap<Reach, number>();
40
72
 
41
73
  export class Harbor {
42
74
  readonly store: Store;
@@ -46,7 +78,10 @@ export class Harbor {
46
78
  readonly reaches = new Map<string, Bound>(); // the directory: foreign ward pk -> reach
47
79
  readonly down = new Set<string>(); // pks this harbor will not reach right now: weather, for a test
48
80
  readonly refused = new Map<string, number>(); // own ward pk -> arrivals its door would not admit. what a terrain does with it is its own
81
+ readonly faults = new Map<string, number>(); // ward name -> saves the store refused. what a terrain does with it is its own
82
+ readonly unbooted: string[] = []; // the wards the store keeps that would not host this run, by name
49
83
  readonly classes: Record<string, BeingClass> = {}; // classes handed in-process, beside the loader's
84
+ readonly #saving = new Map<string, Saving>();
50
85
  // The dialers' sockets, in the order they were dialed: where a pk nobody
51
86
  // here knows is sent, because the listener a harbor dialed is a rendezvous
52
87
  // and may hold that pk for someone else. It is a list because a harbor may
@@ -72,15 +107,68 @@ export class Harbor {
72
107
  this.loader = loader;
73
108
  }
74
109
 
75
- // Boot every ward the store keeps, and learn the hints.
110
+ // Boot every ward the store keeps, and learn the hints. One ward that will
111
+ // not host, a partition this kit cannot read or a loader that throws, is
112
+ // named in `unbooted` and takes nobody else down with it.
76
113
  async boot(): Promise<void> {
77
114
  for (const name of await this.store.list()) {
78
- const kept = await this.store.load(name);
79
- if (kept) await this.host(name, kept);
115
+ try {
116
+ const kept = await this.store.load(name);
117
+ if (kept) await this.host(name, kept);
118
+ } catch {
119
+ this.unbooted.push(name);
120
+ }
80
121
  }
81
122
  for (const [pk, url] of Object.entries(await this.store.hints())) this.hint(pk, url);
82
123
  }
83
124
 
125
+ // The object a ward writes into, by the name it is kept under: the
126
+ // partition the harbor was handed at boot and keeps so it can save it.
127
+ // Nothing in the harbor reads it, and it is not on `Hosted`, because a
128
+ // side that holds a ward would then hold every seed in it and reach any
129
+ // being past every door. It is here for one reader, the conformance
130
+ // suite, whose census is ward state read straight and whose forgeries are
131
+ // ward state written straight, the way `MemoryHarbor.partitions` is. A
132
+ // side asks the ward.
133
+ partitionOf(name: string): Record<string, unknown> | undefined {
134
+ return this.#saving.get(name)?.memory;
135
+ }
136
+
137
+ // ---- saving
138
+
139
+ // The ward wrote. Its partition is saved once the line is free, and once
140
+ // for every burst of writes, in the order the writes came.
141
+ #wrote(name: string): void {
142
+ const s = this.#saving.get(name);
143
+ if (!s || s.gone) return;
144
+ s.dirty = true;
145
+ if (s.timer === undefined) s.timer = setTimeout(() => void this.#flush(name), 0);
146
+ }
147
+ async #flush(name: string): Promise<void> {
148
+ const s = this.#saving.get(name);
149
+ if (!s) return;
150
+ if (s.timer !== undefined) {
151
+ clearTimeout(s.timer);
152
+ s.timer = undefined;
153
+ }
154
+ if (s.running) return s.running;
155
+ s.running = (async () => {
156
+ while (s.dirty && !s.gone) {
157
+ s.dirty = false;
158
+ try {
159
+ await this.store.save(name, s.memory);
160
+ } catch {
161
+ this.faults.set(name, (this.faults.get(name) ?? 0) + 1);
162
+ }
163
+ }
164
+ })();
165
+ try {
166
+ await s.running;
167
+ } finally {
168
+ s.running = null;
169
+ }
170
+ }
171
+
84
172
  // ---- the directory
85
173
 
86
174
  // Bytes to one of this harbor's own doors, and the one bit the door says
@@ -113,7 +201,7 @@ export class Harbor {
113
201
  }
114
202
 
115
203
  hint(pk: string, url: string): void {
116
- if (this.reaches.get(pk)?.held) return; // a socket in hand beats a hint
204
+ if (this.reaches.has(pk)) return; // a reach proven at a door beats a hint, held or reached through a dialed line
117
205
  this.reaches.set(pk, { reach: request(url), held: false });
118
206
  }
119
207
 
@@ -130,12 +218,11 @@ export class Harbor {
130
218
  // signed by the ward key. Only the holder of that ward's seed can write
131
219
  // that reply, the lid is fresh so nothing replays, and a box that does not
132
220
  // open writes nothing at the ward. This is the one box a harbor ever opens,
133
- // the one it sealed itself, and it reads nothing from it but that the
134
- // signature is the claimed key's.
221
+ // the one it sealed itself, and it reads nothing from it but that it is
222
+ // the silence a door owes such a box, signed by the claimed key.
135
223
  async prove(pk: string, reach: Reach): Promise<boolean> {
136
- if (!/^[0-9a-f]{128}$/.test(pk)) return false;
224
+ if (!isWardPk(pk)) return false;
137
225
  try {
138
- const random = (n: number) => globalThis.crypto.getRandomValues(new Uint8Array(n));
139
226
  const lid = await sealingPair(random(KEY));
140
227
  const back = await within(PROBE, reach.carry(pk, concat([lid.pk, random(KEY)])));
141
228
  if (back === LATE || back === undefined) return false;
@@ -148,16 +235,37 @@ export class Harbor {
148
235
 
149
236
  // Bind what the far side claims, each pk proven at its door first. Both
150
237
  // sides bind this way: the listener the dialer's claims, the dialer the
151
- // listener's, since a claim is a claim whichever end made it.
238
+ // listener's, since a claim is a claim whichever end made it. An announce
239
+ // names at most this many pks, and they are proven a few at a time: a
240
+ // side that announced a thousand would otherwise cost a thousand keys, a
241
+ // thousand frames and a thousand waits at once, on its word alone.
152
242
  async bind(pks: string[], reach: Reach, held: boolean): Promise<void> {
153
243
  // An announce is the whole of what that side holds now, so a pk this
154
- // reach was bound for and no longer claims is unbound at once: a ward
244
+ // reach was bound for and does not claim now is unbound at once: a ward
155
245
  // that left a dialer is not reachable through it, proof or no proof.
246
+ // And it is the newest word: a proof still in flight from an earlier
247
+ // announce on this reach binds nothing when it lands, since the side
248
+ // has spoken again since.
249
+ const said = (announces.get(reach) ?? 0) + 1;
250
+ announces.set(reach, said);
156
251
  for (const [pk, b] of this.reaches) if (b.reach === reach && !pks.includes(pk)) this.reaches.delete(pk);
157
- await Promise.all(pks.map(async (pk) => {
158
- if (this.doors.has(pk) || !(await this.prove(pk, reach))) return;
252
+ const claimed = [...new Set(pks)].slice(0, ANNOUNCE);
253
+ const prove = async (pk: string) => {
254
+ if (this.doors.has(pk)) return;
255
+ // A line already held for this pk is not displaced by another line that
256
+ // proves the same pk. A proof says only that the ward is reachable that
257
+ // way, and a relay that forwards to the real ward proves as well as the
258
+ // ward's own line does: taking the binding would add a hop the relay
259
+ // chooses, and can drop. The first held line keeps it until it closes,
260
+ // which unbind says.
261
+ const standing = this.reaches.get(pk);
262
+ if (standing?.held && standing.reach !== reach) return;
263
+ if (!(await this.prove(pk, reach))) return;
264
+ if (announces.get(reach) !== said) return; // a newer announce has spoken since
265
+ if (this.reaches.get(pk)?.held && this.reaches.get(pk)!.reach !== reach) return; // one took it while this was proving
159
266
  this.reaches.set(pk, { reach, held });
160
- }));
267
+ };
268
+ for (let i = 0; i < claimed.length; i += PROVING) await Promise.all(claimed.slice(i, i + PROVING).map(prove));
161
269
  }
162
270
  unbind(reach: Reach): void {
163
271
  for (const [pk, b] of this.reaches) if (b.reach === reach) this.reaches.delete(pk);
@@ -167,7 +275,7 @@ export class Harbor {
167
275
 
168
276
  // Mint a seed and boot an empty ward under a name: a world born here.
169
277
  async create(name: string, user = '', code = DEFAULT_CODE): Promise<Hosted> {
170
- const kept: Kept = { seed: globalThis.crypto.getRandomValues(new Uint8Array(32)), partition: {}, record: { pk: '', code, user } };
278
+ const kept: Kept = { seed: random(KEY), partition: {}, record: { pk: '', code, user } };
171
279
  await this.store.put(name, kept);
172
280
  const hosted = await this.host(name, kept);
173
281
  await this.store.record(name, hosted.record);
@@ -175,12 +283,19 @@ export class Harbor {
175
283
  }
176
284
 
177
285
  // Stop serving a ward and take it out of the store: what was kept comes
178
- // back, for another harbor to put. The partition is written first, because
179
- // a being driven in-process changes it without passing a door.
286
+ // back, for another harbor to put. The partition is written first, and
287
+ // never again under this name: whoever still holds the old pointers holds
288
+ // a ward that saves nothing and answers its owner silence.
180
289
  async drop(name: string): Promise<Kept | undefined> {
181
290
  const h = this.wards.get(name);
182
291
  if (h) {
183
292
  await h.save();
293
+ // Gone for whoever still holds the old pointers, whose save closure has
294
+ // this row in hand, and out of the map, which now says what this harbor
295
+ // serves and nothing else.
296
+ const s = this.#saving.get(name);
297
+ if (s) s.gone = true;
298
+ this.#saving.delete(name);
184
299
  this.wards.delete(name);
185
300
  this.doors.delete(h.pk);
186
301
  this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
@@ -188,50 +303,92 @@ export class Harbor {
188
303
  return this.store.take(name);
189
304
  }
190
305
 
191
- // Put what another harbor dropped, and boot it: same seed, same pk.
306
+ // Put what another harbor dropped, and boot it: same seed, same pk. A ward
307
+ // that will not boot is not adopted. This is the one path where a partition
308
+ // written somewhere else arrives, so it is the one place the ward's own
309
+ // shape check is met, and the name is refused for as long as it is kept:
310
+ // a partition put and left would hold the name against every later try,
311
+ // with a copy of it in a store that can never serve it. It goes back out,
312
+ // and the only copy is the one the caller is still holding.
192
313
  async adopt(name: string, kept: Kept): Promise<Hosted> {
193
314
  await this.store.put(name, kept);
194
- return this.host(name, kept);
315
+ try {
316
+ return await this.host(name, kept);
317
+ } catch (e) {
318
+ await this.store.take(name);
319
+ throw e;
320
+ }
195
321
  }
196
322
 
197
323
  // A ward from what the store keeps: the ground, once, and two pointers.
198
324
  async host(name: string, kept: Kept): Promise<Hosted> {
199
325
  const { seed, partition: memory } = kept;
200
326
  const classes = await this.loader(kept.record);
201
- const objects = new Map<object, BeingLike>(); // cells -> the object instantiate made. a side's hand, never the ward's
327
+ const objects = new WeakMap<object, BeingLike>(); // cells -> the object instantiate made. a side's hand, never the ward's; it follows the cells out when she is unbooted
202
328
  const ground: Ground = {
203
329
  seed,
204
330
  memory,
205
- instantiate: (className, stance) => {
206
- const C = classes[className] ?? this.classes[className];
207
- if (!C) return null;
208
- const obj = new C(stance);
209
- objects.set(stance.cells, obj);
210
- return obj;
211
- },
331
+ // This ward's own classes first, then the harbor's: a ward that names a
332
+ // class of its own is answered with hers.
333
+ instantiate: maker(objects, classes, this.classes),
212
334
  carry: (pk, bytes) => this.carry(pk, new Uint8Array(bytes)),
213
- random: (n) => globalThis.crypto.getRandomValues(new Uint8Array(n)),
335
+ random,
336
+ wrote: () => this.#wrote(name),
214
337
  };
215
- const w = await Ward(ground);
216
- const save = () => this.store.save(name, memory);
217
- const after =
218
- <A extends unknown[], R>(f: (...a: A) => Promise<R>) =>
219
- async (...a: A): Promise<R> => {
220
- try {
221
- return await f(...a);
222
- } finally {
223
- await save();
224
- }
225
- };
226
- const door = after(w.door);
227
- const ask = after(w.ask);
228
- const bp = (await w.ask()) as { notes: { pk: string } }; // learned by asking, as anyone learns anything
338
+ const saving: Saving = { memory, dirty: false, running: null, timer: undefined, gone: false };
339
+ this.#saving.set(name, saving);
340
+ // A ward that will not be born leaves nothing behind: a partition of a
341
+ // shape this kit cannot read throws here, and a name the harbor does not
342
+ // serve must not have a saving row saying it does. Learning the pk is
343
+ // inside this, because a ward that will not say its pk is a ward this
344
+ // harbor cannot route to and is no more born than one that threw.
345
+ let w: WardPointers, pk: string;
346
+ try {
347
+ w = await Ward(ground);
348
+ pk = await learnPk(w);
349
+ } catch (e) {
350
+ saving.gone = true;
351
+ this.#saving.delete(name);
352
+ throw e;
353
+ }
354
+ // A save the caller may wait for: what the ward wrote so far is in the
355
+ // store when this resolves, or the fault is counted. After a call through
356
+ // a pointer the store is current, so a restart right after it finds
357
+ // everything; a refusal at the door wrote nothing and saves nothing, and
358
+ // neither does a read. A ward that wrote nothing has nothing to save, so
359
+ // asking it what it holds is free, which is what makes the ask pointer
360
+ // the way to find out rather than an expensive way.
361
+ const save = async () => {
362
+ if (saving.gone) return;
363
+ await this.#flush(name);
364
+ };
365
+ const door = async (bytes: Uint8Array) => {
366
+ const r = await w.door(bytes);
367
+ // The reply waits for the store, so a restart right after it finds
368
+ // what the arrival wrote. Whether a key this door holds spoke is not
369
+ // the test: a stranger at the public being is not heard and her being
370
+ // may still have written, and a refusal wrote nothing whoever made it.
371
+ await save();
372
+ return r;
373
+ };
374
+ const ask = async (method?: string, args?: Record<string, unknown>) => {
375
+ if (saving.gone) return silence;
376
+ const out = await w.ask(method, args);
377
+ await save();
378
+ return out;
379
+ };
380
+ // The objects map is keyed by the cells the ward handed instantiate, so
381
+ // the way back to a being is her cells; the partition is read for that
382
+ // one lookup and for nothing else, and a key with no object is a being
383
+ // who is not here this run. `keys` is what the map holds, so a caller
384
+ // never needs the beings table to find out what to ask for.
229
385
  const beings = memory.beings as Record<string, object> | undefined;
230
386
  const being = (key: string) => {
231
- const cells = beings?.[key];
387
+ const cells = beings && Object.hasOwn(beings, key) ? beings[key] : undefined;
232
388
  return cells ? objects.get(cells) : undefined;
233
389
  };
234
- const hosted: Hosted = { door, ask, pk: bp.notes.pk, name, record: { ...kept.record, pk: bp.notes.pk }, partition: memory, being, save };
390
+ const keys = () => Object.keys(beings ?? {}).filter((k) => being(k) !== undefined);
391
+ const hosted: Hosted = { door, ask, pk, name, record: { ...kept.record, pk }, being, keys, save };
235
392
  this.wards.set(name, hosted);
236
393
  this.doors.set(hosted.pk, door);
237
394
  await save();
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // A harbor as a dialer: one held socket to a listener's quo. route, its own
2
+ // A harbor as a dialer: one held socket to a listener's URL, its own
3
3
  // pks announced, the listener's bound, and a reconnect with backoff from a
4
4
  // second to thirty when the line drops. Seed, partition and keys are here;
5
5
  // only the reach comes and goes. While the socket is open it is also the
@@ -12,22 +12,35 @@ import type { Harbor } from './core.ts';
12
12
  import { REFUSED, Socket, type Line } from './reach.ts';
13
13
 
14
14
  // `wake` is the device saying it is back: a phone out of a pocket, a
15
- // laptop's lid opened. A device that slept comes back with its timer
16
- // frozen and, often, its socket closed under it by the system without a
17
- // word. So a line in hand is made to speak, an announce, and a dead one
18
- // closes under the word and is dialed again; a line that is down is dialed
19
- // now instead of waiting out the backoff; a dial in flight is left alone.
15
+ // laptop's lid opened. A device that slept comes back with its timer frozen
16
+ // and, often, its socket closed under it by the system without a word. So
17
+ // nothing in hand is trusted: whatever line there is, open or still opening,
18
+ // is put down and a new one is dialed now.
19
+ //
20
+ // Nothing here can tell a line the system dropped from a live one, and
21
+ // asking is worse than dialing. An announce written into a dead line is
22
+ // taken into the send buffer and the error comes when TCP gives up
23
+ // retransmitting, one to two minutes on a Mac and up to fifteen on Linux,
24
+ // and for all of it every ask to a pk bound behind that line hears `late`.
25
+ // A probe with a bound would want a round trip the wire does not have: four
26
+ // words, none of them a ping, and making one out of an ask would give a
27
+ // second kit's listener a meaning to reproduce. Dialing costs one handshake
28
+ // when the line was fine, and an ask made meanwhile waits on the new socket
29
+ // rather than being lost, because a socket is a fallback from the moment it
30
+ // is dialed.
20
31
  export type Dialer = { url: string; socket: Socket | null; wake(): void; close(): void };
21
32
 
22
33
  export function dial(harbor: Harbor, url: string): Dialer {
23
34
  const d: Dialer = { url, socket: null, wake: () => {}, close: () => {} };
24
35
  let stopped = false,
25
36
  wait = 1000,
26
- timer: ReturnType<typeof setTimeout> | undefined;
37
+ timer: ReturnType<typeof setTimeout> | undefined,
38
+ line: Line | undefined, // the line in hand, open or still opening: close() closes it either way
39
+ held: { socket: Socket; shed: () => void } | undefined; // the one this dialer owns now, and how it gives back what the line took
27
40
  const connect = () => {
28
41
  timer = undefined; // no wait pending: this is the dial it was waiting for
29
42
  if (stopped) return;
30
- const line = new WebSocket(url.replace(/\/$/, '').replace(/^http/, 'ws')) as unknown as Line;
43
+ line = new WebSocket(url.replace(/\/$/, '').replace(/^http/, 'ws')) as unknown as Line;
31
44
  const s: Socket = new Socket(
32
45
  line,
33
46
  (pk, bytes) => harbor.deliver(pk, bytes),
@@ -40,11 +53,14 @@ export function dial(harbor: Harbor, url: string): Dialer {
40
53
  void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
41
54
  },
42
55
  (why) => {
43
- harbor.announcers.delete(say);
44
- harbor.unbind(s);
45
- if (d.socket === s) d.socket = null;
46
- const at = harbor.fallbacks.indexOf(s);
47
- if (at !== -1) harbor.fallbacks.splice(at, 1); // this line only: another dialer's stays
56
+ shed();
57
+ // A line this dialer already put down. Its close arrives whenever the
58
+ // system gets round to noticing, which on a socket that died under a
59
+ // sleeping device is minutes; by then the line dialed in its place is
60
+ // the one in hand, so this one gives back what it took and dials
61
+ // nothing. Only the line the dialer still holds dials again.
62
+ if (held?.socket !== s) return;
63
+ held = undefined;
48
64
  // A suite this listener will not speak is not a line that dropped. It
49
65
  // will not become speakable by asking again sooner, so the wait goes
50
66
  // straight to the ceiling and stays there until it changes its mind.
@@ -53,12 +69,23 @@ export function dial(harbor: Harbor, url: string): Dialer {
53
69
  wait = Math.min(wait * 2, 30000);
54
70
  },
55
71
  );
72
+ // Everything this line took from the harbor, given back. Said when it
73
+ // closes, and when the dialer puts it down without waiting for that.
74
+ const shed = () => {
75
+ harbor.announcers.delete(say);
76
+ harbor.unbind(s);
77
+ if (d.socket === s) d.socket = null;
78
+ const at = harbor.fallbacks.indexOf(s);
79
+ if (at !== -1) harbor.fallbacks.splice(at, 1); // this line only: another dialer's stays
80
+ };
81
+ held = { socket: s, shed };
56
82
  harbor.fallbacks.push(s); // from the moment it is dialed: an ask made before the handshake waits on the socket, and is not unreached
57
83
  // What this harbor holds, said when the line opens and again whenever it
58
84
  // changes. A ward booted or adopted after the handshake is announced on
59
85
  // the line already in hand, and a dropped one stops being claimed at once.
60
86
  const say = () => s.announce([...harbor.doors.keys()]);
61
87
  line.addEventListener('open', () => {
88
+ if (stopped) return s.close(); // closed while it was opening: it opens into nothing
62
89
  d.socket = s;
63
90
  harbor.announcers.add(say);
64
91
  say();
@@ -66,17 +93,19 @@ export function dial(harbor: Harbor, url: string): Dialer {
66
93
  };
67
94
  d.wake = () => {
68
95
  if (stopped) return;
69
- if (d.socket) return d.socket.announce([...harbor.doors.keys()]);
70
- if (timer === undefined) return;
71
96
  clearTimeout(timer);
72
97
  timer = undefined;
73
- wait = 1000;
98
+ wait = 1000; // the device is back, and the wait it froze belongs to the sleep, not to this line
99
+ const old = held;
100
+ held = undefined; // put down before the new one is dialed, so a fallback list never holds the dead line ahead of the live one
101
+ old?.shed();
102
+ old?.socket.close();
74
103
  connect();
75
104
  };
76
105
  d.close = () => {
77
106
  stopped = true;
78
107
  clearTimeout(timer);
79
- d.socket?.close();
108
+ line?.close(); // open or opening: a line closed while connecting fires close and never open
80
109
  };
81
110
  connect();
82
111
  return d;
@@ -9,3 +9,6 @@ export { dial, type Dialer } from './dial.ts';
9
9
  export { MemoryStore, values, type Store, type Kept, type WardRecord } from './store.ts';
10
10
  export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
11
11
  export type { Ground, WardPointers } from '../ward/ground.ts';
12
+ // What a harbor builds a ground out of. Convenience, never contract: a kit
13
+ // writing its own harbor may write these three again.
14
+ export { maker, entropy, learnPk } from '../ward/ground.ts';
@@ -4,8 +4,8 @@
4
4
  // keeping TWO pointers. Routes one ward pk to one door. Knows no being,
5
5
  // reads no partition, opens no byte. Several memory harbors may be linked,
6
6
  // which stands in for a wire, and cut, which stands in for weather.
7
- import type { BeingClass, Stance } from '../being/types.ts';
8
- import type { Ground, WardPointers } from '../ward/ground.ts';
7
+ import type { BeingClass, BeingLike } from '../being/types.ts';
8
+ import { maker, entropy, learnPk, type Ground, type WardPointers } from '../ward/ground.ts';
9
9
  import { hex } from '../ward/arithmetic.ts';
10
10
 
11
11
  export type Booted = WardPointers & { pk: string };
@@ -20,7 +20,7 @@ export class MemoryHarbor {
20
20
  cut = false;
21
21
  readonly partitions = new Map<string, Record<string, unknown>>(); // seed -> memory. the harbor keeps it and reads nothing
22
22
  readonly wards = new Map<string, Booted>(); // seed -> pointers
23
- readonly objects = new Map<object, unknown>(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's
23
+ readonly objects = new WeakMap<object, BeingLike>(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
24
24
 
25
25
  // the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
26
26
  async route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
@@ -54,24 +54,19 @@ export class MemoryHarbor {
54
54
  const ground: Ground = {
55
55
  seed,
56
56
  memory,
57
- instantiate: (name: string, stance: Stance) => {
58
- const C = classes[name];
59
- if (!C) return null;
60
- const obj = new C(stance);
61
- this.objects.set(stance.cells, obj);
62
- return obj;
63
- },
57
+ // One objects map for the whole harbor, not one per ward: every ward
58
+ // here is in this process and a test reaches any being of any of them.
59
+ instantiate: maker(this.objects, classes),
64
60
  // bytes cross, copied, never a reference. both ways.
65
61
  carry: async (farPk, bytes) => {
66
62
  this.wire.push(hex(bytes));
67
63
  const back = await this.route(farPk, new Uint8Array(bytes));
68
64
  return back === undefined ? undefined : new Uint8Array(back);
69
65
  },
70
- random: (n) => globalThis.crypto.getRandomValues(new Uint8Array(n)),
66
+ random: entropy,
71
67
  };
72
68
  const w = await Ward(ground);
73
- const bp = (await w.ask()) as { notes: { pk: string } }; // the harbor learns its ward's pk the way anyone learns anything: by asking
74
- const booted: Booted = { ...w, pk: bp.notes.pk };
69
+ const booted: Booted = { ...w, pk: await learnPk(w) };
75
70
  this.doors.set(booted.pk, booted.door);
76
71
  this.wards.set(seed, booted);
77
72
  return booted;