@quo-systems/quo 0.2.13 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/SPEC.md +354 -536
- package/dist/being/being.d.ts +1 -0
- package/dist/being/being.js +8 -3
- package/dist/being/index.d.ts +1 -1
- package/dist/being/index.js +1 -1
- package/dist/being/types.d.ts +2 -0
- package/dist/being/types.js +17 -0
- package/dist/conformance/beings.d.ts +41 -0
- package/dist/conformance/beings.js +28 -2
- package/dist/conformance/index.d.ts +10 -1
- package/dist/conformance/index.js +162 -6
- package/dist/harbor/core.d.ts +4 -2
- package/dist/harbor/core.js +25 -4
- package/dist/harbor/index.d.ts +1 -1
- package/dist/harbor/memory.d.ts +3 -3
- package/dist/harbor/memory.js +6 -3
- package/dist/harbor/reach.js +1 -1
- package/dist/ward/cells.d.ts +2 -0
- package/dist/ward/cells.js +60 -11
- package/dist/ward/door.d.ts +1 -0
- package/dist/ward/door.js +29 -10
- package/dist/ward/ground.d.ts +3 -1
- package/dist/ward/ground.js +1 -1
- package/dist/ward/heirs.js +8 -1
- package/dist/ward/index.d.ts +2 -2
- package/dist/ward/index.js +3 -3
- package/dist/ward/owner.js +49 -8
- package/dist/ward/seal.d.ts +1 -0
- package/dist/ward/seal.js +10 -2
- package/dist/ward/stance.d.ts +1 -0
- package/dist/ward/stance.js +62 -4
- package/dist/ward/ward.js +5 -0
- package/package.json +5 -3
- package/quo-kit.md +523 -0
- package/src/being/being.ts +8 -3
- package/src/being/index.ts +1 -1
- package/src/being/types.ts +34 -0
- package/src/conformance/beings.ts +25 -2
- package/src/conformance/estate.ts +9 -9
- package/src/conformance/index.ts +204 -7
- package/src/conformance/reach.ts +1 -1
- package/src/harbor/core.ts +26 -5
- package/src/harbor/index.ts +1 -1
- package/src/harbor/memory.ts +7 -4
- package/src/harbor/reach.ts +1 -1
- package/src/ward/cells.ts +59 -10
- package/src/ward/door.ts +27 -9
- package/src/ward/ground.ts +39 -11
- package/src/ward/heirs.ts +7 -1
- package/src/ward/index.ts +4 -4
- package/src/ward/owner.ts +45 -10
- package/src/ward/seal.ts +11 -2
- package/src/ward/stance.ts +60 -4
- package/src/ward/ward.ts +6 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// shop, a customer on the base class, a maker, a member, and one raw being
|
|
4
4
|
// with no base at all.
|
|
5
5
|
import { Being } from '../being/being.ts';
|
|
6
|
-
import { answered, isUnreached } from '../being/silence.ts';
|
|
6
|
+
import { answered, isSilence, isUnreached, isWord, wordOf } from '../being/silence.ts';
|
|
7
7
|
import { invitationArgs } from '../being/types.ts';
|
|
8
8
|
import type { Ask, Asker, Blueprint, Invitation, Json, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
|
|
9
9
|
|
|
@@ -138,6 +138,7 @@ export class Maker extends Being {
|
|
|
138
138
|
static override asks = {
|
|
139
139
|
open: { input: { type: 'object', properties: { class: { type: 'string' }, key: { type: 'string' }, id: { type: 'string' } }, required: ['class', 'key'] } },
|
|
140
140
|
say: { input: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
|
|
141
|
+
borrow: { input: { type: 'object', properties: { name: { type: 'string' }, id: { type: 'string' } }, required: ['name', 'id'] } },
|
|
141
142
|
};
|
|
142
143
|
async open({ class: className, key, id }: JsonObject) {
|
|
143
144
|
const made = await this.boot(className as string, key as string, id as string | undefined);
|
|
@@ -148,13 +149,35 @@ export class Maker extends Being {
|
|
|
148
149
|
const standing = this.standings[id as string];
|
|
149
150
|
return { said: standing === undefined ? null : ((await standing.ask('ping')) as Json) };
|
|
150
151
|
}
|
|
152
|
+
// Making's other half: a standing at something the box already has, by the
|
|
153
|
+
// name the box knows it under. She is handed the id and never the value.
|
|
154
|
+
async borrow({ name, id }: JsonObject) {
|
|
155
|
+
return { lent: await this.lend(name as string, id as string) };
|
|
156
|
+
}
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
export class Member extends Being {
|
|
154
160
|
static override cells = { heard: [] as JsonObject[] };
|
|
155
|
-
static override asks = {
|
|
161
|
+
static override asks = {
|
|
162
|
+
ping: { input: { type: 'object' } },
|
|
163
|
+
relay: { input: { type: 'object', properties: { id: { type: 'string' }, method: { type: 'string' }, time: { type: 'number' } }, required: ['id'] } },
|
|
164
|
+
};
|
|
156
165
|
ping(_args: JsonObject, asker: Asker) {
|
|
157
166
|
(this.cells.heard as JsonObject[]).push({ from: asker.id ?? null });
|
|
158
167
|
return { pong: asker.id ?? null };
|
|
159
168
|
}
|
|
169
|
+
// She asks on one of her standings from inside her own answer, and says
|
|
170
|
+
// what came back as a word rather than as the word itself, since a word is
|
|
171
|
+
// her ward's value and never a thing that crosses. `time` is what she is
|
|
172
|
+
// willing to wait, which is the whole of what the third argument is for.
|
|
173
|
+
// Two members holding each other and relaying make a cycle, and a cycle is
|
|
174
|
+
// how a suite that owns no clock can see a wait end.
|
|
175
|
+
async relay({ id, method, args, time }: JsonObject) {
|
|
176
|
+
const standing = this.standings[id as string];
|
|
177
|
+
if (standing === undefined) return { said: 'no standing', out: null };
|
|
178
|
+
const out = await standing.ask(method as string | undefined, (args ?? {}) as JsonObject, time === undefined ? undefined : { time: time as number });
|
|
179
|
+
if (isSilence(out)) return { said: 'silence', out: null };
|
|
180
|
+
if (isWord(out)) return { said: wordOf(out), out: null };
|
|
181
|
+
return { said: null, out: out as Json };
|
|
182
|
+
}
|
|
160
183
|
}
|
|
@@ -76,31 +76,31 @@ function ledger(c: Census, before: Census | undefined, where: string, g?: Graph)
|
|
|
76
76
|
// is caught late, if the sweep happens to ask the affected arc, or never.
|
|
77
77
|
if (g) {
|
|
78
78
|
for (const s of g.seats) {
|
|
79
|
-
const heir = c.wards[c.binds[s.host]
|
|
79
|
+
const heir = c.wards[c.binds[s.host].ward].heirs[s.inv.heir!];
|
|
80
80
|
if (!s.live) {
|
|
81
81
|
assert.ok(!heir, at(`${s.host} kicked ${s.id} and the heir is still held`));
|
|
82
|
-
assert.ok(!(s.id in c.binds[s.host]
|
|
82
|
+
assert.ok(!(s.id in c.binds[s.host].occupants), at(`${s.host} kicked ${s.id} and still holds the occupant`));
|
|
83
83
|
continue;
|
|
84
84
|
}
|
|
85
85
|
assert.ok(heir, at(`${s.host}'s live seat ${s.id} has no heir`));
|
|
86
|
-
assert.equal(heir
|
|
86
|
+
assert.equal(heir.id, s.id, at(`the heir for ${s.host}/${s.id} is filed under ${heir.id}`));
|
|
87
87
|
// A seat nobody has knocked is a heir that has never spoken: fresh, and
|
|
88
88
|
// at zero. One that answered has spoken, so it is neither.
|
|
89
|
-
assert.equal(heir
|
|
90
|
-
assert.equal(heir
|
|
89
|
+
assert.equal(heir.fresh, !s.answered, at(`${s.host}/${s.id} is ${s.answered ? 'answered' : 'unknocked'} and its heir says fresh=${heir.fresh}`));
|
|
90
|
+
assert.equal(heir.mark === 0, !s.answered, at(`${s.host}/${s.id} is ${s.answered ? 'answered' : 'unknocked'} and its heir is at ${heir.mark}`));
|
|
91
91
|
}
|
|
92
92
|
for (const h of g.holds) {
|
|
93
|
-
const standing = c.binds[h.owner]
|
|
93
|
+
const standing = c.binds[h.owner].standings[h.id];
|
|
94
94
|
if (!h.live) {
|
|
95
95
|
assert.ok(!standing, at(`${h.owner} dropped ${h.id} and the standing is still there`));
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
98
|
assert.ok(standing, at(`${h.owner}'s live standing ${h.id} is gone from the bind table`));
|
|
99
|
-
assert.equal(standing
|
|
99
|
+
assert.equal(standing.heir, h.seat.inv.heir, at(`${h.owner}/${h.id} names a heir that is not the seat it was born on`));
|
|
100
100
|
// The count she has spent, against the number the far door honoured.
|
|
101
101
|
// Her side is never behind: she takes a number, then it crosses.
|
|
102
|
-
const far = c.wards[c.binds[h.seat.host]
|
|
103
|
-
if (far) assert.ok(standing
|
|
102
|
+
const far = c.wards[c.binds[h.seat.host].ward].heirs[h.seat.inv.heir!];
|
|
103
|
+
if (far) assert.ok(standing.seq >= far.mark, at(`${h.owner}/${h.id} counted ${standing.seq} and the far door has honoured ${far.mark}`));
|
|
104
104
|
}
|
|
105
105
|
// Every seat and hold the model knows about, and no relation the ward
|
|
106
106
|
// holds that the model never made.
|
package/src/conformance/index.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
//
|
|
4
4
|
// A ward is handed in as a probe:
|
|
5
5
|
//
|
|
6
|
-
// make()
|
|
6
|
+
// make({ lends }) -> world. `lends` maps a name the box lends to the
|
|
7
|
+
// class of the being it lends under it; the probe
|
|
8
|
+
// stands one of each in a ward its harbor roots,
|
|
9
|
+
// and every ward it boots is grounded to lend them
|
|
7
10
|
// world.boot(key, Class, cells?, { isPublic }) -> handle
|
|
8
11
|
// world.heir(invitation) -> { current, announced, fresh } | undefined
|
|
9
12
|
// what the inviting ward holds for that heir
|
|
@@ -19,7 +22,7 @@
|
|
|
19
22
|
//
|
|
20
23
|
// Every assertion here is the truth's. Nothing here knows how a ward is built.
|
|
21
24
|
import { assert } from './assert.ts';
|
|
22
|
-
import { silence, isSilence, isUnreached, told } from '../being/silence.ts';
|
|
25
|
+
import { silence, answered, isSilence, isUnreached, told } from '../being/silence.ts';
|
|
23
26
|
import { digest } from '../being/digest.ts';
|
|
24
27
|
import type { Asker, BeingClass, Cells, Invitation, Json, JsonObject, Reply, Stance } from '../being/types.ts';
|
|
25
28
|
import { Printer, Shop, Customer, Echo, Maker, Member } from './beings.ts';
|
|
@@ -56,6 +59,25 @@ export type World = {
|
|
|
56
59
|
// this one. `key` says which ward: a being of it, and never the ward's pk,
|
|
57
60
|
// because a topology may put every being behind a ward of its own.
|
|
58
61
|
owner(key: string): (method?: string, args?: JsonObject) => Promise<unknown>;
|
|
62
|
+
// The partition of the ward a being lives in, as values, and that ward
|
|
63
|
+
// booted again from one. Together they are the obligation "a ward can be
|
|
64
|
+
// written down and stood up again from what was written", which is what a
|
|
65
|
+
// restart is and what a migration rides on, and they are what lets this
|
|
66
|
+
// suite reach the count, the span and the mark without a hand to build
|
|
67
|
+
// bytes with: rewind a caller's own numbers in her partition, stand her
|
|
68
|
+
// ward up again, and her next ask carries a number the far door has
|
|
69
|
+
// already honoured. What `restart` is given it keeps; given nothing it
|
|
70
|
+
// keeps what the ward had.
|
|
71
|
+
// They are `Standing`, below, and optional here: required in prose and of
|
|
72
|
+
// any harness that says it can, which is the shape every obligation in this
|
|
73
|
+
// suite has. A harness that cannot says `canRestart: false` at the call and
|
|
74
|
+
// skips the chapters that need it, rather than writing a member that lies.
|
|
75
|
+
} & Partial<Standing>;
|
|
76
|
+
|
|
77
|
+
// What a harness that can stand a ward up again offers.
|
|
78
|
+
export type Standing = {
|
|
79
|
+
partition(key: string): JsonObject;
|
|
80
|
+
restart(key: string, partition?: JsonObject): Promise<void>;
|
|
59
81
|
};
|
|
60
82
|
|
|
61
83
|
export { Printer, Shop, Customer, Echo, Maker, Member };
|
|
@@ -70,14 +92,27 @@ export const HEX128 = /^[0-9a-f]{128}$/; // a ward pk: the signing pk then the p
|
|
|
70
92
|
// Her ward's word, as she receives it.
|
|
71
93
|
const said = (x: unknown, w: string): boolean => told(x) === w;
|
|
72
94
|
|
|
95
|
+
// A world whose harness stands a ward up again. The two members are optional
|
|
96
|
+
// on the probe and required of a harness that says it can, so a chapter that
|
|
97
|
+
// needs them says so here rather than reaching past a maybe at every line.
|
|
98
|
+
const able = (w: World): World & Standing => w as World & Standing;
|
|
99
|
+
|
|
73
100
|
// The runner is handed in. Node passes `node:test`; a browser and an edge
|
|
74
101
|
// worker pass a shim of their own. The suite is the ward's truth, and the
|
|
75
102
|
// ward's truth must be checkable wherever a ward runs, not only here.
|
|
76
103
|
export type Runner = (name: string, opts: { skip?: string | false }, fn: () => Promise<void> | void) => void;
|
|
77
104
|
|
|
78
|
-
|
|
105
|
+
// What the probe is asked to build. `lends` is the box's half of the ground's
|
|
106
|
+
// seventh member: a name the harbor lends, and the class of the being it
|
|
107
|
+
// lends under it. A harness that stands none is handed nothing and grounds
|
|
108
|
+
// its wards to lend nothing, which is what a harbor with nothing to lend is.
|
|
109
|
+
export type Made = { lends?: Record<string, string> };
|
|
110
|
+
|
|
111
|
+
export function conform(label: string, make: (made?: Made) => Promise<World>, { canDown = true, canLend = true, canRestart = true, test }: { canDown?: boolean; canLend?: boolean; canRestart?: boolean; test: Runner }) {
|
|
79
112
|
const t = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, {}, fn);
|
|
80
113
|
const down = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, { skip: !canDown && 'nothing to cut inside one ward' }, fn);
|
|
114
|
+
const lends = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, { skip: !canLend && 'this harness stands no ward of its harbor to lend from' }, fn);
|
|
115
|
+
const kept = (name: string, fn: () => Promise<void> | void) => test(`[${label}] ${name}`, { skip: !canRestart && 'this harness cannot hand a ward a partition and stand it up again' }, fn);
|
|
81
116
|
|
|
82
117
|
async function world({ printerInvitesShop = true } = {}) {
|
|
83
118
|
const w = await make();
|
|
@@ -220,8 +255,12 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
220
255
|
// alice hosts here rather than the shop, because the shop's own `invite`
|
|
221
256
|
// takes a tier and mints the id itself. This one is the stance's, which
|
|
222
257
|
// is where the rule lives.
|
|
223
|
-
const { alice, bob } = await world();
|
|
224
|
-
|
|
258
|
+
const { w, alice, bob } = await world();
|
|
259
|
+
// `__proto__` is in the list because a partition cannot hold it. A kit
|
|
260
|
+
// that refuses it only where it writes her cells writes the ward's half
|
|
261
|
+
// first and then throws, which leaves one relation in two standings and a
|
|
262
|
+
// throw where this table promises a null.
|
|
263
|
+
for (const id of ['OWNER', 'PUBLIC', 'knock', 'take', 'remove', '__proto__']) {
|
|
225
264
|
assert.equal(await alice.being.invite(id), null, `${id} is not hers to mint`);
|
|
226
265
|
assert.equal(Object.hasOwn(alice.cells.occupants, id), false, `and nothing was written for ${id}`);
|
|
227
266
|
|
|
@@ -230,6 +269,9 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
230
269
|
assert.deepEqual(await bob.being.knock(inv, 'hi'), { heard: 'hi' }, `the knock for ${id} was answered`);
|
|
231
270
|
assert.equal(await bob.being.take(id, inv), null, `take refuses ${id} for the same reason`);
|
|
232
271
|
assert.equal(Object.hasOwn(bob.cells.standings, id), false, `and nothing was written for ${id}`);
|
|
272
|
+
// The ward's half too, and not only hers: a standing filed here with no
|
|
273
|
+
// record of it in her cells is a lane nobody can see and nobody removes.
|
|
274
|
+
assert.equal(Object.hasOwn(w.census().binds.B!.standings, id), false, `and the ward wrote no keys for ${id}`);
|
|
233
275
|
assert.equal(await bob.being.take(`as-${id}`, inv), `as-${id}`, 'and an ordinary id takes the same invitation');
|
|
234
276
|
}
|
|
235
277
|
// And the three calls are still the calls, not records that shadowed them.
|
|
@@ -245,12 +287,12 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
245
287
|
const { w, shop, alice } = await world();
|
|
246
288
|
const ask = w.owner('S');
|
|
247
289
|
const bp = (await ask()) as { asks: { name: string; description?: string; input?: JsonObject }[]; notes: { pk: string; beings: JsonObject } };
|
|
248
|
-
assert.deepEqual(bp.asks.map((a) => a.name), ['boot', 'public', 'invite', 'knock', 'remove', 'unboot'], '
|
|
290
|
+
assert.deepEqual(bp.asks.map((a) => a.name), ['boot', 'public', 'invite', 'knock', 'remove', 'unboot', 'ask'], 'seven asks, in this order');
|
|
249
291
|
for (const a of bp.asks) {
|
|
250
292
|
assert.equal(typeof a.description, 'string', `${a.name} says what it is for`);
|
|
251
293
|
assert.equal((a.input as { type: string }).type, 'object', `${a.name} takes one object`);
|
|
252
294
|
}
|
|
253
|
-
assert.match(bp.notes.pk
|
|
295
|
+
assert.match(bp.notes.pk, HEX128);
|
|
254
296
|
assert.equal(typeof bp.notes.beings, 'object');
|
|
255
297
|
assert.deepEqual(await ask('nosuch'), { error: 'unknown ask' });
|
|
256
298
|
|
|
@@ -289,6 +331,36 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
289
331
|
assert.ok(said(await alice.being.standings['via-owner']!.ask('hello'), 'removed'), 'and her occupant hears removed at the door');
|
|
290
332
|
});
|
|
291
333
|
|
|
334
|
+
// The owner's seventh ask: reaching into a being and asking her. It is the
|
|
335
|
+
// third asker of the ward-to-being edge, and the ward is the only one who
|
|
336
|
+
// fills it in: nothing outside a ward names OWNER, and no side holds a
|
|
337
|
+
// being's answer to judge it again. Strictly less than the unboot beside
|
|
338
|
+
// it, which takes her out with every relation she holds.
|
|
339
|
+
t('the owner asks a being of the ward, as owner or, with no being named, as nobody at the public being', async () => {
|
|
340
|
+
const { w } = await world();
|
|
341
|
+
const ask = w.owner('S');
|
|
342
|
+
const echo = w.owner('E');
|
|
343
|
+
await w.boot('E', Echo);
|
|
344
|
+
|
|
345
|
+
// Named: she is asked as the owner, and hears the owner's name.
|
|
346
|
+
assert.deepEqual(await echo('ask', { being: 'E', method: 'echo', args: { x: 1 } }), { from: 'OWNER', x: 1 }, 'the ward names the asker, and the name is the owner');
|
|
347
|
+
assert.deepEqual(bp(await ask('ask', { being: 'S' })).asks.map((a) => a.name), ['hello', 'buy'], 'no method is her describe, and a gate that reads an occupant record shows the owner nothing: refund is hers to nobody here');
|
|
348
|
+
assert.deepEqual(await ask('ask', { being: 'S', method: 'hello' }), { welcome: true });
|
|
349
|
+
assert.deepEqual(await ask('ask', { being: 'S', method: 'nosuch' }), { error: 'unknown ask' }, 'her own answer, as any asker gets it');
|
|
350
|
+
|
|
351
|
+
// Nobody named, and nobody home: one refusal, and never a fourth word.
|
|
352
|
+
assert.deepEqual(await echo('ask', {}), { error: 'no such being' }, 'a ward with no public being has nobody to ask as nobody');
|
|
353
|
+
assert.deepEqual(await echo('public', { key: 'E' }), { public: 'E' });
|
|
354
|
+
assert.deepEqual(await echo('ask', { method: 'echo', args: { x: 2 } }), { from: null, x: 2 }, 'with no being named it is the public being, asked as nobody');
|
|
355
|
+
|
|
356
|
+
// What it refuses, each in an object, because the owner hears objects.
|
|
357
|
+
const pk = (bp(await ask()) as unknown as { notes: { pk: string } }).notes.pk;
|
|
358
|
+
assert.deepEqual(await ask('ask', { being: 'nobody' }), { error: 'no such being' });
|
|
359
|
+
assert.deepEqual(await ask('ask', { being: pk }), { error: 'no such being' }, 'the ward is a being to nobody outside, here as at invite and unboot');
|
|
360
|
+
assert.deepEqual(await ask('ask', { being: 42 }), { error: 'no such being' }, 'a name is a string or it is nothing');
|
|
361
|
+
assert.deepEqual(await ask('ask', { being: 'S', method: 42 }), { error: 'an ask is named by a word' }, 'and so is an ask');
|
|
362
|
+
});
|
|
363
|
+
|
|
292
364
|
// The one public being, marked and unmarked through the owner. A second
|
|
293
365
|
// one would leave the first holding every relation she had, reachable by
|
|
294
366
|
// nobody at the bare pk and told by nobody that she had been replaced.
|
|
@@ -341,6 +413,131 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
341
413
|
assert.deepEqual(await maker.being.say({ id: 'nothing' }), { said: null });
|
|
342
414
|
});
|
|
343
415
|
|
|
416
|
+
// The count, reached the one way a suite that owns no bytes can reach it:
|
|
417
|
+
// a caller's numbers are in her own partition, so winding them back and
|
|
418
|
+
// standing her ward up again makes her next ask carry a number the far
|
|
419
|
+
// door has already honoured. A kit with no once-only delivery answers it
|
|
420
|
+
// and passes every other chapter here clean.
|
|
421
|
+
kept('a number already honoured is repeated: the far being is not asked twice, and nothing is written for it', async () => {
|
|
422
|
+
const { w, shop, alice } = await world();
|
|
423
|
+
const inv = await shop.being.invite();
|
|
424
|
+
assert.deepEqual(await alice.being.join(inv), { welcome: true });
|
|
425
|
+
assert.deepEqual(await alice.being.buy('a hat'), { ok: true, receipt: 'receipt for a hat' });
|
|
426
|
+
const sold = (shop.cells.sales as JsonObject[]).length;
|
|
427
|
+
|
|
428
|
+
// Her keys for the shop, wound back to the number she has just spent.
|
|
429
|
+
const kept = able(w);
|
|
430
|
+
const p = kept.partition('A');
|
|
431
|
+
const keys = ((p.bind as JsonObject).A as JsonObject).standings as JsonObject;
|
|
432
|
+
const mine = keys.shop as JsonObject;
|
|
433
|
+
const back = (mine.seq as number) - 1;
|
|
434
|
+
mine.seq = back;
|
|
435
|
+
await kept.restart('A', p);
|
|
436
|
+
|
|
437
|
+
const again = await alice.being.buy('a second hat');
|
|
438
|
+
assert.ok(said(again, 'repeated'), `a number already honoured is repeated, not ${JSON.stringify(again)}`);
|
|
439
|
+
assert.equal((shop.cells.sales as JsonObject[]).length, sold, 'and she was never asked');
|
|
440
|
+
// The count carries on from where it was: the next number is fresh.
|
|
441
|
+
assert.deepEqual(await alice.being.buy('a third hat'), { ok: true, receipt: 'receipt for a third hat' });
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// A restart is silent by construction: the ward comes back on the same
|
|
445
|
+
// seed with the same pk, and every relation it holds still points at it.
|
|
446
|
+
// What must survive with it is what the door has honoured, or a restart is
|
|
447
|
+
// a stretch of time in which every number can be replayed.
|
|
448
|
+
kept('a restart is silent: same pk, every relation still stands, and the far door still refuses what it spent', async () => {
|
|
449
|
+
const { w, shop, alice } = await world();
|
|
450
|
+
const inv = await shop.being.invite();
|
|
451
|
+
assert.deepEqual(await alice.being.join(inv), { welcome: true });
|
|
452
|
+
assert.deepEqual(await alice.being.buy('a hat'), { ok: true, receipt: 'receipt for a hat' });
|
|
453
|
+
const pk = shop.pk;
|
|
454
|
+
const able2 = able(w);
|
|
455
|
+
const mark = JSON.stringify(able2.partition('S').heirs);
|
|
456
|
+
|
|
457
|
+
await able2.restart('S');
|
|
458
|
+
assert.equal(shop.pk, pk, 'a ward that came back changed its pk');
|
|
459
|
+
assert.equal(JSON.stringify(able2.partition('S').heirs), mark, 'and it came back holding what it had honoured');
|
|
460
|
+
assert.deepEqual(await alice.being.buy('a second hat'), { ok: true, receipt: 'receipt for a second hat' }, 'the relation goes on, and she was never told');
|
|
461
|
+
assert.deepEqual(alice.cells.standings.shop!.id, 'shop', 'her side was not told either');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Notes are the one thing on a record that a being seeds and a ward keeps
|
|
465
|
+
// without ever reading. A kit that files what it was handed rather than a
|
|
466
|
+
// copy of it leaves the seeder a hand inside her cells: it writes without
|
|
467
|
+
// the ward being told, so what a restart brings back is not what she read.
|
|
468
|
+
t('notes are values and are kept as a copy: what the seeder still holds is not what the record holds', async () => {
|
|
469
|
+
const { alice } = await world();
|
|
470
|
+
const seeded = { tier: { deep: 'gold' } } as unknown as JsonObject;
|
|
471
|
+
assert.ok((await alice.being.invite('noted', seeded)) !== null, 'she minted with notes');
|
|
472
|
+
(seeded.tier as JsonObject).deep = 'moved';
|
|
473
|
+
assert.deepEqual(alice.cells.occupants.noted!.notes, { tier: { deep: 'gold' } }, 'the record is the copy, all the way down');
|
|
474
|
+
// And notes no harbor could write back are no invitation, for the reason
|
|
475
|
+
// every other write is refused: a record it could not keep.
|
|
476
|
+
assert.equal(await alice.being.invite('bad-notes', { n: 0 / 0 } as unknown as JsonObject), null);
|
|
477
|
+
assert.equal(Object.hasOwn(alice.cells.occupants, 'bad-notes'), false, 'and nothing was written for it');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// The wait, and the one thing a kit can get wrong that nothing else here
|
|
481
|
+
// would notice. A ward bounds every ask itself, so a relation cycle ends:
|
|
482
|
+
// A asks B, B asking back holds A's lane, and the third ask can never be
|
|
483
|
+
// answered. A kit with no bound waits for ever and hangs, and every other
|
|
484
|
+
// chapter in this suite would still pass it. The word is `late`, which
|
|
485
|
+
// promises nothing either way, and the relation lives afterwards.
|
|
486
|
+
t('the wait is bounded, so a cycle ends: the ask that cannot be answered is late, and the relation lives', async () => {
|
|
487
|
+
const { w } = await world();
|
|
488
|
+
const a = await w.boot<Member>('MA', Member);
|
|
489
|
+
const b = await w.boot<Member>('MB', Member);
|
|
490
|
+
const toB = (await b.being.invite('from-a'))!;
|
|
491
|
+
const toA = (await a.being.invite('from-b'))!;
|
|
492
|
+
assert.ok(answered(await a.being.knock(toB, 'ping')), 'a knocks b');
|
|
493
|
+
assert.equal(await a.being.take('b', toB), 'b');
|
|
494
|
+
assert.ok(answered(await b.being.knock(toA, 'ping')), 'b knocks a');
|
|
495
|
+
assert.equal(await b.being.take('a', toA), 'a');
|
|
496
|
+
|
|
497
|
+
// A asks B; B, answering, asks A back; A, answering that, asks B on the
|
|
498
|
+
// lane the first ask still holds. Every link is a legal ask.
|
|
499
|
+
const out = (await a.being.relay({ id: 'b', method: 'relay', args: { id: 'a', method: 'relay', args: { id: 'b', method: 'ping' }, time: 200 } })) as { said: string | null; out: { said: string | null } | null };
|
|
500
|
+
assert.equal(out.said, null, 'the outer ask was answered');
|
|
501
|
+
assert.equal(out.out?.said, 'late', 'and what it carries is the ask that could not be, said as late');
|
|
502
|
+
|
|
503
|
+
// The relation is not the worse for it: the lane is free again and the
|
|
504
|
+
// count moved on, which is what a bound is for.
|
|
505
|
+
const after = (await a.being.relay({ id: 'b', method: 'ping' })) as { said: string | null };
|
|
506
|
+
assert.equal(after.said, null, 'and she asks again on the same standing');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Lend is boot's other half: what the device already has, reached the one
|
|
510
|
+
// way anything is reached. The being lent stands in a ward the harbor
|
|
511
|
+
// roots, so nothing here can look at her from the outside; what the ward
|
|
512
|
+
// owes is what is asserted. She is handed an id, the standing is an
|
|
513
|
+
// ordinary one, and the invitation is nowhere she can read.
|
|
514
|
+
lends('what the device lends is a being: she is handed an id, asks on an ordinary standing, and never sees the invitation', async () => {
|
|
515
|
+
const w = await make({ lends: { pen: 'Member' } });
|
|
516
|
+
const maker = await w.boot<Maker>('MK', Maker);
|
|
517
|
+
assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'pen' }), { lent: 'pen' }, 'the id she gave is the id she gets');
|
|
518
|
+
// An ordinary standing from there on: she asks on it, and arrives at that
|
|
519
|
+
// being's door as somebody the far side named.
|
|
520
|
+
const said = (await maker.being.say({ id: 'pen' })) as { said: { pong: string | null } | null };
|
|
521
|
+
assert.equal(typeof said.said?.pong, 'string', 'she arrived under an id the far being minted');
|
|
522
|
+
// The keys are the ward's. Nothing of the relation is in her cells, which
|
|
523
|
+
// is the whole difference between this standing and one she gave away.
|
|
524
|
+
const c = w.census();
|
|
525
|
+
const heir = c.binds.MK!.standings.pen!.heir;
|
|
526
|
+
assert.equal(typeof heir, 'string', 'the ward bound a heir for it');
|
|
527
|
+
assert.equal(JSON.stringify(c.cells.MK).includes(heir!), false, 'and the invitation reached her nowhere');
|
|
528
|
+
// Null is every kind of no, and she cannot tell them apart.
|
|
529
|
+
assert.deepEqual(await maker.being.borrow({ name: 'gps', id: 'x' }), { lent: null }, 'a name this box does not lend');
|
|
530
|
+
assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'pen' }), { lent: null }, 'an id she already holds');
|
|
531
|
+
assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'OWNER' }), { lent: null }, 'a word the ward reserves');
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
lends('a harbor with nothing to lend lends nothing, and says so the same way', async () => {
|
|
535
|
+
const w = await make();
|
|
536
|
+
const maker = await w.boot<Maker>('MK', Maker);
|
|
537
|
+
assert.deepEqual(await maker.being.borrow({ name: 'pen', id: 'pen' }), { lent: null });
|
|
538
|
+
assert.equal(w.census().binds.MK!.standings.pen, undefined, 'and nothing was written for it');
|
|
539
|
+
});
|
|
540
|
+
|
|
344
541
|
// The case the estate's walk reaches by weight and this reaches by hand: a
|
|
345
542
|
// seat kicked after it answered and before it was taken. She holds a key
|
|
346
543
|
// the door bound for her, so she is the one person who hears why, and she
|
package/src/conformance/reach.ts
CHANGED
|
@@ -47,7 +47,7 @@ export function conformReach(label: string, make: () => Promise<FarSide>, { test
|
|
|
47
47
|
t('asks in flight at once each get their own answer', async () => {
|
|
48
48
|
const far = await make();
|
|
49
49
|
try {
|
|
50
|
-
await far.hold(pk('aa'), async (b) => (await wait(b[0]
|
|
50
|
+
await far.hold(pk('aa'), async (b) => (await wait(b[0] * 10), bytes(b[0])));
|
|
51
51
|
const out = await Promise.all([far.reach.carry(pk('aa'), bytes(3)), far.reach.carry(pk('aa'), bytes(1)), far.reach.carry(pk('aa'), bytes(2))]);
|
|
52
52
|
assert.deepEqual(out, [bytes(3), bytes(1), bytes(2)]);
|
|
53
53
|
} finally {
|
package/src/harbor/core.ts
CHANGED
|
@@ -8,16 +8,18 @@
|
|
|
8
8
|
// edge harbor is this over an object's storage. None of them is in this
|
|
9
9
|
// tree, and every one of them passes the conformance suite untouched.
|
|
10
10
|
//
|
|
11
|
-
// The directory is filled
|
|
11
|
+
// The directory is filled four ways, in this order: the harbor's own
|
|
12
12
|
// doors; a socket a dialer holds to it, bound once the door behind the
|
|
13
|
-
// dialer's claim has proved it holds the key, and unbound at close;
|
|
14
|
-
//
|
|
13
|
+
// dialer's claim has proved it holds the key, and unbound at close; the
|
|
14
|
+
// claims of a listener this harbor dialed, proved the same way and reached
|
|
15
|
+
// through that line; and a hint, a pk at a URL, kept in the store. A hint
|
|
16
|
+
// never displaces a reach proved at a door. And one fallback: a dialer with nothing in its
|
|
15
17
|
// directory for a pk sends down the socket it holds, because the listener
|
|
16
18
|
// it dialed is the rendezvous and may hold that pk on another socket. Bytes
|
|
17
19
|
// that arrive from the wire go to an own door or a held socket and never
|
|
18
20
|
// onward by request or by fallback; that one rule is the rendezvous.
|
|
19
21
|
import { Ward } from '../ward/ward.ts';
|
|
20
|
-
import { maker, entropy as random, learnPk, type Ground, type WardPointers } from '../ward/ground.ts';
|
|
22
|
+
import { maker, entropy as random, learnPk, type Ground, type WardPointers, type Lend } from '../ward/ground.ts';
|
|
21
23
|
import type { BeingClass, BeingLike } from '../being/types.ts';
|
|
22
24
|
import { silence } from '../being/silence.ts';
|
|
23
25
|
import { request, type Reach } from './reach.ts';
|
|
@@ -102,9 +104,22 @@ export class Harbor {
|
|
|
102
104
|
for (const say of this.announcers) say();
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
// What this device lends the beings of the wards it hosts, by name. One
|
|
108
|
+
// for the harbor, and `lendFor` says which ward may ask it: every ward,
|
|
109
|
+
// unless a terrain says a stranger's ward is lent nothing.
|
|
110
|
+
// Set at birth or once the harbor's own ward is standing, which is the
|
|
111
|
+
// usual order: a terrain puts that ward up first, then boots the rest on a
|
|
112
|
+
// ground that can reach it.
|
|
113
|
+
lend: Lend | undefined;
|
|
114
|
+
|
|
115
|
+
constructor(store: Store, loader: Loader = async () => ({}), lend?: Lend) {
|
|
106
116
|
this.store = store;
|
|
107
117
|
this.loader = loader;
|
|
118
|
+
this.lend = lend;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected lendFor(_name: string, _record: WardRecord): Lend | undefined {
|
|
122
|
+
return this.lend;
|
|
108
123
|
}
|
|
109
124
|
|
|
110
125
|
// Boot every ward the store keeps, and learn the hints. One ward that will
|
|
@@ -112,6 +127,10 @@ export class Harbor {
|
|
|
112
127
|
// named in `unbooted` and takes nobody else down with it.
|
|
113
128
|
async boot(): Promise<void> {
|
|
114
129
|
for (const name of await this.store.list()) {
|
|
130
|
+
// A ward this harbor already hosts is not booted twice. A terrain that
|
|
131
|
+
// keeps a ward of its own puts it up before this runs, so that a being
|
|
132
|
+
// born in any other ward can lend from her first line.
|
|
133
|
+
if (this.wards.has(name)) continue;
|
|
115
134
|
try {
|
|
116
135
|
const kept = await this.store.load(name);
|
|
117
136
|
if (kept) await this.host(name, kept);
|
|
@@ -335,6 +354,8 @@ export class Harbor {
|
|
|
335
354
|
random,
|
|
336
355
|
wrote: () => this.#wrote(name),
|
|
337
356
|
};
|
|
357
|
+
const lend = this.lendFor(name, kept.record);
|
|
358
|
+
if (lend) ground.lend = lend;
|
|
338
359
|
const saving: Saving = { memory, dirty: false, running: null, timer: undefined, gone: false };
|
|
339
360
|
this.#saving.set(name, saving);
|
|
340
361
|
// A ward that will not be born leaves nothing behind: a partition of a
|
package/src/harbor/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { Harbor, DEFAULT_CODE, type Hosted, type Bound, type Loader } from './co
|
|
|
8
8
|
export { dial, type Dialer } from './dial.ts';
|
|
9
9
|
export { MemoryStore, values, type Store, type Kept, type WardRecord } from './store.ts';
|
|
10
10
|
export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
|
|
11
|
-
export type { Ground, WardPointers } from '../ward/ground.ts';
|
|
11
|
+
export type { Ground, WardPointers, Lend } from '../ward/ground.ts';
|
|
12
12
|
// What a harbor builds a ground out of. Convenience, never contract: a kit
|
|
13
13
|
// writing its own harbor may write these three again.
|
|
14
14
|
export { maker, entropy, learnPk } from '../ward/ground.ts';
|
package/src/harbor/memory.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// reads no partition, opens no byte. Several memory harbors may be linked,
|
|
6
6
|
// which stands in for a wire, and cut, which stands in for weather.
|
|
7
7
|
import type { BeingClass, BeingLike } from '../being/types.ts';
|
|
8
|
-
import { maker, entropy, learnPk, type Ground, type WardPointers } from '../ward/ground.ts';
|
|
8
|
+
import { maker, entropy, learnPk, type Ground, type WardPointers, type Lend } from '../ward/ground.ts';
|
|
9
9
|
import { hex } from '../ward/arithmetic.ts';
|
|
10
10
|
|
|
11
11
|
export type Booted = WardPointers & { pk: string };
|
|
@@ -42,18 +42,21 @@ export class MemoryHarbor {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// boot by seed. the harbor gets a door and an ask, and learns the pk by asking.
|
|
45
|
-
|
|
45
|
+
// `lend` is what this device lends the ward's beings, and a test's stand-in
|
|
46
|
+
// for a real terrain's: left out, every name answers null.
|
|
47
|
+
async boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, lend?: Lend): Promise<Booted> {
|
|
46
48
|
this.partitions.set(seed, {});
|
|
47
|
-
return this.reboot(seed, Ward, classes);
|
|
49
|
+
return this.reboot(seed, Ward, classes, lend);
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
// a restart: same seed, same partition, fresh ward. the ward's files are the harbor's to keep.
|
|
51
|
-
async reboot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass
|
|
53
|
+
async reboot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, lend?: Lend): Promise<Booted> {
|
|
52
54
|
const memory = this.partitions.get(seed);
|
|
53
55
|
if (!memory) throw new Error(`no partition for ${seed}`);
|
|
54
56
|
const ground: Ground = {
|
|
55
57
|
seed,
|
|
56
58
|
memory,
|
|
59
|
+
...(lend ? { lend } : {}),
|
|
57
60
|
// One objects map for the whole harbor, not one per ward: every ward
|
|
58
61
|
// here is in this process and a test reaches any being of any of them.
|
|
59
62
|
instantiate: maker(this.objects, classes),
|
package/src/harbor/reach.ts
CHANGED
|
@@ -66,7 +66,7 @@ const ASK = 0,
|
|
|
66
66
|
REPLY = 1,
|
|
67
67
|
NONE = 2;
|
|
68
68
|
|
|
69
|
-
// The wire suite: which frames these are and which
|
|
69
|
+
// The wire suite: which frames these are and which five algorithms seal what
|
|
70
70
|
// they carry. One value, and it is not negotiated. It rides in the handshake
|
|
71
71
|
// and never on an ask, so an ask stays bytes nobody can tell from noise, and
|
|
72
72
|
// a line still learns before it carries anything whether the far side speaks
|
package/src/ward/cells.ts
CHANGED
|
@@ -22,40 +22,82 @@ import type { Cells, Json } from '../being/types.ts';
|
|
|
22
22
|
// reply from ending a walk in a stack overflow instead of a refusal.
|
|
23
23
|
export const DEPTH = 64;
|
|
24
24
|
|
|
25
|
+
// How many values one written out may come to. Depth alone does not bound
|
|
26
|
+
// what a value costs to keep: one subvalue may sit under two keys, and
|
|
27
|
+
// nesting that forty levels deep is inside the depth bound and is a trillion
|
|
28
|
+
// values once written down. What is walked here is the object graph, which is
|
|
29
|
+
// small; what a harbor keeps is the tree it expands to, which is not. So the
|
|
30
|
+
// walk counts the tree while it proves the graph, and refuses a value nobody
|
|
31
|
+
// could write down. This is a bound on what a being holds, so it is this
|
|
32
|
+
// kit's number and no word of the spec.
|
|
33
|
+
export const BREADTH = 1 << 20;
|
|
34
|
+
|
|
35
|
+
// Text a harbor can write down: every surrogate in a pair. A lone one is a
|
|
36
|
+
// code unit with no character, which JSON escapes and UTF-8 cannot encode at
|
|
37
|
+
// all, so a harbor that keeps strings as bytes loses it and one that keeps
|
|
38
|
+
// the object does not. I-JSON says a string is text, and this is that rule
|
|
39
|
+
// where the value is written rather than where it is read.
|
|
40
|
+
const WELL_FORMED = /^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/;
|
|
41
|
+
|
|
25
42
|
// I-JSON, all the way down. A number that JSON cannot write is not a number
|
|
26
43
|
// a harbor can keep, a key on the prototype is not a key she wrote, and a
|
|
27
44
|
// hole in a list, or a key named `__proto__`, is a thing JSON writes one way
|
|
28
45
|
// and a runtime reads another.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
//
|
|
47
|
+
// A fault is a sentence; anything else is the count of values under this one,
|
|
48
|
+
// itself included. A value already counted is a value already proven, so a
|
|
49
|
+
// graph that shares is walked once per node rather than once per path.
|
|
50
|
+
function walk(v: unknown, path: string, seen: Set<object>, sized: Map<object, number>, depth: number): string | number {
|
|
51
|
+
if (v === null || typeof v === 'boolean') return 1;
|
|
52
|
+
if (typeof v === 'string') return WELL_FORMED.test(v) ? 1 : `${path} carries a lone surrogate, which is no text a harbor can write down`;
|
|
53
|
+
if (typeof v === 'number') {
|
|
54
|
+
if (!Number.isFinite(v)) return `${path} is ${String(v)}, which no harbor can write down`;
|
|
55
|
+
// Minus zero is a number JSON writes as `0` and reads back as `0`. A
|
|
56
|
+
// harbor that keeps values would hand her the sign she wrote and one that
|
|
57
|
+
// writes JSON would not, and she is never told which she is standing in.
|
|
58
|
+
return Object.is(v, -0) ? `${path} is minus zero, which comes back as zero from every harbor that writes JSON` : 1;
|
|
59
|
+
}
|
|
32
60
|
if (typeof v !== 'object') return `${path} is a ${typeof v}, which is not a value`;
|
|
33
61
|
if (seen.has(v)) return `${path} refers back to itself`;
|
|
62
|
+
const counted = sized.get(v);
|
|
63
|
+
if (counted !== undefined) return counted;
|
|
34
64
|
if (depth >= DEPTH) return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
|
|
35
65
|
seen.add(v);
|
|
36
66
|
try {
|
|
67
|
+
let size = 1;
|
|
68
|
+
const under = (where: string, x: unknown): string | null => {
|
|
69
|
+
const r = walk(x, where, seen, sized, depth + 1);
|
|
70
|
+
if (typeof r === 'string') return r;
|
|
71
|
+
size += r;
|
|
72
|
+
return size > BREADTH ? `${path} writes out to more than ${BREADTH} values, which no harbor can keep` : null;
|
|
73
|
+
};
|
|
37
74
|
if (Array.isArray(v)) {
|
|
38
75
|
for (let i = 0; i < v.length; i += 1) {
|
|
39
76
|
if (!(i in v)) return `${path}[${i}] is a hole, which no harbor can write down`;
|
|
40
|
-
const f =
|
|
77
|
+
const f = under(`${path}[${i}]`, v[i]);
|
|
41
78
|
if (f) return f;
|
|
42
79
|
}
|
|
43
|
-
|
|
80
|
+
sized.set(v, size);
|
|
81
|
+
return size;
|
|
44
82
|
}
|
|
45
83
|
const proto = Object.getPrototypeOf(v);
|
|
46
84
|
if (proto !== Object.prototype && proto !== null) return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
|
|
47
85
|
for (const k of Object.keys(v)) {
|
|
48
86
|
if (k === '__proto__') return `${path}.__proto__ is a key no harbor can keep`;
|
|
49
|
-
const f =
|
|
87
|
+
const f = under(`${path}.${k}`, (v as Record<string, unknown>)[k]);
|
|
50
88
|
if (f) return f;
|
|
51
89
|
}
|
|
52
|
-
|
|
90
|
+
sized.set(v, size);
|
|
91
|
+
return size;
|
|
53
92
|
} finally {
|
|
54
93
|
seen.delete(v);
|
|
55
94
|
}
|
|
56
95
|
}
|
|
57
96
|
|
|
58
|
-
export const cellFault = (v: unknown, path: string): string | null =>
|
|
97
|
+
export const cellFault = (v: unknown, path: string): string | null => {
|
|
98
|
+
const out = walk(v, path, new Set(), new Map(), 0);
|
|
99
|
+
return typeof out === 'string' ? out : null;
|
|
100
|
+
};
|
|
59
101
|
|
|
60
102
|
// The three keys at the root of her cells that are the ward's: it writes
|
|
61
103
|
// them, she reads them, and a write of hers there is refused like a non-value.
|
|
@@ -69,6 +111,13 @@ const wrapped = new WeakMap<object, object>();
|
|
|
69
111
|
const targets = new WeakMap<object, object>();
|
|
70
112
|
const guards = new WeakSet();
|
|
71
113
|
|
|
114
|
+
// A name no partition can hold, wherever it appears. It is here beside the
|
|
115
|
+
// guard that refuses it, because the ward refuses it too: a relation named
|
|
116
|
+
// this one would be written into the bind table and then throw on her cells,
|
|
117
|
+
// which is half a relation and a throw where a null is promised. One rule,
|
|
118
|
+
// one place, read by the guard and by the stance.
|
|
119
|
+
export const unkeepable = (k: string): boolean => k === '__proto__';
|
|
120
|
+
|
|
72
121
|
// A key a write may land on: a string that is not `__proto__`, and not one
|
|
73
122
|
// of the ward's at the root. A symbol key is a thing JSON never writes.
|
|
74
123
|
const refuse: (why: string) => never = (why) => {
|
|
@@ -76,7 +125,7 @@ const refuse: (why: string) => never = (why) => {
|
|
|
76
125
|
};
|
|
77
126
|
function keyFault(t: object, k: string | symbol, v: unknown, path: string, root: boolean): void {
|
|
78
127
|
if (typeof k !== 'string') refuse(`${path} takes no symbol key`);
|
|
79
|
-
if (k
|
|
128
|
+
if (unkeepable(k)) refuse(`${path}.__proto__ is a key no harbor can keep`);
|
|
80
129
|
if (root && WARDS.has(k)) refuse(`${path}.${k} is the ward's to write`);
|
|
81
130
|
// A list grows by one at its end, or it has holes JSON cannot write. A
|
|
82
131
|
// push sets the slot at its length and then the length: both pass. A
|
|
@@ -138,4 +187,4 @@ function guard<T extends object>(target: T, path: string, wrote: () => void, roo
|
|
|
138
187
|
export const guardCells = (cells: Cells, wrote: () => void = () => {}): Cells => guard(cells as unknown as Record<string, Json>, 'cells', wrote, true) as unknown as Cells;
|
|
139
188
|
// The cells behind the guard, for the ward alone: the one writer of the
|
|
140
189
|
// three keys that are its own.
|
|
141
|
-
export const unguarded = (cells: Cells): Cells => (targets.get(cells
|
|
190
|
+
export const unguarded = (cells: Cells): Cells => (targets.get(cells) ?? cells) as Cells;
|