mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
feat(pfapi): deal with sync edge case
This commit is contained in:
parent
12d1307e47
commit
463e1dce7d
6 changed files with 122 additions and 35 deletions
|
|
@ -4,6 +4,11 @@ const config: CapacitorConfig = {
|
|||
appId: 'com.superproductivity.superproductivity',
|
||||
appName: 'super-productivity',
|
||||
webDir: 'dist/browser',
|
||||
plugins: {
|
||||
CapacitorHttp: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
|
|
@ -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 }> {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue