nervur 0.20.1 → 0.20.3

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 +174 -12
  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 +84 -20
  10. package/dist/cli/harbor.d.ts +7 -12
  11. package/dist/cli/harbor.js +73 -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 +71 -0
  15. package/dist/edge/index.js +334 -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 +10 -0
  31. package/dist/harbor/package.js +52 -1
  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 +406 -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 +36 -0
  60. package/dist/node/index.js +228 -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 +74 -20
  92. package/src/cli/harbor.ts +73 -116
  93. package/src/contract/index.ts +11 -8
  94. package/src/edge/index.ts +413 -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 +55 -1
  103. package/src/harbor/probe.ts +34 -0
  104. package/src/harbor/registry.ts +11 -0
  105. package/src/harbor/relay.ts +418 -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 +234 -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
@@ -0,0 +1,334 @@
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
+ import { Custody, Memory } from '../contract/index.js';
10
+ import { decrypt, encrypt, hkdf, isHex, unhex, utf8 } from '../crypto/index.js';
11
+ import { addGround, CarrierFaculty, Harbor, landPlaces, UNDIALED } from '../harbor/index.js';
12
+ import { answerHeld, answerPost, askFrame, FrameReader, TCP, TCP_OPENING, WEB_OPENING, webFaculty, webPush } from '../line/index.js';
13
+ import { CryptoEntropy, HeldLoader, PointerTerrain } from '../pointer/index.js';
14
+ import { tcpAddress, tcpAt } from '../quo/index.js';
15
+ import { ownerAsk } from '../ward/index.js';
16
+ export const EDGE = 'edge';
17
+ const ENTRY = 'm/';
18
+ const SEED = 'c/seed';
19
+ // The most keys one storage call takes.
20
+ const BATCH = 128;
21
+ const SEAL_INFO = utf8('nervur edge custody');
22
+ const MAX_ID = 0xffff_ffff;
23
+ const chunks = (items) => Array.from({ length: Math.ceil(items.length / BATCH) }, (_, i) => items.slice(i * BATCH, (i + 1) * BATCH));
24
+ const bytesOf = (value) => new Uint8Array(value instanceof ArrayBuffer ? value : value);
25
+ // A body keeps its keys under `root`, so one object may hold more than one
26
+ // keeping.
27
+ export class EdgeMemory extends Memory {
28
+ #storage;
29
+ #entry;
30
+ constructor(storage, root = '') {
31
+ super();
32
+ this.#storage = storage;
33
+ this.#entry = `${root}${ENTRY}`;
34
+ }
35
+ async read(place) {
36
+ const prefix = `${this.#entry}${place}/`;
37
+ const listed = await this.#storage.list({ prefix });
38
+ return new Map([...listed].map(([key, value]) => [key.slice(prefix.length), bytesOf(value).slice()]));
39
+ }
40
+ write(place, entries) {
41
+ const put = [];
42
+ const gone = [];
43
+ for (const [name, bytes] of entries) {
44
+ if (bytes === null)
45
+ gone.push(`${this.#entry}${place}/${name}`);
46
+ else
47
+ put.push([`${this.#entry}${place}/${name}`, bytes.slice()]);
48
+ }
49
+ return this.#storage.transaction(async (txn) => {
50
+ for (const batch of chunks(put))
51
+ await txn.put(Object.fromEntries(batch));
52
+ for (const batch of chunks(gone))
53
+ await txn.delete(batch);
54
+ });
55
+ }
56
+ async places() {
57
+ const entry = this.#entry;
58
+ const keys = [...(await this.#storage.list({ prefix: entry })).keys()];
59
+ return [...new Set(keys.map((key) => key.slice(entry.length, key.indexOf('/', entry.length))))];
60
+ }
61
+ async forget(place) {
62
+ const keys = [...(await this.#storage.list({ prefix: `${this.#entry}${place}/` })).keys()];
63
+ await this.#storage.transaction(async (txn) => {
64
+ for (const batch of chunks(keys))
65
+ await txn.delete(batch);
66
+ });
67
+ }
68
+ }
69
+ export class EdgeCustody extends Custody {
70
+ #storage;
71
+ #keys;
72
+ #entropy;
73
+ #key;
74
+ constructor(storage, keys, entropy = new CryptoEntropy(), root = '') {
75
+ super();
76
+ this.#storage = storage;
77
+ this.#keys = keys;
78
+ this.#entropy = entropy;
79
+ this.#key = `${root}${SEED}`;
80
+ }
81
+ async seed() {
82
+ const { seed: named, secret } = this.#keys;
83
+ if (named !== undefined) {
84
+ if (!isHex(named.trim(), 32))
85
+ throw new Error('NERVUR_SEED is not 64 hex');
86
+ return unhex(named.trim());
87
+ }
88
+ if (!secret)
89
+ throw new Error('the worker holds no NERVUR_SECRET');
90
+ const key = await hkdf(utf8(secret), SEAL_INFO, 32);
91
+ return this.#storage.transaction(async (txn) => {
92
+ const kept = (await txn.get(this.#key));
93
+ if (kept) {
94
+ const seed = await decrypt(key, bytesOf(kept.nonce), new Uint8Array(0), bytesOf(kept.sealed));
95
+ if (seed === null)
96
+ throw new Error('the seed does not open under this NERVUR_SECRET');
97
+ return seed;
98
+ }
99
+ const seed = this.#entropy.draw(32);
100
+ const nonce = this.#entropy.draw(12);
101
+ await txn.put({ [this.#key]: { nonce, sealed: await encrypt(key, nonce, new Uint8Array(0), seed) } });
102
+ return seed.slice();
103
+ });
104
+ }
105
+ }
106
+ class EdgeLine {
107
+ #socket;
108
+ #writer;
109
+ #waiting = new Map();
110
+ #next = 1;
111
+ #closed = false;
112
+ constructor(socket, onClose) {
113
+ this.#socket = socket;
114
+ this.#writer = socket.writable.getWriter();
115
+ const reader = new FrameReader();
116
+ void (async () => {
117
+ try {
118
+ for await (const chunk of socket.readable) {
119
+ for (const frame of reader.push(new Uint8Array(chunk))) {
120
+ if (frame.kind === 'ask')
121
+ continue;
122
+ const done = this.#waiting.get(frame.id);
123
+ this.#waiting.delete(frame.id);
124
+ done?.(frame.kind === 'reply' ? frame.box : null);
125
+ }
126
+ if (reader.bad)
127
+ break;
128
+ }
129
+ }
130
+ catch {
131
+ // A line that broke answers nothing more.
132
+ }
133
+ this.#closed = true;
134
+ onClose();
135
+ for (const done of this.#waiting.values())
136
+ done(null);
137
+ this.#waiting.clear();
138
+ await socket.close().catch(() => undefined);
139
+ })();
140
+ }
141
+ ask(pk, box) {
142
+ if (this.#closed)
143
+ return Promise.resolve(null);
144
+ const id = this.#next;
145
+ this.#next = this.#next === MAX_ID ? 1 : this.#next + 1;
146
+ return new Promise((resolve) => {
147
+ this.#waiting.set(id, resolve);
148
+ this.#writer.write(askFrame(id, pk, box)).catch(() => resolve(null));
149
+ });
150
+ }
151
+ close() {
152
+ this.#closed = true;
153
+ return this.#socket.close().catch(() => undefined);
154
+ }
155
+ }
156
+ const sockets = 'cloudflare:sockets';
157
+ const connector = async () => {
158
+ try {
159
+ return (await import(__rewriteRelativeImportExtension(/* @vite-ignore */ sockets))).connect;
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ };
165
+ // Quo over TCP from a worker: it dials, and takes no inbound TCP.
166
+ export const edgeTcp = () => class EdgeTcp extends CarrierFaculty {
167
+ static kind = TCP;
168
+ static asks = {
169
+ listen: ownerAsk('an edge takes no inbound TCP'),
170
+ deaf: ownerAsk('listen nowhere'),
171
+ };
172
+ #lines = new Map();
173
+ accepts(address) {
174
+ return tcpAddress(address) !== null;
175
+ }
176
+ async dial(address, pk, bytes) {
177
+ const at = tcpAddress(address);
178
+ if (at === null || !isHex(pk, 64))
179
+ return UNDIALED;
180
+ const line = await this.#line(at);
181
+ return line ? line.ask(unhex(pk), bytes) : UNDIALED;
182
+ }
183
+ #line({ host, port }) {
184
+ const at = tcpAt(host, port);
185
+ let line = this.#lines.get(at);
186
+ if (!line) {
187
+ line = (async () => {
188
+ const connect = await connector();
189
+ if (!connect)
190
+ return null;
191
+ try {
192
+ const socket = connect({ hostname: host, port });
193
+ await socket.opened;
194
+ return new EdgeLine(socket, () => this.#lines.delete(at));
195
+ }
196
+ catch {
197
+ return null;
198
+ }
199
+ })();
200
+ this.#lines.set(at, line);
201
+ void line.then((open) => open ?? this.#lines.delete(at));
202
+ }
203
+ return line;
204
+ }
205
+ async sleep() {
206
+ const lines = [...this.#lines.values()];
207
+ this.#lines.clear();
208
+ for (const line of await Promise.all(lines))
209
+ await line?.close();
210
+ }
211
+ listen() {
212
+ return { error: 'an edge takes no inbound TCP' };
213
+ }
214
+ deaf() {
215
+ return { deaf: true };
216
+ }
217
+ };
218
+ // Where the edge's web carrier listens: the object's own requests, at the
219
+ // host and path its cells name. A port of 0 is the edge's own https.
220
+ const edgeServe = (ground, listen) => {
221
+ const terrain = ground.terrain;
222
+ if (!(terrain instanceof EdgeTerrain))
223
+ return Promise.reject(new Error('this harbor stands on no edge'));
224
+ terrain.listening = listen;
225
+ const authority = listen.port === 0 ? listen.host : `${tcpAt(listen.host, listen.port).slice('tcp://'.length)}`;
226
+ const [post, held] = listen.port === 0 ? ['https', 'wss'] : ['http', 'ws'];
227
+ return Promise.resolve({
228
+ port: listen.port,
229
+ at: [`${post}://${authority}${listen.path}`, `${held}://${authority}${listen.path}`],
230
+ close: () => {
231
+ if (terrain.listening === listen)
232
+ terrain.listening = undefined;
233
+ return Promise.resolve();
234
+ },
235
+ });
236
+ };
237
+ export class EdgeTerrain extends PointerTerrain {
238
+ given;
239
+ // Where the web carrier listens now, as the object answers requests.
240
+ listening;
241
+ #harbor;
242
+ constructor(parts) {
243
+ const entropy = new CryptoEntropy();
244
+ const { storage } = parts.given.state;
245
+ super({
246
+ entropy,
247
+ loader: new HeldLoader(parts.modules ?? []),
248
+ // Only a harbor carried here takes the worker's NERVUR_SEED; every
249
+ // other object of the worker draws its own.
250
+ custody: new EdgeCustody(storage, { seed: parts.given.package ? parts.given.env.NERVUR_SEED : undefined, secret: parts.given.env.NERVUR_SECRET }, entropy),
251
+ memory: new EdgeMemory(storage),
252
+ faculties: [edgeTcp, webFaculty(edgeServe), webPush],
253
+ opens: [TCP_OPENING, WEB_OPENING],
254
+ });
255
+ this.given = parts.given;
256
+ }
257
+ // A carried package lands where the object keeps nothing yet, and only
258
+ // under the seed it was sealed with, the worker's `NERVUR_SEED`.
259
+ async claim() {
260
+ const { package: carried, env } = this.given;
261
+ if (!carried || (await this.memory.places()).length > 0)
262
+ return;
263
+ if (env.NERVUR_SEED === undefined)
264
+ throw new Error('a carried package opens only under NERVUR_SEED');
265
+ await landPlaces(this.memory, carried);
266
+ }
267
+ stand(harbor) {
268
+ this.#harbor = harbor;
269
+ return Promise.resolve();
270
+ }
271
+ // One request to the object: a post, or a held line opened, where the
272
+ // web carrier listens.
273
+ answer(request) {
274
+ const listen = this.listening;
275
+ const harbor = this.#harbor;
276
+ if (!listen || !harbor)
277
+ return Promise.resolve(new Response(null, { status: 404 }));
278
+ if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket')
279
+ return answerPost(harbor, listen, request);
280
+ const offered = (request.headers.get('sec-websocket-protocol') ?? '').split(',').map((p) => p.trim());
281
+ if (new URL(request.url).pathname !== listen.path || !offered.includes('quo'))
282
+ return Promise.resolve(new Response(null, { status: 400 }));
283
+ const Pair = globalThis.WebSocketPair;
284
+ const pair = new Pair();
285
+ const [client, server] = [pair[0], pair[1]];
286
+ server.accept();
287
+ const side = { send: (body) => server.send(new Uint8Array(body)), close: () => server.close() };
288
+ server.addEventListener('message', (event) => answerHeld(harbor, side, typeof event.data === 'string' ? event.data : new Uint8Array(event.data)));
289
+ return Promise.resolve(new Response(null, { status: 101, webSocket: client, headers: { 'sec-websocket-protocol': 'quo' } }));
290
+ }
291
+ }
292
+ const isGiven = (value) => {
293
+ const given = value;
294
+ return typeof given?.state?.storage?.transaction === 'function' && typeof given.env === 'object';
295
+ };
296
+ // A harbor handed a Durable Object's state and environment.
297
+ export const edgeGround = {
298
+ name: EDGE,
299
+ fits: ({ given }) => isGiven(given),
300
+ terrain: ({ given, modules }) => new EdgeTerrain({ given: given, ...(modules ? { modules } : {}) }),
301
+ };
302
+ // A Durable Object class whose every instance is one harbor: opened once
303
+ // per object, its web carrier opened, and every request it receives
304
+ // answered by that carrier at `path`.
305
+ export const harborObject = ({ modules, package: carried, path = '/quo', stood }) => class HarborObject {
306
+ #opening;
307
+ // The object's harbor, once it stands.
308
+ get harbor() {
309
+ return this.#opening;
310
+ }
311
+ constructor(state, env) {
312
+ addGround(edgeGround);
313
+ const held = typeof carried === 'function' ? carried(env) : carried;
314
+ const given = { state, env, ...(held ? { package: held } : {}) };
315
+ // Opened inside the object's block, so no request reaches it half
316
+ // open and its storage answers the opening.
317
+ this.#opening = state.blockConcurrencyWhile(async () => {
318
+ const harbor = await Harbor.open({ modules, given });
319
+ if (!harbor.box.being(WEB_OPENING.key))
320
+ await harbor.root('open', { key: WEB_OPENING.key, class: WEB_OPENING.kind });
321
+ await stood?.(harbor);
322
+ return harbor;
323
+ });
324
+ }
325
+ async fetch(request) {
326
+ const harbor = await this.#opening;
327
+ const terrain = harbor.terrain;
328
+ if (terrain.listening?.path !== path) {
329
+ const url = new URL(request.url);
330
+ await harbor.root('ask', { being: WEB_OPENING.key, method: 'listen', args: { host: url.hostname, port: url.port === '' ? 0 : Number(url.port), path } });
331
+ }
332
+ return terrain.answer(request);
333
+ }
334
+ };
package/dist/edge.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './index.ts';
package/dist/edge.js ADDED
@@ -0,0 +1,8 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `nervur` as a Workers bundle reads it: the main entry, with the edge
3
+ // ground added, so `Harbor.open({ given: { state, env } })` in a Durable
4
+ // Object keeps a harbor in its storage.
5
+ import { edgeGround } from './edge/index.js';
6
+ import { addGround } from './harbor/index.js';
7
+ export * from './index.js';
8
+ addGround(edgeGround);
@@ -1,10 +1,14 @@
1
1
  import { Custody, Memory, type Entropy } from '../contract/index.ts';
2
+ export type Keep = Record<string, string | null>;
2
3
  export declare class FolderMemory extends Memory {
3
4
  #private;
4
5
  readonly root: string;
5
6
  constructor(root: string);
6
7
  read(place: string): Promise<Map<string, Uint8Array>>;
7
8
  write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
9
+ places(): Promise<string[]>;
10
+ forget(place: string): Promise<void>;
11
+ protected apply(dir: string, keep?: Keep): Promise<void>;
8
12
  }
9
13
  export declare class FolderCustody extends Custody {
10
14
  #private;
@@ -4,17 +4,26 @@
4
4
  //
5
5
  // <root>/seed the harbor's seed, as hex
6
6
  // <root>/<place>/<entry> one sealed entry of the harbor's memory
7
+ // <root>/<place>/keep a keep of that place, until it is applied
7
8
  //
8
9
  // Places and entries are the hex names the kit chose, so no name reaches
9
10
  // 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.
11
+ // whole, beside itself and then renamed over.
12
+ //
13
+ // A keep is whole, by its own file: the whole keep is written under `keep`
14
+ // and renamed over, then its entries are written and removed one by one,
15
+ // then `keep` goes. A read applies a `keep` it finds first, and the same
16
+ // bytes written twice leave the same place, so a keep that a crash cut
17
+ // short finishes at the next read. A place read before its `keep` landed
18
+ // has none of it. `keep` is no hex name, so it is never an entry.
19
+ //
20
+ // The seed is a plain file: custody that seals it is a terrain's.
13
21
  import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
14
22
  import { join } from 'node:path';
15
23
  import { hex, isHex, unhex } from '../crypto/index.js';
16
24
  import { Custody, Memory } from '../contract/index.js';
17
25
  const missing = (e) => e.code === 'ENOENT';
26
+ const KEEP = 'keep';
18
27
  const named = (name) => {
19
28
  if (!isHex(name) || name === '')
20
29
  throw new Error(`${name} is not a name the kit chose`);
@@ -31,10 +40,12 @@ export class FolderMemory extends Memory {
31
40
  super();
32
41
  this.root = root;
33
42
  }
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.
43
+ // The entries as the disk holds them now, a keep left unapplied first.
44
+ // A file that is not an entry, one still being written among them, is
45
+ // not read.
36
46
  async read(place) {
37
47
  const dir = join(this.root, named(place));
48
+ await this.apply(dir);
38
49
  const out = new Map();
39
50
  const files = await readdir(dir, { withFileTypes: true }).catch((e) => (missing(e) ? [] : Promise.reject(e)));
40
51
  for (const file of files) {
@@ -44,22 +55,46 @@ export class FolderMemory extends Memory {
44
55
  }
45
56
  return out;
46
57
  }
47
- // One keep at a time, each entry in the order given.
58
+ // One keep at a time, written whole under `keep` before any entry moves.
48
59
  async write(place, entries) {
49
60
  const dir = join(this.root, named(place));
61
+ const keep = {};
62
+ for (const [name, bytes] of entries)
63
+ keep[named(name)] = bytes === null ? null : hex(bytes);
50
64
  const kept = this.#line.then(async () => {
51
65
  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
- }
66
+ await writeWhole(join(dir, KEEP), JSON.stringify(keep));
67
+ await this.apply(dir, keep);
59
68
  });
60
69
  this.#line = kept.catch(() => undefined);
61
70
  await kept;
62
71
  }
72
+ // Every folder of the root that is a place.
73
+ async places() {
74
+ const files = await readdir(this.root, { withFileTypes: true }).catch((e) => (missing(e) ? [] : Promise.reject(e)));
75
+ return files.filter((f) => f.isDirectory() && isHex(f.name) && f.name !== '').map((f) => f.name);
76
+ }
77
+ forget(place) {
78
+ return rm(join(this.root, named(place)), { recursive: true, force: true });
79
+ }
80
+ // The keep a folder holds, applied and then gone. Writing the same bytes
81
+ // again leaves the same place, so applying twice is applying once.
82
+ async apply(dir, keep) {
83
+ const found = keep ?? (await readFile(join(dir, KEEP), 'utf8').then((text) => JSON.parse(text), () => null));
84
+ if (found === null)
85
+ return;
86
+ for (const [name, written] of Object.entries(found)) {
87
+ const path = join(dir, named(name));
88
+ if (written === null)
89
+ await rm(path, { force: true });
90
+ else
91
+ await writeWhole(path, unhex(written));
92
+ }
93
+ await rm(join(dir, KEEP), { force: true });
94
+ const left = await readdir(dir).catch((e) => (missing(e) ? [] : Promise.reject(e)));
95
+ if (left.length === 0)
96
+ await rm(dir, { recursive: true, force: true });
97
+ }
63
98
  }
64
99
  export class FolderCustody extends Custody {
65
100
  root;
@@ -70,6 +105,7 @@ export class FolderCustody extends Custody {
70
105
  this.#entropy = entropy;
71
106
  }
72
107
  // The seed the folder holds, or a drawn one written there the first time.
108
+ // Two first asks at once keep the one written first.
73
109
  async seed() {
74
110
  const path = join(this.root, 'seed');
75
111
  const kept = await readFile(path, 'utf8').catch((e) => (missing(e) ? undefined : Promise.reject(e)));
@@ -81,7 +117,14 @@ export class FolderCustody extends Custody {
81
117
  }
82
118
  const seed = this.#entropy.draw(32);
83
119
  await mkdir(this.root, { recursive: true, mode: 0o700 });
84
- await writeFile(path, `${hex(seed)}\n`, { mode: 0o600, flag: 'wx' });
120
+ try {
121
+ await writeFile(path, `${hex(seed)}\n`, { mode: 0o600, flag: 'wx' });
122
+ }
123
+ catch (e) {
124
+ if (e.code === 'EEXIST')
125
+ return this.seed();
126
+ throw e;
127
+ }
85
128
  return seed;
86
129
  }
87
130
  }
@@ -0,0 +1,17 @@
1
+ import { Faculty, type BeingClass } from '../being/index.ts';
2
+ export interface Ground {
3
+ arrive(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
4
+ }
5
+ export type GroundFaculty = (ground: Ground) => BeingClass;
6
+ export declare const UNDIALED: unique symbol;
7
+ export type Dialed = Uint8Array | null | typeof UNDIALED;
8
+ export declare abstract class CarrierFaculty extends Faculty {
9
+ static readonly kind: string;
10
+ abstract accepts(address: string): boolean;
11
+ abstract dial(address: string, pk: string, bytes: Uint8Array): Promise<Dialed>;
12
+ listening(): string[];
13
+ relays(_pk: string): boolean;
14
+ relay(_pk: string, _bytes: Uint8Array): Promise<Uint8Array | null>;
15
+ awake(): Promise<void>;
16
+ sleep(): Promise<void>;
17
+ }
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A carrier is a faculty. It speaks the schemes of the addresses it dials,
3
+ // dials an address when the harbor carries a box there, and listens where
4
+ // its cells say, handing each box it hears to the harbor. The harbor keeps
5
+ // every route and walks it in order, handing each address to the carrier
6
+ // that speaks its scheme.
7
+ //
8
+ // A ground ships its carriers as classes the terrain names, each made for
9
+ // one harbor, so the class holds the harbor it hands boxes to. They are
10
+ // the kit's, of kinds under `org.nervur.`, and the dock opens them as
11
+ // faculties, at genesis where the terrain says, and on the root's `open`.
12
+ import { Faculty } from '../being/index.js';
13
+ // Beside a reply and nothing, a dial answers `undialed` when the address
14
+ // could not be reached at all, so the harbor tries the next one. A box a
15
+ // line took may have been heard, so after it no other address is tried.
16
+ export const UNDIALED = Symbol('undialed');
17
+ export class CarrierFaculty extends Faculty {
18
+ static kind = 'org.nervur.carrier';
19
+ // The addresses it listens at now.
20
+ listening() {
21
+ return [];
22
+ }
23
+ // Whether it answers for a ward pk no door of this harbor holds, as a
24
+ // relay does, and its answer.
25
+ relays(_pk) {
26
+ return false;
27
+ }
28
+ relay(_pk, _bytes) {
29
+ return Promise.resolve(null);
30
+ }
31
+ // Once the harbor has unpacked, and once before it closes.
32
+ awake() {
33
+ return Promise.resolve();
34
+ }
35
+ sleep() {
36
+ return Promise.resolve();
37
+ }
38
+ }
@@ -4,9 +4,11 @@ export declare const CATALOGUE = "catalogue";
4
4
  export type ModuleRow = {
5
5
  version: string;
6
6
  from: string;
7
+ kinds: string[];
7
8
  };
8
9
  export interface Cataloguing {
9
10
  load(from: string): Promise<Loaded | null>;
11
+ check(modules: readonly Loaded[]): string | null;
10
12
  run(modules: readonly Loaded[]): string | null;
11
13
  stands(kinds: readonly string[]): boolean;
12
14
  kindsOf(module: string): string[];
@@ -2,13 +2,16 @@
2
2
  // The catalogue: the dock's first faculty, the code a harbor runs. Her
3
3
  // cells name each module by the name it declares, its version, and where
4
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.
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.
7
8
  //
8
9
  // add { from } the module the loader finds there, run from now
9
10
  // 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
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
12
15
  //
13
16
  // She is the kit's, of kind `org.nervur.catalogue`, and answers the root
14
17
  // and an owner alone.
@@ -26,6 +29,7 @@ export class Catalogue extends Faculty {
26
29
  };
27
30
  #harbor;
28
31
  #running = new Map();
32
+ #failed = new Map();
29
33
  constructor(stance, harbor) {
30
34
  super(stance);
31
35
  this.#harbor = harbor;
@@ -33,20 +37,19 @@ export class Catalogue extends Faculty {
33
37
  get #rows() {
34
38
  return this.cells.modules;
35
39
  }
36
- // Every module her cells name, loaded and run, or a throw naming the one
37
- // that cannot be.
40
+ // Every module her cells name, in their order, loaded and run where it
41
+ // is found, is the module named, and splits no kind with those before
42
+ // it. The rest stay out, each with its reason.
38
43
  async wake() {
39
44
  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);
45
+ const loaded = await this.#harbor.load(row.from).catch(() => null);
46
+ const refused = loaded === null ? `not found at ${row.from}` : loaded.module !== module ? `${row.from} is module ${loaded.module}` : this.#harbor.check([...this.#running.values(), loaded]);
47
+ if (refused === null)
48
+ this.#running.set(module, loaded);
49
+ else
50
+ this.#failed.set(module, refused);
46
51
  }
47
- const refused = this.#harbor.run([...this.#running.values()]);
48
- if (refused !== null)
49
- throw new Error(refused);
52
+ this.#harbor.run([...this.#running.values()]);
50
53
  }
51
54
  async add(args) {
52
55
  if (!text(args.from))
@@ -60,26 +63,31 @@ export class Catalogue extends Faculty {
60
63
  if (refused !== null)
61
64
  return { error: refused };
62
65
  this.#running.set(loaded.module, loaded);
63
- this.#rows[loaded.module] = { version: loaded.version, from: loaded.from };
66
+ this.#failed.delete(loaded.module);
67
+ this.#rows[loaded.module] = { version: loaded.version, from: loaded.from, kinds: this.#harbor.kindsOf(loaded.module) };
64
68
  return { added: loaded.module, version: loaded.version };
65
69
  }
66
70
  remove(args) {
67
71
  const module = args.module;
68
- if (!text(module) || !this.#running.has(module))
72
+ if (!text(module) || !Object.hasOwn(this.#rows, module))
69
73
  return { error: 'no such module' };
70
- if (this.#harbor.stands(this.#harbor.kindsOf(module)))
74
+ const kinds = new Set([...this.#rows[module].kinds, ...this.#harbor.kindsOf(module)]);
75
+ if (this.#harbor.stands([...kinds]))
71
76
  return { error: 'a being stands on this module' };
72
77
  const running = new Map(this.#running);
73
78
  running.delete(module);
74
79
  this.#harbor.run([...running.values()]);
75
80
  this.#running.delete(module);
81
+ this.#failed.delete(module);
76
82
  delete this.#rows[module];
77
83
  return { removed: module };
78
84
  }
79
85
  modules() {
80
86
  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 };
87
+ for (const [module, row] of Object.entries(this.#rows)) {
88
+ const failed = this.#failed.get(module);
89
+ out[module] = { version: row.version, running: this.#running.get(module)?.version ?? null, from: row.from, ...(failed === undefined ? {} : { failed }) };
90
+ }
83
91
  return out;
84
92
  }
85
93
  }