nervur 0.20.1 → 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 (131) hide show
  1. package/README.md +130 -8
  2. package/dist/browser/index.d.ts +35 -0
  3. package/dist/browser/index.js +188 -0
  4. package/dist/browser/worker.d.ts +12 -0
  5. package/dist/browser/worker.js +40 -0
  6. package/dist/browser.d.ts +1 -0
  7. package/dist/browser.js +7 -0
  8. package/dist/cli/command.d.ts +1 -1
  9. package/dist/cli/command.js +59 -16
  10. package/dist/cli/harbor.d.ts +9 -11
  11. package/dist/cli/harbor.js +81 -123
  12. package/dist/contract/index.d.ts +2 -1
  13. package/dist/contract/index.js +3 -7
  14. package/dist/edge/index.d.ts +57 -0
  15. package/dist/edge/index.js +306 -0
  16. package/dist/edge.d.ts +1 -0
  17. package/dist/edge.js +8 -0
  18. package/dist/folder/index.d.ts +4 -0
  19. package/dist/folder/index.js +57 -14
  20. package/dist/harbor/carrying.d.ts +17 -0
  21. package/dist/harbor/carrying.js +38 -0
  22. package/dist/harbor/catalogue.d.ts +2 -0
  23. package/dist/harbor/catalogue.js +28 -20
  24. package/dist/harbor/dock.d.ts +22 -3
  25. package/dist/harbor/dock.js +69 -13
  26. package/dist/harbor/harbor.d.ts +14 -4
  27. package/dist/harbor/harbor.js +144 -15
  28. package/dist/harbor/index.d.ts +6 -3
  29. package/dist/harbor/index.js +4 -1
  30. package/dist/harbor/package.d.ts +2 -0
  31. package/dist/harbor/package.js +15 -0
  32. package/dist/harbor/probe.d.ts +18 -0
  33. package/dist/harbor/probe.js +15 -0
  34. package/dist/harbor/registry.d.ts +1 -0
  35. package/dist/harbor/registry.js +11 -0
  36. package/dist/harbor/relay.d.ts +15 -0
  37. package/dist/harbor/relay.js +387 -0
  38. package/dist/harbor/root-line.js +4 -2
  39. package/dist/harbor/terrain.d.ts +12 -0
  40. package/dist/harbor/terrain.js +19 -0
  41. package/dist/http/index.d.ts +4 -0
  42. package/dist/http/index.js +156 -0
  43. package/dist/http/websocket.d.ts +29 -0
  44. package/dist/http/websocket.js +133 -0
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.js +8 -2
  47. package/dist/line/answer.d.ts +8 -0
  48. package/dist/line/answer.js +60 -0
  49. package/dist/line/frame.d.ts +34 -0
  50. package/dist/line/frame.js +81 -0
  51. package/dist/line/ground.d.ts +3 -0
  52. package/dist/line/ground.js +9 -0
  53. package/dist/line/index.d.ts +5 -0
  54. package/dist/line/index.js +9 -0
  55. package/dist/line/push.d.ts +7 -0
  56. package/dist/line/push.js +92 -0
  57. package/dist/line/web.d.ts +30 -0
  58. package/dist/line/web.js +181 -0
  59. package/dist/node/index.d.ts +32 -0
  60. package/dist/node/index.js +164 -0
  61. package/dist/node.d.ts +1 -0
  62. package/dist/node.js +7 -0
  63. package/dist/pointer/bodies.d.ts +2 -0
  64. package/dist/pointer/bodies.js +10 -0
  65. package/dist/pointer/world.d.ts +7 -1
  66. package/dist/pointer/world.js +6 -0
  67. package/dist/quo/address.d.ts +19 -0
  68. package/dist/quo/address.js +84 -0
  69. package/dist/quo/door.d.ts +1 -1
  70. package/dist/quo/door.js +4 -2
  71. package/dist/quo/index.d.ts +1 -0
  72. package/dist/quo/index.js +1 -0
  73. package/dist/quo/invitation.d.ts +1 -0
  74. package/dist/quo/invitation.js +9 -5
  75. package/dist/tcp/frame.d.ts +0 -4
  76. package/dist/tcp/frame.js +0 -10
  77. package/dist/tcp/index.d.ts +8 -2
  78. package/dist/tcp/index.js +123 -27
  79. package/dist/ward/index.d.ts +2 -2
  80. package/dist/ward/index.js +1 -1
  81. package/dist/ward/stance.d.ts +2 -0
  82. package/dist/ward/stance.js +3 -1
  83. package/dist/ward/ward-being.d.ts +2 -1
  84. package/dist/ward/ward-being.js +1 -1
  85. package/dist/ward/ward.d.ts +7 -0
  86. package/dist/ward/ward.js +6 -0
  87. package/package.json +21 -2
  88. package/src/browser/index.ts +207 -0
  89. package/src/browser/worker.ts +66 -0
  90. package/src/browser.ts +9 -0
  91. package/src/cli/command.ts +53 -17
  92. package/src/cli/harbor.ts +82 -116
  93. package/src/contract/index.ts +11 -8
  94. package/src/edge/index.ts +359 -0
  95. package/src/edge.ts +10 -0
  96. package/src/folder/index.ts +57 -12
  97. package/src/harbor/carrying.ts +56 -0
  98. package/src/harbor/catalogue.ts +32 -17
  99. package/src/harbor/dock.ts +88 -17
  100. package/src/harbor/harbor.ts +146 -17
  101. package/src/harbor/index.ts +6 -3
  102. package/src/harbor/package.ts +16 -0
  103. package/src/harbor/probe.ts +34 -0
  104. package/src/harbor/registry.ts +11 -0
  105. package/src/harbor/relay.ts +399 -0
  106. package/src/harbor/root-line.ts +4 -1
  107. package/src/harbor/terrain.ts +28 -2
  108. package/src/http/index.ts +150 -0
  109. package/src/http/websocket.ts +124 -0
  110. package/src/index.ts +35 -2
  111. package/src/line/answer.ts +59 -0
  112. package/src/line/frame.ts +88 -0
  113. package/src/line/ground.ts +19 -0
  114. package/src/line/index.ts +9 -0
  115. package/src/line/push.ts +112 -0
  116. package/src/line/web.ts +227 -0
  117. package/src/node/index.ts +170 -0
  118. package/src/node.ts +9 -0
  119. package/src/pointer/bodies.ts +11 -0
  120. package/src/pointer/world.ts +18 -2
  121. package/src/quo/address.ts +87 -0
  122. package/src/quo/door.ts +4 -2
  123. package/src/quo/index.ts +1 -0
  124. package/src/quo/invitation.ts +10 -6
  125. package/src/stand/stand.ts +60 -21
  126. package/src/tcp/index.ts +142 -31
  127. package/src/ward/index.ts +2 -2
  128. package/src/ward/stance.ts +6 -1
  129. package/src/ward/ward-being.ts +2 -2
  130. package/src/ward/ward.ts +16 -0
  131. package/src/tcp/frame.ts +0 -78
