@vrtmrz/livesync-commonlib 0.1.0-rc.8 → 0.1.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.
Files changed (30) hide show
  1. package/README.md +5 -2
  2. package/dist/common/models/setting.lifecycle.d.ts +1 -0
  3. package/dist/common/models/setting.lifecycle.js +11 -7
  4. package/dist/interfaces/DatabaseFileAccess.d.ts +8 -0
  5. package/dist/interfaces/FileHandler.d.ts +17 -1
  6. package/dist/interfaces/FileReflectionProvenance.d.ts +32 -0
  7. package/dist/interfaces/FileReflectionProvenance.js +25 -0
  8. package/dist/managers/ConflictManager.js +42 -9
  9. package/dist/managers/EntryManager/EntryManager.d.ts +1 -0
  10. package/dist/managers/EntryManager/EntryManager.js +5 -1
  11. package/dist/managers/EntryManager/EntryManagerImpls.d.ts +1 -0
  12. package/dist/managers/EntryManager/EntryManagerImpls.js +29 -1
  13. package/dist/pouchdb/LiveSyncLocalDB.d.ts +1 -0
  14. package/dist/pouchdb/LiveSyncLocalDB.js +3 -0
  15. package/dist/serviceFeatures/offlineScanner.js +3 -2
  16. package/dist/serviceFeatures/remoteConfig.d.ts +2 -2
  17. package/dist/serviceFeatures/remoteConfig.js +2 -2
  18. package/dist/serviceModules/ServiceDatabaseFileAccessBase.d.ts +5 -0
  19. package/dist/serviceModules/ServiceDatabaseFileAccessBase.js +69 -13
  20. package/dist/serviceModules/ServiceFileHandlerBase.d.ts +22 -1
  21. package/dist/serviceModules/ServiceFileHandlerBase.js +323 -48
  22. package/dist/services/base/KeyValueDBService.d.ts +1 -1
  23. package/dist/services/base/KeyValueDBService.js +3 -1
  24. package/dist/worker/bgWorker.js +1 -1
  25. package/docs/conflict-resolution.md +173 -0
  26. package/docs/development.md +6 -0
  27. package/docs/proven-in-use.md +4 -0
  28. package/docs/releasing.md +10 -7
  29. package/docs/settings-lifecycle.md +25 -2
  30. package/package.json +6 -1
@@ -45,6 +45,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
45
45
  path;
46
46
  setting;
47
47
  vault;
48
+ fileReflectionProvenance;
48
49
  constructor(services) {
49
50
  super(services);
50
51
  this.events = services.events;
@@ -54,6 +55,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
54
55
  this.path = services.path;
55
56
  this.setting = services.setting;
56
57
  this.vault = services.vault;
58
+ this.fileReflectionProvenance = services.fileReflectionProvenance;
57
59
  services.fileProcessing.processFileEvent.addHandler(this._anyHandlerProcessesFileEvent.bind(this), 100);
58
60
  services.replication.processSynchroniseResult.addHandler(this._anyProcessReplicatedDoc.bind(this), 100);
59
61
  }
@@ -63,6 +65,66 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
63
65
  get storage() {
64
66
  return this.storageAccess;
65
67
  }
