You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-29 22:48:10 +02:00
Desktop, Mobile: Automatically retrigger the sync if there are more unsynced outgoing changes when sync completes (#12989)
This commit is contained in:
@@ -387,7 +387,7 @@ export default class Synchronizer {
|
|||||||
// 2. DELETE_REMOTE: Delete on the sync target, the items that have been deleted locally.
|
// 2. DELETE_REMOTE: Delete on the sync target, the items that have been deleted locally.
|
||||||
// 3. DELTA: Find on the sync target the items that have been modified or deleted and apply the changes locally.
|
// 3. DELTA: Find on the sync target the items that have been modified or deleted and apply the changes locally.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
public async start(options: any = null) {
|
public async start(options: any = null): Promise<any> {
|
||||||
if (!options) options = {};
|
if (!options) options = {};
|
||||||
|
|
||||||
if (this.state() !== 'idle') {
|
if (this.state() !== 'idle') {
|
||||||
@@ -476,6 +476,7 @@ export default class Synchronizer {
|
|||||||
|
|
||||||
let errorToThrow = null;
|
let errorToThrow = null;
|
||||||
let syncLock = null;
|
let syncLock = null;
|
||||||
|
let hasCaughtError = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api().initialize();
|
await this.api().initialize();
|
||||||
@@ -611,12 +612,25 @@ export default class Synchronizer {
|
|||||||
|
|
||||||
// Safety check to avoid infinite loops.
|
// Safety check to avoid infinite loops.
|
||||||
// - In fact this error is possible if the item is marked for sync (via sync_time or force_sync) while synchronisation is in
|
// - In fact this error is possible if the item is marked for sync (via sync_time or force_sync) while synchronisation is in
|
||||||
// progress. In that case exit anyway to be sure we aren't in a loop and the item will be re-synced next time.
|
// progress. When force_sync is not true, this is because the user is typing while the sync is running, so we should continue
|
||||||
|
// looping, as we don't want the sync to stop when there are still un-synced outgoing changes, otherwise this creates a race condition
|
||||||
|
// on mobile, where additional changes made during upload are not synced and don't trigger another sync, whereas a change made immediately
|
||||||
|
// after the sync has finished will trigger another sync. Once the user has stopped typing, it can then break out of the loop and continue
|
||||||
|
// the rest of the process.
|
||||||
// - It can also happen if the item is directly modified in the sync target, and set with an update_time in the future. In that case,
|
// - It can also happen if the item is directly modified in the sync target, and set with an update_time in the future. In that case,
|
||||||
// the local sync_time will be updated to Date.now() but on the next loop it will see that the remote item still has a date ahead
|
// the local sync_time will be updated to Date.now() but on the next loop it will see that the remote item still has a date ahead
|
||||||
// and will see a conflict. There's currently no automatic fix for this - the remote item on the sync target must be fixed manually
|
// and will see a conflict. There's currently no automatic fix for this - the remote item on the sync target must be fixed manually
|
||||||
// (by setting an updated_time less than current time).
|
// (by setting an updated_time less than current time).
|
||||||
if (donePaths.indexOf(path) >= 0) throw new JoplinError(sprintf('Processing a path that has already been done: %s. sync_time was not updated? Remote item has an updated_time in the future?', path), 'processingPathTwice');
|
if (donePaths.indexOf(path) >= 0) {
|
||||||
|
const syncItem = await BaseItem.syncItem(syncTargetId, local.id, { fields: ['force_sync'] });
|
||||||
|
if (local.updated_time > time.unixMs()) {
|
||||||
|
throw new JoplinError(sprintf('Processing a path that has already been done: %s. Remote item has an updated_time in the future', path), 'processingPathTwice');
|
||||||
|
} else if (syncItem.force_sync) {
|
||||||
|
throw new JoplinError(sprintf('Processing a path that has already been done: %s. Item was marked for sync using force_sync', path), 'processingPathTwice');
|
||||||
|
} else {
|
||||||
|
logger.info(sprintf('Processing a path that has already been done: %s. The user is making changes while the sync is in progress', path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const remote: RemoteItem = result.neverSyncedItemIds.includes(local.id) ? null : await this.apiCall('stat', path);
|
const remote: RemoteItem = result.neverSyncedItemIds.includes(local.id) ? null : await this.apiCall('stat', path);
|
||||||
let action: SyncAction = null;
|
let action: SyncAction = null;
|
||||||
@@ -1121,6 +1135,8 @@ export default class Synchronizer {
|
|||||||
}
|
}
|
||||||
} // DELTA STEP
|
} // DELTA STEP
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
hasCaughtError = true;
|
||||||
|
|
||||||
if (error.code === ErrorCode.MustUpgradeApp) {
|
if (error.code === ErrorCode.MustUpgradeApp) {
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
type: 'MUST_UPGRADE_APP',
|
type: 'MUST_UPGRADE_APP',
|
||||||
@@ -1163,9 +1179,12 @@ export default class Synchronizer {
|
|||||||
|
|
||||||
this.syncTargetIsLocked_ = false;
|
this.syncTargetIsLocked_ = false;
|
||||||
|
|
||||||
|
let cancelledBeforeClearedState = false;
|
||||||
|
|
||||||
if (this.cancelling()) {
|
if (this.cancelling()) {
|
||||||
logger.info('Synchronisation was cancelled.');
|
logger.info('Synchronisation was cancelled.');
|
||||||
this.cancelling_ = false;
|
this.cancelling_ = false;
|
||||||
|
cancelledBeforeClearedState = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.progressReport_.completedTime = time.unixMs();
|
this.progressReport_.completedTime = time.unixMs();
|
||||||
@@ -1174,8 +1193,10 @@ export default class Synchronizer {
|
|||||||
|
|
||||||
await this.logSyncSummary(this.progressReport_);
|
await this.logSyncSummary(this.progressReport_);
|
||||||
|
|
||||||
|
const hasErrors = Synchronizer.reportHasErrors(this.progressReport_);
|
||||||
|
|
||||||
eventManager.emit(EventName.SyncComplete, {
|
eventManager.emit(EventName.SyncComplete, {
|
||||||
withErrors: Synchronizer.reportHasErrors(this.progressReport_),
|
withErrors: hasErrors,
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
@@ -1190,6 +1211,19 @@ export default class Synchronizer {
|
|||||||
|
|
||||||
if (errorToThrow) throw errorToThrow;
|
if (errorToThrow) throw errorToThrow;
|
||||||
|
|
||||||
|
// If there are any un-synced outgoing changes made up to the point just before the sync completes, then trigger the sync again to reduce the likelihood
|
||||||
|
// that the user will close or minimise the app when there are un-synced changes, because the sync is reported as completed.
|
||||||
|
// IMPORTANT: This must be the very last step in the sync, to avoid any window to allow an un-synced change to get missed
|
||||||
|
if (!hasErrors && !hasCaughtError && !cancelledBeforeClearedState && !this.cancelling()) {
|
||||||
|
const result = await BaseItem.itemsThatNeedSync(syncTargetId);
|
||||||
|
options.context = outputContext;
|
||||||
|
|
||||||
|
if (result.items.length > 0) {
|
||||||
|
logger.info('There are more outgoing changes to sync, trigger the sync again');
|
||||||
|
return await this.start(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return outputContext;
|
return outputContext;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user