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