nervur 0.16.0 → 0.17.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/GETTING_STARTED.md +138 -0
- package/NOTICE +1 -1
- package/README.md +108 -20
- package/dist/being/being.d.ts +24 -0
- package/dist/being/being.js +109 -0
- package/dist/being/digest.d.ts +3 -0
- package/dist/being/digest.js +37 -0
- package/dist/being/index.d.ts +5 -0
- package/dist/being/index.js +6 -0
- package/dist/being/silence.d.ts +12 -0
- package/dist/being/silence.js +41 -0
- package/dist/being/types.d.ts +89 -0
- package/dist/being/types.js +58 -0
- package/dist/conformance/assert.d.ts +11 -0
- package/dist/conformance/assert.js +106 -0
- package/dist/conformance/beings.d.ts +198 -0
- package/dist/conformance/beings.js +183 -0
- package/dist/conformance/estate.d.ts +5 -0
- package/dist/conformance/estate.js +388 -0
- package/dist/conformance/index.d.ts +81 -0
- package/dist/conformance/index.js +816 -0
- package/dist/conformance/reach.d.ts +10 -0
- package/dist/conformance/reach.js +72 -0
- package/dist/conformance/store.d.ts +5 -0
- package/dist/conformance/store.js +123 -0
- package/dist/harbor/core.d.ts +53 -0
- package/dist/harbor/core.js +621 -0
- package/dist/harbor/dial.d.ts +9 -0
- package/dist/harbor/dial.js +81 -0
- package/dist/harbor/index.d.ts +8 -0
- package/dist/harbor/index.js +16 -0
- package/dist/harbor/memory.d.ts +25 -0
- package/dist/harbor/memory.js +72 -0
- package/dist/harbor/reach.d.ts +36 -0
- package/dist/harbor/reach.js +191 -0
- package/dist/harbor/store.d.ts +40 -0
- package/dist/harbor/store.js +70 -0
- package/dist/vector/cases.d.ts +41 -0
- package/dist/vector/cases.js +195 -0
- package/dist/vector/index.d.ts +6 -0
- package/dist/vector/index.js +8 -0
- package/dist/vector/stand.d.ts +9 -0
- package/dist/vector/stand.js +76 -0
- package/dist/vector/world.d.ts +143 -0
- package/dist/vector/world.js +198 -0
- package/dist/ward/allowance.d.ts +10 -0
- package/dist/ward/allowance.js +71 -0
- package/dist/ward/arithmetic.d.ts +31 -0
- package/dist/ward/arithmetic.js +245 -0
- package/dist/ward/cells.d.ts +7 -0
- package/dist/ward/cells.js +186 -0
- package/dist/ward/door.d.ts +18 -0
- package/dist/ward/door.js +186 -0
- package/dist/ward/ground.d.ts +23 -0
- package/dist/ward/ground.js +38 -0
- package/dist/ward/heirs.d.ts +13 -0
- package/dist/ward/heirs.js +115 -0
- package/dist/ward/index.d.ts +9 -0
- package/dist/ward/index.js +13 -0
- package/dist/ward/owner.d.ts +13 -0
- package/dist/ward/owner.js +220 -0
- package/dist/ward/partition.d.ts +65 -0
- package/dist/ward/partition.js +295 -0
- package/dist/ward/seal.d.ts +52 -0
- package/dist/ward/seal.js +145 -0
- package/dist/ward/stance.d.ts +25 -0
- package/dist/ward/stance.js +413 -0
- package/dist/ward/ward.d.ts +12 -0
- package/dist/ward/ward.js +361 -0
- package/package.json +37 -15
- package/protocol/SPEC.md +1843 -0
- package/protocol/vectors/arithmetic.json +117 -0
- package/protocol/vectors/door.json +345 -0
- package/protocol/vectors/framing.json +92 -0
- package/protocol/vectors/wire.json +48 -0
- package/quo-kit.md +652 -0
- package/src/being/being.ts +123 -0
- package/src/being/digest.ts +46 -0
- package/src/being/index.ts +7 -0
- package/src/being/silence.ts +46 -0
- package/src/being/types.ts +170 -0
- package/src/conformance/assert.ts +100 -0
- package/src/conformance/beings.ts +183 -0
- package/src/conformance/estate.ts +412 -0
- package/src/conformance/index.ts +962 -0
- package/src/conformance/reach.ts +83 -0
- package/src/conformance/store.ts +136 -0
- package/src/harbor/core.ts +660 -0
- package/src/harbor/dial.ts +112 -0
- package/src/harbor/index.ts +17 -0
- package/src/harbor/memory.ts +83 -0
- package/src/harbor/reach.ts +215 -0
- package/src/harbor/store.ts +101 -0
- package/src/vector/cases.ts +229 -0
- package/src/vector/index.ts +11 -0
- package/src/vector/stand.ts +75 -0
- package/src/vector/world.ts +220 -0
- package/src/ward/allowance.ts +85 -0
- package/src/ward/arithmetic.ts +247 -0
- package/src/ward/cells.ts +190 -0
- package/src/ward/door.ts +186 -0
- package/src/ward/ground.ts +160 -0
- package/src/ward/heirs.ts +114 -0
- package/src/ward/index.ts +17 -0
- package/src/ward/owner.ts +214 -0
- package/src/ward/partition.ts +353 -0
- package/src/ward/seal.ts +174 -0
- package/src/ward/stance.ts +433 -0
- package/src/ward/ward.ts +378 -0
- package/index.js +0 -17
- package/nervur.js +0 -5
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The ids the ward speaks under, and no being may mint. An asker id is hers:
|
|
2
|
+
// she chooses the strings, and the ward has no business in her namespace. But
|
|
3
|
+
// the ward must sometimes stand at her door itself — its describe asks every
|
|
4
|
+
// being under OWNER — and an id it shares with one of her occupants is two
|
|
5
|
+
// different parties wearing one name. She could not tell them apart, and the
|
|
6
|
+
// ward would shape its describe through an occupant's gate.
|
|
7
|
+
//
|
|
8
|
+
// So the ward's words are shouted, and refused at the mint. A word can only
|
|
9
|
+
// be reserved before anyone has used it: a partition already holding an
|
|
10
|
+
// occupant of that name could never have it taken back. PUBLIC guards nothing
|
|
11
|
+
// today — a public asker is {} and carries no id at all — and that is exactly
|
|
12
|
+
// why it is claimed now, while claiming it is free.
|
|
13
|
+
export const OWNER = 'OWNER';
|
|
14
|
+
export const PUBLIC = 'PUBLIC';
|
|
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 a value that arrived is an invitation. The shape is the spec's, so
|
|
27
|
+
// the reading of it is too, and it is one reading: a form that
|
|
28
|
+
// asks a guest for one, a faculty handed one to wake her maker by, and a
|
|
29
|
+
// shell that finds one in a link all ask the same question, and a kinder
|
|
30
|
+
// answer in one of them is a value that fails at a door instead of at the
|
|
31
|
+
// edge it came in by. A ward pk is a hundred and twenty-eight lowercase hex,
|
|
32
|
+
// and a heir comes with its secret or neither comes: an invitation to a
|
|
33
|
+
// public being carries no heir, and a heir with no secret opens nothing.
|
|
34
|
+
export const isInvitation = (v) => {
|
|
35
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v))
|
|
36
|
+
return false;
|
|
37
|
+
const o = v;
|
|
38
|
+
if (typeof o.ward !== 'string' || !/^[0-9a-f]{128}$/.test(o.ward))
|
|
39
|
+
return false;
|
|
40
|
+
const heir = typeof o.heir === 'string', secret = typeof o.secret === 'string';
|
|
41
|
+
return (heir && secret) || (!heir && !secret && !('heir' in o) && !('secret' in o));
|
|
42
|
+
};
|
|
43
|
+
// Whether what came back from an empty ask is a blueprint. Every describe on
|
|
44
|
+
// the far side of a door is somebody else's code, so nothing may be written
|
|
45
|
+
// into a standing's record as a blueprint without being read as one first: a
|
|
46
|
+
// side walks `asks` by name, and a list of anything else, or no list at all,
|
|
47
|
+
// is a side that breaks on a far ward's answer. The asks are checked to the
|
|
48
|
+
// depth a side reads them, a name and an input each, and no further: what a
|
|
49
|
+
// far being puts beside those is hers.
|
|
50
|
+
export const isBlueprint = (v) => {
|
|
51
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v))
|
|
52
|
+
return false;
|
|
53
|
+
const { asks, notes } = v;
|
|
54
|
+
if (!Array.isArray(asks) || notes === undefined)
|
|
55
|
+
return false;
|
|
56
|
+
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));
|
|
57
|
+
};
|
|
58
|
+
export const WORD_KEY = Symbol.for('quo.word');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class Failed extends Error {
|
|
2
|
+
}
|
|
3
|
+
export declare function same(a: unknown, b: unknown): boolean;
|
|
4
|
+
export type Assert = {
|
|
5
|
+
ok(value: unknown, message?: string): asserts value;
|
|
6
|
+
equal(actual: unknown, expected: unknown, message?: string): void;
|
|
7
|
+
notEqual(actual: unknown, expected: unknown, message?: string): void;
|
|
8
|
+
deepEqual(actual: unknown, expected: unknown, message?: string): void;
|
|
9
|
+
match(actual: string, re: RegExp, message?: string): void;
|
|
10
|
+
};
|
|
11
|
+
export declare const assert: Assert;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The five assertions the conformance suite makes, and no more. Node has all
|
|
3
|
+
// five in `node:assert/strict`, and naming that module would tie the suite to
|
|
4
|
+
// one terrain -- the suite is the ward's truth, and the ward's truth has to be
|
|
5
|
+
// checkable wherever a ward runs. So they are written here, in the language
|
|
6
|
+
// alone, and `test/assert.test.ts` holds them to Node's own behaviour.
|
|
7
|
+
export class Failed extends Error {
|
|
8
|
+
}
|
|
9
|
+
const show = (v) => {
|
|
10
|
+
if (typeof v === 'symbol')
|
|
11
|
+
return v.toString();
|
|
12
|
+
if (typeof v === 'bigint')
|
|
13
|
+
return `${v}n`;
|
|
14
|
+
if (v === undefined)
|
|
15
|
+
return 'undefined';
|
|
16
|
+
if (typeof v === 'function')
|
|
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
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return JSON.stringify(v) ?? Object.prototype.toString.call(v);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return Object.prototype.toString.call(v); // a cycle, or a null prototype
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const fail = (why, message) => {
|
|
35
|
+
throw new Failed(message ? `${message}: ${why}` : why);
|
|
36
|
+
};
|
|
37
|
+
// Structural, and strict about shape the way `node:assert/strict` is: a value
|
|
38
|
+
// is not equal to one of another kind, and an array is not equal to an object
|
|
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)];
|
|
49
|
+
export function same(a, b) {
|
|
50
|
+
if (Object.is(a, b))
|
|
51
|
+
return true;
|
|
52
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
53
|
+
return false;
|
|
54
|
+
const proto = Object.getPrototypeOf(a);
|
|
55
|
+
if (proto !== Object.getPrototypeOf(b))
|
|
56
|
+
return false;
|
|
57
|
+
if (!KNOWN.includes(proto))
|
|
58
|
+
return false; // a Date, a RegExp, a boxed number, a class of hers
|
|
59
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
60
|
+
return false;
|
|
61
|
+
if (a instanceof Uint8Array && b instanceof Uint8Array)
|
|
62
|
+
return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
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);
|
|
81
|
+
if (ka.length !== kb.length)
|
|
82
|
+
return false;
|
|
83
|
+
return ka.every((k) => kb.includes(k) && same(a[k], b[k]));
|
|
84
|
+
}
|
|
85
|
+
export const assert = {
|
|
86
|
+
ok(value, message) {
|
|
87
|
+
if (!value)
|
|
88
|
+
fail(`${show(value)} is not truthy`, message);
|
|
89
|
+
},
|
|
90
|
+
equal(actual, expected, message) {
|
|
91
|
+
if (!Object.is(actual, expected))
|
|
92
|
+
fail(`${show(actual)} !== ${show(expected)}`, message);
|
|
93
|
+
},
|
|
94
|
+
notEqual(actual, expected, message) {
|
|
95
|
+
if (Object.is(actual, expected))
|
|
96
|
+
fail(`${show(actual)} === ${show(expected)}`, message);
|
|
97
|
+
},
|
|
98
|
+
deepEqual(actual, expected, message) {
|
|
99
|
+
if (!same(actual, expected))
|
|
100
|
+
fail(`${show(actual)} is not ${show(expected)}`, message);
|
|
101
|
+
},
|
|
102
|
+
match(actual, re, message) {
|
|
103
|
+
if (typeof actual !== 'string' || !re.test(actual))
|
|
104
|
+
fail(`${show(actual)} does not match ${String(re)}`, message);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { Being } from '../being/being.ts';
|
|
2
|
+
import type { Asker, Blueprint, Invitation, Json, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
|
|
3
|
+
export declare class Printer extends Being {
|
|
4
|
+
static cells: {
|
|
5
|
+
jobs: JsonObject[];
|
|
6
|
+
asks: JsonObject[];
|
|
7
|
+
};
|
|
8
|
+
static asks: {
|
|
9
|
+
print: {
|
|
10
|
+
input: {
|
|
11
|
+
type: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
describe(): Blueprint;
|
|
16
|
+
print({ doc }: JsonObject, asker: Asker): {
|
|
17
|
+
printed: string | number | boolean | Json[] | {
|
|
18
|
+
[key: string]: Json;
|
|
19
|
+
} | null;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export declare class Shop extends Being {
|
|
23
|
+
static cells: {
|
|
24
|
+
printerId: string | null;
|
|
25
|
+
sales: JsonObject[];
|
|
26
|
+
minted: number;
|
|
27
|
+
};
|
|
28
|
+
static asks: {
|
|
29
|
+
hello: {
|
|
30
|
+
input: {
|
|
31
|
+
type: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
buy: {
|
|
35
|
+
input: {
|
|
36
|
+
type: string;
|
|
37
|
+
properties: {
|
|
38
|
+
item: {
|
|
39
|
+
type: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
refund: {
|
|
45
|
+
input: {
|
|
46
|
+
type: string;
|
|
47
|
+
};
|
|
48
|
+
for: (rec: OccupantRecord | undefined) => boolean;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
answer(asker: Asker, method?: string, args?: JsonObject): Promise<Reply>;
|
|
52
|
+
invite(tier?: string): Promise<Invitation>;
|
|
53
|
+
keepPrinter(invitation: Invitation): Promise<boolean>;
|
|
54
|
+
hello({ invitation }: JsonObject, asker: Asker): Promise<{
|
|
55
|
+
welcome: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
buy({ item }: JsonObject, asker: Asker): Promise<{
|
|
58
|
+
error: string;
|
|
59
|
+
retry?: undefined;
|
|
60
|
+
ok?: undefined;
|
|
61
|
+
receipt?: undefined;
|
|
62
|
+
} | {
|
|
63
|
+
error: string;
|
|
64
|
+
retry: boolean;
|
|
65
|
+
ok?: undefined;
|
|
66
|
+
receipt?: undefined;
|
|
67
|
+
} | {
|
|
68
|
+
error?: undefined;
|
|
69
|
+
retry?: undefined;
|
|
70
|
+
ok: boolean;
|
|
71
|
+
receipt: Json;
|
|
72
|
+
}>;
|
|
73
|
+
refund(): {
|
|
74
|
+
ok: boolean;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export declare class Customer extends Being {
|
|
78
|
+
static cells: {
|
|
79
|
+
heard: JsonObject[];
|
|
80
|
+
};
|
|
81
|
+
static asks: {
|
|
82
|
+
hi: {
|
|
83
|
+
input: {
|
|
84
|
+
type: string;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
};
|
|
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>;
|
|
91
|
+
buy(item: string): Promise<import("../being/types.ts").Answer>;
|
|
92
|
+
learn(): Promise<import("../being/types.ts").Answer>;
|
|
93
|
+
hi(_args: JsonObject, asker: Asker): {
|
|
94
|
+
heard: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export declare class Echo {
|
|
98
|
+
s: Stance;
|
|
99
|
+
constructor(stance: Stance);
|
|
100
|
+
answer(asker: Asker, method?: string, args?: JsonObject): Reply;
|
|
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
|
+
borrow: {
|
|
136
|
+
input: {
|
|
137
|
+
type: string;
|
|
138
|
+
properties: {
|
|
139
|
+
name: {
|
|
140
|
+
type: string;
|
|
141
|
+
};
|
|
142
|
+
id: {
|
|
143
|
+
type: string;
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
required: string[];
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
open({ class: className, key, id }: JsonObject): Promise<{
|
|
151
|
+
made: string | null;
|
|
152
|
+
}>;
|
|
153
|
+
say({ id }: JsonObject): Promise<{
|
|
154
|
+
said: Json;
|
|
155
|
+
}>;
|
|
156
|
+
borrow({ name, id }: JsonObject): Promise<{
|
|
157
|
+
lent: string | null;
|
|
158
|
+
}>;
|
|
159
|
+
}
|
|
160
|
+
export declare class Member extends Being {
|
|
161
|
+
static cells: {
|
|
162
|
+
heard: JsonObject[];
|
|
163
|
+
};
|
|
164
|
+
static asks: {
|
|
165
|
+
ping: {
|
|
166
|
+
input: {
|
|
167
|
+
type: string;
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
relay: {
|
|
171
|
+
input: {
|
|
172
|
+
type: string;
|
|
173
|
+
properties: {
|
|
174
|
+
id: {
|
|
175
|
+
type: string;
|
|
176
|
+
};
|
|
177
|
+
method: {
|
|
178
|
+
type: string;
|
|
179
|
+
};
|
|
180
|
+
time: {
|
|
181
|
+
type: string;
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
required: string[];
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
ping(_args: JsonObject, asker: Asker): {
|
|
189
|
+
pong: string | null;
|
|
190
|
+
};
|
|
191
|
+
relay({ id, method, args, time }: JsonObject): Promise<{
|
|
192
|
+
said: string;
|
|
193
|
+
out: null;
|
|
194
|
+
} | {
|
|
195
|
+
said: null;
|
|
196
|
+
out: Json;
|
|
197
|
+
}>;
|
|
198
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The fixture beings. Fixed, so that the ward is what varies. A printer, a
|
|
3
|
+
// shop, a customer on the base class, a maker, a member, and one raw being
|
|
4
|
+
// with no base at all.
|
|
5
|
+
import { Being } from '../being/being.js';
|
|
6
|
+
import { answered, isSilence, isUnreached, isWord, wordOf } from '../being/silence.js';
|
|
7
|
+
import { invitationArgs } from '../being/types.js';
|
|
8
|
+
// A printer. Invites whom she is told to, prints for her occupants. Her
|
|
9
|
+
// blueprint lives in her cells so a test can change her shape.
|
|
10
|
+
export class Printer extends Being {
|
|
11
|
+
static cells = { jobs: [], asks: [{ name: 'print', input: { type: 'object' } }] };
|
|
12
|
+
static asks = { print: { input: { type: 'object' } } };
|
|
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.
|
|
16
|
+
return { asks: this.cells.asks, notes: { model: 'LX-2' } };
|
|
17
|
+
}
|
|
18
|
+
print({ doc }, asker) {
|
|
19
|
+
this.cells.jobs.push({ by: asker.id ?? null, doc: doc ?? null });
|
|
20
|
+
return { printed: doc ?? null };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// A shop. Invites customers in her own time. Keeps a printer as a standing.
|
|
24
|
+
// Takes a gold customer back if the customer offers a way. Counts every ask
|
|
25
|
+
// per occupant, by wrapping answer.
|
|
26
|
+
export class Shop extends Being {
|
|
27
|
+
static cells = { printerId: null, sales: [], minted: 0 };
|
|
28
|
+
static asks = {
|
|
29
|
+
hello: { input: { type: 'object' } },
|
|
30
|
+
buy: { input: { type: 'object', properties: { item: { type: 'string' } } } },
|
|
31
|
+
refund: { input: { type: 'object' }, for: (rec) => rec?.notes.tier === 'gold' },
|
|
32
|
+
};
|
|
33
|
+
async answer(asker, method, args) {
|
|
34
|
+
const rec = this.occupant(asker);
|
|
35
|
+
if (rec && method !== undefined)
|
|
36
|
+
rec.notes.asks = (Number(rec.notes.asks) || 0) + 1;
|
|
37
|
+
return super.answer(asker, method, args);
|
|
38
|
+
}
|
|
39
|
+
// Outside any ask. She mints, her ward seals, she hands it out by mail.
|
|
40
|
+
async invite(tier = 'plain') {
|
|
41
|
+
const id = `cust${++this.cells.minted}`;
|
|
42
|
+
const inv = (await super.invite(id));
|
|
43
|
+
this.cells.occupants[id].notes.tier = tier;
|
|
44
|
+
return inv;
|
|
45
|
+
}
|
|
46
|
+
async keepPrinter(invitation) {
|
|
47
|
+
const bp = await this.knock(invitation);
|
|
48
|
+
if (!answered(bp))
|
|
49
|
+
return false;
|
|
50
|
+
this.cells.printerId = await this.take('prn', invitation);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
async hello({ invitation }, asker) {
|
|
54
|
+
const rec = this.occupant(asker);
|
|
55
|
+
if (rec?.notes.tier === 'gold' && invitation) {
|
|
56
|
+
const back = await this.knock(invitation, 'hi');
|
|
57
|
+
if (answered(back))
|
|
58
|
+
await this.take(`vip-${asker.id}`, invitation);
|
|
59
|
+
}
|
|
60
|
+
return { welcome: true };
|
|
61
|
+
}
|
|
62
|
+
async buy({ item }, asker) {
|
|
63
|
+
if (typeof item !== 'string')
|
|
64
|
+
return { error: 'no such item' }; // args are hers to check
|
|
65
|
+
const id = this.cells.printerId;
|
|
66
|
+
const printer = id ? this.standings[id] : undefined;
|
|
67
|
+
if (!printer)
|
|
68
|
+
return { error: 'no printer' };
|
|
69
|
+
const out = await printer.ask('print', { doc: `receipt for ${item}` });
|
|
70
|
+
if (isUnreached(out))
|
|
71
|
+
return { error: 'printer unreachable', retry: true };
|
|
72
|
+
if (!answered(out))
|
|
73
|
+
return { error: 'printer refused' };
|
|
74
|
+
const st = this.cells.standings[id];
|
|
75
|
+
if (st.seen !== st.digest)
|
|
76
|
+
this.occupant(asker).notes.printerChanged = true;
|
|
77
|
+
this.cells.sales.push({ item: item ?? null, by: asker.id ?? null });
|
|
78
|
+
return { ok: true, receipt: out.printed };
|
|
79
|
+
}
|
|
80
|
+
refund() {
|
|
81
|
+
return { ok: true };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// A customer. Consumes an invitation, and only then decides to keep the
|
|
85
|
+
// shop. Offers the shop a way back so the shop can reach her.
|
|
86
|
+
export class Customer extends Being {
|
|
87
|
+
static cells = { heard: [] };
|
|
88
|
+
static asks = { hi: { input: { type: 'object' } } };
|
|
89
|
+
async join(invitation) {
|
|
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))
|
|
97
|
+
return out; // nothing was born
|
|
98
|
+
await this.take('shop', invitation); // now, and only now
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
buy(item) {
|
|
102
|
+
return this.standings.shop.ask('buy', { item });
|
|
103
|
+
}
|
|
104
|
+
learn() {
|
|
105
|
+
return this.standings.shop.ask();
|
|
106
|
+
}
|
|
107
|
+
hi(_args, asker) {
|
|
108
|
+
this.cells.heard.push({ from: asker.id ?? null, method: 'hi' });
|
|
109
|
+
return { heard: 'hi' };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// The raw shape. No base class, no import from the kit. A being all the same.
|
|
113
|
+
export class Echo {
|
|
114
|
+
s;
|
|
115
|
+
constructor(stance) {
|
|
116
|
+
this.s = stance;
|
|
117
|
+
}
|
|
118
|
+
answer(asker, method, args = {}) {
|
|
119
|
+
if (method === undefined)
|
|
120
|
+
return { asks: [{ name: 'echo', input: {} }], notes: {} };
|
|
121
|
+
return { from: asker.id ?? null, ...args };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// A member of an estate. Nothing scripted: she invites whom she is told to,
|
|
125
|
+
// knocks where she is told to, and answers `ping` with the asker's own name.
|
|
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
|
+
borrow: { input: { type: 'object', properties: { name: { type: 'string' }, id: { type: 'string' } }, required: ['name', 'id'] } },
|
|
139
|
+
};
|
|
140
|
+
async open({ class: className, key, id }) {
|
|
141
|
+
const made = await this.boot(className, key, id);
|
|
142
|
+
if (made !== null)
|
|
143
|
+
this.cells.made.push(made);
|
|
144
|
+
return { made };
|
|
145
|
+
}
|
|
146
|
+
async say({ id }) {
|
|
147
|
+
const standing = this.standings[id];
|
|
148
|
+
return { said: standing === undefined ? null : (await standing.ask('ping')) };
|
|
149
|
+
}
|
|
150
|
+
// Making's other half: a standing at something the box already has, by the
|
|
151
|
+
// name the box knows it under. She is handed the id and never the value.
|
|
152
|
+
async borrow({ name, id }) {
|
|
153
|
+
return { lent: await this.lend(name, id) };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export class Member extends Being {
|
|
157
|
+
static cells = { heard: [] };
|
|
158
|
+
static asks = {
|
|
159
|
+
ping: { input: { type: 'object' } },
|
|
160
|
+
relay: { input: { type: 'object', properties: { id: { type: 'string' }, method: { type: 'string' }, time: { type: 'number' } }, required: ['id'] } },
|
|
161
|
+
};
|
|
162
|
+
ping(_args, asker) {
|
|
163
|
+
this.cells.heard.push({ from: asker.id ?? null });
|
|
164
|
+
return { pong: asker.id ?? null };
|
|
165
|
+
}
|
|
166
|
+
// She asks on one of her standings from inside her own answer, and says
|
|
167
|
+
// what came back as a word rather than as the word itself, since a word is
|
|
168
|
+
// her ward's value and never a thing that crosses. `time` is what she is
|
|
169
|
+
// willing to wait, which is the whole of what the third argument is for.
|
|
170
|
+
// Two members holding each other and relaying make a cycle, and a cycle is
|
|
171
|
+
// how a suite that owns no clock can see a wait end.
|
|
172
|
+
async relay({ id, method, args, time }) {
|
|
173
|
+
const standing = this.standings[id];
|
|
174
|
+
if (standing === undefined)
|
|
175
|
+
return { said: 'no standing', out: null };
|
|
176
|
+
const out = await standing.ask(method, (args ?? {}), time === undefined ? undefined : { time: time });
|
|
177
|
+
if (isSilence(out))
|
|
178
|
+
return { said: 'silence', out: null };
|
|
179
|
+
if (isWord(out))
|
|
180
|
+
return { said: wordOf(out), out: null };
|
|
181
|
+
return { said: null, out: out };
|
|
182
|
+
}
|
|
183
|
+
}
|