nervur 0.18.0 → 0.19.1
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 +7 -7
- package/dist/being/index.d.ts +1 -0
- package/dist/being/index.js +1 -0
- package/dist/being/lent.d.ts +32 -0
- package/dist/being/lent.js +63 -0
- package/dist/being/types.js +1 -1
- package/dist/conformance/beings.d.ts +2 -1
- package/dist/conformance/beings.js +7 -2
- package/dist/conformance/index.d.ts +0 -1
- package/dist/conformance/index.js +8 -5
- package/dist/harbor/box.d.ts +121 -0
- package/dist/harbor/box.js +121 -0
- package/dist/harbor/core.d.ts +5 -5
- package/dist/harbor/core.js +73 -72
- package/dist/harbor/index.d.ts +3 -1
- package/dist/harbor/index.js +3 -0
- package/dist/harbor/memory.d.ts +8 -4
- package/dist/harbor/memory.js +43 -11
- package/dist/harbor/reach.js +9 -1
- package/dist/harbor/store.js +1 -1
- package/dist/vector/cases.d.ts +4 -3
- package/dist/vector/cases.js +33 -5
- package/dist/vector/stand.js +2 -1
- package/dist/vector/world.d.ts +1 -0
- package/dist/vector/world.js +17 -6
- package/dist/ward/arithmetic.js +6 -2
- package/dist/ward/ground.d.ts +13 -7
- package/dist/ward/ground.js +29 -11
- package/dist/ward/heirs.js +4 -2
- package/dist/ward/index.d.ts +2 -1
- package/dist/ward/index.js +1 -0
- package/dist/ward/json.d.ts +2 -0
- package/dist/ward/json.js +163 -0
- package/dist/ward/partition.js +2 -2
- package/dist/ward/seal.js +14 -9
- package/dist/ward/stance.d.ts +0 -5
- package/dist/ward/ward.d.ts +1 -1
- package/dist/ward/ward.js +62 -27
- package/package.json +7 -5
- package/src/being/index.ts +1 -0
- package/src/being/lent.ts +66 -0
- package/src/being/types.ts +1 -1
- package/src/conformance/beings.ts +8 -3
- package/src/conformance/index.ts +18 -15
- package/src/harbor/box.ts +131 -0
- package/src/harbor/core.ts +23 -22
- package/src/harbor/index.ts +4 -1
- package/src/harbor/memory.ts +52 -12
- package/src/harbor/reach.ts +7 -1
- package/src/harbor/store.ts +1 -1
- package/src/vector/cases.ts +37 -9
- package/src/vector/stand.ts +2 -1
- package/src/vector/world.ts +18 -6
- package/src/ward/arithmetic.ts +6 -2
- package/src/ward/ground.ts +85 -103
- package/src/ward/heirs.ts +4 -2
- package/src/ward/index.ts +2 -1
- package/src/ward/json.ts +144 -0
- package/src/ward/partition.ts +2 -2
- package/src/ward/seal.ts +12 -8
- package/src/ward/stance.ts +1 -1
- package/src/ward/ward.ts +59 -27
package/src/conformance/index.ts
CHANGED
|
@@ -99,7 +99,7 @@ export { Printer, Shop, Customer, Echo, Maker, Member };
|
|
|
99
99
|
// expects instead of importing ours, and nothing under `src/conformance`
|
|
100
100
|
// reaches into `src/ward` for anything.
|
|
101
101
|
export const HEX64 = /^[0-9a-f]{64}$/; // one key, one secret, one digest
|
|
102
|
-
|
|
102
|
+
const HEX128 = /^[0-9a-f]{128}$/; // a ward pk: the signing pk then the padlock
|
|
103
103
|
// Her ward's word, as she receives it.
|
|
104
104
|
const said = (x: unknown, w: string): boolean => told(x) === w;
|
|
105
105
|
|
|
@@ -116,10 +116,10 @@ const picky = (w: World): World & Refusing => w as World & Refusing;
|
|
|
116
116
|
// ward's truth must be checkable wherever a ward runs, not only here.
|
|
117
117
|
export type Runner = (name: string, opts: { skip?: string | false }, fn: () => Promise<void> | void) => void;
|
|
118
118
|
|
|
119
|
-
// What the probe is asked to build. `lends` is the box's
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
119
|
+
// What the probe is asked to build. `lends` is the box's own ward: a name
|
|
120
|
+
// the box lends, and the class of the lent being it stands under it. A harness
|
|
121
|
+
// that stands none hands its wards no box, which is what a harbor with
|
|
122
|
+
// nothing to lend is.
|
|
123
123
|
export type Made = { lends?: Record<string, string> };
|
|
124
124
|
|
|
125
125
|
// `canRefuse` is the one flag that is off unless a harness says otherwise. A
|
|
@@ -291,7 +291,7 @@ export function conform(label: string, make: (made?: Made) => Promise<World>, {
|
|
|
291
291
|
assert.equal(Object.hasOwn(bob.cells.standings, id), false, `and nothing was written for ${id}`);
|
|
292
292
|
// The ward's half too, and not only hers: a standing filed here with no
|
|
293
293
|
// record of it in her cells is a lane nobody can see and nobody removes.
|
|
294
|
-
assert.equal(Object.hasOwn(w.census().binds.B
|
|
294
|
+
assert.equal(Object.hasOwn(w.census().binds.B.standings, id), false, `and the ward wrote no keys for ${id}`);
|
|
295
295
|
assert.equal(await bob.being.take(`as-${id}`, inv), `as-${id}`, 'and an ordinary id takes the same invitation');
|
|
296
296
|
}
|
|
297
297
|
// And the three calls are still the calls, not records that shadowed them.
|
|
@@ -478,7 +478,7 @@ export function conform(label: string, make: (made?: Made) => Promise<World>, {
|
|
|
478
478
|
assert.equal(shop.pk, pk, 'a ward that came back changed its pk');
|
|
479
479
|
assert.equal(JSON.stringify(able2.partition('S').heirs), mark, 'and it came back holding what it had honoured');
|
|
480
480
|
assert.deepEqual(await alice.being.buy('a second hat'), { ok: true, receipt: 'receipt for a second hat' }, 'the relation goes on, and she was never told');
|
|
481
|
-
assert.deepEqual(alice.cells.standings.shop
|
|
481
|
+
assert.deepEqual(alice.cells.standings.shop.id, 'shop', 'her side was not told either');
|
|
482
482
|
});
|
|
483
483
|
|
|
484
484
|
// Notes are the one thing on a record that a being seeds and a ward keeps
|
|
@@ -490,10 +490,10 @@ export function conform(label: string, make: (made?: Made) => Promise<World>, {
|
|
|
490
490
|
const seeded = { tier: { deep: 'gold' } } as unknown as JsonObject;
|
|
491
491
|
assert.ok((await alice.being.invite('noted', seeded)) !== null, 'she minted with notes');
|
|
492
492
|
(seeded.tier as JsonObject).deep = 'moved';
|
|
493
|
-
assert.deepEqual(alice.cells.occupants.noted
|
|
493
|
+
assert.deepEqual(alice.cells.occupants.noted.notes, { tier: { deep: 'gold' } }, 'the record is the copy, all the way down');
|
|
494
494
|
// And notes no harbor could write back are no invitation, for the reason
|
|
495
495
|
// every other write is refused: a record it could not keep.
|
|
496
|
-
assert.equal(await alice.being.invite('bad-notes', { n: 0 / 0 }
|
|
496
|
+
assert.equal(await alice.being.invite('bad-notes', { n: 0 / 0 }), null);
|
|
497
497
|
assert.equal(Object.hasOwn(alice.cells.occupants, 'bad-notes'), false, 'and nothing was written for it');
|
|
498
498
|
});
|
|
499
499
|
|
|
@@ -527,10 +527,12 @@ export function conform(label: string, make: (made?: Made) => Promise<World>, {
|
|
|
527
527
|
});
|
|
528
528
|
|
|
529
529
|
// Lend is boot's other half: what the device already has, reached the one
|
|
530
|
-
// way anything is reached. The
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
530
|
+
// way anything is reached. The device is one standing the ward holds at
|
|
531
|
+
// the box's being, and a lend is an ask on it; the being lent stands in
|
|
532
|
+
// a ward the harbor roots, so nothing here can look at her from the
|
|
533
|
+
// outside, and what the ward owes is what is asserted. She is handed an
|
|
534
|
+
// id, the standing is an ordinary one, and the invitation is nowhere she
|
|
535
|
+
// can read.
|
|
534
536
|
lends('what the device lends is a being: she is handed an id, asks on an ordinary standing, and never sees the invitation', async () => {
|
|
535
537
|
const w = await make({ lends: { pen: 'Member' } });
|
|
536
538
|
const maker = await w.boot<Maker>('MK', Maker);
|
|
@@ -542,7 +544,7 @@ export function conform(label: string, make: (made?: Made) => Promise<World>, {
|
|
|
542
544
|
// The keys are the ward's. Nothing of the relation is in her cells, which
|
|
543
545
|
// is the whole difference between this standing and one she gave away.
|
|
544
546
|
const c = w.census();
|
|
545
|
-
const heir = c.binds.MK
|
|
547
|
+
const heir = c.binds.MK.standings.pen.heir;
|
|
546
548
|
assert.equal(typeof heir, 'string', 'the ward bound a heir for it');
|
|
547
549
|
assert.equal(JSON.stringify(c.cells.MK).includes(heir!), false, 'and the invitation reached her nowhere');
|
|
548
550
|
// Null is every kind of no, and she cannot tell them apart.
|
|
@@ -555,7 +557,7 @@ export function conform(label: string, make: (made?: Made) => Promise<World>, {
|
|
|
555
557
|
const w = await make();
|
|
556
558
|
const maker = await w.boot<Maker>('MK', Maker);
|
|
557
559
|
assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'pen' }), { lent: null });
|
|
558
|
-
assert.equal(w.census().binds.MK
|
|
560
|
+
assert.equal(w.census().binds.MK.standings.pen, undefined, 'and nothing was written for it');
|
|
559
561
|
});
|
|
560
562
|
|
|
561
563
|
// The case the estate's walk reaches by weight and this reaches by hand: a
|
|
@@ -700,6 +702,7 @@ export function conform(label: string, make: (made?: Made) => Promise<World>, {
|
|
|
700
702
|
const names = (x: unknown) => bp(x).asks.map((a) => a.name);
|
|
701
703
|
assert.deepEqual(names(await alice.being.learn()), ['hello', 'buy', 'refund']);
|
|
702
704
|
assert.deepEqual(names(await bob.being.learn()), ['hello', 'buy']);
|
|
705
|
+
assert.deepEqual(await alice.being.standings.shop!.ask('refund'), { ok: true }, 'and what she is shown she can ask');
|
|
703
706
|
});
|
|
704
707
|
|
|
705
708
|
t('the digest is per relation: two occupants, two digests of one being, both true', async () => {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The box as one being. A harbor boots a ward of its own and one being in
|
|
3
|
+
// it, the box's own, who holds a standing at every lent being the box
|
|
4
|
+
// offers, under the name each is lent as, and keeps the roll of the wards
|
|
5
|
+
// the box hosts. Every hosted ward is handed one invitation on her at boot
|
|
6
|
+
// and takes it as its own standing; from then on the device is one
|
|
7
|
+
// standing, what it offers is her describe, and a lend is the ask `offer`
|
|
8
|
+
// on her.
|
|
9
|
+
//
|
|
10
|
+
// The harbor works in acts and holds no memory of its own. Act one boots the
|
|
11
|
+
// box's ward on the plain ground, no box, because it is the box; her lent
|
|
12
|
+
// beings come up, and the root places her standings and nothing else. Act
|
|
13
|
+
// two asks her who is hosted and boots each with one invitation. Everything
|
|
14
|
+
// the device is lives in her ward as a lent being's cells, placed through
|
|
15
|
+
// asks and read from no file.
|
|
16
|
+
//
|
|
17
|
+
// A runtime that needs her to be more, a resolver of names or a carrier of
|
|
18
|
+
// her lent beings' asks for a screen, extends her and overrides the asks.
|
|
19
|
+
import { Being, isInvitation, OWNER } from '../being/index.ts';
|
|
20
|
+
import { BOX } from '../being/lent.ts';
|
|
21
|
+
import type { Asker, BeingClass, Invitation, JsonObject, OccupantRecord } from '../being/index.ts';
|
|
22
|
+
import { OFFER, RETRACT, type WardPointers } from '../ward/ground.ts';
|
|
23
|
+
|
|
24
|
+
// A ward as its root holds it: the ask pointer is all the acts need.
|
|
25
|
+
export type Root = Pick<WardPointers, 'ask'>;
|
|
26
|
+
|
|
27
|
+
export { BOX } from '../being/lent.ts';
|
|
28
|
+
|
|
29
|
+
// The name of the box's own ward, one per harbor: the seed a memory harbor
|
|
30
|
+
// boots it under, and the name a store keeps it by.
|
|
31
|
+
export const BOX_SEED = 'box';
|
|
32
|
+
|
|
33
|
+
// A ward the box vouches for: invited by the root at act two, with the one
|
|
34
|
+
// note that says so. A screen or a model the root lets in has no such note
|
|
35
|
+
// and is shown nothing of the lend seam.
|
|
36
|
+
const ward = (occ: OccupantRecord | undefined) => occ?.notes.ward === true;
|
|
37
|
+
const root = (_occ: OccupantRecord | undefined, asker: Asker) => asker.id === OWNER;
|
|
38
|
+
|
|
39
|
+
export const BOX_ASKS = {
|
|
40
|
+
[OFFER]: { description: 'an invitation on the being lent under this name', input: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, for: ward },
|
|
41
|
+
[RETRACT]: { description: 'an offer under this name that was not taken, by its heir', input: { type: 'object', properties: { name: { type: 'string' }, heir: { type: 'string' } }, required: ['name', 'heir'] }, for: ward },
|
|
42
|
+
host: { description: 'a ward this box hosts from now', input: { type: 'object', properties: { seed: { type: 'string' } }, required: ['seed'] }, for: root },
|
|
43
|
+
roll: { description: 'the wards this box hosts', input: { type: 'object' }, for: root },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export class Box extends Being {
|
|
47
|
+
static override cells: JsonObject = { hosted: [], offers: {} };
|
|
48
|
+
static override asks = BOX_ASKS;
|
|
49
|
+
|
|
50
|
+
// Offer: the being under the name, asked to offer, answers her own
|
|
51
|
+
// invitation. A subclass that lends under more names than it holds
|
|
52
|
+
// standings, a contract's digest for one, resolves the name first and
|
|
53
|
+
// calls this with the standing's own.
|
|
54
|
+
async offer(args: JsonObject): Promise<JsonObject> {
|
|
55
|
+
const at = this.standings[args.name as string];
|
|
56
|
+
if (!at) return { error: 'lends none' };
|
|
57
|
+
const out = await at.ask(OFFER);
|
|
58
|
+
return isInvitation(out) ? out : { error: 'not offered' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async retract(args: JsonObject): Promise<JsonObject> {
|
|
62
|
+
const at = this.standings[args.name as string];
|
|
63
|
+
if (!at) return { error: 'lends none' };
|
|
64
|
+
const out = await at.ask(RETRACT, { heir: args.heir as string });
|
|
65
|
+
return out !== null && typeof out === 'object' && !Array.isArray(out) ? out : { retracted: null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A ward on the roll, and its one invitation on her. She mints it the first
|
|
69
|
+
// time and keeps it, and hands the same one at every later boot until it
|
|
70
|
+
// is spent: a ward that could not take the box at birth, the box down or
|
|
71
|
+
// the knock lost, takes it at the next, as the spec promises. A ward that
|
|
72
|
+
// holds the standing already ignores it, and a spent heir opens no door,
|
|
73
|
+
// so handing it again costs nothing. The value never leaves her ward: it
|
|
74
|
+
// is read by the root alone and rides in the ground and nowhere else.
|
|
75
|
+
async host(args: JsonObject): Promise<JsonObject> {
|
|
76
|
+
const hosted = ((this.cells.hosted as string[] | undefined) ??= []);
|
|
77
|
+
const offers = ((this.cells.offers as Record<string, Invitation> | undefined) ??= {});
|
|
78
|
+
if (typeof args.seed !== 'string') return { error: 'a seed is a word' };
|
|
79
|
+
if (!hosted.includes(args.seed)) hosted.push(args.seed);
|
|
80
|
+
if (!Object.hasOwn(offers, args.seed)) {
|
|
81
|
+
const inv = await this.invite(args.seed, { ward: true });
|
|
82
|
+
if (inv !== null) offers[args.seed] = inv;
|
|
83
|
+
}
|
|
84
|
+
return { hosted: [...hosted], invitation: offers[args.seed] ?? null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
roll(): JsonObject {
|
|
88
|
+
return { hosted: [...((this.cells.hosted as string[] | undefined) ?? [])] };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Act one's second half, on a box ward already booted: the box's being and
|
|
93
|
+
// each lent being booted by key, and her standing placed at each under its
|
|
94
|
+
// key by the root. A ward woken from its memory already holds all of it,
|
|
95
|
+
// and a key already booted is refused and costs nothing, so this is run at
|
|
96
|
+
// every boot and does the right thing at each.
|
|
97
|
+
export async function stand(box: Root, lent: Record<string, BeingClass>, own = 'Box'): Promise<void> {
|
|
98
|
+
const up = async (key: string, cls: string) => {
|
|
99
|
+
const out = (await box.ask('boot', { key, class: cls })) as { booted?: string; error?: string };
|
|
100
|
+
return out.booted === key;
|
|
101
|
+
};
|
|
102
|
+
await up(BOX, own);
|
|
103
|
+
for (const name of Object.keys(lent)) {
|
|
104
|
+
const fresh = await up(name, lent[name].name);
|
|
105
|
+
if (!fresh) continue;
|
|
106
|
+
await hold(box, name);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The box's standing at one lent being of her ward, under that being's key,
|
|
111
|
+
// placed by the root: the lent being invites the box's being as `box`, and
|
|
112
|
+
// she knocks. Refused where either already holds the relation.
|
|
113
|
+
export async function hold(box: Root, key: string): Promise<boolean> {
|
|
114
|
+
const inv = await box.ask('invite', { being: key, id: BOX });
|
|
115
|
+
if (!isInvitation(inv)) return false;
|
|
116
|
+
const out = (await box.ask('knock', { being: BOX, id: key, invitation: inv as unknown as JsonObject })) as { taken?: unknown };
|
|
117
|
+
return out?.taken === key;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// One invitation on the box's being for the ward named, and the ward on the
|
|
121
|
+
// roll: the same one at every boot, minted once by her and kept by her.
|
|
122
|
+
export async function offerTo(box: Root, seed: string): Promise<Invitation | undefined> {
|
|
123
|
+
const out = (await box.ask('ask', { being: BOX, method: 'host', args: { seed } })) as { invitation?: unknown };
|
|
124
|
+
return isInvitation(out?.invitation) ? out.invitation : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Who the box hosts, asked of the box's being.
|
|
128
|
+
export async function roll(box: Root): Promise<string[]> {
|
|
129
|
+
const out = (await box.ask('ask', { being: BOX, method: 'roll' })) as { hosted?: string[] };
|
|
130
|
+
return out.hosted ?? [];
|
|
131
|
+
}
|
package/src/harbor/core.ts
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
// that arrive from the wire go to an own door or a held socket and never
|
|
20
20
|
// onward by request or by fallback; that one rule is the rendezvous.
|
|
21
21
|
import { Ward } from '../ward/ward.ts';
|
|
22
|
-
import { maker, entropy as random, learnPk, type Ground, type WardPointers
|
|
23
|
-
import type { BeingClass, BeingLike } from '../being/types.ts';
|
|
22
|
+
import { maker, entropy as random, learnPk, type Ground, type WardPointers } from '../ward/ground.ts';
|
|
23
|
+
import type { BeingClass, BeingLike, Invitation } from '../being/types.ts';
|
|
24
24
|
import { silence } from '../being/silence.ts';
|
|
25
25
|
import { request, type Reach } from './reach.ts';
|
|
26
26
|
import { isWardPk, openReply, wardSignPk } from '../ward/seal.ts';
|
|
@@ -149,22 +149,21 @@ export class Harbor {
|
|
|
149
149
|
for (const say of this.announcers) say();
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
152
|
+
// The box, as one invitation per ward: what this device hands a ward it
|
|
153
|
+
// boots so that the ward takes the box's being as a standing. Set once
|
|
154
|
+
// the harbor's own ward is standing, which is the usual order: a terrain
|
|
155
|
+
// puts that ward up first, then boots the rest on a ground that can reach
|
|
156
|
+
// it. A terrain says which ward is handed one by overriding `boxFor`, and
|
|
157
|
+
// a stranger's ward is handed none.
|
|
158
|
+
offering: ((name: string) => Promise<Invitation | undefined>) | undefined;
|
|
159
159
|
|
|
160
|
-
constructor(store: Store, loader: Loader = async () => ({})
|
|
160
|
+
constructor(store: Store, loader: Loader = async () => ({})) {
|
|
161
161
|
this.store = store;
|
|
162
162
|
this.loader = loader;
|
|
163
|
-
this.lend = lend;
|
|
164
163
|
}
|
|
165
164
|
|
|
166
|
-
protected
|
|
167
|
-
return this.
|
|
165
|
+
protected boxFor(name: string, _record: WardRecord): Promise<Invitation | undefined> {
|
|
166
|
+
return this.offering?.(name) ?? Promise.resolve(undefined);
|
|
168
167
|
}
|
|
169
168
|
|
|
170
169
|
// Boot every ward the store keeps, and learn the hints. One ward that will
|
|
@@ -237,7 +236,7 @@ export class Harbor {
|
|
|
237
236
|
const moved: string[] = [];
|
|
238
237
|
for (const row of s.kept.keys()) {
|
|
239
238
|
const was = s.kept.get(row);
|
|
240
|
-
const now = rowsOf(s.memory, [row])[0]
|
|
239
|
+
const now = rowsOf(s.memory, [row])[0][1];
|
|
241
240
|
if (JSON.stringify(was) === JSON.stringify(now)) continue;
|
|
242
241
|
restoreRow(s.memory as unknown as Partition, row, was);
|
|
243
242
|
moved.push(row);
|
|
@@ -283,7 +282,7 @@ export class Harbor {
|
|
|
283
282
|
// rows where the store already has them.
|
|
284
283
|
#settle(name: string, s: Saving): void {
|
|
285
284
|
for (const [row, value] of s.under) {
|
|
286
|
-
const now = rowsOf(s.memory, [row])[0]
|
|
285
|
+
const now = rowsOf(s.memory, [row])[0][1];
|
|
287
286
|
if (JSON.stringify(value) === JSON.stringify(now)) s.kept.set(row, value);
|
|
288
287
|
else s.dirty.add(row);
|
|
289
288
|
}
|
|
@@ -546,13 +545,14 @@ export class Harbor {
|
|
|
546
545
|
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
|
|
547
546
|
const ground: Ground = {
|
|
548
547
|
seed,
|
|
549
|
-
memory,
|
|
550
548
|
// This ward's own classes first, then the harbor's: a ward that names a
|
|
551
549
|
// class of its own is answered with hers.
|
|
552
550
|
instantiate: maker(objects, classes, this.classes),
|
|
553
551
|
carry: (pk, bytes) => this.carry(pk, new Uint8Array(bytes)),
|
|
554
552
|
random,
|
|
555
|
-
|
|
553
|
+
memory: {
|
|
554
|
+
rows: memory,
|
|
555
|
+
told: (row) => this.#wrote(name, row),
|
|
556
556
|
// What the ward wrote is in the store when this resolves, and it says
|
|
557
557
|
// whether the store took it. The ward calls it at the end of every
|
|
558
558
|
// arrival, before it seals, so a call through a pointer has already
|
|
@@ -566,7 +566,7 @@ export class Harbor {
|
|
|
566
566
|
// A ward on its way out keeps nothing and refuses nothing: an arrival
|
|
567
567
|
// racing an unhost is not a store saying no, and saying so would be a
|
|
568
568
|
// lie in the one direction that matters.
|
|
569
|
-
|
|
569
|
+
kept: async () => {
|
|
570
570
|
const s = this.#saving.get(name);
|
|
571
571
|
if (!s || s.gone) return true;
|
|
572
572
|
const ok = await this.#flush(name);
|
|
@@ -585,7 +585,7 @@ export class Harbor {
|
|
|
585
585
|
// ward back. The ward is the one that knows, because a being reaching
|
|
586
586
|
// another being of her own ward goes to that ward's door directly and
|
|
587
587
|
// the harbor never sees that call at all.
|
|
588
|
-
|
|
588
|
+
during: () => {
|
|
589
589
|
const s = this.#saving.get(name);
|
|
590
590
|
if (!s) return () => {};
|
|
591
591
|
s.calls += 1;
|
|
@@ -603,9 +603,10 @@ export class Harbor {
|
|
|
603
603
|
else this.#settle(name, s);
|
|
604
604
|
};
|
|
605
605
|
},
|
|
606
|
+
},
|
|
606
607
|
};
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
608
|
+
const box = await this.boxFor(name, kept.record);
|
|
609
|
+
if (box) ground.box = box;
|
|
609
610
|
// What the store holds for this ward at birth is what it was just handed
|
|
610
611
|
// or just read, row by row. It is copied once here so that a being whose
|
|
611
612
|
// very first write of this run is refused is put back too, rather than
|
|
@@ -633,7 +634,7 @@ export class Harbor {
|
|
|
633
634
|
// here. It answers nothing, because a caller inside the process reads
|
|
634
635
|
// the fault off the harbor and an arrival is the only thing that needs a
|
|
635
636
|
// verdict of its own.
|
|
636
|
-
const save = async (): Promise<void> => void (await ground.
|
|
637
|
+
const save = async (): Promise<void> => void (await ground.memory.kept());
|
|
637
638
|
const door = (bytes: Uint8Array) => w.door(bytes);
|
|
638
639
|
const ask = async (method?: string, args?: Record<string, unknown>) => {
|
|
639
640
|
if (saving.gone) return silence;
|
package/src/harbor/index.ts
CHANGED
|
@@ -11,7 +11,10 @@ export { MemoryStore, values, rowsOf, type Store, type Kept, type WardRecord } f
|
|
|
11
11
|
// a being, and the partition back from the rows it was cut into.
|
|
12
12
|
export { HEAD, fromRows, rowsIn } from '../ward/partition.ts';
|
|
13
13
|
export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
|
|
14
|
-
export type { Ground, WardPointers,
|
|
14
|
+
export type { Ground, WardPointers, Memory } from '../ward/ground.ts';
|
|
15
|
+
export { BOX, OFFER, RETRACT, volatile } from '../ward/ground.ts';
|
|
16
|
+
// The box as one being, and the acts a harbor runs to stand it.
|
|
17
|
+
export { Box, BOX_SEED, BOX_ASKS, stand, hold, offerTo, roll } from './box.ts';
|
|
15
18
|
// What a harbor builds a ground out of. Convenience, never contract: a kit
|
|
16
19
|
// writing its own harbor may write these three again.
|
|
17
20
|
export { maker, entropy, learnPk } from '../ward/ground.ts';
|
package/src/harbor/memory.ts
CHANGED
|
@@ -4,9 +4,17 @@
|
|
|
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
|
-
|
|
8
|
-
|
|
7
|
+
//
|
|
8
|
+
// It works in acts, as every harbor does. `up` is act one: the box's own
|
|
9
|
+
// ward on the plain ground, the box's being and the lent beings in it.
|
|
10
|
+
// `host` is act two for one ward: on the roll, and handed one invitation
|
|
11
|
+
// on the box's being. A
|
|
12
|
+
// ward booted with `boot` alone is handed no box and lends nothing, which is
|
|
13
|
+
// what a suite of the ward wants.
|
|
14
|
+
import type { BeingClass, BeingLike, Invitation } from '../being/types.ts';
|
|
15
|
+
import { maker, entropy, learnPk, volatile, type Ground, type WardPointers } from '../ward/ground.ts';
|
|
9
16
|
import { hex } from '../ward/arithmetic.ts';
|
|
17
|
+
import { Box, BOX_SEED, stand, offerTo, roll } from './box.ts';
|
|
10
18
|
|
|
11
19
|
export type Booted = WardPointers & { pk: string };
|
|
12
20
|
export type WardFactory = (ground: Ground) => Promise<WardPointers>;
|
|
@@ -47,22 +55,22 @@ export class MemoryHarbor {
|
|
|
47
55
|
harbor.peers.add(this);
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
// boot by seed. the harbor gets a door and an ask, and learns the pk by
|
|
51
|
-
// `
|
|
52
|
-
//
|
|
53
|
-
async boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>,
|
|
58
|
+
// boot by seed. the harbor gets a door and an ask, and learns the pk by
|
|
59
|
+
// asking. `box` is the invitation on this device's box being, for this
|
|
60
|
+
// ward: left out, the ward holds no box and every lend is null.
|
|
61
|
+
async boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, box?: Invitation): Promise<Booted> {
|
|
54
62
|
this.partitions.set(seed, {});
|
|
55
|
-
return this.reboot(seed, Ward, classes,
|
|
63
|
+
return this.reboot(seed, Ward, classes, box);
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
// a restart: same seed, same partition, fresh ward. the ward's files are the harbor's to keep.
|
|
59
|
-
async reboot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>,
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
67
|
+
async reboot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, box?: Invitation): Promise<Booted> {
|
|
68
|
+
const rows = this.partitions.get(seed);
|
|
69
|
+
if (!rows) throw new Error(`no partition for ${seed}`);
|
|
62
70
|
const ground: Ground = {
|
|
63
71
|
seed,
|
|
64
|
-
memory,
|
|
65
|
-
...(
|
|
72
|
+
memory: volatile(rows),
|
|
73
|
+
...(box ? { box } : {}),
|
|
66
74
|
// One objects map for the whole harbor, not one per ward: every ward
|
|
67
75
|
// here is in this process and a test reaches any being of any of them.
|
|
68
76
|
instantiate: maker(this.objects, classes),
|
|
@@ -80,4 +88,36 @@ export class MemoryHarbor {
|
|
|
80
88
|
this.wards.set(seed, booted);
|
|
81
89
|
return booted;
|
|
82
90
|
}
|
|
91
|
+
|
|
92
|
+
// Act one. The box's own ward, the box's being and the lent beings named,
|
|
93
|
+
// each booted under its name and held by her under it. Run again on a
|
|
94
|
+
// harbor whose box ward stands, it changes nothing.
|
|
95
|
+
async up(Ward: WardFactory, lent: Record<string, BeingClass> = {}): Promise<Booted> {
|
|
96
|
+
const classes: Record<string, BeingClass> = { Box };
|
|
97
|
+
for (const C of Object.values(lent)) classes[C.name] = C;
|
|
98
|
+
const box = this.wards.get(BOX_SEED) ?? (await this.boot(BOX_SEED, Ward, classes));
|
|
99
|
+
await stand(box, lent);
|
|
100
|
+
return box;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get box(): Booted | undefined {
|
|
104
|
+
return this.wards.get(BOX_SEED);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Act two, one ward: on the roll and handed one invitation on the box's being.
|
|
108
|
+
// A harbor with no box ward up hosts as `boot` does, with nothing.
|
|
109
|
+
async host(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>): Promise<Booted> {
|
|
110
|
+
const box = this.box;
|
|
111
|
+
const inv = box ? await offerTo(box, seed) : undefined;
|
|
112
|
+
return this.partitions.has(seed) ? this.reboot(seed, Ward, classes, inv) : this.boot(seed, Ward, classes, inv);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Act two, whole: the box's being says who is hosted, and each comes up.
|
|
116
|
+
async hosted(Ward: WardFactory, classes: Record<string, BeingClass>): Promise<string[]> {
|
|
117
|
+
const box = this.box;
|
|
118
|
+
if (!box) return [];
|
|
119
|
+
const seeds = await roll(box);
|
|
120
|
+
for (const seed of seeds) await this.host(seed, Ward, classes);
|
|
121
|
+
return seeds;
|
|
122
|
+
}
|
|
83
123
|
}
|
package/src/harbor/reach.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// A reach that sent the bytes and lost the line afterwards answers nothing
|
|
21
21
|
// at all, and the ward's own bound ends the ask.
|
|
22
22
|
import { concat, hex, unhex } from '../ward/arithmetic.ts';
|
|
23
|
+
import { readJson } from '../ward/json.ts';
|
|
23
24
|
|
|
24
25
|
export type Carry = (pk: string, bytes: Uint8Array) => Promise<Uint8Array | undefined>;
|
|
25
26
|
export type Reach = { carry: Carry; close(): void };
|
|
@@ -136,7 +137,8 @@ export class Socket implements Reach {
|
|
|
136
137
|
line.addEventListener('message', (e) => {
|
|
137
138
|
if (typeof e.data === 'string') {
|
|
138
139
|
try {
|
|
139
|
-
const m =
|
|
140
|
+
const m = readJson(e.data) as { announce?: unknown; suite?: unknown };
|
|
141
|
+
if (m === null || typeof m !== 'object') return;
|
|
140
142
|
// Absent is this suite: a side that says nothing says one.
|
|
141
143
|
if (m.suite !== undefined && m.suite !== SUITE) {
|
|
142
144
|
this.far = [];
|
|
@@ -153,9 +155,13 @@ export class Socket implements Reach {
|
|
|
153
155
|
return;
|
|
154
156
|
}
|
|
155
157
|
const buf = e.data instanceof ArrayBuffer ? new Uint8Array(e.data) : ArrayBuffer.isView(e.data) ? new Uint8Array(e.data.buffer, e.data.byteOffset, e.data.byteLength) : undefined;
|
|
158
|
+
// A frame that is none of the three is not a frame: an unknown kind, an
|
|
159
|
+
// ask too short to hold a pk, a nothing with bytes after its id. It is
|
|
160
|
+
// dropped, and tells no ask waiting on its id that nothing was delivered.
|
|
156
161
|
if (!buf || buf.length < 5) return;
|
|
157
162
|
const kind = buf[0],
|
|
158
163
|
id = new DataView(buf.buffer, buf.byteOffset, buf.byteLength).getUint32(1);
|
|
164
|
+
if (kind > NONE || (kind === ASK && buf.length < 69) || (kind === NONE && buf.length !== 5)) return;
|
|
159
165
|
if (kind === ASK) {
|
|
160
166
|
const pk = hex(buf.subarray(5, 69));
|
|
161
167
|
// A deliver that rejects has delivered nothing this side can vouch
|
package/src/harbor/store.ts
CHANGED
|
@@ -46,7 +46,7 @@ export const values = (p: Record<string, unknown>): Record<string, unknown> => J
|
|
|
46
46
|
export const rowsOf = (partition: Record<string, unknown>, rows: readonly string[]): [string, Record<string, unknown> | undefined][] =>
|
|
47
47
|
rows.map((row) => {
|
|
48
48
|
const value = rowOf(partition as unknown as Partition, row);
|
|
49
|
-
return [row, value === undefined ? undefined : (values(value)
|
|
49
|
+
return [row, value === undefined ? undefined : (values(value))];
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
// One store's copy, brought up to date with the rows it was told about.
|
package/src/vector/cases.ts
CHANGED
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
import { Ward } from '../ward/ward.ts';
|
|
19
19
|
import * as seal from '../ward/seal.ts';
|
|
20
20
|
import { hex } from '../ward/arithmetic.ts';
|
|
21
|
+
import { digest } from '../being/digest.ts';
|
|
21
22
|
import type { Json, JsonObject } from '../being/types.ts';
|
|
22
23
|
import type { Booted } from '../harbor/memory.ts';
|
|
23
|
-
import { world, sealed, bound, secret, rnd, partitionDigest, Caller } from './world.ts';
|
|
24
|
+
import { world, sealed, bound, secret, rnd, drawn, partitionDigest, Caller } from './world.ts';
|
|
24
25
|
import type { World, Hand, Sealed } from './world.ts';
|
|
25
26
|
|
|
26
27
|
export type DoorRecord = {
|
|
@@ -33,9 +34,9 @@ export type DoorRecord = {
|
|
|
33
34
|
ask: string; // hex, exactly as the bytes arrive
|
|
34
35
|
reply: string; // hex, exactly as they leave
|
|
35
36
|
opens: Json | null; // what the reply opens to where the hand holds the lid's secret
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
wrote: boolean; // the
|
|
37
|
+
draws: number; // how many words of the stream were spent before the arrival
|
|
38
|
+
blueprint?: JsonObject; // the shape `seen` is the digest of, where a reply carries one
|
|
39
|
+
wrote: boolean; // the partition changed while the door judged. a refusal never sets it
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
// `pin` is where the case's own setup ends and the arrival begins. A record
|
|
@@ -50,21 +51,43 @@ export type Case = { case: string; name: string; state: string; ward: string; ar
|
|
|
50
51
|
|
|
51
52
|
// Stand one case: its world, its setup, its ask. What comes back is enough
|
|
52
53
|
// to knock, and enough for a verifier to check the hand before the door.
|
|
53
|
-
export async function stand(c: Case): Promise<{ w: World; before: string; arrival: Arrival }> {
|
|
54
|
+
export async function stand(c: Case): Promise<{ w: World; before: string; draws: number; arrival: Arrival }> {
|
|
54
55
|
const w = await world();
|
|
55
56
|
let before = await partitionDigest(w.b, c.ward);
|
|
56
57
|
const arrival = await c.arrive(w, async () => {
|
|
57
58
|
before = await partitionDigest(w.b, c.ward);
|
|
58
59
|
});
|
|
59
|
-
|
|
60
|
+
// Where the stream stands now, which is where the door is about to draw
|
|
61
|
+
// from. A kit that stood this case its own way sets its stream here.
|
|
62
|
+
return { w, before, draws: drawn(), arrival };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The blueprint a reply's `seen` was taken over. The host is the only being
|
|
66
|
+
// in this world whose shape reaches a stranger, and the asker is the id her
|
|
67
|
+
// heir was opened for, so there is one blueprint to find and it is checked
|
|
68
|
+
// against the digest before it is written down.
|
|
69
|
+
async function shown(w: World, seen: string): Promise<JsonObject | undefined> {
|
|
70
|
+
for (const id of Object.keys(w.host.cells.occupants)) {
|
|
71
|
+
const bp = w.host.describe({ id }) as unknown as JsonObject;
|
|
72
|
+
if ((await digest(bp)) === seen) return bp;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
async function record(c: Case): Promise<DoorRecord> {
|
|
63
|
-
const { w, before, arrival } = await stand(c);
|
|
78
|
+
const { w, before, draws, arrival } = await stand(c);
|
|
64
79
|
const judged = await arrival.door.door(arrival.ask.bytes);
|
|
65
80
|
const after = await partitionDigest(w.b, c.ward);
|
|
66
81
|
const opens = arrival.ask.open ? ((await arrival.ask.open(judged.bytes)) as Json | null) : null;
|
|
67
|
-
|
|
82
|
+
// A reply that carries `seen` carries a being's shape on the wire, which is
|
|
83
|
+
// the one place in this corpus where what a being answers reaches a
|
|
84
|
+
// stranger. So the blueprint that digest is taken over is written down
|
|
85
|
+
// beside it: a kit stands a being that answers exactly this to the empty
|
|
86
|
+
// ask, and its door's bytes are these bytes. Every other record is the
|
|
87
|
+
// door alone and needs no being at all.
|
|
88
|
+
const seen = opens !== null && typeof opens === 'object' && !Array.isArray(opens) ? (opens).seen : undefined;
|
|
89
|
+
const blueprint = typeof seen === 'string' ? await shown(w, seen) : undefined;
|
|
90
|
+
return { case: c.case, name: c.name, state: c.state, ward: c.ward, kind: arrival.kind, heard: judged.heard, draws, ask: hex(arrival.ask.bytes), reply: hex(judged.bytes), opens, ...(blueprint ? { blueprint } : {}), wrote: before !== after };
|
|
68
91
|
}
|
|
69
92
|
|
|
70
93
|
// One after another and never at once: the cases share one stream, and two
|
|
@@ -142,10 +165,15 @@ export const cases: Case[] = [
|
|
|
142
165
|
name: 'she is not there',
|
|
143
166
|
state: 'the ward restarted without her class: her heirs stand and she does not',
|
|
144
167
|
ward: 'B',
|
|
168
|
+
// Under the key she bound: the heir was spent by her knock and admits
|
|
169
|
+
// nothing now, so an arrival signed with its secret would be D6 and not
|
|
170
|
+
// this case. Her own key is what the door holds for her, and it is the
|
|
171
|
+
// key that hears the word.
|
|
145
172
|
arrive: async (w, pin) => {
|
|
173
|
+
const b = await bound(w);
|
|
146
174
|
const WB = await w.b.reboot('B', Ward, { Caller });
|
|
147
175
|
await pin();
|
|
148
|
-
return at(WB,
|
|
176
|
+
return at(WB, b.inv.heir!, 'word', { signer: b.own });
|
|
149
177
|
},
|
|
150
178
|
},
|
|
151
179
|
{
|
package/src/vector/stand.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// world runs on.
|
|
19
19
|
import { SUITE } from '../harbor/reach.ts';
|
|
20
20
|
import { hex } from '../ward/arithmetic.ts';
|
|
21
|
+
import { readJson } from '../ward/json.ts';
|
|
21
22
|
import { caseNamed, stand } from './cases.ts';
|
|
22
23
|
import type { World } from './world.ts';
|
|
23
24
|
import { partitionDigest } from './world.ts';
|
|
@@ -44,7 +45,7 @@ export class Stand {
|
|
|
44
45
|
if (method === 'POST' && p === '/stand') {
|
|
45
46
|
let named: { case?: unknown; name?: unknown };
|
|
46
47
|
try {
|
|
47
|
-
named =
|
|
48
|
+
named = readJson(body) as typeof named;
|
|
48
49
|
} catch {
|
|
49
50
|
return NONE;
|
|
50
51
|
}
|