@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/src/harbor/core.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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.
|
|
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
|
|
134
|
-
//
|
|
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 (
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
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:
|
|
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,
|
|
179
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
335
|
+
random,
|
|
336
|
+
wrote: () => this.#wrote(name),
|
|
214
337
|
};
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
387
|
+
const cells = beings && Object.hasOwn(beings, key) ? beings[key] : undefined;
|
|
232
388
|
return cells ? objects.get(cells) : undefined;
|
|
233
389
|
};
|
|
234
|
-
const
|
|
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();
|
package/src/harbor/dial.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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;
|
package/src/harbor/index.ts
CHANGED
|
@@ -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';
|
package/src/harbor/memory.ts
CHANGED
|
@@ -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,
|
|
8
|
-
import type
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
66
|
+
random: entropy,
|
|
71
67
|
};
|
|
72
68
|
const w = await Ward(ground);
|
|
73
|
-
const
|
|
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;
|