nervur 0.20.0 → 0.20.2

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 (157) hide show
  1. package/README.md +229 -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/browser/index.d.ts +35 -0
  12. package/dist/browser/index.js +188 -0
  13. package/dist/browser/worker.d.ts +12 -0
  14. package/dist/browser/worker.js +40 -0
  15. package/dist/browser.d.ts +1 -0
  16. package/dist/browser.js +7 -0
  17. package/dist/cli/command.d.ts +13 -0
  18. package/dist/cli/command.js +237 -0
  19. package/dist/cli/harbor.d.ts +19 -0
  20. package/dist/cli/harbor.js +99 -0
  21. package/dist/cli/main.d.ts +2 -0
  22. package/dist/cli/main.js +14 -0
  23. package/dist/contract/index.d.ts +16 -2
  24. package/dist/contract/index.js +13 -3
  25. package/dist/edge/index.d.ts +57 -0
  26. package/dist/edge/index.js +306 -0
  27. package/dist/edge.d.ts +1 -0
  28. package/dist/edge.js +8 -0
  29. package/dist/folder/index.d.ts +9 -4
  30. package/dist/folder/index.js +89 -84
  31. package/dist/harbor/carrying.d.ts +17 -0
  32. package/dist/harbor/carrying.js +38 -0
  33. package/dist/harbor/catalogue.d.ts +27 -0
  34. package/dist/harbor/catalogue.js +101 -0
  35. package/dist/harbor/dock.d.ts +32 -3
  36. package/dist/harbor/dock.js +98 -12
  37. package/dist/harbor/harbor.d.ts +27 -8
  38. package/dist/harbor/harbor.js +217 -26
  39. package/dist/harbor/index.d.ts +8 -2
  40. package/dist/harbor/index.js +8 -1
  41. package/dist/harbor/package.d.ts +12 -0
  42. package/dist/harbor/package.js +130 -0
  43. package/dist/harbor/probe.d.ts +18 -0
  44. package/dist/harbor/probe.js +15 -0
  45. package/dist/harbor/registry.d.ts +6 -3
  46. package/dist/harbor/registry.js +83 -7
  47. package/dist/harbor/relay.d.ts +15 -0
  48. package/dist/harbor/relay.js +387 -0
  49. package/dist/harbor/root-line.d.ts +9 -0
  50. package/dist/harbor/root-line.js +38 -0
  51. package/dist/harbor/terrain.d.ts +14 -1
  52. package/dist/harbor/terrain.js +19 -0
  53. package/dist/http/index.d.ts +4 -0
  54. package/dist/http/index.js +156 -0
  55. package/dist/http/websocket.d.ts +29 -0
  56. package/dist/http/websocket.js +133 -0
  57. package/dist/index.d.ts +5 -4
  58. package/dist/index.js +12 -6
  59. package/dist/line/answer.d.ts +8 -0
  60. package/dist/line/answer.js +60 -0
  61. package/dist/line/frame.d.ts +34 -0
  62. package/dist/line/frame.js +81 -0
  63. package/dist/line/ground.d.ts +3 -0
  64. package/dist/line/ground.js +9 -0
  65. package/dist/line/index.d.ts +5 -0
  66. package/dist/line/index.js +9 -0
  67. package/dist/line/push.d.ts +7 -0
  68. package/dist/line/push.js +92 -0
  69. package/dist/line/web.d.ts +30 -0
  70. package/dist/line/web.js +181 -0
  71. package/dist/node/index.d.ts +32 -0
  72. package/dist/node/index.js +164 -0
  73. package/dist/node.d.ts +1 -0
  74. package/dist/node.js +7 -0
  75. package/dist/pointer/bodies.d.ts +11 -3
  76. package/dist/pointer/bodies.js +47 -18
  77. package/dist/pointer/index.d.ts +1 -1
  78. package/dist/pointer/index.js +1 -1
  79. package/dist/pointer/world.d.ts +12 -5
  80. package/dist/pointer/world.js +29 -14
  81. package/dist/quo/address.d.ts +19 -0
  82. package/dist/quo/address.js +84 -0
  83. package/dist/quo/door.d.ts +1 -1
  84. package/dist/quo/door.js +6 -4
  85. package/dist/quo/index.d.ts +1 -0
  86. package/dist/quo/index.js +1 -0
  87. package/dist/quo/invitation.d.ts +1 -0
  88. package/dist/quo/invitation.js +9 -5
  89. package/dist/quo/keys.js +1 -1
  90. package/dist/tcp/frame.d.ts +25 -0
  91. package/dist/tcp/frame.js +64 -0
  92. package/dist/tcp/index.d.ts +24 -0
  93. package/dist/tcp/index.js +260 -0
  94. package/dist/ward/index.d.ts +3 -2
  95. package/dist/ward/index.js +2 -1
  96. package/dist/ward/pilot.d.ts +8 -0
  97. package/dist/ward/pilot.js +50 -0
  98. package/dist/ward/stance.d.ts +4 -4
  99. package/dist/ward/stance.js +10 -7
  100. package/dist/ward/ward-being.d.ts +4 -1
  101. package/dist/ward/ward-being.js +8 -2
  102. package/dist/ward/ward.d.ts +11 -2
  103. package/dist/ward/ward.js +41 -5
  104. package/package.json +28 -2
  105. package/src/being/being.ts +5 -1
  106. package/src/being/faculty.ts +14 -7
  107. package/src/being/index.ts +1 -0
  108. package/src/being/kind.ts +30 -0
  109. package/src/being/types.ts +8 -6
  110. package/src/browser/index.ts +207 -0
  111. package/src/browser/worker.ts +66 -0
  112. package/src/browser.ts +9 -0
  113. package/src/cli/command.ts +224 -0
  114. package/src/cli/harbor.ts +103 -0
  115. package/src/cli/main.ts +13 -0
  116. package/src/contract/index.ts +42 -10
  117. package/src/edge/index.ts +359 -0
  118. package/src/edge.ts +10 -0
  119. package/src/folder/index.ts +87 -84
  120. package/src/harbor/carrying.ts +56 -0
  121. package/src/harbor/catalogue.ts +125 -0
  122. package/src/harbor/dock.ts +122 -15
  123. package/src/harbor/harbor.ts +240 -36
  124. package/src/harbor/index.ts +10 -3
  125. package/src/harbor/package.ts +147 -0
  126. package/src/harbor/probe.ts +34 -0
  127. package/src/harbor/registry.ts +73 -9
  128. package/src/harbor/relay.ts +399 -0
  129. package/src/harbor/root-line.ts +39 -0
  130. package/src/harbor/terrain.ts +31 -3
  131. package/src/http/index.ts +150 -0
  132. package/src/http/websocket.ts +124 -0
  133. package/src/index.ts +39 -6
  134. package/src/line/answer.ts +59 -0
  135. package/src/line/frame.ts +88 -0
  136. package/src/line/ground.ts +19 -0
  137. package/src/line/index.ts +9 -0
  138. package/src/line/push.ts +112 -0
  139. package/src/line/web.ts +227 -0
  140. package/src/node/index.ts +170 -0
  141. package/src/node.ts +9 -0
  142. package/src/pointer/bodies.ts +45 -16
  143. package/src/pointer/index.ts +1 -1
  144. package/src/pointer/world.ts +38 -19
  145. package/src/quo/address.ts +87 -0
  146. package/src/quo/door.ts +6 -4
  147. package/src/quo/index.ts +1 -0
  148. package/src/quo/invitation.ts +10 -6
  149. package/src/quo/keys.ts +1 -1
  150. package/src/stand/main.ts +1 -0
  151. package/src/stand/stand.ts +87 -6
  152. package/src/tcp/index.ts +284 -0
  153. package/src/ward/index.ts +3 -2
  154. package/src/ward/pilot.ts +50 -0
  155. package/src/ward/stance.ts +14 -8
  156. package/src/ward/ward-being.ts +9 -3
  157. package/src/ward/ward.ts +53 -6