@@ -11,39 +11,63 @@
11
11
  // lend { contract } asked by a hosted ward: an offer from a
12
12
  // faculty that fulfils the contract
13
13
  // retract { contract, heir } an offer that was not taken
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, type Asker, type AskSpec, type Invitation, type JsonObject } from '../being/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';
18
24
  import { CATALOGUE } from './catalogue.ts';
19
- import { readInvitation } from '../quo/index.ts';
20
- import { ownerAsk, WardBeing, type Lender, type WardBeingStance } from '../ward/index.ts';
25
+ import { readAt, readInvitation } from '../quo/index.ts';
26
+ import { ownerAsk, pilots, rootAsk, WardBeing, type Lender, type WardBeingStance } from '../ward/index.ts';
21
27
 
22
28
  // What the dock asks of her harbor: a ward standing, and the seed, as hex,
23
29
  // it stands under.
24
30
  export interface Hosting {
25
31
  host(name: string, invitation: Invitation, seed?: string): Promise<string | null>;
26
32
  // Whether the harbor's carrier took the route.
27
- route(pk: string, at: string | null): boolean;
33
+ route(pk: string, at: readonly string[] | null): boolean;
28
34
  // Whether the box ward's partition was kept.
29
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>;
30
46
  }
31
47
 
32
48
  // One hosted ward, as the dock's cells keep it.
33
49
  export type Hosted = { ward: string; seed: string };
34
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
+
35
55
  export const BOX = 'box';
36
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;
37
59
  const hosted: AskSpec['for'] = (_: Asker, notes: JsonObject | undefined) => notes?.hosted === true;
38
60
 
