Skip to content

Commit 107dadb

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 107dadb

File tree

7 files changed

+79
-33
lines changed

7 files changed

+79
-33
lines changed

lib/controllers/preview-app-controller.ts

+68-25
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,9 @@ 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+
private devicesHmrFiles: IDictionary<string[]> = {};
1517

1618
constructor(
1719
private $analyticsService: IAnalyticsService,
@@ -89,6 +91,7 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon
8991

9092
if (data.useHotModuleReload) {
9193
this.$hmrStatusService.attachToHmrStatusEvent();
94+
this.devicesCanExecuteHmr[device.id] = true;
9295
}
9396

9497
await this.$previewAppPluginsService.comparePluginsOnDevice(data, device);
@@ -129,28 +132,63 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon
129132

130133
@performanceLog()
131134
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);
135+
const { hmrData, files, platform } = currentPrepareData;
136+
const platformHmrData = _.cloneDeep(hmrData);
137+
const connectedDevices = this.$previewDevicesService.getDevicesForPlatform(platform);
138+
if (!connectedDevices || !connectedDevices.length) {
139+
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.");
140+
return;
141+
}
136142

137-
this.promise = this.syncFilesForPlatformSafe(data, { filesToSync: files }, platform);
138-
await this.promise;
143+
await Promise.all(_.map(connectedDevices, async (device) => {
144+
const previousSync = this.devicesLiveSyncChain[device.id] || Promise.resolve();
145+
const currentSyncDeferPromise = deferPromise<void>();
146+
this.devicesLiveSyncChain[device.id] = currentSyncDeferPromise.promise;
147+
this.devicesHmrFiles[device.id] = this.devicesHmrFiles[device.id] || [];
148+
this.devicesHmrFiles[device.id].push(...files);
149+
await previousSync;
150+
if (!this.devicesHmrFiles[device.id] || !this.devicesHmrFiles[device.id].length) {
151+
this.$logger.info("Skipping files sync. The changes are already batch transferred in a previous sync.");
152+
currentSyncDeferPromise.resolve();
153+
return;
154+
}
139155

156+
try {
157+
let executeHmrSync = false;
140158
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-
}));
159+
if (this.devicesCanExecuteHmr[device.id]) {
160+
executeHmrSync = true;
161+
this.$hmrStatusService.watchHmrStatus(device.id, platformHmrData.hash);
162+
}
152163
}
153-
});
164+
165+
const filesToSync = executeHmrSync ? this.devicesHmrFiles[device.id] : platformHmrData.fallbackFiles;
166+
this.devicesHmrFiles[device.id] = [];
167+
if (executeHmrSync) {
168+
await this.syncFilesForPlatformSafe(data, { filesToSync }, platform, device);
169+
const status = await this.$hmrStatusService.getHmrStatus(device.id, platformHmrData.hash);
170+
if (!status) {
171+
this.devicesCanExecuteHmr[device.id] = false;
172+
const noStatusWarning = this.getDeviceMsg(device.name,
173+
"Unable to get LiveSync status from the Preview app. Ensure the app is running in order to sync changes.");
174+
this.$logger.warn(noStatusWarning);
175+
} else {
176+
this.devicesCanExecuteHmr[device.id] = status === HmrConstants.HMR_SUCCESS_STATUS;
177+
}
178+
} else {
179+
const noHmrData = _.assign({}, data, { useHotModuleReload: false });
180+
await this.syncFilesForPlatformSafe(noHmrData, { filesToSync }, platform, device);
181+
this.devicesCanExecuteHmr[device.id] = true;
182+
}
183+
currentSyncDeferPromise.resolve();
184+
} catch (e) {
185+
currentSyncDeferPromise.resolve();
186+
}
187+
}));
188+
}
189+
190+
private getDeviceMsg(deviceId: string, message: string) {
191+
return `[${deviceId}] ${message}`;
154192
}
155193

156194
private async getInitialFilesForPlatformSafe(data: IPreviewAppLiveSyncData, platform: string): Promise<FilesPayload> {
@@ -165,16 +203,21 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon
165203
}
166204
}
167205

168-
private async syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, deviceId?: string): Promise<void> {
206+
private async syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, device?: Device): Promise<void> {
207+
const targetType = device ? "device" : "platform";
208+
const targetValue = device ? device.name : platform;
209+
const deviceId = device && device.id || "";
210+
169211
try {
170212
const payloads = this.$previewAppFilesService.getFilesPayload(data, filesData, platform);
213+
payloads.deviceId = deviceId;
171214
if (payloads && payloads.files && payloads.files.length) {
172-
this.$logger.info(`Start syncing changes for platform ${platform}.`);
215+
this.$logger.info(`Start syncing changes for ${targetType} ${targetValue}.`);
173216
await this.$previewSdkService.applyChanges(payloads);
174-
this.$logger.info(`Successfully synced ${payloads.files.map(filePayload => filePayload.file.yellow)} for platform ${platform}.`);
217+
this.$logger.info(`Successfully synced ${payloads.files.map(filePayload => filePayload.file.yellow)} for ${targetType} ${targetValue}.`);
175218
}
176219
} catch (error) {
177-
this.$logger.warn(`Unable to apply changes for platform ${platform}. Error is: ${error}, ${JSON.stringify(error, null, 2)}.`);
220+
this.$logger.warn(`Unable to apply changes for ${targetType} ${targetValue}. Error is: ${error}, ${JSON.stringify(error, null, 2)}.`);
178221
this.emit(PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, {
179222
error,
180223
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)