Skip to content

Commit 8d44bc9

Browse files
committed
feat: recover after HMR crashes in Preview
Additional Details and Improvements: - no connected devices info is logged when there aren't any connected devices - a hint for starting the Preview app is logged on HMR status timeout - the device specific logs are printed with a device id prefix - run with HMR is now assuming the Status timeout as HMR error and retrying with a full sync - HMR status reading is fixed - we are attaching to the proper Preview logs provider as the previewSdkService is not emitting any logs - HMR updates are now chained in order to avoid getting wrong status from parallel updates - HMR updates are batched in order to avoid slower updates on fast changes
1 parent 15c9196 commit 8d44bc9

File tree

7 files changed

+94
-42
lines changed

7 files changed

+94
-42
lines changed

lib/controllers/preview-app-controller.ts

+83-34
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Device, FilesPayload } from "nativescript-preview-sdk";
21
import { TrackActionNames, PREPARE_READY_EVENT_NAME } from "../constants";
32
import { PrepareController } from "./prepare-controller";
3+
import { Device, FilesPayload } from "nativescript-preview-sdk";
44
import { performanceLog } from "../common/decorators";
5-
import { stringify } from "../common/helpers";
5+
import { stringify, deferPromise } from "../common/helpers";
66
import { HmrConstants } from "../common/constants";
77
import { EventEmitter } from "events";
88
import { PrepareDataService } from "../services/prepare-data-service";
@@ -11,7 +11,12 @@ import { PreviewAppLiveSyncEvents } from "../services/livesync/playground/previe
1111
export class PreviewAppController extends EventEmitter implements IPreviewAppController {
1212
private prepareReadyEventHandler: any = null;
1313
private deviceInitializationPromise: IDictionary<boolean> = {};
14-
private promise = Promise.resolve();
14+
private devicesLiveSyncChain: IDictionary<Promise<void>> = {};
15+
private devicesCanExecuteHmr: IDictionary<boolean> = {};
16+
// holds HMR files per device in order to execute batch upload on fast updates
17+
private devicesHmrFiles: IDictionary<string[]> = {};
18+
// holds the current HMR hash per device in order to watch the proper hash status on fast updates
19+
private devicesCurrentHmrHash: IDictionary<string> = {};
1520

1621
constructor(
1722
private $analyticsService: IAnalyticsService,
@@ -89,6 +94,7 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon
8994

9095
if (data.useHotModuleReload) {
9196
this.$hmrStatusService.attachToHmrStatusEvent();
97+
this.devicesCanExecuteHmr[device.id] = true;
9298
}
9399

94100
await this.$previewAppPluginsService.comparePluginsOnDevice(data, device);
@@ -109,13 +115,13 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon
109115
await this.$prepareController.prepare(prepareData);
110116

111117
try {
112-
const payloads = await this.getInitialFilesForPlatformSafe(data, device.platform);
118+
const payloads = await this.getInitialFilesForDeviceSafe(data, device);
113119
return payloads;
114120
} finally {
115121
this.deviceInitializationPromise[device.id] = null;
116122
}
117123
} catch (error) {
118-
this.$logger.trace(`Error while sending files on device ${device && device.id}. Error is`, error);
124+
this.$logger.trace(`Error while sending files on device '${device && device.id}'. Error is`, error);
119125
this.emit(PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, {
120126
error,
121127
data,
@@ -129,52 +135,95 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon
129135

130136
@performanceLog()
131137
private async handlePrepareReadyEvent(data: IPreviewAppLiveSyncData, currentPrepareData: IFilesChangeEventData) {
132-
await this.promise
133-
.then(async () => {
134-
const { hmrData, files, platform } = currentPrepareData;
135-
const platformHmrData = _.cloneDeep(hmrData);
136-
137-
this.promise = this.syncFilesForPlatformSafe(data, { filesToSync: files }, platform);
138-
await this.promise;
139-
140-
if (data.useHotModuleReload && platformHmrData.hash) {
141-
const devices = this.$previewDevicesService.getDevicesForPlatform(platform);
142-
143-
await Promise.all(_.map(devices, async (previewDevice: Device) => {
144-
const status = await this.$hmrStatusService.getHmrStatus(previewDevice.id, platformHmrData.hash);
145-
if (status === HmrConstants.HMR_ERROR_STATUS) {
146-
const originalUseHotModuleReload = data.useHotModuleReload;
147-
data.useHotModuleReload = false;
148-
await this.syncFilesForPlatformSafe(data, { filesToSync: platformHmrData.fallbackFiles }, platform, previewDevice.id);
149-
data.useHotModuleReload = originalUseHotModuleReload;
150-
}
151-
}));
138+
const { hmrData, files, platform } = currentPrepareData;
139+
const platformHmrData = _.cloneDeep(hmrData);
140+
const connectedDevices = this.$previewDevicesService.getDevicesForPlatform(platform);
141+
if (!connectedDevices || !connectedDevices.length) {
142+
this.$logger.warn("Unable to find any connected devices. In order to execute live sync, open your Preview app and optionally re-scan the QR code using the Playground app.");
143+
return;
144+
}
145+
146+
await Promise.all(_.map(connectedDevices, async (device) => {
147+
const previousSync = this.devicesLiveSyncChain[device.id] || Promise.resolve();
148+
const currentSyncDeferPromise = deferPromise<void>();
149+
this.devicesLiveSyncChain[device.id] = currentSyncDeferPromise.promise;
150+
this.devicesCurrentHmrHash[device.id] = this.devicesCurrentHmrHash[device.id] || platformHmrData.hash;
151+
this.devicesHmrFiles[device.id] = this.devicesHmrFiles[device.id] || [];
152+
this.devicesHmrFiles[device.id].push(...files);
153+
await previousSync;
154+
if (!this.devicesHmrFiles[device.id] || !this.devicesHmrFiles[device.id].length) {
155+
this.$logger.info("Skipping files sync. The changes are already batch transferred in a previous sync.");
156+
currentSyncDeferPromise.resolve();
157+
return;
158+
}
159+
160+
try {
161+
162+
let executeHmrSync = false;
163+
const hmrHash = this.devicesCurrentHmrHash[device.id];
164+
this.devicesCurrentHmrHash[device.id] = null;
165+
if (data.useHotModuleReload && hmrHash) {
166+
if (this.devicesCanExecuteHmr[device.id]) {
167+
executeHmrSync = true;
168+
this.$hmrStatusService.watchHmrStatus(device.id, hmrHash);
169+
}
152170
}
153-
});
171+
172+
const filesToSync = executeHmrSync ? this.devicesHmrFiles[device.id] : platformHmrData.fallbackFiles;
173+
this.devicesHmrFiles[device.id] = [];
174+
if (executeHmrSync) {
175+
await this.syncFilesForPlatformSafe(data, { filesToSync }, platform, device);
176+
const status = await this.$hmrStatusService.getHmrStatus(device.id, hmrHash);
177+
if (!status) {
178+
this.devicesCanExecuteHmr[device.id] = false;
179+
const noStatusWarning = this.getDeviceMsg(device.name,
180+
"Unable to get LiveSync status from the Preview app. Ensure the app is running in order to sync changes.");
181+
this.$logger.warn(noStatusWarning);
182+
} else {
183+
this.devicesCanExecuteHmr[device.id] = status === HmrConstants.HMR_SUCCESS_STATUS;
184+
}
185+
} else {
186+
const noHmrData = _.assign({}, data, { useHotModuleReload: false });
187+
await this.syncFilesForPlatformSafe(noHmrData, { filesToSync }, platform, device);
188+
this.devicesCanExecuteHmr[device.id] = true;
189+
}
190+
currentSyncDeferPromise.resolve();
191+
} catch (e) {
192+
currentSyncDeferPromise.resolve();
193+
}
194+
}));
154195
}
155196

156-
private async getInitialFilesForPlatformSafe(data: IPreviewAppLiveSyncData, platform: string): Promise<FilesPayload> {
157-
this.$logger.info(`Start sending initial files for platform ${platform}.`);
197+
private getDeviceMsg(deviceId: string, message: string) {
198+
return `[${deviceId}] ${message}`;
199+
}
200+
201+
private async getInitialFilesForDeviceSafe(data: IPreviewAppLiveSyncData, device: Device): Promise<FilesPayload> {
202+
const platform = device.platform;
203+
this.$logger.info(`Start sending initial files for device '${device.name}'.`);
158204

159205
try {
160206
const payloads = this.$previewAppFilesService.getInitialFilesPayload(data, platform);
161-
this.$logger.info(`Successfully sent initial files for platform ${platform}.`);
207+
this.$logger.info(`Successfully sent initial files for device '${device.name}'.`);
162208
return payloads;
163209
} catch (err) {
164-
this.$logger.warn(`Unable to apply changes for platform ${platform}. Error is: ${err}, ${stringify(err)}`);
210+
this.$logger.warn(`Unable to apply changes for device '${device.name}'. Error is: ${err}, ${stringify(err)}`);
165211
}
166212
}
167213

168-
private async syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, deviceId?: string): Promise<void> {
214+
private async syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, device: Device): Promise<void> {
215+
const deviceId = device && device.id || "";
216+
169217
try {
170218
const payloads = this.$previewAppFilesService.getFilesPayload(data, filesData, platform);
219+
payloads.deviceId = deviceId;
171220
if (payloads && payloads.files && payloads.files.length) {
172-
this.$logger.info(`Start syncing changes for platform ${platform}.`);
221+
this.$logger.info(`Start syncing changes for device '${device.name}'.`);
173222
await this.$previewSdkService.applyChanges(payloads);
174-
this.$logger.info(`Successfully synced ${payloads.files.map(filePayload => filePayload.file.yellow)} for platform ${platform}.`);
223+
this.$logger.info(`Successfully synced '${payloads.files.map(filePayload => filePayload.file.yellow)}' for device '${device.name}'.`);
175224
}
176225
} catch (error) {
177-
this.$logger.warn(`Unable to apply changes for platform ${platform}. Error is: ${error}, ${JSON.stringify(error, null, 2)}.`);
226+
this.$logger.warn(`Unable to apply changes for device '${device.name}'. Error is: ${error}, ${JSON.stringify(error, null, 2)}.`);
178227
this.emit(PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, {
179228
error,
180229
data,

lib/controllers/run-controller.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,8 @@ export class RunController extends EventEmitter implements IRunController {
396396

397397
if (!liveSyncResultInfo.didRecover && isInHMRMode) {
398398
const status = await this.$hmrStatusService.getHmrStatus(device.deviceInfo.identifier, data.hmrData.hash);
399-
if (status === HmrConstants.HMR_ERROR_STATUS) {
399+
// error or timeout
400+
if (status !== HmrConstants.HMR_SUCCESS_STATUS) {
400401
watchInfo.filesToSync = data.hmrData.fallbackFiles;
401402
liveSyncResultInfo = await platformLiveSyncService.liveSyncWatchAction(device, watchInfo);
402403
// We want to force a restart of the application.

lib/definitions/preview-app-livesync.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ declare global {
1414

1515
interface IPreviewAppLiveSyncData extends IProjectDir, IHasUseHotModuleReloadOption, IEnvOptions { }
1616

17-
interface IPreviewSdkService extends EventEmitter {
17+
interface IPreviewSdkService {
1818
getQrCodeUrl(options: IGetQrCodeUrlOptions): string;
1919
initialize(projectDir: string, getInitialFiles: (device: Device) => Promise<FilesPayload>): void;
2020
applyChanges(filesPayload: FilesPayload): Promise<void>;

lib/services/livesync/playground/preview-sdk-service.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { MessagingService, Config, Device, DeviceConnectedMessage, SdkCallbacks, ConnectedDevices, FilesPayload } from "nativescript-preview-sdk";
2-
import { EventEmitter } from "events";
32
const pako = require("pako");
43

5-
export class PreviewSdkService extends EventEmitter implements IPreviewSdkService {
4+
export class PreviewSdkService implements IPreviewSdkService {
65
private static MAX_FILES_UPLOAD_BYTE_LENGTH = 15 * 1024 * 1024; // In MBs
76
private messagingService: MessagingService = null;
87
private instanceId: string = null;
@@ -13,7 +12,6 @@ export class PreviewSdkService extends EventEmitter implements IPreviewSdkServic
1312
private $previewDevicesService: IPreviewDevicesService,
1413
private $previewAppLogProvider: IPreviewAppLogProvider,
1514
private $previewSchemaService: IPreviewSchemaService) {
16-
super();
1715
}
1816

1917
public getQrCodeUrl(options: IGetQrCodeUrlOptions): string {

lib/services/log-parser-service.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export class LogParserService extends EventEmitter implements ILogParserService
77

88
constructor(private $deviceLogProvider: Mobile.IDeviceLogProvider,
99
private $errors: IErrors,
10-
private $previewSdkService: IPreviewSdkService) {
10+
private $previewAppLogProvider: IPreviewAppLogProvider) {
1111
super();
1212
}
1313

@@ -23,10 +23,12 @@ export class LogParserService extends EventEmitter implements ILogParserService
2323
@cache()
2424
private startParsingLogCore(): void {
2525
this.$deviceLogProvider.on(DEVICE_LOG_EVENT_NAME, this.processDeviceLogResponse.bind(this));
26-
this.$previewSdkService.on(DEVICE_LOG_EVENT_NAME, this.processDeviceLogResponse.bind(this));
26+
this.$previewAppLogProvider.on(DEVICE_LOG_EVENT_NAME, (deviceId: string, message: string) => {
27+
this.processDeviceLogResponse(message, deviceId);
28+
});
2729
}
2830

29-
private processDeviceLogResponse(message: string, deviceIdentifier: string, devicePlatform: string) {
31+
private processDeviceLogResponse(message: string, deviceIdentifier: string, devicePlatform?: string) {
3032
const lines = message.split("\n");
3133
_.forEach(lines, line => {
3234
_.forEach(this.parseRules, parseRule => {

test/services/ios-debugger-port-service.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const device = <Mobile.IDevice>{
3030
function createTestInjector() {
3131
const injector = new Yok();
3232

33+
injector.register("previewAppLogProvider", { on: () => ({}) });
3334
injector.register("devicePlatformsConstants", DevicePlatformsConstants);
3435
injector.register("deviceLogProvider", DeveiceLogProviderMock);
3536
injector.register("errors", ErrorsStub);

test/services/log-parser-service.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class DeveiceLogProviderMock extends EventEmitter {
2020

2121
function createTestInjector() {
2222
const injector = new Yok();
23+
injector.register("previewAppLogProvider", { on: () => ({}) });
2324
injector.register("deviceLogProvider", DeveiceLogProviderMock);
2425
injector.register("previewSdkService", {
2526
on: () => ({})

0 commit comments

Comments
 (0)