feat(pfapi): deal with sync edge case

This commit is contained in:
Johannes Millan 2025-03-26 18:18:18 +01:00
parent 12d1307e47
commit 463e1dce7d
6 changed files with 122 additions and 35 deletions

View file

@ -4,6 +4,11 @@ const config: CapacitorConfig = {
appId: 'com.superproductivity.superproductivity',
appName: 'super-productivity',
webDir: 'dist/browser',
plugins: {
CapacitorHttp: {
enabled: true,
},
},
};
export default config;

View file

@ -81,7 +81,7 @@ export class SyncService {
try {
this._globalProgressBarService.countUp('SYNC');
this.isSyncing$.next(true);
const r = await this._pfapiService.pf.sync();
const r = await this._pfapiService.sync();
this.isSyncing$.next(false);
this._globalProgressBarService.countDown();
@ -116,24 +116,13 @@ export class SyncService {
}).toPromise();
if (res === 'USE_LOCAL') {
this._globalProgressBarService.countUp('SYNC');
this.isSyncing$.next(true);
await this._pfapiService.pf.uploadAll();
this.isSyncing$.next(false);
this._globalProgressBarService.countDown();
return SyncStatus.UpdateLocalAll;
await this._pfapiService.uploadAll();
return SyncStatus.UpdateRemoteAll;
} else if (res === 'USE_REMOTE') {
this._globalProgressBarService.countUp('SYNC');
this.isSyncing$.next(true);
await this._pfapiService.pf.downloadAll();
await this._reInitAppAfterDataModelChange();
this.isSyncing$.next(false);
this._globalProgressBarService.countDown();
return SyncStatus.UpdateLocalAll;
await this._pfapiService.downloadAll();
}
console.log({ res });
// TODO implement and test force cases
// if (!this._c(T.F.SYNC.C.EMPTY_SYNC)) {
// TODO implement and test force cases
@ -165,6 +154,8 @@ export class SyncService {
// msg: T.F.SYNC.S.INCOMPLETE_CFG,
msg: 'Remote Data is currently being written',
type: 'ERROR',
actionFn: async () => this._forceUpload(),
actionStr: 'Force Overwrite',
});
return 'HANDLED_ERROR';
} else {
@ -192,6 +183,37 @@ export class SyncService {
}
}
private async _forceUpload(): Promise<void> {
if (
!this._c(
this._translateService.instant(
'Forcing an upload of your data could lead to data loss. Continue?',
),
)
) {
return;
}
this._globalProgressBarService.countUp('SYNC');
this.isSyncing$.next(true);
try {
await this._pfapiService.uploadAll(true);
this.isSyncing$.next(false);
this._globalProgressBarService.countDown();
} catch (e) {
const errStr = getSyncErrorStr(e);
this.isSyncing$.next(false);
this._globalProgressBarService.countDown();
this._snackService.open({
// msg: T.F.SYNC.S.UNKNOWN_ERROR,
msg: errStr,
type: 'ERROR',
translateParams: {
err: errStr,
},
});
}
}
async configuredAuthForSyncProviderIfNecessary(
providerId: SyncProviderId,
): Promise<{ wasConfigured: boolean }> {

View file

@ -118,6 +118,13 @@ export class Pfapi<const MD extends ModelCfgs> {
}
}
async forceUploadAll(): Promise<{ status: SyncStatus; conflictData?: ConflictData }> {
pfLog(2, `${this.forceUploadAll.name}()`);
const result = await this._syncService.uploadAll(true);
pfLog(2, `${this.forceUploadAll.name}() result:`, result);
return { status: SyncStatus.UpdateRemoteAll };
}
setActiveSyncProvider(activeProviderId: SyncProviderId | null): void {
pfLog(2, `${this.setActiveSyncProvider.name}()`, activeProviderId, activeProviderId);
if (activeProviderId) {

View file

@ -81,8 +81,7 @@ export class Dropbox implements SyncProviderServiceInterface<DropboxPrivateCfg>
}
throw new AuthFailSPError('Dropbox 401 getFileRev', targetPath);
} else {
console.error(e);
throw new Error(e as any);
throw e;
}
}
}

View file

