@vrtmrz/livesync-commonlib 0.1.41 → 0.1.43

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 (74) hide show
  1. package/README.md +29 -1
  2. package/dist/API/DirectFileManipulatorV2.js +16 -1
  3. package/dist/API/processSetting.d.ts +1 -0
  4. package/dist/API/processSetting.js +6 -1
  5. package/dist/common/fileContentHash.d.ts +2 -0
  6. package/dist/common/fileContentHash.js +18 -0
  7. package/dist/common/models/setting.const.defaults.js +1 -0
  8. package/dist/common/models/setting.const.qr.js +1 -0
  9. package/dist/common/models/setting.policy.d.ts +4 -0
  10. package/dist/common/models/setting.policy.js +1 -0
  11. package/dist/common/models/setting.type.d.ts +8 -1
  12. package/dist/common/models/shared.definition.configNames.js +5 -0
  13. package/dist/common/models/tweak.definition.d.ts +6 -0
  14. package/dist/common/models/tweak.definition.js +1 -0
  15. package/dist/common/pathIdentity.d.ts +19 -0
  16. package/dist/common/pathIdentity.js +35 -0
  17. package/dist/common/pathNormalization.d.ts +7 -0
  18. package/dist/common/pathNormalization.js +13 -0
  19. package/dist/common/settingConstants.d.ts +1 -0
  20. package/dist/diagnostics/couchdb.d.ts +15 -0
  21. package/dist/diagnostics/couchdb.js +129 -0
  22. package/dist/diagnostics/index.d.ts +10 -0
  23. package/dist/diagnostics/index.js +26 -0
  24. package/dist/diagnostics/integrity.d.ts +5 -0
  25. package/dist/diagnostics/integrity.js +674 -0
  26. package/dist/diagnostics/local.d.ts +4 -0
  27. package/dist/diagnostics/local.js +25 -0
  28. package/dist/diagnostics/reports.d.ts +108 -0
  29. package/dist/diagnostics/reports.js +353 -0
  30. package/dist/diagnostics/types.d.ts +130 -0
  31. package/dist/diagnostics/types.js +13 -0
  32. package/dist/diagnostics/usage.d.ts +51 -0
  33. package/dist/diagnostics/usage.js +241 -0
  34. package/dist/hashing.d.ts +1 -0
  35. package/dist/hashing.js +3 -1
  36. package/dist/interfaces/FileReflectionProvenance.d.ts +4 -0
  37. package/dist/managers/EntryManager/EntryManager.d.ts +1 -0
  38. package/dist/managers/EntryManager/EntryManager.js +9 -0
  39. package/dist/pouchdb/StreamingFetch.d.ts +3 -1
  40. package/dist/pouchdb/StreamingFetch.js +3 -1
  41. package/dist/pouchdb/encryption.d.ts +3 -1
  42. package/dist/pouchdb/encryption.js +38 -30
  43. package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
  44. package/dist/pouchdb/pathIdentityCompatibility.js +64 -0
  45. package/dist/pouchdb/remoteFeatureCompatibility.d.ts +2 -0
  46. package/dist/pouchdb/remoteFeatureCompatibility.js +9 -1
  47. package/dist/replication/couchdb/LiveSyncReplicator.js +11 -0
  48. package/dist/replication/index.d.ts +1 -1
  49. package/dist/replication/index.js +2 -0
  50. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +2 -0
  51. package/dist/replication/journal/LiveSyncJournalReplicator.js +28 -6
  52. package/dist/replication/trystero/P2PRoomSessionOwner.js +18 -5
  53. package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
  54. package/dist/replication/trystero/useP2PReplicatorFeature.js +2 -1
  55. package/dist/serviceFeatures/offlineScanner.d.ts +12 -3
  56. package/dist/serviceFeatures/offlineScanner.js +4 -2
  57. package/dist/serviceFeatures/targetFilter.d.ts +1 -1
  58. package/dist/serviceFeatures/targetFilter.js +28 -17
  59. package/dist/serviceModules/Rebuilder.d.ts +5 -1
  60. package/dist/serviceModules/Rebuilder.js +85 -3
  61. package/dist/serviceModules/ServiceFileHandlerBase.d.ts +5 -2
  62. package/dist/serviceModules/ServiceFileHandlerBase.js +110 -49
  63. package/dist/services/base/IService.d.ts +2 -1
  64. package/dist/services/base/PathService.d.ts +3 -3
  65. package/dist/services/base/PathService.js +6 -3
  66. package/dist/settings.d.ts +2 -0
  67. package/dist/settings.js +3 -0
  68. package/dist/worker/bgWorker.js +1 -1
  69. package/docs/conflict-resolution.md +17 -8
  70. package/docs/database-diagnostics.md +24 -0
  71. package/docs/development.md +1 -0
  72. package/docs/proven-in-use.md +11 -1
  73. package/docs/unicode-path-identity.md +98 -0
  74. package/package.json +9 -1
