@vrtmrz/livesync-commonlib 0.2.0 → 0.2.2
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 +43 -0
- package/dist/API/processSetting.d.ts +2 -0
- package/dist/common/models/setting.const.defaults.js +2 -0
- package/dist/common/models/setting.const.qr.js +3 -0
- package/dist/common/models/setting.policy.d.ts +8 -0
- package/dist/common/models/setting.policy.js +2 -0
- package/dist/common/models/setting.type.d.ts +6 -0
- package/dist/common/models/shared.definition.d.ts +20 -0
- package/dist/common/models/tweak.definition.d.ts +2 -0
- package/dist/common/settingConstants.d.ts +2 -0
- package/dist/common/settingConstants.js +10 -2
- package/dist/interfaces/FileHandler.d.ts +7 -1
- package/dist/interfaces/StorageAccess.d.ts +2 -0
- package/dist/interfaces/StorageEventManager.d.ts +7 -0
- package/dist/interfaces/StorageEventManager.js +11 -0
- package/dist/managers/StorageEventManager.d.ts +20 -10
- package/dist/managers/StorageEventManager.js +268 -189
- package/dist/replication/LiveSyncAbstractReplicator.d.ts +2 -0
- package/dist/replication/LiveSyncAbstractReplicator.js +5 -2
- package/dist/replication/couchdb/LiveSyncReplicator.d.ts +6 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +39 -2
- package/dist/replication/couchdb/replicationProgress.d.ts +6 -0
- package/dist/replication/couchdb/replicationProgress.js +105 -0
- package/dist/replication/index.d.ts +1 -0
- package/dist/replication/trystero/TrysteroReplicator.d.ts +4 -0
- package/dist/serviceFeatures/offlineScanner.d.ts +4 -4
- package/dist/serviceFeatures/offlineScanner.js +30 -13
- package/dist/serviceFeatures/prepareDatabaseForUse.d.ts +3 -2
- package/dist/serviceFeatures/prepareDatabaseForUse.js +5 -4
- package/dist/serviceModules/ServiceFileAccessBase.d.ts +1 -0
- package/dist/serviceModules/ServiceFileAccessBase.js +8 -0
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +9 -5
- package/dist/serviceModules/ServiceFileHandlerBase.js +96 -38
- package/dist/services/base/AppLifecycleService.js +1 -1
- package/dist/services/base/ControlService.js +6 -2
- package/dist/services/base/DatabaseEventService.d.ts +1 -1
- package/dist/services/base/FileProcessingService.d.ts +7 -1
- package/dist/services/base/FileProcessingService.js +9 -1
- package/dist/services/base/IService.d.ts +12 -4
- package/dist/services/base/ReplicatorService.d.ts +1 -0
- package/dist/services/base/VaultScanResult.d.ts +5 -0
- package/dist/services/base/VaultService.d.ts +1 -1
- package/dist/services/lib/HandlerUtils.d.ts +60 -0
- package/dist/services/lib/HandlerUtils.js +46 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/offline-scanner.md +23 -13
- package/docs/settings-lifecycle.md +2 -0
- package/docs/storage-events-and-reflection.md +113 -10
- package/package.json +2 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
|
2
2
|
import { serialized } from "octagonal-wheels/concurrency/lock";
|
|
3
3
|
import {
|
|
4
|
+
createBlob,
|
|
4
5
|
getDocDataAsArray,
|
|
5
6
|
isDocContentSame,
|
|
6
|
-
readAsBlob,
|
|
7
7
|
readContent
|
|
8
8
|
} from "../common/utils.js";
|
|
9
9
|
import { computeFileContentHash } from "../common/fileContentHash.js";
|
|
@@ -158,7 +158,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
158
158
|
* A valid saved content hash can prove unchanged bytes after revision
|
|
159
159
|
* pruning. It cannot reconstruct ancestry for an edited file.
|
|
160
160
|
*/
|
|
161
|
-
async classifyStorageContent(file, currentEntry, preferredPath, recorded, currentLoaded) {
|
|
161
|
+
async classifyStorageContent(file, currentEntry, preferredPath, recorded, currentLoaded, requireReadableContent = false) {
|
|
162
162
|
const path = preferredPath ?? file.path;
|
|
163
163
|
const recordedRevision = recorded?.revision;
|
|
164
164
|
if (recordedRevision) {
|
|
@@ -173,6 +173,9 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
173
173
|
}
|
|
174
174
|
if (!base) {
|
|
175
175
|
const meta = await this.db.fetchEntryMeta(file, recordedRevision, true);
|
|
176
|
+
if (requireReadableContent && meta && !meta.deleted && !meta._deleted) {
|
|
177
|
+
throw new Error(`Could not read the recorded file revision for ${file.path}`);
|
|
178
|
+
}
|
|
176
179
|
if ((!meta || meta.deleted || meta._deleted) && !hasContentHash(recorded)) await this.deleteProvenance(path);
|
|
177
180
|
}
|
|
178
181
|
}
|
|
@@ -189,13 +192,15 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
189
192
|
/**
|
|
190
193
|
* Read the raw provenance value used by an optimistic storage snapshot.
|
|
191
194
|
* Unlike `getProvenance`, this does not validate or remove the referenced
|
|
192
|
-
* database revision
|
|
195
|
+
* database revision. Recovery callers require a readable value so a store
|
|
196
|
+
* failure cannot be mistaken for absent provenance.
|
|
193
197
|
*/
|
|
194
|
-
async readProvenanceSnapshot(path) {
|
|
198
|
+
async readProvenanceSnapshot(path, requireReadable = false) {
|
|
195
199
|
try {
|
|
196
200
|
const record = await this.fileReflectionProvenance?.get(this.getProvenancePath(path));
|
|
197
201
|
return record ? { ...record } : void 0;
|
|
198
|
-
} catch {
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (requireReadable) throw error;
|
|
199
204
|
return void 0;
|
|
200
205
|
}
|
|
201
206
|
}
|
|
@@ -203,20 +208,20 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
203
208
|
* Check that storage still has the same path, bytes, timestamp, and
|
|
204
209
|
* provenance as a previously captured snapshot.
|
|
205
210
|
*/
|
|
206
|
-
async currentStorageSnapshotMatches(file, revision) {
|
|
211
|
+
async currentStorageSnapshotMatches(file, revision, requireReadableProvenance = false) {
|
|
207
212
|
const current = await this.storage.getStub(file.path);
|
|
208
213
|
if (!current || isFolderInfo(current) || current.path !== file.path) return false;
|
|
209
214
|
const readCurrent = await this.readFileFromStub(current);
|
|
210
|
-
return readCurrent.stat.mtime === file.stat.mtime && await isDocContentSame(readCurrent.body, file.body) && sameProvenance(await this.readProvenanceSnapshot(file.path), revision);
|
|
215
|
+
return readCurrent.stat.mtime === file.stat.mtime && await isDocContentSame(readCurrent.body, file.body) && sameProvenance(await this.readProvenanceSnapshot(file.path, requireReadableProvenance), revision);
|
|
211
216
|
}
|
|
212
217
|
/**
|
|
213
218
|
* Publish provenance after a durable database write if storage is unchanged.
|
|
214
219
|
* A failed revalidation leaves the database revision stored and asks the
|
|
215
220
|
* caller to reread storage before proceeding.
|
|
216
221
|
*/
|
|
217
|
-
async finaliseStoredRevision(file, observedProvenance, storedRevision) {
|
|
222
|
+
async finaliseStoredRevision(file, observedProvenance, storedRevision, requireReadableProvenance = false) {
|
|
218
223
|
const evidence = await this.contentEvidence(file.body);
|
|
219
|
-
if (await this.currentStorageSnapshotMatches(file, observedProvenance)) {
|
|
224
|
+
if (await this.currentStorageSnapshotMatches(file, observedProvenance, requireReadableProvenance)) {
|
|
220
225
|
await this.setProvenance(file.path, storedRevision, file.stat.mtime, file.body, evidence);
|
|
221
226
|
return true;
|
|
222
227
|
} else {
|
|
@@ -253,6 +258,27 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
253
258
|
}
|
|
254
259
|
return readFile;
|
|
255
260
|
}
|
|
261
|
+
async canSkipUnchangedFile(info, expected) {
|
|
262
|
+
const path = getStoragePathFromUXFileInfo(info);
|
|
263
|
+
const provenance = this.fileReflectionProvenance;
|
|
264
|
+
if (!provenance || !expected._rev || expected.deleted || expected._deleted || expected._conflicts?.length || this.storage.hasPendingFileEvent?.(path) !== false) return false;
|
|
265
|
+
try {
|
|
266
|
+
return await this.serializedByFileEventPaths([path], async () => {
|
|
267
|
+
const provenancePath = this.getProvenancePath(path);
|
|
268
|
+
const record = await provenance.get(provenancePath);
|
|
269
|
+
if (!record || !hasContentHash(record) || record.revision !== expected._rev || !Number.isFinite(record.observedStorageMtime) || record.observedStorageMtime !== info.stat.mtime || record.contentSize !== info.stat.size) return false;
|
|
270
|
+
const current = await this.db.fetchEntryMeta(path, void 0, true);
|
|
271
|
+
if (!current || current._rev !== record.revision || current.deleted || current._deleted || (await this.db.getConflictedRevs(path)).length > 0) return false;
|
|
272
|
+
const stat = await this.storage.stat(path);
|
|
273
|
+
if (!stat || stat.type !== "file" || stat.mtime !== record.observedStorageMtime || stat.size !== record.contentSize) return false;
|
|
274
|
+
return sameProvenance(await provenance.get(provenancePath), record) && this.storage.hasPendingFileEvent?.(path) === false;
|
|
275
|
+
});
|
|
276
|
+
} catch (error) {
|
|
277
|
+
this._log(`Could not inspect unchanged file metadata for ${path}`, LOG_LEVEL_VERBOSE);
|
|
278
|
+
this._log(error, LOG_LEVEL_VERBOSE);
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
256
282
|
async tryRecordUntrackedFileRevision(info, expectedRevision) {
|
|
257
283
|
if (!this.fileReflectionProvenance || !expectedRevision) return false;
|
|
258
284
|
const path = getStoragePathFromUXFileInfo(info);
|
|
@@ -293,10 +319,10 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
293
319
|
const file = typeof info === "string" ? await this.storage.getFileStub(info) : info;
|
|
294
320
|
return file;
|
|
295
321
|
}
|
|
296
|
-
async storeFileToDB(info, force = false, onlyChunks = false) {
|
|
322
|
+
async storeFileToDB(info, force = false, onlyChunks = false, options = {}) {
|
|
297
323
|
return await this.serializedByFileEventPaths(
|
|
298
324
|
[getStoragePathFromUXFileInfo(info)],
|
|
299
|
-
() => this.storeFileToDBFromRevision(info, force, onlyChunks)
|
|
325
|
+
() => this.storeFileToDBFromRevision(info, force, onlyChunks, void 0, options.revalidateStorage)
|
|
300
326
|
);
|
|
301
327
|
}
|
|
302
328
|
async storeFileToDBWithBaseRevision(info, baseRevision, createIfDifferent = true) {
|
|
@@ -366,8 +392,8 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
366
392
|
/**
|
|
367
393
|
* Save one storage snapshot while the caller holds its document lock.
|
|
368
394
|
* Ordinary saves read once inside the lock and leave retries disabled.
|
|
369
|
-
* Restored
|
|
370
|
-
*
|
|
395
|
+
* Restored start-up events and recovery scans retain bounded rereads
|
|
396
|
+
* because a watcher event may not cover a concurrent edit.
|
|
371
397
|
*/
|
|
372
398
|
async storeFileToDBFromRevision(info, force = false, onlyChunks = false, preferredBasePath, retryIfStorageChanges = false, snapshotRetry = 0) {
|
|
373
399
|
const file = await this.infoToStub(info);
|
|
@@ -387,32 +413,36 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
387
413
|
return await this.db.createChunks(readFile2, force, true);
|
|
388
414
|
}
|
|
389
415
|
const observedProvenance = await this.readProvenanceSnapshot(
|
|
390
|
-
preferredBasePath ?? file.path
|
|
416
|
+
preferredBasePath ?? file.path,
|
|
417
|
+
retryIfStorageChanges
|
|
391
418
|
);
|
|
392
419
|
const readFile = await this.readFileFromStub(file);
|
|
393
420
|
const entry = await this.db.fetchEntry(file, void 0, true, true, true);
|
|
394
421
|
const currentEntry = entry || await this.db.fetchEntryMeta(file, void 0, true);
|
|
395
422
|
const conflictedRevs = await this.db.getConflictedRevs(file);
|
|
396
423
|
const isConflicted = conflictedRevs.length > 0;
|
|
397
|
-
|
|
424
|
+
const currentIsLive = currentEntry && !currentEntry.deleted && !currentEntry._deleted;
|
|
425
|
+
if (!force && (currentIsLive || retryIfStorageChanges)) {
|
|
426
|
+
if (retryIfStorageChanges && currentIsLive && !entry) return false;
|
|
398
427
|
const classification = await this.classifyStorageContent(
|
|
399
428
|
readFile,
|
|
400
429
|
currentEntry,
|
|
401
430
|
preferredBasePath,
|
|
402
431
|
observedProvenance,
|
|
403
|
-
entry ? { _rev: entry._rev, data: readContent(entry) } : void 0
|
|
432
|
+
entry ? { _rev: entry._rev, data: readContent(entry) } : void 0,
|
|
433
|
+
retryIfStorageChanges
|
|
404
434
|
);
|
|
405
|
-
const upgradeCurrentRecord = classification.kind === "unchanged" && !isConflicted && classification.revision === currentEntry._rev && !hasContentHash(observedProvenance);
|
|
435
|
+
const upgradeCurrentRecord = classification.kind === "unchanged" && !isConflicted && currentEntry && !currentEntry.deleted && !currentEntry._deleted && classification.revision === currentEntry._rev && !hasContentHash(observedProvenance);
|
|
406
436
|
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;
|
|
437
|
+
const snapshotMatches = retryIfStorageChanges || upgradeCurrentRecord ? await this.currentStorageSnapshotMatches(readFile, observedProvenance, retryIfStorageChanges) : true;
|
|
408
438
|
if (retryIfStorageChanges && !snapshotMatches) {
|
|
409
439
|
return await this.retryCurrentStorage(file.path, preferredBasePath, snapshotRetry);
|
|
410
440
|
}
|
|
411
441
|
if (classification.kind === "unchanged") {
|
|
412
442
|
if (isConflicted) {
|
|
413
443
|
await this.queueConflictCheckFor(file.path);
|
|
414
|
-
} else if (classification.revision !== currentEntry._rev) {
|
|
415
|
-
if (!
|
|
444
|
+
} else if (!currentEntry || currentEntry.deleted || currentEntry._deleted || classification.revision !== currentEntry._rev) {
|
|
445
|
+
if (!currentEntry) return false;
|
|
416
446
|
return await this.dbToStorageCore(file.path, file);
|
|
417
447
|
} else {
|
|
418
448
|
if (upgradeCurrentRecord && snapshotMatches) {
|
|
@@ -430,7 +460,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
430
460
|
const storedRevision2 = classification.kind === "edited" ? await this.db.storeWithBaseRevision(readFile, classification.revision, true) : await this.db.storeIndependentRevision(readFile, true);
|
|
431
461
|
if (storedRevision2 === false) return false;
|
|
432
462
|
if (preferredBasePath && preferredBasePath !== file.path) await this.deleteProvenance(preferredBasePath);
|
|
433
|
-
if (retryIfStorageChanges && !await this.finaliseStoredRevision(readFile, observedProvenance, storedRevision2)) {
|
|
463
|
+
if (retryIfStorageChanges && !await this.finaliseStoredRevision(readFile, observedProvenance, storedRevision2, true)) {
|
|
434
464
|
return await this.retryCurrentStorage(file.path, preferredBasePath, snapshotRetry);
|
|
435
465
|
}
|
|
436
466
|
if (!retryIfStorageChanges) await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime, readFile.body);
|
|
@@ -519,6 +549,12 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
519
549
|
() => this.deleteFileFromDBCore(info)
|
|
520
550
|
);
|
|
521
551
|
}
|
|
552
|
+
/** Complete a validated storage deletion when its database effect is already represented. */
|
|
553
|
+
async deleteFileFromDBIfPresent(info) {
|
|
554
|
+
const entry = await this.db.fetchEntryMeta(info, void 0, true);
|
|
555
|
+
if (!entry || entry.deleted || entry._deleted) return true;
|
|
556
|
+
return await this.deleteFileFromDBCore(info);
|
|
557
|
+
}
|
|
522
558
|
/**
|
|
523
559
|
* Record a storage deletion under the caller's document lock, preserving
|
|
524
560
|
* the displayed conflict branch when its provenance is known.
|
|
@@ -892,17 +928,17 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
892
928
|
this._log(`File ${path} is not exist on the database`, LOG_LEVEL_VERBOSE);
|
|
893
929
|
return false;
|
|
894
930
|
}
|
|
931
|
+
const docData = readContent(docRead);
|
|
932
|
+
const reflectedContent = createBlob(docData);
|
|
895
933
|
if (!settings.processSizeMismatchedFiles) {
|
|
896
|
-
if (docRead.size != 0 && docRead.size !==
|
|
934
|
+
if (docRead.size != 0 && docRead.size !== reflectedContent.size) {
|
|
897
935
|
this._log(
|
|
898
|
-
`File ${path} seems to be corrupted! Writing prevented. (${docRead.size} != ${
|
|
936
|
+
`File ${path} seems to be corrupted! Writing prevented. (${docRead.size} != ${reflectedContent.size})`,
|
|
899
937
|
LOG_LEVEL_NOTICE
|
|
900
938
|
);
|
|
901
939
|
return false;
|
|
902
940
|
}
|
|
903
941
|
}
|
|
904
|
-
const docData = readContent(docRead);
|
|
905
|
-
const reflectedContent = readAsBlob(docRead);
|
|
906
942
|
const reflectedEvidence = await this.contentEvidence(reflectedContent);
|
|
907
943
|
if (allowExistingConflicts && existDoc && !force) {
|
|
908
944
|
const readFile = await this.readFileFromStub(existDoc);
|
|
@@ -1080,14 +1116,23 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
1080
1116
|
const relatedPath = type === "RENAME" ? eventItem.oldPath : type === "DELETE" ? eventItem.renameTarget : void 0;
|
|
1081
1117
|
const eventPaths = relatedPath === void 0 ? [path] : [path, relatedPath];
|
|
1082
1118
|
return await this.serializedByFileEventPaths(eventPaths, async () => {
|
|
1083
|
-
if (!await this.isCurrentPathSelected(path))
|
|
1119
|
+
if (!await this.isCurrentPathSelected(path)) {
|
|
1120
|
+
return shouldBeIgnored(path) || !await this.vault.isTargetFile(path, { skipCaseCollisionCheck: true });
|
|
1121
|
+
}
|
|
1084
1122
|
switch (type) {
|
|
1085
1123
|
case "CREATE":
|
|
1086
|
-
case "CHANGED":
|
|
1087
|
-
|
|
1124
|
+
case "CHANGED": {
|
|
1125
|
+
const current = this.getExactCurrentFile(await this.storage.getStub(path), path);
|
|
1126
|
+
if (!current || this.vault.isFileSizeTooLarge(current.stat.size)) return true;
|
|
1127
|
+
return await this.storeFileToDBFromRevision(current);
|
|
1128
|
+
}
|
|
1088
1129
|
case "DELETE":
|
|
1089
|
-
|
|
1090
|
-
|
|
1130
|
+
try {
|
|
1131
|
+
if (!await this.canRecordStorageDeletion(item)) return true;
|
|
1132
|
+
} catch {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
return await this.deleteFileFromDBIfPresent(item.args.file);
|
|
1091
1136
|
case "RENAME":
|
|
1092
1137
|
return await this.renameFileInDBCore(
|
|
1093
1138
|
item.args.file,
|
|
@@ -1137,7 +1182,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
1137
1182
|
} catch (ex) {
|
|
1138
1183
|
this._log(`Could not validate deletion of ${path}; preserved the database entry`, LOG_LEVEL_NOTICE);
|
|
1139
1184
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
1140
|
-
|
|
1185
|
+
throw ex;
|
|
1141
1186
|
}
|
|
1142
1187
|
}
|
|
1143
1188
|
/**
|
|
@@ -1222,15 +1267,19 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
1222
1267
|
action = await this._planRestoredFileEvent(item, isSameDocument);
|
|
1223
1268
|
} catch (ex) {
|
|
1224
1269
|
this.logRestoredEventValidationFailure(path, ex);
|
|
1225
|
-
return
|
|
1270
|
+
return false;
|
|
1226
1271
|
}
|
|
1227
1272
|
switch (action.kind) {
|
|
1228
1273
|
case "none":
|
|
1229
1274
|
return true;
|
|
1230
|
-
case "
|
|
1231
|
-
return
|
|
1275
|
+
case "unresolved":
|
|
1276
|
+
return false;
|
|
1277
|
+
case "store": {
|
|
1278
|
+
const stored = await this.storeFileToDBFromRevision(action.file, false, false, void 0, true);
|
|
1279
|
+
return stored && action.complete !== false;
|
|
1280
|
+
}
|
|
1232
1281
|
case "delete":
|
|
1233
|
-
return await this.
|
|
1282
|
+
return await this.deleteFileFromDBIfPresent(action.path);
|
|
1234
1283
|
case "rename":
|
|
1235
1284
|
return await this.renameFileInDBCore(action.file, action.oldPath, true);
|
|
1236
1285
|
}
|
|
@@ -1266,6 +1315,10 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
1266
1315
|
if (!currentTarget2 || !await this.isCurrentFileSelected(currentTarget2)) {
|
|
1267
1316
|
return { kind: "none" };
|
|
1268
1317
|
}
|
|
1318
|
+
const entry = await this.db.fetchEntryMeta(newPath, void 0, true);
|
|
1319
|
+
if (entry && !entry.deleted && !entry._deleted && this.storage.normalisePath(this.getPath(entry)) === this.storage.normalisePath(newPath)) {
|
|
1320
|
+
return { kind: "store", file: currentTarget2 };
|
|
1321
|
+
}
|
|
1269
1322
|
return { kind: "rename", file: currentTarget2, oldPath };
|
|
1270
1323
|
}
|
|
1271
1324
|
const currentTargetItem = await this.storage.getStub(newPath);
|
|
@@ -1283,11 +1336,16 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
1283
1336
|
return { kind: "none" };
|
|
1284
1337
|
}
|
|
1285
1338
|
if (!sourceWasInspected || currentSourceItem !== null || !await this.canApplyRestoredDeletion(oldPath)) {
|
|
1339
|
+
return { kind: "store", file: currentTarget, complete: sourceWasInspected };
|
|
1340
|
+
}
|
|
1341
|
+
const sourceEntry = await this.db.fetchEntryMeta(oldPath, void 0, true);
|
|
1342
|
+
if (!sourceEntry || sourceEntry.deleted || sourceEntry._deleted) {
|
|
1286
1343
|
return { kind: "store", file: currentTarget };
|
|
1287
1344
|
}
|
|
1288
1345
|
return { kind: "rename", file: currentTarget, oldPath };
|
|
1289
1346
|
}
|
|
1290
|
-
if (!sourceWasInspected
|
|
1347
|
+
if (!sourceWasInspected) return { kind: "unresolved" };
|
|
1348
|
+
if (currentSourceItem !== null || !await this.canApplyRestoredDeletion(oldPath)) {
|
|
1291
1349
|
return { kind: "none" };
|
|
1292
1350
|
}
|
|
1293
1351
|
return { kind: "delete", path: oldPath };
|
|
@@ -1324,12 +1382,12 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
1324
1382
|
return await this.isCurrentPathSelected(path);
|
|
1325
1383
|
} catch (ex) {
|
|
1326
1384
|
this.logRestoredEventValidationFailure(path, ex);
|
|
1327
|
-
|
|
1385
|
+
throw ex;
|
|
1328
1386
|
}
|
|
1329
1387
|
}
|
|
1330
1388
|
logRestoredEventValidationFailure(path, ex) {
|
|
1331
1389
|
this._log(
|
|
1332
|
-
`Could not validate the saved storage operation for ${path} against current storage; the
|
|
1390
|
+
`Could not validate the saved storage operation for ${path} against current storage; the operation remains pending for recovery`,
|
|
1333
1391
|
LOG_LEVEL_NOTICE
|
|
1334
1392
|
);
|
|
1335
1393
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
@@ -60,7 +60,7 @@ class AppLifecycleService extends ServiceBase {
|
|
|
60
60
|
* Event triggered before the plug-in is unloaded.
|
|
61
61
|
* This is typically used to perform any necessary cleanup or save state before the plug-in is unloaded.
|
|
62
62
|
*/
|
|
63
|
-
onBeforeUnload = handlers().
|
|
63
|
+
onBeforeUnload = handlers().allSettled("onBeforeUnload");
|
|
64
64
|
/**
|
|
65
65
|
* Event triggered when the plug-in is being unloaded.
|
|
66
66
|
*
|
|
@@ -40,7 +40,9 @@ class ControlService extends ServiceBase {
|
|
|
40
40
|
await this.services.appLifecycleService.onSuspending();
|
|
41
41
|
await this.services.settingService.onBeforeRealiseSetting();
|
|
42
42
|
this.services.databaseService.localDatabase.refreshSettings();
|
|
43
|
-
await this.services.fileProcessingService.commitPendingFileEvents()
|
|
43
|
+
if (!await this.services.fileProcessingService.commitPendingFileEvents()) {
|
|
44
|
+
throw new Error("Pending local file operations could not be completed before applying settings");
|
|
45
|
+
}
|
|
44
46
|
await this.services.settingService.onRealiseSetting();
|
|
45
47
|
if (this.services.appLifecycleService.isSuspended()) return;
|
|
46
48
|
await this.services.appLifecycleService.onResuming();
|
|
@@ -49,7 +51,8 @@ class ControlService extends ServiceBase {
|
|
|
49
51
|
}
|
|
50
52
|
async _onLiveSyncUnload() {
|
|
51
53
|
this.context.events.emitEvent(EVENT_PLUGIN_UNLOADED);
|
|
52
|
-
await this.services.appLifecycleService.onBeforeUnload();
|
|
54
|
+
const prepared = await this.services.appLifecycleService.onBeforeUnload();
|
|
55
|
+
if (!prepared) this._log("Unload preparation failed; pending storage work may require recovery", LOG_LEVEL_URGENT);
|
|
53
56
|
await Promise.resolve(this.services.appLifecycleService.onAppUnload());
|
|
54
57
|
cancelAllPeriodicTask();
|
|
55
58
|
cancelAllTasks();
|
|
@@ -64,6 +67,7 @@ class ControlService extends ServiceBase {
|
|
|
64
67
|
this.context.events.emitEvent(EVENT_PLATFORM_UNLOADED);
|
|
65
68
|
this.context.events.offAll();
|
|
66
69
|
this._log(this.context.translate("moduleLiveSyncMain.logUnloadingPlugin"));
|
|
70
|
+
if (!prepared) throw new Error("Unload preparation failed");
|
|
67
71
|
return;
|
|
68
72
|
}
|
|
69
73
|
/**
|
|
@@ -34,5 +34,5 @@ export declare abstract class DatabaseEventService<T extends ServiceContext = Se
|
|
|
34
34
|
* @param reopenDatabase Whether to reopen the database if it is already open.
|
|
35
35
|
* @param ignoreSuspending Whether to ignore any suspending state.
|
|
36
36
|
*/
|
|
37
|
-
readonly initialiseDatabase: import("../lib/HandlerUtils.js").MultipleHandlerFunction<(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean) => Promise<import("./VaultScanResult.js").VaultScanResult>, unknown>;
|
|
37
|
+
readonly initialiseDatabase: import("../lib/HandlerUtils.js").MultipleHandlerFunction<(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean, scanOptions?: import("./VaultScanResult.js").VaultScanOptions) => Promise<import("./VaultScanResult.js").VaultScanResult>, unknown>;
|
|
38
38
|
}
|
|
@@ -9,10 +9,16 @@ export declare class FileProcessingService<T extends ServiceContext = ServiceCon
|
|
|
9
9
|
*/
|
|
10
10
|
readonly processFileEvent: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(item: import("../../common/types.js").FileEventItem) => Promise<boolean>>;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* Dispatch a legacy optional notification, without confirming persistence.
|
|
13
13
|
* i.e., hidden files synchronisation or customisation sync.
|
|
14
14
|
*/
|
|
15
15
|
readonly processOptionalFileEvent: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(path: import("../../common/types.js").FilePath) => Promise<boolean>>;
|
|
16
|
+
/**
|
|
17
|
+
* Complete optional file operations through the first applicable handler.
|
|
18
|
+
* Hosts migrate all optional providers together. Once any typed provider is
|
|
19
|
+
* registered, dispatch uses this contract exclusively.
|
|
20
|
+
*/
|
|
21
|
+
readonly processOptionalFileEventWithResult?: import("../lib/HandlerUtils.js").FirstHandledFunction<(path: import("../../common/types.js").FilePath, options?: import("./IService.js").OptionalFileEventOptions) => Promise<import("../lib/HandlerUtils.js").HandlerOutcome<void, string>>>;
|
|
16
22
|
/**
|
|
17
23
|
* Commit any pending file events that have been queued for processing.
|
|
18
24
|
*/
|
|
@@ -7,10 +7,18 @@ class FileProcessingService extends ServiceBase {
|
|
|
7
7
|
*/
|
|
8
8
|
processFileEvent = handlers().anySuccess("processFileEvent");
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Dispatch a legacy optional notification, without confirming persistence.
|
|
11
11
|
* i.e., hidden files synchronisation or customisation sync.
|
|
12
12
|
*/
|
|
13
13
|
processOptionalFileEvent = handlers().anySuccess("processOptionalFileEvent");
|
|
14
|
+
/**
|
|
15
|
+
* Complete optional file operations through the first applicable handler.
|
|
16
|
+
* Hosts migrate all optional providers together. Once any typed provider is
|
|
17
|
+
* registered, dispatch uses this contract exclusively.
|
|
18
|
+
*/
|
|
19
|
+
processOptionalFileEventWithResult = handlers().firstHandled(
|
|
20
|
+
"processOptionalFileEventWithResult"
|
|
21
|
+
);
|
|
14
22
|
/**
|
|
15
23
|
* Commit any pending file events that have been queued for processing.
|
|
16
24
|
*/
|
|
@@ -16,8 +16,8 @@ import type { OwnedCouchDBConnection, RemoteConnectionOpenOptions } from "./Remo
|
|
|
16
16
|
import type { ActiveReplicatorContext, ContinuousReplicationRequest, ReplicationAttemptFailure, ReplicationFailureRequest, ReplicatorProviderDefinitionMap, ReplicationOutcome, ReplicationReadinessRequirements, UnattendedOneShotRequest, UserInitiatedOneShotRequest } from "../../replication/ReplicatorProvider.js";
|
|
17
17
|
import type { RemoteResourceKind, RemoteResourceMap } from "../../replication/RemoteResource.js";
|
|
18
18
|
import type { CentralRemoteAdministrationRequest, CentralRemoteAdministrationResult } from "../../replication/CentralRemoteAdministration.js";
|
|
19
|
-
import type { MultipleHandlerFunction } from "../lib/HandlerUtils.js";
|
|
20
|
-
import type { VaultScanResult } from "./VaultScanResult.js";
|
|
19
|
+
import type { HandlerOutcome, MultipleHandlerFunction } from "../lib/HandlerUtils.js";
|
|
20
|
+
import type { VaultScanResult, VaultScanOptions } from "./VaultScanResult.js";
|
|
21
21
|
import type { UICommandRegistry } from "../../uiCommands.js";
|
|
22
22
|
declare global {
|
|
23
23
|
interface OPTIONAL_SYNC_FEATURES {
|
|
@@ -91,15 +91,23 @@ export interface IDatabaseEventService {
|
|
|
91
91
|
onDatabaseInitialised(showNotice: boolean): Promise<boolean>;
|
|
92
92
|
onDatabaseHasReady(): Promise<boolean>;
|
|
93
93
|
onResetDatabase(db: LiveSyncLocalDB): Promise<boolean>;
|
|
94
|
-
initialiseDatabase(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean): Promise<VaultScanResult>;
|
|
94
|
+
initialiseDatabase(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean, scanOptions?: VaultScanOptions): Promise<VaultScanResult>;
|
|
95
95
|
}
|
|
96
96
|
export interface IKeyValueDBService {
|
|
97
97
|
openSimpleStore<T>(kind: string): SimpleStore<T>;
|
|
98
98
|
simpleStore: SimpleStore<unknown>;
|
|
99
99
|
}
|
|
100
|
+
/** Options for completing an admitted optional file event. */
|
|
101
|
+
export interface OptionalFileEventOptions {
|
|
102
|
+
/** Resolves when a finite flush needs this event to finish without its debounce delay. */
|
|
103
|
+
whenFlushRequested?: Promise<void>;
|
|
104
|
+
}
|
|
100
105
|
export interface IFileProcessingService {
|
|
101
106
|
processFileEvent(item: FileEventItem): Promise<boolean>;
|
|
107
|
+
/** Legacy notification contract; its boolean does not confirm persistence. */
|
|
102
108
|
processOptionalFileEvent(path: FilePath): Promise<boolean>;
|
|
109
|
+
/** Complete the actual optional file operation, or explain why it is not completed. */
|
|
110
|
+
processOptionalFileEventWithResult?(path: FilePath, options?: OptionalFileEventOptions): Promise<HandlerOutcome<void, string>>;
|
|
103
111
|
commitPendingFileEvents(): Promise<boolean>;
|
|
104
112
|
batched: ReactiveSource<number>;
|
|
105
113
|
processing: ReactiveSource<number>;
|
|
@@ -347,7 +355,7 @@ export interface ITweakValueService {
|
|
|
347
355
|
export interface IVaultService {
|
|
348
356
|
vaultName(): string;
|
|
349
357
|
getVaultName(): string;
|
|
350
|
-
scanVault(showingNotice?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean): Promise<VaultScanResult>;
|
|
358
|
+
scanVault(showingNotice?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean, scanOptions?: VaultScanOptions): Promise<VaultScanResult>;
|
|
351
359
|
isIgnoredByIgnoreFile(file: string | UXFileInfoStub): Promise<boolean>;
|
|
352
360
|
isTargetFile(file: string | UXFileInfoStub,
|
|
353
361
|
/** Inspect selection policy without treating a filename collision as an exclusion. */
|
|
@@ -125,5 +125,6 @@ export declare abstract class ReplicatorService<T extends ServiceContext = Servi
|
|
|
125
125
|
lastSyncPullSeq: number;
|
|
126
126
|
lastSyncPushSeq: number;
|
|
127
127
|
syncStatus: import("../../common/types.js").DatabaseConnectingStatus;
|
|
128
|
+
couchDB?: import("../../common/models/shared.definition.js").CouchDBReplicationProgress;
|
|
128
129
|
}>;
|
|
129
130
|
}
|
|
@@ -10,4 +10,9 @@ export declare const VaultScanResults: {
|
|
|
10
10
|
readonly COMPLETED: true;
|
|
11
11
|
readonly COMPLETED_WITH_FILE_FAILURES: "completed-with-file-failures";
|
|
12
12
|
};
|
|
13
|
+
/** Optional policy for an ordinary scan; omitted options retain strict content checks. */
|
|
14
|
+
export interface VaultScanOptions {
|
|
15
|
+
/** False permits unchanged tracked files to be skipped using storage and revision metadata. */
|
|
16
|
+
strictContentCheck?: boolean;
|
|
17
|
+
}
|
|
13
18
|
export type VaultScanResult = (typeof VaultScanResults)[keyof typeof VaultScanResults];
|
|
@@ -26,7 +26,7 @@ export declare abstract class VaultService<T extends ServiceContext = ServiceCon
|
|
|
26
26
|
* @param showingNotice Whether to show a notice to the user.
|
|
27
27
|
* @param ignoreSuspending Whether to ignore any suspending state.
|
|
28
28
|
*/
|
|
29
|
-
readonly scanVault: import("../lib/HandlerUtils.js").MultipleHandlerFunction<(showingNotice?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean) => Promise<import("./VaultScanResult.js").VaultScanResult>, unknown>;
|
|
29
|
+
readonly scanVault: import("../lib/HandlerUtils.js").MultipleHandlerFunction<(showingNotice?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean, scanOptions?: import("./VaultScanResult.js").VaultScanOptions) => Promise<import("./VaultScanResult.js").VaultScanResult>, unknown>;
|
|
30
30
|
/**
|
|
31
31
|
* Check if a file is ignored by the ignore file (e.g., .gitignore, .obsidianignore).
|
|
32
32
|
* @param file The file path or file info stub to check.
|
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
* A function type that can be used as a handler.
|
|
3
3
|
*/
|
|
4
4
|
type HandlerFunc<TArg extends unknown[], TResult> = (...args: TArg) => TResult | Promise<TResult>;
|
|
5
|
+
/**
|
|
6
|
+
* The outcome of a handler.
|
|
7
|
+
*
|
|
8
|
+
* A completed outcome carries a value when `TValue` is not void, and a veto
|
|
9
|
+
* carries a reason when `TVetoReason` is not void. Not-applicable outcomes
|
|
10
|
+
* always continue to the next registered handler.
|
|
11
|
+
*/
|
|
12
|
+
export type HandlerOutcome<TValue = void, TVetoReason = void> = {
|
|
13
|
+
kind: "not-applicable";
|
|
14
|
+
} | ([TValue] extends [void] ? {
|
|
15
|
+
kind: "completed";
|
|
16
|
+
} : {
|
|
17
|
+
kind: "completed";
|
|
18
|
+
value: TValue;
|
|
19
|
+
}) | ([TVetoReason] extends [void] ? {
|
|
20
|
+
kind: "veto";
|
|
21
|
+
} : {
|
|
22
|
+
kind: "veto";
|
|
23
|
+
value: TVetoReason;
|
|
24
|
+
});
|
|
25
|
+
type AnyHandlerOutcome = HandlerOutcome | HandlerOutcome<unknown, unknown>;
|
|
5
26
|
/**
|
|
6
27
|
* A function type that returns a boolean or a Promise of boolean.
|
|
7
28
|
*/
|
|
@@ -244,6 +265,29 @@ export declare class FirstResultHandler<T extends unknown[], U> extends MultiBin
|
|
|
244
265
|
*/
|
|
245
266
|
invoke(...args: T): Promise<U | false>;
|
|
246
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Invokes registered handlers in priority and registration order until one
|
|
270
|
+
* completes or vetoes. Each invocation uses a snapshot of registrations.
|
|
271
|
+
* Callback errors and rejections propagate unchanged, and registrations are
|
|
272
|
+
* owned by this handler.
|
|
273
|
+
*/
|
|
274
|
+
export declare class FirstHandledHandler<T extends unknown[], U extends AnyHandlerOutcome = HandlerOutcome> extends MultiBinder<HandlerFunc<T, U>> implements InvokableHandler<T, U | {
|
|
275
|
+
kind: "not-applicable";
|
|
276
|
+
}> {
|
|
277
|
+
/**
|
|
278
|
+
* Whether at least one callback is currently registered.
|
|
279
|
+
*/
|
|
280
|
+
get hasHandlers(): boolean;
|
|
281
|
+
/**
|
|
282
|
+
* Invokes handlers until one returns a completed or veto outcome.
|
|
283
|
+
* @param args The arguments to pass to the handlers.
|
|
284
|
+
* @returns The first handled outcome, or a not-applicable outcome when no handler applies.
|
|
285
|
+
* @throws Any synchronous callback error or rejected callback value, unchanged.
|
|
286
|
+
*/
|
|
287
|
+
invoke(...args: T): Promise<U | {
|
|
288
|
+
kind: "not-applicable";
|
|
289
|
+
}>;
|
|
290
|
+
}
|
|
247
291
|
/**
|
|
248
292
|
* A function type that can be used as a handler with assignable functionality.
|
|
249
293
|
*/
|
|
@@ -297,6 +341,11 @@ export interface MultipleHandlerFunction<TFunc extends (...args: any[]) => U | P
|
|
|
297
341
|
*/
|
|
298
342
|
removeHandler: (callback: TFunc) => void;
|
|
299
343
|
}
|
|
344
|
+
export type FirstHandledFunction<TFunc extends (...args: any[]) => AnyHandlerOutcome | Promise<AnyHandlerOutcome>> = ((...args: Parameters<TFunc>) => Promise<Awaited<ReturnType<TFunc>> | {
|
|
345
|
+
kind: "not-applicable";
|
|
346
|
+
}>) & Pick<MultipleHandlerFunction<TFunc>, "addHandler" | "removeHandler"> & {
|
|
347
|
+
readonly hasHandlers: boolean;
|
|
348
|
+
};
|
|
300
349
|
/**
|
|
301
350
|
* A function type that can be used as a value-collecting handler with add/remove functionality.
|
|
302
351
|
*/
|
|
@@ -351,12 +400,16 @@ export declare function bailFirstFailureWithResultFunction<TFunc extends (...arg
|
|
|
351
400
|
export declare function allParallelFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
|
|
352
401
|
export declare function anySuccessFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
|
|
353
402
|
export declare function firstResultFunction<TFunc extends (...args: any[]) => Promise<unknown>>(name?: string): MultipleHandlerFunction<TFunc>;
|
|
403
|
+
export declare function firstHandledFunction<TFunc extends (...args: any[]) => AnyHandlerOutcome | Promise<AnyHandlerOutcome>>(name?: string): FirstHandledFunction<TFunc>;
|
|
354
404
|
export declare function dispatchParallelFunction<TFunc extends (...args: any[]) => Promise<unknown[]>>(name?: string): CollectiveHandlerFunction<TFunc>;
|
|
355
405
|
export declare function bindableFunction<TFunc extends (...args: any[]) => any>(name?: string): HandlerFunction<TFunc>;
|
|
356
406
|
export declare function lazyBindableFunction<TFunc extends (...args: any[]) => any>(name?: string): LazyHandlerFunction<TFunc>;
|
|
357
407
|
type FunctionKeys<T> = Extract<{
|
|
358
408
|
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
|
|
359
409
|
}[keyof T], string>;
|
|
410
|
+
type FirstHandledFunctionKeys<T> = Extract<{
|
|
411
|
+
[K in keyof T]: [NonNullable<T[K]>] extends [never] ? never : NonNullable<T[K]> extends (...args: any[]) => AnyHandlerOutcome | Promise<AnyHandlerOutcome> ? K : never;
|
|
412
|
+
}[keyof T], string>;
|
|
360
413
|
export declare function handlers<T extends object>(): {
|
|
361
414
|
/**
|
|
362
415
|
* Create a handler that invokes all added handler functions sequentially until one returns false.
|
|
@@ -403,6 +456,13 @@ export declare function handlers<T extends object>(): {
|
|
|
403
456
|
* @returns
|
|
404
457
|
*/
|
|
405
458
|
firstResult<K extends FunctionKeys<T>>(name: K): MultipleHandlerFunction<Extract<T[K], (...args: any[]) => Promise<unknown>>>;
|
|
459
|
+
/**
|
|
460
|
+
* Create a handler that invokes registered handlers until one completes or vetoes.
|
|
461
|
+
* Callback errors and rejections propagate unchanged.
|
|
462
|
+
* @param name The name of the callback on `T`.
|
|
463
|
+
* @returns A bound handler with a live `hasHandlers` property.
|
|
464
|
+
*/
|
|
465
|
+
firstHandled<K extends FirstHandledFunctionKeys<T>>(name: K): FirstHandledFunction<Extract<NonNullable<T[K]>, (...args: any[]) => AnyHandlerOutcome | Promise<AnyHandlerOutcome>>>;
|
|
406
466
|
/**
|
|
407
467
|
* Create a handler that invokes all added handler functions in parallel.
|
|
408
468
|
* @param name
|