@quo-systems/quo 0.1.0 → 0.1.1
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 +9 -6
- package/SPEC.md +51 -24
- package/dist/being/being.d.ts +22 -0
- package/dist/being/being.js +78 -0
- package/dist/being/digest.d.ts +3 -0
- package/dist/being/digest.js +19 -0
- package/dist/being/index.d.ts +5 -0
- package/dist/being/index.js +6 -0
- package/dist/being/silence.d.ts +5 -0
- package/dist/being/silence.js +11 -0
- package/dist/being/types.d.ts +80 -0
- package/dist/being/types.js +16 -0
- package/dist/conformance/assert.d.ts +11 -0
- package/dist/conformance/assert.js +72 -0
- package/dist/conformance/beings.d.ts +114 -0
- package/dist/conformance/beings.js +126 -0
- package/dist/conformance/estate.d.ts +5 -0
- package/dist/conformance/estate.js +310 -0
- package/dist/conformance/index.d.ts +65 -0
- package/dist/conformance/index.js +446 -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 +97 -0
- package/dist/harbor/core.d.ts +41 -0
- package/dist/harbor/core.js +209 -0
- package/dist/harbor/dial.d.ts +8 -0
- package/dist/harbor/dial.js +45 -0
- package/dist/harbor/index.d.ts +6 -0
- package/dist/harbor/index.js +10 -0
- package/dist/harbor/memory.d.ts +20 -0
- package/dist/harbor/memory.js +63 -0
- package/dist/harbor/reach.d.ts +36 -0
- package/dist/harbor/reach.js +166 -0
- package/dist/harbor/store.d.ts +33 -0
- package/dist/harbor/store.js +41 -0
- package/dist/ward/allowance.d.ts +10 -0
- package/dist/ward/allowance.js +60 -0
- package/dist/ward/arithmetic.d.ts +26 -0
- package/dist/ward/arithmetic.js +159 -0
- package/dist/ward/cells.d.ts +3 -0
- package/dist/ward/cells.js +79 -0
- package/dist/ward/door.d.ts +12 -0
- package/dist/ward/door.js +107 -0
- package/dist/ward/ground.d.ts +12 -0
- package/dist/ward/ground.js +1 -0
- package/dist/ward/heirs.d.ts +12 -0
- package/dist/ward/heirs.js +85 -0
- package/dist/ward/index.d.ts +8 -0
- package/dist/ward/index.js +10 -0
- package/dist/ward/owner.d.ts +32 -0
- package/dist/ward/owner.js +114 -0
- package/dist/ward/partition.d.ts +51 -0
- package/dist/ward/partition.js +62 -0
- package/dist/ward/seal.d.ts +48 -0
- package/dist/ward/seal.js +105 -0
- package/dist/ward/stance.d.ts +19 -0
- package/dist/ward/stance.js +250 -0
- package/dist/ward/ward.d.ts +2 -0
- package/dist/ward/ward.js +197 -0
- package/package.json +10 -6
- package/src/harbor/core.ts +45 -4
- package/src/harbor/dial.ts +1 -1
package/README.md
CHANGED
|
@@ -18,8 +18,10 @@ assumes nothing from any other document, this README included. Read it first.
|
|
|
18
18
|
|
|
19
19
|
## This package
|
|
20
20
|
|
|
21
|
-
A TypeScript implementation, run directly on Node's own type stripping
|
|
22
|
-
|
|
21
|
+
A TypeScript implementation, run directly on Node's own type stripping, with
|
|
22
|
+
no dependencies. The package ships JavaScript with declarations, emitted by
|
|
23
|
+
`npm run build` before every gate and every publish, because Node strips
|
|
24
|
+
types nowhere under `node_modules`.
|
|
23
25
|
|
|
24
26
|
```
|
|
25
27
|
src/being/ the Being side: what a being author imports, if anything
|
|
@@ -80,10 +82,11 @@ a TypeScript ward. `SPEC.md` "The tree" says the whole of it.
|
|
|
80
82
|
|
|
81
83
|
Published from 0.1.0, the first version under this name, with no
|
|
82
84
|
compatibility promise before 1.0.0: the words may still move. `npm pack
|
|
83
|
-
--dry-run` shows what ships: the
|
|
84
|
-
licence — no tests, no configs. The spec ships because it
|
|
85
|
-
source and the vectors are read against. `npm publish`
|
|
86
|
-
first, `check` and `check:terrain`, and refuses on a
|
|
85
|
+
--dry-run` shows what ships: the emitted `dist/`, the source, the vectors,
|
|
86
|
+
the spec, and the licence — no tests, no configs. The spec ships because it
|
|
87
|
+
is the truth the source and the vectors are read against. `npm publish`
|
|
88
|
+
runs both gates first, `check` and `check:terrain`, and refuses on a
|
|
89
|
+
failure.
|
|
87
90
|
|
|
88
91
|
## License
|
|
89
92
|
|
package/SPEC.md
CHANGED
|
@@ -1057,7 +1057,8 @@ byte, a four-byte id, the 64-byte pk and the bytes; a reply is the kind, the
|
|
|
1057
1057
|
id and the bytes; and nothing delivered is the kind and the id alone. Both
|
|
1058
1058
|
kinds are written on the standard surface every terrain carries, fetch and
|
|
1059
1059
|
WebSocket, and this tree holds either end of a line and never a listener:
|
|
1060
|
-
who accepts a socket is the terrain's business. A reach reads nothing
|
|
1060
|
+
who accepts a socket is the terrain's business. A reach reads nothing; a
|
|
1061
|
+
harbor opens no box but its own probe's.
|
|
1061
1062
|
Nothing comes back only where the reach knows nothing was delivered: no such
|
|
1062
1063
|
pk at the far end, a connection that would not open, a line already gone. A
|
|
1063
1064
|
reach that sent the bytes and lost the line after answers nothing at all,
|
|
@@ -1099,8 +1100,24 @@ ward the store keeps, keeps the map of ward pk to door for its own wards
|
|
|
1099
1100
|
and the **directory**, pk to reach, for foreign ones, carries bytes to a
|
|
1100
1101
|
pk and delivers bytes from the wire to one door. The directory is filled
|
|
1101
1102
|
three ways, in this order: its own doors; a socket a dialer holds to it,
|
|
1102
|
-
bound
|
|
1103
|
-
hint, a pk at a URL, kept in the store.
|
|
1103
|
+
bound once the door behind the dialer's claim has proved it, and unbound
|
|
1104
|
+
when the line closes; and a hint, a pk at a URL, kept in the store.
|
|
1105
|
+
|
|
1106
|
+
An announce is a claim, and a claim binds nothing until proven: anyone who
|
|
1107
|
+
can reach a listener could otherwise name a pk that is not theirs and take
|
|
1108
|
+
its reachability, the one thing a rendezvous exists to give. The proof is
|
|
1109
|
+
the door as it already is. The harbor mints a lid and sends it down the
|
|
1110
|
+
line with noise after it, an ask for the claimed pk; that is a box that
|
|
1111
|
+
does not open, D1, and the door answers it with silence sealed to the lid
|
|
1112
|
+
and signed by the ward key. Only the holder of that seed writes that reply,
|
|
1113
|
+
the lid is fresh so nothing replays, and a box that does not open writes
|
|
1114
|
+
nothing at the ward. Each claimed pk is proven on its own, both ways: the
|
|
1115
|
+
listener proves the dialer's claims and the dialer the listener's. What a
|
|
1116
|
+
relay that genuinely reaches a ward elsewhere can still do is forward the
|
|
1117
|
+
probe and pass, and then it is a rendezvous for that ward like any other,
|
|
1118
|
+
able to drop and nothing more. This is the one box a harbor ever opens,
|
|
1119
|
+
the one it sealed itself, and it reads nothing from it but whose key
|
|
1120
|
+
signed. One rule makes a **rendezvous** of
|
|
1104
1121
|
any listener: bytes that arrive from the wire go to an own door or to a
|
|
1105
1122
|
socket this harbor holds for that pk, and never onward by request. So a
|
|
1106
1123
|
harbor that cannot be dialed is reached through the one it dialed, by
|
|
@@ -1296,17 +1313,30 @@ class Watcher {
|
|
|
1296
1313
|
## The tree
|
|
1297
1314
|
|
|
1298
1315
|
The repository is one TypeScript implementation of this document, run
|
|
1299
|
-
directly on Node's type stripping, no
|
|
1316
|
+
directly on Node's type stripping, no dependencies. Node 22.18
|
|
1300
1317
|
or later. The package is `@quo-systems/quo`, published from 0.1.0, the
|
|
1301
1318
|
first version under this name, and it starts over: nothing before it under
|
|
1302
1319
|
any other name is this protocol. Before 1.0.0 every version may change the
|
|
1303
|
-
words, and nothing is kept for a holder of an earlier one.
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1320
|
+
words, and nothing is kept for a holder of an earlier one.
|
|
1321
|
+
|
|
1322
|
+
The tree runs as TypeScript and the package ships as JavaScript. Node
|
|
1323
|
+
strips types nowhere under `node_modules`, so a consumer cannot load the
|
|
1324
|
+
source, and 0.1.0, which shipped it, imported nowhere; 0.1.1 is the first
|
|
1325
|
+
version a stranger can import. `npm run build` emits `dist/` from `src`
|
|
1326
|
+
alone, one JavaScript file and one declaration file per source file with
|
|
1327
|
+
every relative import rewritten, and the exports map points there, types
|
|
1328
|
+
beside default. The
|
|
1329
|
+
build runs before every gate and every publish and is never tracked;
|
|
1330
|
+
nothing under `test/` or `estate/` imports it except through the package
|
|
1331
|
+
name, which is how a consumer does. `test/package.test.ts` is the one
|
|
1332
|
+
suite that meets the package as a stranger: it packs the tarball,
|
|
1333
|
+
installs it into an empty folder, and imports every entry point under
|
|
1334
|
+
plain Node. `npm pack --dry-run` shows what ships: `dist/`, the four
|
|
1335
|
+
source folders, the vectors, this document, the README and the licence,
|
|
1336
|
+
and nothing else. This document ships, decided here: it is the truth the
|
|
1337
|
+
source and the vectors are read against, and a kit in another language is
|
|
1338
|
+
written against it and nothing else. Publishing runs both gates first,
|
|
1339
|
+
`npm run check` and `npm run check:terrain`, and refuses on a failure.
|
|
1310
1340
|
|
|
1311
1341
|
```
|
|
1312
1342
|
src/being/ the Being side. types, the base class, silence, the digest.
|
|
@@ -1435,8 +1465,9 @@ passes the same suite. Those tests are the checklist, not the mock.
|
|
|
1435
1465
|
the frames are asserted with no network under them; the conformance
|
|
1436
1466
|
suite against two harbor cores over memory stores reaching each other
|
|
1437
1467
|
in-process, drop and adopt as the migration; a restart from the store
|
|
1438
|
-
with the hints; the dialer over a stubbed line, announce,
|
|
1439
|
-
|
|
1468
|
+
with the hints; the dialer over a stubbed line, announce, a claim proven
|
|
1469
|
+
at a real door before it binds and a claim nobody there holds left
|
|
1470
|
+
unbound, fallback, unbind and the wait before it dials again; and the frames against
|
|
1440
1471
|
`vectors/wire.json`, the ask, the reply, nothing delivered and the
|
|
1441
1472
|
announce, so a kit reproduces the bytes on a socket; and the request
|
|
1442
1473
|
reach against the request record in the same file, over a fetch that
|
|
@@ -1457,6 +1488,12 @@ passes the same suite. Those tests are the checklist, not the mock.
|
|
|
1457
1488
|
one by one; that the arithmetic spends every one of them; and the two ways
|
|
1458
1489
|
a terrain can be short -- no `crypto.subtle` at all, and a subtle without
|
|
1459
1490
|
the curves -- each failing at the first call, in one sentence.
|
|
1491
|
+
- `test/package.test.ts`: the package as a stranger meets it. The tarball
|
|
1492
|
+
is packed, installed into an empty folder with nothing but Node, and
|
|
1493
|
+
every entry point the exports map names is imported. Every other suite
|
|
1494
|
+
reaches the source through a path or a workspace link, which resolves
|
|
1495
|
+
outside `node_modules`, where Node strips types; this is the one that
|
|
1496
|
+
cannot.
|
|
1460
1497
|
- `test/bundle.test.ts`: the three words bundled as a consumer must bundle
|
|
1461
1498
|
them, the artefact read for anything a terrain cannot provide, and then the
|
|
1462
1499
|
whole conformance suite run out of the bundle.
|
|
@@ -1492,17 +1529,7 @@ passes the same suite. Those tests are the checklist, not the mock.
|
|
|
1492
1529
|
The only place this document may name a gap between itself and the tree.
|
|
1493
1530
|
Each line is a debt to close, not a note to keep.
|
|
1494
1531
|
|
|
1495
|
-
|
|
1496
|
-
announces a ward pk is bound to it, and nothing asks whether it holds that
|
|
1497
|
-
ward. So anyone who can reach a rendezvous can announce a pk that is not
|
|
1498
|
-
theirs, and asks for that pk are carried to them instead of to the harbor
|
|
1499
|
-
that holds it. They learn nothing by it: what arrives is sealed to a ward
|
|
1500
|
-
key they do not have, and they cannot answer, because a reply is signed by
|
|
1501
|
-
that ward. What they take is reachability, which is the one thing a
|
|
1502
|
-
rendezvous exists to give. Measured on the lab, 2026-09-04. The shape of
|
|
1503
|
-
the answer is a proof at the announce, the ward signing something the
|
|
1504
|
-
listener chose, so that binding a pk needs the key behind it; that is a
|
|
1505
|
-
decision this document has not taken.
|
|
1532
|
+
None today.
|
|
1506
1533
|
|
|
1507
1534
|
## Open
|
|
1508
1535
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Asker, Blueprint, Cells, Invitation, JsonObject, Wanted, OccupantRecord, Occupants, Reply, Schema, Stance, Standings, Answer } from './types.ts';
|
|
2
|
+
export type AskSpec = {
|
|
3
|
+
description?: string;
|
|
4
|
+
input?: Schema;
|
|
5
|
+
output?: Schema;
|
|
6
|
+
for?: (occupant: OccupantRecord | undefined, asker: Asker) => boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare class Being {
|
|
9
|
+
static cells: JsonObject;
|
|
10
|
+
static asks: Record<string, AskSpec>;
|
|
11
|
+
readonly stance: Stance;
|
|
12
|
+
constructor(stance: Stance);
|
|
13
|
+
get cells(): Cells;
|
|
14
|
+
get standings(): Standings;
|
|
15
|
+
get occupants(): Occupants;
|
|
16
|
+
invite(id: string): Promise<Invitation | null>;
|
|
17
|
+
knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
|
|
18
|
+
take(id: string, invitation: Invitation): Promise<string | null>;
|
|
19
|
+
occupant(asker: Asker): OccupantRecord | undefined;
|
|
20
|
+
describe(asker: Asker): Blueprint;
|
|
21
|
+
answer(asker: Asker, method?: string, args?: JsonObject): Promise<Reply>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Names a subclass may not use for an ask, because they are the base's own.
|
|
2
|
+
const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'constructor']);
|
|
3
|
+
export class Being {
|
|
4
|
+
// Her cells' defaults. Merged in at birth, only where a key is missing, so
|
|
5
|
+
// a restart keeps what she wrote.
|
|
6
|
+
static cells = {};
|
|
7
|
+
// What she can be asked. Declaration order is blueprint order.
|
|
8
|
+
static asks = {};
|
|
9
|
+
stance;
|
|
10
|
+
constructor(stance) {
|
|
11
|
+
this.stance = stance;
|
|
12
|
+
const C = this.constructor;
|
|
13
|
+
for (const name of Object.keys(C.asks)) {
|
|
14
|
+
if (RESERVED.has(name))
|
|
15
|
+
throw new Error(`ask '${name}' is a reserved name`);
|
|
16
|
+
if (typeof this[name] !== 'function')
|
|
17
|
+
throw new Error(`ask '${name}' has no method`);
|
|
18
|
+
}
|
|
19
|
+
for (const [k, v] of Object.entries(C.cells))
|
|
20
|
+
if (!(k in stance.cells))
|
|
21
|
+
stance.cells[k] = structuredClone(v);
|
|
22
|
+
}
|
|
23
|
+
get cells() {
|
|
24
|
+
return this.stance.cells;
|
|
25
|
+
}
|
|
26
|
+
get standings() {
|
|
27
|
+
return this.stance.standings;
|
|
28
|
+
}
|
|
29
|
+
get occupants() {
|
|
30
|
+
return this.stance.occupants;
|
|
31
|
+
}
|
|
32
|
+
invite(id) {
|
|
33
|
+
return this.stance.occupants.invite(id);
|
|
34
|
+
}
|
|
35
|
+
knock(invitation, method, args, wanted) {
|
|
36
|
+
return this.stance.standings.knock(invitation, method, args, wanted);
|
|
37
|
+
}
|
|
38
|
+
take(id, invitation) {
|
|
39
|
+
return this.stance.standings.take(id, invitation);
|
|
40
|
+
}
|
|
41
|
+
// The occupant record for whoever is at the door. Undefined at a public being.
|
|
42
|
+
occupant(asker) {
|
|
43
|
+
return asker.id === undefined ? undefined : this.cells.occupants[asker.id];
|
|
44
|
+
}
|
|
45
|
+
// Her blueprint for this asker. Override to shape it by hand.
|
|
46
|
+
describe(asker) {
|
|
47
|
+
const C = this.constructor;
|
|
48
|
+
const rec = this.occupant(asker);
|
|
49
|
+
const asks = [];
|
|
50
|
+
for (const [name, spec] of Object.entries(C.asks)) {
|
|
51
|
+
if (spec.for && !spec.for(rec, asker))
|
|
52
|
+
continue;
|
|
53
|
+
const ask = { name, input: spec.input ?? { type: 'object' } };
|
|
54
|
+
if (spec.description !== undefined)
|
|
55
|
+
ask.description = spec.description;
|
|
56
|
+
if (spec.output !== undefined)
|
|
57
|
+
ask.output = spec.output;
|
|
58
|
+
asks.push(ask);
|
|
59
|
+
}
|
|
60
|
+
return { asks, notes: {} };
|
|
61
|
+
}
|
|
62
|
+
// The one function. Override to wrap it; call super to keep the dispatch.
|
|
63
|
+
async answer(asker, method, args = {}) {
|
|
64
|
+
if (method === undefined)
|
|
65
|
+
return this.describe(asker);
|
|
66
|
+
const C = this.constructor;
|
|
67
|
+
// Declared, by her, on purpose. `asks` is an ordinary object, so a bare
|
|
68
|
+
// lookup would also find every name on Object's prototype: `valueOf`
|
|
69
|
+
// would answer with her stance, `toString` with a string, and neither is
|
|
70
|
+
// an ask she wrote. Only her own keys are asks, which is what describe
|
|
71
|
+
// has always shown. What she shows is what she can be asked.
|
|
72
|
+
const spec = typeof method === 'string' && Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
|
|
73
|
+
if (!spec || (spec.for && !spec.for(this.occupant(asker), asker)))
|
|
74
|
+
return { error: 'unknown ask' };
|
|
75
|
+
const fn = this[method];
|
|
76
|
+
return fn.call(this, args ?? {}, asker);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// A value I-JSON has no room for: a key left empty, a function, a symbol.
|
|
2
|
+
// None of them cross an edge, so none of them may reach a digest. A key
|
|
3
|
+
// carrying one is dropped and a slot carrying one is null, which is what
|
|
4
|
+
// crossing does to them, so the digest names what arrived and not what she
|
|
5
|
+
// happened to be holding.
|
|
6
|
+
const absent = (v) => v === undefined || typeof v === 'function' || typeof v === 'symbol';
|
|
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
|
+
export const canonical = (v) => Array.isArray(v)
|
|
10
|
+
? `[${v.map((slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`
|
|
11
|
+
: v !== null && typeof v === 'object'
|
|
12
|
+
? `{${Object.keys(v)
|
|
13
|
+
.filter((k) => !absent(v[k]))
|
|
14
|
+
.sort()
|
|
15
|
+
.map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
|
|
16
|
+
.join(',')}}`
|
|
17
|
+
: JSON.stringify(v);
|
|
18
|
+
const hex = (bytes) => Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
19
|
+
export const digest = async (blueprint) => hex(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonical(blueprint))));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Being, type AskSpec } from './being.ts';
|
|
2
|
+
export { silence, isSilence, unreached, isUnreached } from './silence.ts';
|
|
3
|
+
export { digest, canonical } from './digest.ts';
|
|
4
|
+
export { OWNER, PUBLIC, RESERVED_IDS } from './types.ts';
|
|
5
|
+
export type * from './types.ts';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// @quo-systems/quo — the Being side. What a being author imports, if anything.
|
|
3
|
+
export { Being } from './being.js';
|
|
4
|
+
export { silence, isSilence, unreached, isUnreached } from './silence.js';
|
|
5
|
+
export { digest, canonical } from './digest.js';
|
|
6
|
+
export { OWNER, PUBLIC, RESERVED_IDS } from './types.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Unreached } from './types.ts';
|
|
2
|
+
export declare const silence: unique symbol;
|
|
3
|
+
export declare const isSilence: (x: unknown) => x is typeof silence;
|
|
4
|
+
export declare const unreached: () => Unreached;
|
|
5
|
+
export declare const isUnreached: (x: unknown) => x is Unreached;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Silence is one distinguished value. Null is an answer. Silence is no answer.
|
|
3
|
+
import { UNREACHED_KEY } from './types.js';
|
|
4
|
+
export const silence = Symbol.for('quo.silence');
|
|
5
|
+
export const isSilence = (x) => x === silence;
|
|
6
|
+
// Unreached is the ward's own word: no far door was reached. It carries no
|
|
7
|
+
// reason. Safe to retry, because nothing was delivered. A being cannot
|
|
8
|
+
// produce it: a ward that sees it come out of a being treats it as silence.
|
|
9
|
+
const UNREACHED = Object.freeze({ [UNREACHED_KEY]: true });
|
|
10
|
+
export const unreached = () => UNREACHED;
|
|
11
|
+
export const isUnreached = (x) => x !== null && typeof x === 'object' && x[UNREACHED_KEY] === true;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { silence } from './silence.ts';
|
|
2
|
+
export type Json = null | boolean | number | string | Json[] | {
|
|
3
|
+
[key: string]: Json;
|
|
4
|
+
};
|
|
5
|
+
export type JsonObject = {
|
|
6
|
+
[key: string]: Json;
|
|
7
|
+
};
|
|
8
|
+
export type Asker = {
|
|
9
|
+
id: string;
|
|
10
|
+
} | {
|
|
11
|
+
id?: undefined;
|
|
12
|
+
};
|
|
13
|
+
export declare const OWNER = "OWNER";
|
|
14
|
+
export declare const PUBLIC = "PUBLIC";
|
|
15
|
+
export declare const RESERVED_IDS: readonly string[];
|
|
16
|
+
export type Invitation = {
|
|
17
|
+
ward: string;
|
|
18
|
+
heir?: string;
|
|
19
|
+
secret?: string;
|
|
20
|
+
};
|
|
21
|
+
export type Schema = JsonObject;
|
|
22
|
+
export type Ask = {
|
|
23
|
+
name: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
input: Schema;
|
|
26
|
+
output?: Schema;
|
|
27
|
+
};
|
|
28
|
+
export type Blueprint = {
|
|
29
|
+
asks: Ask[];
|
|
30
|
+
notes: Json;
|
|
31
|
+
};
|
|
32
|
+
export type StandingRecord = {
|
|
33
|
+
id: string;
|
|
34
|
+
digest: string | null;
|
|
35
|
+
blueprint: Blueprint | null;
|
|
36
|
+
seen: string | null;
|
|
37
|
+
};
|
|
38
|
+
export type OccupantRecord = {
|
|
39
|
+
id: string;
|
|
40
|
+
notes: JsonObject;
|
|
41
|
+
};
|
|
42
|
+
export type Cells = {
|
|
43
|
+
standings: Record<string, StandingRecord>;
|
|
44
|
+
occupants: Record<string, OccupantRecord>;
|
|
45
|
+
[hers: string]: Json;
|
|
46
|
+
};
|
|
47
|
+
export type Silence = typeof silence;
|
|
48
|
+
export type Unreached = {
|
|
49
|
+
readonly [K in typeof UNREACHED_KEY]: true;
|
|
50
|
+
};
|
|
51
|
+
export declare const UNREACHED_KEY: unique symbol;
|
|
52
|
+
export type Answer = Json | Silence | Unreached;
|
|
53
|
+
export type Reply = Json | Silence;
|
|
54
|
+
export type Wanted = {
|
|
55
|
+
time?: number;
|
|
56
|
+
};
|
|
57
|
+
export type Standing = {
|
|
58
|
+
readonly id: string;
|
|
59
|
+
ask(method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
|
|
60
|
+
};
|
|
61
|
+
export type Standings = {
|
|
62
|
+
knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
|
|
63
|
+
take(id: string, invitation: Invitation): Promise<string | null>;
|
|
64
|
+
remove(id: string): void;
|
|
65
|
+
} & {
|
|
66
|
+
readonly [id: string]: Standing | undefined;
|
|
67
|
+
};
|
|
68
|
+
export type Occupants = {
|
|
69
|
+
invite(id: string): Promise<Invitation | null>;
|
|
70
|
+
remove(id: string): void;
|
|
71
|
+
};
|
|
72
|
+
export type Stance = {
|
|
73
|
+
readonly cells: Cells;
|
|
74
|
+
readonly occupants: Occupants;
|
|
75
|
+
readonly standings: Standings;
|
|
76
|
+
};
|
|
77
|
+
export interface BeingLike {
|
|
78
|
+
answer(asker: Asker, method?: string, args?: JsonObject): Reply | Promise<Reply>;
|
|
79
|
+
}
|
|
80
|
+
export type BeingClass = new (stance: Stance) => BeingLike;
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
export const UNREACHED_KEY = Symbol.for('quo.unreached');
|
|
@@ -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,72 @@
|
|
|
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
|
+
try {
|
|
19
|
+
return JSON.stringify(v) ?? Object.prototype.toString.call(v);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return Object.prototype.toString.call(v); // a cycle, or a null prototype
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const fail = (why, message) => {
|
|
26
|
+
throw new Failed(message ? `${message}: ${why}` : why);
|
|
27
|
+
};
|
|
28
|
+
// Structural, and strict about shape the way `node:assert/strict` is: a value
|
|
29
|
+
// is not equal to one of another kind, and an array is not equal to an object
|
|
30
|
+
// that happens to hold the same keys.
|
|
31
|
+
export function same(a, b) {
|
|
32
|
+
if (Object.is(a, b))
|
|
33
|
+
return true;
|
|
34
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
35
|
+
return false;
|
|
36
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
|
|
37
|
+
return false;
|
|
38
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
39
|
+
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
|
+
if (a instanceof Uint8Array && b instanceof Uint8Array)
|
|
43
|
+
return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
44
|
+
if (a instanceof Map || b instanceof Map || a instanceof Set || b instanceof Set)
|
|
45
|
+
return false; // the suite has none, and a wrong yes is worse than a refusal
|
|
46
|
+
const ka = Reflect.ownKeys(a), kb = Reflect.ownKeys(b);
|
|
47
|
+
if (ka.length !== kb.length)
|
|
48
|
+
return false;
|
|
49
|
+
return ka.every((k) => kb.includes(k) && same(a[k], b[k]));
|
|
50
|
+
}
|
|
51
|
+
export const assert = {
|
|
52
|
+
ok(value, message) {
|
|
53
|
+
if (!value)
|
|
54
|
+
fail(`${show(value)} is not truthy`, message);
|
|
55
|
+
},
|
|
56
|
+
equal(actual, expected, message) {
|
|
57
|
+
if (!Object.is(actual, expected))
|
|
58
|
+
fail(`${show(actual)} !== ${show(expected)}`, message);
|
|
59
|
+
},
|
|
60
|
+
notEqual(actual, expected, message) {
|
|
61
|
+
if (Object.is(actual, expected))
|
|
62
|
+
fail(`${show(actual)} === ${show(expected)}`, message);
|
|
63
|
+
},
|
|
64
|
+
deepEqual(actual, expected, message) {
|
|
65
|
+
if (!same(actual, expected))
|
|
66
|
+
fail(`${show(actual)} is not ${show(expected)}`, message);
|
|
67
|
+
},
|
|
68
|
+
match(actual, re, message) {
|
|
69
|
+
if (typeof actual !== 'string' || !re.test(actual))
|
|
70
|
+
fail(`${show(actual)} does not match ${String(re)}`, message);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Being } from '../being/being.ts';
|
|
2
|
+
import type { Asker, Blueprint, Invitation, 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 | import("../being/types.ts").Json[] | {
|
|
18
|
+
[key: string]: import("../being/types.ts").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: import("../being/types.ts").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<typeof import("../being/silence.ts").silence | import("../being/types.ts").Unreached | import("../being/types.ts").Json>;
|
|
89
|
+
buy(item: string): Promise<import("../being/types.ts").Answer>;
|
|
90
|
+
learn(): Promise<import("../being/types.ts").Answer>;
|
|
91
|
+
hi(_args: JsonObject, asker: Asker): {
|
|
92
|
+
heard: string;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export declare class Echo {
|
|
96
|
+
s: Stance;
|
|
97
|
+
constructor(stance: Stance);
|
|
98
|
+
answer(asker: Asker, method?: string, args?: JsonObject): Reply;
|
|
99
|
+
}
|
|
100
|
+
export declare class Member extends Being {
|
|
101
|
+
static cells: {
|
|
102
|
+
heard: JsonObject[];
|
|
103
|
+
};
|
|
104
|
+
static asks: {
|
|
105
|
+
ping: {
|
|
106
|
+
input: {
|
|
107
|
+
type: string;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
ping(_args: JsonObject, asker: Asker): {
|
|
112
|
+
pong: string | null;
|
|
113
|
+
};
|
|
114
|
+
}
|