@quo-systems/quo 0.2.9 → 0.2.11
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 +19 -10
- package/SPEC.md +301 -110
- package/dist/being/being.d.ts +2 -2
- package/dist/being/being.js +34 -12
- package/dist/being/digest.js +26 -8
- package/dist/being/index.d.ts +2 -2
- package/dist/being/index.js +2 -2
- package/dist/being/silence.d.ts +2 -0
- package/dist/being/silence.js +12 -0
- package/dist/being/types.d.ts +4 -2
- package/dist/being/types.js +25 -0
- package/dist/conformance/assert.js +40 -6
- package/dist/conformance/beings.d.ts +48 -5
- package/dist/conformance/beings.js +39 -8
- package/dist/conformance/estate.js +110 -21
- package/dist/conformance/index.d.ts +5 -2
- package/dist/conformance/index.js +165 -7
- package/dist/harbor/core.d.ts +5 -2
- package/dist/harbor/core.js +195 -45
- package/dist/harbor/dial.js +32 -15
- package/dist/harbor/index.d.ts +1 -0
- package/dist/harbor/index.js +3 -0
- package/dist/harbor/memory.d.ts +3 -3
- package/dist/harbor/memory.js +7 -12
- package/dist/harbor/reach.js +42 -17
- package/dist/ward/allowance.js +15 -4
- package/dist/ward/arithmetic.d.ts +1 -0
- package/dist/ward/arithmetic.js +22 -6
- package/dist/ward/cells.d.ts +3 -1
- package/dist/ward/cells.js +79 -21
- package/dist/ward/door.d.ts +3 -2
- package/dist/ward/door.js +38 -10
- package/dist/ward/ground.d.ts +5 -1
- package/dist/ward/ground.js +38 -1
- package/dist/ward/heirs.d.ts +2 -3
- package/dist/ward/heirs.js +19 -13
- package/dist/ward/index.d.ts +1 -0
- package/dist/ward/index.js +3 -0
- package/dist/ward/owner.d.ts +6 -27
- package/dist/ward/owner.js +59 -33
- package/dist/ward/partition.d.ts +3 -0
- package/dist/ward/partition.js +109 -4
- package/dist/ward/seal.d.ts +1 -0
- package/dist/ward/seal.js +41 -13
- package/dist/ward/stance.d.ts +7 -3
- package/dist/ward/stance.js +156 -66
- package/dist/ward/ward.d.ts +10 -0
- package/dist/ward/ward.js +123 -51
- package/package.json +4 -2
- package/src/being/being.ts +33 -11
- package/src/being/digest.ts +28 -13
- package/src/being/index.ts +2 -2
- package/src/being/silence.ts +14 -0
- package/src/being/types.ts +39 -5
- package/src/conformance/assert.ts +37 -4
- package/src/conformance/beings.ts +41 -10
- package/src/conformance/estate.ts +107 -20
- package/src/conformance/index.ts +188 -13
- package/src/harbor/core.ts +203 -46
- package/src/harbor/dial.ts +46 -17
- package/src/harbor/index.ts +3 -0
- package/src/harbor/memory.ts +8 -13
- package/src/harbor/reach.ts +47 -21
- package/src/ward/allowance.ts +15 -4
- package/src/ward/arithmetic.ts +25 -8
- package/src/ward/cells.ts +76 -25
- package/src/ward/door.ts +38 -12
- package/src/ward/ground.ts +52 -3
- package/src/ward/heirs.ts +19 -13
- package/src/ward/index.ts +3 -0
- package/src/ward/owner.ts +65 -46
- package/src/ward/partition.ts +109 -5
- package/src/ward/seal.ts +41 -12
- package/src/ward/stance.ts +163 -64
- package/src/ward/ward.ts +124 -52
- package/vectors/arithmetic.json +7 -0
- package/vectors/framing.json +30 -15
- package/vectors/wire.json +4 -4
package/dist/being/being.d.ts
CHANGED
|
@@ -13,10 +13,10 @@ export declare class Being {
|
|
|
13
13
|
get cells(): Cells;
|
|
14
14
|
get standings(): Standings;
|
|
15
15
|
get occupants(): Occupants;
|
|
16
|
-
invite(id: string): Promise<Invitation | null>;
|
|
16
|
+
invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
|
|
17
17
|
knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
|
|
18
18
|
take(id: string, invitation: Invitation): Promise<string | null>;
|
|
19
|
-
boot(className: string, key: string): string | null
|
|
19
|
+
boot(className: string, key: string, id?: string): Promise<string | null>;
|
|
20
20
|
occupant(asker: Asker): OccupantRecord | undefined;
|
|
21
21
|
describe(asker: Asker): Blueprint;
|
|
22
22
|
answer(asker: Asker, method?: string, args?: JsonObject): Promise<Reply>;
|
package/dist/being/being.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
// Names a subclass may not use for an ask, because they are the base's own.
|
|
2
2
|
const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'constructor']);
|
|
3
|
+
// Whether she has a method of that name, written on her own prototype chain
|
|
4
|
+
// below Object's. A name Object lends every object, `hasOwnProperty` or
|
|
5
|
+
// `toString`, is not a method she wrote; and a field she assigns in her own
|
|
6
|
+
// constructor is not there yet when the base checks, so an ask is a method
|
|
7
|
+
// on the prototype and nothing else.
|
|
8
|
+
const method = (self, name) => {
|
|
9
|
+
for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
|
|
10
|
+
if (Object.hasOwn(p, name))
|
|
11
|
+
return typeof p[name] === 'function';
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
};
|
|
15
|
+
// Both statics below are read off the class the object was made from, so a
|
|
16
|
+
// subclass declaring either replaces its parent's rather than adding to it.
|
|
17
|
+
// That is the rule: her blueprint is exactly what the class in front of you
|
|
18
|
+
// declares, in the order she chose, and merging down a chain would hand her
|
|
19
|
+
// asks she may mean to drop and an order she did not write. A subclass that
|
|
20
|
+
// means to extend says so, `static override asks = { ...Parent.asks, mine: {} }`.
|
|
3
21
|
export class Being {
|
|
4
22
|
// Her cells' defaults. Merged in at birth, only where a key is missing, so
|
|
5
23
|
// a restart keeps what she wrote.
|
|
6
24
|
static cells = {};
|
|
7
|
-
// What she can be asked. Declaration order is blueprint order
|
|
25
|
+
// What she can be asked. Declaration order is blueprint order, except a
|
|
26
|
+
// name that reads as an array index, which the language lists first.
|
|
8
27
|
static asks = {};
|
|
9
28
|
stance;
|
|
10
29
|
constructor(stance) {
|
|
@@ -13,11 +32,13 @@ export class Being {
|
|
|
13
32
|
for (const name of Object.keys(C.asks)) {
|
|
14
33
|
if (RESERVED.has(name))
|
|
15
34
|
throw new Error(`ask '${name}' is a reserved name`);
|
|
16
|
-
if (
|
|
17
|
-
throw new Error(`ask '${name}' has no method`);
|
|
35
|
+
if (!method(this, name))
|
|
36
|
+
throw new Error(`ask '${name}' has no method on the prototype`);
|
|
18
37
|
}
|
|
38
|
+
// Own keys only: a default named after a member of Object's prototype is
|
|
39
|
+
// still hers, and still missing until she writes it.
|
|
19
40
|
for (const [k, v] of Object.entries(C.cells))
|
|
20
|
-
if (!(
|
|
41
|
+
if (!Object.hasOwn(stance.cells, k))
|
|
21
42
|
stance.cells[k] = structuredClone(v);
|
|
22
43
|
}
|
|
23
44
|
get cells() {
|
|
@@ -29,8 +50,8 @@ export class Being {
|
|
|
29
50
|
get occupants() {
|
|
30
51
|
return this.stance.occupants;
|
|
31
52
|
}
|
|
32
|
-
invite(id) {
|
|
33
|
-
return this.stance.occupants.invite(id);
|
|
53
|
+
invite(id, notes) {
|
|
54
|
+
return this.stance.occupants.invite(id, notes);
|
|
34
55
|
}
|
|
35
56
|
knock(invitation, method, args, wanted) {
|
|
36
57
|
return this.stance.standings.knock(invitation, method, args, wanted);
|
|
@@ -38,13 +59,14 @@ export class Being {
|
|
|
38
59
|
take(id, invitation) {
|
|
39
60
|
return this.stance.standings.take(id, invitation);
|
|
40
61
|
}
|
|
41
|
-
// A new being of her ward, by class name, under a key she chooses.
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
// A new being of her ward, by class name, under a key she chooses. With an
|
|
63
|
+
// id, she holds a standing to the being she made, who knows her by her key.
|
|
64
|
+
boot(className, key, id) {
|
|
65
|
+
return this.stance.boot(className, key, id);
|
|
44
66
|
}
|
|
45
67
|
// The occupant record for whoever is at the door. Undefined at a public being.
|
|
46
68
|
occupant(asker) {
|
|
47
|
-
return asker.id
|
|
69
|
+
return asker.id !== undefined && Object.hasOwn(this.cells.occupants, asker.id) ? this.cells.occupants[asker.id] : undefined;
|
|
48
70
|
}
|
|
49
71
|
// Her blueprint for this asker. Override to shape it by hand.
|
|
50
72
|
describe(asker) {
|
|
@@ -72,8 +94,8 @@ export class Being {
|
|
|
72
94
|
// lookup would also find every name on Object's prototype: `valueOf`
|
|
73
95
|
// would answer with her stance, `toString` with a string, and neither is
|
|
74
96
|
// an ask she wrote. Only her own keys are asks, which is what describe
|
|
75
|
-
//
|
|
76
|
-
const spec =
|
|
97
|
+
// shows. What she shows is what she can be asked.
|
|
98
|
+
const spec = Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
|
|
77
99
|
if (!spec || (spec.for && !spec.for(this.occupant(asker), asker)))
|
|
78
100
|
return { error: 'unknown ask' };
|
|
79
101
|
const fn = this[method];
|
package/dist/being/digest.js
CHANGED
|
@@ -5,15 +5,33 @@
|
|
|
5
5
|
// happened to be holding.
|
|
6
6
|
const absent = (v) => v === undefined || typeof v === 'function' || typeof v === 'symbol';
|
|
7
7
|
// JCS for I-JSON values: sorted keys, no whitespace, JSON escaping. Numbers
|
|
8
|
-
// are serialized as ES does, which is what RFC 8785 specifies.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
// are serialized as ES does, which is what RFC 8785 specifies. A hole in a
|
|
9
|
+
// list is null, as JSON writes it.
|
|
10
|
+
export const canonical = (v) => {
|
|
11
|
+
if (Array.isArray(v))
|
|
12
|
+
return `[${Array.from(v, (slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`;
|
|
13
|
+
if (v !== null && typeof v === 'object') {
|
|
14
|
+
return `{${Object.keys(v)
|
|
13
15
|
.filter((k) => !absent(v[k]))
|
|
14
16
|
.sort()
|
|
15
17
|
.map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
|
|
16
|
-
.join(',')}}
|
|
17
|
-
|
|
18
|
+
.join(',')}}`;
|
|
19
|
+
}
|
|
20
|
+
return JSON.stringify(v);
|
|
21
|
+
};
|
|
22
|
+
// Hex and the guard are spelled here and again in `src/ward/arithmetic.ts`,
|
|
23
|
+
// which is the price of the boundary: nothing under `src/being` imports
|
|
24
|
+
// anything above it, because this is the whole world a being's own code sees
|
|
25
|
+
// and a being reaching the ward is the thing the shape is against.
|
|
18
26
|
const hex = (bytes) => Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
19
|
-
|
|
27
|
+
// `crypto.subtle` is read at the call and never captured at load. A browser
|
|
28
|
+
// on a plain http:// origin has `crypto` without `subtle`, and a terrain may
|
|
29
|
+
// install one after this module is first imported; either way the failure is
|
|
30
|
+
// one sentence and not a TypeError from inside a digest nobody can read.
|
|
31
|
+
const subtle = () => {
|
|
32
|
+
const s = globalThis.crypto?.subtle;
|
|
33
|
+
if (!s)
|
|
34
|
+
throw new Error('this terrain has no crypto.subtle: a being needs a secure context to be described');
|
|
35
|
+
return s;
|
|
36
|
+
};
|
|
37
|
+
export const digest = async (blueprint) => hex(await subtle().digest('SHA-256', new TextEncoder().encode(canonical(blueprint))));
|
package/dist/being/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Being, type AskSpec } from './being.ts';
|
|
2
|
-
export { silence, isSilence, unreached, isUnreached, word, isWord, wordOf, DOOR_WORDS, isDoorWord } from './silence.ts';
|
|
2
|
+
export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.ts';
|
|
3
3
|
export { digest, canonical } from './digest.ts';
|
|
4
|
-
export { OWNER, PUBLIC, RESERVED_IDS } from './types.ts';
|
|
4
|
+
export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs } from './types.ts';
|
|
5
5
|
export type * from './types.ts';
|
package/dist/being/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// @quo-systems/quo — the Being side. What a being author imports, if anything.
|
|
3
3
|
export { Being } from './being.js';
|
|
4
|
-
export { silence, isSilence, unreached, isUnreached, word, isWord, wordOf, DOOR_WORDS, isDoorWord } from './silence.js';
|
|
4
|
+
export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.js';
|
|
5
5
|
export { digest, canonical } from './digest.js';
|
|
6
|
-
export { OWNER, PUBLIC, RESERVED_IDS } from './types.js';
|
|
6
|
+
export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs } from './types.js';
|
package/dist/being/silence.d.ts
CHANGED
|
@@ -6,5 +6,7 @@ export declare const isDoorWord: (s: unknown) => s is DoorWord;
|
|
|
6
6
|
export declare const word: <W extends WordName>(name: W) => Word<W>;
|
|
7
7
|
export declare const isWord: (x: unknown) => x is Word;
|
|
8
8
|
export declare const wordOf: (x: Word) => WordName;
|
|
9
|
+
export declare const answered: <T>(x: T) => x is Exclude<T, typeof silence | Word>;
|
|
10
|
+
export declare const told: (x: unknown) => unknown;
|
|
9
11
|
export declare const unreached: () => Unreached;
|
|
10
12
|
export declare const isUnreached: (x: unknown) => x is Unreached;
|
package/dist/being/silence.js
CHANGED
|
@@ -22,6 +22,18 @@ export const word = (name) => {
|
|
|
22
22
|
};
|
|
23
23
|
export const isWord = (x) => x !== null && typeof x === 'object' && typeof x[WORD_KEY] === 'string';
|
|
24
24
|
export const wordOf = (x) => x[WORD_KEY];
|
|
25
|
+
// What came back is an answer and not one of the two things that are not
|
|
26
|
+
// answers. A being who only wants the object writes one test instead of
|
|
27
|
+
// three, and the three shapes stay three shapes: silence is no answer, a word
|
|
28
|
+
// is her ward telling her why there is none.
|
|
29
|
+
export const answered = (x) => !isSilence(x) && !isWord(x);
|
|
30
|
+
// What she was told, with a word given its name and everything else left
|
|
31
|
+
// exactly as it came. One value to compare against, so asking which word came
|
|
32
|
+
// back is one test and not two joined by an and: `told(out) === 'late'` says
|
|
33
|
+
// what `isWord(out) && wordOf(out) === 'late'` says, and says which answer
|
|
34
|
+
// arrived instead when it is wrong, where the pair collapses to false and
|
|
35
|
+
// names nothing. Silence stays the symbol it is, and an object stays itself.
|
|
36
|
+
export const told = (x) => (isWord(x) ? wordOf(x) : x);
|
|
25
37
|
// Unreached: no far door was reached. Nothing is known to have been
|
|
26
38
|
// delivered, so asking again is safe. A being cannot produce it: a ward that
|
|
27
39
|
// sees a word come out of a being reads it as her having thrown.
|
package/dist/being/types.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type Invitation = {
|
|
|
18
18
|
heir?: string;
|
|
19
19
|
secret?: string;
|
|
20
20
|
};
|
|
21
|
+
export declare const invitationArgs: (inv: Invitation) => JsonObject;
|
|
21
22
|
export type Schema = JsonObject;
|
|
22
23
|
export type Ask = {
|
|
23
24
|
name: string;
|
|
@@ -29,6 +30,7 @@ export type Blueprint = {
|
|
|
29
30
|
asks: Ask[];
|
|
30
31
|
notes: Json;
|
|
31
32
|
};
|
|
33
|
+
export declare const isBlueprint: (v: unknown) => v is Blueprint;
|
|
32
34
|
export type StandingRecord = {
|
|
33
35
|
id: string;
|
|
34
36
|
digest: string | null;
|
|
@@ -70,14 +72,14 @@ export type Standings = {
|
|
|
70
72
|
readonly [id: string]: Standing | undefined;
|
|
71
73
|
};
|
|
72
74
|
export type Occupants = {
|
|
73
|
-
invite(id: string): Promise<Invitation | null>;
|
|
75
|
+
invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
|
|
74
76
|
remove(id: string): void;
|
|
75
77
|
};
|
|
76
78
|
export type Stance = {
|
|
77
79
|
readonly cells: Cells;
|
|
78
80
|
readonly occupants: Occupants;
|
|
79
81
|
readonly standings: Standings;
|
|
80
|
-
boot(className: string, key: string): string | null
|
|
82
|
+
boot(className: string, key: string, id?: string): Promise<string | null>;
|
|
81
83
|
};
|
|
82
84
|
export interface BeingLike {
|
|
83
85
|
answer(asker: Asker, method?: string, args?: JsonObject): Reply | Promise<Reply>;
|
package/dist/being/types.js
CHANGED
|
@@ -13,4 +13,29 @@
|
|
|
13
13
|
export const OWNER = 'OWNER';
|
|
14
14
|
export const PUBLIC = 'PUBLIC';
|
|
15
15
|
export const RESERVED_IDS = [OWNER, PUBLIC];
|
|
16
|
+
// An invitation as one object of values, which is what it already is. A being
|
|
17
|
+
// who hands one on sends it as args, and args are values; this is the one
|
|
18
|
+
// place that is spelled, so nobody spells it with a cast. Absent stays absent:
|
|
19
|
+
// an invitation to a public being carries no heir and no secret, and a key
|
|
20
|
+
// present and undefined is not the same object once JSON has been through it.
|
|
21
|
+
export const invitationArgs = (inv) => ({
|
|
22
|
+
ward: inv.ward,
|
|
23
|
+
...(inv.heir !== undefined ? { heir: inv.heir } : {}),
|
|
24
|
+
...(inv.secret !== undefined ? { secret: inv.secret } : {}),
|
|
25
|
+
});
|
|
26
|
+
// Whether what came back from an empty ask is a blueprint. Every describe on
|
|
27
|
+
// the far side of a door is somebody else's code, so nothing may be written
|
|
28
|
+
// into a standing's record as a blueprint without being read as one first: a
|
|
29
|
+
// side walks `asks` by name, and a list of anything else, or no list at all,
|
|
30
|
+
// is a side that breaks on a far ward's answer. The asks are checked to the
|
|
31
|
+
// depth a side reads them, a name and an input each, and no further: what a
|
|
32
|
+
// far being puts beside those is hers.
|
|
33
|
+
export const isBlueprint = (v) => {
|
|
34
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v))
|
|
35
|
+
return false;
|
|
36
|
+
const { asks, notes } = v;
|
|
37
|
+
if (!Array.isArray(asks) || notes === undefined)
|
|
38
|
+
return false;
|
|
39
|
+
return asks.every((a) => a !== null && typeof a === 'object' && !Array.isArray(a) && typeof a.name === 'string' && a.input !== null && typeof a.input === 'object' && !Array.isArray(a.input));
|
|
40
|
+
};
|
|
16
41
|
export const WORD_KEY = Symbol.for('quo.word');
|
|
@@ -15,6 +15,15 @@ const show = (v) => {
|
|
|
15
15
|
return 'undefined';
|
|
16
16
|
if (typeof v === 'function')
|
|
17
17
|
return `[function ${v.name}]`;
|
|
18
|
+
// A ward's word is a frozen object under one symbol key, and JSON shows it
|
|
19
|
+
// as `{}`. A red estate chapter saying `{} is not {}` names nothing, so a
|
|
20
|
+
// value whose own keys are all symbols is shown by its keys.
|
|
21
|
+
if (v !== null && typeof v === 'object') {
|
|
22
|
+
const keys = Object.keys(v);
|
|
23
|
+
const syms = Object.getOwnPropertySymbols(v).filter((s) => Object.getOwnPropertyDescriptor(v, s).enumerable);
|
|
24
|
+
if (keys.length === 0 && syms.length > 0)
|
|
25
|
+
return `{ ${syms.map((s) => `${s.toString()}: ${show(v[s])}`).join(', ')} }`;
|
|
26
|
+
}
|
|
18
27
|
try {
|
|
19
28
|
return JSON.stringify(v) ?? Object.prototype.toString.call(v);
|
|
20
29
|
}
|
|
@@ -28,22 +37,47 @@ const fail = (why, message) => {
|
|
|
28
37
|
// Structural, and strict about shape the way `node:assert/strict` is: a value
|
|
29
38
|
// is not equal to one of another kind, and an array is not equal to an object
|
|
30
39
|
// that happens to hold the same keys.
|
|
40
|
+
// The four kinds this compares, and every other object is refused rather than
|
|
41
|
+
// guessed at. A Date, a RegExp, a boxed primitive and a class instance each
|
|
42
|
+
// carry state no key walk can see, so two of them with the same keys are
|
|
43
|
+
// equal here and unequal in node, which is a wrong yes: the one answer a
|
|
44
|
+
// suite must never give. A refusal is a red test that says so.
|
|
45
|
+
const KNOWN = [Object.prototype, null, Array.prototype, Uint8Array.prototype];
|
|
46
|
+
// What node compares: own enumerable keys, strings then enumerable symbols.
|
|
47
|
+
// Not `Reflect.ownKeys`, which also lists what a definition hid.
|
|
48
|
+
const keysOf = (v) => [...Object.keys(v), ...Object.getOwnPropertySymbols(v).filter((s) => Object.getOwnPropertyDescriptor(v, s).enumerable)];
|
|
31
49
|
export function same(a, b) {
|
|
32
50
|
if (Object.is(a, b))
|
|
33
51
|
return true;
|
|
34
52
|
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
35
53
|
return false;
|
|
36
|
-
|
|
54
|
+
const proto = Object.getPrototypeOf(a);
|
|
55
|
+
if (proto !== Object.getPrototypeOf(b))
|
|
37
56
|
return false;
|
|
57
|
+
if (!KNOWN.includes(proto))
|
|
58
|
+
return false; // a Date, a RegExp, a boxed number, a class of hers
|
|
38
59
|
if (Array.isArray(a) !== Array.isArray(b))
|
|
39
60
|
return false;
|
|
40
|
-
if (Array.isArray(a) && Array.isArray(b))
|
|
41
|
-
return a.length === b.length && a.every((x, i) => same(x, b[i]));
|
|
42
61
|
if (a instanceof Uint8Array && b instanceof Uint8Array)
|
|
43
62
|
return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
// An array is its indices and whatever else was hung on it, and a hole is
|
|
64
|
+
// not a present undefined: `[, 1]` and `[undefined, 1]` differ in node and
|
|
65
|
+
// must differ here. `every` skips holes, so the loop is written out.
|
|
66
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
67
|
+
if (a.length !== b.length)
|
|
68
|
+
return false;
|
|
69
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
70
|
+
if (i in a !== i in b)
|
|
71
|
+
return false;
|
|
72
|
+
if (i in a && !same(a[i], b[i]))
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const extra = (v) => keysOf(v).filter((k) => typeof k !== 'string' || !/^(?:0|[1-9]\d*)$/.test(k));
|
|
76
|
+
const xa = extra(a), xb = extra(b);
|
|
77
|
+
const at = (v, k) => v[k];
|
|
78
|
+
return xa.length === xb.length && xa.every((k) => xb.includes(k) && same(at(a, k), at(b, k)));
|
|
79
|
+
}
|
|
80
|
+
const ka = keysOf(a), kb = keysOf(b);
|
|
47
81
|
if (ka.length !== kb.length)
|
|
48
82
|
return false;
|
|
49
83
|
return ka.every((k) => kb.includes(k) && same(a[k], b[k]));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Being } from '../being/being.ts';
|
|
2
|
-
import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
|
|
2
|
+
import type { Asker, Blueprint, Invitation, Json, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
|
|
3
3
|
export declare class Printer extends Being {
|
|
4
4
|
static cells: {
|
|
5
5
|
jobs: JsonObject[];
|
|
@@ -14,8 +14,8 @@ export declare class Printer extends Being {
|
|
|
14
14
|
};
|
|
15
15
|
describe(): Blueprint;
|
|
16
16
|
print({ doc }: JsonObject, asker: Asker): {
|
|
17
|
-
printed: string | number | boolean |
|
|
18
|
-
[key: string]:
|
|
17
|
+
printed: string | number | boolean | Json[] | {
|
|
18
|
+
[key: string]: Json;
|
|
19
19
|
} | null;
|
|
20
20
|
};
|
|
21
21
|
}
|
|
@@ -68,7 +68,7 @@ export declare class Shop extends Being {
|
|
|
68
68
|
error?: undefined;
|
|
69
69
|
retry?: undefined;
|
|
70
70
|
ok: boolean;
|
|
71
|
-
receipt:
|
|
71
|
+
receipt: Json;
|
|
72
72
|
}>;
|
|
73
73
|
refund(): {
|
|
74
74
|
ok: boolean;
|
|
@@ -85,7 +85,9 @@ export declare class Customer extends Being {
|
|
|
85
85
|
};
|
|
86
86
|
};
|
|
87
87
|
};
|
|
88
|
-
join(invitation: Invitation): Promise<typeof import("../being/silence.ts").silence | import("../being/types.ts").Word
|
|
88
|
+
join(invitation: Invitation): Promise<string | number | boolean | typeof import("../being/silence.ts").silence | Json[] | import("../being/types.ts").Word<import("../being/types.ts").WordName> | {
|
|
89
|
+
[key: string]: Json;
|
|
90
|
+
} | null>;
|
|
89
91
|
buy(item: string): Promise<import("../being/types.ts").Answer>;
|
|
90
92
|
learn(): Promise<import("../being/types.ts").Answer>;
|
|
91
93
|
hi(_args: JsonObject, asker: Asker): {
|
|
@@ -97,6 +99,47 @@ export declare class Echo {
|
|
|
97
99
|
constructor(stance: Stance);
|
|
98
100
|
answer(asker: Asker, method?: string, args?: JsonObject): Reply;
|
|
99
101
|
}
|
|
102
|
+
export declare class Maker extends Being {
|
|
103
|
+
static cells: {
|
|
104
|
+
made: string[];
|
|
105
|
+
};
|
|
106
|
+
static asks: {
|
|
107
|
+
open: {
|
|
108
|
+
input: {
|
|
109
|
+
type: string;
|
|
110
|
+
properties: {
|
|
111
|
+
class: {
|
|
112
|
+
type: string;
|
|
113
|
+
};
|
|
114
|
+
key: {
|
|
115
|
+
type: string;
|
|
116
|
+
};
|
|
117
|
+
id: {
|
|
118
|
+
type: string;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
required: string[];
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
say: {
|
|
125
|
+
input: {
|
|
126
|
+
type: string;
|
|
127
|
+
properties: {
|
|
128
|
+
id: {
|
|
129
|
+
type: string;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
required: string[];
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
open({ class: className, key, id }: JsonObject): Promise<{
|
|
137
|
+
made: string | null;
|
|
138
|
+
}>;
|
|
139
|
+
say({ id }: JsonObject): Promise<{
|
|
140
|
+
said: Json;
|
|
141
|
+
}>;
|
|
142
|
+
}
|
|
100
143
|
export declare class Member extends Being {
|
|
101
144
|
static cells: {
|
|
102
145
|
heard: JsonObject[];
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// The fixture beings. Fixed, so that the ward is what varies. A printer, a
|
|
3
|
-
// shop, a customer on the base class, and one raw being
|
|
3
|
+
// shop, a customer on the base class, a maker, a member, and one raw being
|
|
4
|
+
// with no base at all.
|
|
4
5
|
import { Being } from '../being/being.js';
|
|
5
|
-
import {
|
|
6
|
+
import { answered, isUnreached } from '../being/silence.js';
|
|
7
|
+
import { invitationArgs } from '../being/types.js';
|
|
6
8
|
// A printer. Invites whom she is told to, prints for her occupants. Her
|
|
7
9
|
// blueprint lives in her cells so a test can change her shape.
|
|
8
10
|
export class Printer extends Being {
|
|
9
11
|
static cells = { jobs: [], asks: [{ name: 'print', input: { type: 'object' } }] };
|
|
10
12
|
static asks = { print: { input: { type: 'object' } } };
|
|
11
13
|
describe() {
|
|
14
|
+
// Her own cells, which a test writes to change her shape. Cells are Json
|
|
15
|
+
// to the ward, so the asks are named for what she keeps in them.
|
|
12
16
|
return { asks: this.cells.asks, notes: { model: 'LX-2' } };
|
|
13
17
|
}
|
|
14
18
|
print({ doc }, asker) {
|
|
@@ -41,7 +45,7 @@ export class Shop extends Being {
|
|
|
41
45
|
}
|
|
42
46
|
async keepPrinter(invitation) {
|
|
43
47
|
const bp = await this.knock(invitation);
|
|
44
|
-
if (
|
|
48
|
+
if (!answered(bp))
|
|
45
49
|
return false;
|
|
46
50
|
this.cells.printerId = await this.take('prn', invitation);
|
|
47
51
|
return true;
|
|
@@ -50,7 +54,7 @@ export class Shop extends Being {
|
|
|
50
54
|
const rec = this.occupant(asker);
|
|
51
55
|
if (rec?.notes.tier === 'gold' && invitation) {
|
|
52
56
|
const back = await this.knock(invitation, 'hi');
|
|
53
|
-
if (
|
|
57
|
+
if (answered(back))
|
|
54
58
|
await this.take(`vip-${asker.id}`, invitation);
|
|
55
59
|
}
|
|
56
60
|
return { welcome: true };
|
|
@@ -65,7 +69,7 @@ export class Shop extends Being {
|
|
|
65
69
|
const out = await printer.ask('print', { doc: `receipt for ${item}` });
|
|
66
70
|
if (isUnreached(out))
|
|
67
71
|
return { error: 'printer unreachable', retry: true };
|
|
68
|
-
if (
|
|
72
|
+
if (!answered(out))
|
|
69
73
|
return { error: 'printer refused' };
|
|
70
74
|
const st = this.cells.standings[id];
|
|
71
75
|
if (st.seen !== st.digest)
|
|
@@ -83,9 +87,13 @@ export class Customer extends Being {
|
|
|
83
87
|
static cells = { heard: [] };
|
|
84
88
|
static asks = { hi: { input: { type: 'object' } } };
|
|
85
89
|
async join(invitation) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
// A way back for the shop, if she can mint one. She may not: two joins on
|
|
91
|
+
// one invitation race, and the second finds the id taken. Then she offers
|
|
92
|
+
// nothing and knocks all the same, which is what the shop is asked to
|
|
93
|
+
// survive. The old shape said `!` here and was wrong about it.
|
|
94
|
+
const mine = await this.invite('shop-back');
|
|
95
|
+
const out = await this.knock(invitation, 'hello', { invitation: mine === null ? null : invitationArgs(mine) });
|
|
96
|
+
if (!answered(out))
|
|
89
97
|
return out; // nothing was born
|
|
90
98
|
await this.take('shop', invitation); // now, and only now
|
|
91
99
|
return out;
|
|
@@ -116,6 +124,29 @@ export class Echo {
|
|
|
116
124
|
// A member of an estate. Nothing scripted: she invites whom she is told to,
|
|
117
125
|
// knocks where she is told to, and answers `ping` with the asker's own name.
|
|
118
126
|
// The estate chapter drives her, and what it drives is the graph, not her.
|
|
127
|
+
// A being who makes beings. `boot` is on every stance, so it is the ward's
|
|
128
|
+
// promise and not this kit's, and a second kit that never wired it hands its
|
|
129
|
+
// beings a call that answers null for ever. She says what came of it: the key
|
|
130
|
+
// back, or null for a key already taken, a class the harbor does not hold, or
|
|
131
|
+
// a constructor that threw. With an id she also asks for a way back, and then
|
|
132
|
+
// she holds a standing to what she made like any other.
|
|
133
|
+
export class Maker extends Being {
|
|
134
|
+
static cells = { made: [] };
|
|
135
|
+
static asks = {
|
|
136
|
+
open: { input: { type: 'object', properties: { class: { type: 'string' }, key: { type: 'string' }, id: { type: 'string' } }, required: ['class', 'key'] } },
|
|
137
|
+
say: { input: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
|
|
138
|
+
};
|
|
139
|
+
async open({ class: className, key, id }) {
|
|
140
|
+
const made = await this.boot(className, key, id);
|
|
141
|
+
if (made !== null)
|
|
142
|
+
this.cells.made.push(made);
|
|
143
|
+
return { made };
|
|
144
|
+
}
|
|
145
|
+
async say({ id }) {
|
|
146
|
+
const standing = this.standings[id];
|
|
147
|
+
return { said: standing === undefined ? null : (await standing.ask('ping')) };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
119
150
|
export class Member extends Being {
|
|
120
151
|
static cells = { heard: [] };
|
|
121
152
|
static asks = { ping: { input: { type: 'object' } } };
|