fix(sync): scope interval timer to file-based providers and drop unneeded migration

Guard the periodic sync interval timer behind a useIntervalTimer flag so it
only fires for file-based providers (WebDAV, LocalFile, Nextcloud). SuperSync
uses WebSocket push notifications and does not need polling.

Remove the syncFolderPath backwards-compatibility migration: master already
routes localFileSync settings to the credential store via PROP_MAP_TO_FORM,
so no real users have the path in global config.
This commit is contained in:
Johannes Millan 2026-04-14 22:01:19 +02:00
parent 08fdea1772
commit d6880d47da
4 changed files with 15 additions and 75 deletions

View file

@ -3,7 +3,7 @@ import { SyncProviderManager } from '../../op-log/sync-providers/provider-manage
import { GlobalConfigService } from '../../features/config/global-config.service';
import { combineLatest, from, Observable, of } from 'rxjs';
import { SyncConfig } from '../../features/config/global-config.model';
import { first, switchMap, tap } from 'rxjs/operators';
import { switchMap, tap } from 'rxjs/operators';
import {
CurrentProviderPrivateCfg,
PrivateCfgByProviderId,
@ -14,7 +14,6 @@ import { SyncLog } from '../../core/log';
import { clearSessionKeyCache } from '../../op-log/encryption/encryption';
import { SuperSyncPrivateCfg } from '../../op-log/sync-providers/super-sync/super-sync.model';
import { SyncWrapperService } from './sync-wrapper.service';
import { LocalFileSyncPrivateCfg } from '../../op-log/core/types/sync.types';
// Maps sync providers to their corresponding form field in SyncConfig
// Dropbox is null because it doesn't store settings in the form (uses OAuth)
@ -103,19 +102,6 @@ export class SyncConfigService {
private _lastSettings: SyncConfig | null = null;
constructor() {
// One-time startup migration: move syncFolderPath from legacy global config to credential
// store. Must run at startup (not only when sync settings UI is open) so that the app
// correctly finds the configured folder after a restart. Fixes: #5455 (Mac restart).
this._globalConfigService.sync$
.pipe(first((cfg) => !!cfg?.localFileSync?.syncFolderPath))
.subscribe((cfg) => {
this._migrateLegacyLocalFileSyncPath(cfg.localFileSync!.syncFolderPath!).catch(
(e) => SyncLog.err('SyncConfigService: LocalFile path migration failed', e),
);
});
}
private _deriveEncryptionState(
baseConfig: SyncConfig,
currentProviderCfg: CurrentProviderPrivateCfg | null,
@ -423,47 +409,4 @@ export class SyncConfigService {
clearSessionKeyCache();
}
}
/**
* Migrates syncFolderPath from the legacy global config storage to SyncCredentialStore.
*
* Older versions stored the LocalFile sync folder path in globalConfig.sync.localFileSync.
* The current architecture stores it in SyncCredentialStore (IndexedDB 'sup-sync').
* This one-time migration preserves the user's configured folder across upgrades.
* Fixes: Mac forgets sync folder on restart (#5455).
*/
private async _migrateLegacyLocalFileSyncPath(legacyPath: string): Promise<void> {
SyncLog.normal(
'SyncConfigService: Checking for legacy LocalFile syncFolderPath migration',
);
const provider = await this._providerManager.getProviderById(
SyncProviderId.LocalFile,
);
if (!provider) {
// Provider not registered (e.g. web build). Clear the stale field anyway so
// it never triggers migration again and doesn't silently persist.
SyncLog.warn(
'SyncConfigService: LocalFile provider not found during migration; clearing legacy field',
);
} else {
const existingCfg =
(await provider.privateCfg.load()) as LocalFileSyncPrivateCfg | null;
if (!existingCfg?.syncFolderPath) {
SyncLog.normal(
'SyncConfigService: Migrating LocalFile syncFolderPath from global config to credential store',
);
await provider.privateCfg.upsertPartial({
syncFolderPath: legacyPath,
} as LocalFileSyncPrivateCfg);
}
}
// Always clear the legacy path from global config so this migration never re-runs.
// Spread DEFAULT to avoid shallow-merge data loss on any other localFileSync fields.
this._globalConfigService.updateSection('sync', {
localFileSync: {
...DEFAULT_GLOBAL_CONFIG.sync.localFileSync,
syncFolderPath: null,
},
});
}
}

View file

@ -206,7 +206,10 @@ export class SyncTriggerService {
shareReplay(1),
);
getSyncTrigger$(syncInterval: number = SYNC_DEFAULT_AUDIT_TIME): Observable<unknown> {
getSyncTrigger$(
syncInterval: number = SYNC_DEFAULT_AUDIT_TIME,
useIntervalTimer = false,
): Observable<unknown> {
const _immediateSyncTrigger$: Observable<string> = IS_ANDROID_WEB_VIEW
? // ANDROID ONLY
merge(
@ -233,9 +236,11 @@ export class SyncTriggerService {
this._onElectronResumeTrigger$,
// Periodic interval timer: fires every syncInterval ms to detect external file
// changes (e.g. Syncthing on Linux) even without user activity.
// Applies to Electron desktop and web/PWA (not Android, which has its own timer).
// Only for file-based providers — SuperSync uses WebSocket push and doesn't need polling.
// Fixes: Linux auto-sync ignoring interval (#4783)
timer(syncInterval, syncInterval).pipe(mapTo('I_INTERVAL_TIMER')),
...(useIntervalTimer
? [timer(syncInterval, syncInterval).pipe(mapTo('I_INTERVAL_TIMER'))]
: []),
);
return merge(
// once immediately

View file

@ -135,10 +135,14 @@ export class SyncEffects {
combineLatest([
this._syncWrapperService.isEnabledAndReady$,
this._syncWrapperService.syncInterval$,
this._syncWrapperService.syncProviderId$,
]).pipe(
switchMap(([isEnabledAndReady, syncInterval]) =>
switchMap(([isEnabledAndReady, syncInterval, providerId]) =>
isEnabledAndReady && syncInterval
? this._syncTriggerService.getSyncTrigger$(syncInterval)
? this._syncTriggerService.getSyncTrigger$(
syncInterval,
providerId !== SyncProviderId.SuperSync,
)
: EMPTY,
),
),

View file

@ -81,7 +81,6 @@ import { Log } from './app/core/log';
import { OperationWriteFlushService } from './app/op-log/sync/operation-write-flush.service';
import { PluginOAuthRedirectHandler } from './app/plugins/oauth/plugin-oauth-redirect.handler';
import { OAuthCallbackHandlerService } from './app/imex/sync/oauth-callback-handler.service';
import { SyncConfigService } from './app/imex/sync/sync-config.service';
import { GlobalConfigService } from './app/features/config/global-config.service';
import { LocaleDatePipe } from './app/ui/pipes/locale-date.pipe';
import { DateTimeFormatService } from './app/core/date-time-format/date-time-format.service';
@ -281,17 +280,6 @@ bootstrapApplication(AppComponent, {
deps: [OAuthCallbackHandlerService],
multi: true,
},
// Ensure SyncConfigService is instantiated at bootstrap so the startup migration
// (syncFolderPath from global config → credential store) runs on every app launch,
// not only when the sync settings UI is opened. Fixes: #5455 (Mac restart).
{
provide: APP_INITIALIZER,
useFactory: (_svc: SyncConfigService) => {
return () => {};
},
deps: [SyncConfigService],
multi: true,
},
// Note: ImmediateUploadService now initializes itself in constructor
// after DataInitStateService.isAllDataLoadedInitially$ fires to avoid
// race condition where upload attempts happen before sync config is loaded