@vrtmrz/livesync-commonlib 0.2.1 → 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.
Files changed (32) hide show
  1. package/dist/API/processSetting.d.ts +1 -0
  2. package/dist/common/models/setting.const.defaults.js +1 -0
  3. package/dist/common/models/setting.const.qr.js +2 -0
  4. package/dist/common/models/setting.policy.d.ts +4 -0
  5. package/dist/common/models/setting.policy.js +1 -0
  6. package/dist/common/models/setting.type.d.ts +4 -0
  7. package/dist/common/models/tweak.definition.d.ts +1 -0
  8. package/dist/common/settingConstants.d.ts +1 -0
  9. package/dist/common/settingConstants.js +4 -0
  10. package/dist/interfaces/FileHandler.d.ts +2 -0
  11. package/dist/interfaces/StorageAccess.d.ts +2 -0
  12. package/dist/interfaces/StorageEventManager.d.ts +2 -0
  13. package/dist/interfaces/StorageEventManager.js +4 -0
  14. package/dist/managers/StorageEventManager.d.ts +1 -0
  15. package/dist/managers/StorageEventManager.js +9 -0
  16. package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
  17. package/dist/serviceFeatures/offlineScanner.d.ts +4 -4
  18. package/dist/serviceFeatures/offlineScanner.js +11 -6
  19. package/dist/serviceFeatures/prepareDatabaseForUse.d.ts +3 -2
  20. package/dist/serviceFeatures/prepareDatabaseForUse.js +5 -4
  21. package/dist/serviceModules/ServiceFileAccessBase.d.ts +1 -0
  22. package/dist/serviceModules/ServiceFileAccessBase.js +3 -0
  23. package/dist/serviceModules/ServiceFileHandlerBase.d.ts +1 -0
  24. package/dist/serviceModules/ServiceFileHandlerBase.js +21 -0
  25. package/dist/services/base/DatabaseEventService.d.ts +1 -1
  26. package/dist/services/base/IService.d.ts +3 -3
  27. package/dist/services/base/VaultScanResult.d.ts +5 -0
  28. package/dist/services/base/VaultService.d.ts +1 -1
  29. package/dist/worker/bgWorker.js +1 -1
  30. package/docs/offline-scanner.md +12 -4
  31. package/docs/storage-events-and-reflection.md +6 -2
  32. package/package.json +1 -1
