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,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
- };
@@ -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
- };
@@ -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';
@@ -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
- }
@@ -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
- }