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