68
+ async getProvenance(path) {
69
+ if (!this.fileReflectionProvenance) return void 0;
70
+ try {
71
+ const record = await this.fileReflectionProvenance.get(path);
72
+ if (!record) return void 0;
73
+ const entry = await this.db.fetchEntryMeta(path, record.revision, true);
74
+ if (entry && !entry._deleted && !entry.deleted) {
75
+ return record;
76
+ }
77
+ await this.fileReflectionProvenance.delete(path);
78
+ } catch (ex) {
79
+ this._log(`Could not read file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
80
+ this._log(ex, LOG_LEVEL_VERBOSE);
81
+ }
82
+ return void 0;
83
+ }
84
+ async setProvenance(path, revision, observedStorageMtime) {
85
+ if (!this.fileReflectionProvenance || !revision) return;
86
+ try {
87
+ await this.fileReflectionProvenance.set(path, {
88
+ revision,
89
+ observedStorageMtime
90
+ });
91
+ } catch (ex) {
92
+ this._log(`Could not record file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
93
+ this._log(ex, LOG_LEVEL_VERBOSE);
94
+ }
95
+ }
96
+ async deleteProvenance(path) {
97
+ if (!this.fileReflectionProvenance) return;
98
+ try {
99
+ await this.fileReflectionProvenance.delete(path);
100
+ } catch (ex) {
101
+ this._log(`Could not delete file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
102
+ this._log(ex, LOG_LEVEL_VERBOSE);
103
+ }
104
+ }
105
+ async findUniqueContentRevision(file) {
106
+ try {
107
+ const revisions = await this.db.findContentRevisions(file, file.body);
108
+ return revisions.length === 1 ? revisions[0] : void 0;
109
+ } catch (ex) {
110
+ this._log(`Could not reconstruct file reflection provenance for ${file.path}`, LOG_LEVEL_VERBOSE);
111
+ this._log(ex, LOG_LEVEL_VERBOSE);
112
+ return void 0;
113
+ }
114
+ }
115
+ async getProvenBaseRevision(file, preferredPath) {
116
+ const path = preferredPath ?? file.path;
117
+ const recorded = await this.getProvenance(path) ?? (preferredPath && preferredPath !== file.path ? await this.getProvenance(file.path) : void 0);
118
+ if (recorded) {
119
+ return recorded.revision;
120
+ }
121
+ const matched = await this.findUniqueContentRevision(file);
122
+ if (matched) {
123
+ await this.setProvenance(path, matched, file.stat.mtime);
124
+ return matched;
125
+ }
126
+ return void 0;
127
+ }
66
128
  getPath(entry) {
67
129
  return this.path.getPath(entry);
68
130
  }
@@ -85,6 +147,62 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
85
147
  return file;
86
148
  }
87
149
  async storeFileToDB(info, force = false, onlyChunks = false) {
150
+ return await this.storeFileToDBFromRevision(info, force, onlyChunks);
151
+ }
152
+ async storeFileToDBWithBaseRevision(info, baseRevision, createIfDifferent = true) {
153
+ const file = await this.infoToStub(info);
154
+ if (file == null) {
155
+ this._log(`File ${tryGetFilePath(info)} is not exist on the storage`, LOG_LEVEL_VERBOSE);
156
+ return false;
157
+ }
158
+ if (file.isInternal) {
159
+ this._log(
160
+ `Internal file ${file.path} is not allowed to be stored through the ordinary file handler`,
161
+ LOG_LEVEL_VERBOSE
162
+ );
163
+ return false;
164
+ }
165
+ const [baseEntry, currentEntry, conflictedRevisions] = await Promise.all([
166
+ this.db.fetchEntryMeta(file, baseRevision, true),
167
+ this.db.fetchEntryMeta(file, void 0, true),
168
+ this.db.getConflictedRevs(file)
169
+ ]);
170
+ const liveRevisions = /* @__PURE__ */ new Set([
171
+ ...currentEntry && currentEntry._rev ? [currentEntry._rev] : [],
172
+ ...conflictedRevisions
173
+ ]);
174
+ if (!baseEntry || baseEntry._rev !== baseRevision || !liveRevisions.has(baseRevision)) {
175
+ this._log(
176
+ `Could not store ${file.path} on revision ${baseRevision}; the selected revision is no longer live`,
177
+ LOG_LEVEL_NOTICE
178
+ );
179
+ return false;
180
+ }
181
+ const readFile = await this.readFileFromStub(file);
182
+ if (!baseEntry.deleted && !baseEntry._deleted) {
183
+ const loadedBase = await this.db.fetchEntry(file, baseRevision, true, true);
184
+ if (loadedBase && await isDocContentSame(getDocDataAsArray(loadedBase.data), readFile.body)) {
185
+ await this.setProvenance(file.path, baseRevision, readFile.stat.mtime);
186
+ await this.conflict.queueCheckFor(file.path);
187
+ return true;
188
+ }
189
+ }
190
+ if (!createIfDifferent) {
191
+ this._log(
192
+ `Could not mark ${file.path} as revision ${baseRevision}; the storage content differs`,
193
+ LOG_LEVEL_NOTICE
194
+ );
195
+ return false;
196
+ }
197
+ const storedRevision = await this.db.storeWithBaseRevision(readFile, baseRevision, true);
198
+ if (storedRevision === false) {
199
+ return false;
200
+ }
201
+ await this.setProvenance(file.path, storedRevision, readFile.stat.mtime);
202
+ await this.conflict.queueCheckFor(file.path);
203
+ return true;
204
+ }
205
+ async storeFileToDBFromRevision(info, force = false, onlyChunks = false, preferredBasePath) {
88
206
  const file = await this.infoToStub(info);
89
207
  if (file == null) {
90
208
  this._log(`File ${tryGetFilePath(info)} is not exist on the storage`, LOG_LEVEL_VERBOSE);
@@ -97,27 +215,70 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
97
215
  );
98
216
  return false;
99
217
  }
218
+ if (onlyChunks) {
219
+ const readFile2 = await this.readFileFromStub(file);
220
+ return await this.db.createChunks(readFile2, force, true);
221
+ }
222
+ const readFile = await this.readFileFromStub(file);
100
223
  const entry = await this.db.fetchEntry(file, void 0, true, true);
224
+ const conflictedRevs = await this.db.getConflictedRevs(file);
225
+ const isConflicted = conflictedRevs.length > 0;
226
+ if (isConflicted) {
227
+ const baseRevision = await this.getProvenBaseRevision(readFile, preferredBasePath);
228
+ if (baseRevision) {
229
+ const baseEntry = await this.db.fetchEntry(file, baseRevision, true, true);
230
+ if (baseEntry && await isDocContentSame(getDocDataAsArray(baseEntry.data), readFile.body) && !force) {
231
+ await this.setProvenance(file.path, baseRevision, readFile.stat.mtime);
232
+ this._log(`File ${file.path} is not changed on its displayed conflict branch`, LOG_LEVEL_VERBOSE);
233
+ return true;
234
+ }
235
+ const storedRevision3 = await this.db.storeWithBaseRevision(readFile, baseRevision, true);
236
+ if (storedRevision3 === false) return false;
237
+ if (preferredBasePath && preferredBasePath !== file.path) {
238
+ await this.deleteProvenance(preferredBasePath);
239
+ }
240
+ await this.setProvenance(file.path, storedRevision3, readFile.stat.mtime);
241
+ await this.conflict.queueCheckFor(file.path);
242
+ return true;
243
+ }
244
+ const currentEntry = entry || await this.db.fetchEntryMeta(file, void 0, true);
245
+ const currentRevision = currentEntry && currentEntry._rev;
246
+ if (!currentRevision) {
247
+ this._log(
248
+ `Could not preserve the unknown conflict branch for ${file.path}; no current revision is available`,
249
+ LOG_LEVEL_NOTICE
250
+ );
251
+ await this.conflict.queueCheckFor(file.path);
252
+ return false;
253
+ }
254
+ const storedRevision2 = await this.db.storeAsConflictedRevisionWithResult(readFile, currentRevision, true);
255
+ if (storedRevision2 === false) {
256
+ this._log(`Could not preserve the unknown conflict branch for ${file.path}`, LOG_LEVEL_NOTICE);
257
+ await this.conflict.queueCheckFor(file.path);
258
+ return false;
259
+ }
260
+ if (preferredBasePath && preferredBasePath !== file.path) {
261
+ await this.deleteProvenance(preferredBasePath);
262
+ }
263
+ await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime);
264
+ await this.conflict.queueCheckFor(file.path);
265
+ return true;
266
+ }
101
267
  if (!entry || entry.deleted || entry._deleted) {
102
- const readFile = await this.readFileFromStub(file);
103
- if (!onlyChunks) {
104
- return await this.db.store(readFile);
105
- } else {
106
- return await this.db.createChunks(readFile, false, true);
268
+ const storedRevision2 = await this.db.storeWithBaseRevision(readFile, entry && entry._rev, true);
269
+ if (storedRevision2 === false) return false;
270
+ if (preferredBasePath && preferredBasePath !== file.path) {
271
+ await this.deleteProvenance(preferredBasePath);
107
272
  }
273
+ await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime);
274
+ return true;
108
275
  }
109
276
  let shouldApplied = false;
110
277
  if (!force && !onlyChunks) {
111
278
  if (this.path.compareFileFreshness(file, entry) !== EVEN) {
112
279
  shouldApplied = true;
113
280
  }
114
- let readFile = void 0;
115
281
  if (!shouldApplied) {
116
- readFile = await this.readFileFromStub(file);
117
- if (!readFile) {
118
- this._log(`File ${file.path} is not exist on the storage`, LOG_LEVEL_NOTICE);
119
- return false;
120
- }
121
282
  if (await isDocContentSame(getDocDataAsArray(entry.data), readFile.body)) {
122
283
  this.path.markChangesAreSame(readFile, readFile.stat.mtime, entry.mtime);
123
284
  } else {
@@ -125,28 +286,23 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
125
286
  }
126
287
  }
127
288
  if (!shouldApplied) {
289
+ await this.setProvenance(file.path, entry._rev, readFile.stat.mtime);
128
290
  this._log(`File ${file.path} is not changed`, LOG_LEVEL_VERBOSE);
129
291
  return true;
130
292
  }
131
- if (!readFile) readFile = await this.readFileFromStub(file);
132
- if (onlyChunks) {
133
- return await this.db.createChunks(readFile, false, true);
134
- } else {
135
- return await this.db.store(readFile, false, true);
136
- }
137
- } else {
138
- const readFile = await this.readFileFromStub(file);
139
- if (onlyChunks) {
140
- return await this.db.createChunks(readFile, true, true);
141
- } else {
142
- return await this.db.store(readFile, true, true);
143
- }
144
293
  }
294
+ const storedRevision = await this.db.storeWithBaseRevision(readFile, entry._rev, true);
295
+ if (storedRevision === false) return false;
296
+ if (preferredBasePath && preferredBasePath !== file.path) {
297
+ await this.deleteProvenance(preferredBasePath);
298
+ }
299
+ await this.setProvenance(file.path, storedRevision, readFile.stat.mtime);
300
+ return true;
145
301
  }
146
302
  async deleteFileFromDB(info) {
147
303
  const file = await this.infoToStub(info);
304
+ const path = typeof info === "string" ? info : tryGetFilePath(info);
148
305
  if (file == null) {
149
- const path = typeof info === "string" ? info : tryGetFilePath(info);
150
306
  if (path === void 0) {
151
307
  this._log(`File ${tryGetFilePath(info)} is not exist on the storage`, LOG_LEVEL_VERBOSE);
152
308
  return false;
@@ -159,8 +315,27 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
159
315
  );
160
316
  return false;
161
317
  }
318
+ const conflictedRevs2 = await this.db.getConflictedRevs(path);
319
+ if (conflictedRevs2.length > 0) {
320
+ const provenance = await this.getProvenance(path);
321
+ if (!provenance) {
322
+ this._log(
323
+ `The deleted storage file ${path} has conflicts, but its displayed revision is unknown; preserving every database branch`,
324
+ LOG_LEVEL_NOTICE
325
+ );
326
+ await this.conflict.queueCheckFor(path);
327
+ return true;
328
+ }
329
+ const storedRevision = await this.db.storeDeletionWithBaseRevision(path, provenance.revision);
330
+ if (storedRevision === false) return false;
331
+ await this.deleteProvenance(path);
332
+ await this.conflict.queueCheckFor(path);
333
+ return true;
334
+ }
162
335
  this._log(`File ${path} is missing on storage; deleting from the database by path`, LOG_LEVEL_INFO);
163
- return await this.db.delete(path);
336
+ const deleted2 = await this.db.delete(path);
337
+ if (deleted2) await this.deleteProvenance(path);
338
+ return deleted2;
164
339
  }
165
340
  if (file.isInternal) {
166
341
  this._log(
@@ -176,9 +351,31 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
176
351
  }
177
352
  const conflictedRevs = await this.db.getConflictedRevs(file);
178
353
  if (conflictedRevs.length > 0) {
179
- return await this.db.delete(file, entry._rev);
354
+ let baseRevision = (await this.getProvenance(file.path))?.revision;
355
+ if (!baseRevision) {
356
+ try {
357
+ const readFile = await this.readFileFromStub(file);
358
+ baseRevision = await this.findUniqueContentRevision(readFile);
359
+ } catch {
360
+ }
361
+ }
362
+ if (!baseRevision) {
363
+ this._log(
364
+ `The deleted storage file ${file.path} has conflicts, but its displayed revision is unknown; preserving every database branch`,
365
+ LOG_LEVEL_NOTICE
366
+ );
367
+ await this.conflict.queueCheckFor(file.path);
368
+ return true;
369
+ }
370
+ const storedRevision = await this.db.storeDeletionWithBaseRevision(file.path, baseRevision);
371
+ if (storedRevision === false) return false;
372
+ await this.deleteProvenance(file.path);
373
+ await this.conflict.queueCheckFor(file.path);
374
+ return true;
180
375
  }
181
- return await this.db.delete(file);
376
+ const deleted = await this.db.delete(file);
377
+ if (deleted) await this.deleteProvenance(file.path);
378
+ return deleted;
182
379
  }
183
380
  async renameFileInDB(info, oldPath) {
184
381
  const newPath = getStoragePathFromUXFileInfo(info);
@@ -188,9 +385,17 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
188
385
  ]);
189
386
  if (oldDocumentId === newDocumentId) {
190
387
  this._log(`Updating the stored path for case-only rename: ${oldPath} -> ${newPath}`, LOG_LEVEL_VERBOSE);
191
- return await this.storeFileToDB(info, true);
388
+ return await this.storeFileToDBFromRevision(info, true, false, oldPath);
192
389
  }
193
390
  const oldEntry = await this.db.fetchEntryMeta(oldPath, void 0, true);
391
+ const newEntry = await this.db.fetchEntryMeta(newPath, void 0, true);
392
+ if (newEntry && !newEntry.deleted && !newEntry._deleted) {
393
+ this._log(
394
+ `Refusing to overwrite the existing database entry while renaming ${oldPath} to ${newPath}`,
395
+ LOG_LEVEL_NOTICE
396
+ );
397
+ return false;
398
+ }
194
399
  if (!await this.storeFileToDB(info, true)) {
195
400
  this._log(`Failed to store rename target; preserving source in the database: ${oldPath}`, LOG_LEVEL_NOTICE);
196
401
  return false;
@@ -199,54 +404,107 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
199
404
  this._log(`Rename source is not present in the database: ${oldPath}`, LOG_LEVEL_VERBOSE);
200
405
  return true;
201
406
  }
202
- const oldFile = {
203
- path: oldPath,
204
- name: oldPath.split("/").pop() ?? info.name,
205
- stat: info.stat,
206
- deleted: true
207
- };
208
- return await this.deleteFileFromDB(oldFile);
407
+ const oldConflicts = await this.db.getConflictedRevs(oldPath);
408
+ if (oldConflicts.length > 0) {
409
+ let baseRevision = (await this.getProvenance(oldPath))?.revision;
410
+ if (!baseRevision) {
411
+ const readFile = await this.readFileFromStub(info);
412
+ const revisions = await this.db.findContentRevisions(oldPath, readFile.body);
413
+ baseRevision = revisions.length === 1 ? revisions[0] : void 0;
414
+ }
415
+ if (!baseRevision) {
416
+ this._log(
417
+ `Renamed ${oldPath} to ${newPath}, but preserved every conflicted source branch because the displayed source revision is unknown`,
418
+ LOG_LEVEL_NOTICE
419
+ );
420
+ await this.conflict.queueCheckFor(oldPath);
421
+ return true;
422
+ }
423
+ const storedRevision = await this.db.storeDeletionWithBaseRevision(
424
+ oldPath,
425
+ baseRevision
426
+ );
427
+ if (storedRevision === false) return false;
428
+ await this.deleteProvenance(oldPath);
429
+ await this.conflict.queueCheckFor(oldPath);
430
+ return true;
431
+ }
432
+ const deleted = await this.db.delete(oldPath);
433
+ if (deleted) await this.deleteProvenance(oldPath);
434
+ return deleted;
209
435
  }
210
436
  async deleteRevisionFromDB(info, rev) {
211
- return await this.db.delete(info, rev);
437
+ const path = getStoragePathFromUXFileInfo(info);
438
+ const provenance = await this.getProvenance(path);
439
+ const deleted = await this.db.delete(info, rev);
440
+ if (deleted && provenance?.revision === rev) {
441
+ await this.deleteProvenance(path);
442
+ }
443
+ return deleted;
212
444
  }
213
445
  async resolveConflictedByDeletingRevision(info, rev) {
214
446
  const path = getStoragePathFromUXFileInfo(info);
447
+ const file = await this.infoToStub(info);
448
+ const docEntry = await this.db.fetchEntryMeta(file ?? info, rev, true);
449
+ if (!docEntry) {
450
+ this._log(`Failed to read the conflicted revision ${rev} of ${path}`, LOG_LEVEL_VERBOSE);
451
+ return false;
452
+ }
215
453
  if (!await this.deleteRevisionFromDB(info, rev)) {
216
454
  this._log(`Failed to delete the conflicted revision ${rev} of ${path}`, LOG_LEVEL_VERBOSE);
217
455
  return false;
218
456
  }
219
- if (!await this.dbToStorageWithSpecificRev(info, rev, true)) {
457
+ if (!await this.applyDatabaseEntryToStorage(docEntry, file, true)) {
220
458
  this._log(`Failed to apply the resolved revision ${rev} of ${path} to the storage`, LOG_LEVEL_VERBOSE);
221
459
  return false;
222
460
  }
461
+ await this.deleteProvenance(path);
223
462
  }
224
463
  async dbToStorageWithSpecificRev(info, rev, force) {
225
- const file = await this.infoToStub(info);
226
- if (file == null) {
227
- this._log(`File ${tryGetFilePath(info)} is not exist on the storage`, LOG_LEVEL_VERBOSE);
464
+ if (info == null) {
465
+ this._log(`Cannot select database revision ${rev} without a file path`, LOG_LEVEL_VERBOSE);
228
466
  return false;
229
467
  }
230
- const docEntry = await this.db.fetchEntryMeta(file, rev, true);
468
+ const file = await this.infoToStub(info);
469
+ const databaseTarget = file ?? info;
470
+ const [docEntry, currentEntry, conflictedRevisions] = await Promise.all([
471
+ this.db.fetchEntryMeta(databaseTarget, rev, true),
472
+ this.db.fetchEntryMeta(databaseTarget, void 0, true),
473
+ this.db.getConflictedRevs(databaseTarget)
474
+ ]);
231
475
  if (!docEntry) {
232
- this._log(`File ${file.path} is not exist on the database`, LOG_LEVEL_VERBOSE);
476
+ this._log(`File ${tryGetFilePath(info)} is not exist on the database`, LOG_LEVEL_VERBOSE);
477
+ return false;
478
+ }
479
+ const liveRevisions = /* @__PURE__ */ new Set([
480
+ ...currentEntry && currentEntry._rev ? [currentEntry._rev] : [],
481
+ ...conflictedRevisions
482
+ ]);
483
+ if (!liveRevisions.has(rev)) {
484
+ this._log(
485
+ `Could not apply ${tryGetFilePath(info)} revision ${rev}; the selected revision is no longer live`,
486
+ LOG_LEVEL_NOTICE
487
+ );
233
488
  return false;
234
489
  }
235
- return await this.dbToStorage(docEntry, file, force);
490
+ return await this.applyDatabaseEntryToStorage(docEntry, file, force, true);
236
491
  }
237
492
  async dbToStorage(entryInfo, info, force) {
238
493
  const file = await this.infoToStub(info);
239
- const mode = file == null ? "create" : "modify";
240
494
  const pathFromEntryInfo = typeof entryInfo === "string" ? entryInfo : this.getPath(entryInfo);
241
495
  const docEntry = await this.db.fetchEntryMeta(pathFromEntryInfo, void 0, true);
242
496
  if (!docEntry) {
243
497
  this._log(`File ${pathFromEntryInfo} is not exist on the database`, LOG_LEVEL_VERBOSE);
244
498
  return false;
245
499
  }
500
+ return await this.applyDatabaseEntryToStorage(docEntry, file, force);
501
+ }
502
+ async applyDatabaseEntryToStorage(docEntry, file, force, allowExistingConflicts = false) {
503
+ const mode = file == null ? "create" : "modify";
246
504
  const path = this.getPath(docEntry);
247
505
  const settings = this.setting.currentSettings();
248
506
  const revs = await this.db.getConflictedRevs(path);
249
- if (revs.length > 0) {
507
+ if (revs.length > 0 && !allowExistingConflicts) {
250
508
  if (settings.writeDocumentsIfConflicted) {
251
509
  } else {
252
510
  await this.conflict.queueCheckForIfOpen(path);
@@ -261,6 +519,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
261
519
  const existOnDB = !(docEntry._deleted || docEntry.deleted || false);
262
520
  if (!existOnDB && !existDoc) {
263
521
  this._log(`File ${path} seems to be deleted, but already not on storage`, LOG_LEVEL_VERBOSE);
522
+ await this.deleteProvenance(path);
264
523
  return true;
265
524
  }
266
525
  if (!existOnDB && existDoc) {
@@ -268,6 +527,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
268
527
  return true;
269
528
  }
270
529
  await this.storage.deleteVaultItem(path);
530
+ await this.deleteProvenance(path);
271
531
  return true;
272
532
  }
273
533
  if (existDoc && existDoc.path !== path) {
@@ -294,6 +554,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
294
554
  this._log(`Could not apply the stored filename case: ${existDoc.path} -> ${path}`, LOG_LEVEL_NOTICE);
295
555
  return false;
296
556
  }
557
+ await this.deleteProvenance(existDoc.path);
297
558
  existDoc = renamedFile;
298
559
  }
299
560
  const docRead = await this.db.fetchEntryFromMeta(docEntry);
@@ -311,6 +572,14 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
311
572
  }
312
573
  }
313
574
  const docData = readContent(docRead);
575
+ if (allowExistingConflicts && existDoc && !force) {
576
+ const readFile = await this.readFileFromStub(existDoc);
577
+ if (await isDocContentSame(docData, readFile.body)) {
578
+ await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime);
579
+ this.path.markChangesAreSame(docRead, docRead.mtime, existDoc.stat.mtime);
580
+ return true;
581
+ }
582
+ }
314
583
  if (existDoc && !force) {
315
584
  let shouldApplied = false;
316
585
  const freshness = this.path.compareFileFreshness(existDoc, docEntry);
@@ -327,6 +596,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
327
596
  }
328
597
  }
329
598
  if (!shouldApplied) {
599
+ await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime);
330
600
  this._log(`File ${docRead.path} is not changed`, LOG_LEVEL_VERBOSE);
331
601
  return true;
332
602
  }
@@ -343,6 +613,10 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
343
613
  const ret = await this.storage.writeFileAuto(path, docData, { ctime: docRead.ctime, mtime: docRead.mtime });
344
614
  await this.storage.touched(path);
345
615
  this.storage.triggerFileEvent(mode, path);
616
+ if (ret && this.fileReflectionProvenance) {
617
+ const storedStat = await this.storage.stat(path);
618
+ await this.setProvenance(path, docEntry._rev, storedStat?.mtime);
619
+ }
346
620
  return ret;
347
621
  }
348
622
  async preserveUnsyncedStorageAsConflict(path, existDoc, incomingEntry, incomingContent) {
@@ -359,11 +633,12 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
359
633
  if (await this.db.hasContentInRevisionHistory(path, readFile.body, incomingEntry._rev)) {
360
634
  return false;
361
635
  }
362
- const stored = await this.db.storeAsConflictedRevision(readFile, incomingEntry._rev, true);
363
- if (!stored) {
636
+ const storedRevision = await this.db.storeAsConflictedRevisionWithResult(readFile, incomingEntry._rev, true);
637
+ if (storedRevision === false) {
364
638
  this._log(`Prevented overwriting unsynchronised local changes for ${path}`, LOG_LEVEL_NOTICE);
365
639
  return true;
366
640
  }
641
+ await this.setProvenance(path, storedRevision, readFile.stat.mtime);
367
642
  this._log(`Preserved unsynchronised local changes as a conflict for ${path}`, LOG_LEVEL_NOTICE);
368
643
  await this.conflict.queueCheckFor(path);
369
644
  return true;
@@ -37,6 +37,6 @@ export declare abstract class KeyValueDBService<T extends ServiceContext = Servi
37
37
  set: (key: string, value: unknown) => Promise<void>;
38
38
  delete: (key: string) => Promise<void>;
39
39
  keys: (from: string | undefined, to: string | undefined, count?: number) => Promise<string[]>;
40
- db: Promise<KeyValueDatabase>;
40
+ readonly db: Promise<KeyValueDatabase>;
41
41
  };
42
42
  }
@@ -124,7 +124,9 @@ class KeyValueDBService extends ServiceBase {
124
124
  );
125
125
  return ret.map((e) => (typeof e === "string" ? e : JSON.stringify(e)).toString()).filter((e) => e.startsWith(prefix)).map((e) => e.substring(prefix.length));
126
126
  },
127
- db: Promise.resolve(getDB())
127
+ get db() {
128
+ return Promise.resolve(getDB());
129
+ }
128
130
  };
129
131
  }
130
132
  }