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.
Files changed (111) hide show
  1. package/GETTING_STARTED.md +138 -0
  2. package/NOTICE +1 -1
  3. package/README.md +108 -20
  4. package/dist/being/being.d.ts +24 -0
  5. package/dist/being/being.js +109 -0
  6. package/dist/being/digest.d.ts +3 -0
  7. package/dist/being/digest.js +37 -0
  8. package/dist/being/index.d.ts +5 -0
  9. package/dist/being/index.js +6 -0
  10. package/dist/being/silence.d.ts +12 -0
  11. package/dist/being/silence.js +41 -0
  12. package/dist/being/types.d.ts +89 -0
  13. package/dist/being/types.js +58 -0
  14. package/dist/conformance/assert.d.ts +11 -0
  15. package/dist/conformance/assert.js +106 -0
  16. package/dist/conformance/beings.d.ts +198 -0
  17. package/dist/conformance/beings.js +183 -0
  18. package/dist/conformance/estate.d.ts +5 -0
  19. package/dist/conformance/estate.js +388 -0
  20. package/dist/conformance/index.d.ts +81 -0
  21. package/dist/conformance/index.js +816 -0
  22. package/dist/conformance/reach.d.ts +10 -0
  23. package/dist/conformance/reach.js +72 -0
  24. package/dist/conformance/store.d.ts +5 -0
  25. package/dist/conformance/store.js +123 -0
  26. package/dist/harbor/core.d.ts +53 -0
  27. package/dist/harbor/core.js +621 -0
  28. package/dist/harbor/dial.d.ts +9 -0
  29. package/dist/harbor/dial.js +81 -0
  30. package/dist/harbor/index.d.ts +8 -0
  31. package/dist/harbor/index.js +16 -0
  32. package/dist/harbor/memory.d.ts +25 -0
  33. package/dist/harbor/memory.js +72 -0
  34. package/dist/harbor/reach.d.ts +36 -0
  35. package/dist/harbor/reach.js +191 -0
  36. package/dist/harbor/store.d.ts +40 -0
  37. package/dist/harbor/store.js +70 -0
  38. package/dist/vector/cases.d.ts +41 -0
  39. package/dist/vector/cases.js +195 -0
  40. package/dist/vector/index.d.ts +6 -0
  41. package/dist/vector/index.js +8 -0
  42. package/dist/vector/stand.d.ts +9 -0
  43. package/dist/vector/stand.js +76 -0
  44. package/dist/vector/world.d.ts +143 -0
  45. package/dist/vector/world.js +198 -0
  46. package/dist/ward/allowance.d.ts +10 -0
  47. package/dist/ward/allowance.js +71 -0
  48. package/dist/ward/arithmetic.d.ts +31 -0
  49. package/dist/ward/arithmetic.js +245 -0
  50. package/dist/ward/cells.d.ts +7 -0
  51. package/dist/ward/cells.js +186 -0
  52. package/dist/ward/door.d.ts +18 -0
  53. package/dist/ward/door.js +186 -0
  54. package/dist/ward/ground.d.ts +23 -0
  55. package/dist/ward/ground.js +38 -0
  56. package/dist/ward/heirs.d.ts +13 -0
  57. package/dist/ward/heirs.js +115 -0
  58. package/dist/ward/index.d.ts +9 -0
  59. package/dist/ward/index.js +13 -0
  60. package/dist/ward/owner.d.ts +13 -0
  61. package/dist/ward/owner.js +220 -0
  62. package/dist/ward/partition.d.ts +65 -0
  63. package/dist/ward/partition.js +295 -0
  64. package/dist/ward/seal.d.ts +52 -0
  65. package/dist/ward/seal.js +145 -0
  66. package/dist/ward/stance.d.ts +25 -0
  67. package/dist/ward/stance.js +413 -0
  68. package/dist/ward/ward.d.ts +12 -0
  69. package/dist/ward/ward.js +361 -0
  70. package/package.json +37 -15
  71. package/protocol/SPEC.md +1843 -0
  72. package/protocol/vectors/arithmetic.json +117 -0
  73. package/protocol/vectors/door.json +345 -0
  74. package/protocol/vectors/framing.json +92 -0
  75. package/protocol/vectors/wire.json +48 -0
  76. package/quo-kit.md +652 -0
  77. package/src/being/being.ts +123 -0
  78. package/src/being/digest.ts +46 -0
  79. package/src/being/index.ts +7 -0
  80. package/src/being/silence.ts +46 -0
  81. package/src/being/types.ts +170 -0
  82. package/src/conformance/assert.ts +100 -0
  83. package/src/conformance/beings.ts +183 -0
  84. package/src/conformance/estate.ts +412 -0
  85. package/src/conformance/index.ts +962 -0
  86. package/src/conformance/reach.ts +83 -0
  87. package/src/conformance/store.ts +136 -0
  88. package/src/harbor/core.ts +660 -0
  89. package/src/harbor/dial.ts +112 -0
  90. package/src/harbor/index.ts +17 -0
  91. package/src/harbor/memory.ts +83 -0
  92. package/src/harbor/reach.ts +215 -0
  93. package/src/harbor/store.ts +101 -0
  94. package/src/vector/cases.ts +229 -0
  95. package/src/vector/index.ts +11 -0
  96. package/src/vector/stand.ts +75 -0
  97. package/src/vector/world.ts +220 -0
  98. package/src/ward/allowance.ts +85 -0
  99. package/src/ward/arithmetic.ts +247 -0
  100. package/src/ward/cells.ts +190 -0
  101. package/src/ward/door.ts +186 -0
  102. package/src/ward/ground.ts +160 -0
  103. package/src/ward/heirs.ts +114 -0
  104. package/src/ward/index.ts +17 -0
  105. package/src/ward/owner.ts +214 -0
  106. package/src/ward/partition.ts +353 -0
  107. package/src/ward/seal.ts +174 -0
  108. package/src/ward/stance.ts +433 -0
  109. package/src/ward/ward.ts +378 -0
  110. package/index.js +0 -17
  111. package/nervur.js +0 -5
