@vrtmrz/livesync-commonlib 0.1.43 → 0.2.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/README.md +57 -6
- package/dist/API/DirectFileManipulatorV2.d.ts +2 -0
- package/dist/API/DirectFileManipulatorV2.js +3 -0
- package/dist/common/configForDoc.d.ts +1 -0
- package/dist/common/configForDoc.js +11 -1
- package/dist/common/models/db.definition.d.ts +1 -0
- package/dist/common/models/setting.const.preferred.js +1 -3
- 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/dist/managers/EntryManager/EntryManagerImpls.js +3 -2
- package/dist/pouchdb/LiveSyncDBFunctions.js +4 -0
- package/dist/pouchdb/StreamingFetch.d.ts +5 -1
- package/dist/pouchdb/StreamingFetch.js +529 -65
- package/dist/replication/RemoteResource.d.ts +23 -2
- package/dist/replication/RemoteResource.js +6 -2
- package/dist/replication/couchdb/LiveSyncReplicator.d.ts +8 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +132 -1
- package/dist/replication/couchdb/fastForward.d.ts +19 -0
- package/dist/replication/couchdb/fastForward.js +119 -0
- package/dist/replication/index.d.ts +3 -1
- package/dist/replication/joinedDevices.d.ts +25 -0
- package/dist/replication/joinedDevices.js +123 -0
- package/dist/replication/journal/JournalSyncCore.d.ts +2 -1
- package/dist/replication/journal/JournalSyncCore.js +11 -1
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +4 -0
- package/dist/replication/journal/LiveSyncJournalReplicator.js +56 -1
- package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +3 -0
- package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -1
- package/dist/replication/journal/objectstore/MinioStorageAdapter.js +27 -1
- package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +1 -1
- package/dist/replication/trystero/P2PRoomSessionOwner.js +27 -14
- package/dist/serviceModules/Rebuilder.js +41 -15
- package/dist/services/BrowserServices.d.ts +4 -2
- package/dist/services/BrowserServices.js +2 -5
- package/dist/services/HeadlessServices.d.ts +3 -0
- package/dist/services/HeadlessServices.js +2 -5
- package/dist/services/ServiceHub.d.ts +4 -4
- package/dist/services/base/IService.d.ts +10 -18
- package/dist/services/base/ReplicatorService.remoteResourceResolver.js +1 -1
- package/dist/services/base/SettingService.d.ts +8 -18
- package/dist/services/base/SettingService.js +28 -46
- package/dist/services/implements/base/SvelteDialog.d.ts +2 -2
- package/dist/services/implements/base/UIService.d.ts +2 -0
- package/dist/services/implements/base/UIService.js +5 -0
- package/dist/services/implements/injectable/InjectableServiceHub.d.ts +4 -4
- package/dist/services/implements/injectable/InjectableServices.d.ts +2 -2
- package/dist/services/implements/injectable/InjectableSettingService.d.ts +0 -3
- package/dist/services/implements/injectable/InjectableSettingService.js +0 -11
- package/dist/string_and_binary/chunks.js +7 -5
- package/dist/uiCommands.d.ts +26 -0
- package/dist/uiCommands.js +57 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/database-diagnostics.md +45 -0
- package/docs/development.md +15 -3
- package/docs/proven-in-use.md +45 -5
- package/docs/releasing.md +20 -13
- package/docs/settings-lifecycle.md +37 -5
- package/package.json +13 -17
- package/dist/services/base/ConfigService.d.ts +0 -7
- package/dist/services/base/ConfigService.js +0 -6
- package/dist/services/implements/browser/ConfigServiceBrowserCompat.d.ts +0 -16
- package/dist/services/implements/browser/ConfigServiceBrowserCompat.js +0 -23
|
@@ -32,6 +32,12 @@ const INITIAL_COVERAGE = {
|
|
|
32
32
|
function isRecord(value) {
|
|
33
33
|
return value !== null && typeof value === "object";
|
|
34
34
|
}
|
|
35
|
+
function isFiniteNumber(value) {
|
|
36
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
37
|
+
}
|
|
38
|
+
function isRecordedSize(value) {
|
|
39
|
+
return isFiniteNumber(value) && value >= 0;
|
|
40
|
+
}
|
|
35
41
|
function isCurrentMetadata(value) {
|
|
36
42
|
return isRecord(value) && (value.type === EntryTypes.NOTE_BINARY || value.type === EntryTypes.NOTE_PLAIN);
|
|
37
43
|
}
|
|
@@ -52,10 +58,140 @@ function hasUpdateSequence(value) {
|
|
|
52
58
|
function appendError(errors, source, reason, details = {}) {
|
|
53
59
|
errors.push({ source, reason, ...details });
|
|
54
60
|
}
|
|
55
|
-
function
|
|
61
|
+
function concurrencyLimit(options) {
|
|
62
|
+
return Number.isFinite(options.concurrency) ? Math.max(1, Math.min(16, Math.floor(options.concurrency))) : 4;
|
|
63
|
+
}
|
|
64
|
+
class InspectionPhase {
|
|
65
|
+
constructor(options, phase, source) {
|
|
66
|
+
this.options = options;
|
|
67
|
+
this.phase = phase;
|
|
68
|
+
this.source = source;
|
|
69
|
+
this.log("started");
|
|
70
|
+
}
|
|
71
|
+
options;
|
|
72
|
+
phase;
|
|
73
|
+
source;
|
|
74
|
+
processed = 0;
|
|
75
|
+
total;
|
|
76
|
+
started = performance.now();
|
|
77
|
+
lastLog = this.started;
|
|
78
|
+
activeReads = 0;
|
|
79
|
+
peakActiveReads = 0;
|
|
80
|
+
readOperations = 0;
|
|
81
|
+
readDurationMs = 0;
|
|
82
|
+
async read(read, documents = 1) {
|
|
83
|
+
const started = performance.now();
|
|
84
|
+
this.readOperations += documents;
|
|
85
|
+
this.peakActiveReads = Math.max(this.peakActiveReads, ++this.activeReads);
|
|
86
|
+
try {
|
|
87
|
+
return await read();
|
|
88
|
+
} finally {
|
|
89
|
+
this.activeReads--;
|
|
90
|
+
this.readDurationMs += performance.now() - started;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
update() {
|
|
94
|
+
if (this.total !== void 0) this.total = Math.max(this.total, this.processed);
|
|
95
|
+
if (this.phase !== "grouping") {
|
|
96
|
+
try {
|
|
97
|
+
this.options.onProgress?.({
|
|
98
|
+
phase: this.phase,
|
|
99
|
+
source: this.source,
|
|
100
|
+
processed: this.processed,
|
|
101
|
+
total: this.total,
|
|
102
|
+
elapsedMs: Math.round(performance.now() - this.started)
|
|
103
|
+
});
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (performance.now() - this.lastLog >= 5e3) this.log("progress");
|
|
108
|
+
}
|
|
109
|
+
log(event) {
|
|
110
|
+
this.lastLog = performance.now();
|
|
111
|
+
try {
|
|
112
|
+
this.options.onLog?.({
|
|
113
|
+
phase: this.phase,
|
|
114
|
+
source: this.source,
|
|
115
|
+
event,
|
|
116
|
+
processed: this.processed,
|
|
117
|
+
total: this.total,
|
|
118
|
+
elapsedMs: Math.round(this.lastLog - this.started),
|
|
119
|
+
concurrency: concurrencyLimit(this.options),
|
|
120
|
+
activeReads: this.activeReads,
|
|
121
|
+
peakActiveReads: this.peakActiveReads,
|
|
122
|
+
readOperations: this.readOperations,
|
|
123
|
+
readDurationMs: Math.round(this.readDurationMs)
|
|
124
|
+
});
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function inspectBatches(items, options, inspect, merge) {
|
|
130
|
+
const pending = [];
|
|
131
|
+
const drain = async () => {
|
|
132
|
+
const results = await Promise.allSettled(pending.splice(0));
|
|
133
|
+
let failure;
|
|
134
|
+
for (const result of results) {
|
|
135
|
+
if (result.status === "fulfilled") merge(result.value);
|
|
136
|
+
else failure ??= result;
|
|
137
|
+
}
|
|
138
|
+
if (failure) throw failure.reason;
|
|
139
|
+
};
|
|
56
140
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
141
|
+
for await (const item of items) {
|
|
142
|
+
if (cancelled(options)) return false;
|
|
143
|
+
const task = inspect(item);
|
|
144
|
+
void task.catch(() => {
|
|
145
|
+
});
|
|
146
|
+
pending.push(task);
|
|
147
|
+
if (pending.length >= concurrencyLimit(options)) await drain();
|
|
148
|
+
if (cancelled(options)) return false;
|
|
149
|
+
}
|
|
150
|
+
return !cancelled(options);
|
|
151
|
+
} finally {
|
|
152
|
+
await drain();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const DOCUMENT_BATCH_SIZE = 64;
|
|
156
|
+
async function* documentBatches(ids, options) {
|
|
157
|
+
let pending = [];
|
|
158
|
+
let failure;
|
|
159
|
+
try {
|
|
160
|
+
for await (const id of ids) {
|
|
161
|
+
if (cancelled(options)) return;
|
|
162
|
+
pending.push(id);
|
|
163
|
+
if (pending.length === DOCUMENT_BATCH_SIZE) {
|
|
164
|
+
yield pending;
|
|
165
|
+
pending = [];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
failure = { error };
|
|
170
|
+
}
|
|
171
|
+
if (pending.length && !cancelled(options)) yield pending;
|
|
172
|
+
if (failure) throw failure.error;
|
|
173
|
+
}
|
|
174
|
+
async function readMany(source, ids, phase, options) {
|
|
175
|
+
if (!ids.length) return /* @__PURE__ */ new Map();
|
|
176
|
+
try {
|
|
177
|
+
const results = await phase.read(() => source.getMany(ids, options), ids.length);
|
|
178
|
+
const byId = new Map(results.map((result) => [result.id, result]));
|
|
179
|
+
return new Map(
|
|
180
|
+
ids.map((id) => [id, byId.get(id) ?? { id, error: new DiagnosticReadError("document-unreadable") }])
|
|
181
|
+
);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return new Map(ids.map((id) => [id, { id, error }]));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function readResult(result) {
|
|
187
|
+
if ("error" in result) throw result.error;
|
|
188
|
+
return result.document;
|
|
189
|
+
}
|
|
190
|
+
function mergeSourceWork(target, work) {
|
|
191
|
+
target.errors.push(...work.errors);
|
|
192
|
+
target.observations.push(...work.observations);
|
|
193
|
+
for (const key of ["documents", "metadata", "excluded", "conflictBranches", "contentReads"]) {
|
|
194
|
+
target.coverage[key] += work.coverage[key];
|
|
59
195
|
}
|
|
60
196
|
}
|
|
61
197
|
function cancelled(options) {
|
|
@@ -98,6 +234,7 @@ async function classifyObservation(options, run, document, branch, conflictCount
|
|
|
98
234
|
return void 0;
|
|
99
235
|
}
|
|
100
236
|
const rawPath = document.path;
|
|
237
|
+
const recordedSize = isRecordedSize(document.size) ? document.size : void 0;
|
|
101
238
|
const mismatch = identity.status === MetadataDocumentIdentityStatuses.UNRESOLVED ? identity.diagnostic.reason : void 0;
|
|
102
239
|
const report = {
|
|
103
240
|
reference,
|
|
@@ -113,6 +250,16 @@ async function classifyObservation(options, run, document, branch, conflictCount
|
|
|
113
250
|
pathIsNFC: rawPath === rawPath.normalize("NFC"),
|
|
114
251
|
caseEquivalentPaths: false,
|
|
115
252
|
nfcEquivalentPaths: false,
|
|
253
|
+
metadata: {
|
|
254
|
+
type: document.type,
|
|
255
|
+
...recordedSize !== void 0 ? { recordedSize } : {},
|
|
256
|
+
chunkReferences: document.children.length,
|
|
257
|
+
uniqueChunkReferences: new Set(document.children).size,
|
|
258
|
+
embeddedChunkReferences: document.children.reduce(
|
|
259
|
+
(count, child) => count + (getEdenChunk(document, child) !== void 0 ? 1 : 0),
|
|
260
|
+
0
|
|
261
|
+
)
|
|
262
|
+
},
|
|
116
263
|
content: {
|
|
117
264
|
status: "not-checked",
|
|
118
265
|
chunksRead: 0,
|
|
@@ -136,106 +283,121 @@ async function classifyObservation(options, run, document, branch, conflictCount
|
|
|
136
283
|
return void 0;
|
|
137
284
|
}
|
|
138
285
|
}
|
|
139
|
-
async function
|
|
286
|
+
async function inspectMetadataDocument(options, run, id, phase, prefetched) {
|
|
287
|
+
if (cancelled(options)) return;
|
|
288
|
+
run.coverage.documents++;
|
|
289
|
+
if (isSkippedDocumentId(id)) {
|
|
290
|
+
run.coverage.excluded++;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
let winnerValue;
|
|
294
|
+
try {
|
|
295
|
+
winnerValue = prefetched === void 0 ? await phase.read(() => run.source.get(id, { conflicts: true })) : readResult(prefetched);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (cancelled(options)) return;
|
|
298
|
+
appendError(run.errors, run.name, reasonFromReadError(error, "document-unreadable"), { id });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (cancelled(options)) return;
|
|
302
|
+
if (!isRecord(winnerValue) || winnerValue._id !== id) {
|
|
303
|
+
appendError(run.errors, run.name, "invalid-metadata", { id });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!isCurrentMetadata(winnerValue)) {
|
|
307
|
+
run.coverage.excluded++;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const winner = winnerValue;
|
|
311
|
+
if (!isValidCurrentMetadata(winner)) {
|
|
312
|
+
appendError(run.errors, run.name, "invalid-metadata", {
|
|
313
|
+
id,
|
|
314
|
+
revision: typeof winner._rev === "string" ? winner._rev : void 0
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const conflictRevisions = Array.isArray(winner._conflicts) ? winner._conflicts.filter((revision) => typeof revision === "string") : [];
|
|
319
|
+
await classifyObservation(options, run, winner, "winner", conflictRevisions.length, "pending");
|
|
320
|
+
if (cancelled(options)) return;
|
|
321
|
+
for (const revision of conflictRevisions) {
|
|
322
|
+
if (cancelled(options)) return;
|
|
323
|
+
let branchValue;
|
|
324
|
+
try {
|
|
325
|
+
branchValue = await phase.read(() => run.source.get(id, { rev: revision, conflicts: true }));
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (cancelled(options)) return;
|
|
328
|
+
appendError(run.errors, run.name, reasonFromReadError(error, "revision-unavailable"), {
|
|
329
|
+
id,
|
|
330
|
+
revision
|
|
331
|
+
});
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (cancelled(options)) return;
|
|
335
|
+
if (!isRecord(branchValue) || branchValue._id !== id || branchValue._rev !== revision) {
|
|
336
|
+
appendError(run.errors, run.name, "revision-unavailable", { id, revision });
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (!isCurrentMetadata(branchValue)) {
|
|
340
|
+
run.coverage.excluded++;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
await classifyObservation(options, run, branchValue, "conflict", conflictRevisions.length, "pending");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function scanSource(options, run, nextReference) {
|
|
140
347
|
if (cancelled(options)) return;
|
|
348
|
+
const phase = new InspectionPhase(options, "metadata", run.name);
|
|
141
349
|
try {
|
|
142
350
|
const info = await run.source.info();
|
|
143
351
|
run.startSeq = info.update_seq;
|
|
352
|
+
if (Number.isSafeInteger(info.doc_count) && info.doc_count >= 0) phase.total = info.doc_count;
|
|
144
353
|
if (!hasUpdateSequence(run.startSeq)) appendError(run.errors, run.name, "source-unavailable");
|
|
145
354
|
} catch {
|
|
146
355
|
appendError(run.errors, run.name, "source-unavailable");
|
|
147
356
|
}
|
|
148
|
-
|
|
357
|
+
phase.update();
|
|
149
358
|
let enumerationComplete = false;
|
|
359
|
+
const merge = (work) => {
|
|
360
|
+
for (const observation of work.observations) {
|
|
361
|
+
observation.report.reference = nextReference(run.name);
|
|
362
|
+
observation.report.vault.reference = `vault:${observation.report.reference}`;
|
|
363
|
+
}
|
|
364
|
+
mergeSourceWork(run, work);
|
|
365
|
+
};
|
|
366
|
+
const inspect = async (id, prefetched) => {
|
|
367
|
+
const work = makeSourceRun(run.name, run.source);
|
|
368
|
+
if (typeof id !== "string") appendError(work.errors, run.name, "source-unavailable");
|
|
369
|
+
else await inspectMetadataDocument(options, work, id, phase, prefetched);
|
|
370
|
+
if (!cancelled(options)) {
|
|
371
|
+
phase.processed++;
|
|
372
|
+
phase.update();
|
|
373
|
+
}
|
|
374
|
+
return work;
|
|
375
|
+
};
|
|
150
376
|
try {
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
progress(options, "metadata", metadataProgress.processed);
|
|
160
|
-
if (isSkippedDocumentId(id)) {
|
|
161
|
-
run.coverage.excluded++;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
if (cancelled(options)) return;
|
|
165
|
-
let winnerValue;
|
|
166
|
-
try {
|
|
167
|
-
winnerValue = await run.source.get(id, { conflicts: true });
|
|
168
|
-
} catch (error) {
|
|
169
|
-
if (cancelled(options)) return;
|
|
170
|
-
appendError(run.errors, run.name, reasonFromReadError(error, "document-unreadable"), { id });
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (cancelled(options)) return;
|
|
174
|
-
if (!isRecord(winnerValue) || winnerValue._id !== id) {
|
|
175
|
-
appendError(run.errors, run.name, "invalid-metadata", { id });
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (!isCurrentMetadata(winnerValue)) {
|
|
179
|
-
run.coverage.excluded++;
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
const winner = winnerValue;
|
|
183
|
-
if (!isValidCurrentMetadata(winner)) {
|
|
184
|
-
appendError(run.errors, run.name, "invalid-metadata", {
|
|
185
|
-
id,
|
|
186
|
-
revision: typeof winner._rev === "string" ? winner._rev : void 0
|
|
187
|
-
});
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
const conflictRevisions = Array.isArray(winner._conflicts) ? winner._conflicts.filter((revision) => typeof revision === "string") : [];
|
|
191
|
-
await classifyObservation(
|
|
192
|
-
options,
|
|
193
|
-
run,
|
|
194
|
-
winner,
|
|
195
|
-
"winner",
|
|
196
|
-
conflictRevisions.length,
|
|
197
|
-
nextReference(run.name)
|
|
198
|
-
);
|
|
199
|
-
if (cancelled(options)) return;
|
|
200
|
-
for (const revision of conflictRevisions) {
|
|
201
|
-
if (cancelled(options)) return;
|
|
202
|
-
let branchValue;
|
|
203
|
-
try {
|
|
204
|
-
branchValue = await run.source.get(id, { rev: revision, conflicts: true });
|
|
205
|
-
} catch (error) {
|
|
206
|
-
if (cancelled(options)) return;
|
|
207
|
-
appendError(run.errors, run.name, reasonFromReadError(error, "revision-unavailable"), {
|
|
208
|
-
id,
|
|
209
|
-
revision
|
|
210
|
-
});
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
if (cancelled(options)) return;
|
|
214
|
-
if (!isRecord(branchValue) || branchValue._id !== id || branchValue._rev !== revision) {
|
|
215
|
-
appendError(run.errors, run.name, "revision-unavailable", { id, revision });
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if (!isCurrentMetadata(branchValue)) {
|
|
219
|
-
run.coverage.excluded++;
|
|
220
|
-
continue;
|
|
377
|
+
if (!cancelled(options)) {
|
|
378
|
+
if (run.source.getMany) {
|
|
379
|
+
for await (const ids of documentBatches(run.source.enumerateIds(), options)) {
|
|
380
|
+
if (cancelled(options)) break;
|
|
381
|
+
const targets = ids.filter((id) => typeof id === "string" && !isSkippedDocumentId(id));
|
|
382
|
+
const prefetched = await readMany(run.source, targets, phase, { conflicts: true });
|
|
383
|
+
if (cancelled(options)) break;
|
|
384
|
+
await inspectBatches(ids, options, (id) => inspect(id, prefetched.get(id)), merge);
|
|
221
385
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
branchValue,
|
|
226
|
-
"conflict",
|
|
227
|
-
conflictRevisions.length,
|
|
228
|
-
nextReference(run.name)
|
|
229
|
-
);
|
|
386
|
+
enumerationComplete = !cancelled(options);
|
|
387
|
+
} else {
|
|
388
|
+
enumerationComplete = await inspectBatches(run.source.enumerateIds(), options, inspect, merge);
|
|
230
389
|
}
|
|
231
390
|
}
|
|
232
|
-
enumerationComplete = true;
|
|
233
391
|
} catch {
|
|
234
392
|
if (!cancelled(options)) appendError(run.errors, run.name, "source-unavailable");
|
|
235
393
|
}
|
|
236
|
-
if (cancelled(options))
|
|
237
|
-
|
|
238
|
-
|
|
394
|
+
if (!cancelled(options)) {
|
|
395
|
+
run.metadataScanComplete = enumerationComplete && run.errors.length === 0;
|
|
396
|
+
run.enumerationComplete = enumerationComplete;
|
|
397
|
+
if (enumerationComplete) phase.total = phase.processed;
|
|
398
|
+
}
|
|
399
|
+
phase.update();
|
|
400
|
+
phase.log(cancelled(options) ? "cancelled" : run.metadataScanComplete ? "completed" : "partial");
|
|
239
401
|
}
|
|
240
402
|
function groupObservations(runs) {
|
|
241
403
|
const observations = runs.flatMap((run) => run.observations);
|
|
@@ -277,19 +439,42 @@ function markPathEquivalences(observations) {
|
|
|
277
439
|
for (const observation of observations) {
|
|
278
440
|
const path = observation.report.path;
|
|
279
441
|
const caseFold = path.toLowerCase();
|
|
280
|
-
const casePaths = pathsByCaseFold.get(caseFold) ??
|
|
281
|
-
casePaths.
|
|
442
|
+
const casePaths = pathsByCaseFold.get(caseFold) ?? [];
|
|
443
|
+
casePaths.push(observation);
|
|
282
444
|
pathsByCaseFold.set(caseFold, casePaths);
|
|
283
445
|
const nfc = path.normalize("NFC");
|
|
284
|
-
const nfcPaths = pathsByNfc.get(nfc) ??
|
|
285
|
-
nfcPaths.
|
|
446
|
+
const nfcPaths = pathsByNfc.get(nfc) ?? [];
|
|
447
|
+
nfcPaths.push(observation);
|
|
286
448
|
pathsByNfc.set(nfc, nfcPaths);
|
|
287
449
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
450
|
+
const equivalenceGroups = [];
|
|
451
|
+
for (const [kind, groups] of [
|
|
452
|
+
["case", pathsByCaseFold],
|
|
453
|
+
["nfc", pathsByNfc]
|
|
454
|
+
]) {
|
|
455
|
+
let groupNumber = 0;
|
|
456
|
+
for (const members of groups.values()) {
|
|
457
|
+
const hasDifferentSpellings = new Set(members.map((observation) => observation.report.path)).size > 1;
|
|
458
|
+
if (kind === "case") {
|
|
459
|
+
for (const observation of members) observation.report.caseEquivalentPaths = hasDifferentSpellings;
|
|
460
|
+
} else {
|
|
461
|
+
for (const observation of members) observation.report.nfcEquivalentPaths = hasDifferentSpellings;
|
|
462
|
+
}
|
|
463
|
+
if (!hasDifferentSpellings) continue;
|
|
464
|
+
const reference = `${kind}:${++groupNumber}`;
|
|
465
|
+
if (kind === "case") {
|
|
466
|
+
for (const observation of members) observation.report.caseEquivalentGroup = reference;
|
|
467
|
+
} else {
|
|
468
|
+
for (const observation of members) observation.report.nfcEquivalentGroup = reference;
|
|
469
|
+
}
|
|
470
|
+
equivalenceGroups.push({
|
|
471
|
+
reference,
|
|
472
|
+
kind,
|
|
473
|
+
members: members.map((observation) => observation.report.reference)
|
|
474
|
+
});
|
|
475
|
+
}
|
|
292
476
|
}
|
|
477
|
+
return equivalenceGroups;
|
|
293
478
|
}
|
|
294
479
|
function markSuspectGroups(groups, runs) {
|
|
295
480
|
const crossSourceComparisonAvailable = runs.some((run) => run.name === "local" && run.metadataScanComplete) && runs.some((run) => run.name === "remote" && run.metadataScanComplete);
|
|
@@ -336,7 +521,7 @@ function getEdenChunk(document, id) {
|
|
|
336
521
|
if (!isRecord(chunk) || typeof chunk.data !== "string") return void 0;
|
|
337
522
|
return chunk.data;
|
|
338
523
|
}
|
|
339
|
-
async function readObservationContent(options, observation, run) {
|
|
524
|
+
async function readObservationContent(options, observation, run, phase) {
|
|
340
525
|
run.coverage.contentReads++;
|
|
341
526
|
const unavailableChunks = [];
|
|
342
527
|
const data = [];
|
|
@@ -353,54 +538,63 @@ async function readObservationContent(options, observation, run) {
|
|
|
353
538
|
unavailableChunks
|
|
354
539
|
};
|
|
355
540
|
}
|
|
356
|
-
for (
|
|
541
|
+
for (let offset = 0; offset < observation.document.children.length; offset += DOCUMENT_BATCH_SIZE) {
|
|
357
542
|
if (cancelled(options)) return { content: { status: "not-checked" }, chunksRead, unavailableChunks };
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const chunk = await observation.source.get(child);
|
|
543
|
+
const children = observation.document.children.slice(offset, offset + DOCUMENT_BATCH_SIZE);
|
|
544
|
+
const prefetched = observation.source.getMany ? await readMany(
|
|
545
|
+
observation.source,
|
|
546
|
+
[...new Set(children.filter((child) => getEdenChunk(observation.document, child) === void 0))],
|
|
547
|
+
phase
|
|
548
|
+
) : void 0;
|
|
549
|
+
for (const child of children) {
|
|
366
550
|
if (cancelled(options)) return { content: { status: "not-checked" }, chunksRead, unavailableChunks };
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
revision: observation.report.revision,
|
|
372
|
-
path: observation.report.path,
|
|
373
|
-
chunkId: child
|
|
374
|
-
});
|
|
551
|
+
const eden = getEdenChunk(observation.document, child);
|
|
552
|
+
if (eden !== void 0) {
|
|
553
|
+
data.push(eden);
|
|
554
|
+
chunksRead++;
|
|
375
555
|
continue;
|
|
376
556
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
557
|
+
try {
|
|
558
|
+
const chunk = prefetched ? readResult(prefetched.get(child)) : await phase.read(() => observation.source.get(child));
|
|
559
|
+
if (cancelled(options)) return { content: { status: "not-checked" }, chunksRead, unavailableChunks };
|
|
560
|
+
if (!isValidChunk(chunk, child) || chunk._deleted === true) {
|
|
561
|
+
unavailableChunks.push({ id: child, status: "unreadable" });
|
|
562
|
+
appendError(run.errors, run.name, "chunk-unreadable", {
|
|
563
|
+
id: observation.report.id,
|
|
564
|
+
revision: observation.report.revision,
|
|
565
|
+
path: observation.report.path,
|
|
566
|
+
chunkId: child
|
|
567
|
+
});
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
data.push(chunk.data);
|
|
571
|
+
chunksRead++;
|
|
572
|
+
} catch (error) {
|
|
573
|
+
if (cancelled(options)) return { content: { status: "not-checked" }, chunksRead, unavailableChunks };
|
|
574
|
+
if (isMissingRead(error)) {
|
|
575
|
+
const eden2 = getEdenChunk(observation.document, child);
|
|
576
|
+
if (eden2 !== void 0) {
|
|
577
|
+
data.push(eden2);
|
|
578
|
+
chunksRead++;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
unavailableChunks.push({ id: child, status: "missing" });
|
|
582
|
+
appendError(run.errors, run.name, reasonFromReadError(error, "chunk-missing"), {
|
|
583
|
+
id: observation.report.id,
|
|
584
|
+
revision: observation.report.revision,
|
|
585
|
+
path: observation.report.path,
|
|
586
|
+
chunkId: child
|
|
587
|
+
});
|
|
386
588
|
continue;
|
|
387
589
|
}
|
|
388
|
-
unavailableChunks.push({ id: child, status: "
|
|
389
|
-
appendError(run.errors, run.name, reasonFromReadError(error, "chunk-
|
|
590
|
+
unavailableChunks.push({ id: child, status: "unreadable" });
|
|
591
|
+
appendError(run.errors, run.name, reasonFromReadError(error, "chunk-unreadable"), {
|
|
390
592
|
id: observation.report.id,
|
|
391
593
|
revision: observation.report.revision,
|
|
392
594
|
path: observation.report.path,
|
|
393
595
|
chunkId: child
|
|
394
596
|
});
|
|
395
|
-
continue;
|
|
396
597
|
}
|
|
397
|
-
unavailableChunks.push({ id: child, status: "unreadable" });
|
|
398
|
-
appendError(run.errors, run.name, reasonFromReadError(error, "chunk-unreadable"), {
|
|
399
|
-
id: observation.report.id,
|
|
400
|
-
revision: observation.report.revision,
|
|
401
|
-
path: observation.report.path,
|
|
402
|
-
chunkId: child
|
|
403
|
-
});
|
|
404
598
|
}
|
|
405
599
|
}
|
|
406
600
|
if (unavailableChunks.length > 0) {
|
|
@@ -451,10 +645,10 @@ function relation(left, right) {
|
|
|
451
645
|
}
|
|
452
646
|
return "equal";
|
|
453
647
|
}
|
|
454
|
-
async function readVaultContent(options, path, errors) {
|
|
648
|
+
async function readVaultContent(options, path, errors, phase) {
|
|
455
649
|
if (cancelled(options)) return { status: "not-checked" };
|
|
456
650
|
try {
|
|
457
|
-
const content = await options.readStorage(path);
|
|
651
|
+
const content = await phase.read(() => options.readStorage(path));
|
|
458
652
|
if (cancelled(options)) return { status: "not-checked" };
|
|
459
653
|
if (content.status === "unreadable" || content.reason) {
|
|
460
654
|
appendError(errors, "storage", content.reason ?? "content-unreadable", { path });
|
|
@@ -467,7 +661,7 @@ async function readVaultContent(options, path, errors) {
|
|
|
467
661
|
return { status: "unreadable", reason };
|
|
468
662
|
}
|
|
469
663
|
}
|
|
470
|
-
async function inspectGroupContent(options, group, runsByName, errors,
|
|
664
|
+
async function inspectGroupContent(options, group, runsByName, errors, phase) {
|
|
471
665
|
if (!group.suspect) return;
|
|
472
666
|
const byRawPath = /* @__PURE__ */ new Map();
|
|
473
667
|
for (const observation of group.observations) {
|
|
@@ -489,32 +683,42 @@ async function inspectGroupContent(options, group, runsByName, errors, contentPr
|
|
|
489
683
|
if (cancelled(options)) return;
|
|
490
684
|
const run = runsByName.get(observation.report.source);
|
|
491
685
|
if (!run) continue;
|
|
492
|
-
const outcome = await readObservationContent(options, observation, run);
|
|
686
|
+
const outcome = await readObservationContent(options, observation, run, phase);
|
|
687
|
+
const decodedSize = outcome.content.status === "available" ? outcome.content.bytes.length : void 0;
|
|
688
|
+
const recordedSize = observation.report.metadata?.recordedSize;
|
|
493
689
|
observation.report.content = {
|
|
494
690
|
status: outcome.content.status,
|
|
495
691
|
...outcome.content.reason ? { reason: outcome.content.reason } : {},
|
|
496
692
|
chunksRead: outcome.chunksRead,
|
|
497
|
-
unavailableChunks: outcome.unavailableChunks
|
|
693
|
+
unavailableChunks: outcome.unavailableChunks,
|
|
694
|
+
...decodedSize !== void 0 ? {
|
|
695
|
+
size: decodedSize,
|
|
696
|
+
...recordedSize !== void 0 ? { recordedSizeDifference: decodedSize - recordedSize } : {}
|
|
697
|
+
} : {}
|
|
498
698
|
};
|
|
499
699
|
contentByObservation.set(observation, outcome.content);
|
|
500
700
|
contentByReference.set(observation.report.reference, outcome.content);
|
|
501
|
-
contentProgress.processed++;
|
|
502
|
-
progress(options, "content", contentProgress.processed);
|
|
503
701
|
if (cancelled(options)) return;
|
|
504
702
|
}
|
|
505
703
|
const vaultByPath = /* @__PURE__ */ new Map();
|
|
506
704
|
for (const [path] of byRawPath) {
|
|
507
705
|
if (cancelled(options)) return;
|
|
508
|
-
const vaultContent = await readVaultContent(options, path, errors);
|
|
706
|
+
const vaultContent = await readVaultContent(options, path, errors, phase);
|
|
509
707
|
vaultByPath.set(path, vaultContent);
|
|
510
708
|
const vaultReference = vaultReferences.get(path);
|
|
511
709
|
if (vaultReference) contentByReference.set(vaultReference, vaultContent);
|
|
512
710
|
for (const observation of group.observations) {
|
|
513
711
|
if (observation.report.path !== path) continue;
|
|
712
|
+
const observationContent = contentByObservation.get(observation);
|
|
514
713
|
observation.report.vault = {
|
|
515
714
|
reference: vaultReference ?? `vault:${observation.report.reference}`,
|
|
516
715
|
status: vaultContent.status,
|
|
517
|
-
...vaultContent.reason ? { reason: vaultContent.reason } : {}
|
|
716
|
+
...vaultContent.reason ? { reason: vaultContent.reason } : {},
|
|
717
|
+
...vaultContent.status === "available" ? {
|
|
718
|
+
size: vaultContent.bytes.length,
|
|
719
|
+
...observationContent?.status === "available" ? { contentSizeDifference: vaultContent.bytes.length - observationContent.bytes.length } : {},
|
|
720
|
+
...isFiniteNumber(vaultContent.mtime) && isFiniteNumber(observation.document.mtime) ? { mtimeDifferenceMs: vaultContent.mtime - observation.document.mtime } : {}
|
|
721
|
+
} : {}
|
|
518
722
|
};
|
|
519
723
|
}
|
|
520
724
|
if (cancelled(options)) return;
|
|
@@ -540,12 +744,11 @@ function determineStatus(wasCancelled, errors, runs, configurationChanged) {
|
|
|
540
744
|
async function analyseDatabaseIntegrity(options) {
|
|
541
745
|
const errors = [];
|
|
542
746
|
const runs = [];
|
|
543
|
-
const metadataProgress = { processed: 0 };
|
|
544
|
-
const contentProgress = { processed: 0 };
|
|
545
747
|
const referenceNumbers = { local: 0, remote: 0 };
|
|
546
748
|
const nextReference = (source) => `${source}:${++referenceNumbers[source]}`;
|
|
547
749
|
let wasCancelled = cancelled(options);
|
|
548
750
|
let configurationChanged = false;
|
|
751
|
+
let pathEquivalenceGroups = [];
|
|
549
752
|
let ownedRemote;
|
|
550
753
|
if (!wasCancelled && !isConfigurationCurrent(options)) {
|
|
551
754
|
configurationChanged = true;
|
|
@@ -555,8 +758,8 @@ async function analyseDatabaseIntegrity(options) {
|
|
|
555
758
|
if (!wasCancelled && !configurationChanged) {
|
|
556
759
|
const localRun = makeSourceRun("local", options.local);
|
|
557
760
|
runs.push(localRun);
|
|
558
|
-
await scanSource(options, localRun, nextReference
|
|
559
|
-
if (!cancelled(options)) {
|
|
761
|
+
await scanSource(options, localRun, nextReference);
|
|
762
|
+
if (!cancelled(options) && options.includeRemote !== false) {
|
|
560
763
|
if (!options.remote) {
|
|
561
764
|
const remoteRun = { ...makeSourceRun("remote", options.local), source: options.local };
|
|
562
765
|
remoteRun.coverage.status = "failed";
|
|
@@ -571,7 +774,7 @@ async function analyseDatabaseIntegrity(options) {
|
|
|
571
774
|
} else {
|
|
572
775
|
const remoteRun = makeSourceRun("remote", ownedRemote);
|
|
573
776
|
runs.push(remoteRun);
|
|
574
|
-
await scanSource(options, remoteRun, nextReference
|
|
777
|
+
await scanSource(options, remoteRun, nextReference);
|
|
575
778
|
}
|
|
576
779
|
} catch {
|
|
577
780
|
if (cancelled(options)) {
|
|
@@ -588,21 +791,43 @@ async function analyseDatabaseIntegrity(options) {
|
|
|
588
791
|
}
|
|
589
792
|
if (cancelled(options)) wasCancelled = true;
|
|
590
793
|
if (!wasCancelled) {
|
|
794
|
+
const grouping = new InspectionPhase(options, "grouping");
|
|
591
795
|
const groups = groupObservations(runs);
|
|
592
|
-
markPathEquivalences(runs.flatMap((run) => run.observations));
|
|
796
|
+
pathEquivalenceGroups = markPathEquivalences(runs.flatMap((run) => run.observations));
|
|
593
797
|
markSuspectGroups(groups, runs);
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
798
|
+
grouping.processed = groups.length;
|
|
799
|
+
grouping.total = groups.length;
|
|
800
|
+
grouping.log("completed");
|
|
801
|
+
const suspects = groups.filter((group) => group.suspect);
|
|
802
|
+
const content = new InspectionPhase(options, "content");
|
|
803
|
+
content.total = suspects.length;
|
|
804
|
+
content.update();
|
|
805
|
+
await inspectBatches(
|
|
806
|
+
suspects,
|
|
807
|
+
options,
|
|
808
|
+
async (group) => {
|
|
809
|
+
const work = runs.map((run) => makeSourceRun(run.name, run.source));
|
|
810
|
+
const groupErrors = [];
|
|
811
|
+
await inspectGroupContent(
|
|
812
|
+
options,
|
|
813
|
+
group,
|
|
814
|
+
new Map(work.map((run) => [run.name, run])),
|
|
815
|
+
groupErrors,
|
|
816
|
+
content
|
|
817
|
+
);
|
|
818
|
+
if (!cancelled(options)) {
|
|
819
|
+
content.processed++;
|
|
820
|
+
content.update();
|
|
821
|
+
}
|
|
822
|
+
return { work, groupErrors };
|
|
823
|
+
},
|
|
824
|
+
({ work, groupErrors }) => {
|
|
825
|
+
for (let index = 0; index < runs.length; index++) mergeSourceWork(runs[index], work[index]);
|
|
826
|
+
errors.push(...groupErrors);
|
|
604
827
|
}
|
|
605
|
-
|
|
828
|
+
);
|
|
829
|
+
if (cancelled(options)) wasCancelled = true;
|
|
830
|
+
content.log(wasCancelled ? "cancelled" : "completed");
|
|
606
831
|
}
|
|
607
832
|
}
|
|
608
833
|
if (!wasCancelled && !cancelled(options)) {
|
|
@@ -660,8 +885,10 @@ async function analyseDatabaseIntegrity(options) {
|
|
|
660
885
|
settings: { ...options.settings },
|
|
661
886
|
coverage,
|
|
662
887
|
observations: allObservations,
|
|
888
|
+
...pathEquivalenceGroups.length > 0 ? { pathEquivalenceGroups } : {},
|
|
663
889
|
errors,
|
|
664
890
|
scope: {
|
|
891
|
+
remote: options.includeRemote === false ? "excluded" : "included",
|
|
665
892
|
metadata: "normal-files-and-current-conflicts",
|
|
666
893
|
content: "suspect-groups-only",
|
|
667
894
|
history: "not-checked",
|