nervur 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/GETTING_STARTED.md +138 -0
  2. package/NOTICE +1 -1
  3. package/README.md +108 -20
  4. package/dist/being/being.d.ts +24 -0
  5. package/dist/being/being.js +109 -0
  6. package/dist/being/digest.d.ts +3 -0
  7. package/dist/being/digest.js +37 -0
  8. package/dist/being/index.d.ts +5 -0
  9. package/dist/being/index.js +6 -0
  10. package/dist/being/silence.d.ts +12 -0
  11. package/dist/being/silence.js +41 -0
  12. package/dist/being/types.d.ts +89 -0
  13. package/dist/being/types.js +58 -0
  14. package/dist/conformance/assert.d.ts +11 -0
  15. package/dist/conformance/assert.js +106 -0
  16. package/dist/conformance/beings.d.ts +198 -0
  17. package/dist/conformance/beings.js +183 -0
  18. package/dist/conformance/estate.d.ts +5 -0
  19. package/dist/conformance/estate.js +388 -0
  20. package/dist/conformance/index.d.ts +81 -0
  21. package/dist/conformance/index.js +816 -0
  22. package/dist/conformance/reach.d.ts +10 -0
  23. package/dist/conformance/reach.js +72 -0
  24. package/dist/conformance/store.d.ts +5 -0
  25. package/dist/conformance/store.js +123 -0
  26. package/dist/harbor/core.d.ts +53 -0
  27. package/dist/harbor/core.js +621 -0
  28. package/dist/harbor/dial.d.ts +9 -0
  29. package/dist/harbor/dial.js +81 -0
  30. package/dist/harbor/index.d.ts +8 -0
  31. package/dist/harbor/index.js +16 -0
  32. package/dist/harbor/memory.d.ts +25 -0
  33. package/dist/harbor/memory.js +72 -0
  34. package/dist/harbor/reach.d.ts +36 -0
  35. package/dist/harbor/reach.js +191 -0
  36. package/dist/harbor/store.d.ts +40 -0
  37. package/dist/harbor/store.js +70 -0
  38. package/dist/vector/cases.d.ts +41 -0
  39. package/dist/vector/cases.js +195 -0
  40. package/dist/vector/index.d.ts +6 -0
  41. package/dist/vector/index.js +8 -0
  42. package/dist/vector/stand.d.ts +9 -0
  43. package/dist/vector/stand.js +76 -0
  44. package/dist/vector/world.d.ts +143 -0
  45. package/dist/vector/world.js +198 -0
  46. package/dist/ward/allowance.d.ts +10 -0
  47. package/dist/ward/allowance.js +71 -0
  48. package/dist/ward/arithmetic.d.ts +31 -0
  49. package/dist/ward/arithmetic.js +245 -0
  50. package/dist/ward/cells.d.ts +7 -0
  51. package/dist/ward/cells.js +186 -0
  52. package/dist/ward/door.d.ts +18 -0
  53. package/dist/ward/door.js +186 -0
  54. package/dist/ward/ground.d.ts +23 -0
  55. package/dist/ward/ground.js +38 -0
  56. package/dist/ward/heirs.d.ts +13 -0
  57. package/dist/ward/heirs.js +115 -0
  58. package/dist/ward/index.d.ts +9 -0
  59. package/dist/ward/index.js +13 -0
  60. package/dist/ward/owner.d.ts +13 -0
  61. package/dist/ward/owner.js +220 -0
  62. package/dist/ward/partition.d.ts +65 -0
  63. package/dist/ward/partition.js +295 -0
  64. package/dist/ward/seal.d.ts +52 -0
  65. package/dist/ward/seal.js +145 -0
  66. package/dist/ward/stance.d.ts +25 -0
  67. package/dist/ward/stance.js +413 -0
  68. package/dist/ward/ward.d.ts +12 -0
  69. package/dist/ward/ward.js +361 -0
  70. package/package.json +37 -15
  71. package/protocol/SPEC.md +1843 -0
  72. package/protocol/vectors/arithmetic.json +117 -0
  73. package/protocol/vectors/door.json +345 -0
  74. package/protocol/vectors/framing.json +92 -0
  75. package/protocol/vectors/wire.json +48 -0
  76. package/quo-kit.md +652 -0
  77. package/src/being/being.ts +123 -0
  78. package/src/being/digest.ts +46 -0
  79. package/src/being/index.ts +7 -0
  80. package/src/being/silence.ts +46 -0
  81. package/src/being/types.ts +170 -0
  82. package/src/conformance/assert.ts +100 -0
  83. package/src/conformance/beings.ts +183 -0
  84. package/src/conformance/estate.ts +412 -0
  85. package/src/conformance/index.ts +962 -0
  86. package/src/conformance/reach.ts +83 -0
  87. package/src/conformance/store.ts +136 -0
  88. package/src/harbor/core.ts +660 -0
  89. package/src/harbor/dial.ts +112 -0
  90. package/src/harbor/index.ts +17 -0
  91. package/src/harbor/memory.ts +83 -0
  92. package/src/harbor/reach.ts +215 -0
  93. package/src/harbor/store.ts +101 -0
  94. package/src/vector/cases.ts +229 -0
  95. package/src/vector/index.ts +11 -0
  96. package/src/vector/stand.ts +75 -0
  97. package/src/vector/world.ts +220 -0
  98. package/src/ward/allowance.ts +85 -0
  99. package/src/ward/arithmetic.ts +247 -0
  100. package/src/ward/cells.ts +190 -0
  101. package/src/ward/door.ts +186 -0
  102. package/src/ward/ground.ts +160 -0
  103. package/src/ward/heirs.ts +114 -0
  104. package/src/ward/index.ts +17 -0
  105. package/src/ward/owner.ts +214 -0
  106. package/src/ward/partition.ts +353 -0
  107. package/src/ward/seal.ts +174 -0
  108. package/src/ward/stance.ts +433 -0
  109. package/src/ward/ward.ts +378 -0
  110. package/index.js +0 -17
  111. package/nervur.js +0 -5
