nervur 0.19.2 → 0.20.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 +66 -101
- package/dist/being/being.d.ts +7 -15
- package/dist/being/being.js +16 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +9 -0
- package/dist/being/faculty.js +68 -0
- package/dist/being/index.d.ts +4 -5
- package/dist/being/index.js +5 -5
- package/dist/being/types.d.ts +35 -62
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- package/dist/contract/index.d.ts +26 -0
- package/dist/contract/index.js +19 -0
- package/dist/crypto/aes.d.ts +3 -0
- package/dist/crypto/aes.js +24 -0
- package/dist/crypto/bytes.d.ts +6 -0
- package/dist/crypto/bytes.js +33 -0
- package/dist/crypto/ed25519.d.ts +3 -0
- package/dist/crypto/ed25519.js +85 -0
- package/dist/crypto/hash.d.ts +2 -0
- package/dist/crypto/hash.js +11 -0
- package/dist/crypto/index.d.ts +7 -0
- package/dist/crypto/index.js +10 -0
- package/dist/crypto/json.d.ts +8 -0
- package/dist/crypto/json.js +250 -0
- package/dist/crypto/mlkem.d.ts +12 -0
- package/dist/crypto/mlkem.js +36 -0
- package/dist/crypto/subtle.d.ts +10 -0
- package/dist/crypto/subtle.js +29 -0
- package/dist/crypto/x25519.d.ts +2 -0
- package/dist/crypto/x25519.js +21 -0
- package/dist/folder/index.d.ts +13 -0
- package/dist/folder/index.js +125 -0
- package/dist/harbor/dock.d.ts +19 -0
- package/dist/harbor/dock.js +92 -0
- package/dist/harbor/harbor.d.ts +22 -0
- package/dist/harbor/harbor.js +84 -0
- package/dist/harbor/index.d.ts +4 -10
- package/dist/harbor/index.js +5 -18
- package/dist/harbor/registry.d.ts +7 -0
- package/dist/harbor/registry.js +13 -0
- package/dist/harbor/terrain.d.ts +8 -0
- package/dist/harbor/terrain.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/pointer/bodies.d.ts +20 -0
- package/dist/pointer/bodies.js +45 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +34 -0
- package/dist/pointer/world.js +76 -0
- package/dist/quo/door.d.ts +34 -0
- package/dist/quo/door.js +172 -0
- package/dist/quo/index.d.ts +8 -0
- package/dist/quo/index.js +11 -0
- package/dist/quo/invitation.d.ts +7 -0
- package/dist/quo/invitation.js +15 -0
- package/dist/quo/keys.d.ts +40 -0
- package/dist/quo/keys.js +79 -0
- package/dist/quo/payload.d.ts +15 -0
- package/dist/quo/payload.js +55 -0
- package/dist/quo/relations.d.ts +40 -0
- package/dist/quo/relations.js +33 -0
- package/dist/quo/reply.d.ts +13 -0
- package/dist/quo/reply.js +35 -0
- package/dist/quo/seal.d.ts +42 -0
- package/dist/quo/seal.js +78 -0
- package/dist/quo/standing.d.ts +39 -0
- package/dist/quo/standing.js +90 -0
- package/dist/ward/allowance.d.ts +13 -9
- package/dist/ward/allowance.js +28 -67
- package/dist/ward/cells.d.ts +6 -6
- package/dist/ward/cells.js +67 -179
- package/dist/ward/index.d.ts +5 -10
- package/dist/ward/index.js +6 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/stance.d.ts +29 -20
- package/dist/ward/stance.js +173 -406
- package/dist/ward/ward-being.d.ts +33 -0
- package/dist/ward/ward-being.js +76 -0
- package/dist/ward/ward.d.ts +43 -11
- package/dist/ward/ward.js +215 -363
- package/package.json +19 -33
- package/src/being/being.ts +31 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +66 -0
- package/src/being/index.ts +5 -6
- package/src/being/types.ts +47 -145
- package/src/being/words.ts +31 -0
- package/src/contract/index.ts +44 -0
- package/src/crypto/aes.ts +26 -0
- package/src/crypto/bytes.ts +37 -0
- package/src/crypto/ed25519.ts +84 -0
- package/src/crypto/hash.ts +14 -0
- package/src/crypto/index.ts +10 -0
- package/src/crypto/json.ts +241 -0
- package/src/crypto/mlkem.ts +38 -0
- package/src/crypto/subtle.ts +33 -0
- package/src/crypto/x25519.ts +21 -0
- package/src/folder/index.ts +134 -0
- package/src/harbor/dock.ts +101 -0
- package/src/harbor/harbor.ts +105 -0
- package/src/harbor/index.ts +5 -19
- package/src/harbor/registry.ts +20 -0
- package/src/harbor/terrain.ts +13 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +47 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +91 -0
- package/src/quo/door.ts +178 -0
- package/src/quo/index.ts +11 -0
- package/src/quo/invitation.ts +15 -0
- package/src/quo/keys.ts +91 -0
- package/src/quo/payload.ts +61 -0
- package/src/quo/relations.ts +62 -0
- package/src/quo/reply.ts +38 -0
- package/src/quo/seal.ts +101 -0
- package/src/quo/standing.ts +111 -0
- package/src/stand/main.ts +19 -0
- package/src/stand/stand.ts +178 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +6 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/stance.ts +185 -420
- package/src/ward/ward-being.ts +97 -0
- package/src/ward/ward.ts +229 -363
- package/dist/being/lent.d.ts +0 -32
- package/dist/being/lent.js +0 -72
- package/dist/being/silence.d.ts +0 -12
- package/dist/being/silence.js +0 -41
- package/dist/conformance/assert.d.ts +0 -11
- package/dist/conformance/assert.js +0 -106
- package/dist/conformance/beings.d.ts +0 -199
- package/dist/conformance/beings.js +0 -188
- package/dist/conformance/estate.d.ts +0 -5
- package/dist/conformance/estate.js +0 -388
- package/dist/conformance/index.d.ts +0 -80
- package/dist/conformance/index.js +0 -819
- package/dist/conformance/reach.d.ts +0 -10
- package/dist/conformance/reach.js +0 -72
- package/dist/conformance/store.d.ts +0 -5
- package/dist/conformance/store.js +0 -113
- package/dist/harbor/box.d.ts +0 -121
- package/dist/harbor/box.js +0 -121
- package/dist/harbor/core.d.ts +0 -55
- package/dist/harbor/core.js +0 -662
- package/dist/harbor/dial.d.ts +0 -9
- package/dist/harbor/dial.js +0 -81
- package/dist/harbor/memory.d.ts +0 -29
- package/dist/harbor/memory.js +0 -104
- package/dist/harbor/reach.d.ts +0 -36
- package/dist/harbor/reach.js +0 -199
- package/dist/harbor/store.d.ts +0 -35
- package/dist/harbor/store.js +0 -62
- package/dist/vector/cases.d.ts +0 -42
- package/dist/vector/cases.js +0 -223
- package/dist/vector/index.d.ts +0 -6
- package/dist/vector/index.js +0 -8
- package/dist/vector/stand.d.ts +0 -9
- package/dist/vector/stand.js +0 -77
- package/dist/vector/world.d.ts +0 -144
- package/dist/vector/world.js +0 -209
- package/dist/ward/arithmetic.d.ts +0 -31
- package/dist/ward/arithmetic.js +0 -249
- package/dist/ward/door.d.ts +0 -18
- package/dist/ward/door.js +0 -186
- package/dist/ward/ground.d.ts +0 -29
- package/dist/ward/ground.js +0 -56
- package/dist/ward/heirs.d.ts +0 -13
- package/dist/ward/heirs.js +0 -117
- package/dist/ward/json.d.ts +0 -2
- package/dist/ward/json.js +0 -163
- package/dist/ward/owner.d.ts +0 -13
- package/dist/ward/owner.js +0 -220
- package/dist/ward/seal.d.ts +0 -52
- package/dist/ward/seal.js +0 -150
- package/src/being/lent.ts +0 -72
- package/src/being/silence.ts +0 -46
- package/src/conformance/assert.ts +0 -100
- package/src/conformance/beings.ts +0 -188
- package/src/conformance/estate.ts +0 -412
- package/src/conformance/index.ts +0 -965
- package/src/conformance/reach.ts +0 -83
- package/src/conformance/store.ts +0 -125
- package/src/harbor/box.ts +0 -131
- package/src/harbor/core.ts +0 -699
- package/src/harbor/dial.ts +0 -112
- package/src/harbor/memory.ts +0 -123
- package/src/harbor/reach.ts +0 -221
- package/src/harbor/store.ts +0 -91
- package/src/vector/cases.ts +0 -257
- package/src/vector/index.ts +0 -11
- package/src/vector/stand.ts +0 -76
- package/src/vector/world.ts +0 -232
- package/src/ward/arithmetic.ts +0 -251
- package/src/ward/door.ts +0 -186
- package/src/ward/ground.ts +0 -142
- package/src/ward/heirs.ts +0 -116
- package/src/ward/json.ts +0 -144
- package/src/ward/owner.ts +0 -214
- package/src/ward/seal.ts +0 -178
package/dist/ward/seal.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { isDoorWord } from '../being/silence.js';
|
|
2
|
-
import { KEY, SIGNATURE, box, concat, derive, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.js';
|
|
3
|
-
import { cellFault } from './cells.js';
|
|
4
|
-
import { readJson } from './json.js';
|
|
5
|
-
const utf8 = new TextEncoder();
|
|
6
|
-
// The two labels a ward's keys are derived under. They are here and not in
|
|
7
|
-
// arithmetic.ts because a label is a decision and that file is arithmetic.
|
|
8
|
-
// `quo-seal` is taken: it is the label the message cipher derives its key and
|
|
9
|
-
// nonce under, from an agreement rather than from a seed, and two derivations
|
|
10
|
-
// answering to one name is how a kit is read wrong.
|
|
11
|
-
const WARD_SIGN = new TextEncoder().encode('quo-ward-sign');
|
|
12
|
-
const WARD_SEAL = new TextEncoder().encode('quo-ward-seal');
|
|
13
|
-
// The one size in Quo: one mebibyte of bytes each way. It is read before
|
|
14
|
-
// anything is opened, so bytes above it are never decrypted, never parsed and
|
|
15
|
-
// never allocated against. Refusing them is bytes that said nothing, which is
|
|
16
|
-
// already silence, so there is no tenth word and no new door case. Breadth
|
|
17
|
-
// costs bytes, so this one number bounds every other breadth on the wire, and
|
|
18
|
-
// what a being holds is not the protocol's business. An ask is a message and
|
|
19
|
-
// not a file; what is larger is asked for in pieces.
|
|
20
|
-
export const SIZE = 1024 * 1024;
|
|
21
|
-
// The ward's key from its seed. Its pk on the wire is the signing pk then the padlock, 128 hex.
|
|
22
|
-
//
|
|
23
|
-
// One seed, two curves, and each secret derived from it under its own label.
|
|
24
|
-
// Fed the seed straight, the two scalars differ only because Ed25519 hashes
|
|
25
|
-
// what it is given and X25519 clamps it raw, which is an accident of the two
|
|
26
|
-
// designs and no separation at all: one secret would be doing two jobs with
|
|
27
|
-
// nothing said about it, and a second kit would have to reproduce a
|
|
28
|
-
// construction nobody named. HKDF-SHA-256 under a label is the separation
|
|
29
|
-
// said out loud, and it is what `quo/vectors/framing.json` pins.
|
|
30
|
-
//
|
|
31
|
-
// Bytes are key material and text is not. A seed handed in as bytes of the
|
|
32
|
-
// key length is taken as it stands, which is what a harbor mints; anything
|
|
33
|
-
// else, of any length, is hashed to that length first. So a thirty-two
|
|
34
|
-
// character name is a name like any other, and not a key because it happened
|
|
35
|
-
// to be the right size.
|
|
36
|
-
export async function wardKey(seed) {
|
|
37
|
-
const seed32 = typeof seed === 'string' ? await sha256(utf8.encode(seed)) : seed.length === KEY ? seed : await sha256(seed);
|
|
38
|
-
const s = await signingPair(await derive(seed32, WARD_SIGN, KEY));
|
|
39
|
-
const p = await sealingPair(await derive(seed32, WARD_SEAL, KEY));
|
|
40
|
-
return { sign: s.secret, padlock: p.secret, signPk: s.pk, padlockPk: p.pk, pk: hex(s.pk) + hex(p.pk) };
|
|
41
|
-
}
|
|
42
|
-
export const wardSignPk = (pk) => unhex(pk.slice(0, KEY * 2));
|
|
43
|
-
export const wardPadlock = (pk) => unhex(pk.slice(KEY * 2));
|
|
44
|
-
export const isWardPk = (pk) => typeof pk === 'string' && /^[0-9a-f]{128}$/.test(pk);
|
|
45
|
-
// Thirty-two bytes as hex, which is how this kit writes every single key: a
|
|
46
|
-
// heir pk, a heir secret, a being's own key, and a digest, which is the same
|
|
47
|
-
// thirty-two bytes of SHA-256. A ward pk is two of these and has its own
|
|
48
|
-
// shape above. The two are spelled here and nowhere else, so a door, a
|
|
49
|
-
// stance, a harbor and a conformance suite all refuse the same string.
|
|
50
|
-
export const isHex = (v) => typeof v === 'string' && /^[0-9a-f]{64}$/.test(v);
|
|
51
|
-
// A being's relation key: a seed, and the pk it signs as.
|
|
52
|
-
export async function beingKey(seed) {
|
|
53
|
-
return { seed: hex(seed), pk: hex((await signingPair(seed)).pk) };
|
|
54
|
-
}
|
|
55
|
-
// Seal an ask. Returns the bytes and the ephemeral secret the reply will be sealed to.
|
|
56
|
-
export async function sealAsk(to, payload, signer, padlock, seed) {
|
|
57
|
-
const body = utf8.encode(JSON.stringify({ to, ...payload }));
|
|
58
|
-
const inside = concat([body, await sign(body, signer)]);
|
|
59
|
-
const { bytes, ephemeral } = await box(inside, padlock, seed);
|
|
60
|
-
return { bytes, ephemeral: ephemeral.secret };
|
|
61
|
-
}
|
|
62
|
-
// Open an ask at the door. Null when it does not open or does not parse.
|
|
63
|
-
// Verification is the door's own step, because only the door knows which key may speak.
|
|
64
|
-
export async function openAsk(bytes, padlockSecret) {
|
|
65
|
-
try {
|
|
66
|
-
if (!(bytes instanceof Uint8Array) || bytes.length < 1 || bytes.length > SIZE)
|
|
67
|
-
return null;
|
|
68
|
-
const inside = await unbox(bytes, padlockSecret);
|
|
69
|
-
if (inside.length <= SIGNATURE)
|
|
70
|
-
return null;
|
|
71
|
-
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
72
|
-
const signature = inside.subarray(inside.length - SIGNATURE);
|
|
73
|
-
const payload = readJson(body);
|
|
74
|
-
// The heir, or null for the public being. Absent is not null: a payload
|
|
75
|
-
// that never named one is malformed, like any other missing field.
|
|
76
|
-
const to = payload.to;
|
|
77
|
-
if (to !== null && !isHex(to))
|
|
78
|
-
return null;
|
|
79
|
-
if (!isHex(payload.by))
|
|
80
|
-
return null;
|
|
81
|
-
if (payload.next !== null && !isHex(payload.next))
|
|
82
|
-
return null;
|
|
83
|
-
if (payload.method !== undefined && typeof payload.method !== 'string')
|
|
84
|
-
return null; // a name, or the empty ask. never a number, never an object.
|
|
85
|
-
if (payload.args !== undefined && (payload.args === null || typeof payload.args !== 'object' || Array.isArray(payload.args)))
|
|
86
|
-
return null; // args are one object, or absent. a string or a list is not an ask.
|
|
87
|
-
if (payload.args !== undefined && cellFault(payload.args, 'args') !== null)
|
|
88
|
-
return null; // and values all the way down: no key named __proto__, no nesting past the bound
|
|
89
|
-
if (!Number.isSafeInteger(payload.seq) || payload.seq < 1)
|
|
90
|
-
return null; // her count for this relation. one and up, and a whole number.
|
|
91
|
-
// The allowance is not read here. It is the door's, D2, and the door is
|
|
92
|
-
// its one reader: `spent` in allowance.ts says what a budget is and what
|
|
93
|
-
// is left of it, in one sentence, for a payload that opened like any other.
|
|
94
|
-
// The hop count, if this ask carries one: a whole number, never below
|
|
95
|
-
// zero. Absent is the ordinary ask and always will be.
|
|
96
|
-
if (payload.hops !== undefined && (!Number.isSafeInteger(payload.hops) || payload.hops < 0))
|
|
97
|
-
return null;
|
|
98
|
-
return { to, payload, body, signature, ephemeralPk: bytes.subarray(0, KEY) };
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
export const verifyAsk = (a, pk) => verify(a.body, a.signature, unhex(pk));
|
|
105
|
-
// Seal a reply to the ephemeral pk of the ask it answers, signed by the ward.
|
|
106
|
-
export async function sealReply(reply, ephemeralPk, wardSign, seed) {
|
|
107
|
-
const body = utf8.encode(JSON.stringify(reply));
|
|
108
|
-
return (await box(concat([body, await sign(body, wardSign)]), ephemeralPk, seed)).bytes;
|
|
109
|
-
}
|
|
110
|
-
// Open a reply with the ephemeral secret kept from the ask, and verify it
|
|
111
|
-
// came from the ward it was sent to. Null is silence: it did not open, it
|
|
112
|
-
// lied, or it is not a reply. Every ward on the wire is somebody else's code,
|
|
113
|
-
// so the shape is checked here and nowhere later: a reply is `{ silence }`,
|
|
114
|
-
// `{ quo: word }` with a word the door may say, or `{ object, seen }` with an
|
|
115
|
-
// object that is a value and seen a digest or null. A field beside a shape's
|
|
116
|
-
// own makes it none of the three, since a kit that dropped the field would
|
|
117
|
-
// hand her an answer where this one hands her silence.
|
|
118
|
-
// Anything else is bytes that said nothing, and her ward never hands her a
|
|
119
|
-
// throw or an absent value for what a stranger wrote.
|
|
120
|
-
export async function openReply(bytes, ephemeralSecret, signPk) {
|
|
121
|
-
try {
|
|
122
|
-
if (!(bytes instanceof Uint8Array) || bytes.length > SIZE)
|
|
123
|
-
return null;
|
|
124
|
-
const inside = await unbox(bytes, ephemeralSecret);
|
|
125
|
-
if (inside.length <= SIGNATURE)
|
|
126
|
-
return null;
|
|
127
|
-
const body = inside.subarray(0, inside.length - SIGNATURE);
|
|
128
|
-
if (!(await verify(body, inside.subarray(inside.length - SIGNATURE), signPk)))
|
|
129
|
-
return null;
|
|
130
|
-
const reply = readJson(body);
|
|
131
|
-
if (reply === null || typeof reply !== 'object' || Array.isArray(reply))
|
|
132
|
-
return null;
|
|
133
|
-
const r = reply;
|
|
134
|
-
const keys = Object.keys(r).sort().join(',');
|
|
135
|
-
if (keys === 'silence')
|
|
136
|
-
return r.silence === true ? { silence: true } : null;
|
|
137
|
-
if (keys === 'quo')
|
|
138
|
-
return isDoorWord(r.quo) ? { quo: r.quo } : null;
|
|
139
|
-
if (keys !== 'object,seen')
|
|
140
|
-
return null;
|
|
141
|
-
if (r.seen !== null && !isHex(r.seen))
|
|
142
|
-
return null;
|
|
143
|
-
if (cellFault(r.object, 'object') !== null)
|
|
144
|
-
return null; // values in both directions: nested past the bound is none of the shapes
|
|
145
|
-
return { object: r.object, seen: r.seen };
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
}
|
package/src/being/lent.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
// An offer is open until it is taken or retracted. The taker's knock is the
|
|
62
|
-
// first word she hears from the occupant she minted for it, so hearing
|
|
63
|
-
// from that id closes the offer, and `open` holds only offers still
|
|
64
|
-
// waiting and not every lend she ever made.
|
|
65
|
-
override async answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
|
|
66
|
-
const open = this.cells.open as Record<string, string> | undefined;
|
|
67
|
-
if (open && asker.id !== undefined && asker.id !== BOX) for (const [heir, id] of Object.entries(open)) if (id === asker.id) delete open[heir];
|
|
68
|
-
if (asker.id === BOX && method === 'offer') return this.offer();
|
|
69
|
-
if (asker.id === BOX && method === 'retract') return this.retract(args);
|
|
70
|
-
return super.answer(asker, method, args);
|
|
71
|
-
}
|
|
72
|
-
}
|
package/src/being/silence.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Silence is one distinguished value. Null is an answer. Silence is no answer.
|
|
3
|
-
//
|
|
4
|
-
// The ward's words are the other distinguished values: what her ward tells
|
|
5
|
-
// her when no object came back and it knows why. Each is one frozen object
|
|
6
|
-
// under one symbol key, so a being cannot make one by accident, none of them
|
|
7
|
-
// crosses an edge as a value, and a being who compares against them needs
|
|
8
|
-
// no fourth shape. `unreached` is one of them.
|
|
9
|
-
import { WORD_KEY, type DoorWord, type Unreached, type Word, type WordName } from './types.ts';
|
|
10
|
-
|
|
11
|
-
export const silence: unique symbol = Symbol.for('quo.silence');
|
|
12
|
-
export const isSilence = (x: unknown): x is typeof silence => x === silence;
|
|
13
|
-
|
|
14
|
-
// The words a door says to a key it has bound, and no other. They ride on
|
|
15
|
-
// the wire as `{ quo: word }`, so a reply is checked against this list.
|
|
16
|
-
export const DOOR_WORDS: readonly DoorWord[] = ['removed', 'absent', 'unannounced', 'repeated', 'threw'];
|
|
17
|
-
export const isDoorWord = (s: unknown): s is DoorWord => typeof s === 'string' && (DOOR_WORDS as readonly string[]).includes(s);
|
|
18
|
-
|
|
19
|
-
const WORDS = new Map<WordName, Word>();
|
|
20
|
-
export const word = <W extends WordName>(name: W): Word<W> => {
|
|
21
|
-
let w = WORDS.get(name);
|
|
22
|
-
if (!w) WORDS.set(name, (w = Object.freeze({ [WORD_KEY]: name })));
|
|
23
|
-
return w as Word<W>;
|
|
24
|
-
};
|
|
25
|
-
export const isWord = (x: unknown): x is Word => x !== null && typeof x === 'object' && typeof (x as Record<symbol, unknown>)[WORD_KEY] === 'string';
|
|
26
|
-
export const wordOf = (x: Word): WordName => x[WORD_KEY];
|
|
27
|
-
|
|
28
|
-
// What came back is an answer and not one of the two things that are not
|
|
29
|
-
// answers. A being who only wants the object writes one test instead of
|
|
30
|
-
// three, and the three shapes stay three shapes: silence is no answer, a word
|
|
31
|
-
// is her ward telling her why there is none.
|
|
32
|
-
export const answered = <T>(x: T): x is Exclude<T, typeof silence | Word> => !isSilence(x) && !isWord(x);
|
|
33
|
-
|
|
34
|
-
// What she was told, with a word given its name and everything else left
|
|
35
|
-
// exactly as it came. One value to compare against, so asking which word came
|
|
36
|
-
// back is one test and not two joined by an and: `told(out) === 'late'` says
|
|
37
|
-
// what `isWord(out) && wordOf(out) === 'late'` says, and says which answer
|
|
38
|
-
// arrived instead when it is wrong, where the pair collapses to false and
|
|
39
|
-
// names nothing. Silence stays the symbol it is, and an object stays itself.
|
|
40
|
-
export const told = (x: unknown): unknown => (isWord(x) ? wordOf(x) : x);
|
|
41
|
-
|
|
42
|
-
// Unreached: no far door was reached. Nothing is known to have been
|
|
43
|
-
// delivered, so asking again is safe. A being cannot produce it: a ward that
|
|
44
|
-
// sees a word come out of a being reads it as her having thrown.
|
|
45
|
-
export const unreached = (): Unreached => word('unreached');
|
|
46
|
-
export const isUnreached = (x: unknown): x is Unreached => isWord(x) && wordOf(x) === 'unreached';
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The five assertions the conformance suite makes, and no more. Node has all
|
|
3
|
-
// five in `node:assert/strict`, and naming that module would tie the suite to
|
|
4
|
-
// one terrain -- the suite is the ward's truth, and the ward's truth has to be
|
|
5
|
-
// checkable wherever a ward runs. So they are written here, in the language
|
|
6
|
-
// alone, and `test/assert.test.ts` holds them to Node's own behaviour.
|
|
7
|
-
export class Failed extends Error {}
|
|
8
|
-
|
|
9
|
-
const show = (v: unknown): string => {
|
|
10
|
-
if (typeof v === 'symbol') return v.toString();
|
|
11
|
-
if (typeof v === 'bigint') return `${v}n`;
|
|
12
|
-
if (v === undefined) return 'undefined';
|
|
13
|
-
if (typeof v === 'function') return `[function ${v.name}]`;
|
|
14
|
-
// A ward's word is a frozen object under one symbol key, and JSON shows it
|
|
15
|
-
// as `{}`. A red estate chapter saying `{} is not {}` names nothing, so a
|
|
16
|
-
// value whose own keys are all symbols is shown by its keys.
|
|
17
|
-
if (v !== null && typeof v === 'object') {
|
|
18
|
-
const keys = Object.keys(v);
|
|
19
|
-
const syms = Object.getOwnPropertySymbols(v).filter((s) => Object.getOwnPropertyDescriptor(v, s)!.enumerable);
|
|
20
|
-
if (keys.length === 0 && syms.length > 0) return `{ ${syms.map((s) => `${s.toString()}: ${show((v as Record<symbol, unknown>)[s])}`).join(', ')} }`;
|
|
21
|
-
}
|
|
22
|
-
try {
|
|
23
|
-
return JSON.stringify(v) ?? Object.prototype.toString.call(v);
|
|
24
|
-
} catch {
|
|
25
|
-
return Object.prototype.toString.call(v); // a cycle, or a null prototype
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
const fail = (why: string, message?: string): never => {
|
|
29
|
-
throw new Failed(message ? `${message}: ${why}` : why);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Structural, and strict about shape the way `node:assert/strict` is: a value
|
|
33
|
-
// is not equal to one of another kind, and an array is not equal to an object
|
|
34
|
-
// that happens to hold the same keys.
|
|
35
|
-
// The four kinds this compares, and every other object is refused rather than
|
|
36
|
-
// guessed at. A Date, a RegExp, a boxed primitive and a class instance each
|
|
37
|
-
// carry state no key walk can see, so two of them with the same keys are
|
|
38
|
-
// equal here and unequal in node, which is a wrong yes: the one answer a
|
|
39
|
-
// suite must never give. A refusal is a red test that says so.
|
|
40
|
-
const KNOWN: readonly (object | null)[] = [Object.prototype, null, Array.prototype, Uint8Array.prototype];
|
|
41
|
-
|
|
42
|
-
// What node compares: own enumerable keys, strings then enumerable symbols.
|
|
43
|
-
// Not `Reflect.ownKeys`, which also lists what a definition hid.
|
|
44
|
-
const keysOf = (v: object): PropertyKey[] => [...Object.keys(v), ...Object.getOwnPropertySymbols(v).filter((s) => Object.getOwnPropertyDescriptor(v, s)!.enumerable)];
|
|
45
|
-
|
|
46
|
-
export function same(a: unknown, b: unknown): boolean {
|
|
47
|
-
if (Object.is(a, b)) return true;
|
|
48
|
-
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
|
|
49
|
-
const proto = Object.getPrototypeOf(a) as object | null;
|
|
50
|
-
if (proto !== (Object.getPrototypeOf(b) as object | null)) return false;
|
|
51
|
-
if (!KNOWN.includes(proto)) return false; // a Date, a RegExp, a boxed number, a class of hers
|
|
52
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
53
|
-
if (a instanceof Uint8Array && b instanceof Uint8Array) return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
54
|
-
// An array is its indices and whatever else was hung on it, and a hole is
|
|
55
|
-
// not a present undefined: `[, 1]` and `[undefined, 1]` differ in node and
|
|
56
|
-
// must differ here. `every` skips holes, so the loop is written out.
|
|
57
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
58
|
-
if (a.length !== b.length) return false;
|
|
59
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
60
|
-
if (i in a !== i in b) return false;
|
|
61
|
-
if (i in a && !same(a[i], b[i])) return false;
|
|
62
|
-
}
|
|
63
|
-
const extra = (v: unknown[]) => keysOf(v).filter((k) => typeof k !== 'string' || !/^(?:0|[1-9]\d*)$/.test(k));
|
|
64
|
-
const xa = extra(a), xb = extra(b);
|
|
65
|
-
const at = (v: unknown[], k: PropertyKey) => (v as unknown as Record<PropertyKey, unknown>)[k];
|
|
66
|
-
return xa.length === xb.length && xa.every((k) => xb.includes(k) && same(at(a, k), at(b, k)));
|
|
67
|
-
}
|
|
68
|
-
const ka = keysOf(a), kb = keysOf(b);
|
|
69
|
-
if (ka.length !== kb.length) return false;
|
|
70
|
-
return ka.every((k) => kb.includes(k) && same((a as Record<PropertyKey, unknown>)[k], (b as Record<PropertyKey, unknown>)[k]));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// `ok` narrows, the way `node:assert/strict`'s does: the suite leans on it to
|
|
74
|
-
// turn a `string | null` into a `string`. That needs the explicit type below,
|
|
75
|
-
// so the interface is written out rather than inferred.
|
|
76
|
-
export type Assert = {
|
|
77
|
-
ok(value: unknown, message?: string): asserts value;
|
|
78
|
-
equal(actual: unknown, expected: unknown, message?: string): void;
|
|
79
|
-
notEqual(actual: unknown, expected: unknown, message?: string): void;
|
|
80
|
-
deepEqual(actual: unknown, expected: unknown, message?: string): void;
|
|
81
|
-
match(actual: string, re: RegExp, message?: string): void;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export const assert: Assert = {
|
|
85
|
-
ok(value: unknown, message?: string): asserts value {
|
|
86
|
-
if (!value) fail(`${show(value)} is not truthy`, message);
|
|
87
|
-
},
|
|
88
|
-
equal(actual: unknown, expected: unknown, message?: string): void {
|
|
89
|
-
if (!Object.is(actual, expected)) fail(`${show(actual)} !== ${show(expected)}`, message);
|
|
90
|
-
},
|
|
91
|
-
notEqual(actual: unknown, expected: unknown, message?: string): void {
|
|
92
|
-
if (Object.is(actual, expected)) fail(`${show(actual)} === ${show(expected)}`, message);
|
|
93
|
-
},
|
|
94
|
-
deepEqual(actual: unknown, expected: unknown, message?: string): void {
|
|
95
|
-
if (!same(actual, expected)) fail(`${show(actual)} is not ${show(expected)}`, message);
|
|
96
|
-
},
|
|
97
|
-
match(actual: string, re: RegExp, message?: string): void {
|
|
98
|
-
if (typeof actual !== 'string' || !re.test(actual)) fail(`${show(actual)} does not match ${String(re)}`, message);
|
|
99
|
-
},
|
|
100
|
-
};
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The fixture beings. Fixed, so that the ward is what varies. A printer, a
|
|
3
|
-
// shop, a customer on the base class, a maker, a member, and one raw being
|
|
4
|
-
// with no base at all.
|
|
5
|
-
import { Being } from '../being/being.ts';
|
|
6
|
-
import { Lent } from '../being/lent.ts';
|
|
7
|
-
import { answered, isSilence, isUnreached, isWord, wordOf } from '../being/silence.ts';
|
|
8
|
-
import { invitationArgs } from '../being/types.ts';
|
|
9
|
-
import type { Ask, Asker, Blueprint, Invitation, Json, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
|
|
10
|
-
|
|
11
|
-
// A printer. Invites whom she is told to, prints for her occupants. Her
|
|
12
|
-
// blueprint lives in her cells so a test can change her shape.
|
|
13
|
-
export class Printer extends Being {
|
|
14
|
-
static override cells = { jobs: [] as JsonObject[], asks: [{ name: 'print', input: { type: 'object' } }] as JsonObject[] };
|
|
15
|
-
static override asks = { print: { input: { type: 'object' } } };
|
|
16
|
-
override describe(): Blueprint {
|
|
17
|
-
// Her own cells, which a test writes to change her shape. Cells are Json
|
|
18
|
-
// to the ward, so the asks are named for what she keeps in them.
|
|
19
|
-
return { asks: this.cells.asks as Ask[], notes: { model: 'LX-2' } };
|
|
20
|
-
}
|
|
21
|
-
print({ doc }: JsonObject, asker: Asker) {
|
|
22
|
-
(this.cells.jobs as JsonObject[]).push({ by: asker.id ?? null, doc: doc ?? null });
|
|
23
|
-
return { printed: doc ?? null };
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// A shop. Invites customers in her own time. Keeps a printer as a standing.
|
|
28
|
-
// Takes a gold customer back if the customer offers a way. Counts every ask
|
|
29
|
-
// per occupant, by wrapping answer.
|
|
30
|
-
export class Shop extends Being {
|
|
31
|
-
static override cells = { printerId: null as string | null, sales: [] as JsonObject[], minted: 0 };
|
|
32
|
-
static override asks = {
|
|
33
|
-
hello: { input: { type: 'object' } },
|
|
34
|
-
buy: { input: { type: 'object', properties: { item: { type: 'string' } } } },
|
|
35
|
-
refund: { input: { type: 'object' }, for: (rec: OccupantRecord | undefined) => rec?.notes.tier === 'gold' },
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
override async answer(asker: Asker, method?: string, args?: JsonObject): Promise<Reply> {
|
|
39
|
-
const rec = this.occupant(asker);
|
|
40
|
-
if (rec && method !== undefined) rec.notes.asks = (Number(rec.notes.asks) || 0) + 1;
|
|
41
|
-
return super.answer(asker, method, args);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Outside any ask. She mints, her ward seals, she hands it out by mail.
|
|
45
|
-
async invite(tier = 'plain'): Promise<Invitation> {
|
|
46
|
-
const id = `cust${++(this.cells.minted as number)}`;
|
|
47
|
-
const inv = (await super.invite(id))!;
|
|
48
|
-
this.cells.occupants[id].notes.tier = tier;
|
|
49
|
-
return inv;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async keepPrinter(invitation: Invitation): Promise<boolean> {
|
|
53
|
-
const bp = await this.knock(invitation);
|
|
54
|
-
if (!answered(bp)) return false;
|
|
55
|
-
this.cells.printerId = await this.take('prn', invitation);
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async hello({ invitation }: JsonObject, asker: Asker) {
|
|
60
|
-
const rec = this.occupant(asker);
|
|
61
|
-
if (rec?.notes.tier === 'gold' && invitation) {
|
|
62
|
-
const back = await this.knock(invitation as Invitation, 'hi');
|
|
63
|
-
if (answered(back)) await this.take(`vip-${asker.id}`, invitation as Invitation);
|
|
64
|
-
}
|
|
65
|
-
return { welcome: true };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async buy({ item }: JsonObject, asker: Asker) {
|
|
69
|
-
if (typeof item !== 'string') return { error: 'no such item' }; // args are hers to check
|
|
70
|
-
const id = this.cells.printerId as string | null;
|
|
71
|
-
const printer = id ? this.standings[id] : undefined;
|
|
72
|
-
if (!printer) return { error: 'no printer' };
|
|
73
|
-
const out = await printer.ask('print', { doc: `receipt for ${item}` });
|
|
74
|
-
if (isUnreached(out)) return { error: 'printer unreachable', retry: true };
|
|
75
|
-
if (!answered(out)) return { error: 'printer refused' };
|
|
76
|
-
const st = this.cells.standings[id!];
|
|
77
|
-
if (st.seen !== st.digest) this.occupant(asker)!.notes.printerChanged = true;
|
|
78
|
-
(this.cells.sales as JsonObject[]).push({ item: item ?? null, by: asker.id ?? null });
|
|
79
|
-
return { ok: true, receipt: (out as JsonObject).printed };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
refund() {
|
|
83
|
-
return { ok: true };
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// A customer. Consumes an invitation, and only then decides to keep the
|
|
88
|
-
// shop. Offers the shop a way back so the shop can reach her.
|
|
89
|
-
export class Customer extends Being {
|
|
90
|
-
static override cells = { heard: [] as JsonObject[] };
|
|
91
|
-
static override asks = { hi: { input: { type: 'object' } } };
|
|
92
|
-
|
|
93
|
-
async join(invitation: Invitation) {
|
|
94
|
-
// A way back for the shop, if she can mint one. She may not: two joins on
|
|
95
|
-
// one invitation race, and the second finds the id taken. Then she offers
|
|
96
|
-
// nothing and knocks all the same, which is what the shop is asked to
|
|
97
|
-
// survive. The old shape said `!` here and was wrong about it.
|
|
98
|
-
const mine = await this.invite('shop-back');
|
|
99
|
-
const out = await this.knock(invitation, 'hello', { invitation: mine === null ? null : invitationArgs(mine) });
|
|
100
|
-
if (!answered(out)) return out; // nothing was born
|
|
101
|
-
await this.take('shop', invitation); // now, and only now
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
buy(item: string) {
|
|
105
|
-
return this.standings.shop!.ask('buy', { item });
|
|
106
|
-
}
|
|
107
|
-
learn() {
|
|
108
|
-
return this.standings.shop!.ask();
|
|
109
|
-
}
|
|
110
|
-
hi(_args: JsonObject, asker: Asker) {
|
|
111
|
-
(this.cells.heard as JsonObject[]).push({ from: asker.id ?? null, method: 'hi' });
|
|
112
|
-
return { heard: 'hi' };
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// The raw shape. No base class, no import from the kit. A being all the same.
|
|
117
|
-
export class Echo {
|
|
118
|
-
s: Stance;
|
|
119
|
-
constructor(stance: Stance) {
|
|
120
|
-
this.s = stance;
|
|
121
|
-
}
|
|
122
|
-
answer(asker: Asker, method?: string, args: JsonObject = {}): Reply {
|
|
123
|
-
if (method === undefined) return { asks: [{ name: 'echo', input: {} }], notes: {} };
|
|
124
|
-
return { from: asker.id ?? null, ...args };
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// A member of an estate. Nothing scripted: she invites whom she is told to,
|
|
129
|
-
// knocks where she is told to, and answers `ping` with the asker's own name.
|
|
130
|
-
// The estate chapter drives her, and what it drives is the graph, not her.
|
|
131
|
-
// A being who makes beings. `boot` is on every stance, so it is the ward's
|
|
132
|
-
// promise and not this kit's, and a second kit that never wired it hands its
|
|
133
|
-
// beings a call that answers null for ever. She says what came of it: the key
|
|
134
|
-
// back, or null for a key already taken, a class the harbor does not hold, or
|
|
135
|
-
// a constructor that threw. With an id she also asks for a way back, and then
|
|
136
|
-
// she holds a standing to what she made like any other.
|
|
137
|
-
export class Maker extends Being {
|
|
138
|
-
static override cells = { made: [] as string[] };
|
|
139
|
-
static override asks = {
|
|
140
|
-
open: { input: { type: 'object', properties: { class: { type: 'string' }, key: { type: 'string' }, id: { type: 'string' } }, required: ['class', 'key'] } },
|
|
141
|
-
say: { input: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
|
|
142
|
-
borrow: { input: { type: 'object', properties: { name: { type: 'string' }, id: { type: 'string' } }, required: ['name', 'id'] } },
|
|
143
|
-
};
|
|
144
|
-
async open({ class: className, key, id }: JsonObject) {
|
|
145
|
-
const made = await this.boot(className as string, key as string, id as string | undefined);
|
|
146
|
-
if (made !== null) (this.cells.made as string[]).push(made);
|
|
147
|
-
return { made };
|
|
148
|
-
}
|
|
149
|
-
async say({ id }: JsonObject) {
|
|
150
|
-
const standing = this.standings[id as string];
|
|
151
|
-
return { said: standing === undefined ? null : ((await standing.ask('ping')) as Json) };
|
|
152
|
-
}
|
|
153
|
-
// Making's other half: a standing at something the box already has, by the
|
|
154
|
-
// name the box knows it under. She is handed the id and never the value.
|
|
155
|
-
async borrow({ name, id }: JsonObject) {
|
|
156
|
-
return { lent: await this.lend(name as string, id as string) };
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
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[] };
|
|
165
|
-
static override asks = {
|
|
166
|
-
...Lent.asks,
|
|
167
|
-
ping: { input: { type: 'object' } },
|
|
168
|
-
relay: { input: { type: 'object', properties: { id: { type: 'string' }, method: { type: 'string' }, time: { type: 'number' } }, required: ['id'] } },
|
|
169
|
-
};
|
|
170
|
-
ping(_args: JsonObject, asker: Asker) {
|
|
171
|
-
(this.cells.heard as JsonObject[]).push({ from: asker.id ?? null });
|
|
172
|
-
return { pong: asker.id ?? null };
|
|
173
|
-
}
|
|
174
|
-
// She asks on one of her standings from inside her own answer, and says
|
|
175
|
-
// what came back as a word rather than as the word itself, since a word is
|
|
176
|
-
// her ward's value and never a thing that crosses. `time` is what she is
|
|
177
|
-
// willing to wait, which is the whole of what the third argument is for.
|
|
178
|
-
// Two members holding each other and relaying make a cycle, and a cycle is
|
|
179
|
-
// how a suite that owns no clock can see a wait end.
|
|
180
|
-
async relay({ id, method, args, time }: JsonObject) {
|
|
181
|
-
const standing = this.standings[id as string];
|
|
182
|
-
if (standing === undefined) return { said: 'no standing', out: null };
|
|
183
|
-
const out = await standing.ask(method as string | undefined, (args ?? {}) as JsonObject, time === undefined ? undefined : { time: time as number });
|
|
184
|
-
if (isSilence(out)) return { said: 'silence', out: null };
|
|
185
|
-
if (isWord(out)) return { said: wordOf(out), out: null };
|
|
186
|
-
return { said: null, out: out };
|
|
187
|
-
}
|
|
188
|
-
}
|