nervur 0.22.2-4 → 0.22.2-5
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/dist/house/rows.js +32 -15
- package/dist/node/cli.js +2 -1
- package/dist/node/file-memory.js +2 -7
- package/dist/node/hand.d.ts +8 -1
- package/dist/node/hand.js +25 -5
- package/dist/node/ledger-memory.js +3 -2
- package/dist/node/node-ground.js +2 -2
- package/dist/node/sync-folder.d.ts +6 -0
- package/dist/node/sync-folder.js +18 -0
- package/package.json +1 -1
package/dist/house/rows.js
CHANGED
|
@@ -12,6 +12,8 @@ export class Rows {
|
|
|
12
12
|
#placeNames = new Map();
|
|
13
13
|
/** The last write holding each row, which the next one to hold it waits for. */
|
|
14
14
|
#holds = new Map();
|
|
15
|
+
// How many times each row's cache has moved, so a read overtaken by a write never lands behind it.
|
|
16
|
+
#moves = new Map();
|
|
15
17
|
#landed;
|
|
16
18
|
constructor(memory, crypto, tools, names, key) {
|
|
17
19
|
this.#memory = memory;
|
|
@@ -33,22 +35,36 @@ export class Rows {
|
|
|
33
35
|
}
|
|
34
36
|
return place;
|
|
35
37
|
}
|
|
38
|
+
// A row's cache moved, by a write that landed or one refused: a read in flight across it is behind.
|
|
39
|
+
#moved(place) {
|
|
40
|
+
this.#moves.set(place, (this.#moves.get(place) ?? 0) + 1);
|
|
41
|
+
}
|
|
42
|
+
#forget(place) {
|
|
43
|
+
this.#moved(place);
|
|
44
|
+
this.#held.delete(place);
|
|
45
|
+
}
|
|
36
46
|
async #read(place) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
for (;;) {
|
|
48
|
+
const cached = this.#held.get(place);
|
|
49
|
+
if (cached !== undefined)
|
|
50
|
+
return cached;
|
|
51
|
+
const move = this.#moves.get(place) ?? 0;
|
|
52
|
+
const { entries, version } = await this.#memory.read({ place });
|
|
53
|
+
const sealed = entries[ENTRY];
|
|
54
|
+
let held = { row: null, text: null, version };
|
|
55
|
+
if (sealed !== undefined) {
|
|
56
|
+
const opened = await this.#crypto.open(this.#key, sealed.subarray(0, 12), sealed.subarray(12), this.#tools.utf8(place));
|
|
57
|
+
const text = opened === null ? null : this.#tools.text(opened);
|
|
58
|
+
if (text === null)
|
|
59
|
+
throw new Error('a row does not open under these keys');
|
|
60
|
+
held = { row: JSON.parse(text), text, version };
|
|
61
|
+
}
|
|
62
|
+
// A write that moved the row while this read was in flight wins, and this read looks again.
|
|
63
|
+
if ((this.#moves.get(place) ?? 0) !== move)
|
|
64
|
+
continue;
|
|
65
|
+
this.#held.set(place, held);
|
|
66
|
+
return held;
|
|
49
67
|
}
|
|
50
|
-
this.#held.set(place, held);
|
|
51
|
-
return held;
|
|
52
68
|
}
|
|
53
69
|
/** A row as it stands, or `null`. */
|
|
54
70
|
async get(place) {
|
|
@@ -152,10 +168,11 @@ export class Rows {
|
|
|
152
168
|
}
|
|
153
169
|
if (versions === null) {
|
|
154
170
|
for (const place of Object.keys(expect))
|
|
155
|
-
this.#
|
|
171
|
+
this.#forget(place);
|
|
156
172
|
return false;
|
|
157
173
|
}
|
|
158
174
|
for (const [place, held] of landed) {
|
|
175
|
+
this.#moved(place);
|
|
159
176
|
this.#held.set(place, { ...held, version: versions[place] ?? null });
|
|
160
177
|
this.#landed?.(place, held.row);
|
|
161
178
|
}
|
package/dist/node/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { connect } from 'node:net';
|
|
|
20
20
|
import { basename, join, resolve } from 'node:path';
|
|
21
21
|
import { createInterface } from 'node:readline';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { handAt } from './hand.js';
|
|
23
24
|
import { NodeGround } from './node-ground.js';
|
|
24
25
|
import { notifyReady } from './notify.js';
|
|
25
26
|
const USAGE = 'nervur up <folder> | service <folder> | [--at <socket>] help | ask <house> [--id <being>] [<method> [args]] | <faculty> [<method> [args]]';
|
|
@@ -127,7 +128,7 @@ const service = (folder) => {
|
|
|
127
128
|
};
|
|
128
129
|
const words = process.argv.slice(2);
|
|
129
130
|
// The hand: named, or the one a ground in this folder serves.
|
|
130
|
-
let at = process.env.NERVUR_HAND ?? (existsSync(join('state', 'hand')) ?
|
|
131
|
+
let at = process.env.NERVUR_HAND ?? (existsSync(process.platform === 'win32' ? 'state' : join('state', 'hand')) ? handAt('state') : undefined);
|
|
131
132
|
if (words[0] === '--at') {
|
|
132
133
|
at = words[1] ?? fail('--at names no socket');
|
|
133
134
|
words.splice(0, 2);
|
package/dist/node/file-memory.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { mkdir, open, readFile, rename } from 'node:fs/promises';
|
|
6
6
|
import { dirname } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { syncFolder } from './sync-folder.js';
|
|
8
9
|
const hex = (bytes) => Buffer.from(bytes).toString('hex');
|
|
9
10
|
const unhex = (text) => new Uint8Array(Buffer.from(text, 'hex'));
|
|
10
11
|
export class FileMemory {
|
|
@@ -80,12 +81,6 @@ export class FileMemory {
|
|
|
80
81
|
await file.close();
|
|
81
82
|
}
|
|
82
83
|
await rename(temporary, this.#path);
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
await directory.sync();
|
|
86
|
-
}
|
|
87
|
-
finally {
|
|
88
|
-
await directory.close();
|
|
89
|
-
}
|
|
84
|
+
await syncFolder(folder);
|
|
90
85
|
}
|
|
91
86
|
}
|
package/dist/node/hand.d.ts
CHANGED
|
@@ -21,9 +21,16 @@ export interface HandRequest {
|
|
|
21
21
|
/** The answer the owner holds, which makes a `readOnly` ask a watch. */
|
|
22
22
|
readonly after?: Answer;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Where a ground's hand stands for its state folder: a socket inside it,
|
|
26
|
+
* or on Windows a named pipe, which has no place in a folder, named for
|
|
27
|
+
* the folder's path.
|
|
28
|
+
*/
|
|
29
|
+
export declare const handAt: (state: string, platform?: NodeJS.Platform) => string;
|
|
24
30
|
/**
|
|
25
31
|
* Serves `ask` at `path`: a house's own hand, or a ground's, which also
|
|
26
32
|
* reads `house` and changes its record. A socket left there by a process
|
|
27
|
-
* that is gone is replaced.
|
|
33
|
+
* that is gone is replaced. A named pipe on Windows is open to its user
|
|
34
|
+
* alone, as the operating system makes it, and leaves nothing behind.
|
|
28
35
|
*/
|
|
29
36
|
export declare const serveHand: (ask: (request: HandRequest) => Promise<unknown>, path: string) => Promise<Hand>;
|
package/dist/node/hand.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The hand on a local socket
|
|
3
|
-
// line out, in order.
|
|
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';
|
|
4
6
|
import { chmod } from 'node:fs/promises';
|
|
5
7
|
import { createServer } from 'node:net';
|
|
8
|
+
import { join, resolve as absolute } from 'node:path';
|
|
6
9
|
import { createInterface } from 'node:readline';
|
|
7
10
|
import { gone } from './gone.js';
|
|
8
11
|
const object = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -46,16 +49,29 @@ const serve = (ask, socket) => {
|
|
|
46
49
|
// A local socket's path fits the kernel's address: 103 bytes on macOS and
|
|
47
50
|
// the BSDs, 107 on Linux, with the closing zero beyond.
|
|
48
51
|
const LONGEST = process.platform === 'linux' ? 107 : 103;
|
|
52
|
+
const PIPE = /^\\\\[.?]\\pipe\\/;
|
|
53
|
+
/**
|
|
54
|
+
* Where a ground's hand stands for its state folder: a socket inside it,
|
|
55
|
+
* or on Windows a named pipe, which has no place in a folder, named for
|
|
56
|
+
* the folder's path.
|
|
57
|
+
*/
|
|
58
|
+
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');
|
|
49
59
|
/**
|
|
50
60
|
* Serves `ask` at `path`: a house's own hand, or a ground's, which also
|
|
51
61
|
* reads `house` and changes its record. A socket left there by a process
|
|
52
|
-
* that is gone is replaced.
|
|
62
|
+
* that is gone is replaced. A named pipe on Windows is open to its user
|
|
63
|
+
* alone, as the operating system makes it, and leaves nothing behind.
|
|
53
64
|
*/
|
|
54
65
|
export const serveHand = async (ask, path) => {
|
|
66
|
+
if (PIPE.test(path))
|
|
67
|
+
return listen(ask, path, false);
|
|
55
68
|
const length = Buffer.byteLength(path);
|
|
56
69
|
if (length > LONGEST)
|
|
57
70
|
throw new Error(`a socket's path is at most ${LONGEST} bytes here, and ${path} is ${length}: serve the hand from a shorter folder`);
|
|
58
71
|
await gone(path);
|
|
72
|
+
return listen(ask, path, true);
|
|
73
|
+
};
|
|
74
|
+
const listen = async (ask, path, file) => {
|
|
59
75
|
const server = createServer((socket) => serve(ask, socket));
|
|
60
76
|
const sockets = new Set();
|
|
61
77
|
server.on('connection', (socket) => {
|
|
@@ -64,6 +80,8 @@ export const serveHand = async (ask, path) => {
|
|
|
64
80
|
});
|
|
65
81
|
await new Promise((resolve, reject) => {
|
|
66
82
|
server.once('error', reject);
|
|
83
|
+
if (!file)
|
|
84
|
+
return void server.listen(path, () => resolve());
|
|
67
85
|
// Closed to everyone from the first moment, then opened to the user.
|
|
68
86
|
const mask = process.umask(0o177);
|
|
69
87
|
try {
|
|
@@ -73,13 +91,15 @@ export const serveHand = async (ask, path) => {
|
|
|
73
91
|
process.umask(mask);
|
|
74
92
|
}
|
|
75
93
|
});
|
|
76
|
-
|
|
94
|
+
if (file)
|
|
95
|
+
await chmod(path, 0o600);
|
|
77
96
|
return {
|
|
78
97
|
close: async () => {
|
|
79
98
|
for (const socket of sockets)
|
|
80
99
|
socket.destroy();
|
|
81
100
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
82
|
-
|
|
101
|
+
if (file)
|
|
102
|
+
await gone(path);
|
|
83
103
|
},
|
|
84
104
|
};
|
|
85
105
|
};
|
|
@@ -9,6 +9,7 @@ import { createHash } from 'node:crypto';
|
|
|
9
9
|
import { mkdir, open, readFile, rename, truncate } from 'node:fs/promises';
|
|
10
10
|
import { dirname } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { syncFolder } from './sync-folder.js';
|
|
12
13
|
const GENESIS = { n: 0, hash: '0'.repeat(64) };
|
|
13
14
|
const hash = (text) => createHash('sha256').update(text).digest('hex');
|
|
14
15
|
const unhex = (text) => new Uint8Array(Buffer.from(text, 'hex'));
|
|
@@ -103,7 +104,7 @@ export class LedgerMemory {
|
|
|
103
104
|
const temporary = `${this.#witness}.${process.pid}.tmp`;
|
|
104
105
|
await synced(temporary, 'w', (handle) => handle.writeFile(JSON.stringify(head)));
|
|
105
106
|
await rename(temporary, this.#witness);
|
|
106
|
-
await
|
|
107
|
+
await syncFolder(dirname(this.#witness));
|
|
107
108
|
}
|
|
108
109
|
async read({ place }) {
|
|
109
110
|
const held = (await this.#open()).places.get(place);
|
|
@@ -135,7 +136,7 @@ export class LedgerMemory {
|
|
|
135
136
|
try {
|
|
136
137
|
await synced(this.#path, 'a', (handle) => handle.appendFile(`${raw}\n`));
|
|
137
138
|
if (line.n === 1)
|
|
138
|
-
await
|
|
139
|
+
await syncFolder(dirname(this.#path));
|
|
139
140
|
if (this.#witness !== undefined)
|
|
140
141
|
await this.#attest(head);
|
|
141
142
|
}
|
package/dist/node/node-ground.js
CHANGED
|
@@ -20,7 +20,7 @@ import { WebCarry } from '../bodies/web-carry.js';
|
|
|
20
20
|
import { Ground } from '../ground/ground.js';
|
|
21
21
|
import { FolderCustody, KeychainCustody } from './custody.js';
|
|
22
22
|
import { FolderClasses } from './folder-classes.js';
|
|
23
|
-
import { serveHand } from './hand.js';
|
|
23
|
+
import { handAt, serveHand } from './hand.js';
|
|
24
24
|
import { serveHttp } from './http.js';
|
|
25
25
|
import { LedgerMemory } from './ledger-memory.js';
|
|
26
26
|
import { takeLock } from './lock.js';
|
|
@@ -127,7 +127,7 @@ export class NodeGround {
|
|
|
127
127
|
// 6. Hook: HTTP chains the web carry and every faculty's handler, and the hand takes its socket.
|
|
128
128
|
if (httpPort !== undefined)
|
|
129
129
|
http = await serveHttp({ port: httpPort, host: bind }, [web, ...ground.handlers]);
|
|
130
|
-
const hand = env.NERVUR_HAND ??
|
|
130
|
+
const hand = env.NERVUR_HAND ?? handAt(state);
|
|
131
131
|
const served = await serveHand((request) => ground.hand(request), hand);
|
|
132
132
|
return new NodeGround({ ground, hand, tcp, lock, served, http });
|
|
133
133
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes a file's creation or renaming in `folder` durable. On POSIX the
|
|
3
|
+
* folder is synced. Windows opens no folder for syncing, and NTFS journals
|
|
4
|
+
* a creation and a rename itself, so there it does nothing.
|
|
5
|
+
*/
|
|
6
|
+
export declare const syncFolder: (folder: string, platform?: NodeJS.Platform) => Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
/**
|
|
4
|
+
* Makes a file's creation or renaming in `folder` durable. On POSIX the
|
|
5
|
+
* folder is synced. Windows opens no folder for syncing, and NTFS journals
|
|
6
|
+
* a creation and a rename itself, so there it does nothing.
|
|
7
|
+
*/
|
|
8
|
+
export const syncFolder = async (folder, platform = process.platform) => {
|
|
9
|
+
if (platform === 'win32')
|
|
10
|
+
return;
|
|
11
|
+
const handle = await open(folder, 'r');
|
|
12
|
+
try {
|
|
13
|
+
await handle.sync();
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await handle.close();
|
|
17
|
+
}
|
|
18
|
+
};
|