@quo-systems/quo 0.2.9 → 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/ward/stance.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// The one stance builder. Used for every being the ward boots. Nothing
|
|
3
3
|
// outer is in what she holds: ids in, values out, keys in the bind table.
|
|
4
|
-
import { silence, isSilence,
|
|
4
|
+
import { silence, isSilence, isWord, word, wordOf } from '../being/silence.js';
|
|
5
5
|
import { digest } from '../being/digest.js';
|
|
6
6
|
import { RESERVED_IDS } from '../being/types.js';
|
|
7
7
|
import { at, put, drop } from './partition.js';
|
|
8
8
|
import { allow, within, LATE } from './allowance.js';
|
|
9
9
|
import { isWardPk } from './seal.js';
|
|
10
|
+
import { cellFault } from './cells.js';
|
|
10
11
|
// The three calls on `standings` share the object with the ids she takes, so
|
|
11
12
|
// a standing named after one of them would be unreachable: `standings.knock`
|
|
12
13
|
// is the call, whatever record sits under that name. One namespace means one
|
|
@@ -51,9 +52,9 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
51
52
|
// Every ask a being makes, bounded. The bound is outside the lane on
|
|
52
53
|
// purpose: an ask that waits its turn behind a relation that comes back
|
|
53
54
|
// round never reaches a send, and a bound that only watched the wire would
|
|
54
|
-
// never see it hang. A bound that ran out is
|
|
55
|
-
// unreached promises nothing was delivered and is safe to ask
|
|
56
|
-
// wait that gave up knows no such thing.
|
|
55
|
+
// never see it hang. A bound that ran out is the word late, never
|
|
56
|
+
// unreached: unreached promises nothing was delivered and is safe to ask
|
|
57
|
+
// again, and a wait that gave up knows no such thing.
|
|
57
58
|
//
|
|
58
59
|
// The bell rings before the work is reached, too: an ask that was still
|
|
59
60
|
// waiting its turn when the wait ran out is never sent. Sent then, it would
|
|
@@ -67,56 +68,88 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
67
68
|
});
|
|
68
69
|
return out === LATE ? word('late') : out;
|
|
69
70
|
};
|
|
70
|
-
const
|
|
71
|
-
const r = await inside.send(bind, keys, true, method, args, wanted);
|
|
71
|
+
const read = (r) => {
|
|
72
72
|
if (isSilence(r) || isWord(r))
|
|
73
|
-
return r;
|
|
73
|
+
return { answer: r, door: false, seen: null };
|
|
74
74
|
if ('silence' in r)
|
|
75
|
-
return silence;
|
|
75
|
+
return { answer: silence, door: true, seen: null };
|
|
76
76
|
if ('quo' in r)
|
|
77
|
-
return word(r.quo); // the far door's word for her, sealed to her lid
|
|
77
|
+
return { answer: word(r.quo), door: true, seen: null }; // the far door's word for her, sealed to her lid
|
|
78
|
+
return { answer: r.object, door: true, seen: r.seen };
|
|
79
|
+
};
|
|
80
|
+
// Whether the door spent the heir this send spoke under. An object, the
|
|
81
|
+
// door's silence and threw all mean it was honoured. A refusal word wrote
|
|
82
|
+
// nothing and says so. Anything that is not the door speaking says nothing.
|
|
83
|
+
const honoured = (r) => r.door && !(isWord(r.answer) && wordOf(r.answer) !== 'threw');
|
|
84
|
+
const answered = (r) => r.door && !isSilence(r.answer) && !isWord(r.answer);
|
|
85
|
+
const send = async (keys, method, args, wanted, rec) => {
|
|
86
|
+
const r = read(await inside.send(bind, keys, true, method, args, wanted));
|
|
78
87
|
if (rec && method !== undefined && r.seen !== null)
|
|
79
88
|
rec.seen = r.seen; // the digest rode along
|
|
80
|
-
return r.
|
|
89
|
+
return r.answer;
|
|
81
90
|
};
|
|
82
91
|
const knocking = async (inv, method, args, wanted) => {
|
|
83
92
|
const name = nameOf(inv);
|
|
93
|
+
// Taken while this knock waited its turn: the relation lives in the
|
|
94
|
+
// standing now and speaks on its lane, under its keys and its count.
|
|
95
|
+
const taken = takenAs(name);
|
|
96
|
+
if (taken !== undefined) {
|
|
97
|
+
const keys = at(bind.standings, taken);
|
|
98
|
+
if (!keys)
|
|
99
|
+
return word('dropped');
|
|
100
|
+
return lane(`ask:${taken}`, () => asking(taken, keys, at(cells.standings, taken), method, args, wanted));
|
|
101
|
+
}
|
|
84
102
|
// her own key for this relation, minted once: rotation two. reused if she knocks again.
|
|
85
103
|
const k = (bind.knocks[name] ??= { current: (await inside.mintKey(bind)).seed, next: null, spoke: false, sent: false, seq: 0 });
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// spent
|
|
90
|
-
//
|
|
104
|
+
// The first knock on a heir speaks as the heir, the key the inviter
|
|
105
|
+
// handed out, and announces her own. Once the door has honoured it the
|
|
106
|
+
// heir is spent, whatever she then said, and her own key speaks; a heir
|
|
107
|
+
// spent once stays spent.
|
|
108
|
+
//
|
|
109
|
+
// She sent once as the heir and the door did not speak: nothing came
|
|
110
|
+
// back, or bytes that were not Quo's, or the wait ran out. Either the
|
|
111
|
+
// door heard, spent the heir and rotated to the key she announced, or
|
|
112
|
+
// nothing arrived and the heir still stands. Only the far door knows,
|
|
113
|
+
// and it will not say.
|
|
91
114
|
//
|
|
92
115
|
// So she asks, and the asking tells her. Under her own key: if the door
|
|
93
|
-
// heard, that key is the one it admits, and she is answered
|
|
94
|
-
// not, the key means nothing there, the ask is
|
|
95
|
-
// that door writes nothing
|
|
96
|
-
// she knocks as the heir, as she would
|
|
97
|
-
// one case where a reply was lost, and
|
|
116
|
+
// heard, that key is the one it admits, and she is answered, or told the
|
|
117
|
+
// door's word. If it did not, the key means nothing there, the ask is
|
|
118
|
+
// refused, and a refusal at that door writes nothing: the heir is
|
|
119
|
+
// untouched and still speaks. Then she knocks as the heir, as she would
|
|
120
|
+
// have. One extra round trip in the one case where a reply was lost, and
|
|
121
|
+
// the relation is not stranded.
|
|
98
122
|
//
|
|
99
123
|
// No stranger gains anything by it: whoever holds the invitation could
|
|
100
124
|
// always knock as the heir, and her own key is admitted only where the
|
|
101
125
|
// door already bound it to her.
|
|
102
126
|
if (inv.heir !== undefined && !k.spoke && k.sent) {
|
|
103
127
|
const own = { ward: inv.ward, heir: inv.heir, current: k.current, next: k.next, seq: k.seq };
|
|
104
|
-
const r = await inside.send(bind, own, true, method, args, wanted);
|
|
128
|
+
const r = read(await inside.send(bind, own, true, method, args, wanted));
|
|
105
129
|
k.seq = own.seq;
|
|
106
130
|
k.current = own.current;
|
|
107
131
|
k.next = own.next;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
inside.wrote();
|
|
133
|
+
if (!r.door) {
|
|
134
|
+
// Nothing the door said. Unreached and late are hers to hear now; bytes
|
|
135
|
+
// that were not Quo's leave her where she was, and the heir is tried.
|
|
136
|
+
if (!isSilence(r.answer))
|
|
137
|
+
return r.answer;
|
|
138
|
+
}
|
|
139
|
+
else if (!isSilence(r.answer)) {
|
|
140
|
+
// The door spoke to her own key, so it heard the first knock: this is
|
|
141
|
+
// the answer, or the door's word for her. Its silence alone cannot be
|
|
142
|
+
// told from a refusal, so silence is followed by the knock as the heir,
|
|
143
|
+
// which the door then refuses if it had heard, and she hears silence.
|
|
144
|
+
k.spoke = true;
|
|
145
|
+
if (answered(r))
|
|
146
|
+
bind.answered[name] = true;
|
|
147
|
+
return r.answer;
|
|
114
148
|
}
|
|
115
|
-
// Refused. The door never heard her, so the heir is still hers to spend.
|
|
116
149
|
}
|
|
117
150
|
const first = inv.heir !== undefined && !k.spoke;
|
|
118
151
|
const keys = first ? { ward: inv.ward, heir: inv.heir, current: inv.secret, next: k.current, seq: k.seq } : { ward: inv.ward, heir: inv.heir ?? null, current: k.current, next: k.next, seq: k.seq };
|
|
119
|
-
const r = await inside.send(bind, keys, true, method, args, wanted);
|
|
152
|
+
const r = read(await inside.send(bind, keys, true, method, args, wanted));
|
|
120
153
|
k.sent = true; // bytes went out. whether they arrived is the far door's to know.
|
|
121
154
|
// The number she spoke under, and the key she announced, kept whoever
|
|
122
155
|
// answered and kept when nobody did. A reply lost on the way back is a
|
|
@@ -127,20 +160,12 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
127
160
|
k.current = keys.current;
|
|
128
161
|
k.next = keys.next;
|
|
129
162
|
}
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (isSilence(r) || isWord(r))
|
|
137
|
-
return r;
|
|
138
|
-
if ('silence' in r)
|
|
139
|
-
return silence;
|
|
140
|
-
if ('quo' in r)
|
|
141
|
-
return word(r.quo);
|
|
142
|
-
bind.answered[name] = true;
|
|
143
|
-
return r.object;
|
|
163
|
+
if (honoured(r))
|
|
164
|
+
k.spoke = true;
|
|
165
|
+
if (answered(r))
|
|
166
|
+
bind.answered[name] = true;
|
|
167
|
+
inside.wrote();
|
|
168
|
+
return r.answer;
|
|
144
169
|
};
|
|
145
170
|
// The id she took this relation under, if she took it. One relation is one
|
|
146
171
|
// ward and one heir whichever door it is reached by, so the standing and the
|
|
@@ -157,7 +182,7 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
157
182
|
// same relation, and the two would refuse each other.
|
|
158
183
|
const id = takenAs(nameOf(inv));
|
|
159
184
|
if (id !== undefined)
|
|
160
|
-
return
|
|
185
|
+
return standingAt(id).ask(method, args, wanted);
|
|
161
186
|
return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(word('late')))));
|
|
162
187
|
},
|
|
163
188
|
// Take consumes. Until now the relation lived in the knock record, under
|
|
@@ -194,12 +219,14 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
194
219
|
put(cells.standings, id, { id, digest: null, blueprint: null, seen: null });
|
|
195
220
|
delete bind.knocks[name];
|
|
196
221
|
delete bind.answered[name];
|
|
222
|
+
inside.wrote();
|
|
197
223
|
return Promise.resolve(id);
|
|
198
224
|
});
|
|
199
225
|
},
|
|
200
226
|
remove: (id) => {
|
|
201
227
|
drop(cells.standings, id);
|
|
202
228
|
drop(bind.standings, id);
|
|
229
|
+
inside.wrote();
|
|
203
230
|
},
|
|
204
231
|
};
|
|
205
232
|
const standingAt = (id) => ({
|
|
@@ -215,13 +242,19 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
215
242
|
return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted) : Promise.resolve(word('late')))));
|
|
216
243
|
},
|
|
217
244
|
});
|
|
245
|
+
// The record an ask writes into is the one it was issued on. A standing
|
|
246
|
+
// dropped and re-taken under the same id while the ask was in flight is
|
|
247
|
+
// another relation, and the blueprint that came back is not hers.
|
|
218
248
|
const asking = async (id, keys, rec, method, args, wanted) => {
|
|
219
249
|
const out = await send(keys, method, args, wanted, rec);
|
|
220
250
|
if (method === undefined && !isSilence(out) && !isWord(out)) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
251
|
+
// A blueprint her cells cannot keep, nested past the bound or carrying
|
|
252
|
+
// a key no harbor writes, is not Quo's bytes: silence, and nothing written.
|
|
253
|
+
if (cellFault(out, 'blueprint') !== null)
|
|
254
|
+
return silence;
|
|
255
|
+
if (rec && at(cells.standings, id) === rec) {
|
|
256
|
+
rec.blueprint = out;
|
|
257
|
+
rec.digest = rec.seen = await digest(out);
|
|
225
258
|
}
|
|
226
259
|
}
|
|
227
260
|
return out;
|
|
@@ -236,20 +269,48 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
236
269
|
cells,
|
|
237
270
|
// A being may make. The ward refuses what it refuses the owner: a key
|
|
238
271
|
// already booted, a class the harbor does not hold, and a throw at birth
|
|
239
|
-
// leaves the partition as it was. Reaching into
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
272
|
+
// leaves the partition as it was. Reaching into another is still the
|
|
273
|
+
// owner's alone: what a maker may have is a relation to what she made,
|
|
274
|
+
// by naming an id, and it is made the way every relation is. The being
|
|
275
|
+
// made mints an occupant for her maker under the maker's own key, so she
|
|
276
|
+
// knows who made her by that name and by nothing else; her maker knocks,
|
|
277
|
+
// takes it under the id she gave, and holds one standing like any other.
|
|
278
|
+
// Nothing half-lives: a relation that could not be made unmakes the
|
|
279
|
+
// being, who is a moment old and known to nobody.
|
|
280
|
+
boot: async (className, made, id) => {
|
|
281
|
+
if (id !== undefined && (reserved(id) || at(cells.standings, id) || at(cells.occupants, id)))
|
|
282
|
+
return null;
|
|
283
|
+
const born = inside.instantiate(made, className);
|
|
284
|
+
if (born === null || id === undefined)
|
|
285
|
+
return born;
|
|
286
|
+
const inv = await inside.relate(born, key);
|
|
287
|
+
if (inv) {
|
|
288
|
+
const out = await calls.knock(inv);
|
|
289
|
+
if (!isSilence(out) && !isWord(out) && (await calls.take(id, inv)) === id)
|
|
290
|
+
return born;
|
|
291
|
+
}
|
|
292
|
+
inside.unmake(born);
|
|
293
|
+
return null;
|
|
294
|
+
},
|
|
243
295
|
occupants: {
|
|
244
296
|
// rotation one: the ward mints the occupant's first key and gives the secret away.
|
|
245
297
|
// it keeps the pk beside the id and nothing else. the invitation IS the key.
|
|
246
|
-
invite: async (id) => {
|
|
298
|
+
invite: async (id, notes) => {
|
|
247
299
|
if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
|
|
248
300
|
return null;
|
|
249
|
-
|
|
301
|
+
// The key first, and nothing written until it exists: a record put
|
|
302
|
+
// before the mint would be a record a remove in the meantime drops
|
|
303
|
+
// with no heir to close, and the heir opened after it would name an
|
|
304
|
+
// id nobody holds, for a later invite under that id to resurrect.
|
|
250
305
|
const k = await inside.mintKey(bind);
|
|
306
|
+
if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
|
|
307
|
+
return null; // taken while the key was minted
|
|
308
|
+
// The notes are the terms the inviter minted under, hers to read on her
|
|
309
|
+
// gate. She may write more later; nobody outside ever writes them.
|
|
310
|
+
put(cells.occupants, id, { id, notes: notes ? { ...notes } : {} });
|
|
251
311
|
inside.openHeir(k.pk, key, id);
|
|
252
312
|
put(bind.occupants, id, k.pk);
|
|
313
|
+
inside.wrote();
|
|
253
314
|
return { ward: inside.pk, heir: k.pk, secret: k.seed };
|
|
254
315
|
},
|
|
255
316
|
remove: (id) => {
|
|
@@ -258,6 +319,7 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
258
319
|
drop(bind.occupants, id);
|
|
259
320
|
if (heir)
|
|
260
321
|
inside.closeHeir(heir); // the heir dies with the id
|
|
322
|
+
inside.wrote();
|
|
261
323
|
},
|
|
262
324
|
},
|
|
263
325
|
standings,
|
package/dist/ward/ward.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
+
import type { Cells, Stance } from '../being/types.ts';
|
|
1
2
|
import type { Ground, WardPointers } from './ground.ts';
|
|
3
|
+
import { type Bind } from './partition.ts';
|
|
4
|
+
import { type Door } from './door.ts';
|
|
5
|
+
export type Booted = Door & {
|
|
6
|
+
bind: Bind;
|
|
7
|
+
stance: Stance;
|
|
8
|
+
cells: Cells & {
|
|
9
|
+
class?: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
2
12
|
export declare function Ward(ground: Ground): Promise<WardPointers>;
|
package/dist/ward/ward.js
CHANGED
|
@@ -14,7 +14,7 @@ import { ownerAnswer } from './owner.js';
|
|
|
14
14
|
import { allow, within, LATE } from './allowance.js';
|
|
15
15
|
import { beingKey, openReply, sealAsk, wardKey, wardPadlock, wardSignPk } from './seal.js';
|
|
16
16
|
import { unhex } from './arithmetic.js';
|
|
17
|
-
import { guardCells, cellFault } from './cells.js';
|
|
17
|
+
import { guardCells, unguarded, cellFault } from './cells.js';
|
|
18
18
|
// How many minted pks she keeps behind her. Enough to name the keys in play.
|
|
19
19
|
const MINTED = 8;
|
|
20
20
|
// The ward. One call in, two pointers out. Async because keys are derived.
|
|
@@ -38,24 +38,25 @@ class Self {
|
|
|
38
38
|
this.g = ground;
|
|
39
39
|
this.key = key;
|
|
40
40
|
this.pk = key.pk;
|
|
41
|
-
this.p = open(ground.memory); // the partition
|
|
42
|
-
this.heirs = new Heirs(this.p);
|
|
41
|
+
this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
|
|
42
|
+
this.heirs = new Heirs(this.p, () => this.#wrote());
|
|
43
43
|
this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n)); // pointer one
|
|
44
44
|
this.#boot(this.pk, this); // the ward's ward is itself
|
|
45
45
|
// a restart is silent: every being in the cells is constructed again, unasked.
|
|
46
|
-
// A throw on a boot the owner asked for is that boot's
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
46
|
+
// A throw on a boot the owner asked for is that boot's error, and nothing
|
|
47
|
+
// half-lives. This boot nobody asked for, and there is nobody to hand an
|
|
48
|
+
// error to. Any constructor may throw, a default that is not a value, an
|
|
49
|
+
// author's mistake, a shape she does not recognise after an upgrade, and
|
|
50
|
+
// let through here it is one being taking down the ward and every other
|
|
51
|
+
// being in it, on a restart. She is absent instead: no door, so silence
|
|
52
|
+
// at the door, and her cells sit untouched, waiting for the run that can
|
|
53
|
+
// read them. Her row stays hers: nobody boots another class over it, the
|
|
54
|
+
// owner sees her as absent, and the owner may unboot her.
|
|
54
55
|
for (const [k, c] of Object.entries(this.p.beings)) {
|
|
55
56
|
if (k === this.pk || !c.class)
|
|
56
57
|
continue;
|
|
57
58
|
try {
|
|
58
|
-
this.#
|
|
59
|
+
this.#boot(k, c.class);
|
|
59
60
|
}
|
|
60
61
|
catch {
|
|
61
62
|
/* she is not here this run. the ward is, and so is everyone else. */
|
|
@@ -82,20 +83,50 @@ class Self {
|
|
|
82
83
|
return ownerAnswer({
|
|
83
84
|
pk: this.pk,
|
|
84
85
|
doors: this.doors,
|
|
86
|
+
absent: () => this.#absent(),
|
|
85
87
|
publicKey: () => this.p.public,
|
|
86
88
|
instantiate: (key, className) => this.#instantiate(key, className),
|
|
87
89
|
unboot: (key) => this.#unboot(key),
|
|
88
|
-
setPublic: (key) =>
|
|
90
|
+
setPublic: (key) => {
|
|
91
|
+
this.p.public = key;
|
|
92
|
+
this.#wrote();
|
|
93
|
+
},
|
|
89
94
|
}, asker, method, args);
|
|
90
95
|
}
|
|
91
96
|
// ---- ward functions. on the object. no stance reaches them.
|
|
97
|
+
// The partition was written. The harbor is told, and nothing more: what it
|
|
98
|
+
// does with the word is its own, and the ward never learns.
|
|
99
|
+
#wrote() {
|
|
100
|
+
this.g.wrote?.();
|
|
101
|
+
}
|
|
102
|
+
// The rows no door holds this run: beings whose class threw at birth or
|
|
103
|
+
// is not the harbor's to give. Key to class.
|
|
104
|
+
#absent() {
|
|
105
|
+
const out = {};
|
|
106
|
+
for (const [k, c] of Object.entries(this.p.beings))
|
|
107
|
+
if (k !== this.pk && !this.doors.has(k))
|
|
108
|
+
put(out, k, c.class ?? null);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
// A new being under a key nobody holds. A key with a row in the partition
|
|
112
|
+
// is booted, whether a door holds it this run or not: an absent being's
|
|
113
|
+
// row is hers, with every relation in it, and no class is booted over it.
|
|
114
|
+
// A throw at birth is that boot's refusal and nothing half-lives; the
|
|
115
|
+
// ward says which, since the owner hears objects.
|
|
92
116
|
#instantiate(key, className) {
|
|
93
|
-
if (!key || this.doors.has(key))
|
|
117
|
+
if (typeof key !== 'string' || typeof className !== 'string' || !key || this.doors.has(key) || at(this.p.beings, key))
|
|
94
118
|
return null;
|
|
95
|
-
|
|
119
|
+
let door;
|
|
120
|
+
try {
|
|
121
|
+
door = this.#boot(key, className);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return 'threw';
|
|
125
|
+
}
|
|
96
126
|
if (!door)
|
|
97
127
|
return null;
|
|
98
|
-
door.cells.class = className; // so a restart finds her
|
|
128
|
+
unguarded(door.cells).class = className; // so a restart finds her. the ward's key, written behind her guard
|
|
129
|
+
this.#wrote();
|
|
99
130
|
return door;
|
|
100
131
|
}
|
|
101
132
|
// The inverse of boot, and the only way a being leaves a ward. Her
|
|
@@ -104,16 +135,28 @@ class Self {
|
|
|
104
135
|
// rather than meeting a being who is simply not there any more. What is
|
|
105
136
|
// left after that is her cells and her bind table, and they go with her:
|
|
106
137
|
// a row naming a being no door holds would be read on the next restart
|
|
107
|
-
// and boot her again.
|
|
138
|
+
// and boot her again. An absent being leaves the same way: she has no
|
|
139
|
+
// stance to speak for her, so the ward closes her heirs itself.
|
|
108
140
|
#unboot(key) {
|
|
141
|
+
if (key === this.pk)
|
|
142
|
+
return null;
|
|
109
143
|
const door = this.doors.get(key);
|
|
110
|
-
|
|
144
|
+
const cells = door?.cells ?? at(this.p.beings, key);
|
|
145
|
+
if (!cells)
|
|
111
146
|
return null;
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
147
|
+
const ids = (r) => (r !== null && typeof r === 'object' ? Object.keys(r) : []);
|
|
148
|
+
const occupants = ids(cells.occupants), standings = ids(cells.standings);
|
|
149
|
+
if (door) {
|
|
150
|
+
for (const id of occupants)
|
|
151
|
+
door.stance.occupants.remove(id);
|
|
152
|
+
for (const id of standings)
|
|
153
|
+
door.stance.standings.remove(id);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const bind = at(this.p.bind, key);
|
|
157
|
+
for (const heir of Object.values(bind?.occupants ?? {}))
|
|
158
|
+
this.heirs.close(heir);
|
|
159
|
+
}
|
|
117
160
|
// A ward whose public being is gone answers arrivals for no heir with
|
|
118
161
|
// silence, as a ward that never had one does.
|
|
119
162
|
if (this.p.public === key)
|
|
@@ -121,13 +164,14 @@ class Self {
|
|
|
121
164
|
drop(this.p.beings, key);
|
|
122
165
|
drop(this.p.bind, key);
|
|
123
166
|
this.doors.delete(key);
|
|
167
|
+
this.#wrote();
|
|
124
168
|
return [...occupants, ...standings];
|
|
125
169
|
}
|
|
126
170
|
// Nothing is written until there is somebody to write it for: a class the
|
|
127
171
|
// harbor does not know makes no being, and a boot that made nobody must
|
|
128
172
|
// leave the partition as it found it.
|
|
129
173
|
#boot(key, classNameOrSelf) {
|
|
130
|
-
const cells = guardCells(at(this.p.beings, key) ?? emptyCells());
|
|
174
|
+
const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
|
|
131
175
|
const bind = at(this.p.bind, key) ?? emptyBind();
|
|
132
176
|
const stance = buildStance({
|
|
133
177
|
pk: this.pk,
|
|
@@ -138,21 +182,24 @@ class Self {
|
|
|
138
182
|
// A throw at birth is null to her, not a throw in her method: she asked
|
|
139
183
|
// for a being and got none, and her own answer is still hers to give.
|
|
140
184
|
instantiate: (k, className) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
185
|
+
const made = this.#instantiate(k, className);
|
|
186
|
+
return made && made !== 'threw' ? made.key : null;
|
|
147
187
|
},
|
|
188
|
+
// The two halves of a maker's way back, each the ward's own call: the
|
|
189
|
+
// being made mints an occupant for her maker, and a relation that
|
|
190
|
+
// never came about takes her out again.
|
|
191
|
+
relate: async (k, id) => (await this.doors.get(k)?.stance.occupants.invite(id)) ?? null,
|
|
192
|
+
unmake: (k) => void this.#unboot(k),
|
|
193
|
+
wrote: () => this.#wrote(),
|
|
148
194
|
}, key, cells, bind);
|
|
149
195
|
const being = classNameOrSelf === this ? this : this.g.instantiate(classNameOrSelf, stance);
|
|
150
|
-
if (!being)
|
|
151
|
-
return null;
|
|
196
|
+
if (!being || typeof being.answer !== 'function')
|
|
197
|
+
return null; // a being is anything with an answer, and nothing without one
|
|
152
198
|
put(this.p.beings, key, cells);
|
|
153
199
|
put(this.p.bind, key, bind);
|
|
154
200
|
const door = { key, cells, bind, stance, being };
|
|
155
201
|
this.doors.set(key, door);
|
|
202
|
+
this.#wrote();
|
|
156
203
|
return door;
|
|
157
204
|
}
|
|
158
205
|
async #mintKey(bind) {
|
|
@@ -164,6 +211,7 @@ class Self {
|
|
|
164
211
|
bind.minted.push(k.pk);
|
|
165
212
|
if (bind.minted.length > MINTED)
|
|
166
213
|
bind.minted.splice(0, bind.minted.length - MINTED);
|
|
214
|
+
this.#wrote();
|
|
167
215
|
return k;
|
|
168
216
|
}
|
|
169
217
|
// One send for every door, the ward's own included. Mine: never leaves.
|
|
@@ -190,10 +238,12 @@ class Self {
|
|
|
190
238
|
// Args that will not go through the seal are args that never left. A
|
|
191
239
|
// being holds three answers and a throw is not one of them, so an ask
|
|
192
240
|
// that cannot be sealed is told what is true of it: no door was reached.
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
241
|
+
// One object of values, by the rule her cells keep and the far door
|
|
242
|
+
// reads: a Date would arrive as a string, a function would not arrive at
|
|
243
|
+
// all, a list or a string would be refused as a stranger's bytes, and
|
|
244
|
+
// she would never know the far being was asked something other than
|
|
245
|
+
// what she said.
|
|
246
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args) || cellFault(args, 'args') !== null)
|
|
197
247
|
return unreached();
|
|
198
248
|
let sealed;
|
|
199
249
|
try {
|
|
@@ -227,6 +277,7 @@ class Self {
|
|
|
227
277
|
keys.current = keys.next; // the far door holds `next` as announced. move to it.
|
|
228
278
|
keys.next = null;
|
|
229
279
|
}
|
|
280
|
+
this.#wrote(); // the count moved, and the keys may have
|
|
230
281
|
return reply;
|
|
231
282
|
}
|
|
232
283
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quo-systems/quo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"./conformance": {
|
|
34
34
|
"types": "./dist/conformance/index.d.ts",
|
|
35
35
|
"default": "./dist/conformance/index.js"
|
|
36
|
-
}
|
|
36
|
+
},
|
|
37
|
+
"./vectors/*": "./vectors/*",
|
|
38
|
+
"./package.json": "./package.json"
|
|
37
39
|
},
|
|
38
40
|
"scripts": {
|
|
39
41
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
package/src/being/being.ts
CHANGED
|
@@ -18,11 +18,24 @@ export type AskSpec = {
|
|
|
18
18
|
// Names a subclass may not use for an ask, because they are the base's own.
|
|
19
19
|
const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'constructor']);
|
|
20
20
|
|
|
21
|
+
// Whether she has a method of that name, written on her own prototype chain
|
|
22
|
+
// below Object's. A name Object lends every object, `hasOwnProperty` or
|
|
23
|
+
// `toString`, is not a method she wrote; and a field she assigns in her own
|
|
24
|
+
// constructor is not there yet when the base checks, so an ask is a method
|
|
25
|
+
// on the prototype and nothing else.
|
|
26
|
+
const method = (self: object, name: string): boolean => {
|
|
27
|
+
for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
|
|
28
|
+
if (Object.hasOwn(p, name)) return typeof (p as Record<string, unknown>)[name] === 'function';
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
};
|
|
32
|
+
|
|
21
33
|
export class Being {
|
|
22
34
|
// Her cells' defaults. Merged in at birth, only where a key is missing, so
|
|
23
35
|
// a restart keeps what she wrote.
|
|
24
36
|
static cells: JsonObject = {};
|
|
25
|
-
// What she can be asked. Declaration order is blueprint order
|
|
37
|
+
// What she can be asked. Declaration order is blueprint order, except a
|
|
38
|
+
// name that reads as an array index, which the language lists first.
|
|
26
39
|
static asks: Record<string, AskSpec> = {};
|
|
27
40
|
|
|
28
41
|
readonly stance: Stance;
|
|
@@ -32,9 +45,11 @@ export class Being {
|
|
|
32
45
|
const C = this.constructor as typeof Being;
|
|
33
46
|
for (const name of Object.keys(C.asks)) {
|
|
34
47
|
if (RESERVED.has(name)) throw new Error(`ask '${name}' is a reserved name`);
|
|
35
|
-
if (
|
|
48
|
+
if (!method(this, name)) throw new Error(`ask '${name}' has no method on the prototype`);
|
|
36
49
|
}
|
|
37
|
-
|
|
50
|
+
// Own keys only: a default named after a member of Object's prototype is
|
|
51
|
+
// still hers, and still missing until she writes it.
|
|
52
|
+
for (const [k, v] of Object.entries(C.cells)) if (!Object.hasOwn(stance.cells, k)) stance.cells[k] = structuredClone(v);
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
get cells(): Cells {
|
|
@@ -46,8 +61,8 @@ export class Being {
|
|
|
46
61
|
get occupants(): Occupants {
|
|
47
62
|
return this.stance.occupants;
|
|
48
63
|
}
|
|
49
|
-
invite(id: string): Promise<Invitation | null> {
|
|
50
|
-
return this.stance.occupants.invite(id);
|
|
64
|
+
invite(id: string, notes?: JsonObject): Promise<Invitation | null> {
|
|
65
|
+
return this.stance.occupants.invite(id, notes);
|
|
51
66
|
}
|
|
52
67
|
knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer> {
|
|
53
68
|
return this.stance.standings.knock(invitation, method, args, wanted);
|
|
@@ -55,13 +70,14 @@ export class Being {
|
|
|
55
70
|
take(id: string, invitation: Invitation): Promise<string | null> {
|
|
56
71
|
return this.stance.standings.take(id, invitation);
|
|
57
72
|
}
|
|
58
|
-
// A new being of her ward, by class name, under a key she chooses.
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
// A new being of her ward, by class name, under a key she chooses. With an
|
|
74
|
+
// id, she holds a standing to the being she made, who knows her by her key.
|
|
75
|
+
boot(className: string, key: string, id?: string): Promise<string | null> {
|
|
76
|
+
return this.stance.boot(className, key, id);
|
|
61
77
|
}
|
|
62
78
|
// The occupant record for whoever is at the door. Undefined at a public being.
|
|
63
79
|
occupant(asker: Asker): OccupantRecord | undefined {
|
|
64
|
-
return asker.id
|
|
80
|
+
return asker.id !== undefined && Object.hasOwn(this.cells.occupants, asker.id) ? this.cells.occupants[asker.id] : undefined;
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
// Her blueprint for this asker. Override to shape it by hand.
|
|
@@ -87,7 +103,7 @@ export class Being {
|
|
|
87
103
|
// lookup would also find every name on Object's prototype: `valueOf`
|
|
88
104
|
// would answer with her stance, `toString` with a string, and neither is
|
|
89
105
|
// an ask she wrote. Only her own keys are asks, which is what describe
|
|
90
|
-
//
|
|
106
|
+
// shows. What she shows is what she can be asked.
|
|
91
107
|
const spec = typeof method === 'string' && Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
|
|
92
108
|
if (!spec || (spec.for && !spec.for(this.occupant(asker), asker))) return { error: 'unknown ask' };
|
|
93
109
|
const fn = (this as unknown as Record<string, (args: JsonObject, asker: Asker) => Reply | Promise<Reply>>)[method];
|
package/src/being/digest.ts
CHANGED
|
@@ -12,17 +12,19 @@ import type { Json } from './types.ts';
|
|
|
12
12
|
const absent = (v: unknown): boolean => v === undefined || typeof v === 'function' || typeof v === 'symbol';
|
|
13
13
|
|
|
14
14
|
// JCS for I-JSON values: sorted keys, no whitespace, JSON escaping. Numbers
|
|
15
|
-
// are serialized as ES does, which is what RFC 8785 specifies.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
// are serialized as ES does, which is what RFC 8785 specifies. A hole in a
|
|
16
|
+
// list is null, as JSON writes it.
|
|
17
|
+
export const canonical = (v: Json): string => {
|
|
18
|
+
if (Array.isArray(v)) return `[${Array.from(v, (slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`;
|
|
19
|
+
if (v !== null && typeof v === 'object') {
|
|
20
|
+
return `{${Object.keys(v)
|
|
21
|
+
.filter((k) => !absent(v[k]))
|
|
22
|
+
.sort()
|
|
23
|
+
.map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
|
|
24
|
+
.join(',')}}`;
|
|
25
|
+
}
|
|
26
|
+
return JSON.stringify(v);
|
|
27
|
+
};
|
|
26
28
|
|
|
27
29
|
const hex = (bytes: ArrayBuffer): string =>
|
|
28
30
|
Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
|