39
61
  export class Dock extends WardBeing implements Lender {
40
62
  static override readonly kind: string = 'org.nervur.dock';
41
- static override cells: JsonObject = { hosted: [], routes: {} };
63
+ static override cells: JsonObject = { hosted: [], routes: {}, reach: [] };
42
64
  static override asks: Record<string, AskSpec> = {
43
65
  ...WardBeing.asks,
44
66
  open: ownerAsk('a faculty of this harbor', { required: ['key', 'class'] }),
45
67
  host: ownerAsk('a ward this harbor hosts', { required: ['ward'] }),
46
- route: ownerAsk('where a ward is reached, or null', { required: ['ward', 'at'] }),
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'),
47
71
  lend: { description: 'an offer from a faculty fulfilling a contract', input: { type: 'object', required: ['contract'] }, for: hosted },
48
72
  retract: { description: 'an offer that was not taken', input: { type: 'object', required: ['contract', 'heir'] }, for: hosted },
49
73
  };
@@ -55,34 +79,81 @@ export class Dock extends WardBeing implements Lender {
55
79
  this.#hosting = hosting;
56
80
  }
57
81
 
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
+
58
89
  get hosted(): Hosted[] {
59
90
  return (this.cells.hosted as Hosted[]).map(({ ward, seed }) => ({ ward, seed }));
60
91
  }
61
92
 
62
- get routes(): Record<string, string> {
63
- return { ...(this.cells.routes as Record<string, string>) };
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[])];
64
100
  }
65
101
 
66
102
  route(args: JsonObject): JsonObject {
67
- const { ward, at } = args;
68
- if (!text(ward) || (at !== null && !text(at)) || !this.#hosting.route(ward, at)) return { error: 'not routed' };
69
- const routes = this.cells.routes as Record<string, string>;
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[]>;
70
107
  if (at === null) delete routes[ward];
71
- else routes[ward] = at;
108
+ else routes[ward] = [...at];
72
109
  return { routed: ward };
73
110
  }
74
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
+
75
127
  // Genesis: the catalogue stands first, and is kept at once.
76
128
  async standCatalogue(): Promise<void> {
77
129
  const kind = 'org.nervur.catalogue';
78
130
  if ((await this.stance.boot(kind, CATALOGUE, CATALOGUE)) === null || !(await this.#hosting.keep())) throw new Error('the catalogue did not stand');
79
131
  }
80
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();
143
+ }
144
+
81
145
  async open(args: JsonObject): Promise<JsonObject> {
82
146
  if (!text(args.key) || !text(args.class)) return { error: 'key and class are text' };
83
- if (args.class.startsWith(KIT) || !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' };
84
149
  const opened = await this.stance.boot(args.class, args.key, args.key);
85
- 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() };
86
157
  }
87
158
 
88
159
  async host(args: JsonObject): Promise<JsonObject> {
@@ -17,46 +17,105 @@
17
17
  // the seed the dock keeps for it
18
18
  // 7. the beings in each ward, each from her own row
19
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.
23
+ //
20
24
  // Genesis and wake are one path: empty memory unpacks a box ward holding
21
25
  // the dock, the dock stands the catalogue, and the root's asks write the
22
26
  // rest.
23
27
  import type { Answer, BeingClass, Invitation, JsonObject } from '../being/index.ts';
24
28
  import { Carrier, type Loaded } from '../contract/index.ts';
25
29
  import { hex, isHex, unhex } from '../crypto/index.ts';
26
- import { WardKey } from '../quo/index.ts';
27
- import { DOCK, Pilot, Ward, type Catalogue as Resolving, type WardParts } from '../ward/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';
28
32
  import { Catalogue, CATALOGUE, catalogueFor, type Cataloguing } from './catalogue.ts';
29
- import { BOX, Dock, type Hosting } from './dock.ts';
33
+ import { CarrierFaculty, UNDIALED } from './carrying.ts';
34
+ import { BOX, Dock, type Failures, type Hosting } from './dock.ts';
30
35
  import { Package } from './package.ts';
31
36
  import { Registry } from './registry.ts';
32
- 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';
33
40
 
34
41
  export class Harbor extends Carrier implements Hosting, Cataloguing, Resolving {
35
42
  readonly terrain: Terrain;
36
43
  readonly #package: Package;
37
44
  readonly #wards = new Map<string, Ward>();
38
45
  readonly #kit: BeingClass[];
46
+ readonly #grounds: Set<string>;
39
47
  #registry: Registry;
48
+ readonly #failed: Failures = { wards: {}, routes: {} };
40
49
 
41
50
  private constructor(terrain: Terrain, pack: Package) {
42
51
  super();
43
52
  this.terrain = terrain;
44
53
  this.#package = pack;
45
- this.#kit = [catalogueFor(this), Pilot];
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];
46
57
  this.#registry = new Registry(this.#kit, []);
47
58
  }
48
59
 
49
- static async open(terrain: Terrain): Promise<Harbor> {
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;
80
+ }
81
+
82
+ static async #stand(terrain: Terrain): Promise<Harbor> {
50
83
  const seed = await terrain.custody.seed();
51
84
  const harbor = new Harbor(terrain, await Package.open(terrain.memory, terrain.entropy, seed));
52
85
  await harbor.#unpack(BOX, seed, undefined, (stance) => new Dock(stance, harbor));
53
- if (!harbor.box.partition.being(CATALOGUE)) await harbor.dock.standCatalogue();
86
+ const genesis = !harbor.box.partition.being(CATALOGUE);
87
+ if (genesis) await harbor.dock.standCatalogue();
54
88
  await harbor.catalogue.wake();
55
- for (const [pk, at] of Object.entries(harbor.dock.routes)) terrain.carrier.route(pk, at);
56
- for (const hosted of harbor.dock.hosted) await harbor.#unpack(hosted.ward, unhex(hosted.seed));
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
+ }
57
101
  return harbor;
58
102
  }
59
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
+
60
119
  get box(): Ward {
61
120
  return this.#wards.get(BOX)!;
62
121
  }
@@ -90,15 +149,27 @@ export class Harbor extends Carrier implements Hosting, Cataloguing, Resolving {
90
149
  return this.#door(pk) !== undefined;
91
150
  }
92
151
 
93
- // 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.
94
154
  async arrive(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
95
155
  const ward = this.#door(pk);
96
- 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;
97
158
  }
98
159
 
99
- // Carrier: this harbor's own doors first, then the terrain's.
100
- carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
101
- return this.holds(pk) ? this.arrive(pk, bytes) : this.terrain.carrier.carry(pk, bytes);
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;
102
173
  }