@@ -0,0 +1,621 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The harbor core: what every harbor on a device is. It boots wards from a
3
+ // store, keeps the map of ward pk to door for its own wards and pk to reach
4
+ // for foreign ones, carries bytes to a pk, and hands bytes from the wire to
5
+ // one door. It names no terrain: the store, the class loader and the lease
6
+ // are the terrain's, handed in or laid over it. A disk harbor is this plus
7
+ // files and a pid; a browser harbor is this plus IndexedDB and a lock; an
8
+ // edge harbor is this over an object's storage. None of them is in this
9
+ // tree, and every one of them passes the conformance suite untouched.
10
+ //
11
+ // The directory is filled four ways, in this order: the harbor's own
12
+ // doors; a socket a dialer holds to it, bound once the door behind the
13
+ // dialer's claim has proved it holds the key, and unbound at close; the
14
+ // claims of a listener this harbor dialed, proved the same way and reached
15
+ // through that line; and a hint, a pk at a URL, kept in the store. A hint
16
+ // never displaces a reach proved at a door. And one fallback: a dialer with nothing in its
17
+ // directory for a pk sends down the socket it holds, because the listener
18
+ // it dialed is the rendezvous and may hold that pk on another socket. Bytes
19
+ // that arrive from the wire go to an own door or a held socket and never
20
+ // onward by request or by fallback; that one rule is the rendezvous.
21
+ import { Ward } from '../ward/ward.js';
22
+ import { maker, entropy as random, learnPk } from '../ward/ground.js';
23
+ import { silence } from '../being/silence.js';
24
+ import { request } from './reach.js';
25
+ import { isWardPk, openReply, wardSignPk } from '../ward/seal.js';
26
+ import { concat, sealingPair, KEY } from '../ward/arithmetic.js';
27
+ import { within, LATE } from '../ward/allowance.js';
28
+ import { restoreRow, rowsIn } from '../ward/partition.js';
29
+ import { rowsOf } from './store.js';
30
+ export const DEFAULT_CODE = 'classes/index.ts';
31
+ // The device's entropy, spelled once. A harbor draws it for a ward's seed at
32
+ // birth, for the lid of every probe it sends, and it hands the same function
33
+ // to every ward it boots as the ground's `random`.
34
+ // How long a probe waits for the door behind a claim. A claim that answers
35
+ // nothing in this time is not bound; the next announce is another chance.
36
+ const PROBE = 10_000;
37
+ // How many pks one announce may claim, and how many are proven at once.
38
+ const ANNOUNCE = 64;
39
+ const PROVING = 8;
40
+ // How many times each reach has announced: a proof from an older announce
41
+ // binds nothing when it lands.
42
+ const announces = new WeakMap();
43
+ export class Harbor {
44
+ store;
45
+ loader;
46
+ wards = new Map(); // name -> pointers
47
+ doors = new Map(); // ward pk -> door
48
+ reaches = new Map(); // the directory: foreign ward pk -> reach
49
+ down = new Set(); // pks this harbor will not reach right now: weather, for a test
50
+ refused = new Map(); // own ward pk -> arrivals its door would not admit. what a terrain does with it is its own
51
+ faults = new Map(); // ward name -> saves the store refused. what a terrain does with it is its own
52
+ // Told every time a save is refused, with the ward's name and how many of
53
+ // its saves the store has now refused. The refusal is already the answer of
54
+ // the ask that caused it, so nobody is waiting on this; it is for the box's
55
+ // own record, a journal line or a counter, and a terrain that keeps none
56
+ // leaves it alone.
57
+ onFault = () => { };
58
+ unbooted = []; // the wards the store keeps that would not host this run, by name
59
+ classes = {}; // classes handed in-process, beside the loader's
60
+ #saving = new Map();
61
+ // The dialers' sockets, in the order they were dialed: where a pk nobody
62
+ // here knows is sent, because the listener a harbor dialed is a rendezvous
63
+ // and may hold that pk for someone else. It is a list because a harbor may
64
+ // dial more than one, and a rendezvous is a listener and nothing more, so
65
+ // there is never only one of them. A pk is tried down the list until one
66
+ // carries it: with a single slot, a harbor holding two lines answered
67
+ // unreached for a pk the other line could have reached.
68
+ fallbacks = [];
69
+ // How a dialer says what this harbor holds now. An announce is a claim and
70
+ // a claim is worth nothing until the door behind it answers a probe, so
71
+ // saying it again costs a proof and buys nothing to an impostor. What is
72
+ // not free is never saying it again: a ward booted, adopted or dropped
73
+ // after a line opened would be announced only by the next line, and a line
74
+ // that does not drop never opens again, so it would be unreachable through
75
+ // that listener for as long as the socket stayed healthy.
76
+ announcers = new Set();
77
+ #announce() {
78
+ for (const say of this.announcers)
79
+ say();
80
+ }
81
+ // What this device lends the beings of the wards it hosts, by name. One
82
+ // for the harbor, and `lendFor` says which ward may ask it: every ward,
83
+ // unless a terrain says a stranger's ward is lent nothing.
84
+ // Set at birth or once the harbor's own ward is standing, which is the
85
+ // usual order: a terrain puts that ward up first, then boots the rest on a
86
+ // ground that can reach it.
87
+ lend;
88
+ constructor(store, loader = async () => ({}), lend) {
89
+ this.store = store;
90
+ this.loader = loader;
91
+ this.lend = lend;
92
+ }
93
+ lendFor(_name, _record) {
94
+ return this.lend;
95
+ }
96
+ // Boot every ward the store keeps, and learn the hints. One ward that will
97
+ // not host, a partition this kit cannot read or a loader that throws, is
98
+ // named in `unbooted` and takes nobody else down with it.
99
+ async boot() {
100
+ for (const name of await this.store.list()) {
101
+ // A ward this harbor already hosts is not booted twice. A terrain that
102
+ // keeps a ward of its own puts it up before this runs, so that a being
103
+ // born in any other ward can lend from her first line.
104
+ if (this.wards.has(name))
105
+ continue;
106
+ try {
107
+ const kept = await this.store.load(name);
108
+ if (kept)
109
+ await this.host(name, kept);
110
+ }
111
+ catch {
112
+ this.unbooted.push(name);
113
+ }
114
+ }
115
+ for (const [pk, url] of Object.entries(await this.store.hints()))
116
+ this.hint(pk, url);
117
+ }
118
+ // The object a ward writes into, by the name it is kept under: the
119
+ // partition the harbor was handed at boot and keeps so it can save it.
120
+ // Nothing in the harbor reads it, and it is not on `Hosted`, because a
121
+ // side that holds a ward would then hold every seed in it and reach any
122
+ // being past every door. It is here for one reader, the conformance
123
+ // suite, whose census is ward state read straight and whose forgeries are
124
+ // ward state written straight, the way `MemoryHarbor.partitions` is. A
125
+ // side asks the ward.
126
+ partitionOf(name) {
127
+ return this.#saving.get(name)?.memory;
128
+ }
129
+ // ---- saving
130
+ // The ward wrote one row. Its partition is saved once the line is free, and
131
+ // once for every burst of writes, with every row named in that burst.
132
+ #wrote(name, row) {
133
+ const s = this.#saving.get(name);
134
+ if (!s || s.gone)
135
+ return;
136
+ s.dirty.add(row);
137
+ if (s.timer === undefined)
138
+ s.timer = setTimeout(() => void this.#flush(name), 0);
139
+ }
140
+ // The rows a store refused, put back in the ward's memory to what the
141
+ // store holds for them. A being whose row was refused would otherwise go
142
+ // on answering out of a memory that runs ahead of the device: her next
143
+ // write is a row past the same ceiling, refused again, and everything she
144
+ // was told between now and the reload is gone with it. Standing her at
145
+ // what is kept costs her the writes that were never kept, which is what
146
+ // the asker was told, and leaves a small write after it one the store
147
+ // takes.
148
+ //
149
+ // The shape is read where the shape lives. The harbor hands each row and
150
+ // the copy it kept to the partition, which knows which part of itself a row
151
+ // is; nothing here reads a being, a bind table or a head. And nothing here
152
+ // asks the store: what it took is what this harbor handed it, which is
153
+ // `kept`, and a row nobody has handed it since this ward was hosted is a
154
+ // row the ward has not moved, so there is nothing to put back.
155
+ // Every row together, and never the refused ones alone. A relation's state
156
+ // is spread over rows, the head for the door's heirs and a being's own row
157
+ // for the keys she speaks under: put one back and the door stands ahead of
158
+ // the knocker and the relation never speaks again. So the ward is put back
159
+ // to the whole point, which is what the store held at the last save taken
160
+ // while no call was in flight. A row that point never knew is left where it
161
+ // is and stays named: no save has ever taken it, so there is nothing to put
162
+ // it back to. The rows that moved come back, so the store is put back with
163
+ // them and a reload finds no half-done row.
164
+ #standAtKept(s) {
165
+ if (s.gone)
166
+ return [];
167
+ const moved = [];
168
+ for (const row of s.kept.keys()) {
169
+ const was = s.kept.get(row);
170
+ const now = rowsOf(s.memory, [row])[0][1];
171
+ if (JSON.stringify(was) === JSON.stringify(now))
172
+ continue;
173
+ restoreRow(s.memory, row, was);
174
+ moved.push(row);
175
+ }
176
+ for (const row of moved)
177
+ s.dirty.delete(row);
178
+ return moved;
179
+ }
180
+ // The ward back to the point, and the store with it. The store is written
181
+ // because a reload reads it and not the memory: a refusal that put only the
182
+ // memory back would leave the rows the store did take standing ahead of the
183
+ // ward until the next restart, and one half of a relation ahead of the
184
+ // other. What goes out is what the store already took once, so it is a
185
+ // write the store has no reason to refuse; one that refuses it anyway is
186
+ // counted like any other, and there is nothing further to put back to.
187
+ async #putBack(name, s) {
188
+ // What the store took under the refused call is not a point to stand at.
189
+ // It is also what has to be written over: a row the ward never moved
190
+ // after that save comes back unchanged in memory and is still ahead of
191
+ // the point in the store, so the rows that go out are the ones the memory
192
+ // moved and the ones the store took under the call, together.
193
+ const under = [...s.under.keys()];
194
+ s.under.clear();
195
+ const moved = this.#standAtKept(s);
196
+ const rows = [...new Set([...moved, ...under])];
197
+ if (rows.length === 0 || s.gone)
198
+ return;
199
+ try {
200
+ await this.store.save(name, s.memory, rows);
201
+ for (const row of rows)
202
+ s.dirty.delete(row);
203
+ }
204
+ catch {
205
+ // A store that will not take back what it took once is a store this
206
+ // ward cannot be put back in. The count says so, the memory stands at
207
+ // the point all the same, and the rows stay named for the next save.
208
+ this.#fault(name);
209
+ }
210
+ }
211
+ // The call ended and the ward is whole, so the point catches up with it. A
212
+ // row the store took under the call and nobody has touched since is where
213
+ // the ward stands, so it joins the point as it is; a row that moved after
214
+ // that save is named for one more, and that save is quiet and moves the
215
+ // point itself. Nothing is written twice for a ward whose call left its
216
+ // rows where the store already has them.
217
+ #settle(name, s) {
218
+ for (const [row, value] of s.under) {
219
+ const now = rowsOf(s.memory, [row])[0][1];
220
+ if (JSON.stringify(value) === JSON.stringify(now))
221
+ s.kept.set(row, value);
222
+ else
223
+ s.dirty.add(row);
224
+ }
225
+ s.under.clear();
226
+ if (s.dirty.size > 0)
227
+ void this.#flush(name);
228
+ }
229
+ // One refusal, counted and told. What a terrain does with the count is its
230
+ // own; the arrival that caused it is told by the answer, not by this.
231
+ #fault(name) {
232
+ const faults = (this.faults.get(name) ?? 0) + 1;
233
+ this.faults.set(name, faults);
234
+ try {
235
+ this.onFault(name, faults);
236
+ }
237
+ catch {
238
+ /* a box that cannot write its own journal is not a ward's problem */
239
+ }
240
+ }
241
+ // Answers whether everything it wrote was kept. False is a store that
242
+ // refused, and it is the ward's answer to the arrival that caused it, not
243
+ // a number in a map nobody reads. A flush that had nothing to write kept
244
+ // everything it was given, which is true and is why a read is never
245
+ // refused.
246
+ async #flush(name) {
247
+ const s = this.#saving.get(name);
248
+ if (!s)
249
+ return true;
250
+ if (s.timer !== undefined) {
251
+ clearTimeout(s.timer);
252
+ s.timer = undefined;
253
+ }
254
+ // A flush already in flight is the one that will write these rows, and
255
+ // its verdict is theirs: two arrivals whose writes went out together are
256
+ // both refused when that write was refused, because neither of them is
257
+ // in the store.
258
+ if (s.running)
259
+ return s.running;
260
+ s.running = (async () => {
261
+ // A put back that was owed to a refusal under a call is done here, the
262
+ // first moment the ward is whole again. Nothing is written over it
263
+ // until it is done, because what is in memory is half of a call the
264
+ // store already said no to.
265
+ // The pen is held from the refusal to the put back that answers it.
266
+ // What a save would write between the two is about to be put back
267
+ // anyway, so writing it buys nothing and costs the one thing that
268
+ // matters: a store written there holds rows of two moments, one side of
269
+ // a call kept and the other about to be undone, and a box that goes
270
+ // down before the put back leaves it torn with nobody left to mend it.
271
+ if (s.back) {
272
+ if (s.calls > 0)
273
+ return false; // the call that owed it is still in flight, and a put back inside one would roll back rows it is still writing
274
+ s.back = false;
275
+ await this.#putBack(name, s);
276
+ return false; // the verdict of the refusal that owed it, and nothing is written over a put back in the same save
277
+ }
278
+ while (s.dirty.size > 0 && !s.gone) {
279
+ // Taken before the write, so a row named while this one is in flight
280
+ // is written by the next turn of the loop and not lost with it.
281
+ const rows = [...s.dirty];
282
+ s.dirty.clear();
283
+ // The copy of what is about to be written, taken before the store is
284
+ // asked and kept only if the store takes it. It is the same values
285
+ // the store is about to write down, so it costs one copy of the rows
286
+ // of this save and nothing of the rows it does not name.
287
+ const writing = rowsOf(s.memory, rows);
288
+ try {
289
+ const quiet = s.calls === 0; // read before the store is asked: the point is where the ward stood when this save was taken
290
+ await this.store.save(name, s.memory, rows);
291
+ // The store took it, and a restart finds it. Whether it is a point
292
+ // a refused ward may be put back to is another question: a save
293
+ // taken under a call holds half of a relation, so it is written and
294
+ // is not somewhere to stand.
295
+ for (const [row, value] of writing)
296
+ (quiet ? s.kept : s.under).set(row, value);
297
+ }
298
+ catch {
299
+ this.#fault(name);
300
+ // A row the store would not take is a row that is not kept, so it
301
+ // stays named and the verdict is no. The loop ends here rather than
302
+ // trying again: what the next attempt writes is the same bytes to
303
+ // the same store, and it is the next arrival that asks for one, not
304
+ // this one in a circle. Whoever wrote it is told, which is the
305
+ // whole point, and the row waits where a reader can still see it.
306
+ for (const row of rows)
307
+ s.dirty.add(row);
308
+ // The refusal waits for an arrival to hear it: a save taken by a
309
+ // timer answers nobody, and a ward whose row was refused between
310
+ // two arrivals would otherwise have the refusal counted and the
311
+ // next door go on saying yes. And the ward goes back to the point
312
+ // now, unless a call is in flight, in which case the put back is
313
+ // owed until that call ends.
314
+ s.fault = true;
315
+ if (s.calls > 0) {
316
+ s.back = true;
317
+ return false;
318
+ }
319
+ await this.#putBack(name, s);
320
+ return false;
321
+ }
322
+ }
323
+ return true;
324
+ })();
325
+ try {
326
+ return await s.running;
327
+ }
328
+ finally {
329
+ s.running = null;
330
+ }
331
+ }
332
+ // ---- the directory
333
+ // Bytes to one of this harbor's own doors, and the one bit the door says
334
+ // beside its reply, counted.
335
+ async #own(pk, door, bytes) {
336
+ const r = await door(bytes);
337
+ if (!r.heard)
338
+ this.refused.set(pk, (this.refused.get(pk) ?? 0) + 1);
339
+ return new Uint8Array(r.bytes);
340
+ }
341
+ async carry(pk, bytes) {
342
+ if (this.down.has(pk))
343
+ return undefined;
344
+ const door = this.doors.get(pk);
345
+ if (door)
346
+ return this.#own(pk, door, bytes);
347
+ const bound = this.reaches.get(pk);
348
+ if (bound)
349
+ return bound.reach.carry(pk, bytes);
350
+ for (const reach of this.fallbacks) {
351
+ const back = await reach.carry(pk, bytes);
352
+ if (back !== undefined)
353
+ return back;
354
+ }
355
+ return undefined; // nobody here knows it, and no listener we dialed holds it
356
+ }
357
+ async deliver(pk, bytes) {
358
+ if (this.down.has(pk))
359
+ return undefined;
360
+ const door = this.doors.get(pk);
361
+ if (door)
362
+ return this.#own(pk, door, bytes);
363
+ const bound = this.reaches.get(pk);
364
+ return bound?.held ? bound.reach.carry(pk, bytes) : undefined;
365
+ }
366
+ hint(pk, url) {
367
+ if (this.reaches.has(pk))
368
+ return; // a reach proven at a door beats a hint, held or reached through a dialed line
369
+ this.reaches.set(pk, { reach: request(url), held: false });
370
+ }
371
+ async remember(pk, url) {
372
+ this.hint(pk, url);
373
+ await this.store.hint(pk, url);
374
+ }
375
+ // An announce is a claim, and a claim binds nothing until proven: anyone
376
+ // who can reach a listener could otherwise name a pk that is not theirs and
377
+ // take its reachability. The proof is the door itself, as it already is. A
378
+ // lid this harbor minted and noise after it is a box that does not open,
379
+ // and the door answers one such box with silence sealed to the lid and
380
+ // signed by the ward key. Only the holder of that ward's seed can write
381
+ // that reply, the lid is fresh so nothing replays, and a box that does not
382
+ // open writes nothing at the ward. This is the one box a harbor ever opens,
383
+ // the one it sealed itself, and it reads nothing from it but that it is
384
+ // the silence a door owes such a box, signed by the claimed key.
385
+ async prove(pk, reach) {
386
+ if (!isWardPk(pk))
387
+ return false;
388
+ try {
389
+ const lid = await sealingPair(random(KEY));
390
+ const back = await within(PROBE, reach.carry(pk, concat([lid.pk, random(KEY)])));
391
+ if (back === LATE || back === undefined)
392
+ return false;
393
+ const reply = await openReply(back, lid.secret, wardSignPk(pk));
394
+ return reply !== null && 'silence' in reply;
395
+ }
396
+ catch {
397
+ return false;
398
+ }
399
+ }
400
+ // Bind what the far side claims, each pk proven at its door first. Both
401
+ // sides bind this way: the listener the dialer's claims, the dialer the
402
+ // listener's, since a claim is a claim whichever end made it. An announce
403
+ // names at most this many pks, and they are proven a few at a time: a
404
+ // side that announced a thousand would otherwise cost a thousand keys, a
405
+ // thousand frames and a thousand waits at once, on its word alone.
406
+ async bind(pks, reach, held) {
407
+ // An announce is the whole of what that side holds now, so a pk this
408
+ // reach was bound for and does not claim now is unbound at once: a ward
409
+ // that left a dialer is not reachable through it, proof or no proof.
410
+ // And it is the newest word: a proof still in flight from an earlier
411
+ // announce on this reach binds nothing when it lands, since the side
412
+ // has spoken again since.
413
+ const said = (announces.get(reach) ?? 0) + 1;
414
+ announces.set(reach, said);
415
+ for (const [pk, b] of this.reaches)
416
+ if (b.reach === reach && !pks.includes(pk))
417
+ this.reaches.delete(pk);
418
+ const claimed = [...new Set(pks)].slice(0, ANNOUNCE);
419
+ const prove = async (pk) => {
420
+ if (this.doors.has(pk))
421
+ return;
422
+ // A line already held for this pk is not displaced by another line that
423
+ // proves the same pk. A proof says only that the ward is reachable that
424
+ // way, and a relay that forwards to the real ward proves as well as the
425
+ // ward's own line does: taking the binding would add a hop the relay
426
+ // chooses, and can drop. The first held line keeps it until it closes,
427
+ // which unbind says.
428
+ const standing = this.reaches.get(pk);
429
+ if (standing?.held && standing.reach !== reach)
430
+ return;
431
+ if (!(await this.prove(pk, reach)))
432
+ return;
433
+ if (announces.get(reach) !== said)
434
+ return; // a newer announce has spoken since
435
+ if (this.reaches.get(pk)?.held && this.reaches.get(pk).reach !== reach)
436
+ return; // one took it while this was proving
437
+ this.reaches.set(pk, { reach, held });
438
+ };
439
+ for (let i = 0; i < claimed.length; i += PROVING)
440
+ await Promise.all(claimed.slice(i, i + PROVING).map(prove));
441
+ }
442
+ unbind(reach) {
443
+ for (const [pk, b] of this.reaches)
444
+ if (b.reach === reach)
445
+ this.reaches.delete(pk);
446
+ }
447
+ // ---- wards
448
+ // Mint a seed and boot an empty ward under a name: a world born here.
449
+ async create(name, user = '', code = DEFAULT_CODE) {
450
+ const kept = { seed: random(KEY), partition: {}, record: { pk: '', code, user } };
451
+ await this.store.put(name, kept);
452
+ const hosted = await this.host(name, kept);
453
+ await this.store.record(name, hosted.record);
454
+ return hosted;
455
+ }
456
+ // Stop serving a ward and take it out of the store: what was kept comes
457
+ // back, for another harbor to put. The partition is written first, and
458
+ // never again under this name: whoever still holds the old pointers holds
459
+ // a ward that saves nothing and answers its owner silence.
460
+ async drop(name) {
461
+ const h = this.wards.get(name);
462
+ if (h) {
463
+ await h.save();
464
+ // Gone for whoever still holds the old pointers, whose save closure has
465
+ // this row in hand, and out of the map, which now says what this harbor
466
+ // serves and nothing else.
467
+ const s = this.#saving.get(name);
468
+ if (s)
469
+ s.gone = true;
470
+ this.#saving.delete(name);
471
+ this.wards.delete(name);
472
+ this.doors.delete(h.pk);
473
+ this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
474
+ }
475
+ return this.store.take(name);
476
+ }
477
+ // Put what another harbor dropped, and boot it: same seed, same pk. A ward
478
+ // that will not boot is not adopted. This is the one path where a partition
479
+ // written somewhere else arrives, so it is the one place the ward's own
480
+ // shape check is met, and the name is refused for as long as it is kept:
481
+ // a partition put and left would hold the name against every later try,
482
+ // with a copy of it in a store that can never serve it. It goes back out,
483
+ // and the only copy is the one the caller is still holding.
484
+ async adopt(name, kept) {
485
+ await this.store.put(name, kept);
486
+ try {
487
+ return await this.host(name, kept);
488
+ }
489
+ catch (e) {
490
+ await this.store.take(name);
491
+ throw e;
492
+ }
493
+ }
494
+ // A ward from what the store keeps: the ground, once, and two pointers.
495
+ async host(name, kept) {
496
+ const { seed, partition: memory } = kept;
497
+ const classes = await this.loader(kept.record);
498
+ 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
499
+ const ground = {
500
+ seed,
501
+ memory,
502
+ // This ward's own classes first, then the harbor's: a ward that names a
503
+ // class of its own is answered with hers.
504
+ instantiate: maker(objects, classes, this.classes),
505
+ carry: (pk, bytes) => this.carry(pk, new Uint8Array(bytes)),
506
+ random,
507
+ wrote: (row) => this.#wrote(name, row),
508
+ // What the ward wrote is in the store when this resolves, and it says
509
+ // whether the store took it. The ward calls it at the end of every
510
+ // arrival, before it seals, so a call through a pointer has already
511
+ // waited for the store when it comes back: a restart right after one
512
+ // finds everything, and a refusal is that arrival's own answer rather
513
+ // than a number counted behind it. A ward that wrote nothing has
514
+ // nothing to save, so asking it what it holds is free, which is what
515
+ // makes the ask pointer the way to find out rather than an expensive
516
+ // one.
517
+ //
518
+ // A ward on its way out keeps nothing and refuses nothing: an arrival
519
+ // racing an unhost is not a store saying no, and saying so would be a
520
+ // lie in the one direction that matters.
521
+ keep: async () => {
522
+ const s = this.#saving.get(name);
523
+ if (!s || s.gone)
524
+ return true;
525
+ const ok = await this.#flush(name);
526
+ // A refusal is heard once, by the first arrival to ask after it: the
527
+ // save that met it may have been a timer's, which had nobody to tell.
528
+ // Under a call it is left standing, so the call that carried the
529
+ // refused write is refused too, and the outermost arrival hears it.
530
+ const no = !ok || s.fault;
531
+ s.fault = false; // heard. the next arrival is answered by the store and not by what happened before it
532
+ return !no;
533
+ },
534
+ // The ward says a call is in flight, and says when it ends. The harbor
535
+ // goes on writing under it; what it does not do is take a save made
536
+ // there as the point a refused ward is put back to, and a refusal that
537
+ // lands under a call waits here for the call to end before it puts the
538
+ // ward back. The ward is the one that knows, because a being reaching
539
+ // another being of her own ward goes to that ward's door directly and
540
+ // the harbor never sees that call at all.
541
+ calling: () => {
542
+ const s = this.#saving.get(name);
543
+ if (!s)
544
+ return () => { };
545
+ s.calls += 1;
546
+ let lowered = false;
547
+ return () => {
548
+ if (lowered)
549
+ return;
550
+ lowered = true;
551
+ s.calls -= 1;
552
+ if (s.calls > 0 || s.gone)
553
+ return;
554
+ // The moment the ward is whole again: the point catches up with the
555
+ // call that just ended, or a put back that was owed is done now, so
556
+ // a being stands at what is kept before her next word rather than
557
+ // at the next save.
558
+ if (s.back)
559
+ void this.#flush(name);
560
+ else
561
+ this.#settle(name, s);
562
+ };
563
+ },
564
+ };
565
+ const lend = this.lendFor(name, kept.record);
566
+ if (lend)
567
+ ground.lend = lend;
568
+ // What the store holds for this ward at birth is what it was just handed
569
+ // or just read, row by row. It is copied once here so that a being whose
570
+ // very first write of this run is refused is put back too, rather than
571
+ // waiting for a save of hers to succeed before the harbor knows what she
572
+ // was. One copy of a partition per hosted ward, paid at boot and never
573
+ // again: after this, a row's copy is replaced by the save that kept it.
574
+ const saving = { memory, dirty: new Set(), kept: new Map(rowsOf(memory, rowsIn(memory))), running: null, timer: undefined, gone: false, calls: 0, fault: false, back: false, under: new Map() };
575
+ this.#saving.set(name, saving);
576
+ // A ward that will not be born leaves nothing behind: a partition of a
577
+ // shape this kit cannot read throws here, and a name the harbor does not
578
+ // serve must not have a saving row saying it does. Learning the pk is
579
+ // inside this, because a ward that will not say its pk is a ward this
580
+ // harbor cannot route to and is no more born than one that threw.
581
+ let w, pk;
582
+ try {
583
+ w = await Ward(ground);
584
+ pk = await learnPk(w);
585
+ }
586
+ catch (e) {
587
+ saving.gone = true;
588
+ this.#saving.delete(name);
589
+ throw e;
590
+ }
591
+ // The same keep, for a caller that holds the ward rather than asks it:
592
+ // a side that wrote through `being` and wants the store current says so
593
+ // here. It answers nothing, because a caller inside the process reads
594
+ // the fault off the harbor and an arrival is the only thing that needs a
595
+ // verdict of its own.
596
+ const save = async () => void (await ground.keep?.());
597
+ const door = (bytes) => w.door(bytes);
598
+ const ask = async (method, args) => {
599
+ if (saving.gone)
600
+ return silence;
601
+ return w.ask(method, args);
602
+ };
603
+ // The objects map is keyed by the cells the ward handed instantiate, so
604
+ // the way back to a being is her cells; the partition is read for that
605
+ // one lookup and for nothing else, and a key with no object is a being
606
+ // who is not here this run. `keys` is what the map holds, so a caller
607
+ // never needs the beings table to find out what to ask for.
608
+ const beings = memory.beings;
609
+ const being = (key) => {
610
+ const cells = beings && Object.hasOwn(beings, key) ? beings[key] : undefined;
611
+ return cells ? objects.get(cells) : undefined;
612
+ };
613
+ const keys = () => Object.keys(beings ?? {}).filter((k) => being(k) !== undefined);
614
+ const hosted = { door, ask, pk, name, record: { ...kept.record, pk }, being, keys, save };
615
+ this.wards.set(name, hosted);
616
+ this.doors.set(hosted.pk, door);
617
+ await save();
618
+ this.#announce(); // reachable through every line this harbor already holds, not only the next one
619
+ return hosted;
620
+ }
621
+ }
@@ -0,0 +1,9 @@
1
+ import type { Harbor } from './core.ts';
2
+ import { Socket } from './reach.ts';
3
+ export type Dialer = {
4
+ url: string;
5
+ socket: Socket | null;
6
+ wake(): void;
7
+ close(): void;
8
+ };
9
+ export declare function dial(harbor: Harbor, url: string): Dialer;