nervur 0.23.0 → 0.24.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/CONTRIBUTING.md +13 -0
- package/LICENSE +55 -202
- package/NOTICE +3 -8
- package/README.md +14 -156
- package/RELEASING.md +15 -0
- package/SECURITY.md +9 -0
- package/bin/nervur.js +6 -0
- package/package.json +26 -81
- package/AUTHORING.md +0 -935
- package/COMMAND.md +0 -179
- package/FACES.md +0 -280
- package/FACULTIES.md +0 -578
- package/GROUNDS.md +0 -315
- package/KIT-SPEC.md +0 -304
- package/WORLD.md +0 -195
- package/dist/app/app-ground.d.ts +0 -18
- package/dist/app/app-ground.js +0 -23
- package/dist/app/index.d.ts +0 -2
- package/dist/app/index.js +0 -5
- package/dist/app/native.d.ts +0 -11
- package/dist/app/native.js +0 -13
- package/dist/being/being.d.ts +0 -306
- package/dist/being/being.js +0 -20
- package/dist/being/covers.d.ts +0 -9
- package/dist/being/covers.js +0 -21
- package/dist/being/index.d.ts +0 -4
- package/dist/being/index.js +0 -6
- package/dist/being/need.d.ts +0 -49
- package/dist/being/need.js +0 -33
- package/dist/being/schema.d.ts +0 -100
- package/dist/being/schema.js +0 -132
- package/dist/being/table.d.ts +0 -48
- package/dist/being/table.js +0 -344
- package/dist/bench/bench-ground.d.ts +0 -73
- package/dist/bench/bench-ground.js +0 -153
- package/dist/bench/bench.d.ts +0 -101
- package/dist/bench/bench.js +0 -368
- package/dist/bench/fake-clock.d.ts +0 -17
- package/dist/bench/fake-clock.js +0 -34
- package/dist/bench/fake-memory.d.ts +0 -16
- package/dist/bench/fake-memory.js +0 -46
- package/dist/bench/fake-network.d.ts +0 -66
- package/dist/bench/fake-network.js +0 -222
- package/dist/bench/fake-unlock.d.ts +0 -6
- package/dist/bench/fake-unlock.js +0 -10
- package/dist/bench/index.d.ts +0 -3
- package/dist/bench/index.js +0 -5
- package/dist/bench/seeded.d.ts +0 -11
- package/dist/bench/seeded.js +0 -37
- package/dist/bench/settle.d.ts +0 -2
- package/dist/bench/settle.js +0 -49
- package/dist/bench/stewards.d.ts +0 -169
- package/dist/bench/stewards.js +0 -128
- package/dist/bodies/class-list.d.ts +0 -14
- package/dist/bodies/class-list.js +0 -33
- package/dist/bodies/joined-carry.d.ts +0 -29
- package/dist/bodies/joined-carry.js +0 -48
- package/dist/bodies/kept.d.ts +0 -44
- package/dist/bodies/kept.js +0 -91
- package/dist/bodies/noble-crypto.d.ts +0 -22
- package/dist/bodies/noble-crypto.js +0 -88
- package/dist/bodies/seed-keys.d.ts +0 -10
- package/dist/bodies/seed-keys.js +0 -42
- package/dist/bodies/strict-tools.d.ts +0 -10
- package/dist/bodies/strict-tools.js +0 -306
- package/dist/bodies/vouch.d.ts +0 -20
- package/dist/bodies/vouch.js +0 -71
- package/dist/bodies/web-carry.d.ts +0 -72
- package/dist/bodies/web-carry.js +0 -245
- package/dist/bodies/web-clock.d.ts +0 -12
- package/dist/bodies/web-clock.js +0 -33
- package/dist/browser/browser-ground.d.ts +0 -76
- package/dist/browser/browser-ground.js +0 -294
- package/dist/browser/index.d.ts +0 -4
- package/dist/browser/index.js +0 -7
- package/dist/browser/indexeddb-memory.d.ts +0 -17
- package/dist/browser/indexeddb-memory.js +0 -97
- package/dist/browser/locked-unlock.d.ts +0 -24
- package/dist/browser/locked-unlock.js +0 -85
- package/dist/browser/origin-classes.d.ts +0 -7
- package/dist/browser/origin-classes.js +0 -31
- package/dist/edge/durable-clock.d.ts +0 -19
- package/dist/edge/durable-clock.js +0 -61
- package/dist/edge/durable-memory.d.ts +0 -5
- package/dist/edge/durable-memory.js +0 -11
- package/dist/edge/durable.d.ts +0 -18
- package/dist/edge/durable.js +0 -24
- package/dist/edge/edge-ground.d.ts +0 -49
- package/dist/edge/edge-ground.js +0 -223
- package/dist/edge/index.d.ts +0 -7
- package/dist/edge/index.js +0 -9
- package/dist/edge/native-crypto.d.ts +0 -8
- package/dist/edge/native-crypto.js +0 -102
- package/dist/edge/secret-unlock.d.ts +0 -7
- package/dist/edge/secret-unlock.js +0 -13
- package/dist/edge/socket-carry.d.ts +0 -39
- package/dist/edge/socket-carry.js +0 -111
- package/dist/foundation.d.ts +0 -162
- package/dist/foundation.js +0 -4
- package/dist/ground/dock.d.ts +0 -1460
- package/dist/ground/dock.js +0 -1159
- package/dist/ground/ground.d.ts +0 -239
- package/dist/ground/ground.js +0 -787
- package/dist/ground/views.d.ts +0 -76
- package/dist/ground/views.js +0 -208
- package/dist/house/crossing.d.ts +0 -50
- package/dist/house/crossing.js +0 -153
- package/dist/house/house.d.ts +0 -88
- package/dist/house/house.js +0 -2069
- package/dist/house/rows-shape.d.ts +0 -110
- package/dist/house/rows-shape.js +0 -12
- package/dist/house/rows.d.ts +0 -27
- package/dist/house/rows.js +0 -185
- package/dist/index.d.ts +0 -33
- package/dist/index.js +0 -29
- package/dist/node/bridge.d.ts +0 -20
- package/dist/node/bridge.js +0 -284
- package/dist/node/cli.d.ts +0 -2
- package/dist/node/cli.js +0 -225
- package/dist/node/file-memory.d.ts +0 -13
- package/dist/node/file-memory.js +0 -86
- package/dist/node/folder-classes.d.ts +0 -5
- package/dist/node/folder-classes.js +0 -40
- package/dist/node/gone.d.ts +0 -2
- package/dist/node/gone.js +0 -7
- package/dist/node/hand.d.ts +0 -39
- package/dist/node/hand.js +0 -107
- package/dist/node/http.d.ts +0 -11
- package/dist/node/http.js +0 -90
- package/dist/node/index.d.ts +0 -11
- package/dist/node/index.js +0 -13
- package/dist/node/ledger-memory.d.ts +0 -17
- package/dist/node/ledger-memory.js +0 -152
- package/dist/node/lock.d.ts +0 -10
- package/dist/node/lock.js +0 -78
- package/dist/node/node-ground.d.ts +0 -26
- package/dist/node/node-ground.js +0 -228
- package/dist/node/notify.d.ts +0 -1
- package/dist/node/notify.js +0 -18
- package/dist/node/shell.d.ts +0 -11
- package/dist/node/shell.js +0 -35
- package/dist/node/sync-folder.d.ts +0 -6
- package/dist/node/sync-folder.js +0 -18
- package/dist/node/tcp-carry.d.ts +0 -42
- package/dist/node/tcp-carry.js +0 -193
- package/dist/node/unlock.d.ts +0 -17
- package/dist/node/unlock.js +0 -86
- package/dist/node/websocket.d.ts +0 -12
- package/dist/node/websocket.js +0 -134
- package/dist/quo/frame.d.ts +0 -33
- package/dist/quo/frame.js +0 -92
- package/dist/quo/index.d.ts +0 -1
- package/dist/quo/index.js +0 -5
- package/dist/quo/room.d.ts +0 -154
- package/dist/quo/room.js +0 -362
- package/dist/serve/index.d.ts +0 -45
- package/dist/serve/index.js +0 -71
package/dist/house/house.js
DELETED
|
@@ -1,2069 +0,0 @@
|
|
|
1
|
-
import { covers } from '../being/covers.js';
|
|
2
|
-
import { blueprintOf, WAIT } from '../being/need.js';
|
|
3
|
-
import { isJson, offered, resolve } from '../being/table.js';
|
|
4
|
-
import { Room } from '../quo/index.js';
|
|
5
|
-
import { Crossing, Marks, inward, outward } from './crossing.js';
|
|
6
|
-
import { Rows } from './rows.js';
|
|
7
|
-
import { copy, emptyRow } from './rows-shape.js';
|
|
8
|
-
const DAY = 86_400_000;
|
|
9
|
-
const WEEK = 7 * DAY;
|
|
10
|
-
const RETRY_BOUND = 300_000;
|
|
11
|
-
/** What a reply has to come home in, past the time its ask ran. */
|
|
12
|
-
const HOMEWARD = 5_000;
|
|
13
|
-
const STEWARD = 'steward';
|
|
14
|
-
const PUBLIC = 'public';
|
|
15
|
-
const RESERVED_BEINGS = new Set([STEWARD, PUBLIC, 'root', 'stranger']);
|
|
16
|
-
const RESERVED_OCCUPANTS = new Set(['root', 'stranger', STEWARD]);
|
|
17
|
-
const BEING_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
18
|
-
const LANG = 'org.nervur.asks/1';
|
|
19
|
-
const REPLAY_WINDOW = 600_000;
|
|
20
|
-
/** A failure the asker reads: `fail`, a refused call, a mismatch. */
|
|
21
|
-
class Fail extends Error {
|
|
22
|
-
}
|
|
23
|
-
/** Silence where a standing's describe was owed. She reads it as the call she made answering nothing. */
|
|
24
|
-
class Silent extends Fail {
|
|
25
|
-
}
|
|
26
|
-
const answerOf = (outcome) => (outcome !== null && 'answer' in outcome ? outcome.answer : null);
|
|
27
|
-
const errorOf = (message) => ({ error: { message } });
|
|
28
|
-
// The card a far public being is reached by: her ward, and her addresses in order.
|
|
29
|
-
const cardOf = (card) => {
|
|
30
|
-
const { ward, at } = (card ?? {});
|
|
31
|
-
if (typeof ward !== 'string' || !/^[0-9a-f]{128}$/.test(ward) || !Array.isArray(at) || !at.every((address) => typeof address === 'string')) {
|
|
32
|
-
throw new Fail('a public being is reached by its ward and its addresses');
|
|
33
|
-
}
|
|
34
|
-
return { ward, at };
|
|
35
|
-
};
|
|
36
|
-
// How long a handle's invitations may wait unspent: whole milliseconds above zero, or never.
|
|
37
|
-
const expiresOf = (expires) => {
|
|
38
|
-
if (expires === undefined)
|
|
39
|
-
return undefined;
|
|
40
|
-
if (!Number.isSafeInteger(expires) || expires <= 0)
|
|
41
|
-
throw new Fail('expires is a whole number of milliseconds above zero');
|
|
42
|
-
return expires;
|
|
43
|
-
};
|
|
44
|
-
const positionOf = (being) => (being === STEWARD ? 'steward' : being === PUBLIC ? 'public' : 'normal');
|
|
45
|
-
// A standing she holds. Her standing on her steward is the house's, and never a row.
|
|
46
|
-
const standingOf = (being, row, id) => id === STEWARD && being !== STEWARD ? { notes: {}, steward: {}, local: { being: STEWARD, occupant: `being:${being}` } } : row.standings[id];
|
|
47
|
-
/** An ask's args as a handle's holder sends them: what its bind fixes is the house's, and shown to no one. */
|
|
48
|
-
const unbound = (args, bound) => {
|
|
49
|
-
if (typeof bound !== 'object' || bound === null || Array.isArray(bound))
|
|
50
|
-
return args;
|
|
51
|
-
const schema = args;
|
|
52
|
-
if (schema.properties === undefined)
|
|
53
|
-
return args;
|
|
54
|
-
const fixed = new Set(Object.keys(bound));
|
|
55
|
-
return {
|
|
56
|
-
...args,
|
|
57
|
-
properties: Object.fromEntries(Object.entries(schema.properties).filter(([key]) => !fixed.has(key))),
|
|
58
|
-
...(schema.required === undefined ? {} : { required: schema.required.filter((key) => !fixed.has(key)) }),
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
/** Whether an ask is awaited by its caller: a public being's are, unless her author said otherwise. */
|
|
62
|
-
const idempotentIn = (entry, position) => (position === 'public' ? !entry.effect : entry.hints.idempotent);
|
|
63
|
-
const names = (value) => value;
|
|
64
|
-
/** A house opened on its foundation and its offers: its ward, its door and its hand, and nothing more. */
|
|
65
|
-
export const openHouse = async (foundation, offers, options = {}) => {
|
|
66
|
-
const house = new House(foundation, offers, options);
|
|
67
|
-
await house.open();
|
|
68
|
-
await house.tellOpened();
|
|
69
|
-
return {
|
|
70
|
-
ward: house.ward,
|
|
71
|
-
door: (box) => house.door(box),
|
|
72
|
-
ask: (request) => house.hand(request),
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
class House {
|
|
76
|
-
/** The ground's bound on every ask here. */
|
|
77
|
-
#wait;
|
|
78
|
-
#keys;
|
|
79
|
-
#classes;
|
|
80
|
-
#carry;
|
|
81
|
-
#clock;
|
|
82
|
-
#crypto;
|
|
83
|
-
#tools;
|
|
84
|
-
#memory;
|
|
85
|
-
#offers;
|
|
86
|
-
#room;
|
|
87
|
-
/** Its marks on the handles and invitations it hands its beings, known to it alone. */
|
|
88
|
-
#marks = new Marks();
|
|
89
|
-
/** The waits it raced against its clock, each named once. */
|
|
90
|
-
#withins = 0;
|
|
91
|
-
#rows;
|
|
92
|
-
#wardPlace = '';
|
|
93
|
-
ward = '';
|
|
94
|
-
#tables = new Map();
|
|
95
|
-
#queues = new Map();
|
|
96
|
-
/** What wakes each watch on a being, when an ask of hers lands. */
|
|
97
|
-
#changes = new Map();
|
|
98
|
-
/** The watch each asker holds, by being, ask and args, and what ends it. */
|
|
99
|
-
#watching = new Map();
|
|
100
|
-
/** Beings whose `born` has not run yet, and the run every other ask to her waits for. */
|
|
101
|
-
#borning = new Map();
|
|
102
|
-
#inFlight = new Set();
|
|
103
|
-
/** One send at a time on each standing to a far ward, so its count stays straight. */
|
|
104
|
-
#relations = new Map();
|
|
105
|
-
/** One arrival at the door per heir at a time, so each is admitted on the record the last one left. */
|
|
106
|
-
#arriving = new Map();
|
|
107
|
-
/** When each heir's record last moved, on a count of every arrival settled. */
|
|
108
|
-
#moved = new Map();
|
|
109
|
-
#settled = 0;
|
|
110
|
-
/** Far describes by standing, kept until an answer carries another mark. */
|
|
111
|
-
#described = new Map();
|
|
112
|
-
/** Answers on the zero head by the box's signature, for a window. */
|
|
113
|
-
#replays = new Map();
|
|
114
|
-
/** Heirs minted by an ask not yet landed, so a being of this house may bind one at once. */
|
|
115
|
-
#minting = new Map();
|
|
116
|
-
/** Tokens minted by an ask not yet landed, and its end, which a faculty's call through one waits for. */
|
|
117
|
-
#tokening = new Map();
|
|
118
|
-
#armed = Number.POSITIVE_INFINITY;
|
|
119
|
-
#waking = false;
|
|
120
|
-
/** Beings whose due work is all in flight. */
|
|
121
|
-
#blocked = new Set();
|
|
122
|
-
/** The earliest time anything of each being is due, read from her row as it lands. */
|
|
123
|
-
#dueAt = new Map();
|
|
124
|
-
/** Which being each being's place is. */
|
|
125
|
-
#beingAt = new Map();
|
|
126
|
-
constructor(foundation, offers, options) {
|
|
127
|
-
this.#wait = options.wait ?? Number.POSITIVE_INFINITY;
|
|
128
|
-
this.#keys = foundation.keys;
|
|
129
|
-
this.#memory = foundation.memory;
|
|
130
|
-
this.#classes = foundation.classes;
|
|
131
|
-
this.#carry = foundation.carry;
|
|
132
|
-
this.#clock = foundation.clock;
|
|
133
|
-
this.#crypto = foundation.crypto;
|
|
134
|
-
this.#tools = foundation.tools;
|
|
135
|
-
this.#room = new Room(foundation.keys, foundation.crypto, foundation.tools);
|
|
136
|
-
const references = new Set(Object.values(foundation));
|
|
137
|
-
this.#offers = offers.map((offer) => {
|
|
138
|
-
// Held to the rules a need is, where it arrives: a keyword outside the subset refuses it.
|
|
139
|
-
const bp = offered(offer.blueprint);
|
|
140
|
-
// What the ground hands the house is never a being's to call.
|
|
141
|
-
if (references.has(offer.object))
|
|
142
|
-
throw new TypeError(`the offer ${bp.name} is a reference of the house's foundation`);
|
|
143
|
-
for (const name of Object.keys(bp.methods)) {
|
|
144
|
-
if (typeof offer.object[name] !== 'function')
|
|
145
|
-
throw new TypeError(`the offer ${bp.name} has no method ${name}`);
|
|
146
|
-
}
|
|
147
|
-
return { ...offer, bp };
|
|
148
|
-
});
|
|
149
|
-
// Two offers of one blueprint serve two kinds side by side only where their kinds do not meet.
|
|
150
|
-
for (const [index, one] of this.#offers.entries()) {
|
|
151
|
-
for (const two of this.#offers.slice(index + 1)) {
|
|
152
|
-
if (one.bp.name !== two.bp.name)
|
|
153
|
-
continue;
|
|
154
|
-
const meet = one.kinds === undefined || two.kinds === undefined || one.kinds.some((kind) => two.kinds.includes(kind));
|
|
155
|
-
if (meet)
|
|
156
|
-
throw new TypeError(`two offers of ${one.bp.name} serve one kind`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// ---- open ----
|
|
161
|
-
async open() {
|
|
162
|
-
this.#rows = await Rows.open(this.#memory, this.#keys, this.#crypto, this.#tools);
|
|
163
|
-
// A being's row that lands says when she is next due, and unblocks her.
|
|
164
|
-
this.#rows.whenLanded((place, row) => {
|
|
165
|
-
const being = this.#beingAt.get(place);
|
|
166
|
-
if (being === undefined)
|
|
167
|
-
return;
|
|
168
|
-
this.#blocked.delete(being);
|
|
169
|
-
this.#dueFrom(being, row);
|
|
170
|
-
});
|
|
171
|
-
this.#wardPlace = await this.#rows.place('ward');
|
|
172
|
-
this.ward = await this.#room.ward();
|
|
173
|
-
const bound = this.#tools.hex(await this.#keys.derive('bound', 32));
|
|
174
|
-
const places = await this.#rows.list();
|
|
175
|
-
if (places.length === 0) {
|
|
176
|
-
const steward = await this.#resolve(this.#classes.steward());
|
|
177
|
-
if ('absent' in steward)
|
|
178
|
-
throw new TypeError(`the steward is absent: ${steward.absent}`);
|
|
179
|
-
const open = this.#classes.public();
|
|
180
|
-
const opened = await this.#rows.transact(async (draft) => {
|
|
181
|
-
const ward = { bound, beings: { [STEWARD]: { kind: steward.table.kind } } };
|
|
182
|
-
draft.set(await this.#place(STEWARD), emptyRow(steward.table.kind, copy(steward.table.cells)));
|
|
183
|
-
if (open !== undefined) {
|
|
184
|
-
const resolved = await this.#resolve(open);
|
|
185
|
-
if ('absent' in resolved)
|
|
186
|
-
throw new TypeError(`the public being is absent: ${resolved.absent}`);
|
|
187
|
-
ward.beings[PUBLIC] = { kind: open };
|
|
188
|
-
draft.set(await this.#place(PUBLIC), emptyRow(open, copy(resolved.table.cells)));
|
|
189
|
-
}
|
|
190
|
-
draft.set(this.#wardPlace, ward);
|
|
191
|
-
});
|
|
192
|
-
if (!opened)
|
|
193
|
-
throw new Error('memory refused the first write');
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
let ward = null;
|
|
197
|
-
try {
|
|
198
|
-
ward = await this.#rows.get(this.#wardPlace);
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
ward = null;
|
|
202
|
-
}
|
|
203
|
-
if (ward === null || ward.bound !== bound)
|
|
204
|
-
throw new Error('this memory is bound to other keys');
|
|
205
|
-
// What each being has due, read once from her row.
|
|
206
|
-
for (const being of Object.keys(ward.beings))
|
|
207
|
-
this.#dueFrom(being, await this.#rows.get(await this.#place(being)));
|
|
208
|
-
}
|
|
209
|
-
await this.#arm();
|
|
210
|
-
}
|
|
211
|
-
async #place(being) {
|
|
212
|
-
const place = await this.#rows.place(`being:${being}`);
|
|
213
|
-
this.#beingAt.set(place, being);
|
|
214
|
-
return place;
|
|
215
|
-
}
|
|
216
|
-
async #heirPlace(heir) {
|
|
217
|
-
return this.#rows.place(`heir:${heir}`);
|
|
218
|
-
}
|
|
219
|
-
async #tokenPlace(token) {
|
|
220
|
-
return this.#rows.place(`token:${token}`);
|
|
221
|
-
}
|
|
222
|
-
// Her earliest due time: an alarm, or the head of her outbox to each receiver.
|
|
223
|
-
#dueFrom(being, row) {
|
|
224
|
-
const times = [];
|
|
225
|
-
if (row !== null) {
|
|
226
|
-
for (const alarm of Object.values(row.alarms))
|
|
227
|
-
times.push(alarm.at);
|
|
228
|
-
const heads = new Map();
|
|
229
|
-
for (const entry of row.outbox)
|
|
230
|
-
if (!heads.has(laneOf(entry)))
|
|
231
|
-
heads.set(laneOf(entry), entry);
|
|
232
|
-
for (const entry of heads.values())
|
|
233
|
-
times.push(Math.min(entry.next, entry.deadline));
|
|
234
|
-
}
|
|
235
|
-
if (times.length === 0)
|
|
236
|
-
this.#dueAt.delete(being);
|
|
237
|
-
else
|
|
238
|
-
this.#dueAt.set(being, Math.min(...times));
|
|
239
|
-
}
|
|
240
|
-
// ---- classes ----
|
|
241
|
-
#resolve(kind) {
|
|
242
|
-
let found = this.#tables.get(kind);
|
|
243
|
-
if (found === undefined) {
|
|
244
|
-
found = (async () => {
|
|
245
|
-
const Class = await this.#classes.resolve({ kind });
|
|
246
|
-
if (Class === undefined)
|
|
247
|
-
return { absent: `no class answers ${kind}` };
|
|
248
|
-
let table;
|
|
249
|
-
try {
|
|
250
|
-
table = resolve(Class);
|
|
251
|
-
}
|
|
252
|
-
catch (error) {
|
|
253
|
-
return { absent: error instanceof Error ? error.message : String(error) };
|
|
254
|
-
}
|
|
255
|
-
if (table.kind !== kind)
|
|
256
|
-
return { absent: `the class for ${kind} declares ${table.kind}` };
|
|
257
|
-
const faculties = {};
|
|
258
|
-
for (const [member, want] of Object.entries(table.needs)) {
|
|
259
|
-
const offer = this.#offers.find((one) => (one.kinds === undefined || one.kinds.includes(kind)) && covers(one.bp, want).covered);
|
|
260
|
-
if (offer === undefined)
|
|
261
|
-
return { absent: `no offer covers her need ${member}` };
|
|
262
|
-
faculties[member] = offer;
|
|
263
|
-
}
|
|
264
|
-
return { Class, table, faculties };
|
|
265
|
-
})();
|
|
266
|
-
this.#tables.set(kind, found);
|
|
267
|
-
}
|
|
268
|
-
return found;
|
|
269
|
-
}
|
|
270
|
-
async #being(id) {
|
|
271
|
-
const ward = await this.#rows.get(this.#wardPlace);
|
|
272
|
-
const entry = ward?.beings[id];
|
|
273
|
-
if (entry === undefined)
|
|
274
|
-
return null;
|
|
275
|
-
const resolved = await this.#resolve(entry.kind);
|
|
276
|
-
return 'absent' in resolved ? null : { kind: entry.kind, resolved };
|
|
277
|
-
}
|
|
278
|
-
// ---- asking ----
|
|
279
|
-
/**
|
|
280
|
-
* The hand: a being asked by id as the occupant root, the steward where
|
|
281
|
-
* none is named. `cells` reads her cells as last landed, her defaults
|
|
282
|
-
* under them, and asks nothing.
|
|
283
|
-
*/
|
|
284
|
-
async hand({ id = STEWARD, method, args, after, cells, call }) {
|
|
285
|
-
const found = await this.#being(id);
|
|
286
|
-
if (found === null)
|
|
287
|
-
return errorOf(`no being ${id} is here`);
|
|
288
|
-
if (cells === true) {
|
|
289
|
-
if (method !== undefined || args !== undefined || after !== undefined || call !== undefined)
|
|
290
|
-
return errorOf('the hand reads her cells alone');
|
|
291
|
-
await this.#born(id);
|
|
292
|
-
const row = await this.#rows.get(await this.#place(id));
|
|
293
|
-
return { result: { ...copy(found.resolved.table.cells), ...row?.cells } };
|
|
294
|
-
}
|
|
295
|
-
const watch = after === undefined ? {} : { after: await this.digest(after) };
|
|
296
|
-
const outcome = await this.ask({ being: id, occupant: 'root', ...(method === undefined ? {} : { method }), args: args ?? {}, ...watch, ...(call === undefined ? {} : { call }) });
|
|
297
|
-
if (outcome === null)
|
|
298
|
-
return errorOf('the house answered nothing');
|
|
299
|
-
if ('describe' in outcome)
|
|
300
|
-
return { describe: outcome.describe };
|
|
301
|
-
return outcome.answer;
|
|
302
|
-
}
|
|
303
|
-
/** One ask to one being. Asks run one at a time, and `readOnly` ones beside them. */
|
|
304
|
-
async ask(request) {
|
|
305
|
-
if (!(request.house === true && request.method === 'born'))
|
|
306
|
-
await this.#born(request.being);
|
|
307
|
-
const silent = async () => {
|
|
308
|
-
if (request.always && (await this.#rows.transact((draft) => request.always.change(draft, null))))
|
|
309
|
-
request.always.landed?.();
|
|
310
|
-
return null;
|
|
311
|
-
};
|
|
312
|
-
// A chain whose time is spent runs nothing here, on arrival or at its turn: its caller has stopped listening.
|
|
313
|
-
const spent = () => request.until !== undefined && request.until <= this.#clock.now();
|
|
314
|
-
const found = spent() ? null : await this.#being(request.being);
|
|
315
|
-
if (found === null)
|
|
316
|
-
return silent();
|
|
317
|
-
const entry = request.method === undefined ? undefined : found.resolved.table.asks[request.method];
|
|
318
|
-
if (entry?.hints.readOnly === true && request.after !== undefined)
|
|
319
|
-
return this.#watch(request, found.resolved, entry);
|
|
320
|
-
if (request.method === undefined || entry?.hints.readOnly === true)
|
|
321
|
-
return this.#run(request, found.resolved);
|
|
322
|
-
const before = this.#queues.get(request.being) ?? Promise.resolve();
|
|
323
|
-
const run = before.then(() => (spent() ? silent() : this.#run(request, found.resolved)));
|
|
324
|
-
this.#queues.set(request.being, run.catch(() => undefined));
|
|
325
|
-
return run;
|
|
326
|
-
}
|
|
327
|
-
/** The digest a watch compares: the first sixteen hex digits of the SHA-256 of the answer's canonical JSON. */
|
|
328
|
-
async digest(answer) {
|
|
329
|
-
const digest = await this.#crypto.sha256(this.#tools.utf8(this.#tools.canonical(answer)));
|
|
330
|
-
return this.#tools.hex(digest.subarray(0, 8));
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* A watch: her `readOnly` ask, answered once it differs from what the asker
|
|
334
|
-
* holds, or as it stands when the wait runs out. It runs again each time an
|
|
335
|
-
* ask of hers lands. One asker holds one watch on one ask with the same
|
|
336
|
-
* args, and a second answers the first at once.
|
|
337
|
-
*/
|
|
338
|
-
async #watch(request, resolved, entry) {
|
|
339
|
-
// Strangers share one occupant, so a stranger is told apart by the key she signed with.
|
|
340
|
-
const asker = request.signer === undefined ? request.occupant : this.#tools.hex(request.signer);
|
|
341
|
-
const key = `${request.being}\n${asker}\n${entry.method}\n${this.#tools.canonical(request.args ?? {})}`;
|
|
342
|
-
this.#watching.get(key)?.();
|
|
343
|
-
let ended = false;
|
|
344
|
-
let wake = () => undefined;
|
|
345
|
-
const end = () => {
|
|
346
|
-
ended = true;
|
|
347
|
-
wake();
|
|
348
|
-
};
|
|
349
|
-
this.#watching.set(key, end);
|
|
350
|
-
const deadline = Math.min(this.#clock.now() + Math.min(entry.wait, this.#wait), request.until ?? Number.POSITIVE_INFINITY);
|
|
351
|
-
// What lands with the answer, Quo's move and the call id, lands once, with the answer given.
|
|
352
|
-
const { always: _always, call: _call, ...bare } = request;
|
|
353
|
-
const probe = bare;
|
|
354
|
-
const give = (outcome) => (request.always === undefined && request.call === undefined ? outcome : this.#run(request, resolved));
|
|
355
|
-
try {
|
|
356
|
-
for (;;) {
|
|
357
|
-
const changed = new Promise((woken) => {
|
|
358
|
-
wake = woken;
|
|
359
|
-
});
|
|
360
|
-
const listening = wake;
|
|
361
|
-
this.#listen(request.being, listening);
|
|
362
|
-
try {
|
|
363
|
-
const outcome = await this.#run(probe, resolved);
|
|
364
|
-
const same = outcome !== null && 'answer' in outcome && (await this.digest(outcome.answer)) === request.after;
|
|
365
|
-
const left = deadline - this.#clock.now();
|
|
366
|
-
if (!same || ended || left <= 0)
|
|
367
|
-
return await give(outcome);
|
|
368
|
-
if ((await this.within(left, changed)) === null)
|
|
369
|
-
return await give(outcome);
|
|
370
|
-
}
|
|
371
|
-
finally {
|
|
372
|
-
this.#unlisten(request.being, listening);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
finally {
|
|
377
|
-
if (this.#watching.get(key) === end)
|
|
378
|
-
this.#watching.delete(key);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
#listen(being, wake) {
|
|
382
|
-
const waiting = this.#changes.get(being) ?? new Set();
|
|
383
|
-
waiting.add(wake);
|
|
384
|
-
this.#changes.set(being, waiting);
|
|
385
|
-
}
|
|
386
|
-
#unlisten(being, wake) {
|
|
387
|
-
const waiting = this.#changes.get(being);
|
|
388
|
-
waiting?.delete(wake);
|
|
389
|
-
if (waiting?.size === 0)
|
|
390
|
-
this.#changes.delete(being);
|
|
391
|
-
}
|
|
392
|
-
// An ask of hers landed, so every watch on her looks again. The steward's powers reach every being, so hers wake all.
|
|
393
|
-
#touched(being) {
|
|
394
|
-
const woken = being === STEWARD ? [...this.#changes.values()].flatMap((set) => [...set]) : [...(this.#changes.get(being) ?? [])];
|
|
395
|
-
for (const wake of woken)
|
|
396
|
-
wake();
|
|
397
|
-
}
|
|
398
|
-
#asker(being, row, occupant, house, signer) {
|
|
399
|
-
const position = positionOf(being);
|
|
400
|
-
if (house)
|
|
401
|
-
return { id: STEWARD, notes: {}, steward: {} };
|
|
402
|
-
// The holder of the house's hand, who reaches every being and never arrives in a box.
|
|
403
|
-
if (occupant === 'root')
|
|
404
|
-
return { id: 'root', notes: {}, steward: {} };
|
|
405
|
-
if (occupant === STEWARD)
|
|
406
|
-
return position === 'steward' ? undefined : { id: STEWARD, notes: {}, steward: {} };
|
|
407
|
-
if (occupant === 'stranger')
|
|
408
|
-
return position === 'public' ? { id: 'stranger', notes: {}, steward: {}, ...(signer === undefined ? {} : { signer }) } : undefined;
|
|
409
|
-
const held = row.occupants[occupant];
|
|
410
|
-
if (held !== undefined)
|
|
411
|
-
return { id: occupant, notes: held.notes, steward: held.steward, ...(held.ask === undefined ? {} : { handle: held }) };
|
|
412
|
-
if (occupant.startsWith('being:') && position === 'steward')
|
|
413
|
-
return { id: occupant, notes: {}, steward: {} };
|
|
414
|
-
return undefined;
|
|
415
|
-
}
|
|
416
|
-
#roles(table, asker, me) {
|
|
417
|
-
const roles = new Set();
|
|
418
|
-
if (asker.handle !== undefined)
|
|
419
|
-
return new Set(['handle']);
|
|
420
|
-
if (asker.id === STEWARD)
|
|
421
|
-
roles.add(STEWARD);
|
|
422
|
-
if (asker.id === 'root')
|
|
423
|
-
roles.add('root');
|
|
424
|
-
if (asker.id === 'stranger')
|
|
425
|
-
roles.add('stranger');
|
|
426
|
-
if (asker.id.startsWith('being:'))
|
|
427
|
-
roles.add('being');
|
|
428
|
-
for (const [name, test] of Object.entries(table.roles)) {
|
|
429
|
-
try {
|
|
430
|
-
if (test(asker, me))
|
|
431
|
-
roles.add(name);
|
|
432
|
-
}
|
|
433
|
-
catch {
|
|
434
|
-
// A role that throws does not hold.
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
return roles;
|
|
438
|
-
}
|
|
439
|
-
#shown(entry, asker, roles, position) {
|
|
440
|
-
if (asker.handle !== undefined)
|
|
441
|
-
return asker.handle.ask === entry.method && (entry.for ?? []).includes('handle');
|
|
442
|
-
if (position === 'public' && asker.id === 'stranger' && entry.effect)
|
|
443
|
-
return false;
|
|
444
|
-
if (entry.for === null)
|
|
445
|
-
return true;
|
|
446
|
-
return entry.for.some((role) => roles.has(role));
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Her describe for one asker, and its mark, which moves exactly when the
|
|
450
|
-
* describe does. A stranger is not told her kind or her description.
|
|
451
|
-
*/
|
|
452
|
-
async #describe(table, state, shown, position, stranger, bound) {
|
|
453
|
-
const asks = Object.values(table.asks)
|
|
454
|
-
.filter(shown)
|
|
455
|
-
.map((entry) => ({
|
|
456
|
-
method: entry.method,
|
|
457
|
-
in: names(entry.in) ?? table.states,
|
|
458
|
-
to: names(entry.to) ?? names(entry.in) ?? table.states,
|
|
459
|
-
...(entry.description === undefined ? {} : { description: entry.description }),
|
|
460
|
-
args: unbound(entry.args, bound),
|
|
461
|
-
...(entry.result === undefined ? {} : { result: entry.result }),
|
|
462
|
-
hints: { ...entry.hints, idempotent: idempotentIn(entry, position) },
|
|
463
|
-
wait: entry.wait,
|
|
464
|
-
}));
|
|
465
|
-
const told = stranger ? {} : { kind: table.kind, ...(table.description === undefined ? {} : { description: table.description }) };
|
|
466
|
-
// Her view is shown to every asker, a stranger too, since a public page is drawn from it.
|
|
467
|
-
const describe = { lang: LANG, ...told, ...(table.view === undefined ? {} : { view: table.view }), state, asks };
|
|
468
|
-
const digest = await this.#crypto.sha256(this.#tools.utf8(this.#tools.canonical(describe)));
|
|
469
|
-
return { describe, mark: this.#tools.hex(digest.subarray(0, 8)) };
|
|
470
|
-
}
|
|
471
|
-
/**
|
|
472
|
-
* Her `born`, as her first ask, once the steward's write has landed. It is
|
|
473
|
-
* marked before the steward's ask returns, so every ask to her waits for it.
|
|
474
|
-
*/
|
|
475
|
-
bornOf(being) {
|
|
476
|
-
const run = this.#bornOf(being);
|
|
477
|
-
this.#borning.set(being, run);
|
|
478
|
-
void run.finally(() => {
|
|
479
|
-
if (this.#borning.get(being) === run)
|
|
480
|
-
this.#borning.delete(being);
|
|
481
|
-
});
|
|
482
|
-
return run;
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* Her `born` run first where it is pending: the one her steward's write
|
|
486
|
-
* started, or one a stop left between that write and her first ask.
|
|
487
|
-
*/
|
|
488
|
-
async #born(being) {
|
|
489
|
-
const borning = this.#borning.get(being);
|
|
490
|
-
if (borning !== undefined)
|
|
491
|
-
return borning.catch(() => undefined);
|
|
492
|
-
const row = await this.#rows.get(await this.#place(being)).catch(() => null);
|
|
493
|
-
if (row?.born !== undefined)
|
|
494
|
-
await this.bornOf(being).catch(() => undefined);
|
|
495
|
-
}
|
|
496
|
-
async #bornOf(being) {
|
|
497
|
-
const place = await this.#place(being);
|
|
498
|
-
const row = await this.#rows.get(place);
|
|
499
|
-
if (row === null || row.born === undefined)
|
|
500
|
-
return;
|
|
501
|
-
await this.ask({
|
|
502
|
-
being,
|
|
503
|
-
occupant: STEWARD,
|
|
504
|
-
house: true,
|
|
505
|
-
method: 'born',
|
|
506
|
-
args: row.born,
|
|
507
|
-
always: {
|
|
508
|
-
change: async (draft) => {
|
|
509
|
-
const current = await draft.get(place);
|
|
510
|
-
if (current !== null)
|
|
511
|
-
delete current.born;
|
|
512
|
-
},
|
|
513
|
-
},
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
async #run(request, resolved) {
|
|
517
|
-
const { table, Class } = resolved;
|
|
518
|
-
const place = await this.#place(request.being);
|
|
519
|
-
const row = await this.#rows.get(place);
|
|
520
|
-
const callKey = request.call === undefined ? undefined : `${request.occupant}\n${request.call}`;
|
|
521
|
-
// What lands whatever the ask's outcome: its answered call id, and the caller's own changes.
|
|
522
|
-
const land = async (outcome, record) => {
|
|
523
|
-
if (request.always === undefined && !(record && callKey !== undefined))
|
|
524
|
-
return outcome;
|
|
525
|
-
const ok = await this.#rows.transact(async (draft) => {
|
|
526
|
-
if (record && callKey !== undefined && outcome !== null && 'answer' in outcome) {
|
|
527
|
-
const current = await draft.get(place);
|
|
528
|
-
if (current !== null)
|
|
529
|
-
current.calls[callKey] = { answer: outcome.answer, at: this.#clock.now() };
|
|
530
|
-
}
|
|
531
|
-
await request.always?.change(draft, outcome);
|
|
532
|
-
});
|
|
533
|
-
if (!ok)
|
|
534
|
-
return undefined;
|
|
535
|
-
request.always?.landed?.();
|
|
536
|
-
return outcome;
|
|
537
|
-
};
|
|
538
|
-
const silent = async () => (await land(null, false)) ?? null;
|
|
539
|
-
if (row === null)
|
|
540
|
-
return silent();
|
|
541
|
-
const position = positionOf(request.being);
|
|
542
|
-
const asker = this.#asker(request.being, row, request.occupant, request.house === true, request.signer);
|
|
543
|
-
if (asker === undefined)
|
|
544
|
-
return silent();
|
|
545
|
-
const cells = { ...copy(table.cells), ...row.cells };
|
|
546
|
-
// What the house asks itself, her born, her alarms and her replies, it asks as her steward would, with the steward's roles alone.
|
|
547
|
-
const shownFor = (now) => {
|
|
548
|
-
const roles = this.#roles(table, asker, { id: request.being, position, cells: now });
|
|
549
|
-
return (entry) => this.#shown(entry, asker, roles, position);
|
|
550
|
-
};
|
|
551
|
-
const stranger = asker.id === 'stranger';
|
|
552
|
-
// The mark of what this asker is shown on these cells.
|
|
553
|
-
const bound = asker.handle?.bind;
|
|
554
|
-
const markOf = async (now) => (await this.#describe(table, table.state({ id: request.being, position, cells: now }), shownFor(now), position, stranger, bound)).mark;
|
|
555
|
-
let state;
|
|
556
|
-
try {
|
|
557
|
-
state = table.state({ id: request.being, position, cells });
|
|
558
|
-
}
|
|
559
|
-
catch {
|
|
560
|
-
return silent();
|
|
561
|
-
}
|
|
562
|
-
const shown = shownFor(cells);
|
|
563
|
-
// Args that repeat a key are refused before anything runs, with the mark this asker is shown.
|
|
564
|
-
if (request.strange === true)
|
|
565
|
-
return (await land({ answer: errorOf('the args repeat a key'), mark: await markOf(cells) }, true)) ?? null;
|
|
566
|
-
if (request.method === undefined) {
|
|
567
|
-
const described = await this.#describe(table, state, shown, position, stranger, bound);
|
|
568
|
-
return (await land(described, false)) ?? null;
|
|
569
|
-
}
|
|
570
|
-
const failed = async (message) => (await land({ answer: errorOf(message), mark: await markOf(cells) }, true)) ?? null;
|
|
571
|
-
const entry = table.asks[request.method];
|
|
572
|
-
if (entry === undefined || !shown(entry))
|
|
573
|
-
return failed('no such ask');
|
|
574
|
-
if (entry.in !== null && !entry.in.includes(state))
|
|
575
|
-
return failed('not in this state');
|
|
576
|
-
if (callKey !== undefined) {
|
|
577
|
-
const stored = row.calls[callKey];
|
|
578
|
-
if (stored !== undefined)
|
|
579
|
-
return (await land({ answer: stored.answer, mark: await markOf(cells) }, false)) ?? null;
|
|
580
|
-
}
|
|
581
|
-
const wire = typeof bound === 'object' && bound !== null && !Array.isArray(bound) ? { ...request.args, ...bound } : (request.args ?? {});
|
|
582
|
-
const why = this.#tools.check(entry.args, wire);
|
|
583
|
-
if (why !== null)
|
|
584
|
-
return failed(why);
|
|
585
|
-
// Her own wait, never past the ground's bound, nor past the chain that asks.
|
|
586
|
-
const now = this.#clock.now();
|
|
587
|
-
const tx = new Tx(this, request.being, row, resolved, Math.min(now + Math.min(entry.wait, this.#wait), request.until ?? Number.POSITIVE_INFINITY));
|
|
588
|
-
let answer;
|
|
589
|
-
try {
|
|
590
|
-
const args = await inward(this.#tools, this.#marks, entry.args, wire, (hex) => tx.accept(hex));
|
|
591
|
-
const being = Reflect.construct(Class, []);
|
|
592
|
-
tx.cells = copy(cells);
|
|
593
|
-
Object.defineProperties(being, tx.reach(asker, position, table));
|
|
594
|
-
const method = being[request.method];
|
|
595
|
-
// The ask has its own wait, and fails where the method runs past it.
|
|
596
|
-
const ran = await this.within(entry.wait, (async () => {
|
|
597
|
-
const value = await method.call(being, args);
|
|
598
|
-
await tx.settle();
|
|
599
|
-
return { value };
|
|
600
|
-
})());
|
|
601
|
-
if (ran === null)
|
|
602
|
-
throw new Fail('the ask ran out of time');
|
|
603
|
-
const returned = ran.value;
|
|
604
|
-
const after = table.state({ id: request.being, position, cells: tx.cells });
|
|
605
|
-
const to = entry.to ?? [state];
|
|
606
|
-
if (!to.includes(after))
|
|
607
|
-
throw new Error(`she landed in ${after}`);
|
|
608
|
-
if (!isJson(tx.cells))
|
|
609
|
-
throw new Fail('a handle, an invitation or a value that is not JSON stands in her cells');
|
|
610
|
-
if (entry.hints.readOnly && (tx.ops.length > 0 || this.#tools.canonical(tx.cells) !== this.#tools.canonical(cells)))
|
|
611
|
-
throw new Error('a readOnly ask wrote');
|
|
612
|
-
const result = entry.result === undefined ? null : await outward(this.#tools, this.#marks, entry.result, returned, request.being, (handle) => tx.mint(handle, request.reader ?? 'house'), (hex) => tx.redeem(hex, request.reader ?? 'house'));
|
|
613
|
-
answer = { result: result };
|
|
614
|
-
}
|
|
615
|
-
catch (error) {
|
|
616
|
-
await tx.abandon();
|
|
617
|
-
tx.untake();
|
|
618
|
-
this.#dropMinting(tx);
|
|
619
|
-
return failed(error instanceof Fail || error instanceof Crossing ? error.message : 'the ask failed');
|
|
620
|
-
}
|
|
621
|
-
const outcome = { answer, mark: await markOf(tx.cells) };
|
|
622
|
-
// A read writes none of her cells: it ran on the cells last landed, and an ask may have landed beside it. It records no call id, since running it again writes nothing.
|
|
623
|
-
// An ask that changed nothing and carries no call id writes nothing either.
|
|
624
|
-
const changed = tx.ops.length > 0 || callKey !== undefined || this.#tools.canonical(tx.cells) !== this.#tools.canonical(cells);
|
|
625
|
-
const writes = (!entry.hints.readOnly && changed) || request.always !== undefined || asker.handle?.once === true;
|
|
626
|
-
let landed = true;
|
|
627
|
-
try {
|
|
628
|
-
if (writes)
|
|
629
|
-
landed = await this.#rows.transact(async (draft) => {
|
|
630
|
-
const current = await draft.get(place);
|
|
631
|
-
if (current === null)
|
|
632
|
-
throw new Error('she was removed while she was asked');
|
|
633
|
-
if (!entry.hints.readOnly)
|
|
634
|
-
current.cells = tx.cells;
|
|
635
|
-
if (callKey !== undefined && !entry.hints.readOnly)
|
|
636
|
-
current.calls[callKey] = { answer, at: this.#clock.now() };
|
|
637
|
-
if (asker.handle?.once === true)
|
|
638
|
-
await this.#dismiss(draft, current, request.occupant);
|
|
639
|
-
for (const op of tx.ops)
|
|
640
|
-
await op(draft, current);
|
|
641
|
-
await request.always?.change(draft, outcome);
|
|
642
|
-
this.#prune(current);
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
catch (error) {
|
|
646
|
-
// A change her powers could not make, such as an occupant held already: nothing of her ask lands.
|
|
647
|
-
tx.untake();
|
|
648
|
-
this.#dropMinting(tx);
|
|
649
|
-
return failed(error instanceof Fail ? error.message : 'the ask failed');
|
|
650
|
-
}
|
|
651
|
-
if (!landed)
|
|
652
|
-
tx.untake();
|
|
653
|
-
this.#dropMinting(tx);
|
|
654
|
-
if (!landed)
|
|
655
|
-
return null;
|
|
656
|
-
request.always?.landed?.();
|
|
657
|
-
await tx.landed();
|
|
658
|
-
if (!entry.hints.readOnly)
|
|
659
|
-
this.#touched(request.being);
|
|
660
|
-
return outcome;
|
|
661
|
-
}
|
|
662
|
-
#dropMinting(tx) {
|
|
663
|
-
for (const heir of tx.heirs)
|
|
664
|
-
this.#minting.delete(heir);
|
|
665
|
-
for (const token of tx.tokens)
|
|
666
|
-
this.#tokening.delete(token);
|
|
667
|
-
tx.end();
|
|
668
|
-
}
|
|
669
|
-
// Removes an occupant and every way to reach her through it: its heirs' places and its tokens'.
|
|
670
|
-
async #dismiss(draft, row, occupant) {
|
|
671
|
-
const held = row.occupants[occupant];
|
|
672
|
-
delete row.occupants[occupant];
|
|
673
|
-
if (held !== undefined)
|
|
674
|
-
await this.#forget(draft, held);
|
|
675
|
-
}
|
|
676
|
-
// The places of an occupant's heirs and tokens, gone.
|
|
677
|
-
async #forget(draft, held) {
|
|
678
|
-
for (const heir of Object.keys(held.quo ?? {}))
|
|
679
|
-
draft.set(await this.#heirPlace(heir), null);
|
|
680
|
-
for (const token of held.tokens ?? [])
|
|
681
|
-
draft.set(await this.#tokenPlace(token), null);
|
|
682
|
-
}
|
|
683
|
-
/** Call ids and dead letters are kept seven days, and go at her next write after. */
|
|
684
|
-
#prune(row) {
|
|
685
|
-
if (row === null)
|
|
686
|
-
return;
|
|
687
|
-
const since = this.#clock.now() - WEEK;
|
|
688
|
-
for (const [key, call] of Object.entries(row.calls))
|
|
689
|
-
if (call.at < since)
|
|
690
|
-
delete row.calls[key];
|
|
691
|
-
row.dead = row.dead.filter((letter) => letter.at >= since);
|
|
692
|
-
}
|
|
693
|
-
// ---- what is due ----
|
|
694
|
-
/**
|
|
695
|
-
* One wait for the earliest time anything is due. A being whose due work
|
|
696
|
-
* is all in flight is left out until something of hers lands, so the
|
|
697
|
-
* house never wakes for work it is already doing.
|
|
698
|
-
*/
|
|
699
|
-
async #arm() {
|
|
700
|
-
// A wake arms again at its end.
|
|
701
|
-
if (this.#waking)
|
|
702
|
-
return;
|
|
703
|
-
const times = [...this.#dueAt].filter(([being]) => !this.#blocked.has(being)).map(([, at]) => at);
|
|
704
|
-
const earliest = times.length === 0 ? Number.POSITIVE_INFINITY : Math.min(...times);
|
|
705
|
-
const now = this.#clock.now();
|
|
706
|
-
if (earliest <= now) {
|
|
707
|
-
this.#waking = true;
|
|
708
|
-
void this.#wake();
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
if (earliest === this.#armed)
|
|
712
|
-
return;
|
|
713
|
-
this.#armed = earliest;
|
|
714
|
-
if (earliest === Number.POSITIVE_INFINITY) {
|
|
715
|
-
this.#clock.cancel({ id: 'due' });
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
void this.#clock.wait({ id: 'due', ms: earliest - now }).then((fired) => {
|
|
719
|
-
if (!fired)
|
|
720
|
-
return;
|
|
721
|
-
this.#armed = Number.POSITIVE_INFINITY;
|
|
722
|
-
return this.#arm();
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
async #wake() {
|
|
726
|
-
try {
|
|
727
|
-
const now = this.#clock.now();
|
|
728
|
-
const due = [...this.#dueAt].filter(([being, at]) => at <= now && !this.#blocked.has(being));
|
|
729
|
-
await Promise.allSettled(due.map(([being]) => this.#fire(being).then(() => this.#pump(being))));
|
|
730
|
-
for (const [being] of due)
|
|
731
|
-
if ((this.#dueAt.get(being) ?? Number.POSITIVE_INFINITY) <= this.#clock.now())
|
|
732
|
-
this.#blocked.add(being);
|
|
733
|
-
}
|
|
734
|
-
finally {
|
|
735
|
-
this.#waking = false;
|
|
736
|
-
}
|
|
737
|
-
await this.#arm();
|
|
738
|
-
}
|
|
739
|
-
// Her alarms whose time has come, each asked as the occupant steward.
|
|
740
|
-
async #fire(being) {
|
|
741
|
-
const place = await this.#place(being);
|
|
742
|
-
const row = await this.#rows.get(place);
|
|
743
|
-
if (row === null)
|
|
744
|
-
return;
|
|
745
|
-
const now = this.#clock.now();
|
|
746
|
-
for (const [key, alarm] of Object.entries(row.alarms)) {
|
|
747
|
-
if (alarm.at > now)
|
|
748
|
-
continue;
|
|
749
|
-
await this.ask({
|
|
750
|
-
being,
|
|
751
|
-
occupant: STEWARD,
|
|
752
|
-
house: true,
|
|
753
|
-
method: alarm.ask,
|
|
754
|
-
args: alarm.args,
|
|
755
|
-
always: {
|
|
756
|
-
change: async (draft) => {
|
|
757
|
-
const current = await draft.get(place);
|
|
758
|
-
if (current === null || current.alarms[key]?.at !== alarm.at)
|
|
759
|
-
return;
|
|
760
|
-
delete current.alarms[key];
|
|
761
|
-
this.#prune(current);
|
|
762
|
-
},
|
|
763
|
-
},
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
// ---- effects ----
|
|
768
|
-
/** Sends what is due of her outbox, and waits for what comes due next. */
|
|
769
|
-
async pump(being) {
|
|
770
|
-
await this.#pump(being);
|
|
771
|
-
await this.#arm();
|
|
772
|
-
}
|
|
773
|
-
// The first entry to each receiver, one at a time; each send is awaited.
|
|
774
|
-
async #pump(being) {
|
|
775
|
-
const place = await this.#place(being);
|
|
776
|
-
const row = await this.#rows.get(place);
|
|
777
|
-
if (row === null)
|
|
778
|
-
return;
|
|
779
|
-
const now = this.#clock.now();
|
|
780
|
-
const seen = new Set();
|
|
781
|
-
const sends = [];
|
|
782
|
-
for (const entry of row.outbox) {
|
|
783
|
-
const key = laneOf(entry);
|
|
784
|
-
if (seen.has(key))
|
|
785
|
-
continue;
|
|
786
|
-
seen.add(key);
|
|
787
|
-
if (this.#inFlight.has(`${being}\n${entry.id}`))
|
|
788
|
-
continue;
|
|
789
|
-
if (entry.deadline <= now)
|
|
790
|
-
sends.push(this.#giveUp(being, place, entry));
|
|
791
|
-
else if (entry.next <= now)
|
|
792
|
-
sends.push(this.#send(being, place, row, entry));
|
|
793
|
-
}
|
|
794
|
-
// A send is started here and runs on; its own end pumps again.
|
|
795
|
-
void Promise.allSettled(sends);
|
|
796
|
-
}
|
|
797
|
-
async #send(being, place, row, entry) {
|
|
798
|
-
const flight = `${being}\n${entry.id}`;
|
|
799
|
-
this.#inFlight.add(flight);
|
|
800
|
-
// The entry stays in flight until what its answer changed has landed, so
|
|
801
|
-
// it is never sent again while a call for it is pending.
|
|
802
|
-
try {
|
|
803
|
-
let answer;
|
|
804
|
-
try {
|
|
805
|
-
// A watch is held on the far side for its wait, and answers the same when it runs out.
|
|
806
|
-
const watch = entry.after === undefined ? undefined : { until: this.#clock.now() + (entry.wait ?? WAIT), after: entry.after };
|
|
807
|
-
answer = await this.deliver(being, row, entry.to, entry.method, entry.args, entry.id, watch?.until, watch?.after);
|
|
808
|
-
}
|
|
809
|
-
catch {
|
|
810
|
-
answer = null;
|
|
811
|
-
}
|
|
812
|
-
if (answer === null) {
|
|
813
|
-
// Transient: tried again after a wait that doubles, never past five minutes.
|
|
814
|
-
const wait = Math.min(RETRY_BOUND, 1000 * 2 ** entry.tries);
|
|
815
|
-
await this.#rows.transact(async (draft) => {
|
|
816
|
-
const current = await draft.get(place);
|
|
817
|
-
const held = current?.outbox.find((one) => one.id === entry.id);
|
|
818
|
-
if (current === null || held === undefined)
|
|
819
|
-
return;
|
|
820
|
-
held.tries++;
|
|
821
|
-
held.next = this.#clock.now() + wait;
|
|
822
|
-
this.#prune(current);
|
|
823
|
-
});
|
|
824
|
-
await this.#arm();
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
await this.#settle(being, place, entry, answer);
|
|
828
|
-
}
|
|
829
|
-
finally {
|
|
830
|
-
this.#inFlight.delete(flight);
|
|
831
|
-
this.#blocked.delete(being);
|
|
832
|
-
}
|
|
833
|
-
await this.pump(being);
|
|
834
|
-
}
|
|
835
|
-
// The answer lands with the entry's removal: through her reply, or alone.
|
|
836
|
-
async #settle(being, place, entry, answer) {
|
|
837
|
-
const remove = async (draft, replied) => {
|
|
838
|
-
const current = await draft.get(place);
|
|
839
|
-
if (current === null)
|
|
840
|
-
return;
|
|
841
|
-
current.outbox = current.outbox.filter((one) => one.id !== entry.id);
|
|
842
|
-
if (entry.reply !== undefined && (replied === null || 'error' in replied)) {
|
|
843
|
-
current.dead.push({ ask: entry.reply, args: answer, at: this.#clock.now(), why: replied === null ? 'she answered nothing' : replied.error.message });
|
|
844
|
-
}
|
|
845
|
-
this.#prune(current);
|
|
846
|
-
};
|
|
847
|
-
if (entry.reply === undefined) {
|
|
848
|
-
await this.#rows.transact((draft) => remove(draft, { result: null }));
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
await this.ask({ being, occupant: STEWARD, house: true, method: entry.reply, args: answer, always: { change: (draft, outcome) => remove(draft, answerOf(outcome)) } });
|
|
852
|
-
}
|
|
853
|
-
// Every entry to that receiver fails with the first. Each stays in flight
|
|
854
|
-
// until its reply has landed, so no pump sends one while its turn comes.
|
|
855
|
-
async #giveUp(being, place, entry) {
|
|
856
|
-
const held = new Set([`${being}\n${entry.id}`]);
|
|
857
|
-
this.#inFlight.add(`${being}\n${entry.id}`);
|
|
858
|
-
try {
|
|
859
|
-
const row = await this.#rows.get(place);
|
|
860
|
-
const behind = (row?.outbox ?? []).filter((one) => laneOf(one) === laneOf(entry));
|
|
861
|
-
for (const one of behind) {
|
|
862
|
-
held.add(`${being}\n${one.id}`);
|
|
863
|
-
this.#inFlight.add(`${being}\n${one.id}`);
|
|
864
|
-
}
|
|
865
|
-
for (const one of behind)
|
|
866
|
-
await this.#settle(being, place, one, errorOf('the call gave up at its deadline'));
|
|
867
|
-
}
|
|
868
|
-
finally {
|
|
869
|
-
for (const flight of held)
|
|
870
|
-
this.#inFlight.delete(flight);
|
|
871
|
-
this.#blocked.delete(being);
|
|
872
|
-
}
|
|
873
|
-
await this.pump(being);
|
|
874
|
-
}
|
|
875
|
-
/**
|
|
876
|
-
* One call to where a target points: a faculty, a being of this house, or a
|
|
877
|
-
* far ward. `until` is when the chain that calls ends, near and far alike.
|
|
878
|
-
* `null` is transient.
|
|
879
|
-
*/
|
|
880
|
-
async deliver(being, row, to, method, args, id, until, after) {
|
|
881
|
-
if ('need' in to) {
|
|
882
|
-
const found = await this.#being(being);
|
|
883
|
-
const offer = found?.resolved.faculties[to.need];
|
|
884
|
-
if (offer === undefined)
|
|
885
|
-
return null;
|
|
886
|
-
return this.#faculty(offer, method, args, id, found.resolved.table.needs[to.need].methods[method]);
|
|
887
|
-
}
|
|
888
|
-
const standing = 'standing' in to ? standingOf(being, row, to.standing) : undefined;
|
|
889
|
-
if ('standing' in to && standing === undefined)
|
|
890
|
-
return errorOf('the standing was dropped');
|
|
891
|
-
if (standing?.quo !== undefined && 'standing' in to) {
|
|
892
|
-
const far = await this.far(being, to.standing, method, args, id, until, after);
|
|
893
|
-
return far === null || 'describe' in far ? null : far;
|
|
894
|
-
}
|
|
895
|
-
const local = 'being' in to ? { being: to.being, occupant: STEWARD } : standing?.local;
|
|
896
|
-
if (local === undefined)
|
|
897
|
-
return null;
|
|
898
|
-
return answerOf(await this.ask({ being: local.being, occupant: local.occupant, method, args, call: id, ...(until === undefined ? {} : { until }), ...(after === undefined ? {} : { after }) }));
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* One ask on a standing to a far ward, through the room and the carry. The
|
|
902
|
-
* count and the knock land before the box leaves, and the move after the
|
|
903
|
-
* reply. `null` is transient: silence, a word, or nothing.
|
|
904
|
-
*/
|
|
905
|
-
far(being, standing, method, args, call, until, after) {
|
|
906
|
-
const key = `${being}\n${standing}`;
|
|
907
|
-
const described = method === undefined ? this.#described.get(key) : undefined;
|
|
908
|
-
if (described !== undefined)
|
|
909
|
-
return Promise.resolve({ describe: described.describe });
|
|
910
|
-
const turn = (this.#relations.get(key) ?? Promise.resolve()).then(() => this.#far(being, standing, method, args, call, until, after));
|
|
911
|
-
this.#relations.set(key, turn.catch(() => undefined));
|
|
912
|
-
return turn;
|
|
913
|
-
}
|
|
914
|
-
/**
|
|
915
|
-
* The domains that vouch for a far standing's ward, read by the carry and
|
|
916
|
-
* kept on the standing. It runs after her ask lands, so a take never
|
|
917
|
-
* waits on the web.
|
|
918
|
-
*/
|
|
919
|
-
async vouch(being, standing) {
|
|
920
|
-
const place = await this.#place(being);
|
|
921
|
-
const held = (await this.#rows.get(place))?.standings[standing]?.quo;
|
|
922
|
-
if (held === undefined)
|
|
923
|
-
return;
|
|
924
|
-
let vouched;
|
|
925
|
-
try {
|
|
926
|
-
vouched = await this.#carry.vouched({ ward: held.invitation.ward, at: held.invitation.at ?? [] });
|
|
927
|
-
}
|
|
928
|
-
catch {
|
|
929
|
-
vouched = [];
|
|
930
|
-
}
|
|
931
|
-
await this.#rows.transact(async (draft) => {
|
|
932
|
-
const relation = (await draft.get(place))?.standings[standing];
|
|
933
|
-
if (relation !== undefined)
|
|
934
|
-
relation.vouched = [...vouched];
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
async #far(being, standing, method, args, call, until, after) {
|
|
938
|
-
const place = await this.#place(being);
|
|
939
|
-
const row = await this.#rows.get(place);
|
|
940
|
-
const held = row?.standings[standing]?.quo;
|
|
941
|
-
if (held === undefined)
|
|
942
|
-
return errorOf('the standing was dropped');
|
|
943
|
-
const moveTo = async (next, route) => this.#rows.transact(async (draft) => {
|
|
944
|
-
const current = await draft.get(place);
|
|
945
|
-
const relation = current?.standings[standing];
|
|
946
|
-
if (relation === undefined)
|
|
947
|
-
return;
|
|
948
|
-
relation.quo = next;
|
|
949
|
-
if (route !== undefined)
|
|
950
|
-
relation.route = route;
|
|
951
|
-
});
|
|
952
|
-
// What is left of the chain goes with the ask, and a spent chain sends nothing.
|
|
953
|
-
const left = until === undefined ? undefined : until - this.#clock.now();
|
|
954
|
-
if (left !== undefined && left <= 0)
|
|
955
|
-
return null;
|
|
956
|
-
const sealed = await this.#room.seal(held, {
|
|
957
|
-
...(method === undefined ? {} : { method, args: this.#tools.canonical(args) }),
|
|
958
|
-
...(call === undefined ? {} : { call }),
|
|
959
|
-
...(left === undefined ? {} : { within: left }),
|
|
960
|
-
...(after === undefined ? {} : { after }),
|
|
961
|
-
});
|
|
962
|
-
if (!(await moveTo(sealed.standing)))
|
|
963
|
-
return null;
|
|
964
|
-
// The address that last answered goes first, then the invitation's in its order.
|
|
965
|
-
const route = row.standings[standing].route;
|
|
966
|
-
const at = [...new Set([...(route === undefined ? [] : [route]), ...(held.invitation.at ?? [])])];
|
|
967
|
-
let reply = null;
|
|
968
|
-
let via;
|
|
969
|
-
// A carry that failed says nothing of where the box went, so it may have been heard.
|
|
970
|
-
let heard = true;
|
|
971
|
-
try {
|
|
972
|
-
// An ask that runs its whole time, a watch above all, still has a margin for its reply to come home.
|
|
973
|
-
const sent = await this.#carry.send({ ward: held.invitation.ward, at, box: sealed.box, ...(left === undefined ? {} : { wait: left + HOMEWARD }) });
|
|
974
|
-
if (sent.reply !== null)
|
|
975
|
-
({ reply, via } = sent);
|
|
976
|
-
else
|
|
977
|
-
heard = sent.heard;
|
|
978
|
-
}
|
|
979
|
-
catch {
|
|
980
|
-
reply = null;
|
|
981
|
-
}
|
|
982
|
-
const { read, standing: moved } = await this.#room.read(sealed.standing, sealed.pending, reply, heard);
|
|
983
|
-
// Where the ward says it is reached now replaces where its invitation said.
|
|
984
|
-
const told = 'object' in read && read.at !== undefined ? { ...moved, invitation: { ...moved.invitation, at: read.at } } : moved;
|
|
985
|
-
// A route is pinned only by a reply that opened as the far ward's, an
|
|
986
|
-
// object or a word. Silence may be bytes no key of hers sealed, and pins nothing.
|
|
987
|
-
await moveTo(told, 'object' in read || 'quo' in read ? via : undefined);
|
|
988
|
-
// A ward reached somewhere new may be vouched for by another domain.
|
|
989
|
-
if (told !== moved)
|
|
990
|
-
void this.vouch(being, standing);
|
|
991
|
-
if (!('object' in read))
|
|
992
|
-
return null;
|
|
993
|
-
const key = `${being}\n${standing}`;
|
|
994
|
-
if (method === undefined) {
|
|
995
|
-
this.#described.set(key, { mark: read.seen, describe: read.object });
|
|
996
|
-
return { describe: read.object };
|
|
997
|
-
}
|
|
998
|
-
if (this.#described.get(key)?.mark !== read.seen)
|
|
999
|
-
this.#described.delete(key);
|
|
1000
|
-
const object = read.object;
|
|
1001
|
-
if (object !== null && typeof object === 'object' && 'result' in object && Object.keys(object).length === 1)
|
|
1002
|
-
return { result: object.result };
|
|
1003
|
-
if (object !== null && typeof object === 'object' && typeof object.error?.message === 'string')
|
|
1004
|
-
return errorOf(object.error.message);
|
|
1005
|
-
return errorOf('the far being answered no answer');
|
|
1006
|
-
}
|
|
1007
|
-
// What calls a faculty's tokens: the same for a method's context and for the one it is told the house opened with.
|
|
1008
|
-
#calls(offer) {
|
|
1009
|
-
return {
|
|
1010
|
-
call: ({ token, method: asked, args: called, id: callId, after, home }) => this.#token(token, { method: asked, args: called ?? {}, id: callId, after, home }, offer),
|
|
1011
|
-
describe: ({ token }) => this.#tokenDescribe(token, offer),
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
/** Each offer told the house opened, with the context that calls its tokens. */
|
|
1015
|
-
async tellOpened() {
|
|
1016
|
-
for (const offer of this.#offers)
|
|
1017
|
-
await offer.opened?.({ ward: this.ward, ...this.#calls(offer) });
|
|
1018
|
-
}
|
|
1019
|
-
async #faculty(offer, method, args, id, want) {
|
|
1020
|
-
const context = { id, ...this.#calls(offer) };
|
|
1021
|
-
// Her need let the args pass, and the offer may be narrower: covering is decided on names, and types on each call.
|
|
1022
|
-
const spec = offer.bp.methods[method];
|
|
1023
|
-
const refused = spec === undefined ? `the offer ${offer.bp.name} has no method ${method}` : this.#tools.check(spec.args, args);
|
|
1024
|
-
if (refused !== null)
|
|
1025
|
-
return errorOf(refused);
|
|
1026
|
-
let answered;
|
|
1027
|
-
try {
|
|
1028
|
-
answered = await offer.object[method](args, context);
|
|
1029
|
-
}
|
|
1030
|
-
catch {
|
|
1031
|
-
// A throw is a failure to answer, and transient: silence, which no cause is read from.
|
|
1032
|
-
return null;
|
|
1033
|
-
}
|
|
1034
|
-
if (typeof answered !== 'object' || answered === null)
|
|
1035
|
-
return errorOf('the faculty answered no answer');
|
|
1036
|
-
if ('error' in answered) {
|
|
1037
|
-
const message = answered.error?.message;
|
|
1038
|
-
return errorOf(typeof message === 'string' ? message : 'the faculty failed');
|
|
1039
|
-
}
|
|
1040
|
-
if (!('result' in answered))
|
|
1041
|
-
return errorOf('the faculty answered no answer');
|
|
1042
|
-
const result = answered.result;
|
|
1043
|
-
const why = want?.result === undefined ? null : this.#tools.check(want.result, result);
|
|
1044
|
-
return why === null ? { result } : errorOf(why);
|
|
1045
|
-
}
|
|
1046
|
-
// The occupant a faculty's token reaches, and the one ask a handle's
|
|
1047
|
-
// token is admitted to. A token answers the faculty it was handed to
|
|
1048
|
-
// alone: another holding the string is told there is no such token.
|
|
1049
|
-
async #tokenHolder(token, offer) {
|
|
1050
|
-
// A token her ask handed out before it landed answers once it has.
|
|
1051
|
-
await this.#tokening.get(token);
|
|
1052
|
-
const owner = await this.#rows.get(await this.#tokenPlace(token));
|
|
1053
|
-
if (owner === null || owner.faculty !== offer.bp.name)
|
|
1054
|
-
return null;
|
|
1055
|
-
const found = await this.#being(owner.being);
|
|
1056
|
-
if (found === null || !Object.values(found.resolved.faculties).includes(offer))
|
|
1057
|
-
return null;
|
|
1058
|
-
const held = (await this.#rows.get(await this.#place(owner.being)))?.occupants[owner.occupant];
|
|
1059
|
-
if (held === undefined)
|
|
1060
|
-
return null;
|
|
1061
|
-
return { being: owner.being, occupant: owner.occupant, ...(held.ask === undefined ? {} : { ask: held.ask }) };
|
|
1062
|
-
}
|
|
1063
|
-
// A faculty asks through a token it was handed: a handle's one ask, or
|
|
1064
|
-
// any ask its occupant may call, as that occupant.
|
|
1065
|
-
async #token(token, { method: asked, args, id, after, home }, offer) {
|
|
1066
|
-
const holder = await this.#tokenHolder(token, offer);
|
|
1067
|
-
if (holder === null)
|
|
1068
|
-
return errorOf('no such token');
|
|
1069
|
-
const method = holder.ask ?? asked;
|
|
1070
|
-
if (method === undefined)
|
|
1071
|
-
return errorOf('a call on an occupant names its method');
|
|
1072
|
-
if (asked !== undefined && asked !== method)
|
|
1073
|
-
return errorOf('no such ask');
|
|
1074
|
-
const watch = after === undefined ? {} : { after: await this.digest(after) };
|
|
1075
|
-
// A door carried home leaves for a house, as an invitation; every other for this faculty, as a token.
|
|
1076
|
-
const reader = home === true ? 'house' : { faculty: offer.bp.name };
|
|
1077
|
-
const outcome = await this.ask({ being: holder.being, occupant: holder.occupant, method, args, call: `token:${id}`, reader, ...watch });
|
|
1078
|
-
return outcome !== null && 'answer' in outcome ? outcome.answer : errorOf('the house answered nothing');
|
|
1079
|
-
}
|
|
1080
|
-
// What the occupant behind a token may ask now: her describe, as that occupant sees it.
|
|
1081
|
-
async #tokenDescribe(token, offer) {
|
|
1082
|
-
const holder = await this.#tokenHolder(token, offer);
|
|
1083
|
-
if (holder === null)
|
|
1084
|
-
return errorOf('no such token');
|
|
1085
|
-
const outcome = await this.ask({ being: holder.being, occupant: holder.occupant, reader: { faculty: offer.bp.name } });
|
|
1086
|
-
if (outcome !== null && 'describe' in outcome)
|
|
1087
|
-
return { describe: outcome.describe };
|
|
1088
|
-
return errorOf('the house answered nothing');
|
|
1089
|
-
}
|
|
1090
|
-
// ---- what a transaction reaches ----
|
|
1091
|
-
get tools() {
|
|
1092
|
-
return this.#tools;
|
|
1093
|
-
}
|
|
1094
|
-
get marks() {
|
|
1095
|
-
return this.#marks;
|
|
1096
|
-
}
|
|
1097
|
-
/** A promise raced against the house's clock: `null` where the wait runs out first. */
|
|
1098
|
-
async within(ms, promise) {
|
|
1099
|
-
const id = `within:${++this.#withins}`;
|
|
1100
|
-
const timeout = this.#clock.wait({ id, ms }).then((fired) => (fired ? null : new Promise(() => undefined)));
|
|
1101
|
-
try {
|
|
1102
|
-
return await Promise.race([promise, timeout]);
|
|
1103
|
-
}
|
|
1104
|
-
finally {
|
|
1105
|
-
this.#clock.cancel({ id });
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
get crypto() {
|
|
1109
|
-
return this.#crypto;
|
|
1110
|
-
}
|
|
1111
|
-
get clock() {
|
|
1112
|
-
return this.#clock;
|
|
1113
|
-
}
|
|
1114
|
-
get room() {
|
|
1115
|
-
return this.#room;
|
|
1116
|
-
}
|
|
1117
|
-
get carry() {
|
|
1118
|
-
return this.#carry;
|
|
1119
|
-
}
|
|
1120
|
-
/** The key this house signs with as a stranger to one far ward: its own for that ward, the same every time. */
|
|
1121
|
-
async strangerSecret(ward) {
|
|
1122
|
-
return this.#tools.hex(await this.#keys.derive(`stranger:${ward}`, 32));
|
|
1123
|
-
}
|
|
1124
|
-
get wardPlace() {
|
|
1125
|
-
return this.#wardPlace;
|
|
1126
|
-
}
|
|
1127
|
-
get minting() {
|
|
1128
|
-
return this.#minting;
|
|
1129
|
-
}
|
|
1130
|
-
get tokening() {
|
|
1131
|
-
return this.#tokening;
|
|
1132
|
-
}
|
|
1133
|
-
place(being) {
|
|
1134
|
-
return this.#place(being);
|
|
1135
|
-
}
|
|
1136
|
-
resolveKind(kind) {
|
|
1137
|
-
return this.#resolve(kind);
|
|
1138
|
-
}
|
|
1139
|
-
beingOf(id) {
|
|
1140
|
-
return this.#being(id);
|
|
1141
|
-
}
|
|
1142
|
-
rows() {
|
|
1143
|
-
return this.#rows;
|
|
1144
|
-
}
|
|
1145
|
-
heirPlace(heir) {
|
|
1146
|
-
return this.#heirPlace(heir);
|
|
1147
|
-
}
|
|
1148
|
-
tokenPlace(token) {
|
|
1149
|
-
return this.#tokenPlace(token);
|
|
1150
|
-
}
|
|
1151
|
-
dismissIn(draft, row, occupant) {
|
|
1152
|
-
return this.#dismiss(draft, row, occupant);
|
|
1153
|
-
}
|
|
1154
|
-
forget(draft, held) {
|
|
1155
|
-
return this.#forget(draft, held);
|
|
1156
|
-
}
|
|
1157
|
-
/**
|
|
1158
|
-
* Every heir of hers left unspent past its time, let go. It runs where
|
|
1159
|
-
* she mints and where one of her heirs binds, so nothing waits on a clock
|
|
1160
|
-
* to clean, and it reads her row alone.
|
|
1161
|
-
*/
|
|
1162
|
-
async sweep(draft, row) {
|
|
1163
|
-
const now = this.#clock.now();
|
|
1164
|
-
for (const held of Object.values(row.occupants)) {
|
|
1165
|
-
for (const [heir, until] of Object.entries(held.until ?? {})) {
|
|
1166
|
-
if (until > now)
|
|
1167
|
-
continue;
|
|
1168
|
-
delete held.until[heir];
|
|
1169
|
-
if (held.quo?.[heir]?.spent === true)
|
|
1170
|
-
continue;
|
|
1171
|
-
if (held.quo !== undefined)
|
|
1172
|
-
delete held.quo[heir];
|
|
1173
|
-
if (held.told !== undefined)
|
|
1174
|
-
delete held.told[heir];
|
|
1175
|
-
draft.set(await this.#heirPlace(heir), null);
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
// Whose occupant an heir reaches, and whether it may still bind now.
|
|
1180
|
-
async #heir(heir) {
|
|
1181
|
-
const owner = await this.#rows.get(await this.#heirPlace(heir));
|
|
1182
|
-
if (owner === null)
|
|
1183
|
-
return undefined;
|
|
1184
|
-
const row = await this.#rows.get(await this.#place(owner.being));
|
|
1185
|
-
const until = row?.occupants[owner.occupant]?.until?.[heir];
|
|
1186
|
-
// An heir left unspent past its time is one the door never held.
|
|
1187
|
-
if (row === null || (until !== undefined && until <= this.#clock.now()))
|
|
1188
|
-
return undefined;
|
|
1189
|
-
return { owner, row };
|
|
1190
|
-
}
|
|
1191
|
-
heir(heir) {
|
|
1192
|
-
return this.#heir(heir);
|
|
1193
|
-
}
|
|
1194
|
-
arm() {
|
|
1195
|
-
return this.#arm();
|
|
1196
|
-
}
|
|
1197
|
-
asker(being, row, occupant) {
|
|
1198
|
-
return this.#asker(being, row, occupant, false);
|
|
1199
|
-
}
|
|
1200
|
-
rolesFor(table, asker, me) {
|
|
1201
|
-
return this.#roles(table, asker, me);
|
|
1202
|
-
}
|
|
1203
|
-
shownTo(entry, asker, roles, position) {
|
|
1204
|
-
return this.#shown(entry, asker, roles, position);
|
|
1205
|
-
}
|
|
1206
|
-
// ---- the door ----
|
|
1207
|
-
/** Sealed bytes in, sealed bytes or nothing out. It settles once what the ask caused is written. */
|
|
1208
|
-
async door(box) {
|
|
1209
|
-
const since = this.#settled;
|
|
1210
|
-
const ward = await this.#rows.get(this.#wardPlace);
|
|
1211
|
-
let found;
|
|
1212
|
-
const arrival = await this.#room.arrive(box, {
|
|
1213
|
-
occupant: async (heir) => {
|
|
1214
|
-
const held = await this.#heir(heir);
|
|
1215
|
-
found = held?.owner;
|
|
1216
|
-
return held?.row.occupants[held.owner.occupant]?.quo?.[heir];
|
|
1217
|
-
},
|
|
1218
|
-
zero: ward?.beings[PUBLIC] !== undefined,
|
|
1219
|
-
});
|
|
1220
|
-
if (arrival.refused)
|
|
1221
|
-
return arrival.reply;
|
|
1222
|
-
const parsed = this.#tools.parse(arrival.args);
|
|
1223
|
-
const args = (parsed?.value ?? {});
|
|
1224
|
-
if (arrival.heir === null)
|
|
1225
|
-
return this.#stranger(arrival, args);
|
|
1226
|
-
// Admitted on a record another arrival is moving, or moved since this
|
|
1227
|
-
// one read it: judged again, on the record as it stands.
|
|
1228
|
-
const heir = arrival.heir;
|
|
1229
|
-
const busy = this.#arriving.get(heir);
|
|
1230
|
-
if (busy !== undefined || (this.#moved.get(heir) ?? -1) >= since) {
|
|
1231
|
-
await busy?.catch(() => undefined);
|
|
1232
|
-
return this.door(box);
|
|
1233
|
-
}
|
|
1234
|
-
const owner = found;
|
|
1235
|
-
let done;
|
|
1236
|
-
this.#arriving.set(heir, new Promise((settle) => (done = settle)));
|
|
1237
|
-
try {
|
|
1238
|
-
const place = await this.#place(owner.being);
|
|
1239
|
-
let reply = null;
|
|
1240
|
-
let chosen = null;
|
|
1241
|
-
// Quo's move and count land with the ask, whatever its outcome.
|
|
1242
|
-
const always = {
|
|
1243
|
-
change: async (draft, outcome) => {
|
|
1244
|
-
const row = await draft.get(place);
|
|
1245
|
-
const held = row?.occupants[owner.occupant];
|
|
1246
|
-
// Where the ward is reached now goes in a reply only when this holder last heard otherwise.
|
|
1247
|
-
const now = this.#carry.at({});
|
|
1248
|
-
const told = held?.told?.[heir];
|
|
1249
|
-
const moved = outcome !== null && (told === undefined || told.length !== now.length || told.some((address, at) => address !== now[at]));
|
|
1250
|
-
const { reply: sealed, occupant } = await arrival.choose(this.#choice(outcome, moved ? now : undefined));
|
|
1251
|
-
chosen = sealed;
|
|
1252
|
-
const binds = held?.quo?.[heir]?.spent === false && occupant?.spent === true;
|
|
1253
|
-
if (held?.quo !== undefined && occupant !== null)
|
|
1254
|
-
held.quo[heir] = occupant;
|
|
1255
|
-
if (held !== undefined && moved)
|
|
1256
|
-
held.told = { ...held.told, [heir]: now };
|
|
1257
|
-
// The knock that binds spends the heir, which never expires after, and lets go of every other past its time.
|
|
1258
|
-
if (binds && row !== null) {
|
|
1259
|
-
if (held?.until !== undefined)
|
|
1260
|
-
delete held.until[heir];
|
|
1261
|
-
await this.sweep(draft, row);
|
|
1262
|
-
}
|
|
1263
|
-
},
|
|
1264
|
-
landed: () => {
|
|
1265
|
-
reply = chosen;
|
|
1266
|
-
},
|
|
1267
|
-
};
|
|
1268
|
-
await this.ask({
|
|
1269
|
-
...(arrival.strange ? { strange: true } : {}),
|
|
1270
|
-
being: owner.being,
|
|
1271
|
-
occupant: owner.occupant,
|
|
1272
|
-
...(arrival.method === undefined ? {} : { method: arrival.method }),
|
|
1273
|
-
args,
|
|
1274
|
-
...(arrival.call === undefined ? {} : { call: arrival.call }),
|
|
1275
|
-
...(arrival.within === undefined ? {} : { until: this.#clock.now() + arrival.within }),
|
|
1276
|
-
...(arrival.after === undefined ? {} : { after: arrival.after }),
|
|
1277
|
-
always,
|
|
1278
|
-
});
|
|
1279
|
-
return reply;
|
|
1280
|
-
}
|
|
1281
|
-
finally {
|
|
1282
|
-
this.#moved.set(heir, this.#settled++);
|
|
1283
|
-
this.#arriving.delete(heir);
|
|
1284
|
-
done();
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
#choice(outcome, at) {
|
|
1288
|
-
if (outcome === null)
|
|
1289
|
-
return { silence: true };
|
|
1290
|
-
const object = 'describe' in outcome ? outcome.describe : outcome.answer;
|
|
1291
|
-
return { object: this.#tools.canonical(object), seen: outcome.mark, ...(at === undefined ? {} : { at }) };
|
|
1292
|
-
}
|
|
1293
|
-
// The zero head: a stranger asks the public being, and a replay within the window gets the same answer.
|
|
1294
|
-
async #stranger(arrival, args) {
|
|
1295
|
-
const now = this.#clock.now();
|
|
1296
|
-
for (const [signature, kept] of this.#replays)
|
|
1297
|
-
if (kept.at + REPLAY_WINDOW <= now)
|
|
1298
|
-
this.#replays.delete(signature);
|
|
1299
|
-
const kept = this.#replays.get(arrival.signature);
|
|
1300
|
-
if (kept !== undefined)
|
|
1301
|
-
return (await arrival.choose(kept.choice)).reply;
|
|
1302
|
-
// Where memory refuses her write, the house answers nothing, so the stranger asks again.
|
|
1303
|
-
let landed = false;
|
|
1304
|
-
const outcome = await this.ask({
|
|
1305
|
-
being: PUBLIC,
|
|
1306
|
-
occupant: 'stranger',
|
|
1307
|
-
...(arrival.strange ? { strange: true } : {}),
|
|
1308
|
-
signer: this.#tools.bytes(arrival.by),
|
|
1309
|
-
...(arrival.method === undefined ? {} : { method: arrival.method }),
|
|
1310
|
-
args,
|
|
1311
|
-
...(arrival.within === undefined ? {} : { until: now + arrival.within }),
|
|
1312
|
-
...(arrival.after === undefined ? {} : { after: arrival.after }),
|
|
1313
|
-
always: {
|
|
1314
|
-
change: () => undefined,
|
|
1315
|
-
landed: () => {
|
|
1316
|
-
landed = true;
|
|
1317
|
-
},
|
|
1318
|
-
},
|
|
1319
|
-
});
|
|
1320
|
-
if (!landed)
|
|
1321
|
-
return null;
|
|
1322
|
-
const choice = this.#choice(outcome);
|
|
1323
|
-
this.#replays.set(arrival.signature, { choice, at: now });
|
|
1324
|
-
return (await arrival.choose(choice)).reply;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
const targetKey = (to) => ('need' in to ? `need:${to.need}` : 'standing' in to ? `standing:${to.standing}` : `being:${to.being}`);
|
|
1328
|
-
// The lane an entry leaves in: its receiver's, one at a time and in order, or a watch's own beside it.
|
|
1329
|
-
const laneOf = (entry) => (entry.after === undefined ? targetKey(entry.to) : `${targetKey(entry.to)}\nwatch:${entry.method}`);
|
|
1330
|
-
/** One ask's work: her reach, and the changes that land with her cells. */
|
|
1331
|
-
class Tx {
|
|
1332
|
-
#house;
|
|
1333
|
-
#being;
|
|
1334
|
-
#row;
|
|
1335
|
-
#resolved;
|
|
1336
|
-
ops = [];
|
|
1337
|
-
heirs = [];
|
|
1338
|
-
tokens = [];
|
|
1339
|
-
#end;
|
|
1340
|
-
/** Settles once her ask has landed or failed. */
|
|
1341
|
-
ended = new Promise((settle) => (this.#end = settle));
|
|
1342
|
-
/** Occupants her steward's powers minted on other beings in this ask. */
|
|
1343
|
-
#invited = new Set();
|
|
1344
|
-
#pending = [];
|
|
1345
|
-
#after = [];
|
|
1346
|
-
/** Invitations she took before their mint landed, each put back where her ask lands nothing. */
|
|
1347
|
-
#untaken = [];
|
|
1348
|
-
/** Far standings she took in this ask, which she asks from the next. */
|
|
1349
|
-
#taken = new Set();
|
|
1350
|
-
cells = {};
|
|
1351
|
-
#effects = false;
|
|
1352
|
-
/** When her ask's own wait runs out. */
|
|
1353
|
-
#deadline;
|
|
1354
|
-
constructor(house, being, row, resolved, deadline) {
|
|
1355
|
-
this.#house = house;
|
|
1356
|
-
this.#being = being;
|
|
1357
|
-
this.#row = copy(row);
|
|
1358
|
-
this.#resolved = resolved;
|
|
1359
|
-
this.#deadline = deadline;
|
|
1360
|
-
}
|
|
1361
|
-
#op(op, mirror) {
|
|
1362
|
-
this.ops.push(op);
|
|
1363
|
-
mirror?.(this.#row);
|
|
1364
|
-
}
|
|
1365
|
-
end() {
|
|
1366
|
-
this.#end();
|
|
1367
|
-
}
|
|
1368
|
-
/** Her ask landed nothing, so what she took was never taken. */
|
|
1369
|
-
untake() {
|
|
1370
|
-
for (const back of this.#untaken.splice(0))
|
|
1371
|
-
back();
|
|
1372
|
-
}
|
|
1373
|
-
/** Waits for every call she made and did not await. */
|
|
1374
|
-
async settle() {
|
|
1375
|
-
while (this.#pending.length > 0)
|
|
1376
|
-
await Promise.all(this.#pending.splice(0));
|
|
1377
|
-
}
|
|
1378
|
-
/** Her calls let go where her ask failed: nothing of them lands. */
|
|
1379
|
-
async abandon() {
|
|
1380
|
-
await Promise.allSettled(this.#pending.splice(0));
|
|
1381
|
-
}
|
|
1382
|
-
/** What runs once her write has landed. */
|
|
1383
|
-
async landed() {
|
|
1384
|
-
if (this.#effects)
|
|
1385
|
-
void this.#house.pump(this.#being);
|
|
1386
|
-
for (const after of this.#after)
|
|
1387
|
-
void after();
|
|
1388
|
-
await this.#house.arm();
|
|
1389
|
-
}
|
|
1390
|
-
// An invitation's hex made a standing: bound at once where it is this house's.
|
|
1391
|
-
async accept(hex) {
|
|
1392
|
-
const house = this.#house;
|
|
1393
|
-
const bytes = house.tools.bytes(hex);
|
|
1394
|
-
const text = bytes === null ? null : house.tools.text(bytes);
|
|
1395
|
-
const parsed = text === null ? null : house.tools.parse(text);
|
|
1396
|
-
const invitation = parsed === null ? null : await house.room.invitation(parsed.value);
|
|
1397
|
-
if (invitation === null)
|
|
1398
|
-
throw new Fail('an invitation was owed and none came');
|
|
1399
|
-
// Named by its heir, so a paper she took already answers the standing it made.
|
|
1400
|
-
const id = `standing:${house.tools.hex((await house.crypto.sha256(house.tools.utf8(invitation.heir))).subarray(0, 8))}`;
|
|
1401
|
-
if (this.#row.standings[id] !== undefined)
|
|
1402
|
-
return id;
|
|
1403
|
-
if (invitation.ward === (await house.room.ward())) {
|
|
1404
|
-
const minting = house.minting.get(invitation.heir);
|
|
1405
|
-
const landed = (await house.heir(invitation.heir))?.owner;
|
|
1406
|
-
const owner = landed ?? (minting !== undefined && (minting.until ?? Number.POSITIVE_INFINITY) > house.clock.now() ? minting : undefined);
|
|
1407
|
-
if (owner === undefined)
|
|
1408
|
-
throw new Fail('the invitation is spent or unknown');
|
|
1409
|
-
if (owner.being === this.#being)
|
|
1410
|
-
throw new Fail('she takes no invitation to herself');
|
|
1411
|
-
// Taken while its mint has not landed, it is spent before it lands: the mint writes nothing of it.
|
|
1412
|
-
// Where her ask fails, it was never taken, and goes back.
|
|
1413
|
-
house.minting.delete(invitation.heir);
|
|
1414
|
-
if (landed === undefined)
|
|
1415
|
-
this.#untaken.push(() => house.minting.set(invitation.heir, owner));
|
|
1416
|
-
const standing = { notes: {}, steward: {}, local: { being: owner.being, occupant: owner.occupant } };
|
|
1417
|
-
this.#op(async (draft, row) => {
|
|
1418
|
-
const heirPlace = await house.heirPlace(invitation.heir);
|
|
1419
|
-
// Read in her write, so a taker who landed first leaves this one nothing to bind.
|
|
1420
|
-
if (landed !== undefined && (await draft.get(heirPlace)) === null)
|
|
1421
|
-
throw new Fail('the invitation is spent or unknown');
|
|
1422
|
-
row.standings[id] = standing;
|
|
1423
|
-
const target = owner.being === this.#being ? row : await draft.get(await house.place(owner.being));
|
|
1424
|
-
draft.set(heirPlace, null);
|
|
1425
|
-
const occupant = target?.occupants[owner.occupant];
|
|
1426
|
-
if (occupant?.quo !== undefined)
|
|
1427
|
-
delete occupant.quo[invitation.heir];
|
|
1428
|
-
if (occupant?.told !== undefined)
|
|
1429
|
-
delete occupant.told[invitation.heir];
|
|
1430
|
-
if (occupant?.until !== undefined)
|
|
1431
|
-
delete occupant.until[invitation.heir];
|
|
1432
|
-
}, (row) => {
|
|
1433
|
-
row.standings[id] = standing;
|
|
1434
|
-
});
|
|
1435
|
-
return id;
|
|
1436
|
-
}
|
|
1437
|
-
const standing = { notes: {}, steward: {}, quo: house.room.standing(invitation) };
|
|
1438
|
-
this.#taken.add(id);
|
|
1439
|
-
this.#after.push(() => house.vouch(this.#being, id));
|
|
1440
|
-
this.#op((_draft, row) => {
|
|
1441
|
-
row.standings[id] = standing;
|
|
1442
|
-
}, (row) => {
|
|
1443
|
-
row.standings[id] = standing;
|
|
1444
|
-
});
|
|
1445
|
-
return id;
|
|
1446
|
-
}
|
|
1447
|
-
/** A handle she holds made what its reader can call: an invitation's hex, or a token. */
|
|
1448
|
-
async mint({ being, occupant, expires }, reader) {
|
|
1449
|
-
const house = this.#house;
|
|
1450
|
-
const own = being === this.#being;
|
|
1451
|
-
if (own ? this.#row.occupants[occupant] === undefined : !this.#invited.has(`${being}\n${occupant}`))
|
|
1452
|
-
throw new Fail('the handle was let go');
|
|
1453
|
-
const target = async (draft, row) => (own ? row : draft.get(await house.place(being)));
|
|
1454
|
-
if (reader !== 'house')
|
|
1455
|
-
return this.#token(being, occupant, reader);
|
|
1456
|
-
const { invitation, occupant: end } = await house.room.invite(house.carry.at({}));
|
|
1457
|
-
const until = expires === undefined ? undefined : house.clock.now() + expires;
|
|
1458
|
-
this.heirs.push(invitation.heir);
|
|
1459
|
-
house.minting.set(invitation.heir, { being, occupant, ...(until === undefined ? {} : { until }) });
|
|
1460
|
-
this.#op(async (draft, row) => {
|
|
1461
|
-
const minted = await target(draft, row);
|
|
1462
|
-
if (minted === null)
|
|
1463
|
-
return;
|
|
1464
|
-
await house.sweep(draft, minted);
|
|
1465
|
-
const held = minted.occupants[occupant];
|
|
1466
|
-
// A being of this house took it while this ask ran, so it lands spent.
|
|
1467
|
-
if (held === undefined || !house.minting.has(invitation.heir))
|
|
1468
|
-
return;
|
|
1469
|
-
held.quo = { ...held.quo, [invitation.heir]: end };
|
|
1470
|
-
held.told = { ...held.told, [invitation.heir]: invitation.at ?? [] };
|
|
1471
|
-
if (until !== undefined)
|
|
1472
|
-
held.until = { ...held.until, [invitation.heir]: until };
|
|
1473
|
-
draft.set(await house.heirPlace(invitation.heir), { being, occupant });
|
|
1474
|
-
});
|
|
1475
|
-
return house.tools.hex(house.tools.utf8(house.tools.canonical(invitation)));
|
|
1476
|
-
}
|
|
1477
|
-
// A token a faculty calls, for one occupant of a being of this house.
|
|
1478
|
-
#token(being, occupant, reader) {
|
|
1479
|
-
const house = this.#house;
|
|
1480
|
-
const token = house.tools.hex(house.crypto.random(16));
|
|
1481
|
-
this.tokens.push(token);
|
|
1482
|
-
house.tokening.set(token, this.ended);
|
|
1483
|
-
this.#op(async (draft, row) => {
|
|
1484
|
-
const held = (being === this.#being ? row : await draft.get(await house.place(being)))?.occupants[occupant];
|
|
1485
|
-
if (held === undefined)
|
|
1486
|
-
return;
|
|
1487
|
-
held.tokens = [...(held.tokens ?? []), token];
|
|
1488
|
-
draft.set(await house.tokenPlace(token), { being, occupant, faculty: reader.faculty });
|
|
1489
|
-
});
|
|
1490
|
-
return token;
|
|
1491
|
-
}
|
|
1492
|
-
/**
|
|
1493
|
-
* An invitation this house minted, carried unopened to a faculty, made a
|
|
1494
|
-
* token for its occupant there. Its heir is spent, so no house ever binds
|
|
1495
|
-
* it after. Any other invitation, or one bound already, stays itself.
|
|
1496
|
-
*/
|
|
1497
|
-
async redeem(hex, reader) {
|
|
1498
|
-
if (reader === 'house')
|
|
1499
|
-
return undefined;
|
|
1500
|
-
const house = this.#house;
|
|
1501
|
-
const bytes = house.tools.bytes(hex);
|
|
1502
|
-
const text = bytes === null ? null : house.tools.text(bytes);
|
|
1503
|
-
if (text === null)
|
|
1504
|
-
return undefined;
|
|
1505
|
-
let read;
|
|
1506
|
-
try {
|
|
1507
|
-
read = JSON.parse(text);
|
|
1508
|
-
}
|
|
1509
|
-
catch {
|
|
1510
|
-
return undefined;
|
|
1511
|
-
}
|
|
1512
|
-
const { ward, heir } = (read ?? {});
|
|
1513
|
-
if (ward !== house.ward || typeof heir !== 'string')
|
|
1514
|
-
return undefined;
|
|
1515
|
-
const minting = house.minting.get(heir);
|
|
1516
|
-
const owner = minting ?? (await house.heir(heir))?.owner;
|
|
1517
|
-
if (owner === undefined)
|
|
1518
|
-
return undefined;
|
|
1519
|
-
const { being, occupant } = owner;
|
|
1520
|
-
// An heir minted in an ask still running lands spent; one landed is let go here.
|
|
1521
|
-
if (minting !== undefined)
|
|
1522
|
-
house.minting.delete(heir);
|
|
1523
|
-
else {
|
|
1524
|
-
const bound = (await house.heir(heir))?.row.occupants[occupant]?.quo?.[heir];
|
|
1525
|
-
if (bound === undefined || bound.spent)
|
|
1526
|
-
return undefined;
|
|
1527
|
-
}
|
|
1528
|
-
this.#op(async (draft, row) => {
|
|
1529
|
-
const minted = being === this.#being ? row : await draft.get(await house.place(being));
|
|
1530
|
-
const held = minted?.occupants[occupant];
|
|
1531
|
-
if (held !== undefined) {
|
|
1532
|
-
if (held.quo !== undefined)
|
|
1533
|
-
delete held.quo[heir];
|
|
1534
|
-
if (held.told !== undefined)
|
|
1535
|
-
delete held.told[heir];
|
|
1536
|
-
if (held.until !== undefined)
|
|
1537
|
-
delete held.until[heir];
|
|
1538
|
-
}
|
|
1539
|
-
draft.set(await house.heirPlace(heir), null);
|
|
1540
|
-
});
|
|
1541
|
-
return this.#token(being, occupant, reader);
|
|
1542
|
-
}
|
|
1543
|
-
#fresh(prefix) {
|
|
1544
|
-
return `${prefix}${this.#house.tools.hex(this.#house.crypto.random(8))}`;
|
|
1545
|
-
}
|
|
1546
|
-
// A far standing she took in this ask: its knock would spend the heir on an ask that may land nothing.
|
|
1547
|
-
#untakenYet(standing) {
|
|
1548
|
-
if (this.#taken.has(standing))
|
|
1549
|
-
throw new Fail(`${standing} was taken in this ask, and is asked from the next`);
|
|
1550
|
-
}
|
|
1551
|
-
#checkReply(reply) {
|
|
1552
|
-
if (reply !== undefined && this.#resolved.table.asks[reply] === undefined)
|
|
1553
|
-
throw new Fail(`she has no ask ${reply} to reply to`);
|
|
1554
|
-
}
|
|
1555
|
-
// A call out: awaited now, or an effect queued to leave once she lands.
|
|
1556
|
-
#call(to, method, args, awaited, reply, schema, watch) {
|
|
1557
|
-
const house = this.#house;
|
|
1558
|
-
const reader = 'need' in to ? { faculty: this.#resolved.faculties[to.need].bp.name } : 'house';
|
|
1559
|
-
const wire = async () => {
|
|
1560
|
-
try {
|
|
1561
|
-
return (await outward(house.tools, house.marks, schema?.args, args ?? {}, this.#being, (handle) => this.mint(handle, reader)));
|
|
1562
|
-
}
|
|
1563
|
-
catch (error) {
|
|
1564
|
-
if (error instanceof Crossing)
|
|
1565
|
-
throw new Fail(error.message);
|
|
1566
|
-
throw error;
|
|
1567
|
-
}
|
|
1568
|
-
};
|
|
1569
|
-
// A watch given a reply leaves after she lands, as an effect does, and its answer asks her reply.
|
|
1570
|
-
if (awaited && !(watch !== undefined && reply !== undefined)) {
|
|
1571
|
-
return (async () => {
|
|
1572
|
-
if ('standing' in to)
|
|
1573
|
-
this.#untakenYet(to.standing);
|
|
1574
|
-
const sent = await wire();
|
|
1575
|
-
const id = house.tools.hex(house.crypto.random(16));
|
|
1576
|
-
// A watch carries the digest of the result she holds.
|
|
1577
|
-
const after = watch === undefined ? undefined : await house.digest({ result: (watch.held ?? null) });
|
|
1578
|
-
// The callee's wait, and never past what is left of her own.
|
|
1579
|
-
const wait = Math.min(schema?.wait ?? WAIT, this.#deadline - house.clock.now());
|
|
1580
|
-
// The callee has what she waits for, and not a moment more, near or far.
|
|
1581
|
-
const answer = wait <= 0 ? null : await house.within(wait, house.deliver(this.#being, this.#row, to, method, sent, id, house.clock.now() + wait, after));
|
|
1582
|
-
// Silence has no cause she may read: a dead address, a spent heir and a late reply are one.
|
|
1583
|
-
if (answer === null)
|
|
1584
|
-
throw new Fail(`${method} answered nothing`);
|
|
1585
|
-
if ('error' in answer)
|
|
1586
|
-
throw new Fail(answer.error.message);
|
|
1587
|
-
return inward(house.tools, house.marks, schema?.result, answer.result, (hex) => this.accept(hex));
|
|
1588
|
-
})();
|
|
1589
|
-
}
|
|
1590
|
-
this.#checkReply(reply);
|
|
1591
|
-
const queued = (async () => {
|
|
1592
|
-
const sent = await wire();
|
|
1593
|
-
const now = house.clock.now();
|
|
1594
|
-
const window = 'need' in to ? (this.#resolved.faculties[to.need]?.window ?? WEEK) : WEEK;
|
|
1595
|
-
const watching = watch === undefined ? {} : { after: await house.digest({ result: (watch.held ?? null) }), wait: schema?.wait ?? WAIT };
|
|
1596
|
-
const entry = { id: house.tools.hex(house.crypto.random(16)), to, method, args: sent, ...(reply === undefined ? {} : { reply }), ...watching, queued: now, deadline: now + window, next: now, tries: 0 };
|
|
1597
|
-
this.#effects = true;
|
|
1598
|
-
this.#op((_draft, row) => {
|
|
1599
|
-
row.outbox.push(entry);
|
|
1600
|
-
});
|
|
1601
|
-
})();
|
|
1602
|
-
this.#pending.push(queued);
|
|
1603
|
-
return undefined;
|
|
1604
|
-
}
|
|
1605
|
-
// The face of one need, or of a standing matched to a need: its methods alone.
|
|
1606
|
-
#face(to, blueprint) {
|
|
1607
|
-
const face = {};
|
|
1608
|
-
for (const [method, spec] of Object.entries(blueprint.methods)) {
|
|
1609
|
-
face[method] = (args, options) => {
|
|
1610
|
-
const watching = options !== undefined && 'after' in options;
|
|
1611
|
-
// A watch asks a being's readOnly ask, and nothing else.
|
|
1612
|
-
if (watching && 'need' in to)
|
|
1613
|
-
throw new Fail(`${method} is a faculty's, and only a being is watched`);
|
|
1614
|
-
if (watching && !spec.hints.readOnly)
|
|
1615
|
-
throw new Fail(`${method} is not readOnly, so it is never watched`);
|
|
1616
|
-
return this.#call(to, method, args, spec.hints.idempotent, options?.reply, spec, watching ? { held: options.after } : undefined);
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
|
-
return Object.freeze(face);
|
|
1620
|
-
}
|
|
1621
|
-
// What a standing shows her now: a far one's describe, kept until its mark moves, or a near one's.
|
|
1622
|
-
async #describeOf(standing) {
|
|
1623
|
-
const house = this.#house;
|
|
1624
|
-
const held = standingOf(this.#being, this.#row, standing);
|
|
1625
|
-
if (held === undefined)
|
|
1626
|
-
throw new Fail(`she holds no standing ${standing}`);
|
|
1627
|
-
this.#untakenYet(standing);
|
|
1628
|
-
const left = this.#deadline - house.clock.now();
|
|
1629
|
-
const read = left <= 0
|
|
1630
|
-
? null
|
|
1631
|
-
: await house.within(left, held.local === undefined
|
|
1632
|
-
? house.far(this.#being, standing, undefined, {}, undefined, this.#deadline)
|
|
1633
|
-
: house.ask({ being: held.local.being, occupant: held.local.occupant, until: this.#deadline }).then((outcome) => (outcome !== null && 'describe' in outcome ? { describe: outcome.describe } : null)));
|
|
1634
|
-
if (read === null || !('describe' in read))
|
|
1635
|
-
throw new Fail(`${standing} answered nothing`);
|
|
1636
|
-
const describe = read.describe;
|
|
1637
|
-
// Data that holds no list of asks is data, and no describe.
|
|
1638
|
-
if (!Array.isArray(describe.asks) || typeof describe.state !== 'string')
|
|
1639
|
-
throw new Fail(`${standing} answered no describe`);
|
|
1640
|
-
return { state: describe.state, asks: describe.asks };
|
|
1641
|
-
}
|
|
1642
|
-
// A standing asked with no need: her describe first, then any ask it shows, awaited or an effect as it says.
|
|
1643
|
-
#undeclared(standing) {
|
|
1644
|
-
const shown = new Map();
|
|
1645
|
-
return Object.freeze({
|
|
1646
|
-
describe: async () => {
|
|
1647
|
-
const described = await this.#describeOf(standing);
|
|
1648
|
-
for (const entry of described.asks)
|
|
1649
|
-
shown.set(entry.method, entry);
|
|
1650
|
-
return described;
|
|
1651
|
-
},
|
|
1652
|
-
ask: (method, args, options) => {
|
|
1653
|
-
const entry = shown.get(method);
|
|
1654
|
-
if (entry === undefined)
|
|
1655
|
-
throw new Fail(`${method} is not in the describe she read of ${standing}`);
|
|
1656
|
-
const hints = { readOnly: entry.hints?.readOnly === true, idempotent: entry.hints?.readOnly === true || entry.hints?.idempotent === true };
|
|
1657
|
-
const watching = options !== undefined && 'after' in options;
|
|
1658
|
-
if (watching && !hints.readOnly)
|
|
1659
|
-
throw new Fail(`${method} is not readOnly, so it is never watched`);
|
|
1660
|
-
const spec = { args: entry.args, result: entry.result, wait: typeof entry.wait === 'number' ? entry.wait : WAIT };
|
|
1661
|
-
return this.#call({ standing }, method, args, hints.idempotent, options?.reply, spec, watching ? { held: options.after } : undefined);
|
|
1662
|
-
},
|
|
1663
|
-
});
|
|
1664
|
-
}
|
|
1665
|
-
// A far house's public being, asked as a stranger: every ask awaited, on the zero head, signed with this house's key for that ward.
|
|
1666
|
-
#stranger(card, need) {
|
|
1667
|
-
const blueprint = blueprintOf(need);
|
|
1668
|
-
if (blueprint === undefined)
|
|
1669
|
-
throw new Fail('stranger takes a need');
|
|
1670
|
-
const reached = cardOf(card);
|
|
1671
|
-
const face = {};
|
|
1672
|
-
for (const [method, spec] of Object.entries(blueprint.methods)) {
|
|
1673
|
-
face[method] = async (args) => {
|
|
1674
|
-
// A stranger's every ask is idempotent, so she awaits each one.
|
|
1675
|
-
if (!spec.hints.idempotent)
|
|
1676
|
-
throw new Fail(`${method} is no idempotent ask, and a stranger asks no other`);
|
|
1677
|
-
return this.#asStranger(reached, method, args, spec);
|
|
1678
|
-
};
|
|
1679
|
-
}
|
|
1680
|
-
return Object.freeze(face);
|
|
1681
|
-
}
|
|
1682
|
-
// A far public being asked as a stranger with no need declared: her describe first, then any ask it shows, each awaited.
|
|
1683
|
-
#strangerUndeclared(card) {
|
|
1684
|
-
const reached = cardOf(card);
|
|
1685
|
-
const shown = new Map();
|
|
1686
|
-
return Object.freeze({
|
|
1687
|
-
describe: async () => {
|
|
1688
|
-
const described = (await this.#zero(reached, undefined, {}, WAIT, 'the public being'));
|
|
1689
|
-
// Data that holds no list of asks is data, and no describe.
|
|
1690
|
-
if (described === null || typeof described !== 'object' || !Array.isArray(described.asks) || typeof described.state !== 'string')
|
|
1691
|
-
throw new Fail('the public being answered no describe');
|
|
1692
|
-
for (const entry of described.asks)
|
|
1693
|
-
shown.set(entry.method, entry);
|
|
1694
|
-
return { state: described.state, asks: described.asks };
|
|
1695
|
-
},
|
|
1696
|
-
ask: async (method, args) => {
|
|
1697
|
-
const entry = shown.get(method);
|
|
1698
|
-
if (entry === undefined)
|
|
1699
|
-
throw new Fail(`${method} is not in the describe she read of the public being`);
|
|
1700
|
-
return this.#asStranger(reached, method, args, { args: entry.args, result: entry.result, wait: typeof entry.wait === 'number' ? entry.wait : WAIT });
|
|
1701
|
-
},
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
// One ask of a far public being as a stranger: her args out, and her result in.
|
|
1705
|
-
async #asStranger(card, method, args, spec) {
|
|
1706
|
-
const house = this.#house;
|
|
1707
|
-
let sent;
|
|
1708
|
-
try {
|
|
1709
|
-
sent = (await outward(house.tools, house.marks, spec.args, args ?? {}, this.#being, (handle) => this.mint(handle, 'house')));
|
|
1710
|
-
}
|
|
1711
|
-
catch (error) {
|
|
1712
|
-
throw error instanceof Crossing ? new Fail(error.message) : error;
|
|
1713
|
-
}
|
|
1714
|
-
const answer = (await this.#zero(card, method, sent, spec.wait ?? WAIT, method));
|
|
1715
|
-
if (answer !== null && typeof answer === 'object' && typeof answer.error?.message === 'string')
|
|
1716
|
-
throw new Fail(answer.error.message);
|
|
1717
|
-
if (answer === null || typeof answer !== 'object' || !('result' in answer))
|
|
1718
|
-
throw new Fail(`${method} answered no answer`);
|
|
1719
|
-
return inward(house.tools, house.marks, spec.result, answer.result, (hex) => this.accept(hex));
|
|
1720
|
-
}
|
|
1721
|
-
// One box on the zero head, signed with this house's key for that ward, and what it answered: her describe where no method is named.
|
|
1722
|
-
async #zero({ ward, at }, method, sent, wait, what) {
|
|
1723
|
-
const house = this.#house;
|
|
1724
|
-
const { box, lid } = await house.room.stranger(ward, await house.strangerSecret(ward), { ...(method === undefined ? {} : { method }), args: house.tools.canonical(sent) });
|
|
1725
|
-
const left = Math.min(wait, this.#deadline - house.clock.now());
|
|
1726
|
-
const carried = left <= 0 ? null : await house.within(left, house.carry.send({ ward, at, box, wait: left }));
|
|
1727
|
-
const reply = carried === null ? null : carried.reply;
|
|
1728
|
-
const read = reply === null ? null : await house.room.strangerRead(ward, lid, reply);
|
|
1729
|
-
// Silence has no cause she may read, near or far.
|
|
1730
|
-
if (read === null || !('object' in read))
|
|
1731
|
-
throw new Fail(`${what} answered nothing`);
|
|
1732
|
-
return read.object;
|
|
1733
|
-
}
|
|
1734
|
-
// A standing of this house matched to a need through her describe.
|
|
1735
|
-
async #covered(standing, blueprint) {
|
|
1736
|
-
const house = this.#house;
|
|
1737
|
-
const held = standingOf(this.#being, this.#row, standing);
|
|
1738
|
-
if (held === undefined)
|
|
1739
|
-
throw new Fail(`she holds no standing ${standing}`);
|
|
1740
|
-
this.#untakenYet(standing);
|
|
1741
|
-
if (held.local === undefined) {
|
|
1742
|
-
// A far standing's describe, asked with the empty ask.
|
|
1743
|
-
// The describe is asked within what is left of her ask, as any call of hers is.
|
|
1744
|
-
const left = this.#deadline - house.clock.now();
|
|
1745
|
-
const described = left <= 0 ? null : await house.within(left, house.far(this.#being, standing, undefined, {}, undefined, this.#deadline));
|
|
1746
|
-
if (described === null || !('describe' in described))
|
|
1747
|
-
throw new Silent(`${standing} answered nothing`);
|
|
1748
|
-
const asks = described.describe.asks;
|
|
1749
|
-
// Data that holds no list of asks is data, and no describe.
|
|
1750
|
-
if (!Array.isArray(asks))
|
|
1751
|
-
throw new Fail(`${standing} answered no describe`);
|
|
1752
|
-
const methods = {};
|
|
1753
|
-
for (const entry of asks) {
|
|
1754
|
-
methods[entry.method] = {
|
|
1755
|
-
wait: typeof entry.wait === 'number' ? entry.wait : WAIT,
|
|
1756
|
-
args: (entry.args ?? {}),
|
|
1757
|
-
...(entry.result === undefined ? {} : { result: entry.result }),
|
|
1758
|
-
hints: { readOnly: entry.hints?.readOnly === true, idempotent: entry.hints?.idempotent === true, destructive: entry.hints?.destructive === true },
|
|
1759
|
-
};
|
|
1760
|
-
}
|
|
1761
|
-
const cover = covers({ name: blueprint.name, methods }, blueprint);
|
|
1762
|
-
if (!cover.covered)
|
|
1763
|
-
throw new Fail(`${standing} does not cover ${blueprint.name}: ${cover.why}`);
|
|
1764
|
-
return;
|
|
1765
|
-
}
|
|
1766
|
-
const target = await house.beingOf(held.local.being);
|
|
1767
|
-
if (target === null)
|
|
1768
|
-
throw new Fail(`${standing} is absent`);
|
|
1769
|
-
const row = await house.rows().get(await house.place(held.local.being));
|
|
1770
|
-
const asker = row === null ? undefined : house.asker(held.local.being, row, held.local.occupant);
|
|
1771
|
-
if (row === null || asker === undefined)
|
|
1772
|
-
throw new Fail(`${standing} answers her nothing`);
|
|
1773
|
-
const table = target.resolved.table;
|
|
1774
|
-
const position = positionOf(held.local.being);
|
|
1775
|
-
const me = { id: held.local.being, position, cells: { ...table.cells, ...row.cells } };
|
|
1776
|
-
const roles = house.rolesFor(table, asker, me);
|
|
1777
|
-
const methods = Object.fromEntries(Object.values(table.asks).filter((entry) => house.shownTo(entry, asker, roles, position)).map((entry) => [entry.method, entry]));
|
|
1778
|
-
const cover = covers({ name: blueprint.name, methods }, blueprint);
|
|
1779
|
-
if (!cover.covered)
|
|
1780
|
-
throw new Fail(`${standing} does not cover ${blueprint.name}: ${cover.why}`);
|
|
1781
|
-
}
|
|
1782
|
-
/** Her reach, as property descriptors on the being. */
|
|
1783
|
-
reach(asker, position, table) {
|
|
1784
|
-
const house = this.#house;
|
|
1785
|
-
const being = this.#being;
|
|
1786
|
-
const value = (v) => ({ value: v, enumerable: false });
|
|
1787
|
-
const ownAsk = (ask) => {
|
|
1788
|
-
if (table.asks[ask] === undefined)
|
|
1789
|
-
throw new Fail(`she has no ask ${ask}`);
|
|
1790
|
-
};
|
|
1791
|
-
const relation = (id, held) => ({ id, notes: held.notes, steward: held.steward });
|
|
1792
|
-
const descriptors = {
|
|
1793
|
-
id: value(being),
|
|
1794
|
-
position: value(position),
|
|
1795
|
-
asker: value(Object.freeze({ ...asker, handle: undefined })),
|
|
1796
|
-
cells: {
|
|
1797
|
-
get: () => this.cells,
|
|
1798
|
-
set: (cells) => {
|
|
1799
|
-
this.cells = cells;
|
|
1800
|
-
},
|
|
1801
|
-
enumerable: false,
|
|
1802
|
-
},
|
|
1803
|
-
house: value(Object.freeze({
|
|
1804
|
-
now: () => house.clock.now(),
|
|
1805
|
-
random: ({ length }) => house.crypto.random(length),
|
|
1806
|
-
alarm: ({ at, ask, args, key }) => {
|
|
1807
|
-
ownAsk(ask);
|
|
1808
|
-
const alarm = { at, ask, args: args ?? {} };
|
|
1809
|
-
this.#op((_draft, row) => {
|
|
1810
|
-
row.alarms[key] = alarm;
|
|
1811
|
-
}, (row) => {
|
|
1812
|
-
row.alarms[key] = alarm;
|
|
1813
|
-
});
|
|
1814
|
-
},
|
|
1815
|
-
cancelAlarm: ({ key }) => this.#op((_draft, row) => {
|
|
1816
|
-
delete row.alarms[key];
|
|
1817
|
-
}, (row) => {
|
|
1818
|
-
delete row.alarms[key];
|
|
1819
|
-
}),
|
|
1820
|
-
})),
|
|
1821
|
-
standings: value(Object.freeze({
|
|
1822
|
-
list: () => [
|
|
1823
|
-
...(position === 'steward' ? [] : [{ ...relation(STEWARD, { notes: {}, steward: {} }), vouched: [] }]),
|
|
1824
|
-
...Object.entries(this.#row.standings).map(([id, held]) => ({ ...relation(id, held), vouched: [...(held.vouched ?? [])] })),
|
|
1825
|
-
],
|
|
1826
|
-
note: (id, notes) => {
|
|
1827
|
-
if (this.#row.standings[id] === undefined)
|
|
1828
|
-
throw new Fail(`she holds no standing ${id}`);
|
|
1829
|
-
this.#op((_draft, row) => {
|
|
1830
|
-
if (row.standings[id])
|
|
1831
|
-
row.standings[id].notes = notes;
|
|
1832
|
-
}, (row) => {
|
|
1833
|
-
row.standings[id].notes = notes;
|
|
1834
|
-
});
|
|
1835
|
-
},
|
|
1836
|
-
drop: (id) => {
|
|
1837
|
-
if (id === STEWARD)
|
|
1838
|
-
throw new Fail('she cannot drop her steward');
|
|
1839
|
-
this.#op((_draft, row) => {
|
|
1840
|
-
delete row.standings[id];
|
|
1841
|
-
}, (row) => {
|
|
1842
|
-
delete row.standings[id];
|
|
1843
|
-
});
|
|
1844
|
-
},
|
|
1845
|
-
})),
|
|
1846
|
-
occupants: value(Object.freeze({
|
|
1847
|
-
list: () => Object.entries(this.#row.occupants).map(([id, held]) => relation(id, held)),
|
|
1848
|
-
note: (id, notes) => {
|
|
1849
|
-
if (this.#row.occupants[id] === undefined)
|
|
1850
|
-
throw new Fail(`she holds no occupant ${id}`);
|
|
1851
|
-
this.#op((_draft, row) => {
|
|
1852
|
-
if (row.occupants[id])
|
|
1853
|
-
row.occupants[id].notes = notes;
|
|
1854
|
-
}, (row) => {
|
|
1855
|
-
row.occupants[id].notes = notes;
|
|
1856
|
-
});
|
|
1857
|
-
},
|
|
1858
|
-
dismiss: (id) => {
|
|
1859
|
-
if (RESERVED_OCCUPANTS.has(id))
|
|
1860
|
-
throw new Fail(`she cannot dismiss ${id}`);
|
|
1861
|
-
this.#op((draft, row) => house.dismissIn(draft, row, id), (row) => {
|
|
1862
|
-
delete row.occupants[id];
|
|
1863
|
-
});
|
|
1864
|
-
},
|
|
1865
|
-
})),
|
|
1866
|
-
held: value((id, need) => {
|
|
1867
|
-
if (need === undefined)
|
|
1868
|
-
return this.#undeclared(id);
|
|
1869
|
-
const blueprint = blueprintOf(need);
|
|
1870
|
-
if (blueprint === undefined)
|
|
1871
|
-
throw new Fail('held takes a need');
|
|
1872
|
-
const face = this.#face({ standing: id }, blueprint);
|
|
1873
|
-
// An effect on a far standing is checked where it lands, so it never waits on the far side's describe.
|
|
1874
|
-
const standing = standingOf(this.#being, this.#row, id);
|
|
1875
|
-
const far = standing !== undefined && standing.local === undefined;
|
|
1876
|
-
let checked;
|
|
1877
|
-
const check = () => {
|
|
1878
|
-
checked ??= this.#covered(id, blueprint);
|
|
1879
|
-
checked.catch(() => undefined);
|
|
1880
|
-
return checked;
|
|
1881
|
-
};
|
|
1882
|
-
return Object.freeze(Object.fromEntries(Object.entries(face).map(([method, call]) => [
|
|
1883
|
-
method,
|
|
1884
|
-
(args, options) => {
|
|
1885
|
-
const spec = blueprint.methods[method];
|
|
1886
|
-
// Every silence reads as the call she made, whether it met the describe or the ask.
|
|
1887
|
-
const covered = () => check().catch((error) => {
|
|
1888
|
-
throw error instanceof Silent ? new Fail(`${method} answered nothing`) : error;
|
|
1889
|
-
});
|
|
1890
|
-
// A watch given a reply leaves as an effect does, so it too never waits on the far side.
|
|
1891
|
-
const effect = !spec.hints.idempotent || (options !== undefined && 'after' in options && options.reply !== undefined);
|
|
1892
|
-
if (!effect)
|
|
1893
|
-
return covered().then(() => call(args, options));
|
|
1894
|
-
if (far)
|
|
1895
|
-
return call(args, options);
|
|
1896
|
-
this.#pending.push(covered());
|
|
1897
|
-
return call(args, options);
|
|
1898
|
-
},
|
|
1899
|
-
])));
|
|
1900
|
-
}),
|
|
1901
|
-
stranger: value((card, need) => (need === undefined ? this.#strangerUndeclared(card) : this.#stranger(card, need))),
|
|
1902
|
-
handle: value((ask, options = {}) => {
|
|
1903
|
-
if (position === 'public')
|
|
1904
|
-
throw new Fail('a public being holds no handle to her own asks');
|
|
1905
|
-
const expires = expiresOf(options.expires);
|
|
1906
|
-
ownAsk(ask);
|
|
1907
|
-
if (!(table.asks[ask].for ?? []).includes('handle'))
|
|
1908
|
-
throw new Fail(`her ask ${ask} is not for handle`);
|
|
1909
|
-
const id = this.#fresh('handle:');
|
|
1910
|
-
const held = { notes: options.notes ?? {}, steward: {}, ask, ...(options.bind === undefined ? {} : { bind: options.bind }), ...(options.once ? { once: true } : {}) };
|
|
1911
|
-
this.#op((_draft, row) => {
|
|
1912
|
-
row.occupants[id] = held;
|
|
1913
|
-
}, (row) => {
|
|
1914
|
-
row.occupants[id] = held;
|
|
1915
|
-
});
|
|
1916
|
-
return house.marks.handle(being, id, being, expires);
|
|
1917
|
-
}),
|
|
1918
|
-
invite: value((id, { notes = {}, expires: given } = {}) => {
|
|
1919
|
-
if (position === 'public')
|
|
1920
|
-
throw new Fail('a public being cannot invite');
|
|
1921
|
-
const expires = expiresOf(given);
|
|
1922
|
-
if (RESERVED_OCCUPANTS.has(id) || id.startsWith('handle:') || id.startsWith('being:'))
|
|
1923
|
-
throw new Fail(`the occupant id ${id} is the house's`);
|
|
1924
|
-
if (this.#row.occupants[id] !== undefined)
|
|
1925
|
-
throw new Fail(`she holds ${id} already`);
|
|
1926
|
-
const held = { notes, steward: {} };
|
|
1927
|
-
this.#op((_draft, row) => {
|
|
1928
|
-
row.occupants[id] = held;
|
|
1929
|
-
}, (row) => {
|
|
1930
|
-
row.occupants[id] = held;
|
|
1931
|
-
});
|
|
1932
|
-
return house.marks.handle(being, id, being, expires);
|
|
1933
|
-
}),
|
|
1934
|
-
fail: value((message) => {
|
|
1935
|
-
throw new Fail(message);
|
|
1936
|
-
}),
|
|
1937
|
-
};
|
|
1938
|
-
for (const member of Object.keys(table.needs)) {
|
|
1939
|
-
descriptors[member] = value(this.#face({ need: member }, table.needs[member]));
|
|
1940
|
-
}
|
|
1941
|
-
if (position !== 'steward')
|
|
1942
|
-
descriptors.steward = value(this.#untyped({ standing: STEWARD }, STEWARD));
|
|
1943
|
-
else
|
|
1944
|
-
descriptors.powers = value(this.#powers());
|
|
1945
|
-
return descriptors;
|
|
1946
|
-
}
|
|
1947
|
-
// Her steward, asked with the flag of the ask she calls.
|
|
1948
|
-
#untyped(to, target) {
|
|
1949
|
-
return new Proxy({}, {
|
|
1950
|
-
get: (_object, method) => {
|
|
1951
|
-
if (typeof method !== 'string')
|
|
1952
|
-
return undefined;
|
|
1953
|
-
return (args, options) => this.#byFlag(to, target, method, args, options?.reply);
|
|
1954
|
-
},
|
|
1955
|
-
});
|
|
1956
|
-
}
|
|
1957
|
-
// A call whose flag the callee's own entry decides.
|
|
1958
|
-
#byFlag(to, being, method, args, reply) {
|
|
1959
|
-
const decided = (async () => {
|
|
1960
|
-
const found = await this.#house.beingOf(being);
|
|
1961
|
-
const entry = found?.resolved.table.asks[method];
|
|
1962
|
-
if (entry === undefined)
|
|
1963
|
-
throw new Fail(`${being} has no ask ${method}`);
|
|
1964
|
-
return entry;
|
|
1965
|
-
})();
|
|
1966
|
-
const awaited = (entry) => idempotentIn(entry, positionOf(being));
|
|
1967
|
-
const result = decided.then((entry) => this.#call(to, method, args, awaited(entry), reply, entry));
|
|
1968
|
-
// An effect that cannot be queued fails her ask; an awaited call fails only where she awaits it.
|
|
1969
|
-
this.#pending.push(decided.then((entry) => (awaited(entry) ? undefined : result.then(() => undefined))));
|
|
1970
|
-
return result;
|
|
1971
|
-
}
|
|
1972
|
-
// A being of this house described to her steward, within what is left of the steward's ask.
|
|
1973
|
-
async #shownToSteward(id) {
|
|
1974
|
-
const house = this.#house;
|
|
1975
|
-
const left = this.#deadline - house.clock.now();
|
|
1976
|
-
const outcome = left <= 0 ? null : await house.within(left, house.ask({ being: id, occupant: STEWARD, until: this.#deadline }));
|
|
1977
|
-
if (outcome === null || !('describe' in outcome))
|
|
1978
|
-
throw new Fail(`${id} answered nothing`);
|
|
1979
|
-
const describe = outcome.describe;
|
|
1980
|
-
return { state: describe.state, asks: describe.asks };
|
|
1981
|
-
}
|
|
1982
|
-
#powers() {
|
|
1983
|
-
const house = this.#house;
|
|
1984
|
-
const guard = (id) => {
|
|
1985
|
-
if (id === STEWARD || id === PUBLIC)
|
|
1986
|
-
throw new Fail(`${id} is the house's`);
|
|
1987
|
-
};
|
|
1988
|
-
return Object.freeze({
|
|
1989
|
-
bear: ({ kind, id, args }) => {
|
|
1990
|
-
if (typeof id !== 'string' || !BEING_ID.test(id) || RESERVED_BEINGS.has(id))
|
|
1991
|
-
throw new Fail(`${id} is no being id`);
|
|
1992
|
-
const resolving = house.resolveKind(kind);
|
|
1993
|
-
// The op stands in her order now, so an invite after it finds the new row.
|
|
1994
|
-
this.#op(async (draft) => {
|
|
1995
|
-
const resolved = await resolving;
|
|
1996
|
-
if ('absent' in resolved)
|
|
1997
|
-
return;
|
|
1998
|
-
const ward = await draft.get(house.wardPlace);
|
|
1999
|
-
if (ward === null || ward.beings[id] !== undefined)
|
|
2000
|
-
return;
|
|
2001
|
-
ward.beings[id] = { kind };
|
|
2002
|
-
const born = resolved.table.asks.born !== undefined;
|
|
2003
|
-
draft.set(await house.place(id), { ...emptyRow(kind, copy(resolved.table.cells)), ...(born ? { born: args ?? {} } : {}) });
|
|
2004
|
-
});
|
|
2005
|
-
this.#pending.push(resolving.then((resolved) => {
|
|
2006
|
-
if ('absent' in resolved)
|
|
2007
|
-
throw new Fail(resolved.absent);
|
|
2008
|
-
if (resolved.table.asks.born !== undefined)
|
|
2009
|
-
this.#after.push(() => house.bornOf(id));
|
|
2010
|
-
}));
|
|
2011
|
-
},
|
|
2012
|
-
invite: ({ id, occupant, notes = {}, expires: given }) => {
|
|
2013
|
-
const expires = expiresOf(given);
|
|
2014
|
-
if (RESERVED_OCCUPANTS.has(occupant) || occupant.startsWith('handle:') || occupant.startsWith('being:'))
|
|
2015
|
-
throw new Fail(`the occupant id ${occupant} is the house's`);
|
|
2016
|
-
if (id === STEWARD)
|
|
2017
|
-
throw new Fail('the steward invites her own occupants with invite');
|
|
2018
|
-
this.#invited.add(`${id}\n${occupant}`);
|
|
2019
|
-
this.#op(async (draft) => {
|
|
2020
|
-
const row = await draft.get(await house.place(id));
|
|
2021
|
-
if (row === null)
|
|
2022
|
-
throw new Fail(`${id} is not here`);
|
|
2023
|
-
if (row.occupants[occupant] !== undefined)
|
|
2024
|
-
throw new Fail(`${id} holds ${occupant} already`);
|
|
2025
|
-
row.occupants[occupant] = { notes: {}, steward: notes };
|
|
2026
|
-
});
|
|
2027
|
-
return house.marks.handle(id, occupant, this.#being, expires);
|
|
2028
|
-
},
|
|
2029
|
-
remove: ({ id }) => {
|
|
2030
|
-
guard(id);
|
|
2031
|
-
this.#op(async (draft) => {
|
|
2032
|
-
const ward = await draft.get(house.wardPlace);
|
|
2033
|
-
if (ward === null || ward.beings[id] === undefined)
|
|
2034
|
-
return;
|
|
2035
|
-
delete ward.beings[id];
|
|
2036
|
-
// Every heir and token of hers goes with her row.
|
|
2037
|
-
const place = await house.place(id);
|
|
2038
|
-
for (const held of Object.values((await draft.get(place))?.occupants ?? {}))
|
|
2039
|
-
await house.forget(draft, held);
|
|
2040
|
-
draft.set(place, null);
|
|
2041
|
-
});
|
|
2042
|
-
},
|
|
2043
|
-
list: async () => {
|
|
2044
|
-
const ward = await house.rows().get(house.wardPlace);
|
|
2045
|
-
const out = [];
|
|
2046
|
-
for (const [id, { kind }] of Object.entries(ward?.beings ?? {})) {
|
|
2047
|
-
const row = await house.rows().get(await house.place(id));
|
|
2048
|
-
out.push({ id, kind, absent: 'absent' in (await house.resolveKind(kind)), dead: row?.dead ?? [] });
|
|
2049
|
-
}
|
|
2050
|
-
return out;
|
|
2051
|
-
},
|
|
2052
|
-
// With no method, the empty ask: what she shows her steward now.
|
|
2053
|
-
ask: ({ id, method, args }, options) => (method === undefined ? this.#shownToSteward(id) : this.#byFlag({ being: id }, id, method, args, options?.reply)),
|
|
2054
|
-
introduce: ({ from, to, notes = {} }) => {
|
|
2055
|
-
if (from === to)
|
|
2056
|
-
throw new Fail('no being stands on herself');
|
|
2057
|
-
this.#op(async (draft, row) => {
|
|
2058
|
-
const fromRow = from === STEWARD ? row : await draft.get(await house.place(from));
|
|
2059
|
-
const toRow = to === STEWARD ? row : await draft.get(await house.place(to));
|
|
2060
|
-
if (fromRow === null || toRow === null)
|
|
2061
|
-
throw new Error('introduce names a being that is not here');
|
|
2062
|
-
// Introduced again, each side keeps the notes she wrote: the steward writes only her own.
|
|
2063
|
-
fromRow.standings[to] = { notes: fromRow.standings[to]?.notes ?? {}, steward: notes, local: { being: to, occupant: `being:${from}` } };
|
|
2064
|
-
toRow.occupants[`being:${from}`] = { ...toRow.occupants[`being:${from}`], notes: toRow.occupants[`being:${from}`]?.notes ?? {}, steward: notes };
|
|
2065
|
-
});
|
|
2066
|
-
},
|
|
2067
|
-
});
|
|
2068
|
-
}
|
|
2069
|
-
}
|