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/hand.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { Json } from '../being/being.ts';
|
|
2
|
-
import type { Answer } from '../house/rows-shape.ts';
|
|
3
|
-
export interface Hand {
|
|
4
|
-
/** Stops listening, and removes the socket. */
|
|
5
|
-
close(): Promise<void>;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* One line to the hand. On a house's own hand, an ask of a being. On a
|
|
9
|
-
* ground's, an ask of a being in the house it names, of the dock's steward
|
|
10
|
-
* where it names none, or `describe` for all it holds.
|
|
11
|
-
*/
|
|
12
|
-
export interface HandRequest {
|
|
13
|
-
readonly house?: string;
|
|
14
|
-
readonly describe?: true;
|
|
15
|
-
readonly id?: string;
|
|
16
|
-
readonly method?: string;
|
|
17
|
-
readonly args?: {
|
|
18
|
-
readonly [key: string]: Json;
|
|
19
|
-
};
|
|
20
|
-
/** The answer the owner holds, which makes a `readOnly` ask a watch. */
|
|
21
|
-
readonly after?: Answer;
|
|
22
|
-
/** Her cells read, and nothing asked. */
|
|
23
|
-
readonly cells?: true;
|
|
24
|
-
/** The ask's call id: asked again, it answers what the first answered. */
|
|
25
|
-
readonly call?: string;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Where a ground's hand stands for its state folder: a socket inside it,
|
|
29
|
-
* or on Windows a named pipe, which has no place in a folder, named for
|
|
30
|
-
* the folder's path.
|
|
31
|
-
*/
|
|
32
|
-
export declare const handAt: (state: string, platform?: NodeJS.Platform) => string;
|
|
33
|
-
/**
|
|
34
|
-
* Serves `ask` at `path`: a house's own hand, or a ground's, which also
|
|
35
|
-
* reads `house` and changes its record. A socket left there by a process
|
|
36
|
-
* that is gone is replaced. A named pipe on Windows is open to its user
|
|
37
|
-
* alone, as the operating system makes it, and leaves nothing behind.
|
|
38
|
-
*/
|
|
39
|
-
export declare const serveHand: (ask: (request: HandRequest) => Promise<unknown>, path: string) => Promise<Hand>;
|
package/dist/node/hand.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The hand on a local socket, or on Windows a named pipe: one JSON object
|
|
3
|
-
// a line in, one answer a line out, in order. It is open to the ground's
|
|
4
|
-
// user alone.
|
|
5
|
-
import { createHash } from 'node:crypto';
|
|
6
|
-
import { chmod } from 'node:fs/promises';
|
|
7
|
-
import { createServer } from 'node:net';
|
|
8
|
-
import { join, resolve as absolute } from 'node:path';
|
|
9
|
-
import { createInterface } from 'node:readline';
|
|
10
|
-
import { gone } from './gone.js';
|
|
11
|
-
const object = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
-
const request = (line) => {
|
|
13
|
-
let value;
|
|
14
|
-
try {
|
|
15
|
-
value = JSON.parse(line);
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
if (!object(value))
|
|
21
|
-
return null;
|
|
22
|
-
const { house, describe, id, method, args, after, cells, call, ...rest } = value;
|
|
23
|
-
if (Object.keys(rest).length > 0)
|
|
24
|
-
return null;
|
|
25
|
-
for (const text of [house, id, method, call])
|
|
26
|
-
if (text !== undefined && typeof text !== 'string')
|
|
27
|
-
return null;
|
|
28
|
-
if (args !== undefined && !object(args))
|
|
29
|
-
return null;
|
|
30
|
-
if (describe !== undefined && describe !== true)
|
|
31
|
-
return null;
|
|
32
|
-
if (cells !== undefined && cells !== true)
|
|
33
|
-
return null;
|
|
34
|
-
// An answer is `{ result }` or `{ error }`, and nothing else.
|
|
35
|
-
if (after !== undefined && !(object(after) && Object.keys(after).length === 1 && ('result' in after || 'error' in after)))
|
|
36
|
-
return null;
|
|
37
|
-
return Object.fromEntries(Object.entries({ house, describe, id, method, args, after, cells, call }).filter(([, field]) => field !== undefined));
|
|
38
|
-
};
|
|
39
|
-
const serve = (ask, socket) => {
|
|
40
|
-
let turn = Promise.resolve();
|
|
41
|
-
createInterface({ input: socket }).on('line', (line) => {
|
|
42
|
-
turn = turn.then(async () => {
|
|
43
|
-
const asked = request(line);
|
|
44
|
-
const answer = asked === null ? { error: { message: 'bad request' } } : await ask(asked);
|
|
45
|
-
if (!socket.destroyed)
|
|
46
|
-
socket.write(`${JSON.stringify(answer)}\n`);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
socket.on('error', () => undefined);
|
|
50
|
-
};
|
|
51
|
-
// A local socket's path fits the kernel's address: 103 bytes on macOS and
|
|
52
|
-
// the BSDs, 107 on Linux, with the closing zero beyond.
|
|
53
|
-
const LONGEST = process.platform === 'linux' ? 107 : 103;
|
|
54
|
-
const PIPE = /^\\\\[.?]\\pipe\\/;
|
|
55
|
-
/**
|
|
56
|
-
* Where a ground's hand stands for its state folder: a socket inside it,
|
|
57
|
-
* or on Windows a named pipe, which has no place in a folder, named for
|
|
58
|
-
* the folder's path.
|
|
59
|
-
*/
|
|
60
|
-
export const handAt = (state, platform = process.platform) => platform === 'win32' ? `\\\\.\\pipe\\nervur-${createHash('sha256').update(absolute(state).toLowerCase()).digest('hex').slice(0, 16)}` : join(state, 'hand');
|
|
61
|
-
/**
|
|
62
|
-
* Serves `ask` at `path`: a house's own hand, or a ground's, which also
|
|
63
|
-
* reads `house` and changes its record. A socket left there by a process
|
|
64
|
-
* that is gone is replaced. A named pipe on Windows is open to its user
|
|
65
|
-
* alone, as the operating system makes it, and leaves nothing behind.
|
|
66
|
-
*/
|
|
67
|
-
export const serveHand = async (ask, path) => {
|
|
68
|
-
if (PIPE.test(path))
|
|
69
|
-
return listen(ask, path, false);
|
|
70
|
-
const length = Buffer.byteLength(path);
|
|
71
|
-
if (length > LONGEST)
|
|
72
|
-
throw new Error(`a socket's path is at most ${LONGEST} bytes here, and ${path} is ${length}: serve the hand from a shorter folder`);
|
|
73
|
-
await gone(path);
|
|
74
|
-
return listen(ask, path, true);
|
|
75
|
-
};
|
|
76
|
-
const listen = async (ask, path, file) => {
|
|
77
|
-
const server = createServer((socket) => serve(ask, socket));
|
|
78
|
-
const sockets = new Set();
|
|
79
|
-
server.on('connection', (socket) => {
|
|
80
|
-
sockets.add(socket);
|
|
81
|
-
socket.on('close', () => sockets.delete(socket));
|
|
82
|
-
});
|
|
83
|
-
await new Promise((resolve, reject) => {
|
|
84
|
-
server.once('error', reject);
|
|
85
|
-
if (!file)
|
|
86
|
-
return void server.listen(path, () => resolve());
|
|
87
|
-
// Closed to everyone from the first moment, then opened to the user.
|
|
88
|
-
const mask = process.umask(0o177);
|
|
89
|
-
try {
|
|
90
|
-
server.listen(path, () => resolve());
|
|
91
|
-
}
|
|
92
|
-
finally {
|
|
93
|
-
process.umask(mask);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
if (file)
|
|
97
|
-
await chmod(path, 0o600);
|
|
98
|
-
return {
|
|
99
|
-
close: async () => {
|
|
100
|
-
for (const socket of sockets)
|
|
101
|
-
socket.destroy();
|
|
102
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
103
|
-
if (file)
|
|
104
|
-
await gone(path);
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
};
|
package/dist/node/http.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { Handler } from '../bodies/web-carry.ts';
|
|
2
|
-
export interface Served {
|
|
3
|
-
/** The port it listens on. */
|
|
4
|
-
readonly port: number;
|
|
5
|
-
close(): Promise<void>;
|
|
6
|
-
}
|
|
7
|
-
/** Listens on `port`, `0` for one the system picks, and hands every request to `handlers` in order. */
|
|
8
|
-
export declare const serveHttp: ({ port, host }: {
|
|
9
|
-
port?: number;
|
|
10
|
-
host?: string;
|
|
11
|
-
}, handlers: readonly Handler[]) => Promise<Served>;
|
package/dist/node/http.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// One HTTP listener for a ground, chaining its handlers: each request goes
|
|
3
|
-
// to the first handler that answers it, and each WebSocket to the first
|
|
4
|
-
// that takes it. `WebCarry` is one handler, and every face is another, so
|
|
5
|
-
// a ground holds one port whatever speaks HTTP on it.
|
|
6
|
-
import { createServer } from 'node:http';
|
|
7
|
-
import { acceptOf, hold } from './websocket.js';
|
|
8
|
-
/** The longest request body a handler is handed, and the longest WebSocket message. */
|
|
9
|
-
const LONGEST = 8 * 1024 * 1024;
|
|
10
|
-
const requestOf = (message, body) => {
|
|
11
|
-
const headers = new Headers();
|
|
12
|
-
for (let index = 0; index + 1 < message.rawHeaders.length; index += 2)
|
|
13
|
-
headers.append(message.rawHeaders[index], message.rawHeaders[index + 1]);
|
|
14
|
-
const url = `http://${message.headers.host ?? 'localhost'}${message.url ?? '/'}`;
|
|
15
|
-
return new Request(url, { method: message.method ?? 'GET', headers, ...(body === null ? {} : { body: new Uint8Array(body) }) });
|
|
16
|
-
};
|
|
17
|
-
const read = (message) => new Promise((resolve) => {
|
|
18
|
-
const chunks = [];
|
|
19
|
-
let length = 0;
|
|
20
|
-
message.on('data', (chunk) => {
|
|
21
|
-
length += chunk.length;
|
|
22
|
-
if (length > LONGEST) {
|
|
23
|
-
resolve(null);
|
|
24
|
-
message.destroy();
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
chunks.push(chunk);
|
|
28
|
-
});
|
|
29
|
-
message.on('end', () => resolve(Buffer.concat(chunks)));
|
|
30
|
-
message.on('error', () => resolve(null));
|
|
31
|
-
});
|
|
32
|
-
/** Listens on `port`, `0` for one the system picks, and hands every request to `handlers` in order. */
|
|
33
|
-
export const serveHttp = async ({ port = 0, host = '0.0.0.0' }, handlers) => {
|
|
34
|
-
const sockets = new Set();
|
|
35
|
-
const server = createServer(async (message, response) => {
|
|
36
|
-
try {
|
|
37
|
-
const bodied = message.method !== 'GET' && message.method !== 'HEAD';
|
|
38
|
-
const body = bodied ? await read(message) : null;
|
|
39
|
-
if (bodied && body === null) {
|
|
40
|
-
response.writeHead(413).end();
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const request = requestOf(message, body);
|
|
44
|
-
for (const handler of handlers) {
|
|
45
|
-
const answered = await handler.fetch(request);
|
|
46
|
-
if (answered === null)
|
|
47
|
-
continue;
|
|
48
|
-
response.writeHead(answered.status, Object.fromEntries(answered.headers));
|
|
49
|
-
response.end(Buffer.from(await answered.arrayBuffer()));
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
response.writeHead(404).end();
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
if (!response.headersSent)
|
|
56
|
-
response.writeHead(500);
|
|
57
|
-
response.end();
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
server.on('connection', (socket) => {
|
|
61
|
-
sockets.add(socket);
|
|
62
|
-
socket.on('close', () => sockets.delete(socket));
|
|
63
|
-
});
|
|
64
|
-
server.on('upgrade', (message, socket, head) => {
|
|
65
|
-
const key = message.headers['sec-websocket-key'];
|
|
66
|
-
const request = requestOf(message, null);
|
|
67
|
-
const taken = typeof key === 'string' && message.headers['sec-websocket-version'] === '13' ? handlers.map((handler) => handler.upgrade?.(request) ?? null).find((one) => one !== null) : undefined;
|
|
68
|
-
if (taken === undefined || typeof key !== 'string') {
|
|
69
|
-
socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
socket.write(['HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${acceptOf(key)}`, `Sec-WebSocket-Protocol: ${taken.protocol}`, '', ''].join('\r\n'));
|
|
73
|
-
hold(socket, taken.open, LONGEST);
|
|
74
|
-
if (head.length > 0)
|
|
75
|
-
socket.unshift(head);
|
|
76
|
-
});
|
|
77
|
-
await new Promise((resolve, reject) => {
|
|
78
|
-
server.once('error', reject);
|
|
79
|
-
server.listen(port, host, () => resolve());
|
|
80
|
-
});
|
|
81
|
-
const address = server.address();
|
|
82
|
-
return {
|
|
83
|
-
port: typeof address === 'object' && address !== null ? address.port : port,
|
|
84
|
-
close: async () => {
|
|
85
|
-
for (const socket of sockets)
|
|
86
|
-
socket.destroy();
|
|
87
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
};
|
package/dist/node/index.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export { NodeGround } from './node-ground.ts';
|
|
2
|
-
export { bridge } from './bridge.ts';
|
|
3
|
-
export { FileMemory } from './file-memory.ts';
|
|
4
|
-
export { FolderClasses } from './folder-classes.ts';
|
|
5
|
-
export { serveHand } from './hand.ts';
|
|
6
|
-
export { serveHttp, type Served } from './http.ts';
|
|
7
|
-
export { LedgerMemory } from './ledger-memory.ts';
|
|
8
|
-
export { takeLock } from './lock.ts';
|
|
9
|
-
export { notifyReady } from './notify.ts';
|
|
10
|
-
export { TcpCarry } from './tcp-carry.ts';
|
|
11
|
-
export { FileUnlock, KeychainUnlock } from './unlock.ts';
|
package/dist/node/index.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// `nervur/node`: the bodies a ground on Node passes, and what it runs beside them.
|
|
3
|
-
export { NodeGround } from './node-ground.js';
|
|
4
|
-
export { bridge } from './bridge.js';
|
|
5
|
-
export { FileMemory } from './file-memory.js';
|
|
6
|
-
export { FolderClasses } from './folder-classes.js';
|
|
7
|
-
export { serveHand } from './hand.js';
|
|
8
|
-
export { serveHttp } from './http.js';
|
|
9
|
-
export { LedgerMemory } from './ledger-memory.js';
|
|
10
|
-
export { takeLock } from './lock.js';
|
|
11
|
-
export { notifyReady } from './notify.js';
|
|
12
|
-
export { TcpCarry } from './tcp-carry.js';
|
|
13
|
-
export { FileUnlock, KeychainUnlock } from './unlock.js';
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Memory, PlaceRead } from '../foundation.ts';
|
|
2
|
-
type Writes = Readonly<Record<string, Readonly<Record<string, Uint8Array | null>>>>;
|
|
3
|
-
export declare class LedgerMemory implements Memory {
|
|
4
|
-
#private;
|
|
5
|
-
constructor(at: string | URL, { witness }?: {
|
|
6
|
-
witness?: string | URL;
|
|
7
|
-
});
|
|
8
|
-
read({ place }: {
|
|
9
|
-
place: string;
|
|
10
|
-
}): Promise<PlaceRead>;
|
|
11
|
-
list(): Promise<readonly string[]>;
|
|
12
|
-
write(options: {
|
|
13
|
-
writes: Writes;
|
|
14
|
-
expect: Readonly<Record<string, string | null>>;
|
|
15
|
-
}): Promise<Readonly<Record<string, string | null>> | null>;
|
|
16
|
-
}
|
|
17
|
-
export {};
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Memory as a ledger: every write appended as one line, never rewritten.
|
|
3
|
-
// Each line carries its sequence and the hash of the line before it, and
|
|
4
|
-
// is synced before its write counts. So the file is also the whole record
|
|
5
|
-
// of what the house wrote. A witness, where the ground names one, keeps
|
|
6
|
-
// the last sequence and hash in a second place, and a ledger behind it is
|
|
7
|
-
// refused: a restored copy cannot replay what the house already answered.
|
|
8
|
-
import { createHash } from 'node:crypto';
|
|
9
|
-
import { mkdir, open, readFile, rename, truncate } from 'node:fs/promises';
|
|
10
|
-
import { dirname } from 'node:path';
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { syncFolder } from './sync-folder.js';
|
|
13
|
-
const GENESIS = { n: 0, hash: '0'.repeat(64) };
|
|
14
|
-
const hash = (text) => createHash('sha256').update(text).digest('hex');
|
|
15
|
-
const unhex = (text) => new Uint8Array(Buffer.from(text, 'hex'));
|
|
16
|
-
const path = (at) => (typeof at === 'string' ? at : fileURLToPath(at));
|
|
17
|
-
/** Opens a file, runs `work` on it, syncs it and lets it go. */
|
|
18
|
-
const synced = async (file, flags, work) => {
|
|
19
|
-
const handle = await open(file, flags, 0o600);
|
|
20
|
-
try {
|
|
21
|
-
await work(handle);
|
|
22
|
-
await handle.sync();
|
|
23
|
-
}
|
|
24
|
-
finally {
|
|
25
|
-
await handle.close();
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
/** A line laid on the places: each place it names takes the line's sequence as its version. */
|
|
29
|
-
const apply = (places, line) => {
|
|
30
|
-
const version = String(line.n);
|
|
31
|
-
const versions = {};
|
|
32
|
-
for (const [place, entries] of Object.entries(line.writes)) {
|
|
33
|
-
const held = places.get(place) ?? { version, entries: new Map() };
|
|
34
|
-
for (const [name, value] of Object.entries(entries)) {
|
|
35
|
-
if (value === null)
|
|
36
|
-
held.entries.delete(name);
|
|
37
|
-
else
|
|
38
|
-
held.entries.set(name, value);
|
|
39
|
-
}
|
|
40
|
-
held.version = version;
|
|
41
|
-
if (held.entries.size === 0)
|
|
42
|
-
places.delete(place);
|
|
43
|
-
else
|
|
44
|
-
places.set(place, held);
|
|
45
|
-
versions[place] = held.entries.size === 0 ? null : version;
|
|
46
|
-
}
|
|
47
|
-
return versions;
|
|
48
|
-
};
|
|
49
|
-
export class LedgerMemory {
|
|
50
|
-
#path;
|
|
51
|
-
#witness;
|
|
52
|
-
#state;
|
|
53
|
-
#turn = Promise.resolve();
|
|
54
|
-
constructor(at, { witness } = {}) {
|
|
55
|
-
this.#path = path(at);
|
|
56
|
-
this.#witness = witness === undefined ? undefined : path(witness);
|
|
57
|
-
}
|
|
58
|
-
#open() {
|
|
59
|
-
this.#state ??= this.#load();
|
|
60
|
-
this.#state.catch(() => (this.#state = undefined));
|
|
61
|
-
return this.#state;
|
|
62
|
-
}
|
|
63
|
-
async #load() {
|
|
64
|
-
await mkdir(dirname(this.#path), { recursive: true });
|
|
65
|
-
let text = '';
|
|
66
|
-
try {
|
|
67
|
-
text = await readFile(this.#path, 'utf8');
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
if (error.code !== 'ENOENT')
|
|
71
|
-
throw error;
|
|
72
|
-
}
|
|
73
|
-
// A last line with no newline was never synced whole, so its write never
|
|
74
|
-
// counted: it is cut away.
|
|
75
|
-
const whole = text.lastIndexOf('\n') + 1;
|
|
76
|
-
if (whole < text.length)
|
|
77
|
-
await truncate(this.#path, Buffer.byteLength(text.slice(0, whole)));
|
|
78
|
-
const places = new Map();
|
|
79
|
-
let head = GENESIS;
|
|
80
|
-
for (const raw of text.slice(0, whole).split('\n').slice(0, -1)) {
|
|
81
|
-
const line = JSON.parse(raw);
|
|
82
|
-
if (line.n !== head.n + 1 || line.prev !== head.hash)
|
|
83
|
-
throw new Error(`the ledger ${this.#path} is broken at line ${head.n + 1}`);
|
|
84
|
-
apply(places, line);
|
|
85
|
-
head = { n: line.n, hash: hash(raw) };
|
|
86
|
-
}
|
|
87
|
-
const witnessed = this.#witness === undefined ? null : await this.#witnessed();
|
|
88
|
-
if (witnessed !== null && (head.n < witnessed.n || (head.n === witnessed.n && head.hash !== witnessed.hash))) {
|
|
89
|
-
throw new Error(`the ledger ${this.#path} is behind its witness: it holds ${head.n} lines, and the witness saw ${witnessed.n}`);
|
|
90
|
-
}
|
|
91
|
-
return { places, head };
|
|
92
|
-
}
|
|
93
|
-
async #witnessed() {
|
|
94
|
-
try {
|
|
95
|
-
return JSON.parse(await readFile(this.#witness, 'utf8'));
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
if (error.code === 'ENOENT')
|
|
99
|
-
return null;
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
async #attest(head) {
|
|
104
|
-
const temporary = `${this.#witness}.${process.pid}.tmp`;
|
|
105
|
-
await synced(temporary, 'w', (handle) => handle.writeFile(JSON.stringify(head)));
|
|
106
|
-
await rename(temporary, this.#witness);
|
|
107
|
-
await syncFolder(dirname(this.#witness));
|
|
108
|
-
}
|
|
109
|
-
async read({ place }) {
|
|
110
|
-
const held = (await this.#open()).places.get(place);
|
|
111
|
-
if (held === undefined)
|
|
112
|
-
return { entries: {}, version: null };
|
|
113
|
-
return { entries: Object.fromEntries([...held.entries].map(([name, value]) => [name, unhex(value)])), version: held.version };
|
|
114
|
-
}
|
|
115
|
-
async list() {
|
|
116
|
-
return [...(await this.#open()).places.keys()];
|
|
117
|
-
}
|
|
118
|
-
write(options) {
|
|
119
|
-
const turn = this.#turn.then(() => this.#write(options));
|
|
120
|
-
this.#turn = turn.catch(() => undefined);
|
|
121
|
-
return turn;
|
|
122
|
-
}
|
|
123
|
-
async #write({ writes, expect }) {
|
|
124
|
-
const state = await this.#open();
|
|
125
|
-
for (const place of new Set([...Object.keys(writes), ...Object.keys(expect)])) {
|
|
126
|
-
if (!(place in expect) || expect[place] !== (state.places.get(place)?.version ?? null))
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
const line = {
|
|
130
|
-
n: state.head.n + 1,
|
|
131
|
-
prev: state.head.hash,
|
|
132
|
-
writes: Object.fromEntries(Object.entries(writes).map(([place, entries]) => [place, Object.fromEntries(Object.entries(entries).map(([name, bytes]) => [name, bytes === null ? null : Buffer.from(bytes).toString('hex')]))])),
|
|
133
|
-
};
|
|
134
|
-
const raw = JSON.stringify(line);
|
|
135
|
-
const head = { n: line.n, hash: hash(raw) };
|
|
136
|
-
try {
|
|
137
|
-
await synced(this.#path, 'a', (handle) => handle.appendFile(`${raw}\n`));
|
|
138
|
-
if (line.n === 1)
|
|
139
|
-
await syncFolder(dirname(this.#path));
|
|
140
|
-
if (this.#witness !== undefined)
|
|
141
|
-
await this.#attest(head);
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
// What landed is read again from the file: a torn line is cut, and a
|
|
145
|
-
// whole one stands. The write answers as failed either way.
|
|
146
|
-
this.#state = undefined;
|
|
147
|
-
throw error;
|
|
148
|
-
}
|
|
149
|
-
state.head = head;
|
|
150
|
-
return apply(state.places, line);
|
|
151
|
-
}
|
|
152
|
-
}
|
package/dist/node/lock.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export interface Lock {
|
|
2
|
-
/** Lets the lock go, where this process still holds it. */
|
|
3
|
-
release(): Promise<void>;
|
|
4
|
-
}
|
|
5
|
-
/**
|
|
6
|
-
* Takes the lock of `dir`, or refuses where a living process holds it.
|
|
7
|
-
* The file is written whole beside the lock and linked into place, so a
|
|
8
|
-
* lock is never seen half written, and two takers never both land.
|
|
9
|
-
*/
|
|
10
|
-
export declare const takeLock: (dir: string) => Promise<Lock>;
|
package/dist/node/lock.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// One instance per folder. The lock is a file named `lock` holding the
|
|
3
|
-
// process id that took it. A lock whose process is gone is taken over.
|
|
4
|
-
import { link, readFile, rename, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import { gone } from './gone.js';
|
|
7
|
-
const alive = (pid) => {
|
|
8
|
-
try {
|
|
9
|
-
process.kill(pid, 0);
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
catch (error) {
|
|
13
|
-
return error.code === 'EPERM';
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
const holder = async (path) => {
|
|
17
|
-
try {
|
|
18
|
-
const pid = Number((await readFile(path, 'utf8')).trim());
|
|
19
|
-
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
if (error.code === 'ENOENT')
|
|
23
|
-
return null;
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* Takes the lock of `dir`, or refuses where a living process holds it.
|
|
29
|
-
* The file is written whole beside the lock and linked into place, so a
|
|
30
|
-
* lock is never seen half written, and two takers never both land.
|
|
31
|
-
*/
|
|
32
|
-
export const takeLock = async (dir) => {
|
|
33
|
-
const path = join(dir, 'lock');
|
|
34
|
-
const mine = join(dir, `lock.${process.pid}`);
|
|
35
|
-
await writeFile(mine, `${process.pid}\n`, { mode: 0o600 });
|
|
36
|
-
try {
|
|
37
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
38
|
-
try {
|
|
39
|
-
await link(mine, path);
|
|
40
|
-
return {
|
|
41
|
-
release: async () => {
|
|
42
|
-
if ((await holder(path)) === process.pid)
|
|
43
|
-
await gone(path);
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
if (error.code !== 'EEXIST')
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
const pid = await holder(path);
|
|
52
|
-
if (pid !== null && alive(pid))
|
|
53
|
-
throw new Error(`process ${pid} holds the lock of ${dir}`);
|
|
54
|
-
// Set aside what stands; where a living lock was set aside instead,
|
|
55
|
-
// it goes back and this taker refuses.
|
|
56
|
-
const aside = join(dir, `lock.stale.${process.pid}`);
|
|
57
|
-
try {
|
|
58
|
-
await rename(path, aside);
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
if (error.code === 'ENOENT')
|
|
62
|
-
continue;
|
|
63
|
-
throw error;
|
|
64
|
-
}
|
|
65
|
-
const took = await holder(aside);
|
|
66
|
-
if (took !== pid && took !== null && alive(took)) {
|
|
67
|
-
await link(aside, path).catch(() => undefined);
|
|
68
|
-
await gone(aside);
|
|
69
|
-
throw new Error(`process ${took} holds the lock of ${dir}`);
|
|
70
|
-
}
|
|
71
|
-
await gone(aside);
|
|
72
|
-
}
|
|
73
|
-
throw new Error(`the lock of ${dir} is contended`);
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
await gone(mine);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { Ground, type Unlock } from '../ground/ground.ts';
|
|
2
|
-
export interface NodeGroundOptions {
|
|
3
|
-
/** The folder of its code: a folder for each house, and each module or program its entries name. */
|
|
4
|
-
readonly folder: string;
|
|
5
|
-
/** The folder of its state; `NERVUR_STATE`, or `<folder>/state`, where none is named. */
|
|
6
|
-
readonly state?: string;
|
|
7
|
-
/**
|
|
8
|
-
* What opens its drawer and its hand, and nothing else: `NERVUR_STATE`,
|
|
9
|
-
* `NERVUR_UNLOCK` and `NERVUR_HAND`. The process's own where none are
|
|
10
|
-
* named. Every other setting is an entry the dock holds.
|
|
11
|
-
*/
|
|
12
|
-
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
13
|
-
/** Its unlock, where the host opens the ground from its own code; `NERVUR_UNLOCK`'s, or the key file in its state, where omitted. */
|
|
14
|
-
readonly unlock?: Unlock;
|
|
15
|
-
}
|
|
16
|
-
export declare class NodeGround {
|
|
17
|
-
readonly ground: Ground;
|
|
18
|
-
/** The socket of the ground's hand. */
|
|
19
|
-
readonly hand: string;
|
|
20
|
-
private constructor();
|
|
21
|
-
/** The port HTTP listens on, where its web carry listens: a cell of the web's twin, read through the hand. */
|
|
22
|
-
port(): Promise<number | undefined>;
|
|
23
|
-
static open({ folder: given, state: stateGiven, env, unlock }: NodeGroundOptions): Promise<NodeGround>;
|
|
24
|
-
/** A stop is the boot reversed: the hand, the houses and every body, then the ledger and its lock. */
|
|
25
|
-
close(): Promise<void>;
|
|
26
|
-
}
|