nervur 0.20.0 → 0.20.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.
Files changed (93) hide show
  1. package/README.md +107 -20
  2. package/dist/being/being.d.ts +1 -0
  3. package/dist/being/being.js +5 -1
  4. package/dist/being/faculty.d.ts +1 -0
  5. package/dist/being/faculty.js +13 -7
  6. package/dist/being/index.d.ts +1 -0
  7. package/dist/being/index.js +1 -0
  8. package/dist/being/kind.d.ts +4 -0
  9. package/dist/being/kind.js +28 -0
  10. package/dist/being/types.d.ts +3 -4
  11. package/dist/cli/command.d.ts +13 -0
  12. package/dist/cli/command.js +194 -0
  13. package/dist/cli/harbor.d.ts +21 -0
  14. package/dist/cli/harbor.js +141 -0
  15. package/dist/cli/main.d.ts +2 -0
  16. package/dist/cli/main.js +14 -0
  17. package/dist/contract/index.d.ts +15 -2
  18. package/dist/contract/index.js +16 -2
  19. package/dist/folder/index.d.ts +5 -4
  20. package/dist/folder/index.js +45 -83
  21. package/dist/harbor/catalogue.d.ts +25 -0
  22. package/dist/harbor/catalogue.js +93 -0
  23. package/dist/harbor/dock.d.ts +12 -2
  24. package/dist/harbor/dock.js +39 -9
  25. package/dist/harbor/harbor.d.ts +15 -6
  26. package/dist/harbor/harbor.js +83 -21
  27. package/dist/harbor/index.d.ts +4 -1
  28. package/dist/harbor/index.js +5 -1
  29. package/dist/harbor/package.d.ts +10 -0
  30. package/dist/harbor/package.js +115 -0
  31. package/dist/harbor/registry.d.ts +5 -3
  32. package/dist/harbor/registry.js +72 -7
  33. package/dist/harbor/root-line.d.ts +9 -0
  34. package/dist/harbor/root-line.js +36 -0
  35. package/dist/harbor/terrain.d.ts +2 -1
  36. package/dist/index.d.ts +4 -4
  37. package/dist/index.js +5 -5
  38. package/dist/pointer/bodies.d.ts +9 -3
  39. package/dist/pointer/bodies.js +37 -18
  40. package/dist/pointer/index.d.ts +1 -1
  41. package/dist/pointer/index.js +1 -1
  42. package/dist/pointer/world.d.ts +5 -4
  43. package/dist/pointer/world.js +23 -14
  44. package/dist/quo/door.js +2 -2
  45. package/dist/quo/keys.js +1 -1
  46. package/dist/tcp/frame.d.ts +29 -0
  47. package/dist/tcp/frame.js +74 -0
  48. package/dist/tcp/index.d.ts +18 -0
  49. package/dist/tcp/index.js +164 -0
  50. package/dist/ward/index.d.ts +1 -0
  51. package/dist/ward/index.js +1 -0
  52. package/dist/ward/pilot.d.ts +8 -0
  53. package/dist/ward/pilot.js +50 -0
  54. package/dist/ward/stance.d.ts +2 -4
  55. package/dist/ward/stance.js +7 -6
  56. package/dist/ward/ward-being.d.ts +2 -0
  57. package/dist/ward/ward-being.js +7 -1
  58. package/dist/ward/ward.d.ts +4 -2
  59. package/dist/ward/ward.js +35 -5
  60. package/package.json +8 -1
  61. package/src/being/being.ts +5 -1
  62. package/src/being/faculty.ts +14 -7
  63. package/src/being/index.ts +1 -0
  64. package/src/being/kind.ts +30 -0
  65. package/src/being/types.ts +8 -6
  66. package/src/cli/command.ts +188 -0
  67. package/src/cli/harbor.ts +137 -0
  68. package/src/cli/main.ts +13 -0
  69. package/src/contract/index.ts +38 -9
  70. package/src/folder/index.ts +44 -86
  71. package/src/harbor/catalogue.ts +110 -0
  72. package/src/harbor/dock.ts +48 -12
  73. package/src/harbor/harbor.ts +104 -29
  74. package/src/harbor/index.ts +6 -2
  75. package/src/harbor/package.ts +131 -0
  76. package/src/harbor/registry.ts +62 -9
  77. package/src/harbor/root-line.ts +36 -0
  78. package/src/harbor/terrain.ts +3 -1
  79. package/src/index.ts +5 -5
  80. package/src/pointer/bodies.ts +34 -16
  81. package/src/pointer/index.ts +1 -1
  82. package/src/pointer/world.ts +21 -18
  83. package/src/quo/door.ts +2 -2
  84. package/src/quo/keys.ts +1 -1
  85. package/src/stand/main.ts +1 -0
  86. package/src/stand/stand.ts +46 -4
  87. package/src/tcp/frame.ts +78 -0
  88. package/src/tcp/index.ts +173 -0
  89. package/src/ward/index.ts +1 -0
  90. package/src/ward/pilot.ts +50 -0
  91. package/src/ward/stance.ts +8 -7
  92. package/src/ward/ward-being.ts +7 -1
  93. package/src/ward/ward.ts +37 -6
