@volter/world-runtime 2.0.32 → 2.0.34
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/src/served-world.js +20 -5
- package/dist/src/tail.js +44 -8
- package/package.json +5 -5
- package/src/served-world.ts +19 -5
- package/src/tail.ts +23 -5
package/dist/src/served-world.js
CHANGED
|
@@ -26,7 +26,7 @@ import { getActiveWorldStore, withAncestryLock, captureHistory, historyAtInstant
|
|
|
26
26
|
// to `.volter/token` and `.volter/token.read` (0600).
|
|
27
27
|
// One writer per world by construction: the doors run in this process, serialized per twin.
|
|
28
28
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
29
|
-
import { assertSafeChangesetName, blobDigest, destinationOf, openSealedAt, getActiveBlobStore, journalTwinRequest, loadCheck, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin } from '@volter/world-core';
|
|
29
|
+
import { assertSafeChangesetName, blobDigest, destinationOf, openSealedAt, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin } from '@volter/world-core';
|
|
30
30
|
import { APP_READ_ENDPOINT_ENV } from "./init.js";
|
|
31
31
|
import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass } from '@volter/world-access';
|
|
32
32
|
import { createHash } from 'node:crypto';
|
|
@@ -950,7 +950,12 @@ export class WorldDoors {
|
|
|
950
950
|
if (offered.some((p) => p.startsWith('tok_')) || (offered.length && grantOf(offered, null, this.issued())))
|
|
951
951
|
caller.delete('authorization');
|
|
952
952
|
// an opened seal keeps the audit a twin keeps: the same line in the same journal under the link's
|
|
953
|
-
// name, with the credential shape of what the caller sent on
|
|
953
|
+
// name, with the credential shape of what the caller sent on. As an audit device does, the request is
|
|
954
|
+
// written before the real credential is used, and a request whose audit cannot be written is refused.
|
|
955
|
+
const credentials = twinRequestCredentials(caller, url);
|
|
956
|
+
if (!journalTwinRequest(name, { phase: 'request', method: request.method, path: `/${rest}`, status: 0, credentials }, this.controlRoot(name))) {
|
|
957
|
+
return Response.json({ error: `the ${name} link's audit could not be written: nothing was sent` }, { status: 503 });
|
|
958
|
+
}
|
|
954
959
|
const startedAt = Date.now();
|
|
955
960
|
let status = 500;
|
|
956
961
|
try {
|
|
@@ -959,7 +964,7 @@ export class WorldDoors {
|
|
|
959
964
|
return answer;
|
|
960
965
|
}
|
|
961
966
|
finally {
|
|
962
|
-
journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials
|
|
967
|
+
journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials }, this.controlRoot(name));
|
|
963
968
|
}
|
|
964
969
|
}
|
|
965
970
|
async forwardSealed(request, caller, url, rest, name, origin) {
|
|
@@ -1184,6 +1189,15 @@ export class WorldDoors {
|
|
|
1184
1189
|
}
|
|
1185
1190
|
if (kind === 'links' && !name && request.method === 'GET')
|
|
1186
1191
|
return Response.json({ links: this.links() });
|
|
1192
|
+
// OPENING A SEAL — pointing this World at a vendor (a link, a twin's root) or sealing the credential it uses there — is
|
|
1193
|
+
// the World token's act, or a person's at its page: never a key's, which an app or a script holds (an app that could
|
|
1194
|
+
// open its own seal would be unsealed by itself, not deliberately)
|
|
1195
|
+
const opensSeal = (kind === 'links' && name && (request.method === 'PUT' || request.method === 'DELETE'))
|
|
1196
|
+
|| (kind === 'twins' && name && (verb === 'root' || verb === 'credential') && request.method === 'PUT')
|
|
1197
|
+
// a check runs in a local World's own process, with all it can reach: putting or removing one is the same act
|
|
1198
|
+
|| (kind === 'checks' && name && (request.method === 'PUT' || request.method === 'DELETE'));
|
|
1199
|
+
if (opensSeal && via === 'key')
|
|
1200
|
+
return Response.json({ error: 'opening a seal is the World\'s token\'s act, or a person\'s at its page: a key cannot point this World at a vendor, seal a credential, or change its checks' }, { status: 403 });
|
|
1187
1201
|
if (kind === 'links' && name && request.method === 'PUT') {
|
|
1188
1202
|
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name) || this.up().services[name])
|
|
1189
1203
|
return Response.json({ error: 'a link is named [a-z0-9_-] and is not a twin of this World' }, { status: 400 });
|
|
@@ -1328,7 +1342,8 @@ export class WorldDoors {
|
|
|
1328
1342
|
const since = Date.now() - sinceMs;
|
|
1329
1343
|
const twins = [];
|
|
1330
1344
|
for (const vendor of [...new Set([...Object.keys(this.up().services), ...Object.keys(this.links())])]) {
|
|
1331
|
-
|
|
1345
|
+
// a link's request entries (written before it forwards) are its audit, not traffic: its outcomes are counted
|
|
1346
|
+
const entries = readTwinRequestJournal(vendor, this.controlRoot(vendor), { since }).filter((e) => e.phase !== 'request' && (!e.at || Date.parse(e.at) >= since));
|
|
1332
1347
|
const byStatus = {};
|
|
1333
1348
|
const routes = new Map();
|
|
1334
1349
|
const times = [];
|
|
@@ -1344,7 +1359,7 @@ export class WorldDoors {
|
|
|
1344
1359
|
const pct = (p) => (times.length ? times[Math.min(times.length - 1, Math.floor((p / 100) * times.length))] : null);
|
|
1345
1360
|
twins.push({ twin: vendor, requests: entries.length, byStatus, p50Ms: pct(50), p95Ms: pct(95), topRoutes: [...routes.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([route, requests]) => ({ route, requests })) });
|
|
1346
1361
|
}
|
|
1347
|
-
return Response.json({ world: this.served, hours: sinceMs / 3_600_000, twins, requests: twins.reduce((n, t) => n + t.requests, 0) });
|
|
1362
|
+
return Response.json({ world: this.served, hours: sinceMs / 3_600_000, twins, requests: twins.reduce((n, t) => n + t.requests, 0), journalFailures: twins.reduce((n, t) => n + twinRequestJournalFailures(t.twin, this.controlRoot(t.twin)), 0) });
|
|
1348
1363
|
}
|
|
1349
1364
|
if (kind === 'twins' && !name && request.method === 'GET') {
|
|
1350
1365
|
// every twin of the world as a consumer sees it — the status door mapped over the world; what a
|
package/dist/src/tail.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// records its first action, so discovery re-runs on every poll and a missing ledger is a
|
|
8
8
|
// notice, never an error.
|
|
9
9
|
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
|
|
10
|
-
import { join } from 'node:path';
|
|
10
|
+
import { basename, dirname, join } from 'node:path';
|
|
11
11
|
import { stateDirName } from '@volter/world-core';
|
|
12
12
|
import { statusWorld } from "./runtime.js";
|
|
13
13
|
/** Every action ledger a world service currently owns. One world service can record actions
|
|
@@ -43,10 +43,24 @@ function readNewLines(path, offsets) {
|
|
|
43
43
|
catch {
|
|
44
44
|
return []; // vanished between discovery and read (e.g. a purge mid-tail) — next poll rediscovers
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
let closed = [];
|
|
47
|
+
if (size < offset) {
|
|
48
|
+
// a request journal that closed into its next segment (world-core serve.ts): what was appended there
|
|
49
|
+
// after the last poll is read from the segment first; any other shorter rewrite (scrub/rebuild) replays
|
|
50
|
+
const segment = newestSegment(path);
|
|
51
|
+
let closedSize = -1;
|
|
52
|
+
try {
|
|
53
|
+
closedSize = segment ? statSync(segment).size : -1;
|
|
54
|
+
}
|
|
55
|
+
catch { /* gone between list and stat */ }
|
|
56
|
+
if (segment && closedSize >= offset)
|
|
57
|
+
closed = readNewLines(segment, new Map([[segment, offset]]));
|
|
58
|
+
offset = 0;
|
|
59
|
+
}
|
|
60
|
+
if (size === offset) {
|
|
61
|
+
offsets.set(path, offset);
|
|
62
|
+
return closed;
|
|
63
|
+
}
|
|
50
64
|
const fd = openSync(path, 'r');
|
|
51
65
|
const buffer = Buffer.alloc(size - offset);
|
|
52
66
|
try {
|
|
@@ -56,10 +70,29 @@ function readNewLines(path, offsets) {
|
|
|
56
70
|
closeSync(fd);
|
|
57
71
|
}
|
|
58
72
|
const lastNewline = buffer.lastIndexOf(0x0a);
|
|
59
|
-
if (lastNewline < 0)
|
|
60
|
-
|
|
73
|
+
if (lastNewline < 0) {
|
|
74
|
+
offsets.set(path, offset);
|
|
75
|
+
return closed;
|
|
76
|
+
}
|
|
61
77
|
offsets.set(path, offset + lastNewline + 1);
|
|
62
|
-
return buffer.subarray(0, lastNewline).toString('utf8').split('\n').filter((line) => line.trim() !== '');
|
|
78
|
+
return [...closed, ...buffer.subarray(0, lastNewline).toString('utf8').split('\n').filter((line) => line.trim() !== '')];
|
|
79
|
+
}
|
|
80
|
+
/** A journal's newest closed segment (`<file>.<n>`), or null. */
|
|
81
|
+
function newestSegment(path) {
|
|
82
|
+
const dir = dirname(path);
|
|
83
|
+
const base = basename(path);
|
|
84
|
+
let best = 0;
|
|
85
|
+
try {
|
|
86
|
+
for (const name of readdirSync(dir)) {
|
|
87
|
+
const n = name.startsWith(`${base}.`) ? Number(name.slice(base.length + 1)) : NaN;
|
|
88
|
+
if (Number.isInteger(n) && n > best)
|
|
89
|
+
best = n;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return best ? `${path}.${best}` : null;
|
|
63
96
|
}
|
|
64
97
|
/** Local wall-clock `HH:MM:SS.mmm` — the operator is watching a live feed on this machine. */
|
|
65
98
|
function formatOccurredAt(occurredAt) {
|
|
@@ -85,6 +118,9 @@ function formatEntry(entry, json) {
|
|
|
85
118
|
const creds = (record.credentials ?? [])
|
|
86
119
|
.map((c) => `${c.name ?? '?'}${c.in === 'query' ? '(query)' : ''}=${c.empty ? 'EMPTY' : `${c.scheme ? `${c.scheme} ` : ''}${(c.fp ?? '').replace(/^sha256:/, '').slice(0, 8)}`}`)
|
|
87
120
|
.join(' ');
|
|
121
|
+
// a link's request entry, written before it forwards: the request on its way, its outcome follows
|
|
122
|
+
if (record.phase === 'request')
|
|
123
|
+
return `${formatOccurredAt(entryInstant(entry))} ${entry.service} → ${record.method ?? '?'} ${record.path ?? '?'}${creds ? ` [${creds}]` : ''}`;
|
|
88
124
|
return `${formatOccurredAt(entryInstant(entry))} ${entry.service} ${record.method ?? '?'} ${record.path ?? '?'} ${record.status ?? '?'}${typeof record.ms === 'number' ? ` in ${record.ms}ms` : ''}${creds ? ` [${creds}]` : ''}`;
|
|
89
125
|
}
|
|
90
126
|
return `${formatOccurredAt(record.occurredAt)} ${entry.service} ${record.operation ?? record.op ?? '?'} ${record.subject?.id ?? '?'}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.34",
|
|
4
4
|
"description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -67,14 +67,14 @@
|
|
|
67
67
|
"node": ">=22.3"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@volter/world-core": "2.0.
|
|
71
|
-
"@volter/world-console": "2.0.
|
|
70
|
+
"@volter/world-core": "2.0.20",
|
|
71
|
+
"@volter/world-console": "2.0.12"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@electric-sql/pglite": "0.5.8",
|
|
75
75
|
"@electric-sql/pglite-pgvector": "0.0.9",
|
|
76
76
|
"@volter/world-access": "2.0.2",
|
|
77
|
-
"@volter/world-core": "2.0.
|
|
77
|
+
"@volter/world-core": "2.0.20",
|
|
78
78
|
"pg-gateway": "0.3.0-beta.4",
|
|
79
79
|
"smol-toml": "^1.8.0"
|
|
80
80
|
},
|
|
@@ -84,6 +84,6 @@
|
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
86
|
"optionalDependencies": {
|
|
87
|
-
"@volter/twin-mongodb": "0.1.
|
|
87
|
+
"@volter/twin-mongodb": "0.1.20"
|
|
88
88
|
}
|
|
89
89
|
}
|
package/src/served-world.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { getActiveWorldStore, withAncestryLock, captureHistory, historyAtInstant
|
|
|
26
26
|
// to `.volter/token` and `.volter/token.read` (0600).
|
|
27
27
|
// One writer per world by construction: the doors run in this process, serialized per twin.
|
|
28
28
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync , rmSync} from 'node:fs';
|
|
29
|
-
import { assertSafeChangesetName, blobDigest, destinationOf, openSealedAt, getActiveBlobStore, journalTwinRequest, loadCheck, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core';
|
|
29
|
+
import { assertSafeChangesetName, blobDigest, destinationOf, openSealedAt, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, mirrorShellUnder, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core';
|
|
30
30
|
import { APP_READ_ENDPOINT_ENV } from './init.ts';
|
|
31
31
|
import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass, type TrustedIssuer, type WorldCredentials } from '@volter/world-access';
|
|
32
32
|
import { createHash } from 'node:crypto';
|
|
@@ -849,14 +849,19 @@ export class WorldDoors {
|
|
|
849
849
|
const offered = authorization ? presentedSecrets(new Headers({ authorization }), { token: TOKEN_HEADER, twinsKey: TWINS_KEY_HEADER }) : [];
|
|
850
850
|
if (offered.some((p) => p.startsWith('tok_')) || (offered.length && grantOf(offered, null, this.issued()))) caller.delete('authorization');
|
|
851
851
|
// an opened seal keeps the audit a twin keeps: the same line in the same journal under the link's
|
|
852
|
-
// name, with the credential shape of what the caller sent on
|
|
852
|
+
// name, with the credential shape of what the caller sent on. As an audit device does, the request is
|
|
853
|
+
// written before the real credential is used, and a request whose audit cannot be written is refused.
|
|
854
|
+
const credentials = twinRequestCredentials(caller, url);
|
|
855
|
+
if (!journalTwinRequest(name, { phase: 'request', method: request.method, path: `/${rest}`, status: 0, credentials }, this.controlRoot(name))) {
|
|
856
|
+
return Response.json({ error: `the ${name} link's audit could not be written: nothing was sent` }, { status: 503 });
|
|
857
|
+
}
|
|
853
858
|
const startedAt = Date.now(); let status = 500;
|
|
854
859
|
try {
|
|
855
860
|
const answer = await this.forwardSealed(request, caller, url, rest, name, origin);
|
|
856
861
|
status = answer.status;
|
|
857
862
|
return answer;
|
|
858
863
|
} finally {
|
|
859
|
-
journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials
|
|
864
|
+
journalTwinRequest(name, { method: request.method, path: `/${rest}`, status, ms: Date.now() - startedAt, credentials }, this.controlRoot(name));
|
|
860
865
|
}
|
|
861
866
|
}
|
|
862
867
|
|
|
@@ -1032,6 +1037,14 @@ export class WorldDoors {
|
|
|
1032
1037
|
return Response.json(approveWorldChangeset(name, { root: this.worldRoot, world: this.name, principal, ...(body.note ? { note: body.note } : {}) }));
|
|
1033
1038
|
}
|
|
1034
1039
|
if (kind === 'links' && !name && request.method === 'GET') return Response.json({ links: this.links() });
|
|
1040
|
+
// OPENING A SEAL — pointing this World at a vendor (a link, a twin's root) or sealing the credential it uses there — is
|
|
1041
|
+
// the World token's act, or a person's at its page: never a key's, which an app or a script holds (an app that could
|
|
1042
|
+
// open its own seal would be unsealed by itself, not deliberately)
|
|
1043
|
+
const opensSeal = (kind === 'links' && name && (request.method === 'PUT' || request.method === 'DELETE'))
|
|
1044
|
+
|| (kind === 'twins' && name && (verb === 'root' || verb === 'credential') && request.method === 'PUT')
|
|
1045
|
+
// a check runs in a local World's own process, with all it can reach: putting or removing one is the same act
|
|
1046
|
+
|| (kind === 'checks' && name && (request.method === 'PUT' || request.method === 'DELETE'));
|
|
1047
|
+
if (opensSeal && via === 'key') return Response.json({ error: 'opening a seal is the World\'s token\'s act, or a person\'s at its page: a key cannot point this World at a vendor, seal a credential, or change its checks' }, { status: 403 });
|
|
1035
1048
|
if (kind === 'links' && name && request.method === 'PUT') {
|
|
1036
1049
|
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name) || this.up().services[name]) return Response.json({ error: 'a link is named [a-z0-9_-] and is not a twin of this World' }, { status: 400 });
|
|
1037
1050
|
const body = (await request.json()) as { origin?: string };
|
|
@@ -1145,13 +1158,14 @@ export class WorldDoors {
|
|
|
1145
1158
|
const sinceMs = Number(url.searchParams.get('hours') ?? '24') * 3_600_000; const since = Date.now() - sinceMs;
|
|
1146
1159
|
const twins: Array<{ twin: string; requests: number; byStatus: Record<string, number>; p50Ms: number | null; p95Ms: number | null; topRoutes: Array<{ route: string; requests: number }> }> = [];
|
|
1147
1160
|
for (const vendor of [...new Set([...Object.keys(this.up().services), ...Object.keys(this.links())])]) {
|
|
1148
|
-
|
|
1161
|
+
// a link's request entries (written before it forwards) are its audit, not traffic: its outcomes are counted
|
|
1162
|
+
const entries = readTwinRequestJournal(vendor, this.controlRoot(vendor), { since }).filter((e) => e.phase !== 'request' && (!e.at || Date.parse(e.at) >= since));
|
|
1149
1163
|
const byStatus: Record<string, number> = {}; const routes = new Map<string, number>(); const times: number[] = [];
|
|
1150
1164
|
for (const e of entries) { const cls = `${String(e.status)[0]}xx`; byStatus[cls] = (byStatus[cls] ?? 0) + 1; const route = `${e.method} ${e.path.replace(/\/[0-9]+(?=\/|$)/g, '/{id}')}`; routes.set(route, (routes.get(route) ?? 0) + 1); if (typeof e.ms === 'number') times.push(e.ms); }
|
|
1151
1165
|
times.sort((a, b) => a - b); const pct = (p: number): number | null => (times.length ? times[Math.min(times.length - 1, Math.floor((p / 100) * times.length))]! : null);
|
|
1152
1166
|
twins.push({ twin: vendor, requests: entries.length, byStatus, p50Ms: pct(50), p95Ms: pct(95), topRoutes: [...routes.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([route, requests]) => ({ route, requests })) });
|
|
1153
1167
|
}
|
|
1154
|
-
return Response.json({ world: this.served, hours: sinceMs / 3_600_000, twins, requests: twins.reduce((n, t) => n + t.requests, 0) });
|
|
1168
|
+
return Response.json({ world: this.served, hours: sinceMs / 3_600_000, twins, requests: twins.reduce((n, t) => n + t.requests, 0), journalFailures: twins.reduce((n, t) => n + twinRequestJournalFailures(t.twin, this.controlRoot(t.twin)), 0) });
|
|
1155
1169
|
}
|
|
1156
1170
|
if (kind === 'twins' && !name && request.method === 'GET') {
|
|
1157
1171
|
// every twin of the world as a consumer sees it — the status door mapped over the world; what a
|
package/src/tail.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// records its first action, so discovery re-runs on every poll and a missing ledger is a
|
|
8
8
|
// notice, never an error.
|
|
9
9
|
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
|
|
10
|
-
import { join } from 'node:path';
|
|
10
|
+
import { basename, dirname, join } from 'node:path';
|
|
11
11
|
import { stateDirName } from '@volter/world-core';
|
|
12
12
|
import { statusWorld } from './runtime.ts';
|
|
13
13
|
|
|
@@ -84,8 +84,16 @@ function readNewLines(path: string, offsets: Map<string, number>): string[] {
|
|
|
84
84
|
} catch {
|
|
85
85
|
return []; // vanished between discovery and read (e.g. a purge mid-tail) — next poll rediscovers
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
if (size
|
|
87
|
+
let closed: string[] = [];
|
|
88
|
+
if (size < offset) {
|
|
89
|
+
// a request journal that closed into its next segment (world-core serve.ts): what was appended there
|
|
90
|
+
// after the last poll is read from the segment first; any other shorter rewrite (scrub/rebuild) replays
|
|
91
|
+
const segment = newestSegment(path);
|
|
92
|
+
let closedSize = -1; try { closedSize = segment ? statSync(segment).size : -1; } catch { /* gone between list and stat */ }
|
|
93
|
+
if (segment && closedSize >= offset) closed = readNewLines(segment, new Map([[segment, offset]]));
|
|
94
|
+
offset = 0;
|
|
95
|
+
}
|
|
96
|
+
if (size === offset) { offsets.set(path, offset); return closed; }
|
|
89
97
|
const fd = openSync(path, 'r');
|
|
90
98
|
const buffer = Buffer.alloc(size - offset);
|
|
91
99
|
try {
|
|
@@ -94,9 +102,17 @@ function readNewLines(path: string, offsets: Map<string, number>): string[] {
|
|
|
94
102
|
closeSync(fd);
|
|
95
103
|
}
|
|
96
104
|
const lastNewline = buffer.lastIndexOf(0x0a);
|
|
97
|
-
if (lastNewline < 0) return
|
|
105
|
+
if (lastNewline < 0) { offsets.set(path, offset); return closed; }
|
|
98
106
|
offsets.set(path, offset + lastNewline + 1);
|
|
99
|
-
return buffer.subarray(0, lastNewline).toString('utf8').split('\n').filter((line) => line.trim() !== '');
|
|
107
|
+
return [...closed, ...buffer.subarray(0, lastNewline).toString('utf8').split('\n').filter((line) => line.trim() !== '')];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A journal's newest closed segment (`<file>.<n>`), or null. */
|
|
111
|
+
function newestSegment(path: string): string | null {
|
|
112
|
+
const dir = dirname(path); const base = basename(path);
|
|
113
|
+
let best = 0;
|
|
114
|
+
try { for (const name of readdirSync(dir)) { const n = name.startsWith(`${base}.`) ? Number(name.slice(base.length + 1)) : NaN; if (Number.isInteger(n) && n > best) best = n; } } catch { return null; }
|
|
115
|
+
return best ? `${path}.${best}` : null;
|
|
100
116
|
}
|
|
101
117
|
|
|
102
118
|
/** Local wall-clock `HH:MM:SS.mmm` — the operator is watching a live feed on this machine. */
|
|
@@ -123,6 +139,8 @@ function formatEntry(entry: TailEntry, json: boolean): string {
|
|
|
123
139
|
const creds = (record.credentials ?? [])
|
|
124
140
|
.map((c) => `${c.name ?? '?'}${c.in === 'query' ? '(query)' : ''}=${c.empty ? 'EMPTY' : `${c.scheme ? `${c.scheme} ` : ''}${(c.fp ?? '').replace(/^sha256:/, '').slice(0, 8)}`}`)
|
|
125
141
|
.join(' ');
|
|
142
|
+
// a link's request entry, written before it forwards: the request on its way, its outcome follows
|
|
143
|
+
if (record.phase === 'request') return `${formatOccurredAt(entryInstant(entry))} ${entry.service} → ${record.method ?? '?'} ${record.path ?? '?'}${creds ? ` [${creds}]` : ''}`;
|
|
126
144
|
return `${formatOccurredAt(entryInstant(entry))} ${entry.service} ${record.method ?? '?'} ${record.path ?? '?'} ${record.status ?? '?'}${typeof record.ms === 'number' ? ` in ${record.ms}ms` : ''}${creds ? ` [${creds}]` : ''}`;
|
|
127
145
|
}
|
|
128
146
|
return `${formatOccurredAt(record.occurredAt)} ${entry.service} ${record.operation ?? record.op ?? '?'} ${record.subject?.id ?? '?'}`;
|