103
174
 
104
175
  // Resolving: a kind, by the registry the catalogue runs.
@@ -111,6 +182,10 @@ export class Harbor extends Carrier implements Hosting, Cataloguing, Resolving {
111
182
  return this.terrain.loader.load(from);
112
183
  }
113
184
 
185
+ check(modules: readonly Loaded[]): string | null {
186
+ return Registry.refusal(this.#kit, modules);
187
+ }
188
+
114
189
  run(modules: readonly Loaded[]): string | null {
115
190
  try {
116
191
  this.#registry = new Registry(this.#kit, modules);
@@ -153,9 +228,49 @@ export class Harbor extends Carrier implements Hosting, Cataloguing, Resolving {
153
228
  return this.box.partition.kept();
154
229
  }
155
230
 
156
- // Hosting: the terrain's carrier takes a route, or does not dial.
157
- override route(pk: string, at: string | null): boolean {
158
- return this.terrain.carrier.route(pk, at);
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);
242
+ }
243
+
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)));
159
274
  }
160
275
 
161
276
  async #unpack(name: string, seed: Uint8Array, dock?: Invitation, root?: WardParts['root']): Promise<void> {
@@ -167,12 +282,26 @@ export class Harbor extends Carrier implements Hosting, Cataloguing, Resolving {
167
282
  entropy,
168
283
  clock,
169
284
  carrier: this,
285
+ routes: this.#routes,
170
286
  ...(dock ? { dock } : {}),
171
287
  ...(root ? { root } : {}),
172
288
  });
173
289
  this.#wards.set(name, ward);
174
290
  }
175
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
+
176
305
  #door(pk: string): Ward | undefined {
