nervur 0.23.0 → 0.24.0
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/CONTRIBUTING.md +13 -0
- package/LICENSE +55 -202
- package/NOTICE +3 -8
- package/README.md +14 -156
- package/RELEASING.md +15 -0
- package/SECURITY.md +9 -0
- package/bin/nervur.js +6 -0
- package/package.json +26 -81
- package/AUTHORING.md +0 -935
- package/COMMAND.md +0 -179
- package/FACES.md +0 -280
- package/FACULTIES.md +0 -578
- package/GROUNDS.md +0 -315
- package/KIT-SPEC.md +0 -304
- package/WORLD.md +0 -195
- package/dist/app/app-ground.d.ts +0 -18
- package/dist/app/app-ground.js +0 -23
- package/dist/app/index.d.ts +0 -2
- package/dist/app/index.js +0 -5
- package/dist/app/native.d.ts +0 -11
- package/dist/app/native.js +0 -13
- package/dist/being/being.d.ts +0 -306
- package/dist/being/being.js +0 -20
- package/dist/being/covers.d.ts +0 -9
- package/dist/being/covers.js +0 -21
- package/dist/being/index.d.ts +0 -4
- package/dist/being/index.js +0 -6
- package/dist/being/need.d.ts +0 -49
- package/dist/being/need.js +0 -33
- package/dist/being/schema.d.ts +0 -100
- package/dist/being/schema.js +0 -132
- package/dist/being/table.d.ts +0 -48
- package/dist/being/table.js +0 -344
- package/dist/bench/bench-ground.d.ts +0 -73
- package/dist/bench/bench-ground.js +0 -153
- package/dist/bench/bench.d.ts +0 -101
- package/dist/bench/bench.js +0 -368
- package/dist/bench/fake-clock.d.ts +0 -17
- package/dist/bench/fake-clock.js +0 -34
- package/dist/bench/fake-memory.d.ts +0 -16
- package/dist/bench/fake-memory.js +0 -46
- package/dist/bench/fake-network.d.ts +0 -66
- package/dist/bench/fake-network.js +0 -222
- package/dist/bench/fake-unlock.d.ts +0 -6
- package/dist/bench/fake-unlock.js +0 -10
- package/dist/bench/index.d.ts +0 -3
- package/dist/bench/index.js +0 -5
- package/dist/bench/seeded.d.ts +0 -11
- package/dist/bench/seeded.js +0 -37
- package/dist/bench/settle.d.ts +0 -2
- package/dist/bench/settle.js +0 -49
- package/dist/bench/stewards.d.ts +0 -169
- package/dist/bench/stewards.js +0 -128
- package/dist/bodies/class-list.d.ts +0 -14
- package/dist/bodies/class-list.js +0 -33
- package/dist/bodies/joined-carry.d.ts +0 -29
- package/dist/bodies/joined-carry.js +0 -48
- package/dist/bodies/kept.d.ts +0 -44
- package/dist/bodies/kept.js +0 -91
- package/dist/bodies/noble-crypto.d.ts +0 -22
- package/dist/bodies/noble-crypto.js +0 -88
- package/dist/bodies/seed-keys.d.ts +0 -10
- package/dist/bodies/seed-keys.js +0 -42
- package/dist/bodies/strict-tools.d.ts +0 -10
- package/dist/bodies/strict-tools.js +0 -306
- package/dist/bodies/vouch.d.ts +0 -20
- package/dist/bodies/vouch.js +0 -71
- package/dist/bodies/web-carry.d.ts +0 -72
- package/dist/bodies/web-carry.js +0 -245
- package/dist/bodies/web-clock.d.ts +0 -12
- package/dist/bodies/web-clock.js +0 -33
- package/dist/browser/browser-ground.d.ts +0 -76
- package/dist/browser/browser-ground.js +0 -294
- package/dist/browser/index.d.ts +0 -4
- package/dist/browser/index.js +0 -7
- package/dist/browser/indexeddb-memory.d.ts +0 -17
- package/dist/browser/indexeddb-memory.js +0 -97
- package/dist/browser/locked-unlock.d.ts +0 -24
- package/dist/browser/locked-unlock.js +0 -85
- package/dist/browser/origin-classes.d.ts +0 -7
- package/dist/browser/origin-classes.js +0 -31
- package/dist/edge/durable-clock.d.ts +0 -19
- package/dist/edge/durable-clock.js +0 -61
- package/dist/edge/durable-memory.d.ts +0 -5
- package/dist/edge/durable-memory.js +0 -11
- package/dist/edge/durable.d.ts +0 -18
- package/dist/edge/durable.js +0 -24
- package/dist/edge/edge-ground.d.ts +0 -49
- package/dist/edge/edge-ground.js +0 -223
- package/dist/edge/index.d.ts +0 -7
- package/dist/edge/index.js +0 -9
- package/dist/edge/native-crypto.d.ts +0 -8
- package/dist/edge/native-crypto.js +0 -102
- package/dist/edge/secret-unlock.d.ts +0 -7
- package/dist/edge/secret-unlock.js +0 -13
- package/dist/edge/socket-carry.d.ts +0 -39
- package/dist/edge/socket-carry.js +0 -111
- package/dist/foundation.d.ts +0 -162
- package/dist/foundation.js +0 -4
- package/dist/ground/dock.d.ts +0 -1460
- package/dist/ground/dock.js +0 -1159
- package/dist/ground/ground.d.ts +0 -239
- package/dist/ground/ground.js +0 -787
- package/dist/ground/views.d.ts +0 -76
- package/dist/ground/views.js +0 -208
- package/dist/house/crossing.d.ts +0 -50
- package/dist/house/crossing.js +0 -153
- package/dist/house/house.d.ts +0 -88
- package/dist/house/house.js +0 -2069
- package/dist/house/rows-shape.d.ts +0 -110
- package/dist/house/rows-shape.js +0 -12
- package/dist/house/rows.d.ts +0 -27
- package/dist/house/rows.js +0 -185
- package/dist/index.d.ts +0 -33
- package/dist/index.js +0 -29
- package/dist/node/bridge.d.ts +0 -20
- package/dist/node/bridge.js +0 -284
- package/dist/node/cli.d.ts +0 -2
- package/dist/node/cli.js +0 -225
- package/dist/node/file-memory.d.ts +0 -13
- package/dist/node/file-memory.js +0 -86
- package/dist/node/folder-classes.d.ts +0 -5
- package/dist/node/folder-classes.js +0 -40
- package/dist/node/gone.d.ts +0 -2
- package/dist/node/gone.js +0 -7
- package/dist/node/hand.d.ts +0 -39
- package/dist/node/hand.js +0 -107
- package/dist/node/http.d.ts +0 -11
- package/dist/node/http.js +0 -90
- package/dist/node/index.d.ts +0 -11
- package/dist/node/index.js +0 -13
- package/dist/node/ledger-memory.d.ts +0 -17
- package/dist/node/ledger-memory.js +0 -152
- package/dist/node/lock.d.ts +0 -10
- package/dist/node/lock.js +0 -78
- package/dist/node/node-ground.d.ts +0 -26
- package/dist/node/node-ground.js +0 -228
- package/dist/node/notify.d.ts +0 -1
- package/dist/node/notify.js +0 -18
- package/dist/node/shell.d.ts +0 -11
- package/dist/node/shell.js +0 -35
- package/dist/node/sync-folder.d.ts +0 -6
- package/dist/node/sync-folder.js +0 -18
- package/dist/node/tcp-carry.d.ts +0 -42
- package/dist/node/tcp-carry.js +0 -193
- package/dist/node/unlock.d.ts +0 -17
- package/dist/node/unlock.js +0 -86
- package/dist/node/websocket.d.ts +0 -12
- package/dist/node/websocket.js +0 -134
- package/dist/quo/frame.d.ts +0 -33
- package/dist/quo/frame.js +0 -92
- package/dist/quo/index.d.ts +0 -1
- package/dist/quo/index.js +0 -5
- package/dist/quo/room.d.ts +0 -154
- package/dist/quo/room.js +0 -362
- package/dist/serve/index.d.ts +0 -45
- package/dist/serve/index.js +0 -71
package/dist/node/bridge.js
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The bridge: a faculty written in any language, as a program the ground
|
|
3
|
-
// starts beside it. It speaks one JSON value a line on its standard
|
|
4
|
-
// streams. Its first answer, to `describe`, is its blueprint and its
|
|
5
|
-
// window, fixed for as long as its faculty stands. It starts with an
|
|
6
|
-
// empty environment beside what `env` names, and it is started again
|
|
7
|
-
// whenever it exits. The object the house holds never changes. It keeps
|
|
8
|
-
// its state in the faculty's memory, through lines of its own.
|
|
9
|
-
//
|
|
10
|
-
// in { id, method, args, call } a call to the program
|
|
11
|
-
// out { id, result } | { id, error } its answer
|
|
12
|
-
// out { id, token, args, call } the program calls a handle
|
|
13
|
-
// in { id, result } | { id, error } the house's answer to that
|
|
14
|
-
// out { id, memory, args } the program reads or writes its memory
|
|
15
|
-
// in { id, result } | { id, error } the ground's answer to that
|
|
16
|
-
//
|
|
17
|
-
// `id` pairs a line with its answer, and a program numbers its own lines.
|
|
18
|
-
// `call` is the call id: the house's on a call in, and the program's own
|
|
19
|
-
// on a call out, the same every time that call is sent, in any life of
|
|
20
|
-
// the program, so a handle called twice acts once. A memory line names
|
|
21
|
-
// `read`, `list` or `write`, with the memory contract's args, every
|
|
22
|
-
// entry's bytes in lowercase hex.
|
|
23
|
-
import { spawn } from 'node:child_process';
|
|
24
|
-
import { accessSync, constants } from 'node:fs';
|
|
25
|
-
import { delimiter, isAbsolute, join } from 'node:path';
|
|
26
|
-
import { createInterface } from 'node:readline';
|
|
27
|
-
import { need } from '../being/need.js';
|
|
28
|
-
import { StrictTools } from '../bodies/strict-tools.js';
|
|
29
|
-
const tools = new StrictTools();
|
|
30
|
-
// The bytes of every entry a memory line writes, read from hex.
|
|
31
|
-
const writesOf = (writes) => {
|
|
32
|
-
if (typeof writes !== 'object' || writes === null || Array.isArray(writes))
|
|
33
|
-
throw new TypeError('a write names its places in writes');
|
|
34
|
-
return Object.fromEntries(Object.entries(writes).map(([place, entries]) => {
|
|
35
|
-
if (typeof entries !== 'object' || entries === null || Array.isArray(entries))
|
|
36
|
-
throw new TypeError(`the place ${place} is an object of entries`);
|
|
37
|
-
return [
|
|
38
|
-
place,
|
|
39
|
-
Object.fromEntries(Object.entries(entries).map(([entry, hex]) => {
|
|
40
|
-
if (hex === null)
|
|
41
|
-
return [entry, null];
|
|
42
|
-
const bytes = typeof hex === 'string' ? tools.bytes(hex) : null;
|
|
43
|
-
if (bytes === null)
|
|
44
|
-
throw new TypeError(`the entry ${entry} of ${place} holds no lowercase hex`);
|
|
45
|
-
return [entry, bytes];
|
|
46
|
-
})),
|
|
47
|
-
];
|
|
48
|
-
}));
|
|
49
|
-
};
|
|
50
|
-
const expectOf = (expect) => {
|
|
51
|
-
if (typeof expect !== 'object' || expect === null || Array.isArray(expect) || !Object.values(expect).every((version) => version === null || typeof version === 'string')) {
|
|
52
|
-
throw new TypeError('a write names the versions it read in expect');
|
|
53
|
-
}
|
|
54
|
-
return expect;
|
|
55
|
-
};
|
|
56
|
-
// One memory line answered from the faculty's memory.
|
|
57
|
-
const remembered = async (memory, op, args) => {
|
|
58
|
-
if (memory === undefined)
|
|
59
|
-
return { error: { message: 'this program holds no memory' } };
|
|
60
|
-
const given = (typeof args === 'object' && args !== null && !Array.isArray(args) ? args : {});
|
|
61
|
-
try {
|
|
62
|
-
if (op === 'read') {
|
|
63
|
-
if (typeof given.place !== 'string')
|
|
64
|
-
throw new TypeError('a read names its place');
|
|
65
|
-
const { entries, version } = await memory.read({ place: given.place });
|
|
66
|
-
return { result: { entries: Object.fromEntries(Object.entries(entries).map(([entry, bytes]) => [entry, tools.hex(bytes)])), version } };
|
|
67
|
-
}
|
|
68
|
-
if (op === 'list')
|
|
69
|
-
return { result: [...(await memory.list())] };
|
|
70
|
-
if (op === 'write')
|
|
71
|
-
return { result: (await memory.write({ writes: writesOf(given.writes), expect: expectOf(given.expect) })) };
|
|
72
|
-
return { error: { message: `no memory line ${op}` } };
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
return { error: { message: error instanceof Error ? error.message : String(error) } };
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
/** The longest line either side writes, as a Quo frame's bound. */
|
|
79
|
-
const LONGEST = 1_048_576;
|
|
80
|
-
const BACKOFF = 60_000;
|
|
81
|
-
// The contexts of recent calls, where a token the program calls back is sought.
|
|
82
|
-
const CONTEXTS = 32;
|
|
83
|
-
// A name found on the ground's PATH, so the program's own empty environment needs none.
|
|
84
|
-
const located = (command) => {
|
|
85
|
-
if (isAbsolute(command) || command.includes('/'))
|
|
86
|
-
return command;
|
|
87
|
-
for (const dir of (process.env.PATH ?? '').split(delimiter)) {
|
|
88
|
-
const at = join(dir, command);
|
|
89
|
-
try {
|
|
90
|
-
accessSync(at, constants.X_OK);
|
|
91
|
-
return at;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
// Not in this folder.
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
throw new Error(`no program ${command} on the ground's PATH`);
|
|
98
|
-
};
|
|
99
|
-
class Bridge {
|
|
100
|
-
#options;
|
|
101
|
-
#command;
|
|
102
|
-
#child;
|
|
103
|
-
#calls = new Map();
|
|
104
|
-
#next = 0;
|
|
105
|
-
#blueprint;
|
|
106
|
-
#contexts = [];
|
|
107
|
-
#stopped = false;
|
|
108
|
-
/** Why it was stopped for good: a program that described another blueprint than it did at the boot. */
|
|
109
|
-
#refused;
|
|
110
|
-
#failures = 0;
|
|
111
|
-
#up;
|
|
112
|
-
constructor(options) {
|
|
113
|
-
this.#options = options;
|
|
114
|
-
this.#command = located(options.command);
|
|
115
|
-
}
|
|
116
|
-
// The program started, its describe read, and its blueprint held to the first one.
|
|
117
|
-
start() {
|
|
118
|
-
this.#up = (async () => {
|
|
119
|
-
const child = spawn(this.#command, [...(this.#options.args ?? [])], { cwd: this.#options.cwd, env: { ...this.#options.env }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
120
|
-
this.#child = child;
|
|
121
|
-
child.stdin.on('error', () => undefined);
|
|
122
|
-
child.stderr.on('data', (chunk) => process.stderr.write(`[${this.#options.command}] ${chunk.toString()}`));
|
|
123
|
-
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
124
|
-
lines.on('line', (line) => this.#read(line));
|
|
125
|
-
child.once('exit', () => this.#exited(child));
|
|
126
|
-
// A program that describes nothing, or runs past the describe's wait, is ended, so its exit starts it again.
|
|
127
|
-
const failed = (why) => {
|
|
128
|
-
child.kill('SIGKILL');
|
|
129
|
-
return new Error(`the program ${this.#options.command} ${why}`);
|
|
130
|
-
};
|
|
131
|
-
const described = await this.#send('describe', {}, undefined, this.#options.wait ?? 10_000).catch((error) => {
|
|
132
|
-
throw failed(`described nothing: ${error instanceof Error ? error.message : String(error)}`);
|
|
133
|
-
});
|
|
134
|
-
if ('error' in described)
|
|
135
|
-
throw failed(`described nothing: ${described.error.message}`);
|
|
136
|
-
const result = described.result;
|
|
137
|
-
if (typeof result?.blueprint?.name !== 'string' || typeof result.blueprint.methods !== 'object')
|
|
138
|
-
throw failed('described no blueprint');
|
|
139
|
-
const text = JSON.stringify(result.blueprint);
|
|
140
|
-
// Another blueprint than at the boot is refused and stopped for good: new code arrives when its faculty stands again.
|
|
141
|
-
if (this.#blueprint !== undefined && this.#blueprint !== text) {
|
|
142
|
-
this.#stopped = true;
|
|
143
|
-
this.#refused = `the program ${this.#options.command} describes another blueprint than it did at the boot, and is stopped`;
|
|
144
|
-
process.stderr.write(`[${this.#options.command}] ${this.#refused}\n`);
|
|
145
|
-
throw failed('describes another blueprint than it did at the boot');
|
|
146
|
-
}
|
|
147
|
-
this.#blueprint = text;
|
|
148
|
-
this.#failures = 0;
|
|
149
|
-
return { blueprint: result.blueprint, ...(typeof result.window === 'number' ? { window: result.window } : {}) };
|
|
150
|
-
})();
|
|
151
|
-
return this.#up;
|
|
152
|
-
}
|
|
153
|
-
#read(line) {
|
|
154
|
-
if (line.length > LONGEST) {
|
|
155
|
-
this.#child?.kill('SIGKILL');
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
let message;
|
|
159
|
-
try {
|
|
160
|
-
message = JSON.parse(line);
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
process.stderr.write(`[${this.#options.command}] a line that is no JSON: ${line.slice(0, 200)}\n`);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
// The program reads or writes its memory.
|
|
167
|
-
if (typeof message.memory === 'string') {
|
|
168
|
-
const { id } = message;
|
|
169
|
-
void remembered(this.#options.memory, message.memory, message.args).then((answer) => this.#write({ id, ...answer }));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
// The program calls a handle it was handed.
|
|
173
|
-
if (typeof message.token === 'string') {
|
|
174
|
-
if (typeof message.call === 'string' && message.call !== '')
|
|
175
|
-
void this.#callBack(message.id, message.token, message.args, message.call);
|
|
176
|
-
else
|
|
177
|
-
this.#write({ id: message.id, error: { message: 'a handle is called with a call id of the program’s own' } });
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const waiting = typeof message.id === 'number' ? this.#calls.get(message.id) : undefined;
|
|
181
|
-
if (waiting === undefined)
|
|
182
|
-
return;
|
|
183
|
-
this.#calls.delete(message.id);
|
|
184
|
-
if ('result' in message)
|
|
185
|
-
waiting.resolve({ result: message.result });
|
|
186
|
-
else
|
|
187
|
-
waiting.resolve({ error: { message: typeof message.error?.message === 'string' ? message.error.message : 'the program failed' } });
|
|
188
|
-
}
|
|
189
|
-
// A token is answered by the house that handed it, found among the contexts of recent calls.
|
|
190
|
-
async #callBack(id, token, args, call) {
|
|
191
|
-
let answer = { error: { message: 'no such token' } };
|
|
192
|
-
for (const context of [...this.#contexts].reverse()) {
|
|
193
|
-
answer = await context.call({ token, args: (args ?? {}), id: call });
|
|
194
|
-
if (!('error' in answer) || answer.error.message !== 'no such token')
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
this.#write({ id, ...answer });
|
|
198
|
-
}
|
|
199
|
-
#write(message) {
|
|
200
|
-
this.#child?.stdin?.write(`${JSON.stringify(message)}\n`);
|
|
201
|
-
}
|
|
202
|
-
#send(method, args, call, wait) {
|
|
203
|
-
const child = this.#child;
|
|
204
|
-
if (child === undefined || child.exitCode !== null || child.signalCode !== null)
|
|
205
|
-
return Promise.reject(new Error(`the program ${this.#options.command} is down`));
|
|
206
|
-
const id = ++this.#next;
|
|
207
|
-
return new Promise((resolve, reject) => {
|
|
208
|
-
const timer = wait === undefined ? undefined : setTimeout(() => reject(new Error(`the program ${this.#options.command} did not describe itself in time`)), wait);
|
|
209
|
-
this.#calls.set(id, {
|
|
210
|
-
resolve: (answer) => {
|
|
211
|
-
clearTimeout(timer);
|
|
212
|
-
resolve(answer);
|
|
213
|
-
},
|
|
214
|
-
reject: (error) => {
|
|
215
|
-
clearTimeout(timer);
|
|
216
|
-
reject(error);
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
this.#write({ id, method, args, ...(call === undefined ? {} : { call }) });
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
// Every call in flight failed, as a crash fails it, and the program started again after a pause that grows.
|
|
223
|
-
#exited(child) {
|
|
224
|
-
if (this.#child !== child)
|
|
225
|
-
return;
|
|
226
|
-
for (const waiting of this.#calls.values())
|
|
227
|
-
waiting.reject(new Error(`the program ${this.#options.command} exited`));
|
|
228
|
-
this.#calls.clear();
|
|
229
|
-
if (this.#stopped)
|
|
230
|
-
return;
|
|
231
|
-
const pause = this.#failures === 0 ? 0 : Math.min(BACKOFF, 1_000 * 2 ** (this.#failures - 1));
|
|
232
|
-
this.#failures++;
|
|
233
|
-
setTimeout(() => {
|
|
234
|
-
if (!this.#stopped)
|
|
235
|
-
this.start().catch((error) => process.stderr.write(`[${this.#options.command}] ${error instanceof Error ? error.message : String(error)}\n`));
|
|
236
|
-
}, pause).unref();
|
|
237
|
-
}
|
|
238
|
-
/** One call, as the house makes it. A throw is a failure to answer, and the house tries again. */
|
|
239
|
-
async call(method, args, context) {
|
|
240
|
-
this.#contexts = [...this.#contexts.filter((held) => held !== context), context].slice(-CONTEXTS);
|
|
241
|
-
// A program refused for good answers why, which is final: asking again changes nothing until its ground restarts.
|
|
242
|
-
const refused = () => (this.#refused === undefined ? undefined : { error: { message: this.#refused } });
|
|
243
|
-
try {
|
|
244
|
-
await this.#up;
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
const why = refused();
|
|
248
|
-
if (why !== undefined)
|
|
249
|
-
return why;
|
|
250
|
-
throw error;
|
|
251
|
-
}
|
|
252
|
-
return refused() ?? this.#send(method, args, context.id);
|
|
253
|
-
}
|
|
254
|
-
async stop() {
|
|
255
|
-
this.#stopped = true;
|
|
256
|
-
const child = this.#child;
|
|
257
|
-
// A program ended by a signal has no exit code, and has exited all the same.
|
|
258
|
-
if (child === undefined || child.exitCode !== null || child.signalCode !== null)
|
|
259
|
-
return;
|
|
260
|
-
const exited = new Promise((resolve) => child.once('exit', () => resolve()));
|
|
261
|
-
child.stdin?.end();
|
|
262
|
-
const term = setTimeout(() => child.kill('SIGTERM'), 2_000);
|
|
263
|
-
const kill = setTimeout(() => child.kill('SIGKILL'), 4_000);
|
|
264
|
-
await exited;
|
|
265
|
-
clearTimeout(term);
|
|
266
|
-
clearTimeout(kill);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* A program in any language, raised as a body: its blueprint and its
|
|
271
|
-
* window from its own `describe`, and an object whose every method is a
|
|
272
|
-
* line to it. It answers once the program has described itself.
|
|
273
|
-
*/
|
|
274
|
-
export const bridge = async (options) => {
|
|
275
|
-
const held = new Bridge(options);
|
|
276
|
-
// A program that never described itself is offered to no one, so nothing starts it again.
|
|
277
|
-
const { blueprint, window } = await held.start().catch(async (error) => {
|
|
278
|
-
await held.stop();
|
|
279
|
-
throw error;
|
|
280
|
-
});
|
|
281
|
-
const { name, methods } = blueprint;
|
|
282
|
-
const object = Object.fromEntries(Object.keys(methods).map((method) => [method, (args, context) => held.call(method, args, context)]));
|
|
283
|
-
return { blueprint: need(name, methods), object, ...(window === undefined ? {} : { window }), down: () => held.stop() };
|
|
284
|
-
};
|
package/dist/node/cli.d.ts
DELETED
package/dist/node/cli.js
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
// The package's command: a face on a ground's hand, holding no business of
|
|
4
|
-
// its own. Two words start things on this machine; every other word is
|
|
5
|
-
// read from what the running ground describes.
|
|
6
|
-
//
|
|
7
|
-
// nervur up <folder> runs a NodeGround on its folder until it is stopped
|
|
8
|
-
// nervur service <folder> prints its systemd unit, or its launchd job on macOS
|
|
9
|
-
// nervur help what the ground holds: the dock's asks, its faculties, its houses
|
|
10
|
-
// nervur <word> [<word>] [args] an ask of the dock, `houses add` asking housesAdd; one word alone, the asks it begins
|
|
11
|
-
// nervur <faculty> [<method> [args]] a faculty's method, called through the dock; with none, its methods
|
|
12
|
-
// nervur ask <house> [--id <being>] [<method> [args]] an ask of a being; with no method, its describe
|
|
13
|
-
// nervur ask <house> [--id <being>] --cells her cells, read and nothing asked
|
|
14
|
-
// nervur ask dock --id <being> [--cells] the same of a being of the dock, `faculty.web` or `house.shop`
|
|
15
|
-
//
|
|
16
|
-
// Args are one JSON object, or words `key=value`, each value read as JSON
|
|
17
|
-
// where it reads and as text where it does not, or `-`, one JSON object
|
|
18
|
-
// read from standard input, so no secret stands in a shell's history, or
|
|
19
|
-
// `-- <command> [<arg>...]`, a command line: `nervur shell run -- ls -l`.
|
|
20
|
-
// `--at <socket>` or `NERVUR_HAND` names the hand, and `state/hand` in
|
|
21
|
-
// this folder is it where neither does. `--call <id>` names the ask's call
|
|
22
|
-
// id, so the same ask sent again answers what the first answered. Each
|
|
23
|
-
// prints one JSON value and exits 0 on a result, 1 on an error answered,
|
|
24
|
-
// and 2 where nothing was asked.
|
|
25
|
-
import { existsSync } from 'node:fs';
|
|
26
|
-
import { connect } from 'node:net';
|
|
27
|
-
import { basename, join, resolve } from 'node:path';
|
|
28
|
-
import { createInterface } from 'node:readline';
|
|
29
|
-
import { fileURLToPath } from 'node:url';
|
|
30
|
-
import { handAt } from './hand.js';
|
|
31
|
-
import { NodeGround } from './node-ground.js';
|
|
32
|
-
import { notifyReady } from './notify.js';
|
|
33
|
-
const USAGE = 'nervur up <folder> | service <folder> | [--at <socket>] [--call <id>] help | ask <house> [--id <being>] [--cells | <method> [args | -]] | <word> [<word>] [args | - | -- <command> [<arg>...]]';
|
|
34
|
-
const fail = (message) => {
|
|
35
|
-
process.stderr.write(`${message}\n${USAGE}\n`);
|
|
36
|
-
process.exit(2);
|
|
37
|
-
};
|
|
38
|
-
// One JSON object, or nothing where the text is none.
|
|
39
|
-
const objectOf = (text) => {
|
|
40
|
-
try {
|
|
41
|
-
const value = JSON.parse(text);
|
|
42
|
-
if (typeof value === 'object' && value !== null && !Array.isArray(value))
|
|
43
|
-
return value;
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// No JSON: the caller says why.
|
|
47
|
-
}
|
|
48
|
-
return undefined;
|
|
49
|
-
};
|
|
50
|
-
// Standard input, whole.
|
|
51
|
-
const stdin = async () => {
|
|
52
|
-
let text = '';
|
|
53
|
-
for await (const chunk of process.stdin)
|
|
54
|
-
text += String(chunk);
|
|
55
|
-
return text;
|
|
56
|
-
};
|
|
57
|
-
// Args as one JSON object, as words `key=value`, or as one JSON object on standard input.
|
|
58
|
-
const argsOf = async (words) => {
|
|
59
|
-
if (words.length === 0)
|
|
60
|
-
return undefined;
|
|
61
|
-
if (words.length === 1 && words[0] === '-')
|
|
62
|
-
return objectOf(await stdin()) ?? fail('standard input holds no JSON object');
|
|
63
|
-
if (words.length === 1 && words[0].startsWith('{'))
|
|
64
|
-
return objectOf(words[0]) ?? fail('args are one JSON object');
|
|
65
|
-
// A command line after `--`, taken whole: its first word the command, and every word after it an arg.
|
|
66
|
-
if (words[0] === '--')
|
|
67
|
-
return words.length > 1 ? { command: words[1], args: words.slice(2) } : fail('-- names no command');
|
|
68
|
-
return Object.fromEntries(words.map((word) => {
|
|
69
|
-
const at = word.indexOf('=');
|
|
70
|
-
if (at <= 0)
|
|
71
|
-
return fail(`${word} is not key=value`);
|
|
72
|
-
const text = word.slice(at + 1);
|
|
73
|
-
try {
|
|
74
|
-
return [word.slice(0, at), JSON.parse(text)];
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return [word.slice(0, at), text];
|
|
78
|
-
}
|
|
79
|
-
}));
|
|
80
|
-
};
|
|
81
|
-
// One line to the hand, and its answer.
|
|
82
|
-
const exchange = (at, line) => {
|
|
83
|
-
if (at === undefined || at === '')
|
|
84
|
-
return fail('no socket: pass --at or set NERVUR_HAND');
|
|
85
|
-
return new Promise((answered) => {
|
|
86
|
-
const socket = connect(at);
|
|
87
|
-
socket.on('error', (error) => fail(`the hand at ${at} does not answer: ${error.message}`));
|
|
88
|
-
socket.on('connect', () => socket.write(`${JSON.stringify(line)}\n`));
|
|
89
|
-
createInterface({ input: socket }).once('line', (text) => {
|
|
90
|
-
socket.end();
|
|
91
|
-
answered(JSON.parse(text));
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
};
|
|
95
|
-
// An answer printed. A result and a describe are what was asked; an error answered is the one refusal.
|
|
96
|
-
const print = (answer) => {
|
|
97
|
-
process.stdout.write(`${JSON.stringify(answer)}\n`);
|
|
98
|
-
process.exitCode = 'error' in answer ? 1 : 0;
|
|
99
|
-
};
|
|
100
|
-
// One line to the hand, with the call's id where one was named, and its answer printed.
|
|
101
|
-
const send = async (at, line) => print(await exchange(at, { ...line, ...(call === undefined || 'describe' in line ? {} : { call }) }));
|
|
102
|
-
const up = async (folder) => {
|
|
103
|
-
const ground = await NodeGround.open({ folder }).catch((error) => {
|
|
104
|
-
process.stderr.write(`the ground did not open: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
105
|
-
return process.exit(1);
|
|
106
|
-
});
|
|
107
|
-
await notifyReady();
|
|
108
|
-
process.stdout.write(`${JSON.stringify({ hand: ground.hand, houses: ground.ground.list() })}\n`);
|
|
109
|
-
// A stop is the boot reversed, on an interrupt or on the service manager's SIGTERM.
|
|
110
|
-
const stop = async () => {
|
|
111
|
-
await ground.close();
|
|
112
|
-
process.exit(0);
|
|
113
|
-
};
|
|
114
|
-
process.on('SIGINT', stop);
|
|
115
|
-
process.on('SIGTERM', stop);
|
|
116
|
-
};
|
|
117
|
-
const service = (folder) => {
|
|
118
|
-
const cli = fileURLToPath(import.meta.url);
|
|
119
|
-
const unit = process.platform === 'darwin'
|
|
120
|
-
? [
|
|
121
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
122
|
-
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
123
|
-
'<plist version="1.0">',
|
|
124
|
-
'<dict>',
|
|
125
|
-
' <key>Label</key>',
|
|
126
|
-
` <string>org.nervur.${basename(folder).replace(/[^A-Za-z0-9.-]/g, '-')}</string>`,
|
|
127
|
-
' <key>ProgramArguments</key>',
|
|
128
|
-
' <array>',
|
|
129
|
-
` <string>${process.execPath}</string>`,
|
|
130
|
-
` <string>${cli}</string>`,
|
|
131
|
-
' <string>up</string>',
|
|
132
|
-
` <string>${folder}</string>`,
|
|
133
|
-
' </array>',
|
|
134
|
-
' <key>KeepAlive</key>',
|
|
135
|
-
' <true/>',
|
|
136
|
-
' <key>RunAtLoad</key>',
|
|
137
|
-
' <true/>',
|
|
138
|
-
'</dict>',
|
|
139
|
-
'</plist>',
|
|
140
|
-
]
|
|
141
|
-
: [
|
|
142
|
-
'[Unit]',
|
|
143
|
-
`Description=A nervur ground on ${folder}`,
|
|
144
|
-
'After=network-online.target',
|
|
145
|
-
'',
|
|
146
|
-
'[Service]',
|
|
147
|
-
'Type=notify',
|
|
148
|
-
'NotifyAccess=all',
|
|
149
|
-
`ExecStart=${process.execPath} ${cli} up ${folder}`,
|
|
150
|
-
'Restart=always',
|
|
151
|
-
'RestartSec=1',
|
|
152
|
-
'',
|
|
153
|
-
'[Install]',
|
|
154
|
-
'WantedBy=default.target',
|
|
155
|
-
];
|
|
156
|
-
process.stdout.write(`${unit.join('\n')}\n`);
|
|
157
|
-
};
|
|
158
|
-
const words = process.argv.slice(2);
|
|
159
|
-
// The hand: named, or the one a ground in this folder serves.
|
|
160
|
-
let at = process.env.NERVUR_HAND ?? (existsSync(process.platform === 'win32' ? 'state' : join('state', 'hand')) ? handAt('state') : undefined);
|
|
161
|
-
if (words[0] === '--at') {
|
|
162
|
-
at = words[1] ?? fail('--at names no socket');
|
|
163
|
-
words.splice(0, 2);
|
|
164
|
-
}
|
|
165
|
-
// The call's id: the same ask sent again with it answers what the first answered, and runs nothing twice.
|
|
166
|
-
let call;
|
|
167
|
-
if (words[0] === '--call') {
|
|
168
|
-
call = words[1] ?? fail('--call names no id');
|
|
169
|
-
words.splice(0, 2);
|
|
170
|
-
}
|
|
171
|
-
const [command, ...rest] = words;
|
|
172
|
-
if (command === undefined)
|
|
173
|
-
fail('no command');
|
|
174
|
-
else if (command === 'up' || command === 'service') {
|
|
175
|
-
if (rest.length !== 1)
|
|
176
|
-
fail(`${command} names one folder`);
|
|
177
|
-
if (command === 'up')
|
|
178
|
-
await up(resolve(rest[0]));
|
|
179
|
-
else
|
|
180
|
-
service(resolve(rest[0]));
|
|
181
|
-
}
|
|
182
|
-
else if (command === 'help')
|
|
183
|
-
await send(at, { describe: true });
|
|
184
|
-
else if (command === 'ask') {
|
|
185
|
-
const [named, ...more] = rest;
|
|
186
|
-
if (named === undefined)
|
|
187
|
-
fail('ask names a house');
|
|
188
|
-
// The dock is the name no house takes: asked by it, the hand reaches the dock's own beings.
|
|
189
|
-
const house = named === 'dock' ? {} : { house: named };
|
|
190
|
-
let id;
|
|
191
|
-
if (more[0] === '--id') {
|
|
192
|
-
id = more[1] ?? fail('--id names no being');
|
|
193
|
-
more.splice(0, 2);
|
|
194
|
-
}
|
|
195
|
-
if (more[0] === '--cells') {
|
|
196
|
-
if (more.length > 1)
|
|
197
|
-
fail('--cells reads her cells alone');
|
|
198
|
-
await send(at, { ...house, ...(id === undefined ? {} : { id }), cells: true });
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
const [method, ...args] = more;
|
|
202
|
-
await send(at, { ...house, ...(id === undefined ? {} : { id }), ...(method === undefined ? {} : { method }), ...(args.length === 0 ? {} : { args: await argsOf(args) }) });
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
// Every other word is read from what the ground describes: an ask of the dock, as one word or two, or a faculty of its ladder.
|
|
207
|
-
const described = (await exchange(at, { describe: true })).result;
|
|
208
|
-
const asks = (described?.dock?.asks ?? []).map(({ method }) => method);
|
|
209
|
-
const [word, ...more] = rest;
|
|
210
|
-
const joined = word === undefined ? undefined : `${command}${word[0].toUpperCase()}${word.slice(1)}`;
|
|
211
|
-
const faculty = described?.faculties?.[command];
|
|
212
|
-
if (joined !== undefined && asks.includes(joined))
|
|
213
|
-
await send(at, { method: joined, ...(more.length === 0 ? {} : { args: await argsOf(more) }) });
|
|
214
|
-
else if (asks.includes(command))
|
|
215
|
-
await send(at, { method: command, ...(rest.length === 0 ? {} : { args: await argsOf(rest) }) });
|
|
216
|
-
else if (faculty !== undefined && word === undefined)
|
|
217
|
-
print({ result: faculty });
|
|
218
|
-
else if (faculty !== undefined)
|
|
219
|
-
await send(at, { method: 'callFaculty', args: { faculty: command, method: word, ...(more.length === 0 ? {} : { args: await argsOf(more) }) } });
|
|
220
|
-
else {
|
|
221
|
-
// A word the dock's asks begin with shows those asks.
|
|
222
|
-
const begun = (described?.dock?.asks ?? []).filter(({ method }) => method.startsWith(command) && /^[A-Z]/.test(method.slice(command.length)));
|
|
223
|
-
print(word === undefined && begun.length > 0 ? { result: begun } : { error: { message: `no ask and no faculty ${command}` } });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Memory, PlaceRead } from '../foundation.ts';
|
|
2
|
-
export declare class FileMemory implements Memory {
|
|
3
|
-
#private;
|
|
4
|
-
constructor(path: string | URL);
|
|
5
|
-
read({ place }: {
|
|
6
|
-
place: string;
|
|
7
|
-
}): Promise<PlaceRead>;
|
|
8
|
-
list(): Promise<readonly string[]>;
|
|
9
|
-
write(options: {
|
|
10
|
-
writes: Readonly<Record<string, Readonly<Record<string, Uint8Array | null>>>>;
|
|
11
|
-
expect: Readonly<Record<string, string | null>>;
|
|
12
|
-
}): Promise<Readonly<Record<string, string | null>> | null>;
|
|
13
|
-
}
|
package/dist/node/file-memory.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Memory in one file, rewritten whole. A write goes to a temporary file,
|
|
3
|
-
// which is synced, renamed over the file, and followed by a sync of its
|
|
4
|
-
// folder, so every place lands together. It suits a small house.
|
|
5
|
-
import { mkdir, open, readFile, rename } from 'node:fs/promises';
|
|
6
|
-
import { dirname } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { syncFolder } from './sync-folder.js';
|
|
9
|
-
const hex = (bytes) => Buffer.from(bytes).toString('hex');
|
|
10
|
-
const unhex = (text) => new Uint8Array(Buffer.from(text, 'hex'));
|
|
11
|
-
export class FileMemory {
|
|
12
|
-
#path;
|
|
13
|
-
#stored;
|
|
14
|
-
#turn = Promise.resolve();
|
|
15
|
-
constructor(path) {
|
|
16
|
-
this.#path = typeof path === 'string' ? path : fileURLToPath(path);
|
|
17
|
-
}
|
|
18
|
-
#load() {
|
|
19
|
-
this.#stored ??= readFile(this.#path, 'utf8').then((text) => JSON.parse(text), (error) => {
|
|
20
|
-
if (error.code === 'ENOENT')
|
|
21
|
-
return { places: {} };
|
|
22
|
-
throw error;
|
|
23
|
-
});
|
|
24
|
-
return this.#stored;
|
|
25
|
-
}
|
|
26
|
-
async read({ place }) {
|
|
27
|
-
const held = (await this.#load()).places[place];
|
|
28
|
-
if (held === undefined)
|
|
29
|
-
return { entries: {}, version: null };
|
|
30
|
-
return { entries: Object.fromEntries(Object.entries(held.entries).map(([name, text]) => [name, unhex(text)])), version: String(held.version) };
|
|
31
|
-
}
|
|
32
|
-
async list() {
|
|
33
|
-
return Object.keys((await this.#load()).places);
|
|
34
|
-
}
|
|
35
|
-
write(options) {
|
|
36
|
-
const turn = this.#turn.then(() => this.#write(options));
|
|
37
|
-
this.#turn = turn.catch(() => undefined);
|
|
38
|
-
return turn;
|
|
39
|
-
}
|
|
40
|
-
async #write({ writes, expect, }) {
|
|
41
|
-
const stored = await this.#load();
|
|
42
|
-
for (const place of new Set([...Object.keys(writes), ...Object.keys(expect)])) {
|
|
43
|
-
const held = stored.places[place];
|
|
44
|
-
if (!(place in expect) || expect[place] !== (held === undefined ? null : String(held.version)))
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
const next = structuredClone(stored);
|
|
48
|
-
const versions = {};
|
|
49
|
-
for (const [place, entries] of Object.entries(writes)) {
|
|
50
|
-
const held = next.places[place] ?? { version: 0, entries: {} };
|
|
51
|
-
for (const [name, bytes] of Object.entries(entries)) {
|
|
52
|
-
if (bytes === null)
|
|
53
|
-
delete held.entries[name];
|
|
54
|
-
else
|
|
55
|
-
held.entries[name] = hex(bytes);
|
|
56
|
-
}
|
|
57
|
-
held.version++;
|
|
58
|
-
if (Object.keys(held.entries).length === 0) {
|
|
59
|
-
delete next.places[place];
|
|
60
|
-
versions[place] = null;
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
next.places[place] = held;
|
|
64
|
-
versions[place] = String(held.version);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
await this.#persist(next);
|
|
68
|
-
this.#stored = Promise.resolve(next);
|
|
69
|
-
return versions;
|
|
70
|
-
}
|
|
71
|
-
async #persist(stored) {
|
|
72
|
-
const folder = dirname(this.#path);
|
|
73
|
-
await mkdir(folder, { recursive: true });
|
|
74
|
-
const temporary = `${this.#path}.${process.pid}.tmp`;
|
|
75
|
-
const file = await open(temporary, 'w', 0o600);
|
|
76
|
-
try {
|
|
77
|
-
await file.writeFile(JSON.stringify(stored));
|
|
78
|
-
await file.sync();
|
|
79
|
-
}
|
|
80
|
-
finally {
|
|
81
|
-
await file.close();
|
|
82
|
-
}
|
|
83
|
-
await rename(temporary, this.#path);
|
|
84
|
-
await syncFolder(folder);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
10
|
-
// Classes from a folder the ground names. The folder's entry, `index.js`,
|
|
11
|
-
// `index.mjs` or `index.ts`, exports `steward`, and `public` and `beings`
|
|
12
|
-
// where it has them, as `ClassList` takes them. How code reaches the
|
|
13
|
-
// folder is not the library's: a copy, a checkout, a deploy.
|
|
14
|
-
import { access } from 'node:fs/promises';
|
|
15
|
-
import { join, resolve } from 'node:path';
|
|
16
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
|
-
import { ClassList } from '../bodies/class-list.js';
|
|
18
|
-
const ENTRIES = ['index.js', 'index.mjs', 'index.ts'];
|
|
19
|
-
export const FolderClasses = Object.freeze({
|
|
20
|
-
/** The folder's classes, loaded once, or a refusal that says what the folder lacks. */
|
|
21
|
-
async open(dir) {
|
|
22
|
-
const folder = resolve(typeof dir === 'string' ? dir : fileURLToPath(dir));
|
|
23
|
-
let entry;
|
|
24
|
-
for (const name of ENTRIES) {
|
|
25
|
-
const at = join(folder, name);
|
|
26
|
-
if (await access(at).then(() => true, () => false)) {
|
|
27
|
-
entry = at;
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
if (entry === undefined)
|
|
32
|
-
throw new Error(`the folder ${folder} has no ${ENTRIES.join(', ')}`);
|
|
33
|
-
const module = (await import(__rewriteRelativeImportExtension(pathToFileURL(entry).href)));
|
|
34
|
-
if (module.steward === undefined)
|
|
35
|
-
throw new Error(`${entry} exports no steward`);
|
|
36
|
-
if (module.beings !== undefined && !Array.isArray(module.beings))
|
|
37
|
-
throw new Error(`${entry} exports beings that are not a list`);
|
|
38
|
-
return new ClassList({ steward: module.steward, ...(module.public === undefined ? {} : { public: module.public }), ...(module.beings === undefined ? {} : { beings: module.beings }) });
|
|
39
|
-
},
|
|
40
|
-
});
|
package/dist/node/gone.d.ts
DELETED
package/dist/node/gone.js
DELETED