Skip to content

Commit f279816

Browse files
committed
fix: prevent redundant restarts on rapid changes and recover from app crashes in watch mode
Two issues in the livesync watch loop: 1. Multiple rapid file changes caused each webpack recompilation to trigger a separate sync+restart, even when earlier restarts were already stale. Added exhaustMapWithTrailing semantics: while a sync is in progress, incoming compilation events are coalesced per-platform into a single pending event that runs after the current sync completes. 2. If the app crashed at certain times (e.g. during JS startup), the error handler removed the device from the descriptor list and stopped watchers, permanently killing the watch loop. Fixed by no longer calling stop() on transient sync errors — the device stays in the list and retries on the next file change. Also added .catch() to the promise action chain so a rejected action no longer breaks all subsequent chained actions.
1 parent 7d540b6 commit f279816

1 file changed

Lines changed: 86 additions & 23 deletions

File tree

lib/controllers/run-controller.ts

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ import { injector } from "../common/yok";
2626

2727
export class RunController extends EventEmitter implements IRunController {
2828
private prepareReadyEventHandler: any = null;
29+
private _syncInProgress = false;
30+
private _pendingSyncs: Map<
31+
string,
32+
{
33+
data: IFilesChangeEventData;
34+
projectData: IProjectData;
35+
liveSyncInfo: ILiveSyncInfo;
36+
}
37+
> = new Map();
2938

3039
constructor(
3140
protected $analyticsService: IAnalyticsService,
@@ -97,16 +106,11 @@ export class RunController extends EventEmitter implements IRunController {
97106
projectData,
98107
prepareData,
99108
);
100-
if (changesInfo.hasChanges) {
101-
await this.syncChangedDataOnDevices(
102-
data,
103-
projectData,
104-
liveSyncInfo,
105-
);
109+
if (!changesInfo.hasChanges) {
110+
return;
106111
}
107-
} else {
108-
await this.syncChangedDataOnDevices(data, projectData, liveSyncInfo);
109112
}
113+
this.scheduleSyncOnDevices(data, projectData, liveSyncInfo);
110114
};
111115

112116
this.prepareReadyEventHandler = handler.bind(this);
@@ -840,11 +844,11 @@ export class RunController extends EventEmitter implements IRunController {
840844
watchInfo.connectTimeout = null;
841845
await watchAction();
842846
}
843-
} catch (err) {
847+
} catch (err: any) {
844848
this.$logger.warn(
845849
`Unable to apply changes for device: ${
846850
device.deviceInfo.identifier
847-
}. Error is: ${err && err.message}.`,
851+
}. Error is: ${err && err.message}. Will retry on next change.`,
848852
);
849853

850854
this.emitCore(RunOnDeviceEvents.runOnDeviceError, {
@@ -856,12 +860,6 @@ export class RunController extends EventEmitter implements IRunController {
856860
],
857861
error: err,
858862
});
859-
860-
await this.stop({
861-
projectDir: projectData.projectDir,
862-
deviceIdentifiers: [device.deviceInfo.identifier],
863-
stopOptions: { shouldAwaitAllActions: false },
864-
});
865863
}
866864
};
867865

@@ -885,20 +883,85 @@ export class RunController extends EventEmitter implements IRunController {
885883
);
886884
}
887885

886+
private scheduleSyncOnDevices(
887+
data: IFilesChangeEventData,
888+
projectData: IProjectData,
889+
liveSyncInfo: ILiveSyncInfo,
890+
): void {
891+
if (this._syncInProgress) {
892+
const platform = data.platform;
893+
const existing = this._pendingSyncs.get(platform);
894+
if (existing) {
895+
existing.data = this.mergeFilesChangeEvents(existing.data, data);
896+
} else {
897+
this._pendingSyncs.set(platform, { data, projectData, liveSyncInfo });
898+
}
899+
return;
900+
}
901+
902+
this.executeSyncOnDevices(data, projectData, liveSyncInfo);
903+
}
904+
905+
private async executeSyncOnDevices(
906+
data: IFilesChangeEventData,
907+
projectData: IProjectData,
908+
liveSyncInfo: ILiveSyncInfo,
909+
): Promise<void> {
910+
this._syncInProgress = true;
911+
try {
912+
await this.syncChangedDataOnDevices(data, projectData, liveSyncInfo);
913+
} catch (err: any) {
914+
this.$logger.trace(`Error during sync on devices: ${err.message || err}`);
915+
} finally {
916+
const nextEntry = this._pendingSyncs.entries().next();
917+
if (!nextEntry.done) {
918+
const [platform, pending] = nextEntry.value;
919+
this._pendingSyncs.delete(platform);
920+
this.executeSyncOnDevices(
921+
pending.data,
922+
pending.projectData,
923+
pending.liveSyncInfo,
924+
);
925+
} else {
926+
this._syncInProgress = false;
927+
}
928+
}
929+
}
930+
931+
private mergeFilesChangeEvents(
932+
a: IFilesChangeEventData,
933+
b: IFilesChangeEventData,
934+
): IFilesChangeEventData {
935+
return {
936+
files: [...new Set([...a.files, ...b.files])],
937+
staleFiles: [
938+
...new Set([...(a.staleFiles || []), ...(b.staleFiles || [])]),
939+
],
940+
hasOnlyHotUpdateFiles: a.hasOnlyHotUpdateFiles && b.hasOnlyHotUpdateFiles,
941+
hasNativeChanges: a.hasNativeChanges || b.hasNativeChanges,
942+
hmrData: b.hmrData,
943+
platform: b.platform,
944+
};
945+
}
946+
888947
private async addActionToChain<T>(
889948
projectDir: string,
890949
action: () => Promise<T>,
891950
): Promise<T> {
892951
const liveSyncInfo =
893952
this.$liveSyncProcessDataService.getPersistedData(projectDir);
894953
if (liveSyncInfo) {
895-
liveSyncInfo.actionsChain = liveSyncInfo.actionsChain.then(async () => {
896-
if (!liveSyncInfo.isStopped) {
897-
liveSyncInfo.currentSyncAction = action();
898-
const res = await liveSyncInfo.currentSyncAction;
899-
return res;
900-
}
901-
});
954+
liveSyncInfo.actionsChain = liveSyncInfo.actionsChain
955+
.then(async () => {
956+
if (!liveSyncInfo.isStopped) {
957+
liveSyncInfo.currentSyncAction = action();
958+
const res = await liveSyncInfo.currentSyncAction;
959+
return res;
960+
}
961+
})
962+
.catch((err: any) => {
963+
this.$logger.warn(`Error in action chain: ${err.message || err}`);
964+
});
902965

903966
const result = await liveSyncInfo.actionsChain;
904967
return result;

0 commit comments

Comments
 (0)