@vrtmrz/livesync-commonlib 0.1.43 → 0.1.44
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/README.md +20 -2
- package/dist/diagnostics/couchdb.js +86 -13
- package/dist/diagnostics/index.d.ts +2 -2
- package/dist/diagnostics/index.js +2 -0
- package/dist/diagnostics/integrity.js +385 -158
- package/dist/diagnostics/names.d.ts +16 -0
- package/dist/diagnostics/names.js +50 -0
- package/dist/diagnostics/reports.d.ts +13 -10
- package/dist/diagnostics/reports.js +191 -50
- package/dist/diagnostics/types.d.ts +64 -4
- package/dist/diagnostics/usage.d.ts +4 -0
- package/dist/diagnostics/usage.js +158 -54
- package/docs/database-diagnostics.md +45 -0
- package/docs/proven-in-use.md +7 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -122,7 +122,9 @@ The focused Setup URI entry offers an Ephemeral mode bound to the current fixed
|
|
|
122
122
|
|
|
123
123
|
## Database diagnostics
|
|
124
124
|
|
|
125
|
-
The focused `/diagnostics` entry provides read-only Usage and integrity reports, with original names or temporary pseudonyms selected by the host for each run. Integrity inspection covers normal-file Metadata, current conflict branches, and content comparisons for suspect groups in the local database and CouchDB. See [Database diagnostics](docs/database-diagnostics.md) for coverage, cancellation, and privacy boundaries.
|
|
125
|
+
The focused `/diagnostics` entry provides read-only Usage and integrity reports, with original names or temporary pseudonyms selected by the host for each run. Integrity inspection covers normal-file Metadata, current conflict branches, and content comparisons for suspect groups in the local database and, by default, CouchDB; the host can exclude the remote source for one run. See [Database diagnostics](docs/database-diagnostics.md) for coverage, progress, cancellation, pseudonym lookup, and privacy boundaries.
|
|
126
|
+
|
|
127
|
+
Integrity Markdown places a compact state summary and review index before all findings, while retaining each observation and its follow-up references. This formatting does not change the projected JSON data or schema; see [the Markdown field guide](docs/database-diagnostics.md#compact-integrity-markdown-report) for the meaning and limits of its review index.
|
|
126
128
|
|
|
127
129
|
```typescript
|
|
128
130
|
import {
|
|
@@ -130,17 +132,33 @@ import {
|
|
|
130
132
|
createDiagnosticNameMapper,
|
|
131
133
|
createLocalDiagnosticSource,
|
|
132
134
|
formatUsageTSV,
|
|
135
|
+
parseDiagnosticPathToken,
|
|
133
136
|
projectUsageReport,
|
|
134
137
|
} from '@vrtmrz/livesync-commonlib/diagnostics';
|
|
135
138
|
|
|
136
139
|
const result = await analyseDatabaseUsage({ source: createLocalDiagnosticSource(localPouchDB) });
|
|
137
|
-
const
|
|
140
|
+
const mapper = createDiagnosticNameMapper('pseudonym');
|
|
141
|
+
const report = projectUsageReport(result, mapper);
|
|
138
142
|
if (report.status === 'complete') {
|
|
139
143
|
const tsv = formatUsageTSV(report);
|
|
140
144
|
// Present the report through the host's own UI or output stream.
|
|
141
145
|
}
|
|
142
146
|
```
|
|
143
147
|
|
|
148
|
+
Path pseudonyms include the report salt. A consumer can parse that token and supply its salt to a new mapper for a targeted follow-up against exact local Metadata paths:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const pathToken = report.rows[0]?.path;
|
|
152
|
+
const parsed = pathToken === undefined ? undefined : parseDiagnosticPathToken(pathToken);
|
|
153
|
+
if (parsed) {
|
|
154
|
+
const followUpMapper = createDiagnosticNameMapper('pseudonym', parsed.salt);
|
|
155
|
+
const candidateToken = followUpMapper.path(storedVaultRelativePath);
|
|
156
|
+
// Compare this token with the path pseudonym on this device.
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The parser accepts only complete path tokens; it does not reverse them to recover a path. The salt is public, so a guessed path can be checked. Older ordinal pseudonyms have no corresponding lookup.
|
|
161
|
+
|
|
144
162
|
## Chunk ID calculation
|
|
145
163
|
|
|
146
164
|
The `/hashing` entry exports `HashManager` and its `HashManagerCoreOptions` type. It uses the same implementation as database writes, including legacy algorithms and independent ID keys. Hosts supply their settings service and await `initialise()` before calling `computeHash(piece)`.
|
|
@@ -6,6 +6,8 @@ import { decompressDoc } from "../pouchdb/compress.js";
|
|
|
6
6
|
import { AuthorizationHeaderGenerator, generateCredentialObject } from "../replication/httplib.js";
|
|
7
7
|
import { base64ToArrayBuffer } from "../string_and_binary/convert.js";
|
|
8
8
|
import { DiagnosticReadError } from "./types.js";
|
|
9
|
+
const MAX_BULK_DOCUMENTS = 64;
|
|
10
|
+
const MAX_BULK_URL_LENGTH = 6e3;
|
|
9
11
|
function createCouchDBDiagnosticSource({
|
|
10
12
|
settings: originalSettings,
|
|
11
13
|
fetch,
|
|
@@ -30,13 +32,16 @@ function createCouchDBDiagnosticSource({
|
|
|
30
32
|
const checkCancelled = () => {
|
|
31
33
|
if (controller.signal.aborted) throw new DOMException("Diagnostic cancelled", "AbortError");
|
|
32
34
|
};
|
|
35
|
+
const requestURL = (path, parameters = {}) => {
|
|
36
|
+
const requestURL2 = new URL(`${root}${path}`);
|
|
37
|
+
for (const [key, value] of Object.entries(parameters)) requestURL2.searchParams.set(key, value);
|
|
38
|
+
return requestURL2.toString();
|
|
39
|
+
};
|
|
33
40
|
const readJSON = async (path, parameters = {}) => {
|
|
34
41
|
checkCancelled();
|
|
35
|
-
const requestURL = new URL(`${root}${path}`);
|
|
36
|
-
for (const [key, value] of Object.entries(parameters)) requestURL.searchParams.set(key, value);
|
|
37
42
|
headers.set("authorization", await auth.getAuthorizationHeader(credentials));
|
|
38
43
|
checkCancelled();
|
|
39
|
-
const response = await fetch(requestURL
|
|
44
|
+
const response = await fetch(requestURL(path, parameters), {
|
|
40
45
|
method: "GET",
|
|
41
46
|
headers,
|
|
42
47
|
cache: "no-store",
|
|
@@ -73,10 +78,20 @@ function createCouchDBDiagnosticSource({
|
|
|
73
78
|
() => {
|
|
74
79
|
}
|
|
75
80
|
).outgoing : async (doc) => doc;
|
|
76
|
-
|
|
81
|
+
const decode = async (id, value) => {
|
|
82
|
+
if (!value || typeof value !== "object" || !("_id" in value) || value._id !== id || !("_rev" in value) || typeof value._rev !== "string") {
|
|
83
|
+
throw new DiagnosticReadError("document-unreadable");
|
|
84
|
+
}
|
|
85
|
+
const decoded = await decompressDoc(await decrypt(value));
|
|
86
|
+
if ("e_" in decoded && decoded.e_ === true || "path" in decoded && typeof decoded.path === "string" && (isEncryptedMeta(decoded) || isObfuscatedEntry(decoded) && isPathProbablyObfuscated(decoded.path)))
|
|
87
|
+
throw new DiagnosticReadError("document-unreadable");
|
|
88
|
+
checkCancelled();
|
|
89
|
+
return decoded;
|
|
90
|
+
};
|
|
91
|
+
const source = {
|
|
77
92
|
async info() {
|
|
78
93
|
const info = await readJSON("");
|
|
79
|
-
return { update_seq: info.update_seq };
|
|
94
|
+
return { update_seq: info.update_seq, doc_count: info.doc_count };
|
|
80
95
|
},
|
|
81
96
|
async *enumerateIds() {
|
|
82
97
|
let previous;
|
|
@@ -106,16 +121,73 @@ function createCouchDBDiagnosticSource({
|
|
|
106
121
|
if (options.conflicts) parameters.conflicts = "true";
|
|
107
122
|
if (options.revs) parameters.revs = "true";
|
|
108
123
|
if (options.revs_info) parameters.revs_info = "true";
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
return await decode(id, await readJSON(`/${encodeURIComponent(id)}`, parameters));
|
|
125
|
+
},
|
|
126
|
+
async getMany(ids, options = {}) {
|
|
127
|
+
const result = [];
|
|
128
|
+
const parameters = (keys) => ({
|
|
129
|
+
keys: JSON.stringify(keys),
|
|
130
|
+
include_docs: "true",
|
|
131
|
+
...options.conflicts ? { conflicts: "true" } : {}
|
|
132
|
+
});
|
|
133
|
+
const readBatch = async (keys) => {
|
|
134
|
+
if (!keys.length) return;
|
|
135
|
+
let rows;
|
|
136
|
+
try {
|
|
137
|
+
const page = await readJSON("/_all_docs", parameters(keys));
|
|
138
|
+
if (!Array.isArray(page.rows)) throw new DiagnosticReadError("document-unreadable");
|
|
139
|
+
rows = page.rows;
|
|
140
|
+
} catch {
|
|
141
|
+
checkCancelled();
|
|
142
|
+
result.push(...keys.map((id) => ({ id, error: new DiagnosticReadError("document-unreadable") })));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (let offset = 0; offset < keys.length; offset += 4) {
|
|
146
|
+
checkCancelled();
|
|
147
|
+
const decoded = await Promise.allSettled(
|
|
148
|
+
keys.slice(offset, offset + 4).map(async (id, index) => {
|
|
149
|
+
try {
|
|
150
|
+
const row = rows[offset + index];
|
|
151
|
+
if (!row || row.key !== id) throw new DiagnosticReadError("document-unreadable");
|
|
152
|
+
if (row.error === "not_found" || row.value?.deleted)
|
|
153
|
+
throw new DiagnosticReadError("document-unreadable", true);
|
|
154
|
+
if (row.error || row.id !== id) throw new DiagnosticReadError("document-unreadable");
|
|
155
|
+
return { id, document: await decode(id, row.doc) };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
checkCancelled();
|
|
158
|
+
return {
|
|
159
|
+
id,
|
|
160
|
+
error: error instanceof DiagnosticReadError ? error : new DiagnosticReadError("document-unreadable")
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
checkCancelled();
|
|
166
|
+
for (const outcome of decoded) {
|
|
167
|
+
if (outcome.status === "rejected") throw outcome.reason;
|
|
168
|
+
result.push(outcome.value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
let pending = [];
|
|
173
|
+
for (const id of ids) {
|
|
174
|
+
checkCancelled();
|
|
175
|
+
if (pending.length >= MAX_BULK_DOCUMENTS || requestURL("/_all_docs", parameters([...pending, id])).length > MAX_BULK_URL_LENGTH) {
|
|
176
|
+
await readBatch(pending);
|
|
177
|
+
pending = [];
|
|
178
|
+
}
|
|
179
|
+
if (requestURL("/_all_docs", parameters([id])).length > MAX_BULK_URL_LENGTH) {
|
|
180
|
+
try {
|
|
181
|
+
result.push({ id, document: await source.get(id, options) });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
checkCancelled();
|
|
184
|
+
result.push({ id, error });
|
|
185
|
+
}
|
|
186
|
+
} else pending.push(id);
|
|
116
187
|
}
|
|
188
|
+
await readBatch(pending);
|
|
117
189
|
checkCancelled();
|
|
118
|
-
return
|
|
190
|
+
return result;
|
|
119
191
|
},
|
|
120
192
|
close() {
|
|
121
193
|
signal?.removeEventListener("abort", abort);
|
|
@@ -123,6 +195,7 @@ function createCouchDBDiagnosticSource({
|
|
|
123
195
|
return Promise.resolve();
|
|
124
196
|
}
|
|
125
197
|
};
|
|
198
|
+
return source;
|
|
126
199
|
}
|
|
127
200
|
export {
|
|
128
201
|
createCouchDBDiagnosticSource
|
|
@@ -4,7 +4,7 @@ export { analyseDatabaseIntegrity } from "./integrity.js";
|
|
|
4
4
|
export { createLocalDiagnosticSource } from "./local.js";
|
|
5
5
|
export { createCouchDBDiagnosticSource } from "./couchdb.js";
|
|
6
6
|
export type { CouchDBDiagnosticSourceOptions } from "./couchdb.js";
|
|
7
|
-
export { createDiagnosticNameMapper, projectIntegrityReport, projectUsageReport, formatDiagnosticJSON, formatIntegrityMarkdown, formatUsageTSV, } from "./reports.js";
|
|
7
|
+
export { createDiagnosticNameMapper, parseDiagnosticPathToken, projectIntegrityReport, projectUsageReport, formatDiagnosticJSON, formatIntegrityMarkdown, formatUsageTSV, } from "./reports.js";
|
|
8
8
|
export type { DiagnosticNameMapper, DiagnosticReportVersions, ProjectedDiagnosticReport, ProjectedDatabaseIntegrityReport, ProjectedDatabaseUsageReport, } from "./reports.js";
|
|
9
9
|
export { DiagnosticReadError } from "./types.js";
|
|
10
|
-
export type { DiagnosticStatus, DiagnosticNameMode, DiagnosticSourceName, DiagnosticReason, DiagnosticReadOptions, DiagnosticDatabaseSource, DiagnosticError, DiagnosticContent, DiagnosticCoverage, DiagnosticObservation, DiagnosticIdentitySettings, DatabaseIntegrityResult, DatabaseIntegrityOptions, } from "./types.js";
|
|
10
|
+
export type { DiagnosticStatus, DiagnosticNameMode, DiagnosticSourceName, DiagnosticReason, DiagnosticReadOptions, DiagnosticDocumentReadResult, DiagnosticDatabaseSource, DiagnosticError, DiagnosticContent, DiagnosticCoverage, DiagnosticObservation, DiagnosticPathEquivalenceGroup, DiagnosticIdentitySettings, DatabaseIntegrityResult, DatabaseIntegrityOptions, DatabaseIntegrityProgress, DatabaseIntegrityLog, } from "./types.js";
|
|
@@ -4,6 +4,7 @@ import { createLocalDiagnosticSource } from "./local.js";
|
|
|
4
4
|
import { createCouchDBDiagnosticSource } from "./couchdb.js";
|
|
5
5
|
import {
|
|
6
6
|
createDiagnosticNameMapper,
|
|
7
|
+
parseDiagnosticPathToken,
|
|
7
8
|
projectIntegrityReport,
|
|
8
9
|
projectUsageReport,
|
|
9
10
|
formatDiagnosticJSON,
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
formatDiagnosticJSON,
|
|
22
23
|
formatIntegrityMarkdown,
|
|
23
24
|
formatUsageTSV,
|
|
25
|
+
parseDiagnosticPathToken,
|
|
24
26
|
projectIntegrityReport,
|
|
25
27
|
projectUsageReport
|
|
26
28
|
};
|