177
306
  for (const ward of this.#wards.values()) if (ward.pk === pk) return ward;
178
307
  return undefined;
@@ -2,9 +2,12 @@
2
2
  // The onion: a terrain, the package, the dock, the catalogue and its
3
3
  // registry, and the harbor.
4
4
  export { Catalogue, CATALOGUE, type Cataloguing, type ModuleRow } from './catalogue.ts';
5
- export { BOX, Dock, type Hosted, type Hosting } from './dock.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';
6
7
  export { Harbor } from './harbor.ts';
7
- export { Package } from './package.ts';
8
+ export { copyPackage, Package } from './package.ts';
8
9
  export { readRootRequest, rootLine, type RootRequest } from './root-line.ts';
10
+ export { addGround, grounds, probeGround, type GroundProbe, type Probing } from './probe.ts';
9
11
  export { Registry } from './registry.ts';
10
- 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';
@@ -17,6 +17,17 @@ import { concat, decrypt, encrypt, hex, hkdf, utf8 } from '../crypto/index.ts';
17
17
  import type { Entropy, Memory, Rows } from '../contract/index.ts';
18
18
  import { HEAD_ROW } from '../ward/index.ts';
19
19
 
20
+ // Every place of one memory, written into another, and the places
21
+ // carried. The bytes are the package's own, sealed under a seed neither
22
+ // memory holds, so a harbor opened on the second memory under that seed
23
+ // stands what the first one stood. A place the second memory already
24
+ // holds is written over.
25
+ export const copyPackage = async (from: Memory, to: Memory): Promise<string[]> => {
26
+ const places = await from.places();
27
+ for (const place of places) await to.write(place, await from.read(place));
28
+ return places;
29
+ };
30
+
20
31
  const NONCE = 12;
21
32
  const LABEL = utf8('nervur-package');
22
33
 
@@ -113,6 +124,11 @@ export class Package {
113
124
  return hkdf(this.#key, utf8(JSON.stringify(info)), length);
114
125
  }
115
126
 
127
+ // The place a ward's rows live in.
128
+ place(name: string): Promise<string> {
129
+ return this.derive(['place', name], 16).then(hex);
130
+ }
131
+
116
132
  // One ward's rows, as the package kept them.
117
133
  async rows(name: string): Promise<Rows> {
118
134
  const ward = new Ward(this, name, await this.derive(['seal', name], 32), hex(await this.derive(['place', name], 16)));
@@ -0,0 +1,34 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The ground probe: `Harbor.open()` with no terrain asks each ground an
3
+ // entry knows whether it fits here, the one added last first, and opens on
4
+ // the terrain the first that fits makes. The main entry adds a ground that
5
+ // fits everywhere; an entry that names a platform adds its own.
6
+ import type { Module } from '../contract/index.ts';
7
+ import type { Terrain } from './terrain.ts';
8
+
9
+ // What a probed harbor is told: the modules its code holds, where it keeps
10
+ // itself, as its ground reads a place, and what the engine handed the
11
+ // code, such as an edge's object state and environment.
12
+ export type Probing = { readonly modules?: readonly Module[]; readonly where?: string; readonly given?: unknown };
13
+
14
+ export type GroundProbe = {
15
+ readonly name: string;
16
+ fits(probing: Probing): boolean;
17
+ terrain(probing: Probing): Terrain;
18
+ };
19
+
20
+ const known: GroundProbe[] = [];
21
+
22
+ // A ground this process can stand on, known once by its name.
23
+ export const addGround = (probe: GroundProbe): void => {
24
+ if (!known.some((k) => k.name === probe.name)) known.push(probe);
25
+ };
26
+
27
+ // The grounds known, in the order they are tried.
28
+ export const grounds = (): string[] => known.map((k) => k.name).reverse();
29
+
30
+ // The terrain of the first ground that fits, and its name.
31
+ export const probeGround = (probing: Probing, probes: readonly GroundProbe[] = known): { ground: string; terrain: Terrain } => {
32
+ for (const probe of [...probes].reverse()) if (probe.fits(probing)) return { ground: probe.name, terrain: probe.terrain(probing) };
33
+ throw new Error('no ground fits here');
34
+ };
@@ -62,6 +62,17 @@ export class Registry implements Catalogue {
62
62
  }
63
63
  }
64
64
 
65
+ // Why a registry of these modules would be refused, or null.
66
+ static refusal(kit: readonly BeingClass[], modules: readonly Module[]): string | null {
67
+ try {
68
+ const registry = new Registry(kit, modules);
69
+ void registry;
70
+ return null;
71
+ } catch (e) {
72
+ return (e as Error).message;
73
+ }
74
+ }
75
+
65
76
  classOf(kind: string): BeingClass | undefined {
66
77
  return this.#classes.get(kind);
67
78
  }