@ -18,9 +18,9 @@ import {
LockFileFromLocalClientPresentError,
LockFilePresentError,
ModelVersionToImportNewerThanLocalError,
RemoteFileNotFoundAPIError,
NoRemoteMetaFile,
NoSyncProviderSetError,
RemoteFileNotFoundAPIError,
RevMapModelMismatchErrorOnDownload,
RevMapModelMismatchErrorOnUpload,
RevMismatchError,
@ -90,7 +90,7 @@ export class SyncService<const MD extends ModelCfgs> {
}
// NOTE: for cascading mode we don't need to check the lock file before
const [{ remoteMeta, remoteRev }] = this.IS_MAIN_FILE_MODE
const [{ remoteMeta, remoteMetaRev }] = this.IS_MAIN_FILE_MODE
? await Promise.all([this._downloadMetaFile(localMeta0.metaRev)])
: // since we delete the lock file only AFTER writing the meta file, we can safely execute these in parallel
// NOTE: a race condition introduced is, that one error might pop up before the other
@ -114,7 +114,7 @@ export class SyncService<const MD extends ModelCfgs> {
r: remoteMeta.lastUpdate && new Date(remoteMeta.lastUpdate).toISOString(),
remoteMetaFileContent: remoteMeta,
localSyncMetaData: localMeta,
remoteRev,
remoteMetaRev,
},
);
@ -145,7 +145,7 @@ export class SyncService<const MD extends ModelCfgs> {
await this.updateLocal(
remoteMeta,
localMeta,
remoteRev,
remoteMetaRev,
// NOTE: because we checked lock file above for multi file mode
!this.IS_MAIN_FILE_MODE,
);
@ -155,6 +155,7 @@ export class SyncService<const MD extends ModelCfgs> {
await this.updateRemote(
remoteMeta,
localMeta,
remoteMetaRev,
// NOTE: because we checked lock file above for multi file mode
!this.IS_MAIN_FILE_MODE,
);
@ -203,6 +204,7 @@ export class SyncService<const MD extends ModelCfgs> {
revMap: {},
},
{ ...local, revMap: this._fakeFullRevMap() },
null,
isSkipLockFileCheck,
);
} catch (e) {
@ -217,7 +219,7 @@ export class SyncService<const MD extends ModelCfgs> {
async downloadAll(isSkipLockFileCheck = false): Promise<void> {
alert('DOWNLOAD ALL TO LOCAL');
const local = await this._metaModelCtrl.loadMetaModel();
const { remoteMeta, remoteRev } = await this._downloadMetaFile();
const { remoteMeta, remoteMetaRev } = await this._downloadMetaFile();
const fakeLocal: LocalMeta = {
// NOTE: we still need to use local modelVersions here, since they contain the latest model versions for migrations
crossModelVersion: local.crossModelVersion,
@ -227,7 +229,12 @@ export class SyncService<const MD extends ModelCfgs> {
metaRev: null,
revMap: {},
};
return await this.updateLocal(remoteMeta, fakeLocal, remoteRev, isSkipLockFileCheck);
return await this.updateLocal(
remoteMeta,
fakeLocal,
remoteMetaRev,
isSkipLockFileCheck,
);
}
// --------------------------------------------------
@ -375,10 +382,11 @@ export class SyncService<const MD extends ModelCfgs> {
async updateRemote(
remote: RemoteMeta,
local: LocalMeta,
lastRemoteRev: string | null = null,
isSkipLockFileCheck = false,
): Promise<void> {
if (this.IS_MAIN_FILE_MODE) {
return this._updateRemoteMAIN(remote, local, isSkipLockFileCheck);
return this._updateRemoteMAIN(remote, local, lastRemoteRev, isSkipLockFileCheck);
} else {
return this._updateRemoteMULTI(remote, local, isSkipLockFileCheck);
}
@ -387,6 +395,7 @@ export class SyncService<const MD extends ModelCfgs> {
async _updateRemoteMAIN(
remote: RemoteMeta,
local: LocalMeta,
lastRemoteRev: string | null = null,
isSkipLockFileCheck = false,
): Promise<void> {
pfLog(2, `${SyncService.name}.${this._updateRemoteMAIN.name}()`, {
@ -403,13 +412,28 @@ export class SyncService<const MD extends ModelCfgs> {
if (toUpdate.length === 0 && toDelete.length === 0) {
const mainModelData = await this._getMainFileModelData();
const metaRevAfterUpdate = await this._uploadMetaFile({
revMap: local.revMap,
lastUpdate: local.lastUpdate,
crossModelVersion: local.crossModelVersion,
modelVersions: local.modelVersions,
mainModelData,
});
// NOT necessary when there is a lastRemoteRev, since there are only 4 cases:
// 1. no conflicting update going on
// 2. there is a meta file only update going on
// ===> rev match error for the slower client will occur
// 3. there is a full update going on and the meta file was written already in the meantime
// ===> rev match error here
// 4. there is a full update going on and the meta file was not written yet
// ===> the remote meta file will be overwritten and next sync, will result in a sync conflict, which will be dealt in a full sync
// -------------------------
if (!lastRemoteRev && !isSkipLockFileCheck) {
await this._checkLockFileQuick();
}
const metaRevAfterUpdate = await this._uploadMetaFile(
{
revMap: local.revMap,
lastUpdate: local.lastUpdate,
crossModelVersion: local.crossModelVersion,
modelVersions: local.modelVersions,
mainModelData,
},
lastRemoteRev,
);
// ON AFTER SUCCESS
await this._saveLocalMetaFileContent({
...local,
@ -617,7 +641,7 @@ export class SyncService<const MD extends ModelCfgs> {
// ----------
private async _uploadMetaFile(
meta: RemoteMeta,
rev: string | null = null,
revToMatch: string | null = null,
): Promise<string> {
const encryptedAndCompressedData = await this._compressAndeEncryptData(
validateMetaBase(meta),
@ -638,7 +662,7 @@ export class SyncService<const MD extends ModelCfgs> {
await syncProvider.uploadFile(
MetaModelCtrl.META_MODEL_REMOTE_FILE_NAME,
encryptedAndCompressedData,
rev,
revToMatch,
true,
)
).rev;
@ -664,7 +688,7 @@ export class SyncService<const MD extends ModelCfgs> {
private async _downloadMetaFile(
localRev?: string | null,
): Promise<{ remoteMeta: RemoteMeta; remoteRev: string }> {
): Promise<{ remoteMeta: RemoteMeta; remoteMetaRev: string }> {
// return {} as any as MetaFileContent;
pfLog(2, `${SyncService.name}.${this._downloadMetaFile.name}()`, { localRev });
const syncProvider = this._getCurrentSyncProviderOrError();
@ -676,7 +700,7 @@ export class SyncService<const MD extends ModelCfgs> {
const data = await this._decompressAndDecryptData<RemoteMeta>(r.dataStr);
console.log(data);
return { remoteMeta: validateMetaBase(data), remoteRev: r.rev };
return { remoteMeta: validateMetaBase(data), remoteMetaRev: r.rev };
} catch (e) {
if (e instanceof RemoteFileNotFoundAPIError) {
throw new NoRemoteMetaFile();
@ -760,6 +784,33 @@ export class SyncService<const MD extends ModelCfgs> {
}
// --------------------------------------------------
private async _checkLockFileQuick(): Promise<void> {
pfLog(2, `${SyncService.name}.${this._checkLockFileQuick.name}()`);
const syncProvider = this._getCurrentSyncProviderOrError();
try {
await syncProvider.getFileRev(LOCK_FILE_NAME, null);
throw new LockFilePresentError();
} catch (e) {
// NOTE this is what we want :)
if (e instanceof RemoteFileNotFoundAPIError) {
return;
}
if (e instanceof LockFilePresentError) {
const res = await syncProvider.downloadFile(LOCK_FILE_NAME, null).catch(() => {
console.error(e);
throw new LockFileEmptyOrMessedUpError();
});
const localClientId = await this._metaModelCtrl.loadClientId();
if (res.dataStr && res.dataStr === localClientId) {
throw new LockFileFromLocalClientPresentError();
}
throw new LockFilePresentError();
}
throw e;
}
}
private async _awaitLockFilePermissionAndWrite(): Promise<void> {
pfLog(2, `${SyncService.name}.${this._awaitLockFilePermissionAndWrite.name}()`);
const syncProvider = this._getCurrentSyncProviderOrError();

View file

@ -93,6 +93,9 @@ export class PfapiService {
private _invalidDataCount = 0;
sync = this.pf.sync.bind(this.pf);
uploadAll = this.pf.uploadAll.bind(this.pf);
downloadAll = this.pf.downloadAll.bind(this.pf);
getAllSyncModelData = this.pf.getAllSyncModelData.bind(this.pf);
importAllSycModelData = this.pf.importAllSycModelData.bind(this.pf);
isValidateComplete = this.pf.isValidateComplete.bind(this.pf);