@@ -44,11 +44,14 @@ export declare abstract class ServiceFileHandlerBase extends ServiceModuleBase<S
44
44
  private setting;
45
45
  private vault;
46
46
  private fileReflectionProvenance?;
47
+ private readonly contentHashes;
47
48
  private readonly deferredConflictChecks;
48
49
  constructor(services: ServiceFileHandlerDependencies);
49
50
  get db(): DatabaseFileAccess;
50
51
  get storage(): StorageAccess;
52
+ private getProvenancePath;
51
53
  private getProvenance;
54
+ private contentEvidence;
52
55
  private setProvenance;
53
56
  private deleteProvenance;
54
57
  private findUniqueContentRevision;
@@ -62,8 +65,8 @@ export declare abstract class ServiceFileHandlerBase extends ServiceModuleBase<S
62
65
  * identifies provenance; no match is `unknown`.
63
66
  * `preferredPath` selects the provenance key during a rename, and
64
67
  * `currentLoaded` supplies an already decoded candidate body when available.
65
- * Missing chunks do not clear provenance unless their metadata is also
66
- * missing or deleted.
68
+ * A valid saved content hash can prove unchanged bytes after revision
69
+ * pruning. It cannot reconstruct ancestry for an edited file.
67
70
  */
68
71
  private classifyStorageContent;
69
72
  /** Record provenance only when storage matches one unambiguous live leaf. */
@@ -6,6 +6,7 @@ import {
6
6
  readAsBlob,
7
7
  readContent
8
8
  } from "../common/utils.js";
9
+ import { computeFileContentHash } from "../common/fileContentHash.js";
9
10
  import { EVENT_CONFLICT_CANCELLED } from "../events/coreEvents.js";
10
11
  import { shouldBeIgnored, stripAllPrefixes } from "../string_and_binary/path.js";
11
12
  import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
@@ -13,6 +14,7 @@ import { ServiceModuleBase } from "./ServiceModuleBase.js";
13
14
  import { getStoragePathFromUXFileInfo } from "../common/typeUtils.js";
14
15
  import { EVEN } from "../common/models/shared.const.symbols.js";
15
16
  import { tryGetFilePath } from "../common/utils.doc.js";