@@ -0,0 +1,81 @@
1
+ import { REFUSED, Socket } from './reach.js';
2
+ export function dial(harbor, url) {
3
+ const d = { url, socket: null, wake: () => { }, close: () => { } };
4
+ let stopped = false, wait = 1000, timer, line, // the line in hand, open or still opening: close() closes it either way
5
+ held; // the one this dialer owns now, and how it gives back what the line took
6
+ const connect = () => {
7
+ timer = undefined; // no wait pending: this is the dial it was waiting for
8
+ if (stopped)
9
+ return;
10
+ line = new WebSocket(url.replace(/\/$/, '').replace(/^http/, 'ws'));
11
+ const s = new Socket(line, (pk, bytes) => harbor.deliver(pk, bytes), (far) => {
12
+ // A line that opened is not a line that works. The wait goes back to
13
+ // a second here, where the far side has answered and been taken, and
14
+ // not at the handshake: a listener that opens and then refuses every
15
+ // time would otherwise be dialed at a fixed two seconds for good.
16
+ wait = 1000;
17
+ void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
18
+ }, (why) => {
19
+ shed();
20
+ // A line this dialer already put down. Its close arrives whenever the
21
+ // system gets round to noticing, which on a socket that died under a
22
+ // sleeping device is minutes; by then the line dialed in its place is
23
+ // the one in hand, so this one gives back what it took and dials
24
+ // nothing. Only the line the dialer still holds dials again.
25
+ if (held?.socket !== s)
26
+ return;
27
+ held = undefined;
28
+ // A suite this listener will not speak is not a line that dropped. It
29
+ // will not become speakable by asking again sooner, so the wait goes
30
+ // straight to the ceiling and stays there until it changes its mind.
31
+ if (why.code === REFUSED)
32
+ wait = 30000;
33
+ if (!stopped)
34
+ timer = setTimeout(connect, wait);
35
+ wait = Math.min(wait * 2, 30000);
36
+ });
37
+ // Everything this line took from the harbor, given back. Said when it
38
+ // closes, and when the dialer puts it down without waiting for that.
39
+ const shed = () => {
40
+ harbor.announcers.delete(say);
41
+ harbor.unbind(s);
42
+ if (d.socket === s)
43
+ d.socket = null;
44
+ const at = harbor.fallbacks.indexOf(s);
45
+ if (at !== -1)
46
+ harbor.fallbacks.splice(at, 1); // this line only: another dialer's stays
47
+ };
48
+ held = { socket: s, shed };
49
+ harbor.fallbacks.push(s); // from the moment it is dialed: an ask made before the handshake waits on the socket, and is not unreached
50
+ // What this harbor holds, said when the line opens and again whenever it
51
+ // changes. A ward booted or adopted after the handshake is announced on
52
+ // the line already in hand, and a dropped one stops being claimed at once.
53
+ const say = () => s.announce([...harbor.doors.keys()]);
54
+ line.addEventListener('open', () => {
55
+ if (stopped)
56
+ return s.close(); // closed while it was opening: it opens into nothing
57
+ d.socket = s;
58
+ harbor.announcers.add(say);
59
+ say();
60
+ });
61
+ };
62
+ d.wake = () => {
63
+ if (stopped)
64
+ return;
65
+ clearTimeout(timer);
66
+ timer = undefined;
67
+ wait = 1000; // the device is back, and the wait it froze belongs to the sleep, not to this line
68
+ const old = held;
69
+ held = undefined; // put down before the new one is dialed, so a fallback list never holds the dead line ahead of the live one
70
+ old?.shed();
71
+ old?.socket.close();
72
+ connect();
73
+ };
74
+ d.close = () => {
75
+ stopped = true;
76
+ clearTimeout(timer);
77
+ line?.close(); // open or opening: a line closed while connecting fires close and never open
78
+ };
79
+ connect();
80
+ return d;
81
+ }
@@ -0,0 +1,8 @@
1
+ export { MemoryHarbor, type Booted, type WardFactory } from './memory.ts';
2
+ export { Harbor, DEFAULT_CODE, type Hosted, type Bound, type Loader } from './core.ts';
3
+ export { dial, type Dialer } from './dial.ts';
4
+ export { MemoryStore, values, rowsOf, type Store, type Kept, type WardRecord } from './store.ts';
5
+ export { HEAD, fromRows, rowsIn } from '../ward/partition.ts';
6
+ export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
7
+ export type { Ground, WardPointers, Lend } from '../ward/ground.ts';
8
+ export { maker, entropy, learnPk } from '../ward/ground.ts';
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // nervur/harbor — the Ground contract and the harbors. The only
3
+ // place in the package that may know a wire, and it knows one on the
4
+ // standard surface alone: fetch and WebSocket. MemoryHarbor knows none: it
5
+ // is one process talking to itself, where every ward assertion is made.
6
+ export { MemoryHarbor } from './memory.js';
7
+ export { Harbor, DEFAULT_CODE } from './core.js';
8
+ export { dial } from './dial.js';
9
+ export { MemoryStore, values, rowsOf } from './store.js';
10
+ // What a store keeping a ward in rows needs: the name of the row that is not
11
+ // a being, and the partition back from the rows it was cut into.
12
+ export { HEAD, fromRows, rowsIn } from '../ward/partition.js';
13
+ export { request, Socket, SUITE, REFUSED } from './reach.js';
14
+ // What a harbor builds a ground out of. Convenience, never contract: a kit
15
+ // writing its own harbor may write these three again.
16
+ export { maker, entropy, learnPk } from '../ward/ground.js';
@@ -0,0 +1,25 @@
1
+ import type { BeingClass, BeingLike } from '../being/types.ts';
2
+ import { type Ground, type WardPointers, type Lend } from '../ward/ground.ts';
3
+ export type Booted = WardPointers & {
4
+ pk: string;
5
+ };
6
+ export type WardFactory = (ground: Ground) => Promise<WardPointers>;
7
+ export declare class MemoryHarbor {
8
+ readonly doors: Map<string, (bytes: Uint8Array) => Promise<{
9
+ bytes: Uint8Array;
10
+ heard: boolean;
11
+ }>>;
12
+ readonly down: Set<string>;
13
+ readonly wire: string[];
14
+ readonly refused: Map<string, number>;
15
+ readonly peers: Set<MemoryHarbor>;
16
+ cut: boolean;
17
+ readonly partitions: Map<string, Record<string, unknown>>;
18
+ readonly wards: Map<string, Booted>;
19
+ readonly objects: WeakMap<object, BeingLike>;
20
+ random: Ground['random'];
21
+ route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
22
+ link(harbor: MemoryHarbor): void;
23
+ boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, lend?: Lend): Promise<Booted>;
24
+ reboot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, lend?: Lend): Promise<Booted>;
25
+ }
@@ -0,0 +1,72 @@
1
+ import { maker, entropy, learnPk } from '../ward/ground.js';
2
+ import { hex } from '../ward/arithmetic.js';
3
+ export class MemoryHarbor {
4
+ doors = new Map(); // ward pk -> door
5
+ down = new Set(); // ward pks this harbor cannot reach right now
6
+ wire = []; // every byte string that ever crossed, as hex. for inspection
7
+ refused = new Map(); // ward pk -> arrivals its door would not admit: strangers, counted
8
+ peers = new Set();
9
+ cut = false;
10
+ partitions = new Map(); // seed -> memory. the harbor keeps it and reads nothing
11
+ wards = new Map(); // seed -> pointers
12
+ objects = new WeakMap(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
13
+ // Where the wards this harbor boots draw their entropy. The device's own
14
+ // by default. A harbor may hand a fixed stream instead, which is what pins
15
+ // bytes: every key a ward mints and every nonce it seals under comes from
16
+ // here, so a ward on a known seed with a known stream answers the same
17
+ // bytes every run, and a corpus of them is a corpus and not a sample.
18
+ random = entropy;
19
+ // the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
20
+ async route(farPk, bytes) {
21
+ if (this.down.has(farPk))
22
+ return undefined;
23
+ const door = this.doors.get(farPk);
24
+ if (door) {
25
+ const r = await door(bytes);
26
+ if (!r.heard)
27
+ this.refused.set(farPk, (this.refused.get(farPk) ?? 0) + 1);
28
+ return r.bytes;
29
+ }
30
+ const peer = [...this.peers].find((h) => h.doors.has(farPk) && !h.down.has(farPk));
31
+ if (!peer || this.cut)
32
+ return undefined; // nothing. the harbor has no word silence
33
+ return peer.route(farPk, bytes);
34
+ }
35
+ link(harbor) {
36
+ this.peers.add(harbor);
37
+ harbor.peers.add(this);
38
+ }
39
+ // boot by seed. the harbor gets a door and an ask, and learns the pk by asking.
40
+ // `lend` is what this device lends the ward's beings, and a test's stand-in
41
+ // for a real terrain's: left out, every name answers null.
42
+ async boot(seed, Ward, classes, lend) {
43
+ this.partitions.set(seed, {});
44
+ return this.reboot(seed, Ward, classes, lend);
45
+ }
46
+ // a restart: same seed, same partition, fresh ward. the ward's files are the harbor's to keep.
47
+ async reboot(seed, Ward, classes, lend) {
48
+ const memory = this.partitions.get(seed);
49
+ if (!memory)
50
+ throw new Error(`no partition for ${seed}`);
51
+ const ground = {
52
+ seed,
53
+ memory,
54
+ ...(lend ? { lend } : {}),
55
+ // One objects map for the whole harbor, not one per ward: every ward
56
+ // here is in this process and a test reaches any being of any of them.
57
+ instantiate: maker(this.objects, classes),
58
+ // bytes cross, copied, never a reference. both ways.
59
+ carry: async (farPk, bytes) => {
60
+ this.wire.push(hex(bytes));
61
+ const back = await this.route(farPk, new Uint8Array(bytes));
62
+ return back === undefined ? undefined : new Uint8Array(back);
63
+ },
64
+ random: (n) => this.random(n),
65
+ };
66
+ const w = await Ward(ground);
67
+ const booted = { ...w, pk: await learnPk(w) };
68
+ this.doors.set(booted.pk, booted.door);
69
+ this.wards.set(seed, booted);
70
+ return booted;
71
+ }
72
+ }
@@ -0,0 +1,36 @@
1
+ export type Carry = (pk: string, bytes: Uint8Array) => Promise<Uint8Array | undefined>;
2
+ export type Reach = {
3
+ carry: Carry;
4
+ close(): void;
5
+ };
6
+ export declare function request(url: string): Reach;
7
+ export declare const SUITE = 1;
8
+ export declare const REFUSED = 4001;
9
+ export type Line = {
10
+ readyState: number;
11
+ binaryType: string;
12
+ send(data: string | Uint8Array): void;
13
+ close(code?: number, reason?: string): void;
14
+ addEventListener(type: 'open' | 'error', fn: () => void): void;
15
+ addEventListener(type: 'close', fn: (e?: {
16
+ code?: number;
17
+ reason?: string;
18
+ }) => void): void;
19
+ addEventListener(type: 'message', fn: (e: {
20
+ data: unknown;
21
+ }) => void): void;
22
+ };
23
+ export type Announce = (pks: string[]) => void;
24
+ export declare class Socket implements Reach {
25
+ #private;
26
+ readonly line: Line;
27
+ readonly pending: Map<number, (bytes: Uint8Array | undefined) => void>;
28
+ far: string[];
29
+ constructor(line: Line, deliver: Carry, onAnnounce: Announce, onClose: (why: {
30
+ code?: number;
31
+ reason?: string;
32
+ }) => void);
33
+ announce(pks: string[]): void;
34
+ carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
35
+ close(): void;
36
+ }
@@ -0,0 +1,191 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Reach: carrying bytes to a pk off the device. One interface, two
3
+ // implementations:
4
+ //
5
+ // request a URL. POST the bytes to `<url>/<pk>`, get bytes back.
6
+ // Listener to listener.
7
+ // socket a held WebSocket at `<url>`, opened by whichever side can
8
+ // dial, used in both directions with a frame id. Each side
9
+ // announces the ward pks it holds when the socket opens, and the
10
+ // other binds them.
11
+ //
12
+ // Both are written on what every terrain has: fetch, and the standard
13
+ // WebSocket surface, which a browser, Node, an edge worker's pair and the
14
+ // `ws` server all offer. The listener half of a socket is the terrain's
15
+ // own and stays outside this tree; what is here is the framing, either end
16
+ // of a line, and the request. Nothing here names a runtime, and
17
+ // `src/conformance/reach.ts` is what every reach passes. A reach carries and returns; it reads
18
+ // nothing. `undefined` is a promise that nothing was delivered: no such pk
19
+ // at the far end, a connection that would not open, a socket that is gone.
20
+ // A reach that sent the bytes and lost the line afterwards answers nothing
21
+ // at all, and the ward's own bound ends the ask.
22
+ import { concat, hex, unhex } from '../ward/arithmetic.js';
23
+ const never = () => new Promise(() => { }); // sent, and no word since: the ward's bound decides
24
+ // A request reach to one listener.
25
+ export function request(url) {
26
+ const base = url.replace(/\/$/, '');
27
+ return {
28
+ close: () => { },
29
+ carry: async (pk, bytes) => {
30
+ let res;
31
+ // A body of its own, not the caller's view: fetch reads it after this
32
+ // line returns, and a buffer somebody else may write is not a body. The
33
+ // copy is its own ArrayBuffer, which is what a body is; the bytes handed
34
+ // in are typed as backed by anything, shared memory included.
35
+ const body = new Uint8Array(bytes.length);
36
+ body.set(bytes);
37
+ try {
38
+ res = await fetch(`${base}/${pk}`, { method: 'POST', headers: { 'content-type': 'application/octet-stream', 'quo-suite': String(SUITE) }, body });
39
+ }
40
+ catch {
41
+ return undefined; // the connection never opened: nothing was delivered
42
+ }
43
+ // A status that says the listener did not take the bytes is nothing
44
+ // delivered: no reach for that pk, a request it refused on its face, a
45
+ // gateway with nobody behind it. Any other failure is a listener that
46
+ // took them and failed after, which is sent and no word since.
47
+ if (res.status === 404 || (res.status >= 400 && res.status < 500) || res.status === 502 || res.status === 503 || res.status === 504)
48
+ return undefined;
49
+ if (!res.ok)
50
+ return never();
51
+ return new Uint8Array(await res.arrayBuffer());
52
+ },
53
+ };
54
+ }
55
+ // The frames on a socket. Binary, one byte of kind first.
56
+ // ask [0][id: 4 bytes][pk: 64 bytes][bytes]
57
+ // reply [1][id: 4 bytes][bytes]
58
+ // none [2][id: 4 bytes] nothing was delivered
59
+ // And one text frame, JSON: { announce: [pk, ...], suite }, the ward pks this
60
+ // side holds and the wire suite it speaks, sent when the socket opens and
61
+ // whenever the pks change.
62
+ const ASK = 0, REPLY = 1, NONE = 2;
63
+ // The wire suite: which frames these are and which five algorithms seal what
64
+ // they carry. One value, and it is not negotiated. It rides in the handshake
65
+ // and never on an ask, so an ask stays bytes nobody can tell from noise, and
66
+ // a line still learns before it carries anything whether the far side speaks
67
+ // what this side speaks. A side meeting a suite it does not know carries
68
+ // nothing for it and closes: the alternative is bytes that will never open,
69
+ // answered by a silence that says nothing about why, forever. Nothing
70
+ // changes this number before 1.0.0. After it, this is the one place a second
71
+ // suite can be told from a first without guessing.
72
+ export const SUITE = 1;
73
+ // The close a side sends when it refuses a suite, in the range a protocol
74
+ // may spend on itself. A line that just closes tells the far side nothing,
75
+ // and a dialer that learns nothing dials again on its backoff for good: the
76
+ // whole reason this number exists is that a kit which cannot be understood
77
+ // should be told so instead of meeting a silence that names no reason. So
78
+ // the refusal names itself on the way out, and the far side may read it.
79
+ export const REFUSED = 4001;
80
+ const OPEN = 1; // WebSocket.OPEN, on every terrain
81
+ // How many asks a socket keeps a resolver for. An ask the far side never
82
+ // answers is ended by the ward's bound, and its resolver stays until the
83
+ // line closes; a far side that answers nothing for long enough would grow
84
+ // the map for as long as this side asks, so the oldest goes when it is full.
85
+ const PENDING = 4096;
86
+ // One held socket, either side of it. `deliver` is what this side does with
87
+ // an ask that arrives for a pk: its own door, or a socket it holds for that
88
+ // pk, and nothing else. `onAnnounce` learns the far side's pks; `onClose`
89
+ // forgets them.
90
+ export class Socket {
91
+ line;
92
+ pending = new Map();
93
+ far = []; // the pks the far side announced
94
+ #id = 1;
95
+ #open;
96
+ constructor(line, deliver, onAnnounce, onClose) {
97
+ this.line = line;
98
+ line.binaryType = 'arraybuffer';
99
+ // A line that opens is a reach; one that errors, or closes before it
100
+ // opened, is a reach that failed, and an ask waiting on the handshake is
101
+ // nothing delivered.
102
+ this.#open =
103
+ line.readyState === OPEN
104
+ ? Promise.resolve()
105
+ : new Promise((ok, no) => {
106
+ line.addEventListener('open', ok);
107
+ line.addEventListener('error', no);
108
+ line.addEventListener('close', () => no(new Error('closed before it opened')));
109
+ });
110
+ this.#open.catch(() => { }); // a handshake refused is a reach that failed, not a process that dies
111
+ line.addEventListener('message', (e) => {
112
+ if (typeof e.data === 'string') {
113
+ try {
114
+ const m = JSON.parse(e.data);
115
+ // Absent is this suite: a side that says nothing says one.
116
+ if (m.suite !== undefined && m.suite !== SUITE) {
117
+ this.far = [];
118
+ line.close(REFUSED, 'suite');
119
+ return;
120
+ }
121
+ if (Array.isArray(m.announce)) {
122
+ this.far = m.announce.filter((x) => typeof x === 'string');
123
+ onAnnounce(this.far);
124
+ }
125
+ }
126
+ catch {
127
+ /* not for us */
128
+ }
129
+ return;
130
+ }
131
+ const buf = e.data instanceof ArrayBuffer ? new Uint8Array(e.data) : ArrayBuffer.isView(e.data) ? new Uint8Array(e.data.buffer, e.data.byteOffset, e.data.byteLength) : undefined;
132
+ if (!buf || buf.length < 5)
133
+ return;
134
+ const kind = buf[0], id = new DataView(buf.buffer, buf.byteOffset, buf.byteLength).getUint32(1);
135
+ if (kind === ASK) {
136
+ const pk = hex(buf.subarray(5, 69));
137
+ // A deliver that rejects has delivered nothing this side can vouch
138
+ // for, and a listener does not die of it: the far side hears none.
139
+ void deliver(pk, new Uint8Array(buf.subarray(69)))
140
+ .then((back) => back, () => undefined)
141
+ .then((back) => {
142
+ const head = new Uint8Array(5);
143
+ head[0] = back === undefined ? NONE : REPLY;
144
+ new DataView(head.buffer).setUint32(1, id);
145
+ if (line.readyState === OPEN)
146
+ line.send(back === undefined ? head : concat([head, back]));
147
+ });
148
+ return;
149
+ }
150
+ const waiting = this.pending.get(id);
151
+ if (!waiting)
152
+ return;
153
+ this.pending.delete(id);
154
+ waiting(kind === REPLY ? new Uint8Array(buf.subarray(5)) : undefined);
155
+ });
156
+ line.addEventListener('error', () => { });
157
+ line.addEventListener('close', (e) => {
158
+ this.pending.clear(); // in flight and no word since: nothing is answered
159
+ onClose({ code: e?.code, reason: e?.reason });
160
+ });
161
+ }
162
+ // Say which ward pks this side holds.
163
+ announce(pks) {
164
+ if (this.line.readyState === OPEN)
165
+ this.line.send(JSON.stringify({ announce: pks, suite: SUITE }));
166
+ }
167
+ async carry(pk, bytes) {
168
+ try {
169
+ await this.#open;
170
+ }
171
+ catch {
172
+ return undefined;
173
+ }
174
+ if (this.line.readyState !== OPEN)
175
+ return undefined;
176
+ const id = this.#id++;
177
+ const head = new Uint8Array(69);
178
+ head[0] = ASK;
179
+ new DataView(head.buffer).setUint32(1, id);
180
+ head.set(unhex(pk), 5);
181
+ return new Promise((ok) => {
182
+ if (this.pending.size >= PENDING)
183
+ this.pending.delete(this.pending.keys().next().value); // the oldest, which the ward's bound ended long ago
184
+ this.pending.set(id, ok);
185
+ this.line.send(concat([head, bytes]));
186
+ });
187
+ }
188
+ close() {
189
+ this.line.close();
190
+ }
191
+ }
@@ -0,0 +1,40 @@
1
+ export type WardRecord = {
2
+ pk: string;
3
+ code: string;
4
+ user: string;
5
+ };
6
+ export type Kept = {
7
+ seed: Uint8Array;
8
+ partition: Record<string, unknown>;
9
+ record: WardRecord;
10
+ };
11
+ export type Store = {
12
+ list(): Promise<string[]>;
13
+ load(name: string): Promise<Kept | undefined>;
14
+ put(name: string, kept: Kept): Promise<void>;
15
+ save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
16
+ record(name: string, record: WardRecord): Promise<void>;
17
+ take(name: string): Promise<Kept | undefined>;
18
+ hints(): Promise<Record<string, string>>;
19
+ hint(pk: string, url: string): Promise<void>;
20
+ };
21
+ export declare const values: (p: Record<string, unknown>) => Record<string, unknown>;
22
+ export declare const rowsOf: (partition: Record<string, unknown>, rows: readonly string[]) => [string, Record<string, unknown> | undefined][];
23
+ type Row = {
24
+ seed: Uint8Array;
25
+ parts: Map<string, Record<string, unknown>>;
26
+ record: WardRecord;
27
+ };
28
+ export declare class MemoryStore implements Store {
29
+ readonly rows: Map<string, Row>;
30
+ readonly reach: Map<string, string>;
31
+ list(): Promise<string[]>;
32
+ load(name: string): Promise<Kept | undefined>;
33
+ put(name: string, kept: Kept): Promise<void>;
34
+ save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
35
+ record(name: string, record: WardRecord): Promise<void>;
36
+ take(name: string): Promise<Kept | undefined>;
37
+ hints(): Promise<Record<string, string>>;
38
+ hint(pk: string, url: string): Promise<void>;
39
+ }
40
+ export {};
@@ -0,0 +1,70 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The store: keeping wards by name. A ward is three things and the store
3
+ // keeps all three under one name: the seed, the partition and the ward
4
+ // record, which says where the class bodies come from and which being is
5
+ // the user's. Plus the directory's hints, which are the harbor's and
6
+ // survive a restart. The store reads nothing it keeps; the partition is
7
+ // values the ward wrote, and the seed is bytes only the ward derives from.
8
+ // Every terrain has one: files on a disk, IndexedDB in a tab, an object's
9
+ // storage on the edge; the memory store below is the library's own, for a
10
+ // harbor core with no device under it. `src/conformance/store.ts` is what
11
+ // every one of them passes.
12
+ import { fromRows, rowOf, rowsIn } from '../ward/partition.js';
13
+ // The partition is values only, and the ward hands it out through a guard,
14
+ // so a store takes a copy of the values the way a file would: through JSON.
15
+ export const values = (p) => JSON.parse(JSON.stringify(p));
16
+ // The rows a store was told about, as values, ready to be written down. A row
17
+ // whose key names nobody is a being who left, and comes back undefined, which
18
+ // is the store dropping what it holds for her.
19
+ export const rowsOf = (partition, rows) => rows.map((row) => {
20
+ const value = rowOf(partition, row);
21
+ return [row, value === undefined ? undefined : values(value)];
22
+ });
23
+ // One store's copy, brought up to date with the rows it was told about.
24
+ const keep = (row, partition, rows) => {
25
+ for (const [id, value] of rowsOf(partition, rows)) {
26
+ if (value === undefined)
27
+ row.parts.delete(id);
28
+ else
29
+ row.parts.set(id, value);
30
+ }
31
+ };
32
+ export class MemoryStore {
33
+ rows = new Map();
34
+ reach = new Map();
35
+ async list() {
36
+ return [...this.rows.keys()];
37
+ }
38
+ async load(name) {
39
+ const row = this.rows.get(name);
40
+ return row && { seed: new Uint8Array(row.seed), partition: fromRows(Object.fromEntries(row.parts)), record: { ...row.record } };
41
+ }
42
+ async put(name, kept) {
43
+ if (this.rows.has(name))
44
+ throw new Error(`ward ${name} already exists here`);
45
+ const row = { seed: new Uint8Array(kept.seed), parts: new Map(), record: { ...kept.record } };
46
+ this.rows.set(name, row);
47
+ keep(row, kept.partition, rowsIn(kept.partition));
48
+ }
49
+ async save(name, partition, rows) {
50
+ const row = this.rows.get(name);
51
+ if (row)
52
+ keep(row, partition, rows);
53
+ }
54
+ async record(name, record) {
55
+ const row = this.rows.get(name);
56
+ if (row)
57
+ row.record = { ...record };
58
+ }
59
+ async take(name) {
60
+ const kept = await this.load(name);
61
+ this.rows.delete(name);
62
+ return kept;
63
+ }
64
+ async hints() {
65
+ return Object.fromEntries(this.reach);
66
+ }
67
+ async hint(pk, url) {
68
+ this.reach.set(pk, url);
69
+ }
70
+ }
@@ -0,0 +1,41 @@
1
+ import type { Json } from '../being/types.ts';
2
+ import type { Booted } from '../harbor/memory.ts';
3
+ import type { World, Sealed } from './world.ts';
4
+ export type DoorRecord = {
5
+ case: string;
6
+ name: string;
7
+ state: string;
8
+ ward: string;
9
+ kind: 'silence' | 'word' | 'object';
10
+ heard: boolean;
11
+ ask: string;
12
+ reply: string;
13
+ opens: Json | null;
14
+ before: string;
15
+ after: string;
16
+ wrote: boolean;
17
+ };
18
+ export type Pin = () => Promise<void>;
19
+ export type Arrival = {
20
+ kind: DoorRecord['kind'];
21
+ door: Booted;
22
+ ask: Sealed | {
23
+ bytes: Uint8Array;
24
+ open: null;
25
+ };
26
+ };
27
+ export type Case = {
28
+ case: string;
29
+ name: string;
30
+ state: string;
31
+ ward: string;
32
+ arrive: (w: World, pin: Pin) => Promise<Arrival>;
33
+ };
34
+ export declare function stand(c: Case): Promise<{
35
+ w: World;
36
+ before: string;
37
+ arrival: Arrival;
38
+ }>;
39
+ export declare function doorCases(): Promise<DoorRecord[]>;
40
+ export declare const caseNamed: (kase: string, name: string) => Case | undefined;
41
+ export declare const cases: Case[];