@@ -62,6 +62,7 @@ export declare function decodeSettingsFromSetupURI(uri: string, passphrase: stri
62
62
  batchSaveInLiveSync?: boolean;
63
63
  syncOnSave: boolean;
64
64
  syncOnStart: boolean;
65
+ strictContentCheckOnStartup?: boolean;
65
66
  syncOnFileOpen: boolean;
66
67
  syncOnEditorSave: boolean;
67
68
  keepReplicationActiveInBackground: boolean;
@@ -43,6 +43,7 @@ const SETTINGS_SCHEMA_DEFAULTS = {
43
43
  batchSaveInLiveSync: false,
44
44
  syncOnSave: false,
45
45
  syncOnStart: false,
46
+ strictContentCheckOnStartup: false,
46
47
  savingDelay: 200,
47
48
  lessInformationInLog: false,
48
49
  gcDelay: 300,
@@ -51,6 +51,8 @@ const KeyIndexOfSettings = {
51
51
  liveSync: 49,
52
52
  syncOnSave: 50,
53
53
  syncOnStart: 51,
54
+ strictContentCheckOnStartup: -1,
55
+ // Installation-local preference; do not encode into the QR Code.
54
56
  syncOnFileOpen: 52,
55
57
  syncOnEditorSave: 53,
56
58
  keepReplicationActiveInBackground: -1,
@@ -65,6 +65,10 @@ export declare const SETTINGS_PROPERTY_POLICY: {
65
65
  readonly markdown: "include";
66
66
  readonly persistence: "include";
67
67
  };
68
+ readonly strictContentCheckOnStartup: {
69
+ readonly markdown: "omit";
70
+ readonly persistence: "include";
71
+ };
68
72
  readonly syncOnFileOpen: {
69
73
  readonly markdown: "include";
70
74
  readonly persistence: "include";
@@ -17,6 +17,7 @@ const SETTINGS_PROPERTY_POLICY = {
17
17
  batchSaveInLiveSync: SettingPolicies.Default,
18
18
  syncOnSave: SettingPolicies.Default,
19
19
  syncOnStart: SettingPolicies.Default,
20
+ strictContentCheckOnStartup: SettingPolicies.LocalOnly,
20
21
  syncOnFileOpen: SettingPolicies.Default,
21
22
  syncOnEditorSave: SettingPolicies.Default,
22
23
  keepReplicationActiveInBackground: SettingPolicies.Default,
@@ -91,6 +91,10 @@ interface SyncMethodSettings {
91
91
  * automatically run sync on starting the plug-in.
92
92
  */
93
93
  syncOnStart: boolean;
94
+ /**
95
+ * Compare file contents during startup checks when modification time and size match.
96
+ */
97
+ strictContentCheckOnStartup?: boolean;
94
98
  /**
95
99
  * automatically run sync on opening a file.
96
100
  */
@@ -145,6 +145,7 @@ export declare const TweakValuesTemplate: {
145
145
  batchSaveInLiveSync?: boolean;
146
146
  syncOnSave?: boolean;
147
147
  syncOnStart?: boolean;
148
+ strictContentCheckOnStartup?: boolean;
148
149
  syncOnFileOpen?: boolean;
149
150
  syncOnEditorSave?: boolean;
150
151
  keepReplicationActiveInBackground?: boolean;
@@ -33,6 +33,7 @@ export declare const AllSettingDefault: {
33
33
  batchSaveInLiveSync?: boolean;
34
34
  syncOnSave: boolean;
35
35
  syncOnStart: boolean;
36
+ strictContentCheckOnStartup?: boolean;
36
37
  syncOnFileOpen: boolean;
37
38
  syncOnEditorSave: boolean;
38
39
  keepReplicationActiveInBackground: boolean;
@@ -104,6 +104,10 @@ const SettingInformation = {
104
104
  name: "Sync on Startup",
105
105
  desc: "Automatically Sync all files when opening Obsidian."
106
106
  },
107
+ strictContentCheckOnStartup: {
108
+ name: "Strict content check on startup",
109
+ desc: "Compare file contents even when modification time and size match. This can slow startup in large vaults."
110
+ },
107
111
  syncAfterMerge: {
108
112
  name: "Sync after merging file",
109
113
  desc: "Sync automatically after merging files"
@@ -12,6 +12,8 @@ export interface IFileHandler {
12
12
  * operation never stores a document revision or writes to storage.
13
13
  */
14
14
  tryRecordUntrackedFileRevision(info: UXFileInfoStub | UXFileInfo, expectedRevision: string): Promise<boolean>;
15
+ /** Metadata-only scan hint; never certifies content equality or changes provenance. */
16
+ canSkipUnchangedFile?(info: UXFileInfoStub, expected: MetaEntry): Promise<boolean>;
15
17
  storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean, options?: StoreFileOptions): Promise<boolean>;
16
18
  /**
17
19
  * Store the current storage content as a child of an exact live database revision.
@@ -12,6 +12,8 @@ export interface IStorageAccessManager {
12
12
  export interface StorageAccess {
13
13
  normalisePath(path: string): string;
14
14
  restoreState(): Promise<void>;
15
+ /** Includes queued, running, and failed work; absence requires a conservative content check. */
16
+ hasPendingFileEvent?(path: FilePath): boolean;
15
17
  deleteVaultItem(file: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise<void>;
16
18
  renameFile(file: UXFileInfoStub | FilePathWithPrefix, newPath: FilePathWithPrefix): Promise<UXFileInfoStub | null>;
17
19
  writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<boolean>;
@@ -11,6 +11,8 @@ export declare abstract class StorageEventManager {
11
11
  abstract beginWatch(): Promise<void>;
12
12
  abstract appendQueue(items: FileEvent[], ctx?: unknown): Promise<void>;
13
13
  abstract isWaiting(filename: FilePath): boolean;
14
+ /** Compatibility hosts without an ownership view must retain content checks. */
15
+ hasPendingFileEvent?(_filename: FilePath): boolean;
14
16
  abstract waitForIdle(): Promise<void>;
15
17
  abstract restoreState(): Promise<void>;
16
18
  /**
@@ -1,4 +1,8 @@
1
1
  class StorageEventManager {
2
+ /** Compatibility hosts without an ownership view must retain content checks. */
3
+ hasPendingFileEvent(_filename) {
4
+ return true;
5
+ }
2
6
  /**
3
7
  * Complete accepted work before its storage closes. Custom hosts must stop
4
8
  * their watcher admission before invoking this default implementation.
@@ -134,6 +134,7 @@ export declare abstract class StorageEventManagerBase<TAdapter extends IStorageE
134
134
  protected runQueuedEvents(options?: RunQueuedEventsOptions): Promise<void>;
135
135
  protected processingCount: number;
136
136
  protected requestProcessQueue(fei: FileEventItem): Promise<void>;
137
+ hasPendingFileEvent(filename: FilePath): boolean;
137
138
  isWaiting(filename: FilePath): boolean;
138
139
  protected handleFileEvent(queue: FileEventItem): Promise<void>;
139
140
  protected cancelRelativeEvent(item: FileEventItem): void;
@@ -530,6 +530,15 @@ class StorageEventManagerBase extends StorageEventManager {
530
530
  this.updateStatus();
531
531
  }
532
532
  }
533
+ hasPendingFileEvent(filename) {
534
+ for (const entry of this.pendingSnapshot.values()) {
535
+ if (entry.completed || entry.event.type === TYPE_SENTINEL_FLUSH) continue;
536
+ const event = entry.event;
537
+ const relatedPath = event.type === "RENAME" ? event.args.oldPath : event.args.renameTarget;
538
+ if ([event.args.file.path, relatedPath].some((path) => path && (filename === path || filename.startsWith(`${path}/`)))) return true;
539
+ }
540
+ return false;
541
+ }
533
542
  isWaiting(filename) {
534
543
  return this._waitingMap.has(filename);
535
544
  }
@@ -147,6 +147,7 @@ export declare class TrysteroReplicator {
147
147
  batchSaveInLiveSync?: boolean;
148
148
  syncOnSave?: boolean;
149
149
  syncOnStart?: boolean;
150
+ strictContentCheckOnStartup?: boolean;
150
151
  syncOnFileOpen?: boolean;
151
152
  syncOnEditorSave?: boolean;
152
153
  keepReplicationActiveInBackground?: boolean;
@@ -336,6 +337,7 @@ export declare class TrysteroReplicator {
336
337
  batchSaveInLiveSync?: boolean;
337
338
  syncOnSave?: boolean;
338
339
  syncOnStart?: boolean;
340
+ strictContentCheckOnStartup?: boolean;
339
341
  syncOnFileOpen?: boolean;
340
342
  syncOnEditorSave?: boolean;
341
343
  keepReplicationActiveInBackground?: boolean;
@@ -3,7 +3,7 @@ import { type LogFunction } from "../services/lib/logUtils.js";
3
3
  import type { NecessaryServices } from "../interfaces/ServiceModule.js";
4
4
  import type { IPathService } from "../services/base/IService.js";
5
5
  import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
6
- import { VaultScanResults, type VaultScanResult } from "../services/base/VaultScanResult.js";
6
+ import { VaultScanResults, type VaultScanResult, type VaultScanOptions } from "../services/base/VaultScanResult.js";
7
7
  export { VaultScanResults };
8
8
  export type { VaultScanResult };
9
9
  /**
@@ -156,7 +156,7 @@ export declare function getPathFromEntry(host: {
156
156
  * @param file Storage file information
157
157
  * @param doc Database entry
158
158
  */
159
- export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry): Promise<FilePairProcessResult>;
159
+ export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry, scanOptions?: VaultScanOptions): Promise<FilePairProcessResult>;
160
160
  export declare function canProceedScan(host: NecessaryServices<"keyValueDB" | "setting", never>, errorManager: UnresolvedErrorManager, log: LogFunction, showingNotice?: boolean, ignoreSuspending?: boolean): boolean;
161
161
  /**
162
162
  * Normalise the logical comparison key and lower case it when filename case should be handled insensitively.
@@ -180,7 +180,7 @@ export declare function collectDatabaseFiles(host: NecessaryServices<"database"
180
180
  }>;
181
181
  export declare function updateToDatabase(host: NecessaryServices<"vault", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, file: UXFileInfoStub): Promise<FilePairProcessResult>;
182
182
  export declare function updateToStorage(host: NecessaryServices<"vault" | "path", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, w: MetaEntry): Promise<FilePairProcessResult>;
183
- export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry): Promise<FilePairProcessResult>;
183
+ export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry, scanOptions?: VaultScanOptions): Promise<FilePairProcessResult>;
184
184
  export declare const FullScanModes: {
185
185
  readonly DB_APPLY: "db-apply";
186
186
  readonly NEWER_WINS: "newer-wins";
@@ -205,7 +205,7 @@ export declare const ExtraOnLocal: {
205
205
  */
206
206
  readonly APPEND_STORAGE_ONLY: "append-storage-only";
207
207
  };
208
- export interface FullScanOptions {
208
+ export interface FullScanOptions extends VaultScanOptions {
209
209
  mode: FullScanMode;
210
210
  extraOnLocal?: (typeof ExtraOnLocal)[keyof typeof ExtraOnLocal];
211
211
  extraOnRemote?: (typeof ExtraOnRemote)[keyof typeof ExtraOnRemote];
@@ -350,7 +350,7 @@ function getPathFromEntry(host, doc) {
350
350
  const path = host.services.path.getPath(doc);
351
351
  return path;
352
352
  }
353
- async function syncFileBetweenDBandStorage(host, log, file, doc) {
353
+ async function syncFileBetweenDBandStorage(host, log, file, doc, scanOptions) {
354
354
  const docPath = getPathFromEntry(host, doc);
355
355
  if (!doc) {
356
356
  throw new Error(`Missing doc:${docPath}`);
@@ -393,6 +393,10 @@ async function syncFileBetweenDBandStorage(host, log, file, doc) {
393
393
  log(`STORAGE ? DB : ${file.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE);
394
394
  return FilePairProcessResults.SKIPPED;
395
395
  }
396
+ if (scanOptions?.strictContentCheck === false && await host.serviceModules.fileHandler.canSkipUnchangedFile?.(file, doc)) {
397
+ log(`STORAGE ? DB : ${file.path} skipped using unchanged storage and revision metadata`, LOG_LEVEL_DEBUG);
398
+ return FilePairProcessResults.SKIPPED;
399
+ }
396
400
  if (doc._rev && await host.serviceModules.fileHandler.tryRecordUntrackedFileRevision(file, doc._rev)) {
397
401
  log("STORAGE == DB :" + file.path, LOG_LEVEL_DEBUG);
398
402
  return FilePairProcessResults.COMPLETED;
@@ -549,13 +553,13 @@ async function updateToStorage(host, log, logLevel, w) {
549
553
  return FilePairProcessResults.FAILED;
550
554
  }
551
555
  }
552
- async function syncStorageAndDatabase(host, log, file, logLevel, doc) {
556
+ async function syncStorageAndDatabase(host, log, file, logLevel, doc, scanOptions) {
553
557
  if ((doc._conflicts?.length ?? 0) > 0) {
554
558
  log(`SYNC DATABASE AND STORAGE: ${file.path} has conflicts. skipped`, LOG_LEVEL_INFO);
555
559
  return FilePairProcessResults.SKIPPED;
556
560
  }
557
561
  if (!host.services.vault.isFileSizeTooLarge(file.stat.size) && !host.services.vault.isFileSizeTooLarge(doc.size)) {
558
- return await syncFileBetweenDBandStorage(host, log, file, doc);
562
+ return await syncFileBetweenDBandStorage(host, log, file, doc, scanOptions);
559
563
  } else {
560
564
  log(
561
565
  `SYNC DATABASE AND STORAGE: ${getPathFromEntry(host, doc)} has been skipped due to file size exceeding the limit`,
@@ -698,7 +702,7 @@ async function processFilePair(host, log, pair, options) {
698
702
  if (!file || !doc) {
699
703
  throw new Error(`Cannot compare freshness for ${path}`);
700
704
  }
701
- const syncResult = await syncStorageAndDatabase(host, log, file, LOG_LEVEL_INFO, doc);
705
+ const syncResult = await syncStorageAndDatabase(host, log, file, LOG_LEVEL_INFO, doc, options);
702
706
  if (syncResult === FilePairProcessResults.COMPLETED) {
703
707
  updateFileMTimeInMap(host, fileMapKey, Math.max(file.stat.mtime, doc.mtime));
704
708
  }
@@ -922,11 +926,12 @@ async function performFullScan(host, log, errorManager, showingNoticeOrOptions =
922
926
  function useOfflineScanner(host) {
923
927
  const log = createInstanceLogFunction("SF:OfflineScanner", host.services.API);
924
928
  const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events);
925
- const handleScanVault = async (showingNotice, ignoreSuspending = false, continueOnFileFailure = false) => {
929
+ const handleScanVault = async (showingNotice, ignoreSuspending = false, continueOnFileFailure = false, scanOptions) => {
926
930
  return await performFullScan(host, log, errorManager, {
927
931
  showingNotice,
928
932
  ignoreSuspending,
929
- continueOnFileFailure
933
+ continueOnFileFailure,
934
+ strictContentCheck: scanOptions?.strictContentCheck
930
935
  });
931
936
  };
932
937
  host.services.vault.scanVault.addHandler(handleScanVault);
@@ -1,7 +1,7 @@
1
1
  import type { NecessaryServices } from "../interfaces/ServiceModule.js";
2
2
  import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
3
3
  import { type LogFunction } from "../services/lib/logUtils.js";
4
- import { type VaultScanResult } from "../services/base/VaultScanResult.js";
4
+ import { type VaultScanResult, type VaultScanOptions } from "../services/base/VaultScanResult.js";
5
5
  /**
6
6
  * Initialise the database and trigger a full vault scan.
7
7
  * @param host Services container
@@ -11,9 +11,10 @@ import { type VaultScanResult } from "../services/base/VaultScanResult.js";
11
11
  * @param reopenDatabase Whether to reopen the database connection
12
12
  * @param ignoreSuspending Whether to ignore suspension settings
13
13
  * @param continueOnFileFailure Whether individual file failures may satisfy application readiness
14
+ * @param scanOptions Optional content-check policy; omitted options keep strict checks
14
15
  * @returns The initialisation outcome, including accepted individual file failures
15
16
  */
16
- export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean): Promise<VaultScanResult>;
17
+ export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean, scanOptions?: VaultScanOptions): Promise<VaultScanResult>;
17
18
  /**
18
19
  * Associate the initialiser file feature with the app lifecycle events.
19
20
  * This function binds initialization handlers to the appropriate lifecycle events.
@@ -6,7 +6,7 @@ function reportPreparationFailure(log, message) {
6
6
  log(message, LOG_LEVEL_VERBOSE);
7
7
  return false;
8
8
  }
9
- async function prepareDatabaseForUse(host, log, errorManager, showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false) {
9
+ async function prepareDatabaseForUse(host, log, errorManager, showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false, scanOptions) {
10
10
  const appLifecycle = host.services.appLifecycle;
11
11
  appLifecycle.resetIsReady();
12
12
  try {
@@ -25,7 +25,7 @@ async function prepareDatabaseForUse(host, log, errorManager, showingNotice = fa
25
25
  "Database preparation stopped because the local database is not ready."
26
26
  );
27
27
  }
28
- const scanResult = await host.services.vault.scanVault(showingNotice, ignoreSuspending, continueOnFileFailure);
28
+ const scanResult = await host.services.vault.scanVault(showingNotice, ignoreSuspending, continueOnFileFailure, scanOptions);
29
29
  if (scanResult === VaultScanResults.FAILED) {
30
30
  return reportPreparationFailure(log, "Database preparation stopped because the Vault scan failed.");
31
31
  }
@@ -57,7 +57,7 @@ async function prepareDatabaseForUse(host, log, errorManager, showingNotice = fa
57
57
  function usePrepareDatabaseForUse(host) {
58
58
  const log = createInstanceLogFunction("SF:prepareDatabaseForUse", host.services.API);
59
59
  const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events);
60
- const initialiseDatabaseHandler = async (showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false) => {
60
+ const initialiseDatabaseHandler = async (showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false, scanOptions) => {
61
61
  return await prepareDatabaseForUse(
62
62
  host,
63
63
  log,
@@ -65,7 +65,8 @@ function usePrepareDatabaseForUse(host) {
65
65
  showingNotice,
66
66
  reopenDatabase,
67
67
  ignoreSuspending,
68
- continueOnFileFailure
68
+ continueOnFileFailure,
69
+ scanOptions
69
70
  );
70
71
  };
71
72
  host.services.databaseEvents.initialiseDatabase.addHandler(initialiseDatabaseHandler);
@@ -26,6 +26,7 @@ export declare class ServiceFileAccessBase<TAdapter extends IFileSystemAdapter<a
26
26
  private vault;
27
27
  private setting;
28
28
  constructor(services: StorageAccessBaseDependencies<TAdapter>);
29
+ hasPendingFileEvent(path: FilePath): boolean;
29
30
  restoreState(): Promise<void>;
30
31
  _everyOnFirstInitialize(): Promise<boolean>;
31
32
  _everyCommitPendingFileEvent(): Promise<boolean>;
@@ -21,6 +21,9 @@ class ServiceFileAccessBase extends ServiceModuleBase {
21
21
  return true;
22
22
  });
23
23
  }
24
+ hasPendingFileEvent(path) {
25
+ return this.vaultManager.hasPendingFileEvent?.(path) ?? true;
26
+ }
24
27
  restoreState() {
25
28
  return this.vaultManager.restoreState();
26
29
  }
@@ -98,6 +98,7 @@ export declare abstract class ServiceFileHandlerBase extends ServiceModuleBase<S
98
98
  getPath(entry: AnyEntry): FilePathWithPrefix;
99
99
  getPathWithoutPrefix(entry: AnyEntry): FilePathWithPrefix;
100
100
  readFileFromStub(file: UXFileInfoStub | UXFileInfo): Promise<UXFileInfo>;
101
+ canSkipUnchangedFile(info: UXFileInfoStub, expected: MetaEntry): Promise<boolean>;
101
102
  tryRecordUntrackedFileRevision(info: UXFileInfoStub | UXFileInfo, expectedRevision: string): Promise<boolean>;
102
103
  private infoToStub;
103
104
  storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean, options?: StoreFileOptions): Promise<boolean>;
@@ -258,6 +258,27 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
258
258
  }
259
259
  return readFile;
260
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
+ }
261
282
  async tryRecordUntrackedFileRevision(info, expectedRevision) {
262
283
  if (!this.fileReflectionProvenance || !expectedRevision) return false;
263
284
  const path = getStoragePathFromUXFileInfo(info);
@@ -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
  }
@@ -17,7 +17,7 @@ import type { ActiveReplicatorContext, ContinuousReplicationRequest, Replication
17
17
  import type { RemoteResourceKind, RemoteResourceMap } from "../../replication/RemoteResource.js";
18
18
  import type { CentralRemoteAdministrationRequest, CentralRemoteAdministrationResult } from "../../replication/CentralRemoteAdministration.js";
19
19
  import type { HandlerOutcome, MultipleHandlerFunction } from "../lib/HandlerUtils.js";
20
- import type { VaultScanResult } from "./VaultScanResult.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,7 +91,7 @@ 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>;
@@ -355,7 +355,7 @@ export interface ITweakValueService {
355
355
  export interface IVaultService {
356
356
  vaultName(): string;
357
357
  getVaultName(): string;
358
- scanVault(showingNotice?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean): Promise<VaultScanResult>;
358
+ scanVault(showingNotice?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean, scanOptions?: VaultScanOptions): Promise<VaultScanResult>;
359
359
  isIgnoredByIgnoreFile(file: string | UXFileInfoStub): Promise<boolean>;
360
360
  isTargetFile(file: string | UXFileInfoStub,
361
361
  /** Inspect selection policy without treating a filename collision as an exclusion. */
@@ -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.