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.
- package/README.md +130 -8
- package/dist/browser/index.d.ts +35 -0
- package/dist/browser/index.js +188 -0
- package/dist/browser/worker.d.ts +12 -0
- package/dist/browser/worker.js +40 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +7 -0
- package/dist/cli/command.d.ts +1 -1
- package/dist/cli/command.js +59 -16
- package/dist/cli/harbor.d.ts +9 -11
- package/dist/cli/harbor.js +81 -123
- package/dist/contract/index.d.ts +2 -1
- package/dist/contract/index.js +3 -7
- package/dist/edge/index.d.ts +57 -0
- package/dist/edge/index.js +306 -0
- package/dist/edge.d.ts +1 -0
- package/dist/edge.js +8 -0
- package/dist/folder/index.d.ts +4 -0
- package/dist/folder/index.js +57 -14
- package/dist/harbor/carrying.d.ts +17 -0
- package/dist/harbor/carrying.js +38 -0
- package/dist/harbor/catalogue.d.ts +2 -0
- package/dist/harbor/catalogue.js +28 -20
- package/dist/harbor/dock.d.ts +22 -3
- package/dist/harbor/dock.js +69 -13
- package/dist/harbor/harbor.d.ts +14 -4
- package/dist/harbor/harbor.js +144 -15
- package/dist/harbor/index.d.ts +6 -3
- package/dist/harbor/index.js +4 -1
- package/dist/harbor/package.d.ts +2 -0
- package/dist/harbor/package.js +15 -0
- package/dist/harbor/probe.d.ts +18 -0
- package/dist/harbor/probe.js +15 -0
- package/dist/harbor/registry.d.ts +1 -0
- package/dist/harbor/registry.js +11 -0
- package/dist/harbor/relay.d.ts +15 -0
- package/dist/harbor/relay.js +387 -0
- package/dist/harbor/root-line.js +4 -2
- package/dist/harbor/terrain.d.ts +12 -0
- package/dist/harbor/terrain.js +19 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.js +156 -0
- package/dist/http/websocket.d.ts +29 -0
- package/dist/http/websocket.js +133 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -2
- package/dist/line/answer.d.ts +8 -0
- package/dist/line/answer.js +60 -0
- package/dist/line/frame.d.ts +34 -0
- package/dist/line/frame.js +81 -0
- package/dist/line/ground.d.ts +3 -0
- package/dist/line/ground.js +9 -0
- package/dist/line/index.d.ts +5 -0
- package/dist/line/index.js +9 -0
- package/dist/line/push.d.ts +7 -0
- package/dist/line/push.js +92 -0
- package/dist/line/web.d.ts +30 -0
- package/dist/line/web.js +181 -0
- package/dist/node/index.d.ts +32 -0
- package/dist/node/index.js +164 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +7 -0
- package/dist/pointer/bodies.d.ts +2 -0
- package/dist/pointer/bodies.js +10 -0
- package/dist/pointer/world.d.ts +7 -1
- package/dist/pointer/world.js +6 -0
- package/dist/quo/address.d.ts +19 -0
- package/dist/quo/address.js +84 -0
- package/dist/quo/door.d.ts +1 -1
- package/dist/quo/door.js +4 -2
- package/dist/quo/index.d.ts +1 -0
- package/dist/quo/index.js +1 -0
- package/dist/quo/invitation.d.ts +1 -0
- package/dist/quo/invitation.js +9 -5
- package/dist/tcp/frame.d.ts +0 -4
- package/dist/tcp/frame.js +0 -10
- package/dist/tcp/index.d.ts +8 -2
- package/dist/tcp/index.js +123 -27
- package/dist/ward/index.d.ts +2 -2
- package/dist/ward/index.js +1 -1
- package/dist/ward/stance.d.ts +2 -0
- package/dist/ward/stance.js +3 -1
- package/dist/ward/ward-being.d.ts +2 -1
- package/dist/ward/ward-being.js +1 -1
- package/dist/ward/ward.d.ts +7 -0
- package/dist/ward/ward.js +6 -0
- package/package.json +21 -2
- package/src/browser/index.ts +207 -0
- package/src/browser/worker.ts +66 -0
- package/src/browser.ts +9 -0
- package/src/cli/command.ts +53 -17
- package/src/cli/harbor.ts +82 -116
- package/src/contract/index.ts +11 -8
- package/src/edge/index.ts +359 -0
- package/src/edge.ts +10 -0
- package/src/folder/index.ts +57 -12
- package/src/harbor/carrying.ts +56 -0
- package/src/harbor/catalogue.ts +32 -17
- package/src/harbor/dock.ts +88 -17
- package/src/harbor/harbor.ts +146 -17
- package/src/harbor/index.ts +6 -3
- package/src/harbor/package.ts +16 -0
- package/src/harbor/probe.ts +34 -0
- package/src/harbor/registry.ts +11 -0
- package/src/harbor/relay.ts +399 -0
- package/src/harbor/root-line.ts +4 -1
- package/src/harbor/terrain.ts +28 -2
- package/src/http/index.ts +150 -0
- package/src/http/websocket.ts +124 -0
- package/src/index.ts +35 -2
- package/src/line/answer.ts +59 -0
- package/src/line/frame.ts +88 -0
- package/src/line/ground.ts +19 -0
- package/src/line/index.ts +9 -0
- package/src/line/push.ts +112 -0
- package/src/line/web.ts +227 -0
- package/src/node/index.ts +170 -0
- package/src/node.ts +9 -0
- package/src/pointer/bodies.ts +11 -0
- package/src/pointer/world.ts +18 -2
- package/src/quo/address.ts +87 -0
- package/src/quo/door.ts +4 -2
- package/src/quo/index.ts +1 -0
- package/src/quo/invitation.ts +10 -6
- package/src/stand/stand.ts +60 -21
- package/src/tcp/index.ts +142 -31
- package/src/ward/index.ts +2 -2
- package/src/ward/stance.ts +6 -1
- package/src/ward/ward-being.ts +2 -2
- package/src/ward/ward.ts +16 -0
- package/src/tcp/frame.ts +0 -78
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The browser harbor in a worker, one for every tab. A shared, dedicated
|
|
3
|
+
// or service worker calls `serveWorker` as its script starts, with the
|
|
4
|
+
// harbor it opens; every tab reaches that harbor's root line with
|
|
5
|
+
// `rootOf`, one request on a channel of its own. A service worker that
|
|
6
|
+
// hears a push asks every relay client of its harbor to `ring`, since a
|
|
7
|
+
// push carries nothing but the news that something waits.
|
|
8
|
+
import type { JsonObject } from '../being/index.ts';
|
|
9
|
+
import { RELAY_CLIENT, rootLine, type Harbor } from '../harbor/index.ts';
|
|
10
|
+
|
|
11
|
+
type Asked = { request?: unknown };
|
|
12
|
+
type Answering = { postMessage(message: unknown): void };
|
|
13
|
+
type Scope = {
|
|
14
|
+
addEventListener(type: string, listener: (event: never) => void): void;
|
|
15
|
+
};
|
|
16
|
+
type Connecting = { ports: readonly MessagePort[] };
|
|
17
|
+
type Messaging = { data: unknown; ports: readonly MessagePort[] };
|
|
18
|
+
type Pushing = { waitUntil(work: Promise<unknown>): void };
|
|
19
|
+
|
|
20
|
+
const answer = async (harbor: Promise<Harbor>, data: unknown, reply: Answering | undefined): Promise<void> => {
|
|
21
|
+
if (!reply) return;
|
|
22
|
+
let out: JsonObject;
|
|
23
|
+
try {
|
|
24
|
+
out = await rootLine(await harbor, (data as Asked | null)?.request);
|
|
25
|
+
} catch {
|
|
26
|
+
out = { error: 'the harbor threw' };
|
|
27
|
+
}
|
|
28
|
+
reply.postMessage(out);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Every relay client of the harbor asked to collect what waits.
|
|
32
|
+
export const ringAll = async (harbor: Harbor): Promise<JsonObject[]> => {
|
|
33
|
+
const keys = Object.entries(harbor.box.partition.beings)
|
|
34
|
+
.filter(([, row]) => row.class === RELAY_CLIENT)
|
|
35
|
+
.map(([key]) => key);
|
|
36
|
+
return Promise.all(keys.map(async (being) => (await harbor.root('ask', { being, method: 'ring' })) as JsonObject));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Called as the worker's script starts, so no tab's first message is
|
|
40
|
+
// lost while the harbor opens.
|
|
41
|
+
export const serveWorker = (harbor: Promise<Harbor>, scope: Scope = globalThis as unknown as Scope): void => {
|
|
42
|
+
scope.addEventListener('connect', (event: Connecting) => {
|
|
43
|
+
const port = event.ports[0]!;
|
|
44
|
+
port.onmessage = (message) => void answer(harbor, message.data, message.ports[0]);
|
|
45
|
+
port.start();
|
|
46
|
+
});
|
|
47
|
+
scope.addEventListener('message', (event: Messaging) => void answer(harbor, event.data, event.ports[0]));
|
|
48
|
+
scope.addEventListener('push', (event: Pushing) => event.waitUntil(harbor.then(ringAll)));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Where a tab sends root requests: a shared worker's port, a dedicated
|
|
52
|
+
// worker, or a service worker.
|
|
53
|
+
export type Reaching = { postMessage(message: unknown, transfer: Transferable[]): void };
|
|
54
|
+
|
|
55
|
+
// A tab's root line to the worker's harbor.
|
|
56
|
+
export const rootOf =
|
|
57
|
+
(worker: Reaching) =>
|
|
58
|
+
(request: JsonObject): Promise<JsonObject> =>
|
|
59
|
+
new Promise((resolve) => {
|
|
60
|
+
const channel = new MessageChannel();
|
|
61
|
+
channel.port1.onmessage = (message) => {
|
|
62
|
+
channel.port1.close();
|
|
63
|
+
resolve(message.data as JsonObject);
|
|
64
|
+
};
|
|
65
|
+
worker.postMessage({ request }, [channel.port2]);
|
|
66
|
+
});
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// `nervur` as a browser bundle reads it: the main entry, with the browser
|
|
3
|
+
// ground added, so `Harbor.open()` keeps a harbor in IndexedDB.
|
|
4
|
+
import { browserGround } from './browser/index.ts';
|
|
5
|
+
import { addGround } from './harbor/index.ts';
|
|
6
|
+
|
|
7
|
+
export * from './index.ts';
|
|
8
|
+
|
|
9
|
+
addGround(browserGround);
|
package/src/cli/command.ts
CHANGED
|
@@ -11,9 +11,18 @@
|
|
|
11
11
|
// nervur module remove <module> a module it runs no more
|
|
12
12
|
// nervur modules what the catalogue runs
|
|
13
13
|
// nervur census [--ward W] the empty ask
|
|
14
|
+
// nervur sweep every place of the folder this
|
|
15
|
+
// harbor does not name, gone
|
|
16
|
+
// nervur export [--with-seed] the harbor as one JSON line, to
|
|
17
|
+
// carry it to another ground; with
|
|
18
|
+
// --with-seed the seed goes too
|
|
19
|
+
// nervur import <file> a carried harbor into an empty
|
|
20
|
+
// folder, `-` being this input
|
|
14
21
|
// nervur open <key> <kind> a faculty stands in the box ward
|
|
15
22
|
// nervur host <ward> [--seed S] a hosted ward stands
|
|
16
|
-
// nervur route <ward pk> <
|
|
23
|
+
// nervur route <ward pk> <address... | none> where a ward is reached, in order
|
|
24
|
+
// nervur reach <address...> where this harbor is reached, as
|
|
25
|
+
// every invitation it gives says
|
|
17
26
|
// nervur boot [--ward W] <key> <kind> a being stands
|
|
18
27
|
// nervur unboot [--ward W] <being> a being goes
|
|
19
28
|
// nervur public [--ward W] <key | none> the being a stranger reaches
|
|
@@ -21,8 +30,9 @@
|
|
|
21
30
|
// nervur ask [--ward W] [being] [method] [args] a being, asked as the owner
|
|
22
31
|
// nervur own [--ward W] <id> an invitation for an owner
|
|
23
32
|
// nervur disown [--ward W] <id> an owner no more
|
|
24
|
-
// nervur pilot <name> <invitation>
|
|
25
|
-
// pilot being of the box ward
|
|
33
|
+
// nervur pilot <name> <invitation> [address] a far ward held as its owner, by a
|
|
34
|
+
// pilot being of the box ward, at
|
|
35
|
+
// the address or the invitation's at
|
|
26
36
|
// nervur <command> --via <name> ... the command asked of the far ward
|
|
27
37
|
// that pilot holds, as its owner
|
|
28
38
|
//
|
|
@@ -31,14 +41,17 @@
|
|
|
31
41
|
// ward, and a pilot holds one ward, so --via takes no --ward. The exit is 1
|
|
32
42
|
// when the answer is an error.
|
|
33
43
|
import { existsSync } from 'node:fs';
|
|
34
|
-
import {
|
|
44
|
+
import { readFile } from 'node:fs/promises';
|
|
35
45
|
import { join, resolve } from 'node:path';
|
|
36
46
|
import type { JsonObject } from '../being/index.ts';
|
|
37
47
|
import type { RootRequest } from '../harbor/index.ts';
|
|
38
|
-
import {
|
|
48
|
+
import { harborDir } from '../node/index.ts';
|
|
49
|
+
import { carry, isServed, land, open, request, serve } from './harbor.ts';
|
|
39
50
|
|
|
40
|
-
const USAGE = 'usage: nervur <init|serve|module|modules|census|open|host|route|boot|unboot|public|invite|ask|own|disown|pilot> [args] [--dir D] [--ward W] [--via V] [--seed S] [--host H] [--port P]';
|
|
51
|
+
const USAGE = 'usage: nervur <init|serve|module|modules|census|sweep|open|host|route|reach|boot|unboot|public|invite|ask|own|disown|pilot|export|import> [args] [--dir D] [--ward W] [--via V] [--seed S] [--with-seed] [--host H] [--port P]';
|
|
41
52
|
const FLAGS = new Set(['dir', 'ward', 'via', 'seed', 'host', 'port']);
|
|
53
|
+
// The one flag that stands alone, since what it names is a yes.
|
|
54
|
+
const ALONE = 'with-seed';
|
|
42
55
|
|
|
43
56
|
export const parse = (argv: string[]): { command?: string; words: string[]; flags: Record<string, string> } => {
|
|
44
57
|
const flags: Record<string, string> = {};
|
|
@@ -47,6 +60,7 @@ export const parse = (argv: string[]): { command?: string; words: string[]; flag
|
|
|
47
60
|
const arg = argv[i]!;
|
|
48
61
|
const name = arg.startsWith('--') ? arg.slice(2) : undefined;
|
|
49
62
|
if (name === undefined) words.push(arg);
|
|
63
|
+
else if (name === ALONE) flags[name] = 'yes';
|
|
50
64
|
else if (!FLAGS.has(name) || argv[i + 1] === undefined) throw new Error(`--${name} is no flag with a value`);
|
|
51
65
|
else flags[name] = argv[(i += 1)]!;
|
|
52
66
|
}
|
|
@@ -54,6 +68,13 @@ export const parse = (argv: string[]): { command?: string; words: string[]; flag
|
|
|
54
68
|
return { ...(command === undefined ? {} : { command }), words: rest, flags };
|
|
55
69
|
};
|
|
56
70
|
|
|
71
|
+
// What this command was handed on its input, for `import -`.
|
|
72
|
+
const standingIn = async (): Promise<string> => {
|
|
73
|
+
let text = '';
|
|
74
|
+
for await (const chunk of process.stdin) text += (chunk as Buffer).toString('utf8');
|
|
75
|
+
return text;
|
|
76
|
+
};
|
|
77
|
+
|
|
57
78
|
const none = (word: string): string | null => (word === 'none' ? null : word);
|
|
58
79
|
const json = (word: string | undefined): JsonObject | undefined => (word === undefined ? undefined : (JSON.parse(word) as JsonObject));
|
|
59
80
|
const catalogue = (method: string, args: JsonObject = {}): RootRequest => ({ method: 'ask', args: { being: 'catalogue', method, args } });
|
|
@@ -74,6 +95,8 @@ export const requestOf = (command: string, words: string[], flags: Record<string
|
|
|
74
95
|
}
|
|
75
96
|
case 'modules':
|
|
76
97
|
return (need(0), catalogue('modules'));
|
|
98
|
+
case 'sweep':
|
|
99
|
+
return (need(0), { method: 'sweep', args: {} });
|
|
77
100
|
case 'census':
|
|
78
101
|
return (need(0), at);
|
|
79
102
|
case 'open': {
|
|
@@ -85,9 +108,13 @@ export const requestOf = (command: string, words: string[], flags: Record<string
|
|
|
85
108
|
return { method: 'host', args: { ward: ward!, ...(flags.seed === undefined ? {} : { seed: flags.seed }) } };
|
|
86
109
|
}
|
|
87
110
|
case 'route': {
|
|
88
|
-
const [ward, where] =
|
|
89
|
-
|
|
111
|
+
const [ward, ...where] = words;
|
|
112
|
+
if (ward === undefined || where.length === 0) throw new Error('route takes a ward pk and its addresses, or none');
|
|
113
|
+
return { method: 'route', args: { ward, at: where.length === 1 && where[0] === 'none' ? null : where } };
|
|
90
114
|
}
|
|
115
|
+
case 'reach':
|
|
116
|
+
if (words.length === 0) throw new Error('reach takes the addresses this harbor is reached at');
|
|
117
|
+
return { method: 'reach', args: { at: words } };
|
|
91
118
|
case 'boot': {
|
|
92
119
|
const [key, kind] = need(2);
|
|
93
120
|
return { ...at, method: 'boot', args: { key: key!, class: kind! } };
|
|
@@ -123,7 +150,7 @@ const failed = (answer: unknown): boolean => {
|
|
|
123
150
|
export const main = async (argv: string[], env: Record<string, string | undefined>, print: (line: string) => void): Promise<number> => {
|
|
124
151
|
const { command, words, flags } = parse(argv);
|
|
125
152
|
if (command === undefined) throw new Error(USAGE);
|
|
126
|
-
const dir = flags.dir ?? env
|
|
153
|
+
const dir = flags.dir ?? harborDir(env);
|
|
127
154
|
const out = (answer: JsonObject): number => {
|
|
128
155
|
print(JSON.stringify(answer));
|
|
129
156
|
return failed(answer) ? 1 : 0;
|
|
@@ -131,26 +158,34 @@ export const main = async (argv: string[], env: Record<string, string | undefine
|
|
|
131
158
|
if (command === 'init') {
|
|
132
159
|
if (words.length > 0) throw new Error('init takes no argument');
|
|
133
160
|
if (existsSync(join(dir, 'seed'))) return out({ error: `a harbor stands at ${dir}` });
|
|
134
|
-
const
|
|
161
|
+
const harbor = await open(dir);
|
|
135
162
|
try {
|
|
136
163
|
const owner = await harbor.root('own', { id: 'owner' });
|
|
137
164
|
return out({ pk: harbor.box.pk, owner: owner as JsonObject });
|
|
138
165
|
} finally {
|
|
139
|
-
await
|
|
166
|
+
await harbor.close();
|
|
140
167
|
}
|
|
141
168
|
}
|
|
169
|
+
if (command === 'export' || command === 'import') {
|
|
170
|
+
if (words.length !== (command === 'import' ? 1 : 0)) throw new Error(`${command} takes ${command === 'import' ? 'one file' : 'no argument'}`);
|
|
171
|
+
if (flags.via !== undefined) throw new Error(`${command} reads this folder, so it takes no --via`);
|
|
172
|
+
if (await isServed(dir)) throw new Error(`a harbor is served from ${dir}: stop it first`);
|
|
173
|
+
if (command === 'export') return out((await carry(dir, flags[ALONE] !== undefined)) as unknown as JsonObject);
|
|
174
|
+
const text = words[0] === '-' ? await standingIn() : await readFile(words[0]!, 'utf8');
|
|
175
|
+
return out({ landed: await land(dir, JSON.parse(text)) });
|
|
176
|
+
}
|
|
142
177
|
if (command === 'serve') {
|
|
143
178
|
if (words.length > 0) throw new Error('serve takes no argument');
|
|
144
179
|
const served = await serve(dir, flags.host, flags.port === undefined ? undefined : Number(flags.port));
|
|
145
180
|
const stop = (): void => void served.close().then(() => process.exit(0));
|
|
146
181
|
process.once('SIGINT', stop);
|
|
147
182
|
process.once('SIGTERM', stop);
|
|
148
|
-
print(JSON.stringify({ pk: served.pk, at: served.at, root: served.root }));
|
|
183
|
+
print(JSON.stringify({ pk: served.pk, at: served.at, listening: served.listening, root: served.root }));
|
|
149
184
|
return SERVING;
|
|
150
185
|
}
|
|
151
186
|
if (command === 'pilot') {
|
|
152
|
-
if (words.length !== 3) throw new Error('pilot takes
|
|
153
|
-
const [name, invitation, at] = words as [string, string, string];
|
|
187
|
+
if (words.length !== 2 && words.length !== 3) throw new Error('pilot takes a name, an invitation, and an address where the invitation carries none');
|
|
188
|
+
const [name, invitation, at] = words as [string, string, string | undefined];
|
|
154
189
|
const held = JSON.parse(invitation) as JsonObject;
|
|
155
190
|
for (const req of pilotRequests(name, held, at)) {
|
|
156
191
|
const answer = await request(dir, req);
|
|
@@ -165,9 +200,10 @@ export const main = async (argv: string[], env: Record<string, string | undefine
|
|
|
165
200
|
return out(farAnswer(await request(dir, via(flags.via, requestOf(command, words, flags)))));
|
|
166
201
|
};
|
|
167
202
|
|
|
168
|
-
// The root requests that hold a far ward: its route
|
|
169
|
-
|
|
170
|
-
|
|
203
|
+
// The root requests that hold a far ward: its route where one is named,
|
|
204
|
+
// else the take learns it from the invitation's `at`; the pilot; the take.
|
|
205
|
+
export const pilotRequests = (name: string, invitation: JsonObject, at?: string): RootRequest[] => [
|
|
206
|
+
...(at === undefined ? [] : [{ method: 'route', args: { ward: invitation.ward ?? null, at: [at] } }]),
|
|
171
207
|
{ method: 'boot', args: { key: name, class: 'org.nervur.pilot' } },
|
|
172
208
|
{ method: 'ask', args: { being: name, method: 'hold', args: { invitation } } },
|
|
173
209
|
];
|
package/src/cli/harbor.ts
CHANGED
|
@@ -1,137 +1,103 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// A harbor on this machine, as the `nervur` command stands it:
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// local socket beside them.
|
|
6
|
-
//
|
|
7
|
-
// <dir>/seed the harbor's seed
|
|
8
|
-
// <dir>/<place>/ its sealed memory
|
|
9
|
-
// <dir>/root.sock the root line while the harbor is served, open to
|
|
10
|
-
// this user alone, or in the temporary folder for a
|
|
11
|
-
// folder too deep for a socket path
|
|
2
|
+
// A harbor on this machine, as the `nervur` command stands it: a harbor of
|
|
3
|
+
// the Node ground in a folder, served while `serve` runs, and asked over
|
|
4
|
+
// its root line's socket.
|
|
12
5
|
//
|
|
13
6
|
// A root request goes to a served harbor over its socket. With none served,
|
|
14
7
|
// the command unpacks the harbor from its folder, asks, and closes it, so
|
|
15
8
|
// one process at a time writes the folder.
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { tmpdir } from 'node:os';
|
|
20
|
-
import { join, resolve } from 'node:path';
|
|
21
|
-
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
11
|
+
import { join } from 'node:path';
|
|
22
12
|
import type { JsonObject } from '../being/index.ts';
|
|
23
|
-
import {
|
|
13
|
+
import { hex, isHex, unhex } from '../crypto/index.ts';
|
|
24
14
|
import { FolderCustody, FolderMemory } from '../folder/index.ts';
|
|
25
|
-
import { Harbor, rootLine, type RootRequest } from '../harbor/index.ts';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
// A socket path has a hundred bytes or so on every system, so a folder too
|
|
30
|
-
// deep for one puts its socket in this user's temporary folder, under a
|
|
31
|
-
// name drawn from the folder's own path.
|
|
32
|
-
const SOCKET_PATH = 100;
|
|
33
|
-
export const socketOf = (dir: string): string => {
|
|
34
|
-
const beside = join(resolve(dir), 'root.sock');
|
|
35
|
-
if (Buffer.byteLength(beside) <= SOCKET_PATH) return beside;
|
|
36
|
-
return join(tmpdir(), `nervur-${createHash('sha256').update(resolve(dir)).digest('hex').slice(0, 32)}.sock`);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Code from a file: a module that exports `module`, `version` and
|
|
40
|
-
// `classes`, found again by its absolute path. A file that cannot be
|
|
41
|
-
// imported, or exports no module, is none.
|
|
42
|
-
export class ImportLoader extends Loader {
|
|
43
|
-
async load(from: string): Promise<Loaded | null> {
|
|
44
|
-
const path = resolve(from);
|
|
45
|
-
let exported: { module?: unknown; version?: unknown; classes?: unknown };
|
|
46
|
-
try {
|
|
47
|
-
exported = (await import(pathToFileURL(path).href)) as typeof exported;
|
|
48
|
-
} catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
const { module, version, classes } = exported;
|
|
52
|
-
if (typeof module !== 'string' || typeof version !== 'string' || !Array.isArray(classes)) return null;
|
|
53
|
-
return { module, version, classes, from: path };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export type Opened = { harbor: Harbor; carrier: TcpCarrier };
|
|
15
|
+
import { Harbor, rootLine, type CarrierFaculty, type RootRequest } from '../harbor/index.ts';
|
|
16
|
+
import { askServed, NodeTerrain, socketOf } from '../node/index.ts';
|
|
17
|
+
import { CryptoEntropy } from '../pointer/index.ts';
|
|
18
|
+
import { TCP_OPENING } from '../tcp/index.ts';
|
|
58
19
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const harbor = await Harbor.open(new PointerTerrain({ entropy, carrier, loader: new ImportLoader(), custody: new FolderCustody(dir, entropy), memory: new FolderMemory(dir) }));
|
|
63
|
-
return { harbor, carrier };
|
|
64
|
-
};
|
|
20
|
+
// A harbor of a folder, opened for one ask: it listens nowhere and serves
|
|
21
|
+
// nothing.
|
|
22
|
+
export const open = (dir: string): Promise<Harbor> => Harbor.open(new NodeTerrain({ dir, wakes: false, serves: false }));
|
|
65
23
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
const overSocket = (dir: string, request: RootRequest): Promise<JsonObject | undefined> =>
|
|
69
|
-
new Promise((done, fail) => {
|
|
70
|
-
const socket = connect(socketOf(dir));
|
|
71
|
-
let text = '';
|
|
72
|
-
socket.once('connect', () => socket.write(`${JSON.stringify(request)}\n`));
|
|
73
|
-
socket.on('data', (chunk: Buffer) => {
|
|
74
|
-
text += chunk.toString('utf8');
|
|
75
|
-
const end = text.indexOf('\n');
|
|
76
|
-
if (end < 0) return;
|
|
77
|
-
socket.end();
|
|
78
|
-
done(JSON.parse(text.slice(0, end)) as JsonObject);
|
|
79
|
-
});
|
|
80
|
-
socket.once('error', (e: NodeJS.ErrnoException) => (e.code === 'ENOENT' || e.code === 'ECONNREFUSED' ? done(undefined) : fail(e)));
|
|
81
|
-
});
|
|
24
|
+
// Whether a harbor of this folder is served now.
|
|
25
|
+
export const isServed = async (dir: string): Promise<boolean> => (await askServed(dir, {})) !== undefined;
|
|
82
26
|
|
|
83
27
|
export const request = async (dir: string, req: RootRequest): Promise<JsonObject> => {
|
|
84
|
-
const served = await
|
|
28
|
+
const served = await askServed(dir, req);
|
|
85
29
|
if (served !== undefined) return served;
|
|
86
|
-
const
|
|
30
|
+
const harbor = await open(dir);
|
|
87
31
|
try {
|
|
88
32
|
return await rootLine(harbor, req);
|
|
89
33
|
} finally {
|
|
90
|
-
await
|
|
34
|
+
await harbor.close();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// A harbor as one JSON value, for carrying it to another ground: every
|
|
39
|
+
// place of its memory with its sealed entries as hex, and its seed only
|
|
40
|
+
// where the one who asks says so, since custody holds it and a package
|
|
41
|
+
// without it opens nowhere.
|
|
42
|
+
export type Carried = { seed?: string; places: Record<string, Record<string, string>> };
|
|
43
|
+
|
|
44
|
+
const carried = (value: unknown): Carried | null => {
|
|
45
|
+
if (typeof value !== 'object' || value === null) return null;
|
|
46
|
+
const { seed, places } = value as { seed?: unknown; places?: unknown };
|
|
47
|
+
if (seed !== undefined && !isHex(seed, 32)) return null;
|
|
48
|
+
if (typeof places !== 'object' || places === null || Array.isArray(places)) return null;
|
|
49
|
+
for (const entries of Object.values(places)) {
|
|
50
|
+
if (typeof entries !== 'object' || entries === null || Array.isArray(entries)) return null;
|
|
51
|
+
for (const bytes of Object.values(entries)) if (!isHex(bytes)) return null;
|
|
52
|
+
}
|
|
53
|
+
return { ...(seed === undefined ? {} : { seed }), places: places as Carried['places'] };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// The harbor of a folder, read as bytes alone. No harbor opens, so
|
|
57
|
+
// nothing is asked and nothing is written.
|
|
58
|
+
export const carry = async (dir: string, seed: boolean): Promise<Carried> => {
|
|
59
|
+
const memory = new FolderMemory(dir);
|
|
60
|
+
const places: Carried['places'] = {};
|
|
61
|
+
for (const place of await memory.places()) {
|
|
62
|
+
places[place] = Object.fromEntries([...(await memory.read(place))].map(([name, bytes]) => [name, hex(bytes)]));
|
|
91
63
|
}
|
|
64
|
+
if (!seed) return { places };
|
|
65
|
+
return { seed: hex(await new FolderCustody(dir, new CryptoEntropy()).seed()), places };
|
|
92
66
|
};
|
|
93
67
|
|
|
94
|
-
|
|
68
|
+
// A carried harbor into an empty folder. A folder holding a seed or a
|
|
69
|
+
// place is left as it stands, and a package with no seed needs one there
|
|
70
|
+
// already, which a terrain that seals its custody writes its own way.
|
|
71
|
+
export const land = async (dir: string, value: unknown): Promise<string[]> => {
|
|
72
|
+
const held = carried(value);
|
|
73
|
+
if (held === null) throw new Error('that is no carried harbor');
|
|
74
|
+
const memory = new FolderMemory(dir);
|
|
75
|
+
if ((await memory.places()).length > 0 || (held.seed !== undefined && existsSync(join(dir, 'seed')))) throw new Error(`a harbor stands at ${dir}`);
|
|
76
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
77
|
+
if (held.seed !== undefined) await writeFile(join(dir, 'seed'), `${held.seed}\n`, { mode: 0o600, flag: 'wx' });
|
|
78
|
+
else if (!existsSync(join(dir, 'seed'))) throw new Error('the package carries no seed, and none is held here');
|
|
79
|
+
for (const [place, entries] of Object.entries(held.places)) {
|
|
80
|
+
await memory.write(place, new Map(Object.entries(entries).map(([name, bytes]) => [name, unhex(bytes)])));
|
|
81
|
+
}
|
|
82
|
+
return Object.keys(held.places);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// A served harbor: its pk, its tcp address, every address its carriers
|
|
86
|
+
// listen at, and its root line's socket.
|
|
87
|
+
export type Served = { pk: string; at: string; listening: string[]; root: string; close(): Promise<void> };
|
|
95
88
|
|
|
96
|
-
// The harbor served:
|
|
97
|
-
//
|
|
89
|
+
// The harbor served: a harbor of the Node ground, with Quo over TCP on
|
|
90
|
+
// host and port.
|
|
98
91
|
export const serve = async (dir: string, host?: string, port?: number): Promise<Served> => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
text = text.slice(end + 1);
|
|
111
|
-
let value: unknown;
|
|
112
|
-
try {
|
|
113
|
-
value = JSON.parse(line);
|
|
114
|
-
} catch {
|
|
115
|
-
value = undefined;
|
|
116
|
-
}
|
|
117
|
-
void rootLine(harbor, value)
|
|
118
|
-
.catch(() => ({ error: 'the harbor threw' }))
|
|
119
|
-
.then((answer) => socket.write(`${JSON.stringify(answer)}\n`));
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
socket.on('error', () => socket.destroy());
|
|
123
|
-
});
|
|
124
|
-
await new Promise<void>((done) => server.listen(root, done));
|
|
125
|
-
await chmod(root, 0o600);
|
|
126
|
-
return {
|
|
127
|
-
pk: harbor.box.pk,
|
|
128
|
-
at: listener.at,
|
|
129
|
-
root,
|
|
130
|
-
close: async () => {
|
|
131
|
-
await new Promise((done) => server.close(done));
|
|
132
|
-
await rm(root, { force: true });
|
|
133
|
-
await listener.close();
|
|
134
|
-
await carrier.close();
|
|
135
|
-
},
|
|
136
|
-
};
|
|
92
|
+
const harbor = await Harbor.open(new NodeTerrain({ dir }));
|
|
93
|
+
if (!harbor.box.being(TCP_OPENING.key)) await harbor.root('open', { key: TCP_OPENING.key, class: TCP_OPENING.kind });
|
|
94
|
+
const tcp = harbor.box.being(TCP_OPENING.key) as CarrierFaculty;
|
|
95
|
+
if (host !== undefined || port !== undefined || tcp.listening().length === 0) {
|
|
96
|
+
const heard = (await harbor.root('ask', { being: TCP_OPENING.key, method: 'listen', args: { ...(host === undefined ? {} : { host }), ...(port === undefined ? {} : { port }) } })) as JsonObject;
|
|
97
|
+
if (typeof (heard.answer as JsonObject | undefined)?.at !== 'string') {
|
|
98
|
+
await harbor.close();
|
|
99
|
+
throw new Error(`the harbor does not listen: ${JSON.stringify(heard)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { pk: harbor.box.pk, at: tcp.listening()[0]!, listening: harbor.carriers.flatMap((c) => c.listening()), root: socketOf(dir), close: () => harbor.close() };
|
|
137
103
|
};
|
package/src/contract/index.ts
CHANGED
|
@@ -36,8 +36,15 @@ export interface Rows {
|
|
|
36
36
|
export abstract class Memory {
|
|
37
37
|
// Every entry kept in a place; a place nothing was kept in is empty.
|
|
38
38
|
abstract read(place: string): Promise<Map<string, Uint8Array>>;
|
|
39
|
-
// One keep:
|
|
39
|
+
// One keep, whole: every entry written and every null removed together,
|
|
40
|
+
// or none of them. A read after a keep that did not finish, in this run
|
|
41
|
+
// or in the run before it, holds the place as it was or as the keep
|
|
42
|
+
// leaves it, and never half of each.
|
|
40
43
|
abstract write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
|
|
44
|
+
// Every place this memory holds something in, in any order.
|
|
45
|
+
abstract places(): Promise<string[]>;
|
|
46
|
+
// A place and everything in it, gone.
|
|
47
|
+
abstract forget(place: string): Promise<void>;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
// The harbor's seed: thirty-two bytes, the same every time. It is the one
|
|
@@ -61,13 +68,9 @@ export abstract class Loader {
|
|
|
61
68
|
abstract load(from: string): Promise<Loaded | null>;
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
// Bytes to a ward pk in; bytes, or null for nothing, out.
|
|
71
|
+
// Bytes to a ward pk in; bytes, or null for nothing, out. A harbor hands a
|
|
72
|
+
// terrain's carrier what it holds no route for; what it routes goes to its
|
|
73
|
+
// carrier faculties.
|
|
65
74
|
export abstract class Carrier {
|
|
66
75
|
abstract carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
67
|
-
// Where a ward pk is reached, or null to forget it: true when this
|
|
68
|
-
// carrier dials by address and took it. The addresses live in the dock's
|
|
69
|
-
// cells, and the harbor hands them here as it unpacks.
|
|
70
|
-
route(_pk: string, _at: string | null): boolean {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
76
|
}
|