nervur 0.18.0 → 0.19.0
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/store.js +1 -1
- package/dist/vector/cases.d.ts +4 -3
- package/dist/vector/cases.js +33 -5
- package/dist/vector/world.d.ts +1 -0
- package/dist/vector/world.js +17 -6
- 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/partition.js +2 -2
- package/dist/ward/seal.js +1 -1
- 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/store.ts +1 -1
- package/src/vector/cases.ts +37 -9
- package/src/vector/world.ts +18 -6
- package/src/ward/ground.ts +85 -103
- package/src/ward/heirs.ts +4 -2
- package/src/ward/index.ts +2 -1
- package/src/ward/partition.ts +2 -2
- package/src/ward/seal.ts +1 -1
- package/src/ward/stance.ts +1 -1
- package/src/ward/ward.ts +59 -27
package/dist/ward/stance.d.ts
CHANGED
|
@@ -17,9 +17,4 @@ export type Inside = {
|
|
|
17
17
|
unmake(key: string): void;
|
|
18
18
|
wrote(): void;
|
|
19
19
|
};
|
|
20
|
-
export type SealedInvitation = {
|
|
21
|
-
ward: string;
|
|
22
|
-
heir?: string;
|
|
23
|
-
secret?: string;
|
|
24
|
-
};
|
|
25
20
|
export declare function buildStance(inside: Inside, key: string, cells: Cells, bind: Bind): Stance;
|
package/dist/ward/ward.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Cells, Stance } from '../being/types.ts';
|
|
2
|
-
import type
|
|
2
|
+
import { type Ground, type WardPointers } from './ground.ts';
|
|
3
3
|
import { type Bind } from './partition.ts';
|
|
4
4
|
import { type Door } from './door.ts';
|
|
5
5
|
export type Resident = Door & {
|
package/dist/ward/ward.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// a stance built by the same code. Mints every key, seals every ask that
|
|
6
6
|
// leaves, opens every one that arrives.
|
|
7
7
|
import { silence, isSilence, unreached, isWord, word } from '../being/silence.js';
|
|
8
|
-
import { OWNER } from '../being/types.js';
|
|
8
|
+
import { OWNER, isInvitation } from '../being/types.js';
|
|
9
|
+
import { BOX, OFFER, RETRACT } from './ground.js';
|
|
9
10
|
import { at, drop, open, put, emptyBind, emptyCells, HEAD, MINTED } from './partition.js';
|
|
10
11
|
import { Heirs } from './heirs.js';
|
|
11
12
|
import { makeDoor } from './door.js';
|
|
@@ -30,14 +31,50 @@ class Self {
|
|
|
30
31
|
#birthing = new Set(); // keys whose constructor is running: taken, though no door and no row say so yet
|
|
31
32
|
door;
|
|
32
33
|
ask;
|
|
34
|
+
// Birth, in order: the ward as its own first being, the box taken, and
|
|
35
|
+
// only then every being the partition names constructed again. A being
|
|
36
|
+
// who takes what her box lends does it at her birth, so the box must be
|
|
37
|
+
// hers to ask before she is born, or a restart would be the one birth at
|
|
38
|
+
// which the device lends nothing.
|
|
33
39
|
static async boot(ground) {
|
|
34
|
-
|
|
40
|
+
const w = new Self(ground, await wardKey(ground.seed));
|
|
41
|
+
await w.#takeBox();
|
|
42
|
+
w.#reborn();
|
|
43
|
+
return w;
|
|
44
|
+
}
|
|
45
|
+
// The device, taken as one standing under `BOX` by the ward's own being,
|
|
46
|
+
// once: a ward woken from its partition already holds it. A knock that is
|
|
47
|
+
// not answered leaves the ward up and lending nothing this run.
|
|
48
|
+
async #takeBox() {
|
|
49
|
+
const inv = this.g.box;
|
|
50
|
+
const mine = this.doors.get(this.pk)?.stance;
|
|
51
|
+
if (!inv || !mine || mine.standings[BOX])
|
|
52
|
+
return;
|
|
53
|
+
const out = await mine.standings.knock(inv);
|
|
54
|
+
if (isSilence(out) || isWord(out))
|
|
55
|
+
return;
|
|
56
|
+
await mine.standings.take(BOX, inv);
|
|
57
|
+
}
|
|
58
|
+
// What this device lends this ward's beings: the box's being, asked to
|
|
59
|
+
// offer a name, answers the lent being's own invitation, and the taker knocks
|
|
60
|
+
// and takes it. The ward reads nothing in the value but that it is one.
|
|
61
|
+
async #lend(name, take) {
|
|
62
|
+
const box = this.doors.get(this.pk)?.stance.standings[BOX];
|
|
63
|
+
if (!box)
|
|
64
|
+
return false;
|
|
65
|
+
const inv = await box.ask(OFFER, { name });
|
|
66
|
+
if (!isInvitation(inv))
|
|
67
|
+
return false;
|
|
68
|
+
const took = await take(inv);
|
|
69
|
+
if (!took)
|
|
70
|
+
await box.ask(RETRACT, { name, heir: inv.heir ?? null });
|
|
71
|
+
return took;
|
|
35
72
|
}
|
|
36
73
|
constructor(ground, key) {
|
|
37
74
|
this.g = ground;
|
|
38
75
|
this.key = key;
|
|
39
76
|
this.pk = key.pk;
|
|
40
|
-
this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
|
|
77
|
+
this.p = open(ground.memory.rows); // the partition: the ward's own state, and every being's row
|
|
41
78
|
// Opening is itself a write: a memory nobody has written comes back with
|
|
42
79
|
// a version stamped on it and the ward's own tables under it. A ward that
|
|
43
80
|
// said nothing here would have every being of it kept and the head that
|
|
@@ -47,6 +84,24 @@ class Self {
|
|
|
47
84
|
this.heirs = new Heirs(this.p, () => this.#wrote(HEAD));
|
|
48
85
|
this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n), () => this.#keep()); // pointer one
|
|
49
86
|
this.#boot(this.pk, () => this); // the ward's ward is itself
|
|
87
|
+
// pointer two. the owner. the same answer every named asker gets, asker filled in as owner.
|
|
88
|
+
this.ask = async (method, args = {}) => {
|
|
89
|
+
try {
|
|
90
|
+
const out = await this.answer({ id: OWNER }, method, args);
|
|
91
|
+
// The same question the door asks, in the same place: after she has
|
|
92
|
+
// answered and before the owner holds the answer. Silence is what a
|
|
93
|
+
// failed ask says here, so a store that refused is silence, and the
|
|
94
|
+
// owner reads it as it reads every other ask that did not take.
|
|
95
|
+
if (!(await this.#keep().catch(() => false)))
|
|
96
|
+
return silence;
|
|
97
|
+
return isSilence(out) || isWord(out) ? silence : out;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return silence;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
#reborn() {
|
|
50
105
|
// a restart is silent: every being in the cells is constructed again, unasked.
|
|
51
106
|
// A throw on a boot the owner asked for is that boot's error, and nothing
|
|
52
107
|
// half-lives. This boot nobody asked for, and there is nobody to hand an
|
|
@@ -68,22 +123,6 @@ class Self {
|
|
|
68
123
|
/* she is not here this run. the ward is, and so is everyone else. */
|
|
69
124
|
}
|
|
70
125
|
}
|
|
71
|
-
// pointer two. the owner. the same answer every named asker gets, asker filled in as owner.
|
|
72
|
-
this.ask = async (method, args = {}) => {
|
|
73
|
-
try {
|
|
74
|
-
const out = await this.answer({ id: OWNER }, method, args);
|
|
75
|
-
// The same question the door asks, in the same place: after she has
|
|
76
|
-
// answered and before the owner holds the answer. Silence is what a
|
|
77
|
-
// failed ask says here, so a store that refused is silence, and the
|
|
78
|
-
// owner reads it as it reads every other ask that did not take.
|
|
79
|
-
if (!(await this.#keep().catch(() => false)))
|
|
80
|
-
return silence;
|
|
81
|
-
return isSilence(out) || isWord(out) ? silence : out;
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return silence;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
126
|
}
|
|
88
127
|
// ---- the ward as a being. asked only by its owners: the root, through
|
|
89
128
|
// the ask pointer, and its occupants, through the door, whom only the root
|
|
@@ -116,14 +155,14 @@ class Self {
|
|
|
116
155
|
// because a harbor told only that something moved must keep all of it, and
|
|
117
156
|
// a being's row is the one part of a partition that grows without limit.
|
|
118
157
|
#wrote(row) {
|
|
119
|
-
this.g.
|
|
158
|
+
this.g.memory.told(row);
|
|
120
159
|
}
|
|
121
160
|
// Whether what was written is kept, asked once at the end of an arrival
|
|
122
161
|
// and never during one. A harbor that keeps nothing was never going to
|
|
123
162
|
// refuse anything, so its silence here is yes: a ward driven with no store
|
|
124
163
|
// under it answers exactly as it did before a harbor could say no.
|
|
125
164
|
async #keep() {
|
|
126
|
-
return this.g.
|
|
165
|
+
return this.g.memory.kept();
|
|
127
166
|
}
|
|
128
167
|
// The rows no door holds this run: beings whose class threw at birth or
|
|
129
168
|
// is not the harbor's to give. Key to class.
|
|
@@ -233,11 +272,7 @@ class Self {
|
|
|
233
272
|
// never came about takes her out again.
|
|
234
273
|
relate: async (k, id) => (await this.doors.get(k)?.stance.occupants.invite(id)) ?? null,
|
|
235
274
|
unmake: (k) => void this.#unboot(k),
|
|
236
|
-
|
|
237
|
-
// inside the harbor's own call and reads nothing in the value: which
|
|
238
|
-
// names there are, which ward may ask for one, and what becomes of one
|
|
239
|
-
// she did not take, is the harbor's and no word of the ward.
|
|
240
|
-
lend: async (name, take) => (await this.g.lend?.(name, take)) ?? false,
|
|
275
|
+
lend: (name, take) => this.#lend(name, take),
|
|
241
276
|
wrote: () => this.#wrote(key),
|
|
242
277
|
}, key, cells, bind);
|
|
243
278
|
const being = make(stance);
|
|
@@ -274,7 +309,7 @@ class Self {
|
|
|
274
309
|
// the reply opens, and whatever the being writes on either side of her
|
|
275
310
|
// own ask. A harbor may write those rows down; what it may not do is put
|
|
276
311
|
// the ward back to them.
|
|
277
|
-
const lower = this.g.
|
|
312
|
+
const lower = this.g.memory.during();
|
|
278
313
|
try {
|
|
279
314
|
return await this.#sent(bind, keys, method, args, wanted, row);
|
|
280
315
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nervur",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
7
7
|
"protocol",
|
|
@@ -43,9 +43,11 @@
|
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
46
|
-
"check": "node
|
|
47
|
-
"
|
|
48
|
-
"
|
|
46
|
+
"check": "node ../../test.mjs \"test/*.test.ts\"",
|
|
47
|
+
"experimental": "node ../../test.mjs \"experimental/*.test.ts\"",
|
|
48
|
+
"deep": "node --test --test-timeout=300000 \"test/terrain/*.test.ts\"",
|
|
49
|
+
"cover": "node --test --test-timeout=60000 --experimental-test-coverage --test-coverage-exclude=\"**/dist/**\" --test-coverage-exclude=\"**/test/**\" --test-coverage-lines=99 --test-coverage-branches=93 --test-coverage-functions=95 \"test/*.test.ts\"",
|
|
50
|
+
"prepublishOnly": "test \"$NERVUR_GATED\" = 1 || { echo 'a publish is /release, from the root, on the human'\"'\"'s word' >&2; exit 1; }"
|
|
49
51
|
},
|
|
50
52
|
"publishConfig": {
|
|
51
53
|
"access": "public"
|
package/src/being/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// nervur — the Being side. What a being author imports, if anything.
|
|
3
3
|
export { Being, type AskSpec } from './being.ts';
|
|
4
|
+
export { Lent, BOX, LENT_ASKS } from './lent.ts';
|
|
4
5
|
export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.ts';
|
|
5
6
|
export { digest, canonical } from './digest.ts';
|
|
6
7
|
export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs, isInvitation } from './types.ts';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A lent being: a being of the box's own ward, standing for one thing the
|
|
3
|
+
// device can do. What makes her one is two asks the box's being alone may
|
|
4
|
+
// make, whom she knows as the occupant `box`. `offer`: she mints an
|
|
5
|
+
// invitation on herself and answers it, so that a ward the box vouches for
|
|
6
|
+
// may knock and take a standing at her. `retract`: an offer that was not
|
|
7
|
+
// taken, named by its heir, is removed, so a refused lend leaves nothing.
|
|
8
|
+
//
|
|
9
|
+
// The two are the base's and never a subclass's to declare: a class says
|
|
10
|
+
// her own asks and nothing of the seam she is lent through, so they are
|
|
11
|
+
// answered here before her asks are read, and shown to the box alone. No
|
|
12
|
+
// root mints for her, and nothing else about her is decided here.
|
|
13
|
+
//
|
|
14
|
+
// A convenience, not a contract: a being that answers the two the same way
|
|
15
|
+
// to `box` is lendable, whatever she extends.
|
|
16
|
+
import { Being } from './being.ts';
|
|
17
|
+
import type { Asker, Blueprint, JsonObject, Reply } from './types.ts';
|
|
18
|
+
|
|
19
|
+
// The one word for the box's being: her key in the box's ward, the id every
|
|
20
|
+
// lent being knows her by, and the standing every hosted ward takes at
|
|
21
|
+
// birth.
|
|
22
|
+
export const BOX = 'box';
|
|
23
|
+
|
|
24
|
+
export const LENT_ASKS = {
|
|
25
|
+
offer: { name: 'offer', description: 'an invitation on me, for a ward the box vouches for', input: { type: 'object' } },
|
|
26
|
+
retract: { name: 'retract', description: 'an offer that was not taken, by its heir', input: { type: 'object', properties: { heir: { type: 'string' } }, required: ['heir'] } },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class Lent extends Being {
|
|
30
|
+
// The offers she has open, heir to the id she minted under.
|
|
31
|
+
#open(): Record<string, string> {
|
|
32
|
+
return ((this.cells.open as Record<string, string> | undefined) ??= {});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async offer(): Promise<JsonObject> {
|
|
36
|
+
const n = ((this.cells.lent as number | undefined) ?? 0) + 1;
|
|
37
|
+
this.cells.lent = n;
|
|
38
|
+
const id = `lent:${n}`;
|
|
39
|
+
const inv = await this.invite(id);
|
|
40
|
+
if (inv === null) return { error: 'could not mint' };
|
|
41
|
+
this.#open()[inv.heir!] = id;
|
|
42
|
+
return inv;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
retract(args: JsonObject): JsonObject {
|
|
46
|
+
const open = this.#open();
|
|
47
|
+
const heir = typeof args.heir === 'string' ? args.heir : '';
|
|
48
|
+
const id = Object.hasOwn(open, heir) ? open[heir] : undefined;
|
|
49
|
+
if (id === undefined) return { retracted: null };
|
|
50
|
+
this.occupants.remove(id);
|
|
51
|
+
delete open[heir];
|
|
52
|
+
return { retracted: id };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override describe(asker: Asker): Blueprint {
|
|
56
|
+
const bp = super.describe(asker);
|
|
57
|
+
if (asker.id !== BOX) return bp;
|
|
58
|
+
return { ...bp, asks: [...bp.asks, LENT_ASKS.offer, LENT_ASKS.retract] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override async answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
|
|
62
|
+
if (asker.id === BOX && method === 'offer') return this.offer();
|
|
63
|
+
if (asker.id === BOX && method === 'retract') return this.retract(args);
|
|
64
|
+
return super.answer(asker, method, args);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/being/types.ts
CHANGED
|
@@ -43,7 +43,7 @@ export const invitationArgs = (inv: Invitation): JsonObject => ({
|
|
|
43
43
|
|
|
44
44
|
// Whether a value that arrived is an invitation. The shape is the spec's, so
|
|
45
45
|
// the reading of it is too, and it is one reading: a form that
|
|
46
|
-
// asks a guest for one, a
|
|
46
|
+
// asks a guest for one, a lent being handed one to wake her maker by, and a
|
|
47
47
|
// shell that finds one in a link all ask the same question, and a kinder
|
|
48
48
|
// answer in one of them is a value that fails at a door instead of at the
|
|
49
49
|
// edge it came in by. A ward pk is a hundred and twenty-eight lowercase hex,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// shop, a customer on the base class, a maker, a member, and one raw being
|
|
4
4
|
// with no base at all.
|
|
5
5
|
import { Being } from '../being/being.ts';
|
|
6
|
+
import { Lent } from '../being/lent.ts';
|
|
6
7
|
import { answered, isSilence, isUnreached, isWord, wordOf } from '../being/silence.ts';
|
|
7
8
|
import { invitationArgs } from '../being/types.ts';
|
|
8
9
|
import type { Ask, Asker, Blueprint, Invitation, Json, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
|
|
@@ -156,9 +157,13 @@ export class Maker extends Being {
|
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
// A member is also lendable: lent by a box that stands her in its own ward,
|
|
161
|
+
// she offers an invitation on herself when the box's being asks, and is an
|
|
162
|
+
// ordinary being to everyone else.
|
|
163
|
+
export class Member extends Lent {
|
|
164
|
+
static override cells = { ...Lent.cells, heard: [] as JsonObject[] };
|
|
161
165
|
static override asks = {
|
|
166
|
+
...Lent.asks,
|
|
162
167
|
ping: { input: { type: 'object' } },
|
|
163
168
|
relay: { input: { type: 'object', properties: { id: { type: 'string' }, method: { type: 'string' }, time: { type: 'number' } }, required: ['id'] } },
|
|
164
169
|
};
|
|
@@ -178,6 +183,6 @@ export class Member extends Being {
|
|
|
178
183
|
const out = await standing.ask(method as string | undefined, (args ?? {}) as JsonObject, time === undefined ? undefined : { time: time as number });
|
|
179
184
|
if (isSilence(out)) return { said: 'silence', out: null };
|
|
180
185
|
if (isWord(out)) return { said: wordOf(out), out: null };
|
|
181
|
-
return { said: null, out: out
|
|
186
|
+
return { said: null, out: out };
|
|
182
187
|
}
|
|
183
188
|
}
|
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
|
+
}
|