@@ -0,0 +1,125 @@
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, and she runs every module she names that is found and splits
6
+ // no kind. One that is not stays out alone, with its reason, and the
7
+ // beings of its kinds stand absent.
8
+ //
9
+ // add { from } the module the loader finds there, run from now
10
+ // on; the same module again takes its new version
11
+ // remove { module } a module no being stands on, by the kinds kept
12
+ // for it, run no more
13
+ // modules what she names, each with the version running,
14
+ // and why one does not run
15
+ //
16
+ // She is the kit's, of kind `org.nervur.catalogue`, and answers the root
17
+ // and an owner alone.
18
+ import { Faculty, type AskSpec, type BeingClass, type JsonObject, type Stance } from '../being/index.ts';
19
+ import type { Loaded } from '../contract/index.ts';
20
+ import { ownerAsk } from '../ward/index.ts';
21
+
22
+ export const CATALOGUE = 'catalogue';
23
+
24
+ // One module, as her cells keep it.
25
+ // Its kinds are kept beside it, so a module that does not load is still
26
+ // known by the beings that stand on it.
27
+ export type ModuleRow = { version: string; from: string; kinds: string[] };
28
+
29
+ // What the catalogue asks of her harbor.
30
+ export interface Cataloguing {
31
+ load(from: string): Promise<Loaded | null>;
32
+ // Why these modules cannot run together, or null.
33
+ check(modules: readonly Loaded[]): string | null;
34
+ // The modules run from now on, or why they cannot be.
35
+ run(modules: readonly Loaded[]): string | null;
36
+ // Whether a being of any ward stands on one of these kinds.
37
+ stands(kinds: readonly string[]): boolean;
38
+ kindsOf(module: string): string[];
39
+ }
40
+
41
+ const text = (v: unknown): v is string => typeof v === 'string';
42
+
43
+ export class Catalogue extends Faculty {
44
+ static override readonly kind: string = 'org.nervur.catalogue';
45
+ static override cells: JsonObject = { modules: {} };
46
+ static override asks: Record<string, AskSpec> = {
47
+ add: ownerAsk('a module run from now on', { required: ['from'] }),
48
+ remove: ownerAsk('a module run no more', { required: ['module'] }),
49
+ modules: ownerAsk('the modules this harbor runs'),
50
+ };
51
+
52
+ readonly #harbor: Cataloguing;
53
+ readonly #running = new Map<string, Loaded>();
54
+ readonly #failed = new Map<string, string>();
55
+
56
+ constructor(stance: Stance, harbor: Cataloguing) {
57
+ super(stance);
58
+ this.#harbor = harbor;
59
+ }
60
+
61
+ get #rows(): Record<string, ModuleRow> {
62
+ return this.cells.modules as Record<string, ModuleRow>;
63
+ }
64
+
65
+ // Every module her cells name, in their order, loaded and run where it
66
+ // is found, is the module named, and splits no kind with those before
67
+ // it. The rest stay out, each with its reason.
68
+ async wake(): Promise<void> {
69
+ for (const [module, row] of Object.entries(this.#rows)) {
70
+ const loaded = await this.#harbor.load(row.from).catch(() => null);
71
+ const refused =
72
+ loaded === null ? `not found at ${row.from}` : loaded.module !== module ? `${row.from} is module ${loaded.module}` : this.#harbor.check([...this.#running.values(), loaded]);
73
+ if (refused === null) this.#running.set(module, loaded!);
74
+ else this.#failed.set(module, refused);
75
+ }
76
+ this.#harbor.run([...this.#running.values()]);
77
+ }
78
+
79
+ async add(args: JsonObject): Promise<JsonObject> {
80
+ if (!text(args.from)) return { error: 'from is text' };
81
+ const loaded = await this.#harbor.load(args.from);
82
+ if (loaded === null) return { error: 'no such module' };
83
+ const running = new Map(this.#running);
84
+ running.set(loaded.module, loaded);
85
+ const refused = this.#harbor.run([...running.values()]);
86
+ if (refused !== null) return { error: refused };
87
+ this.#running.set(loaded.module, loaded);
88
+ this.#failed.delete(loaded.module);
89
+ this.#rows[loaded.module] = { version: loaded.version, from: loaded.from, kinds: this.#harbor.kindsOf(loaded.module) };
90
+ return { added: loaded.module, version: loaded.version };
91
+ }
92
+
93
+ remove(args: JsonObject): JsonObject {
94
+ const module = args.module;
95
+ if (!text(module) || !Object.hasOwn(this.#rows, module)) return { error: 'no such module' };
96
+ const kinds = new Set([...this.#rows[module]!.kinds, ...this.#harbor.kindsOf(module)]);
97
+ if (this.#harbor.stands([...kinds])) return { error: 'a being stands on this module' };
98
+ const running = new Map(this.#running);
99
+ running.delete(module);
100
+ this.#harbor.run([...running.values()]);
101
+ this.#running.delete(module);
102
+ this.#failed.delete(module);
103
+ delete this.#rows[module];
104
+ return { removed: module };
105
+ }
106
+
107
+ modules(): JsonObject {
108
+ const out: JsonObject = {};
109
+ for (const [module, row] of Object.entries(this.#rows)) {
110
+ const failed = this.#failed.get(module);
111
+ out[module] = { version: row.version, running: this.#running.get(module)?.version ?? null, from: row.from, ...(failed === undefined ? {} : { failed }) };
112
+ }
113
+ return out;
114
+ }
115
+ }
116
+
117
+ // The catalogue class one harbor stands: a being is born of her class and
118
+ // her stance alone, so the class holds its harbor.
119
+ export const catalogueFor = (harbor: Cataloguing): BeingClass =>
120
+ class extends Catalogue {
121
+ static override readonly kind: string = Catalogue.kind;
122
+ constructor(stance: Stance) {
123
+ super(stance, harbor);
124
+ }
125
+ };
@@ -5,30 +5,69 @@
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, type Asker, type AskSpec, type Invitation, type JsonObject } from '../being/index.ts';
14
- import { readInvitation } from '../quo/index.ts';
15
- import { ownerAsk, WardBeing, type Lender, type WardBeingStance } from '../ward/index.ts';
14
+ // route { ward, at } where a ward pk is reached, addresses in the
15
+ // order they are tried, or null to forget
16
+ // it; the dock keeps it and the harbor hands
17
+ // it to its carrier
18
+ // reach { at } where this harbor is reached, the
19
+ // addresses every invitation it gives carries
20
+ //
21
+ // A take of an invitation that carries `at` teaches the dock that ward's
22
+ // route, where the root set none.
23
+ import { Faculty, KIT, type Asker, type AskSpec, type Blueprint, type Invitation, type JsonObject } from '../being/index.ts';
24
+ import { CATALOGUE } from './catalogue.ts';
25
+ import { readAt, readInvitation } from '../quo/index.ts';
26
+ import { ownerAsk, pilots, rootAsk, WardBeing, type Lender, type WardBeingStance } from '../ward/index.ts';
16
27
 
17
- // What the dock asks of her harbor.
28
+ // What the dock asks of her harbor: a ward standing, and the seed, as hex,
29
+ // it stands under.
18
30
  export interface Hosting {
19
- host(name: string, invitation: Invitation): Promise<boolean>;
31
+ host(name: string, invitation: Invitation, seed?: string): Promise<string | null>;
32
+ // Whether the harbor's carrier took the route.
33
+ route(pk: string, at: readonly string[] | null): boolean;
34
+ // Whether the box ward's partition was kept.
35
+ keep(): Promise<boolean>;
36
+ // What did not stand when the harbor opened.
37
+ failures(): Failures;
38
+ // Every place of the memory the dock does not name, gone.
39
+ sweep(): Promise<string[]>;
40
+ // Whether a kind is a carrier this harbor's ground ships.
41
+ grounds(kind: string): boolean;
42
+ // A faculty just opened, woken where it wakes, and one about to go, put
43
+ // to sleep.
44
+ awaken(key: string): Promise<void>;
45
+ lull(key: string): Promise<void>;
20
46
  }
21
47
 
48
+ // One hosted ward, as the dock's cells keep it.
49
+ export type Hosted = { ward: string; seed: string };
50
+
51
+ // Each hosted ward that did not unpack and each route the carrier did not
52
+ // take, with the reason, by name and by ward pk.
53
+ export type Failures = { wards: Record<string, string>; routes: Record<string, string> };
54
+
22
55
  export const BOX = 'box';
23
56
  const text = (v: unknown): v is string => typeof v === 'string';
57
+ // A list of addresses, each a URI with a scheme, and at least one.
58
+ const addresses = (v: unknown): v is string[] => Array.isArray(v) && v.length > 0 && readAt(v).length === v.length;
24
59
  const hosted: AskSpec['for'] = (_: Asker, notes: JsonObject | undefined) => notes?.hosted === true;
25
60
 
26
61
  export class Dock extends WardBeing implements Lender {
27
- static override cells: JsonObject = { hosted: [] };
62
+ static override readonly kind: string = 'org.nervur.dock';
63
+ static override cells: JsonObject = { hosted: [], routes: {}, reach: [] };
28
64
  static override asks: Record<string, AskSpec> = {
29
65
  ...WardBeing.asks,
30
66
  open: ownerAsk('a faculty of this harbor', { required: ['key', 'class'] }),
31
67
  host: ownerAsk('a ward this harbor hosts', { required: ['ward'] }),
68
+ route: ownerAsk('where a ward is reached, addresses in order, or null', { required: ['ward', 'at'] }),
69
+ reach: ownerAsk('where this harbor is reached, the addresses every invitation carries', { required: ['at'] }),
70
+ sweep: rootAsk('every place of the memory this harbor does not name, gone'),
32
71
  lend: { description: 'an offer from a faculty fulfilling a contract', input: { type: 'object', required: ['contract'] }, for: hosted },
33
72
  retract: { description: 'an offer that was not taken', input: { type: 'object', required: ['contract', 'heir'] }, for: hosted },
34
73
  };
@@ -40,27 +79,95 @@ export class Dock extends WardBeing implements Lender {
40
79
  this.#hosting = hosting;
41
80
  }
42
81
 
43
- get hosted(): string[] {
44
- return [...(this.cells.hosted as string[])];
82
+ // The census adds what did not stand, for whoever pilots.
83
+ override describe(asker: Asker): Blueprint {
84
+ const blueprint = super.describe(asker);
85
+ if (pilots(asker, this.notes(asker))) (blueprint.notes as JsonObject).failed = this.#hosting.failures();
86
+ return blueprint;
87
+ }
88
+
89
+ get hosted(): Hosted[] {
90
+ return (this.cells.hosted as Hosted[]).map(({ ward, seed }) => ({ ward, seed }));
91
+ }
92
+
93
+ get routes(): Record<string, string[]> {
94
+ return Object.fromEntries(Object.entries(this.cells.routes as Record<string, string[]>).map(([pk, at]) => [pk, [...at]]));
95
+ }
96
+
97
+ // Where this harbor is reached, in the order a caller tries it.
98
+ get addresses(): string[] {
99
+ return [...(this.cells.reach as string[])];
100
+ }
101
+
102
+ route(args: JsonObject): JsonObject {
103
+ const { ward } = args;
104
+ const at = typeof args.at === 'string' ? [args.at] : args.at;
105
+ if (!text(ward) || (at !== null && !addresses(at)) || !this.#hosting.route(ward, at)) return { error: 'not routed' };
106
+ const routes = this.cells.routes as Record<string, string[]>;
107
+ if (at === null) delete routes[ward];
108
+ else routes[ward] = [...at];
109
+ return { routed: ward };
110
+ }
111
+
112
+ reach(args: JsonObject): JsonObject {
113
+ if (!addresses(args.at)) return { error: 'at is a list of addresses' };
114
+ this.cells.reach = [...args.at];
115
+ return { reach: [...args.at] };
116
+ }
117
+
118
+ // A route learned from an invitation's `at`, where the root set none,
119
+ // and where the carrier dials one of its addresses.
120
+ async learn(ward: string, at: readonly string[]): Promise<void> {
121
+ const routes = this.cells.routes as Record<string, string[]>;
122
+ if (Object.hasOwn(routes, ward) || !this.#hosting.route(ward, at)) return;
123
+ routes[ward] = [...at];
124
+ await this.#hosting.keep();
125
+ }
126
+
127
+ // Genesis: the catalogue stands first, and is kept at once.
128
+ async standCatalogue(): Promise<void> {
129
+ const kind = 'org.nervur.catalogue';
130
+ if ((await this.stance.boot(kind, CATALOGUE, CATALOGUE)) === null || !(await this.#hosting.keep())) throw new Error('the catalogue did not stand');
131
+ }
132
+
133
+ // A faculty that goes stops listening first.
134
+ override async unboot(args: JsonObject): Promise<JsonObject> {
135
+ if (typeof args.being === 'string') await this.#hosting.lull(args.being);
136
+ return super.unboot(args);
137
+ }
138
+
139
+ // Genesis: the carriers the terrain names stand next, each kept at once.
140
+ async standCarriers(opens: readonly { key: string; kind: string }[]): Promise<void> {
141
+ for (const { key, kind } of opens) await this.open({ key, class: kind });
142
+ await this.#hosting.keep();
45
143
  }
46
144
 
47
145
  async open(args: JsonObject): Promise<JsonObject> {
48
146
  if (!text(args.key) || !text(args.class)) return { error: 'key and class are text' };
49
- if (!Faculty.fulfils(this.steward.classOf(args.class))) return { error: 'no such faculty' };
147
+ const mine = args.class.startsWith(KIT) ? this.#hosting.grounds(args.class) : true;
148
+ if (!mine || !Faculty.fulfils(this.steward.classOf(args.class))) return { error: 'no such faculty' };
50
149
  const opened = await this.stance.boot(args.class, args.key, args.key);
51
- return opened === null ? { error: 'not opened' } : { opened };
150
+ if (opened === null) return { error: 'not opened' };
151
+ await this.#hosting.awaken(opened);
152
+ return { opened };
153
+ }
154
+
155
+ async sweep(): Promise<JsonObject> {
156
+ return { swept: await this.#hosting.sweep() };
52
157
  }
53
158
 
54
159
  async host(args: JsonObject): Promise<JsonObject> {
55
160
  const name = args.ward;
56
- if (!text(name) || name === BOX || this.hosted.includes(name)) return { error: 'no such ward to host' };
161
+ if (!text(name) || name === BOX || this.hosted.some((h) => h.ward === name)) return { error: 'no such ward to host' };
162
+ if (args.seed !== undefined && !text(args.seed)) return { error: 'seed is text' };
57
163
  const invitation = await this.stance.occupants.invite(name, { hosted: true });
58
164
  if (invitation === null) return { error: 'not hosted' };
59
- if (!(await this.#hosting.host(name, invitation))) {
165
+ const seed = await this.#hosting.host(name, invitation, args.seed);
166
+ if (seed === null) {
60
167
  this.stance.occupants.remove(name);
61
168
  return { error: 'not hosted' };
62
169
  }
63
- (this.cells.hosted as string[]).push(name);
170
+ (this.cells.hosted as Hosted[]).push({ ward: name, seed });
64
171
  return { hosted: name };
65
172
  }
66
173
 
@@ -1,39 +1,121 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The harbor: the onion, unpacked from a terrain and a catalogue.
2
+ // The harbor: the onion, unpacked from a terrain alone.
3
3
  //
4
- // 1. custody and memory the box ward's seed and rows, from the terrain
5
- // 2. the box ward unpacked as every ward is, its ward-being the dock
6
- // 3. the faculties the box ward's beings, each from her own row
7
- // 4. the hosted wards as the dock's cells list them
8
- // 5. the beings in each ward, each from her own row
4
+ // 1. the seed and the package the harbor's one secret, from custody,
5
+ // opens its sealed memory
6
+ // 2. the box ward unpacked as every ward is, under the
7
+ // harbor's seed, its ward-being the dock
8
+ // 3. the catalogue the dock's first faculty, a kit class:
9
+ // every module her cells name is loaded
10
+ // through the terrain, or the harbor does
11
+ // not stand
12
+ // 4. the faculties the box ward's other beings, each from her
13
+ // own row, by the kinds the modules bring
14
+ // 5. the routes as the dock's cells keep them, handed to
15
+ // the terrain's carrier
16
+ // 6. the hosted wards as the dock's cells list them, each under
17
+ // the seed the dock keeps for it
18
+ // 7. the beings in each ward, each from her own row
19
+ //
20
+ // The harbor fails only where the seed or the box ward's own place cannot
21
+ // be read. A module, a route, a hosted ward or a being that cannot stand
22
+ // fails alone, and the census names it.
9
23
  //
10
24
  // Genesis and wake are one path: empty memory unpacks a box ward holding
11
- // the dock alone, and the root's asks write the rest.
25
+ // the dock, the dock stands the catalogue, and the root's asks write the
26
+ // rest.
12
27
  import type { Answer, BeingClass, Invitation, JsonObject } from '../being/index.ts';
13
- import { Carrier } from '../contract/index.ts';
14
- import { DOCK, Ward, type WardParts } from '../ward/index.ts';
15
- import { BOX, Dock, type Hosting } from './dock.ts';
28
+ import { Carrier, type Loaded } from '../contract/index.ts';
29
+ import { hex, isHex, unhex } from '../crypto/index.ts';
30
+ import { unspecified, WardKey } from '../quo/index.ts';
31
+ import { DOCK, Pilot, Ward, type Catalogue as Resolving, type Routes, type WardParts } from '../ward/index.ts';
32
+ import { Catalogue, CATALOGUE, catalogueFor, type Cataloguing } from './catalogue.ts';
33
+ import { CarrierFaculty, UNDIALED } from './carrying.ts';
34
+ import { BOX, Dock, type Failures, type Hosting } from './dock.ts';
35
+ import { Package } from './package.ts';
16
36
  import { Registry } from './registry.ts';
17
- import type { Terrain } from './terrain.ts';
37
+ import { relayClient, relayFaculty } from './relay.ts';
38
+ import { probeGround, type Probing } from './probe.ts';
39
+ import { Terrain } from './terrain.ts';
18
40
 
19
- export class Harbor extends Carrier implements Hosting {
41
+ export class Harbor extends Carrier implements Hosting, Cataloguing, Resolving {
20
42
  readonly terrain: Terrain;
21
- readonly registry: Registry;
43
+ readonly #package: Package;
22
44
  readonly #wards = new Map<string, Ward>();
45
+ readonly #kit: BeingClass[];
46
+ readonly #grounds: Set<string>;
47
+ #registry: Registry;
48
+ readonly #failed: Failures = { wards: {}, routes: {} };
23
49
 
24
- private constructor(terrain: Terrain, registry: Registry) {
50
+ private constructor(terrain: Terrain, pack: Package) {
25
51
  super();
26
52
  this.terrain = terrain;
27
- this.registry = registry;
53
+ this.#package = pack;
54
+ const faculties = [...terrain.faculties.map((make) => make(this)), relayFaculty(terrain.clock), relayClient(this, terrain.clock)];
55
+ this.#grounds = new Set(faculties.map((C) => C.kind));
56
+ this.#kit = [catalogueFor(this), Pilot, ...faculties];
57
+ this.#registry = new Registry(this.#kit, []);
58
+ }
59
+
60
+ // A harbor on the terrain handed in, or, with none, on the ground the
61
+ // probe finds here. A ground another run holds refuses, and what the
62
+ // ground serves beside the harbor stands once it is whole.
63
+ static async open(from: Terrain | Probing = {}): Promise<Harbor> {
64
+ const terrain = from instanceof Terrain ? from : probeGround(from).terrain;
65
+ await terrain.claim();
66
+ let harbor: Harbor;
67
+ try {
68
+ harbor = await Harbor.#stand(terrain);
69
+ } catch (e) {
70
+ await terrain.release();
71
+ throw e;
72
+ }
73
+ try {
74
+ await terrain.stand(harbor);
75
+ } catch (e) {
76
+ await harbor.close();
77
+ throw e;
78
+ }
79
+ return harbor;
28
80
  }
29
81
 
30
- static async open(terrain: Terrain, classes: readonly BeingClass[]): Promise<Harbor> {
31
- const harbor = new Harbor(terrain, new Registry(classes));
32
- await harbor.#unpack(BOX, undefined, (stance) => new Dock(stance, harbor));
33
- for (const name of harbor.dock.hosted) await harbor.#unpack(name);
82
+ static async #stand(terrain: Terrain): Promise<Harbor> {
83
+ const seed = await terrain.custody.seed();
84
+ const harbor = new Harbor(terrain, await Package.open(terrain.memory, terrain.entropy, seed));
85
+ await harbor.#unpack(BOX, seed, undefined, (stance) => new Dock(stance, harbor));
86
+ const genesis = !harbor.box.partition.being(CATALOGUE);
87
+ if (genesis) await harbor.dock.standCatalogue();
88
+ await harbor.catalogue.wake();
89
+ if (genesis) await harbor.dock.standCarriers(terrain.opens);
90
+ else if (terrain.wakes) for (const carrier of harbor.carriers) await carrier.awake();
91
+ for (const [pk, at] of Object.entries(harbor.dock.routes)) {
92
+ if (!harbor.#dialable(at)) harbor.#failed.routes[pk] = 'no carrier dials it';
93
+ }
94
+ for (const hosted of harbor.dock.hosted) {
95
+ try {
96
+ await harbor.#unpack(hosted.ward, unhex(hosted.seed));
97
+ } catch (e) {
98
+ harbor.#failed.wards[hosted.ward] = e instanceof Error ? e.message : String(e);
99
+ }
100
+ }
34
101
  return harbor;
35
102
  }
36
103
 
104
+ // Hosting: every place of the terrain's memory the dock does not name,
105
+ // gone. A crash inside `host` leaves such a place, sealed under a seed
106
+ // nothing kept, and nothing else writes one.
107
+ async sweep(): Promise<string[]> {
108
+ const mine = new Set(await Promise.all([BOX, ...this.dock.hosted.map((h) => h.ward)].map((name) => this.#package.place(name))));
109
+ const swept = (await this.terrain.memory.places()).filter((place) => !mine.has(place));
110
+ for (const place of swept) await this.terrain.memory.forget(place);
111
+ return swept;
112
+ }
113
+
114
+ // Hosting: what did not stand when the harbor opened, each alone.
115
+ failures(): Failures {
116
+ return { wards: { ...this.#failed.wards }, routes: { ...this.#failed.routes } };
117
+ }
118
+
37
119
  get box(): Ward {
38
120
  return this.#wards.get(BOX)!;
39
121
  }
@@ -42,6 +124,14 @@ export class Harbor extends Carrier implements Hosting {
42
124
  return this.box.wardBeing as Dock;
43
125
  }
44
126
 
127
+ get catalogue(): Catalogue {
128
+ return this.box.being(CATALOGUE) as Catalogue;
129
+ }
130
+
131
+ get registry(): Registry {
132
+ return this.#registry;
133
+ }
134
+
45
135
  ward(name: string): Ward | undefined {
46
136
  return this.#wards.get(name);
47
137
  }
@@ -59,45 +149,159 @@ export class Harbor extends Carrier implements Hosting {
59
149
  return this.#door(pk) !== undefined;
60
150
  }
61
151
 
62
- // Bytes for one of this harbor's doors, or nothing.
152
+ // Bytes for one of this harbor's doors, or for a ward a relay of this
153
+ // harbor answers for, or nothing.
63
154
  async arrive(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
64
155
  const ward = this.#door(pk);
65
- return ward ? (await ward.door.arrive(bytes)).bytes : null;
156
+ if (ward) return (await ward.door.arrive(bytes)).bytes;
157
+ return (await this.#relaying(pk)?.relay(pk, bytes)) ?? null;
158
+ }
159
+
160
+ // Carrier: this harbor's own doors and relays first, then a route's
161
+ // addresses in order, each to the carrier that dials it, and with no
162
+ // route the terrain's carrier.
163
+ async carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
164
+ if (this.holds(pk) || this.#relaying(pk)) return this.arrive(pk, bytes);
165
+ const at = this.dock.routes[pk];
166
+ if (at === undefined) return this.terrain.carrier.carry(pk, bytes);
167
+ for (const address of at) {
168
+ const carrier = this.carriers.find((c) => c.accepts(address));
169
+ const out = carrier ? await carrier.dial(address, pk, bytes) : UNDIALED;
170
+ if (out !== UNDIALED) return out;
171
+ }
172
+ return null;
66
173
  }
67
174
 
68
- // Carrier: this harbor's own doors first, then the terrain's.
69
- carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
70
- return this.holds(pk) ? this.arrive(pk, bytes) : this.terrain.carrier.carry(pk, bytes);
175
+ // Resolving: a kind, by the registry the catalogue runs.
176
+ classOf(kind: string): BeingClass | undefined {
177
+ return this.#registry.classOf(kind);
71
178
  }
72
179
 
73
- // Hosting: a ward the dock hosts stands now.
74
- async host(name: string, invitation: Invitation): Promise<boolean> {
75
- if (this.#wards.has(name)) return false;
180
+ // Cataloguing: code through the terrain, and the modules run.
181
+ load(from: string): Promise<Loaded | null> {
182
+ return this.terrain.loader.load(from);
183
+ }
184
+
185
+ check(modules: readonly Loaded[]): string | null {
186
+ return Registry.refusal(this.#kit, modules);
187
+ }
188
+
189
+ run(modules: readonly Loaded[]): string | null {
76
190
  try {
77
- await this.#unpack(name, invitation);
191
+ this.#registry = new Registry(this.#kit, modules);
192
+ } catch (e) {
193
+ return (e as Error).message;
194
+ }
195
+ for (const ward of this.#wards.values()) ward.bearAnew();
196
+ return null;
197
+ }
198
+
199
+ stands(kinds: readonly string[]): boolean {
200
+ for (const ward of this.#wards.values()) {
201
+ for (const row of Object.values(ward.partition.beings)) if (kinds.includes(row.class)) return true;
202
+ }
203
+ return false;
204
+ }
205
+
206
+ kindsOf(module: string): string[] {
207
+ return this.#registry.kindsOf(module);
208
+ }
209
+
210
+ // Hosting: a ward the dock hosts stands now, under the seed named or a
211
+ // drawn one, and the seed it stands under comes back. A ward whose key
212
+ // another ward of this harbor already has does not stand.
213
+ async host(name: string, invitation: Invitation, seed?: string): Promise<string | null> {
214
+ if (this.#wards.has(name)) return null;
215
+ const bytes = seed === undefined ? this.terrain.entropy.draw(32) : isHex(seed, 32) ? unhex(seed) : null;
216
+ if (bytes === null || this.holds((await WardKey.from(bytes)).pk)) return null;
217
+ try {
218
+ await this.#unpack(name, bytes, invitation);
78
219
  } catch {
79
- return false;
220
+ return null;
80
221
  }
81
- if (this.#wards.get(name)!.wardBeing.stance.standings.get(DOCK)) return true;
222
+ if (this.#wards.get(name)!.wardBeing.stance.standings.get(DOCK)) return hex(bytes);
82
223
  this.#wards.delete(name);
83
- return false;
224
+ return null;
225
+ }
226
+
227
+ keep(): Promise<boolean> {
228
+ return this.box.partition.kept();
229
+ }
230
+
231
+ // Hosting: a route is taken where a carrier of this harbor dials one of
232
+ // its addresses; forgetting one always is.
233
+ route(pk: string, at: readonly string[] | null): boolean {
234
+ const taken = isHex(pk, 64) && (at === null || this.#dialable(at));
235
+ if (taken) delete this.#failed.routes[pk];
236
+ return taken;
237
+ }
238
+
239
+ // Hosting: the carriers this ground ships, and their sleep and waking.
240
+ grounds(kind: string): boolean {
241
+ return this.#grounds.has(kind);
84
242
  }
85
243
 
86
- async #unpack(name: string, dock?: Invitation, root?: WardParts['root']): Promise<void> {
87
- const { entropy, clock, custody, memory } = this.terrain;
244
+ async awaken(key: string): Promise<void> {
245
+ const being = this.box.being(key);
246
+ if (being instanceof CarrierFaculty) await being.awake();
247
+ }
248
+
249
+ async lull(key: string): Promise<void> {
250
+ const being = this.box.being(key);
251
+ if (being instanceof CarrierFaculty) await being.sleep();
252
+ }
253
+
254
+ // The carrier faculties standing in the box ward.
255
+ get carriers(): CarrierFaculty[] {
256
+ return Object.keys(this.box.partition.beings)
257
+ .map((key) => this.box.being(key))
258
+ .filter((being): being is CarrierFaculty => being instanceof CarrierFaculty);
259
+ }
260
+
261
+ // Every carrier asleep, the last opened first: nothing listens and no
262
+ // line stays open.
263
+ async close(): Promise<void> {
264
+ for (const carrier of this.carriers.reverse()) await carrier.sleep();
265
+ await this.terrain.release();
266
+ }
267
+
268
+ #relaying(pk: string): CarrierFaculty | undefined {
269
+ return this.carriers.find((carrier) => carrier.relays(pk));
270
+ }
271
+
272
+ #dialable(at: readonly string[]): boolean {
273
+ return at.some((address) => this.carriers.some((carrier) => carrier.accepts(address)));
274
+ }
275
+
276
+ async #unpack(name: string, seed: Uint8Array, dock?: Invitation, root?: WardParts['root']): Promise<void> {
277
+ const { entropy, clock } = this.terrain;
88
278
  const ward = await Ward.unpack({
89
- seed: await custody.seed(name),
90
- memory: await memory.open(name),
91
- catalogue: this.registry,
279
+ seed,
280
+ memory: await this.#package.rows(name),
281
+ catalogue: this,
92
282
  entropy,
93
283
  clock,
94
284
  carrier: this,
285
+ routes: this.#routes,
95
286
  ...(dock ? { dock } : {}),
96
287
  ...(root ? { root } : {}),
97
288
  });
98
289
  this.#wards.set(name, ward);
99
290
  }
100
291
 
292
+ // Every ward is reached where the harbor is: where the root's `reach`
293
+ // says, and where it said nothing, where its carriers listen, save the
294
+ // unspecified host. What a ward learns from an invitation's `at` is the
295
+ // dock's to keep. A ward of this harbor needs no route.
296
+ readonly #routes: Routes = {
297
+ reach: () => {
298
+ if (!this.#wards.has(BOX)) return [];
299
+ const set = this.dock.addresses;
300
+ return set.length > 0 ? set : this.carriers.flatMap((carrier) => carrier.listening()).filter((address) => !unspecified(address));
301
+ },
302
+ learn: (ward, at) => (this.holds(ward) ? Promise.resolve() : this.dock.learn(ward, at)),
303
+ };
304
+
101
305
  #door(pk: string): Ward | undefined {
102
306
  for (const ward of this.#wards.values()) if (ward.pk === pk) return ward;
103
307
  return undefined;
@@ -1,6 +1,13 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The onion: a terrain, a registry, the dock, and the harbor.
3
- export { BOX, Dock, type Hosting } from './dock.ts';
2
+ // The onion: a terrain, the package, the dock, the catalogue and its
3
+ // registry, and the harbor.
4
+ export { Catalogue, CATALOGUE, type Cataloguing, type ModuleRow } from './catalogue.ts';
5
+ export { BOX, Dock, type Failures, type Hosted, type Hosting } from './dock.ts';
6
+ export { CarrierFaculty, UNDIALED, type Dialed, type Ground, type GroundFaculty } from './carrying.ts';
4
7
  export { Harbor } from './harbor.ts';
8
+ export { copyPackage, Package } from './package.ts';
9
+ export { readRootRequest, rootLine, type RootRequest } from './root-line.ts';
10
+ export { addGround, grounds, probeGround, type GroundProbe, type Probing } from './probe.ts';
5
11
  export { Registry } from './registry.ts';
6
- export { Terrain } from './terrain.ts';
12
+ export { Push, PUSH, RELAY, RELAY_CLIENT, RELAY_HOLD, RELAY_REST, RELAY_WAIT, RELAYED } from './relay.ts';
13
+ export { Terrain, type Opening } from './terrain.ts';