17
+ import { getPathNormalizationVersion, normalisePathForIdentity } from "../common/pathNormalization.js";
16
18
  async function serializedByKeys(keys, callback) {
17
19
  const [key, ...remainingKeys] = keys;
18
20
  if (key === void 0) return await callback();
@@ -25,6 +27,12 @@ function getParentPath(path) {
25
27
  function isFolderInfo(info) {
26
28
  return info?.isFolder === true;
27
29
  }
30
+ function hasContentHash(record) {
31
+ return !!record && typeof record.contentHash === "string" && /^sha256:[0-9a-f]{64}$/.test(record.contentHash) && Number.isSafeInteger(record.contentSize) && record.contentSize >= 0;
32
+ }
33
+ function sameProvenance(left, right) {
34
+ return left?.revision === right?.revision && left?.observedStorageMtime === right?.observedStorageMtime && left?.contentHash === right?.contentHash && left?.contentSize === right?.contentSize;
35
+ }
28
36
  class ServiceFileHandlerBase extends ServiceModuleBase {
29
37
  events;
30
38
  databaseFileAccess;
@@ -34,6 +42,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
34
42
  setting;
35
43
  vault;
36
44
  fileReflectionProvenance;
45
+ contentHashes = /* @__PURE__ */ new WeakMap();
37
46
  deferredConflictChecks = /* @__PURE__ */ new Map();
38
47
  constructor(services) {
39
48
  super(services);
@@ -54,28 +63,51 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
54
63
  get storage() {
55
64
  return this.storageAccess;
56
65
  }
66
+ getProvenancePath(path) {
67
+ return normalisePathForIdentity(
68
+ path,
69
+ getPathNormalizationVersion(this.setting.currentSettings())
70
+ );
71
+ }
57
72
  async getProvenance(path) {
58
73
  if (!this.fileReflectionProvenance) return void 0;
59
74
  try {
60
- const record = await this.fileReflectionProvenance.get(path);
75
+ const record = await this.fileReflectionProvenance.get(this.getProvenancePath(path));
61
76
  if (!record) return void 0;
62
77
  const entry = await this.db.fetchEntryMeta(path, record.revision, true);
63
78
  if (entry && !entry._deleted && !entry.deleted) {
64
79
  return record;
65
80
  }
66
- await this.fileReflectionProvenance.delete(path);
81
+ if (!hasContentHash(record)) await this.fileReflectionProvenance.delete(this.getProvenancePath(path));
67
82
  } catch (ex) {
68
83
  this._log(`Could not read file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
69
84
  this._log(ex, LOG_LEVEL_VERBOSE);
70
85
  }
71
86
  return void 0;
72
87
  }
73
- async setProvenance(path, revision, observedStorageMtime) {
88
+ async contentEvidence(content) {
89
+ if (!this.fileReflectionProvenance) return {};
90
+ try {
91
+ let hash = this.contentHashes.get(content);
92
+ if (!hash) {
93
+ hash = computeFileContentHash(content);
94
+ this.contentHashes.set(content, hash);
95
+ }
96
+ return { contentHash: await hash, contentSize: content.size };
97
+ } catch (ex) {
98
+ this.contentHashes.delete(content);
99
+ this._log("Could not hash file reflection content", LOG_LEVEL_VERBOSE);
100
+ this._log(ex, LOG_LEVEL_VERBOSE);
101
+ return {};
102
+ }
103
+ }
104
+ async setProvenance(path, revision, observedStorageMtime, content, evidence) {
74
105
  if (!this.fileReflectionProvenance || !revision) return;
75
106
  try {
76
- await this.fileReflectionProvenance.set(path, {
107
+ await this.fileReflectionProvenance.set(this.getProvenancePath(path), {
77
108
  revision,
78
- observedStorageMtime
109
+ observedStorageMtime,
110
+ ...evidence ?? await this.contentEvidence(content)
79
111
  });
80
112
  } catch (ex) {
81
113
  this._log(`Could not record file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
@@ -85,7 +117,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
85
117
  async deleteProvenance(path) {
86
118
  if (!this.fileReflectionProvenance) return;
87
119
  try {
88
- await this.fileReflectionProvenance.delete(path);
120
+ await this.fileReflectionProvenance.delete(this.getProvenancePath(path));
89
121
  } catch (ex) {
90
122
  this._log(`Could not delete file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
91
123
  this._log(ex, LOG_LEVEL_VERBOSE);
@@ -109,7 +141,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
109
141
  }
110
142
  const matched = await this.findUniqueContentRevision(file);
111
143
  if (matched) {
112
- await this.setProvenance(path, matched, file.stat.mtime);
144
+ await this.setProvenance(path, matched, file.stat.mtime, file.body);
113
145
  return matched;
114
146
  }
115
147
  return void 0;
@@ -123,12 +155,16 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
123
155
  * identifies provenance; no match is `unknown`.
124
156
  * `preferredPath` selects the provenance key during a rename, and
125
157
  * `currentLoaded` supplies an already decoded candidate body when available.
126
- * Missing chunks do not clear provenance unless their metadata is also
127
- * missing or deleted.
158
+ * A valid saved content hash can prove unchanged bytes after revision
159
+ * pruning. It cannot reconstruct ancestry for an edited file.
128
160
  */
129
- async classifyStorageContent(file, currentEntry, preferredPath, recordedRevision, currentLoaded) {
161
+ async classifyStorageContent(file, currentEntry, preferredPath, recorded, currentLoaded) {
130
162
  const path = preferredPath ?? file.path;
163
+ const recordedRevision = recorded?.revision;
131
164
  if (recordedRevision) {
165
+ if (hasContentHash(recorded) && recorded.contentSize === file.body.size && (await this.contentEvidence(file.body)).contentHash === recorded.contentHash) {
166
+ return { kind: "unchanged", revision: recordedRevision };
167
+ }
132
168
  const loadedContentIsDecoded = currentLoaded?._rev === recordedRevision;
133
169
  const base = loadedContentIsDecoded ? currentLoaded : await this.db.fetchEntry(file, recordedRevision, true, true, true);
134
170
  if (base && base._rev === recordedRevision && !("deleted" in base && base.deleted) && !("_deleted" in base && base._deleted)) {
@@ -137,7 +173,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
137
173
  }
138
174
  if (!base) {
139
175
  const meta = await this.db.fetchEntryMeta(file, recordedRevision, true);
140
- if (!meta || meta.deleted || meta._deleted) await this.deleteProvenance(path);
176
+ if ((!meta || meta.deleted || meta._deleted) && !hasContentHash(recorded)) await this.deleteProvenance(path);
141
177
  }
142
178
  }
143
179
  const matches = currentEntry ? await this.db.findLiveContentRevisions(file, file.body) : [];
@@ -147,8 +183,8 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
147
183
  return { kind: "unknown" };
148
184
  }
149
185
  /** Record provenance only when storage matches one unambiguous live leaf. */
150
- async rememberMatchingLeaf(path, revision, mtime) {
151
- if (revision) await this.setProvenance(path, revision, mtime);
186
+ async rememberMatchingLeaf(path, revision, mtime, content, evidence) {
187
+ if (revision) await this.setProvenance(path, revision, mtime, content, evidence);
152
188
  }
153
189
  /**
154
190
  * Read the raw provenance value used by an optimistic storage snapshot.
@@ -157,7 +193,8 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
157
193
  */
158
194
  async readProvenanceSnapshot(path) {
159
195
  try {
160
- return (await this.fileReflectionProvenance?.get(path))?.revision;
196
+ const record = await this.fileReflectionProvenance?.get(this.getProvenancePath(path));
197
+ return record ? { ...record } : void 0;
161
198
  } catch {
162
199
  return void 0;
163
200
  }
@@ -170,7 +207,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
170
207
  const current = await this.storage.getStub(file.path);
171
208
  if (!current || isFolderInfo(current) || current.path !== file.path) return false;
172
209
  const readCurrent = await this.readFileFromStub(current);
173
- return readCurrent.stat.mtime === file.stat.mtime && await isDocContentSame(readCurrent.body, file.body) && await this.readProvenanceSnapshot(file.path) === revision;
210
+ return readCurrent.stat.mtime === file.stat.mtime && await isDocContentSame(readCurrent.body, file.body) && sameProvenance(await this.readProvenanceSnapshot(file.path), revision);
174
211
  }
175
212
  /**
176
213
  * Publish provenance after a durable database write if storage is unchanged.
@@ -178,8 +215,9 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
178
215
  * caller to reread storage before proceeding.
179
216
  */
180
217
  async finaliseStoredRevision(file, observedProvenance, storedRevision) {
218
+ const evidence = await this.contentEvidence(file.body);
181
219
  if (await this.currentStorageSnapshotMatches(file, observedProvenance)) {
182
- await this.setProvenance(file.path, storedRevision, file.stat.mtime);
220
+ await this.setProvenance(file.path, storedRevision, file.stat.mtime, file.body, evidence);
183
221
  return true;
184
222
  } else {
185
223
  return false;
@@ -221,7 +259,10 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
221
259
  try {
222
260
  return await this.serializedByFileEventPaths([path], async () => {
223
261
  const provenance = this.fileReflectionProvenance;
224
- if (await provenance.get(path)) return false;
262
+ const provenancePath = this.getProvenancePath(path);
263
+ const existing = await provenance.get(provenancePath);
264
+ const observed = existing ? { ...existing } : void 0;
265
+ if (observed && (observed.revision !== expectedRevision || hasContentHash(observed))) return false;
225
266
  const stub = await this.storage.getStub(path);
226
267
  if (!stub || isFolderInfo(stub) || stub.path !== path) return false;
227
268
  const file = await this.readFileFromStub(stub);
@@ -231,12 +272,14 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
231
272
  const entry = await this.db.fetchEntry(path, expectedRevision, true, true, true);
232
273
  if (!entry || entry._rev !== expectedRevision || entry.deleted || entry._deleted) return false;
233
274
  if (!await isDocContentSame(readContent(entry), file.body)) return false;
275
+ const evidence = await this.contentEvidence(file.body);
276
+ if (!evidence.contentHash) return false;
234
277
  const latest = await this.db.fetchEntryMeta(path, void 0, true);
235
278
  if (!latest || latest._rev !== expectedRevision || latest.deleted || latest._deleted) return false;
236
279
  if ((await this.db.getConflictedRevs(path)).length > 0) return false;
237
- if (!await this.currentStorageSnapshotMatches(file, void 0)) return false;
238
- if (await provenance.get(path)) return false;
239
- await provenance.set(path, { revision: expectedRevision, observedStorageMtime: file.stat.mtime });
280
+ if (!await this.currentStorageSnapshotMatches(file, observed)) return false;
281
+ if (!sameProvenance(await provenance.get(provenancePath), observed)) return false;
282
+ await provenance.set(provenancePath, { revision: expectedRevision, observedStorageMtime: file.stat.mtime, ...evidence });
240
283
  return true;
241
284
  });
242
285
  } catch (ex) {
@@ -300,7 +343,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
300
343
  if (!baseEntry.deleted && !baseEntry._deleted) {
301
344
  const loadedBase = await this.db.fetchEntry(file, baseRevision, true, true);
302
345
  if (loadedBase && await isDocContentSame(getDocDataAsArray(loadedBase.data), readFile.body)) {
303
- await this.setProvenance(file.path, baseRevision, readFile.stat.mtime);
346
+ await this.setProvenance(file.path, baseRevision, readFile.stat.mtime, readFile.body);
304
347
  await this.queueConflictCheckFor(file.path);
305
348
  return true;
306
349
  }
@@ -316,7 +359,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
316
359
  if (storedRevision === false) {
317
360
  return false;
318
361
  }
319
- await this.setProvenance(file.path, storedRevision, readFile.stat.mtime);
362
+ await this.setProvenance(file.path, storedRevision, readFile.stat.mtime, readFile.body);
320
363
  await this.queueConflictCheckFor(file.path);
321
364
  return true;
322
365
  }
@@ -359,7 +402,10 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
359
402
  observedProvenance,
360
403
  entry ? { _rev: entry._rev, data: readContent(entry) } : void 0
361
404
  );
362
- if (retryIfStorageChanges && !await this.currentStorageSnapshotMatches(readFile, observedProvenance)) {
405
+ const upgradeCurrentRecord = classification.kind === "unchanged" && !isConflicted && classification.revision === currentEntry._rev && !hasContentHash(observedProvenance);
406
+ const evidence = upgradeCurrentRecord || classification.kind === "matching-leaf" && classification.revision ? await this.contentEvidence(readFile.body) : void 0;
407
+ const snapshotMatches = retryIfStorageChanges || upgradeCurrentRecord ? await this.currentStorageSnapshotMatches(readFile, observedProvenance) : true;
408
+ if (retryIfStorageChanges && !snapshotMatches) {
363
409
  return await this.retryCurrentStorage(file.path, preferredBasePath, snapshotRetry);
364
410
  }
365
411
  if (classification.kind === "unchanged") {
@@ -369,12 +415,15 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
369
415
  if (!entry) return false;
370
416
  return await this.dbToStorageCore(file.path, file);
371
417
  } else {
418
+ if (upgradeCurrentRecord && snapshotMatches) {
419
+ await this.setProvenance(file.path, classification.revision, readFile.stat.mtime, readFile.body, evidence);
420
+ }
372
421
  this.path.markChangesAreSame(readFile, readFile.stat.mtime, currentEntry.mtime);
373
422
  }
374
423
  return true;
375
424
  }
376
425
  if (classification.kind === "matching-leaf") {
377
- await this.rememberMatchingLeaf(file.path, classification.revision, readFile.stat.mtime);
426
+ await this.rememberMatchingLeaf(file.path, classification.revision, readFile.stat.mtime, readFile.body, evidence);
378
427
  if (isConflicted) await this.queueConflictCheckFor(file.path);
379
428
  return true;
380
429
  }
@@ -384,7 +433,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
384
433
  if (retryIfStorageChanges && !await this.finaliseStoredRevision(readFile, observedProvenance, storedRevision2)) {
385
434
  return await this.retryCurrentStorage(file.path, preferredBasePath, snapshotRetry);
386
435
  }
387
- if (!retryIfStorageChanges) await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime);
436
+ if (!retryIfStorageChanges) await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime, readFile.body);
388
437
  if (isConflicted || classification.kind === "unknown") await this.queueConflictCheckFor(file.path);
389
438
  return true;
390
439
  }
@@ -393,7 +442,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
393
442
  if (baseRevision) {
394
443
  const baseEntry = await this.db.fetchEntry(file, baseRevision, true, true);
395
444
  if (baseEntry && await isDocContentSame(getDocDataAsArray(baseEntry.data), readFile.body) && !force) {
396
- await this.setProvenance(file.path, baseRevision, readFile.stat.mtime);
445
+ await this.setProvenance(file.path, baseRevision, readFile.stat.mtime, readFile.body);
397
446
  this._log(`File ${file.path} is not changed on its displayed conflict branch`, LOG_LEVEL_VERBOSE);
398
447
  return true;
399
448
  }
@@ -402,7 +451,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
402
451
  if (preferredBasePath && preferredBasePath !== file.path) {
403
452
  await this.deleteProvenance(preferredBasePath);
404
453
  }
405
- await this.setProvenance(file.path, storedRevision3, readFile.stat.mtime);
454
+ await this.setProvenance(file.path, storedRevision3, readFile.stat.mtime, readFile.body);
406
455
  await this.queueConflictCheckFor(file.path);
407
456
  return true;
408
457
  }
@@ -425,7 +474,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
425
474
  if (preferredBasePath && preferredBasePath !== file.path) {
426
475
  await this.deleteProvenance(preferredBasePath);
427
476
  }
428
- await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime);
477
+ await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime, readFile.body);
429
478
  await this.queueConflictCheckFor(file.path);
430
479
  return true;
431
480
  }
@@ -435,7 +484,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
435
484
  if (preferredBasePath && preferredBasePath !== file.path) {
436
485
  await this.deleteProvenance(preferredBasePath);
437
486
  }
438
- await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime);
487
+ await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime, readFile.body);
439
488
  return true;
440
489
  }
441
490
  let shouldApplied = false;
@@ -451,7 +500,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
451
500
  }
452
501
  }
453
502
  if (!shouldApplied) {
454
- await this.setProvenance(file.path, entry._rev, readFile.stat.mtime);
503
+ await this.setProvenance(file.path, entry._rev, readFile.stat.mtime, readFile.body);
455
504
  this._log(`File ${file.path} is not changed`, LOG_LEVEL_VERBOSE);
456
505
  return true;
457
506
  }
@@ -461,7 +510,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
461
510
  if (preferredBasePath && preferredBasePath !== file.path) {
462
511
  await this.deleteProvenance(preferredBasePath);
463
512
  }
464
- await this.setProvenance(file.path, storedRevision, readFile.stat.mtime);
513
+ await this.setProvenance(file.path, storedRevision, readFile.stat.mtime, readFile.body);
465
514
  return true;
466
515
  }
467
516
  async deleteFileFromDB(info) {
@@ -813,20 +862,30 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
813
862
  );
814
863
  return false;
815
864
  }
816
- if (getParentPath(existDoc.path) !== getParentPath(path)) {
817
- this._log(
818
- `Refusing to apply a filename case change across differently cased parent directories: ${existDoc.path} -> ${path}`,
819
- LOG_LEVEL_NOTICE
820
- );
821
- return false;
822
- }
823
- const renamedFile = await this.storage.renameFile(existDoc, path);
824
- if (!renamedFile) {
825
- this._log(`Could not apply the stored filename case: ${existDoc.path} -> ${path}`, LOG_LEVEL_NOTICE);
826
- return false;
865
+ const pathNormalizationVersion = getPathNormalizationVersion(settings);
866
+ const sameNormalisedPath = pathNormalizationVersion === 1 && normalisePathForIdentity(existDoc.path, pathNormalizationVersion) === normalisePathForIdentity(path, pathNormalizationVersion);
867
+ if (!sameNormalisedPath) {
868
+ const existingParent = getParentPath(existDoc.path);
869
+ const targetParent = getParentPath(path);
870
+ const sameNormalisedParent = pathNormalizationVersion === 1 && normalisePathForIdentity(existingParent, pathNormalizationVersion) === normalisePathForIdentity(targetParent, pathNormalizationVersion);
871
+ if (existingParent !== targetParent && !sameNormalisedParent) {
872
+ this._log(
873
+ `Refusing to apply a filename case change across differently cased parent directories: ${existDoc.path} -> ${path}`,
874
+ LOG_LEVEL_NOTICE
875
+ );
876
+ return false;
877
+ }
878
+ const renamedFile = await this.storage.renameFile(existDoc, path);
879
+ if (!renamedFile) {
880
+ this._log(
881
+ `Could not apply the stored filename case: ${existDoc.path} -> ${path}`,
882
+ LOG_LEVEL_NOTICE
883
+ );
884
+ return false;
885
+ }
886
+ await this.deleteProvenance(existDoc.path);
887
+ existDoc = renamedFile;
827
888
  }
828
- await this.deleteProvenance(existDoc.path);
829
- existDoc = renamedFile;
830
889
  }
831
890
  const docRead = await this.db.fetchEntryFromMeta(docEntry);
832
891
  if (!docRead) {
@@ -843,10 +902,12 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
843
902
  }
844
903
  }
845
904
  const docData = readContent(docRead);
905
+ const reflectedContent = readAsBlob(docRead);
906
+ const reflectedEvidence = await this.contentEvidence(reflectedContent);
846
907
  if (allowExistingConflicts && existDoc && !force) {
847
908
  const readFile = await this.readFileFromStub(existDoc);
848
909
  if (await isDocContentSame(docData, readFile.body)) {
849
- await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime);
910
+ await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime, reflectedContent, reflectedEvidence);
850
911
  this.path.markChangesAreSame(docRead, docRead.mtime, existDoc.stat.mtime);
851
912
  return true;
852
913
  }
@@ -868,7 +929,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
868
929
  }
869
930
  }
870
931
  if (!shouldApplied) {
871
- await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime);
932
+ await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime, reflectedContent, reflectedEvidence);
872
933
  this._log(`File ${docRead.path} is not changed`, LOG_LEVEL_VERBOSE);
873
934
  return true;
874
935
  }
@@ -913,10 +974,10 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
913
974
  this.storage.triggerFileEvent(mode, path);
914
975
  if (ret && this.fileReflectionProvenance) {
915
976
  const storedStat = await this.storage.stat(path);
916
- if (safeSnapshot && await this.readProvenanceSnapshot(path) !== safeSnapshot.revision) {
977
+ if (safeSnapshot && !sameProvenance(await this.readProvenanceSnapshot(path), safeSnapshot.revision)) {
917
978
  return ret;
918
979
  }
919
- await this.setProvenance(path, docEntry._rev, storedStat?.mtime);
980
+ await this.setProvenance(path, docEntry._rev, storedStat?.mtime, reflectedContent, reflectedEvidence);
920
981
  }
921
982
  return ret;
922
983
  }
@@ -954,7 +1015,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
954
1015
  return { preserved: false, snapshot };
955
1016
  }
956
1017
  if (classification.kind === "matching-leaf") {
957
- await this.rememberMatchingLeaf(path, classification.revision, readFile.stat.mtime);
1018
+ await this.rememberMatchingLeaf(path, classification.revision, readFile.stat.mtime, readFile.body);
958
1019
  await this.queueConflictCheckFor(path);
959
1020
  return { preserved: true };
960
1021
  }
@@ -60,13 +60,14 @@ export interface IAPIService {
60
60
  export interface IPathService {
61
61
  id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix;
62
62
  path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID>;
63
- path2idWithSettings(filename: FilePathWithPrefix | FilePath, setting: Pick<RemoteDBSettings, "encrypt" | "usePathObfuscation" | "passphrase" | "handleFilenameCaseSensitive" | "idDerivationVersion" | "idDerivationKey">, prefix?: string): Promise<DocumentID>;
63
+ path2idWithSettings(filename: FilePathWithPrefix | FilePath, setting: PathIDSettings, prefix?: string): Promise<DocumentID>;
64
64
  getPath(entry: AnyEntry): FilePathWithPrefix;
65
65
  markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined;
66
66
  unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void;
67
67
  compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN;
68
68
  isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | FilePathWithPrefix, mtimes: number[]): undefined | typeof EVEN;
69
69
  }
70
+ export type PathIDSettings = Pick<RemoteDBSettings, "encrypt" | "usePathObfuscation" | "passphrase" | "handleFilenameCaseSensitive" | "idDerivationVersion" | "idDerivationKey"> & Partial<Pick<RemoteDBSettings, "pathNormalizationVersion">>;
70
71
  export interface openDatabaseParameters {
71
72
  replicator: ReplicatorService;
72
73
  databaseEvents: DatabaseEventService;
@@ -1,5 +1,5 @@
1
- import type { DocumentID, EntryHasPath, FilePathWithPrefix, FilePath, AnyEntry, RemoteDBSettings, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
2
- import type { IPathService, ISettingService } from "./IService.js";
1
+ import type { DocumentID, EntryHasPath, FilePathWithPrefix, FilePath, AnyEntry, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
2
+ import type { IPathService, ISettingService, PathIDSettings } from "./IService.js";
3
3
  import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
4
4
  import type { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "../../common/models/shared.const.symbols.js";
5
5
  export interface PathServiceDependencies {
@@ -37,7 +37,7 @@ export declare abstract class PathService<T extends ServiceContext = ServiceCont
37
37
  */
38
38
  path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID>;
39
39
  /** Convert a path using the caller's settings snapshot and this host's path normalisation. */
40
- path2idWithSettings(filename: FilePathWithPrefix | FilePath, setting: Pick<RemoteDBSettings, "encrypt" | "usePathObfuscation" | "passphrase" | "handleFilenameCaseSensitive" | "idDerivationVersion" | "idDerivationKey">, prefix?: string): Promise<DocumentID>;
40
+ path2idWithSettings(filename: FilePathWithPrefix | FilePath, setting: PathIDSettings, prefix?: string): Promise<DocumentID>;
41
41
  getPath(entry: AnyEntry): FilePathWithPrefix;
42
42
  abstract markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined;
43
43
  abstract unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void;
@@ -1,3 +1,4 @@
1
+ import { getPathNormalizationVersion, normalisePathForIdentity } from "../../common/pathNormalization.js";
1
2
  import { ServiceBase } from "./ServiceBase.js";
2
3
  import { addPrefix, expandFilePathPrefix, id2path_base, path2id_base } from "../../string_and_binary/path.js";
3
4
  import { configuredIdKey } from "../../common/idDerivation.js";
@@ -18,9 +19,9 @@ class PathService extends ServiceBase {
18
19
  const [prefix, path] = expandFilePathPrefix(filename);
19
20
  return prefix + this.normalizePath(path);
20
21
  }
21
- async _path2id(filename, obfuscatePassphrase, caseInsensitive, idDerivationKey) {
22
+ async _path2id(filename, obfuscatePassphrase, caseInsensitive, idDerivationKey, pathNormalizationVersion) {
22
23
  const [prefix, path] = expandFilePathPrefix(filename);
23
- const fixedPath = prefix + this.normalizePath(path);
24
+ const fixedPath = prefix + normalisePathForIdentity(this.normalizePath(path), pathNormalizationVersion);
24
25
  const out = await path2id_base(fixedPath, obfuscatePassphrase, caseInsensitive, idDerivationKey);
25
26
  return out;
26
27
  }
@@ -49,13 +50,15 @@ class PathService extends ServiceBase {
49
50
  }
50
51
  /** Convert a path using the caller's settings snapshot and this host's path normalisation. */
51
52
  async path2idWithSettings(filename, setting, prefix) {
53
+ const pathNormalizationVersion = getPathNormalizationVersion(setting);
52
54
  const destPath = addPrefix(filename, prefix ?? "");
53
55
  const pathObfuscationPassphrase = this.getPathObfuscationPassphrase?.() ?? (setting.usePathObfuscation ? setting.passphrase : false);
54
56
  return await this._path2id(
55
57
  destPath,
56
58
  pathObfuscationPassphrase,
57
59
  !setting.handleFilenameCaseSensitive,
58
- setting.encrypt && configuredIdKey(setting) || void 0
60
+ setting.encrypt && configuredIdKey(setting) || void 0,
61
+ pathNormalizationVersion
59
62
  );
60
63
  }
61
64
  getPath(entry) {
@@ -7,6 +7,8 @@ export { DEFAULT_SETTINGS, NEW_VAULT_SETTINGS, P2P_DEFAULT_SETTINGS, SETTINGS_SC
7
7
  export { PREFERRED_BASE, PREFERRED_JOURNAL_SYNC, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, } from "./common/models/setting.const.preferred.js";
8
8
  export { CURRENT_SETTING_VERSION } from "./common/models/setting.const.js";
9
9
  export { deriveIdKey, deriveOrImportIdKey, formatIdRecoveryCode, configuredIdKey, ID_DERIVATION_VERSION, ID_RECOVERY_CODE_PREFIX, } from "./common/idDerivation.js";
10
+ export { getPathNormalizationVersion, normalisePathForIdentity } from "./common/pathNormalization.js";
11
+ export type { PathNormalizationVersion, PathIdentitySettings } from "./common/models/setting.type.js";
10
12
  export { prepareSettingsForLoad, SettingsMigrationReviewCodes, type PreparedSettings, type SettingsMigrationReviewCode, type SettingsMigrationReviewReason, type SettingsMigrationState, } from "./common/models/setting.lifecycle.js";
11
13
  export { assessTweakCompatibility } from "./common/models/tweak.compatibility.js";
12
14
  export type { TweakAssessment, TweakAssessmentEntry, TweakAssessmentRelation, TweakAssessmentValue, TweakTransition, TweakTransitionReason, } from "./common/models/tweak.compatibility.js";
package/dist/settings.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  ID_DERIVATION_VERSION,
21
21
  ID_RECOVERY_CODE_PREFIX
22
22
  } from "./common/idDerivation.js";
23
+ import { getPathNormalizationVersion, normalisePathForIdentity } from "./common/pathNormalization.js";
23
24
  import {
24
25
  prepareSettingsForLoad,
25
26
  SettingsMigrationReviewCodes
@@ -53,6 +54,8 @@ export {
53
54
  deriveIdKey,
54
55
  deriveOrImportIdKey,
55
56
  formatIdRecoveryCode,
57
+ getPathNormalizationVersion,
56
58
  mergeMarkdownSettings,
59
+ normalisePathForIdentity,
57
60
  prepareSettingsForLoad
58
61
  };