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.
Files changed (157) hide show
  1. package/CONTRIBUTING.md +13 -0
  2. package/LICENSE +55 -202
  3. package/NOTICE +3 -8
  4. package/README.md +14 -156
  5. package/RELEASING.md +15 -0
  6. package/SECURITY.md +9 -0
  7. package/bin/nervur.js +6 -0
  8. package/package.json +26 -81
  9. package/AUTHORING.md +0 -935
  10. package/COMMAND.md +0 -179
  11. package/FACES.md +0 -280
  12. package/FACULTIES.md +0 -578
  13. package/GROUNDS.md +0 -315
  14. package/KIT-SPEC.md +0 -304
  15. package/WORLD.md +0 -195
  16. package/dist/app/app-ground.d.ts +0 -18
  17. package/dist/app/app-ground.js +0 -23
  18. package/dist/app/index.d.ts +0 -2
  19. package/dist/app/index.js +0 -5
  20. package/dist/app/native.d.ts +0 -11
  21. package/dist/app/native.js +0 -13
  22. package/dist/being/being.d.ts +0 -306
  23. package/dist/being/being.js +0 -20
  24. package/dist/being/covers.d.ts +0 -9
  25. package/dist/being/covers.js +0 -21
  26. package/dist/being/index.d.ts +0 -4
  27. package/dist/being/index.js +0 -6
  28. package/dist/being/need.d.ts +0 -49
  29. package/dist/being/need.js +0 -33
  30. package/dist/being/schema.d.ts +0 -100
  31. package/dist/being/schema.js +0 -132
  32. package/dist/being/table.d.ts +0 -48
  33. package/dist/being/table.js +0 -344
  34. package/dist/bench/bench-ground.d.ts +0 -73
  35. package/dist/bench/bench-ground.js +0 -153
  36. package/dist/bench/bench.d.ts +0 -101
  37. package/dist/bench/bench.js +0 -368
  38. package/dist/bench/fake-clock.d.ts +0 -17
  39. package/dist/bench/fake-clock.js +0 -34
  40. package/dist/bench/fake-memory.d.ts +0 -16
  41. package/dist/bench/fake-memory.js +0 -46
  42. package/dist/bench/fake-network.d.ts +0 -66
  43. package/dist/bench/fake-network.js +0 -222
  44. package/dist/bench/fake-unlock.d.ts +0 -6
  45. package/dist/bench/fake-unlock.js +0 -10
  46. package/dist/bench/index.d.ts +0 -3
  47. package/dist/bench/index.js +0 -5
  48. package/dist/bench/seeded.d.ts +0 -11
  49. package/dist/bench/seeded.js +0 -37
  50. package/dist/bench/settle.d.ts +0 -2
  51. package/dist/bench/settle.js +0 -49
  52. package/dist/bench/stewards.d.ts +0 -169
  53. package/dist/bench/stewards.js +0 -128
  54. package/dist/bodies/class-list.d.ts +0 -14
  55. package/dist/bodies/class-list.js +0 -33
  56. package/dist/bodies/joined-carry.d.ts +0 -29
  57. package/dist/bodies/joined-carry.js +0 -48
  58. package/dist/bodies/kept.d.ts +0 -44
  59. package/dist/bodies/kept.js +0 -91
  60. package/dist/bodies/noble-crypto.d.ts +0 -22
  61. package/dist/bodies/noble-crypto.js +0 -88
  62. package/dist/bodies/seed-keys.d.ts +0 -10
  63. package/dist/bodies/seed-keys.js +0 -42
  64. package/dist/bodies/strict-tools.d.ts +0 -10
  65. package/dist/bodies/strict-tools.js +0 -306
  66. package/dist/bodies/vouch.d.ts +0 -20
  67. package/dist/bodies/vouch.js +0 -71
  68. package/dist/bodies/web-carry.d.ts +0 -72
  69. package/dist/bodies/web-carry.js +0 -245
  70. package/dist/bodies/web-clock.d.ts +0 -12
  71. package/dist/bodies/web-clock.js +0 -33
  72. package/dist/browser/browser-ground.d.ts +0 -76
  73. package/dist/browser/browser-ground.js +0 -294
  74. package/dist/browser/index.d.ts +0 -4
  75. package/dist/browser/index.js +0 -7
  76. package/dist/browser/indexeddb-memory.d.ts +0 -17
  77. package/dist/browser/indexeddb-memory.js +0 -97
  78. package/dist/browser/locked-unlock.d.ts +0 -24
  79. package/dist/browser/locked-unlock.js +0 -85
  80. package/dist/browser/origin-classes.d.ts +0 -7
  81. package/dist/browser/origin-classes.js +0 -31
  82. package/dist/edge/durable-clock.d.ts +0 -19
  83. package/dist/edge/durable-clock.js +0 -61
  84. package/dist/edge/durable-memory.d.ts +0 -5
  85. package/dist/edge/durable-memory.js +0 -11
  86. package/dist/edge/durable.d.ts +0 -18
  87. package/dist/edge/durable.js +0 -24
  88. package/dist/edge/edge-ground.d.ts +0 -49
  89. package/dist/edge/edge-ground.js +0 -223
  90. package/dist/edge/index.d.ts +0 -7
  91. package/dist/edge/index.js +0 -9
  92. package/dist/edge/native-crypto.d.ts +0 -8
  93. package/dist/edge/native-crypto.js +0 -102
  94. package/dist/edge/secret-unlock.d.ts +0 -7
  95. package/dist/edge/secret-unlock.js +0 -13
  96. package/dist/edge/socket-carry.d.ts +0 -39
  97. package/dist/edge/socket-carry.js +0 -111
  98. package/dist/foundation.d.ts +0 -162
  99. package/dist/foundation.js +0 -4
  100. package/dist/ground/dock.d.ts +0 -1460
  101. package/dist/ground/dock.js +0 -1159
  102. package/dist/ground/ground.d.ts +0 -239
  103. package/dist/ground/ground.js +0 -787
  104. package/dist/ground/views.d.ts +0 -76
  105. package/dist/ground/views.js +0 -208
  106. package/dist/house/crossing.d.ts +0 -50
  107. package/dist/house/crossing.js +0 -153
  108. package/dist/house/house.d.ts +0 -88
  109. package/dist/house/house.js +0 -2069
  110. package/dist/house/rows-shape.d.ts +0 -110
  111. package/dist/house/rows-shape.js +0 -12
  112. package/dist/house/rows.d.ts +0 -27
  113. package/dist/house/rows.js +0 -185
  114. package/dist/index.d.ts +0 -33
  115. package/dist/index.js +0 -29
  116. package/dist/node/bridge.d.ts +0 -20
  117. package/dist/node/bridge.js +0 -284
  118. package/dist/node/cli.d.ts +0 -2
  119. package/dist/node/cli.js +0 -225
  120. package/dist/node/file-memory.d.ts +0 -13
  121. package/dist/node/file-memory.js +0 -86
  122. package/dist/node/folder-classes.d.ts +0 -5
  123. package/dist/node/folder-classes.js +0 -40
  124. package/dist/node/gone.d.ts +0 -2
  125. package/dist/node/gone.js +0 -7
  126. package/dist/node/hand.d.ts +0 -39
  127. package/dist/node/hand.js +0 -107
  128. package/dist/node/http.d.ts +0 -11
  129. package/dist/node/http.js +0 -90
  130. package/dist/node/index.d.ts +0 -11
  131. package/dist/node/index.js +0 -13
  132. package/dist/node/ledger-memory.d.ts +0 -17
  133. package/dist/node/ledger-memory.js +0 -152
  134. package/dist/node/lock.d.ts +0 -10
  135. package/dist/node/lock.js +0 -78
  136. package/dist/node/node-ground.d.ts +0 -26
  137. package/dist/node/node-ground.js +0 -228
  138. package/dist/node/notify.d.ts +0 -1
  139. package/dist/node/notify.js +0 -18
  140. package/dist/node/shell.d.ts +0 -11
  141. package/dist/node/shell.js +0 -35
  142. package/dist/node/sync-folder.d.ts +0 -6
  143. package/dist/node/sync-folder.js +0 -18
  144. package/dist/node/tcp-carry.d.ts +0 -42
  145. package/dist/node/tcp-carry.js +0 -193
  146. package/dist/node/unlock.d.ts +0 -17
  147. package/dist/node/unlock.js +0 -86
  148. package/dist/node/websocket.d.ts +0 -12
  149. package/dist/node/websocket.js +0 -134
  150. package/dist/quo/frame.d.ts +0 -33
  151. package/dist/quo/frame.js +0 -92
  152. package/dist/quo/index.d.ts +0 -1
  153. package/dist/quo/index.js +0 -5
  154. package/dist/quo/room.d.ts +0 -154
  155. package/dist/quo/room.js +0 -362
  156. package/dist/serve/index.d.ts +0 -45
  157. package/dist/serve/index.js +0 -71
@@ -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
- };
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
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
- }
@@ -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,5 +0,0 @@
1
- import type { Classes } from '../foundation.ts';
2
- export declare const FolderClasses: Readonly<{
3
- /** The folder's classes, loaded once, or a refusal that says what the folder lacks. */
4
- open(dir: string | URL): Promise<Classes>;
5
- }>;
@@ -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
- });
@@ -1,2 +0,0 @@
1
- /** Removes a file, and is content where it is already gone. */
2
- export declare const gone: (path: string) => Promise<void>;
package/dist/node/gone.js DELETED
@@ -1,7 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- import { unlink } from 'node:fs/promises';
3
- /** Removes a file, and is content where it is already gone. */
4
- export const gone = (path) => unlink(path).catch((error) => {
5
- if (error.code !== 'ENOENT')
6
- throw error;
7
- });