@@ -0,0 +1,141 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // SPDX-License-Identifier: Apache-2.0
10
+ // A harbor on this machine, as the `nervur` command stands it: its seed and
11
+ // sealed memory in a folder, its code imported from the files its
12
+ // catalogue names, Quo over TCP as its carrier, and the root line on a
13
+ // local socket beside them.
14
+ //
15
+ // <dir>/seed the harbor's seed
16
+ // <dir>/<place>/ its sealed memory
17
+ // <dir>/root.sock the root line while the harbor is served, open to
18
+ // this user alone, or in the temporary folder for a
19
+ // folder too deep for a socket path
20
+ //
21
+ // A root request goes to a served harbor over its socket. With none served,
22
+ // the command unpacks the harbor from its folder, asks, and closes it, so
23
+ // one process at a time writes the folder.
24
+ import { createHash } from 'node:crypto';
25
+ import { chmod, rm } from 'node:fs/promises';
26
+ import { connect, createServer } from 'node:net';
27
+ import { tmpdir } from 'node:os';
28
+ import { join, resolve } from 'node:path';
29
+ import { pathToFileURL } from 'node:url';
30
+ import { Loader } from '../contract/index.js';
31
+ import { FolderCustody, FolderMemory } from '../folder/index.js';
32
+ import { Harbor, rootLine } from '../harbor/index.js';
33
+ import { CryptoEntropy, PointerTerrain } from '../pointer/index.js';
34
+ import { TcpCarrier, TcpListener } from '../tcp/index.js';
35
+ // A socket path has a hundred bytes or so on every system, so a folder too
36
+ // deep for one puts its socket in this user's temporary folder, under a
37
+ // name drawn from the folder's own path.
38
+ const SOCKET_PATH = 100;
39
+ export const socketOf = (dir) => {
40
+ const beside = join(resolve(dir), 'root.sock');
41
+ if (Buffer.byteLength(beside) <= SOCKET_PATH)
42
+ return beside;
43
+ return join(tmpdir(), `nervur-${createHash('sha256').update(resolve(dir)).digest('hex').slice(0, 32)}.sock`);
44
+ };
45
+ // Code from a file: a module that exports `module`, `version` and
46
+ // `classes`, found again by its absolute path. A file that cannot be
47
+ // imported, or exports no module, is none.
48
+ export class ImportLoader extends Loader {
49
+ async load(from) {
50
+ const path = resolve(from);
51
+ let exported;
52
+ try {
53
+ exported = (await import(__rewriteRelativeImportExtension(pathToFileURL(path).href)));
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ const { module, version, classes } = exported;
59
+ if (typeof module !== 'string' || typeof version !== 'string' || !Array.isArray(classes))
60
+ return null;
61
+ return { module, version, classes, from: path };
62
+ }
63
+ }
64
+ export const open = async (dir) => {
65
+ const entropy = new CryptoEntropy();
66
+ const carrier = new TcpCarrier();
67
+ const harbor = await Harbor.open(new PointerTerrain({ entropy, carrier, loader: new ImportLoader(), custody: new FolderCustody(dir, entropy), memory: new FolderMemory(dir) }));
68
+ return { harbor, carrier };
69
+ };
70
+ // One root request over a served harbor's socket, or undefined where none
71
+ // is served.
72
+ const overSocket = (dir, request) => new Promise((done, fail) => {
73
+ const socket = connect(socketOf(dir));
74
+ let text = '';
75
+ socket.once('connect', () => socket.write(`${JSON.stringify(request)}\n`));
76
+ socket.on('data', (chunk) => {
77
+ text += chunk.toString('utf8');
78
+ const end = text.indexOf('\n');
79
+ if (end < 0)
80
+ return;
81
+ socket.end();
82
+ done(JSON.parse(text.slice(0, end)));
83
+ });
84
+ socket.once('error', (e) => (e.code === 'ENOENT' || e.code === 'ECONNREFUSED' ? done(undefined) : fail(e)));
85
+ });
86
+ export const request = async (dir, req) => {
87
+ const served = await overSocket(dir, req);
88
+ if (served !== undefined)
89
+ return served;
90
+ const { harbor, carrier } = await open(dir);
91
+ try {
92
+ return await rootLine(harbor, req);
93
+ }
94
+ finally {
95
+ await carrier.close();
96
+ }
97
+ };
98
+ // The harbor served: Quo over TCP on host and port, the root line on the
99
+ // socket. A socket file nobody answers on is a stale one, and is replaced.
100
+ export const serve = async (dir, host, port) => {
101
+ if ((await overSocket(dir, {})) !== undefined)
102
+ throw new Error(`a harbor is already served from ${dir}`);
103
+ const { harbor, carrier } = await open(dir);
104
+ const listener = await TcpListener.listen(harbor, host, port);
105
+ const root = socketOf(dir);
106
+ await rm(root, { force: true });
107
+ const server = createServer((socket) => {
108
+ let text = '';
109
+ socket.on('data', (chunk) => {
110
+ text += chunk.toString('utf8');
111
+ for (let end = text.indexOf('\n'); end >= 0; end = text.indexOf('\n')) {
112
+ const line = text.slice(0, end);
113
+ text = text.slice(end + 1);
114
+ let value;
115
+ try {
116
+ value = JSON.parse(line);
117
+ }
118
+ catch {
119
+ value = undefined;
120
+ }
121
+ void rootLine(harbor, value)
122
+ .catch(() => ({ error: 'the harbor threw' }))
123
+ .then((answer) => socket.write(`${JSON.stringify(answer)}\n`));
124
+ }
125
+ });
126
+ socket.on('error', () => socket.destroy());
127
+ });
128
+ await new Promise((done) => server.listen(root, done));
129
+ await chmod(root, 0o600);
130
+ return {
131
+ pk: harbor.box.pk,
132
+ at: listener.at,
133
+ root,
134
+ close: async () => {
135
+ await new Promise((done) => server.close(done));
136
+ await rm(root, { force: true });
137
+ await listener.close();
138
+ await carrier.close();
139
+ },
140
+ };
141
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // The `nervur` program: `command.ts` over this process's arguments. A
4
+ // command that cannot run exits 2 and says why on stderr.
5
+ import { main, SERVING } from './command.js';
6
+ try {
7
+ const code = await main(process.argv.slice(2), process.env, (line) => process.stdout.write(`${line}\n`));
8
+ if (code !== SERVING)
9
+ process.exitCode = code;
10
+ }
11
+ catch (e) {
12
+ process.stderr.write(`nervur: ${e.message}\n`);
13
+ process.exitCode = 2;
14
+ }
@@ -16,11 +16,24 @@ export interface Rows {
16
16
  kept(): Promise<boolean>;
17
17
  }
18
18
  export declare abstract class Memory {
19
- abstract open(name: string): Promise<Rows>;
19
+ abstract read(place: string): Promise<Map<string, Uint8Array>>;
20
+ abstract write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
20
21
  }
21
22
  export declare abstract class Custody {
22
- abstract seed(name: string): Promise<Uint8Array>;
23
+ abstract seed(): Promise<Uint8Array>;
24
+ }
25
+ export type Module = {
26
+ readonly module: string;
27
+ readonly version: string;
28
+ readonly classes: readonly unknown[];
29
+ };
30
+ export type Loaded = Module & {
31
+ readonly from: string;
32
+ };
33
+ export declare abstract class Loader {
34
+ abstract load(from: string): Promise<Loaded | null>;
23
35
  }
24
36
  export declare abstract class Carrier {
25
37
  abstract carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
38
+ route(_pk: string, _at: string | null): boolean;
26
39
  }
@@ -8,12 +8,26 @@ export class Entropy {
8
8
  // The moment, and a wait that ends.
9
9
  export class Clock {
10
10
  }
11
+ // A harbor's memory as a terrain keeps it: sealed entries it cannot read,
12
+ // in places, each entry by name. Every name is lowercase hex the kit
13
+ // chose, so a body may use it as a file name as it stands.
11
14
  export class Memory {
12
15
  }
13
- // A ward's seed by the harbor's name for it: thirty-two bytes, the same
14
- // every time that name is asked.
16
+ // The harbor's seed: thirty-two bytes, the same every time. It is the one
17
+ // secret a terrain holds.
15
18
  export class Custody {
16
19
  }
20
+ // Code, by the name a harbor keeps for it, or null where this loader has
21
+ // none. Code never crosses a door and never lives in a cell, so it is the
22
+ // one thing beside the seed a terrain hands in: a map, an import, a bundle.
23
+ export class Loader {
24
+ }
17
25
  // Bytes to a ward pk in; bytes, or null for nothing, out.
18
26
  export class Carrier {
27
+ // Where a ward pk is reached, or null to forget it: true when this
28
+ // carrier dials by address and took it. The addresses live in the dock's
29
+ // cells, and the harbor hands them here as it unpacks.
30
+ route(_pk, _at) {
31
+ return false;
32
+ }
19
33
  }
@@ -1,13 +1,14 @@
1
- import { Custody, Memory, type Entropy, type Rows } from '../contract/index.ts';
2
- export declare const folderName: (name: string) => string;
1
+ import { Custody, Memory, type Entropy } from '../contract/index.ts';
3
2
  export declare class FolderMemory extends Memory {
3
+ #private;
4
4
  readonly root: string;
5
5
  constructor(root: string);
6
- open(name: string): Promise<Rows>;
6
+ read(place: string): Promise<Map<string, Uint8Array>>;
7
+ write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
7
8
  }
8
9
  export declare class FolderCustody extends Custody {
9
10
  #private;
10
11
  readonly root: string;
11
12
  constructor(root: string, entropy: Entropy);
12
- seed(name: string): Promise<Uint8Array>;
13
+ seed(): Promise<Uint8Array>;
13
14
  }
@@ -1,102 +1,64 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // A harbor on a disk, for local proof: `nervur/folder`, the one entry that
2
+ // A harbor on a disk, for local proof: `nervur/folder`, an entry that
3
3
  // names a platform.
4
4
  //
5
- // <root>/<ward>/seed the ward's seed, as hex
6
- // <root>/<ward>/ward.json the ward's head
7
- // <root>/<ward>/<key>/being.json one being: class, cells, relations
5
+ // <root>/seed the harbor's seed, as hex
6
+ // <root>/<place>/<entry> one sealed entry of the harbor's memory
8
7
  //
9
- // A folder name is the ward's name or the being's key, escaped, so no name
10
- // reaches outside its folder. A file is written whole, beside itself and
11
- // then renamed over, so a crash leaves the last kept copy. Seeds are plain
12
- // files: custody that seals them is a terrain's, in the dock.
8
+ // Places and entries are the hex names the kit chose, so no name reaches
9
+ // outside its folder, and the disk shows no ward's name. A file is written
10
+ // whole, beside itself and then renamed over, so a crash leaves the last
11
+ // kept copy. The seed is a plain file: custody that seals it is a
12
+ // terrain's, in the dock.
13
13
  import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
14
14
  import { join } from 'node:path';
15
15
  import { hex, isHex, unhex } from '../crypto/index.js';
16
16
  import { Custody, Memory } from '../contract/index.js';
17
- import { HEAD_ROW } from '../ward/index.js';
18
- const HEAD = 'ward.json';
19
- const BEING = 'being.json';
20
- export const folderName = (name) => encodeURIComponent(name).replaceAll('.', '%2E').replaceAll('*', '%2A') || '%';
21
- const nameOf = (folder) => (folder === '%' ? '' : decodeURIComponent(folder));
22
17
  const missing = (e) => e.code === 'ENOENT';
23
- const readJson = async (path) => {
24
- try {
25
- return JSON.parse(await readFile(path, 'utf8'));
26
- }
27
- catch (e) {
28
- if (missing(e))
29
- return undefined;
30
- throw e;
31
- }
18
+ const named = (name) => {
19
+ if (!isHex(name) || name === '')
20
+ throw new Error(`${name} is not a name the kit chose`);
21
+ return name;
32
22
  };
33
- const writeWhole = async (path, text) => {
34
- await writeFile(`${path}.next`, text, { mode: 0o600 });
23
+ const writeWhole = async (path, bytes) => {
24
+ await writeFile(`${path}.next`, bytes, { mode: 0o600 });
35
25
  await rename(`${path}.next`, path);
36
26
  };
37
- class FolderRows {
38
- rows;
39
- #dir;
40
- #told = new Set();
41
- #line = Promise.resolve(true);
42
- constructor(dir, rows) {
43
- this.#dir = dir;
44
- this.rows = rows;
45
- }
46
- told(row) {
47
- this.#told.add(row);
48
- }
49
- // Every row told since the last keep, written in order, one keep at a time.
50
- kept() {
51
- const rows = [...this.#told];
52
- this.#told.clear();
53
- this.#line = this.#line.then(() => this.#write(rows)).catch(() => false);
54
- return this.#line;
55
- }
56
- async #write(told) {
57
- await mkdir(this.#dir, { recursive: true, mode: 0o700 });
58
- const beings = (this.rows.beings ?? {});
59
- for (const row of told) {
60
- if (row === HEAD_ROW) {
61
- await writeWhole(join(this.#dir, HEAD), JSON.stringify(this.rows.ward ?? null));
62
- continue;
63
- }
64
- const dir = join(this.#dir, folderName(row));
65
- if (!Object.hasOwn(beings, row)) {
66
- await rm(dir, { recursive: true, force: true });
67
- continue;
68
- }
69
- await mkdir(dir, { recursive: true, mode: 0o700 });
70
- await writeWhole(join(dir, BEING), JSON.stringify(beings[row]));
71
- }
72
- return true;
73
- }
74
- }
75
27
  export class FolderMemory extends Memory {
76
28
  root;
29
+ #line = Promise.resolve();
77
30
  constructor(root) {
78
31
  super();
79
32
  this.root = root;
80
33
  }
81
- // The rows as the disk holds them now.
82
- async open(name) {
83
- const dir = join(this.root, folderName(name));
84
- const rows = {};
85
- const head = await readJson(join(dir, HEAD));
86
- if (head !== undefined)
87
- rows.ward = head;
88
- const beings = {};
89
- const entries = await readdir(dir, { withFileTypes: true }).catch((e) => (missing(e) ? [] : Promise.reject(e)));
90
- for (const entry of entries) {
91
- if (!entry.isDirectory())
34
+ // The entries as the disk holds them now. A file that is not an entry,
35
+ // one still being written among them, is not read.
36
+ async read(place) {
37
+ const dir = join(this.root, named(place));
38
+ const out = new Map();
39
+ const files = await readdir(dir, { withFileTypes: true }).catch((e) => (missing(e) ? [] : Promise.reject(e)));
40
+ for (const file of files) {
41
+ if (!file.isFile() || !isHex(file.name) || file.name === '')
92
42
  continue;
93
- const row = await readJson(join(dir, entry.name, BEING));
94
- if (row !== undefined)
95
- beings[nameOf(entry.name)] = row;
43
+ out.set(file.name, new Uint8Array(await readFile(join(dir, file.name))));
96
44
  }
97
- if (head !== undefined || Object.keys(beings).length > 0)
98
- rows.beings = beings;
99
- return new FolderRows(dir, rows);
45
+ return out;
46
+ }
47
+ // One keep at a time, each entry in the order given.
48
+ async write(place, entries) {
49
+ const dir = join(this.root, named(place));
50
+ const kept = this.#line.then(async () => {
51
+ await mkdir(dir, { recursive: true, mode: 0o700 });
52
+ for (const [name, bytes] of entries) {
53
+ const path = join(dir, named(name));
54
+ if (bytes === null)
55
+ await rm(path, { force: true });
56
+ else
57
+ await writeWhole(path, bytes);
58
+ }
59
+ });
60
+ this.#line = kept.catch(() => undefined);
61
+ await kept;
100
62
  }
101
63
  }
102
64
  export class FolderCustody extends Custody {
@@ -107,9 +69,9 @@ export class FolderCustody extends Custody {
107
69
  this.root = root;
108
70
  this.#entropy = entropy;
109
71
  }
110
- async seed(name) {
111
- const dir = join(this.root, folderName(name));
112
- const path = join(dir, 'seed');
72
+ // The seed the folder holds, or a drawn one written there the first time.
73
+ async seed() {
74
+ const path = join(this.root, 'seed');
113
75
  const kept = await readFile(path, 'utf8').catch((e) => (missing(e) ? undefined : Promise.reject(e)));
114
76
  if (kept !== undefined) {
115
77
  const text = kept.trim();
@@ -118,7 +80,7 @@ export class FolderCustody extends Custody {
118
80
  return unhex(text);
119
81
  }
120
82
  const seed = this.#entropy.draw(32);
121
- await mkdir(dir, { recursive: true, mode: 0o700 });
83
+ await mkdir(this.root, { recursive: true, mode: 0o700 });
122
84
  await writeFile(path, `${hex(seed)}\n`, { mode: 0o600, flag: 'wx' });
123
85
  return seed;
124
86
  }
@@ -0,0 +1,25 @@
1
+ import { Faculty, type AskSpec, type BeingClass, type JsonObject, type Stance } from '../being/index.ts';
2
+ import type { Loaded } from '../contract/index.ts';
3
+ export declare const CATALOGUE = "catalogue";
4
+ export type ModuleRow = {
5
+ version: string;
6
+ from: string;
7
+ };
8
+ export interface Cataloguing {
9
+ load(from: string): Promise<Loaded | null>;
10
+ run(modules: readonly Loaded[]): string | null;
11
+ stands(kinds: readonly string[]): boolean;
12
+ kindsOf(module: string): string[];
13
+ }
14
+ export declare class Catalogue extends Faculty {
15
+ #private;
16
+ static readonly kind: string;
17
+ static cells: JsonObject;
18
+ static asks: Record<string, AskSpec>;
19
+ constructor(stance: Stance, harbor: Cataloguing);
20
+ wake(): Promise<void>;
21
+ add(args: JsonObject): Promise<JsonObject>;
22
+ remove(args: JsonObject): JsonObject;
23
+ modules(): JsonObject;
24
+ }
25
+ export declare const catalogueFor: (harbor: Cataloguing) => BeingClass;
@@ -0,0 +1,93 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The catalogue: the dock's first faculty, the code a harbor runs. Her
3
+ // cells name each module by the name it declares, its version, and where
4
+ // the terrain's loader finds it. The harbor wakes her before any other
5
+ // faculty, loads every module she names, and refuses to stand when one is
6
+ // missing.
7
+ //
8
+ // add { from } the module the loader finds there, run from now
9
+ // on; the same module again takes its new version
10
+ // remove { module } a module no being stands on, run no more
11
+ // modules what she names, each with the version running
12
+ //
13
+ // She is the kit's, of kind `org.nervur.catalogue`, and answers the root
14
+ // and an owner alone.
15
+ import { Faculty } from '../being/index.js';
16
+ import { ownerAsk } from '../ward/index.js';
17
+ export const CATALOGUE = 'catalogue';
18
+ const text = (v) => typeof v === 'string';
19
+ export class Catalogue extends Faculty {
20
+ static kind = 'org.nervur.catalogue';
21
+ static cells = { modules: {} };
22
+ static asks = {
23
+ add: ownerAsk('a module run from now on', { required: ['from'] }),
24
+ remove: ownerAsk('a module run no more', { required: ['module'] }),
25
+ modules: ownerAsk('the modules this harbor runs'),
26
+ };
27
+ #harbor;
28
+ #running = new Map();
29
+ constructor(stance, harbor) {
30
+ super(stance);
31
+ this.#harbor = harbor;
32
+ }
33
+ get #rows() {
34
+ return this.cells.modules;
35
+ }
36
+ // Every module her cells name, loaded and run, or a throw naming the one
37
+ // that cannot be.
38
+ async wake() {
39
+ for (const [module, row] of Object.entries(this.#rows)) {
40
+ const loaded = await this.#harbor.load(row.from);
41
+ if (loaded === null)
42
+ throw new Error(`module ${module} is not found at ${row.from}`);
43
+ if (loaded.module !== module)
44
+ throw new Error(`${row.from} is module ${loaded.module}, not ${module}`);
45
+ this.#running.set(module, loaded);
46
+ }
47
+ const refused = this.#harbor.run([...this.#running.values()]);
48
+ if (refused !== null)
49
+ throw new Error(refused);
50
+ }
51
+ async add(args) {
52
+ if (!text(args.from))
53
+ return { error: 'from is text' };
54
+ const loaded = await this.#harbor.load(args.from);
55
+ if (loaded === null)
56
+ return { error: 'no such module' };
57
+ const running = new Map(this.#running);
58
+ running.set(loaded.module, loaded);
59
+ const refused = this.#harbor.run([...running.values()]);
60
+ if (refused !== null)
61
+ return { error: refused };
62
+ this.#running.set(loaded.module, loaded);
63
+ this.#rows[loaded.module] = { version: loaded.version, from: loaded.from };
64
+ return { added: loaded.module, version: loaded.version };
65
+ }
66
+ remove(args) {
67
+ const module = args.module;
68
+ if (!text(module) || !this.#running.has(module))
69
+ return { error: 'no such module' };
70
+ if (this.#harbor.stands(this.#harbor.kindsOf(module)))
71
+ return { error: 'a being stands on this module' };
72
+ const running = new Map(this.#running);
73
+ running.delete(module);
74
+ this.#harbor.run([...running.values()]);
75
+ this.#running.delete(module);
76
+ delete this.#rows[module];
77
+ return { removed: module };
78
+ }
79
+ modules() {
80
+ const out = {};
81
+ for (const [module, row] of Object.entries(this.#rows))
82
+ out[module] = { version: row.version, running: this.#running.get(module)?.version ?? null, from: row.from };
83
+ return out;
84
+ }
85
+ }
86
+ // The catalogue class one harbor stands: a being is born of her class and
87
+ // her stance alone, so the class holds its harbor.
88
+ export const catalogueFor = (harbor) => class extends Catalogue {
89
+ static kind = Catalogue.kind;
90
+ constructor(stance) {
91
+ super(stance, harbor);
92
+ }
93
+ };
@@ -1,15 +1,25 @@
1
1
  import { type AskSpec, type Invitation, type JsonObject } from '../being/index.ts';
2
2
  import { WardBeing, type Lender, type WardBeingStance } from '../ward/index.ts';
3
3
  export interface Hosting {
4
- host(name: string, invitation: Invitation): Promise<boolean>;
4
+ host(name: string, invitation: Invitation, seed?: string): Promise<string | null>;
5
+ route(pk: string, at: string | null): boolean;
6
+ keep(): Promise<boolean>;
5
7
  }
8
+ export type Hosted = {
9
+ ward: string;
10
+ seed: string;
11
+ };
6
12
  export declare const BOX = "box";
7
13
  export declare class Dock extends WardBeing implements Lender {
8
14
  #private;
15
+ static readonly kind: string;
9
16
  static cells: JsonObject;
10
17
  static asks: Record<string, AskSpec>;
11
18
  constructor(stance: WardBeingStance, hosting: Hosting);
12
- get hosted(): string[];
19
+ get hosted(): Hosted[];
20
+ get routes(): Record<string, string>;
21
+ route(args: JsonObject): JsonObject;
22
+ standCatalogue(): Promise<void>;
13
23
  open(args: JsonObject): Promise<JsonObject>;
14
24
  host(args: JsonObject): Promise<JsonObject>;
15
25
  lend(args: JsonObject): Promise<JsonObject>;
@@ -5,23 +5,30 @@
5
5
  //
6
6
  // open { key, class } a faculty stands in the box ward, and the
7
7
  // dock holds a standing on her under her key
8
- // host { ward } a hosted ward stands, holding a standing on
9
- // the dock
8
+ // host { ward, seed? } a hosted ward stands under the seed named,
9
+ // or a drawn one, holding a standing on the
10
+ // dock; the dock keeps its name and seed
10
11
  // lend { contract } asked by a hosted ward: an offer from a
11
12
  // faculty that fulfils the contract
12
13
  // retract { contract, heir } an offer that was not taken
13
- import { Faculty } from '../being/index.js';
14
+ // route { ward, at } where a ward pk is reached, `host:port`, or
15
+ // null to forget it; the dock keeps it and
16
+ // the harbor hands it to its carrier
17
+ import { Faculty, KIT } from '../being/index.js';
18
+ import { CATALOGUE } from './catalogue.js';
14
19
  import { readInvitation } from '../quo/index.js';
15
20
  import { ownerAsk, WardBeing } from '../ward/index.js';
16
21
  export const BOX = 'box';
17
22
  const text = (v) => typeof v === 'string';
18
23
  const hosted = (_, notes) => notes?.hosted === true;
19
24
  export class Dock extends WardBeing {
20
- static cells = { hosted: [] };
25
+ static kind = 'org.nervur.dock';
26
+ static cells = { hosted: [], routes: {} };
21
27
  static asks = {
22
28
  ...WardBeing.asks,
23
29
  open: ownerAsk('a faculty of this harbor', { required: ['key', 'class'] }),
24
30
  host: ownerAsk('a ward this harbor hosts', { required: ['ward'] }),
31
+ route: ownerAsk('where a ward is reached, or null', { required: ['ward', 'at'] }),
25
32
  lend: { description: 'an offer from a faculty fulfilling a contract', input: { type: 'object', required: ['contract'] }, for: hosted },
26
33
  retract: { description: 'an offer that was not taken', input: { type: 'object', required: ['contract', 'heir'] }, for: hosted },
27
34
  };
@@ -31,28 +38,51 @@ export class Dock extends WardBeing {
31
38
  this.#hosting = hosting;
32
39
  }
33
40
  get hosted() {
34
- return [...this.cells.hosted];
41
+ return this.cells.hosted.map(({ ward, seed }) => ({ ward, seed }));
42
+ }
43
+ get routes() {
44
+ return { ...this.cells.routes };
45
+ }
46
+ route(args) {
47
+ const { ward, at } = args;
48
+ if (!text(ward) || (at !== null && !text(at)) || !this.#hosting.route(ward, at))
49
+ return { error: 'not routed' };
50
+ const routes = this.cells.routes;
51
+ if (at === null)
52
+ delete routes[ward];
53
+ else
54
+ routes[ward] = at;
55
+ return { routed: ward };
56
+ }
57
+ // Genesis: the catalogue stands first, and is kept at once.
58
+ async standCatalogue() {
59
+ const kind = 'org.nervur.catalogue';
60
+ if ((await this.stance.boot(kind, CATALOGUE, CATALOGUE)) === null || !(await this.#hosting.keep()))
61
+ throw new Error('the catalogue did not stand');
35
62
  }
36
63
  async open(args) {
37
64
  if (!text(args.key) || !text(args.class))
38
65
  return { error: 'key and class are text' };
39
- if (!Faculty.fulfils(this.steward.classOf(args.class)))
66
+ if (args.class.startsWith(KIT) || !Faculty.fulfils(this.steward.classOf(args.class)))
40
67
  return { error: 'no such faculty' };
41
68
  const opened = await this.stance.boot(args.class, args.key, args.key);
42
69
  return opened === null ? { error: 'not opened' } : { opened };
43
70
  }
44
71
  async host(args) {
45
72
  const name = args.ward;
46
- if (!text(name) || name === BOX || this.hosted.includes(name))
73
+ if (!text(name) || name === BOX || this.hosted.some((h) => h.ward === name))
47
74
  return { error: 'no such ward to host' };
75
+ if (args.seed !== undefined && !text(args.seed))
76
+ return { error: 'seed is text' };
48
77
  const invitation = await this.stance.occupants.invite(name, { hosted: true });
49
78
  if (invitation === null)
50
79
  return { error: 'not hosted' };
51
- if (!(await this.#hosting.host(name, invitation))) {
80
+ const seed = await this.#hosting.host(name, invitation, args.seed);
81
+ if (seed === null) {
52
82
  this.stance.occupants.remove(name);
53
83
  return { error: 'not hosted' };
54
84
  }
55
- this.cells.hosted.push(name);
85
+ this.cells.hosted.push({ ward: name, seed });
56
86
  return { hosted: name };
57
87
  }
58
88
  async lend(args) {