diff --git a/lib/controllers/preview-app-controller.ts b/lib/controllers/preview-app-controller.ts index 7a9929ac02..e568475a5b 100644 --- a/lib/controllers/preview-app-controller.ts +++ b/lib/controllers/preview-app-controller.ts @@ -1,8 +1,8 @@ -import { Device, FilesPayload } from "nativescript-preview-sdk"; import { TrackActionNames, PREPARE_READY_EVENT_NAME } from "../constants"; import { PrepareController } from "./prepare-controller"; +import { Device, FilesPayload } from "nativescript-preview-sdk"; import { performanceLog } from "../common/decorators"; -import { stringify } from "../common/helpers"; +import { stringify, deferPromise } from "../common/helpers"; import { HmrConstants } from "../common/constants"; import { EventEmitter } from "events"; import { PrepareDataService } from "../services/prepare-data-service"; @@ -11,7 +11,14 @@ import { PreviewAppLiveSyncEvents } from "../services/livesync/playground/previe export class PreviewAppController extends EventEmitter implements IPreviewAppController { private prepareReadyEventHandler: any = null; private deviceInitializationPromise: IDictionary = {}; - private promise = Promise.resolve(); + private devicesLiveSyncChain: IDictionary> = {}; + private devicesCanExecuteHmr: IDictionary = {}; + // holds HMR files per device in order to execute batch upload on fast updates + private devicesHmrFiles: IDictionary = {}; + // holds app files per device in order to execute batch upload on fast updates on failed HMR or --no-hmr + private devicesAppFiles: IDictionary = {}; + // holds the current HMR hash per device in order to watch the proper hash status on fast updates + private devicesCurrentHmrHash: IDictionary = {}; constructor( private $analyticsService: IAnalyticsService, @@ -89,6 +96,7 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon if (data.useHotModuleReload) { this.$hmrStatusService.attachToHmrStatusEvent(); + this.devicesCanExecuteHmr[device.id] = true; } await this.$previewAppPluginsService.comparePluginsOnDevice(data, device); @@ -109,13 +117,13 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon await this.$prepareController.prepare(prepareData); try { - const payloads = await this.getInitialFilesForPlatformSafe(data, device.platform); + const payloads = await this.getInitialFilesForDeviceSafe(data, device); return payloads; } finally { this.deviceInitializationPromise[device.id] = null; } } catch (error) { - this.$logger.trace(`Error while sending files on device ${device && device.id}. Error is`, error); + this.$logger.trace(`Error while sending files on device '${device && device.id}'. Error is`, error); this.emit(PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, { error, data, @@ -129,52 +137,101 @@ export class PreviewAppController extends EventEmitter implements IPreviewAppCon @performanceLog() private async handlePrepareReadyEvent(data: IPreviewAppLiveSyncData, currentPrepareData: IFilesChangeEventData) { - await this.promise - .then(async () => { - const { hmrData, files, platform } = currentPrepareData; - const platformHmrData = _.cloneDeep(hmrData); - - this.promise = this.syncFilesForPlatformSafe(data, { filesToSync: files }, platform); - await this.promise; - - if (data.useHotModuleReload && platformHmrData.hash) { - const devices = this.$previewDevicesService.getDevicesForPlatform(platform); - - await Promise.all(_.map(devices, async (previewDevice: Device) => { - const status = await this.$hmrStatusService.getHmrStatus(previewDevice.id, platformHmrData.hash); - if (status === HmrConstants.HMR_ERROR_STATUS) { - const originalUseHotModuleReload = data.useHotModuleReload; - data.useHotModuleReload = false; - await this.syncFilesForPlatformSafe(data, { filesToSync: platformHmrData.fallbackFiles }, platform, previewDevice.id); - data.useHotModuleReload = originalUseHotModuleReload; - } - })); + const { hmrData, files, platform } = currentPrepareData; + const platformHmrData = _.cloneDeep(hmrData); + const connectedDevices = this.$previewDevicesService.getDevicesForPlatform(platform); + if (!connectedDevices || !connectedDevices.length) { + this.$logger.warn(`Unable to find any connected devices for platform '${platform}'. In order to execute live sync, open your Preview app and optionally re-scan the QR code using the Playground app.`); + return; + } + + await Promise.all(_.map(connectedDevices, async (device) => { + const previousSync = this.devicesLiveSyncChain[device.id] || Promise.resolve(); + const currentSyncDeferPromise = deferPromise(); + this.devicesLiveSyncChain[device.id] = currentSyncDeferPromise.promise; + this.devicesCurrentHmrHash[device.id] = this.devicesCurrentHmrHash[device.id] || platformHmrData.hash; + if (data.useHotModuleReload) { + this.devicesHmrFiles[device.id] = this.devicesHmrFiles[device.id] || []; + this.devicesHmrFiles[device.id].push(...files); + this.devicesAppFiles[device.id] = platformHmrData.fallbackFiles; + } else { + this.devicesHmrFiles[device.id] = []; + this.devicesAppFiles[device.id] = files; + } + + await previousSync; + + try { + let canExecuteHmrSync = false; + const hmrHash = this.devicesCurrentHmrHash[device.id]; + this.devicesCurrentHmrHash[device.id] = null; + if (data.useHotModuleReload && hmrHash) { + if (this.devicesCanExecuteHmr[device.id]) { + canExecuteHmrSync = true; + } } - }); + + const filesToSync = canExecuteHmrSync ? this.devicesHmrFiles[device.id] : this.devicesAppFiles[device.id]; + if (!filesToSync || !filesToSync.length) { + this.$logger.info(`Skipping files sync for device ${this.getDeviceDisplayName(device)}. The changes are already batch transferred in a previous sync.`); + currentSyncDeferPromise.resolve(); + return; + } + + this.devicesHmrFiles[device.id] = []; + this.devicesAppFiles[device.id] = []; + if (canExecuteHmrSync) { + this.$hmrStatusService.watchHmrStatus(device.id, hmrHash); + await this.syncFilesForPlatformSafe(data, { filesToSync }, platform, device); + const status = await this.$hmrStatusService.getHmrStatus(device.id, hmrHash); + if (!status) { + this.devicesCanExecuteHmr[device.id] = false; + this.$logger.warn(`Unable to get LiveSync status from the Preview app for device ${this.getDeviceDisplayName(device)}. Ensure the app is running in order to sync changes.`); + } else { + this.devicesCanExecuteHmr[device.id] = status === HmrConstants.HMR_SUCCESS_STATUS; + } + } else { + const noHmrData = _.assign({}, data, { useHotModuleReload: false }); + await this.syncFilesForPlatformSafe(noHmrData, { filesToSync }, platform, device); + this.devicesCanExecuteHmr[device.id] = true; + } + currentSyncDeferPromise.resolve(); + } catch (e) { + currentSyncDeferPromise.resolve(); + } + })); + } + + private getDeviceDisplayName(device: Device) { + return `${device.name} (${device.id})`.cyan; } - private async getInitialFilesForPlatformSafe(data: IPreviewAppLiveSyncData, platform: string): Promise { - this.$logger.info(`Start sending initial files for platform ${platform}.`); + private async getInitialFilesForDeviceSafe(data: IPreviewAppLiveSyncData, device: Device): Promise { + const platform = device.platform; + this.$logger.info(`Start sending initial files for device ${this.getDeviceDisplayName(device)}.`); try { const payloads = this.$previewAppFilesService.getInitialFilesPayload(data, platform); - this.$logger.info(`Successfully sent initial files for platform ${platform}.`); + this.$logger.info(`Successfully sent initial files for device ${this.getDeviceDisplayName(device)}.`); return payloads; } catch (err) { - this.$logger.warn(`Unable to apply changes for platform ${platform}. Error is: ${err}, ${stringify(err)}`); + this.$logger.warn(`Unable to apply changes for device ${this.getDeviceDisplayName(device)}. Error is: ${err}, ${stringify(err)}`); } } - private async syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, deviceId?: string): Promise { + private async syncFilesForPlatformSafe(data: IPreviewAppLiveSyncData, filesData: IPreviewAppFilesData, platform: string, device: Device): Promise { + const deviceId = device && device.id || ""; + try { const payloads = this.$previewAppFilesService.getFilesPayload(data, filesData, platform); + payloads.deviceId = deviceId; if (payloads && payloads.files && payloads.files.length) { - this.$logger.info(`Start syncing changes for platform ${platform}.`); + this.$logger.info(`Start syncing changes for device ${this.getDeviceDisplayName(device)}.`); await this.$previewSdkService.applyChanges(payloads); - this.$logger.info(`Successfully synced ${payloads.files.map(filePayload => filePayload.file.yellow)} for platform ${platform}.`); + this.$logger.info(`Successfully synced '${payloads.files.map(filePayload => filePayload.file.yellow)}' for device ${this.getDeviceDisplayName(device)}.`); } } catch (error) { - this.$logger.warn(`Unable to apply changes for platform ${platform}. Error is: ${error}, ${JSON.stringify(error, null, 2)}.`); + this.$logger.warn(`Unable to apply changes for device ${this.getDeviceDisplayName(device)}. Error is: ${error}, ${JSON.stringify(error, null, 2)}.`); this.emit(PreviewAppLiveSyncEvents.PREVIEW_APP_LIVE_SYNC_ERROR, { error, data, diff --git a/lib/controllers/run-controller.ts b/lib/controllers/run-controller.ts index caee785a21..fd2a8f2c60 100644 --- a/lib/controllers/run-controller.ts +++ b/lib/controllers/run-controller.ts @@ -396,7 +396,8 @@ export class RunController extends EventEmitter implements IRunController { if (!liveSyncResultInfo.didRecover && isInHMRMode) { const status = await this.$hmrStatusService.getHmrStatus(device.deviceInfo.identifier, data.hmrData.hash); - if (status === HmrConstants.HMR_ERROR_STATUS) { + // error or timeout + if (status !== HmrConstants.HMR_SUCCESS_STATUS) { watchInfo.filesToSync = data.hmrData.fallbackFiles; liveSyncResultInfo = await platformLiveSyncService.liveSyncWatchAction(device, watchInfo); // We want to force a restart of the application. diff --git a/lib/definitions/preview-app-livesync.d.ts b/lib/definitions/preview-app-livesync.d.ts index ef51843024..467da95e92 100644 --- a/lib/definitions/preview-app-livesync.d.ts +++ b/lib/definitions/preview-app-livesync.d.ts @@ -14,7 +14,7 @@ declare global { interface IPreviewAppLiveSyncData extends IProjectDir, IHasUseHotModuleReloadOption, IEnvOptions { } - interface IPreviewSdkService extends EventEmitter { + interface IPreviewSdkService { getQrCodeUrl(options: IGetQrCodeUrlOptions): string; initialize(projectDir: string, getInitialFiles: (device: Device) => Promise): void; applyChanges(filesPayload: FilesPayload): Promise; diff --git a/lib/services/livesync/playground/preview-sdk-service.ts b/lib/services/livesync/playground/preview-sdk-service.ts index 63e805b067..48cb48b2ce 100644 --- a/lib/services/livesync/playground/preview-sdk-service.ts +++ b/lib/services/livesync/playground/preview-sdk-service.ts @@ -1,8 +1,7 @@ import { MessagingService, Config, Device, DeviceConnectedMessage, SdkCallbacks, ConnectedDevices, FilesPayload } from "nativescript-preview-sdk"; -import { EventEmitter } from "events"; const pako = require("pako"); -export class PreviewSdkService extends EventEmitter implements IPreviewSdkService { +export class PreviewSdkService implements IPreviewSdkService { private static MAX_FILES_UPLOAD_BYTE_LENGTH = 15 * 1024 * 1024; // In MBs private messagingService: MessagingService = null; private instanceId: string = null; @@ -13,7 +12,6 @@ export class PreviewSdkService extends EventEmitter implements IPreviewSdkServic private $previewDevicesService: IPreviewDevicesService, private $previewAppLogProvider: IPreviewAppLogProvider, private $previewSchemaService: IPreviewSchemaService) { - super(); } public getQrCodeUrl(options: IGetQrCodeUrlOptions): string { diff --git a/lib/services/log-parser-service.ts b/lib/services/log-parser-service.ts index 97cf19c410..5d88bf2517 100644 --- a/lib/services/log-parser-service.ts +++ b/lib/services/log-parser-service.ts @@ -7,7 +7,7 @@ export class LogParserService extends EventEmitter implements ILogParserService constructor(private $deviceLogProvider: Mobile.IDeviceLogProvider, private $errors: IErrors, - private $previewSdkService: IPreviewSdkService) { + private $previewAppLogProvider: IPreviewAppLogProvider) { super(); } @@ -23,10 +23,12 @@ export class LogParserService extends EventEmitter implements ILogParserService @cache() private startParsingLogCore(): void { this.$deviceLogProvider.on(DEVICE_LOG_EVENT_NAME, this.processDeviceLogResponse.bind(this)); - this.$previewSdkService.on(DEVICE_LOG_EVENT_NAME, this.processDeviceLogResponse.bind(this)); + this.$previewAppLogProvider.on(DEVICE_LOG_EVENT_NAME, (deviceId: string, message: string) => { + this.processDeviceLogResponse(message, deviceId); + }); } - private processDeviceLogResponse(message: string, deviceIdentifier: string, devicePlatform: string) { + private processDeviceLogResponse(message: string, deviceIdentifier: string, devicePlatform?: string) { const lines = message.split("\n"); _.forEach(lines, line => { _.forEach(this.parseRules, parseRule => { diff --git a/test/services/ios-debugger-port-service.ts b/test/services/ios-debugger-port-service.ts index 512ea74ebf..e3edad7996 100644 --- a/test/services/ios-debugger-port-service.ts +++ b/test/services/ios-debugger-port-service.ts @@ -30,6 +30,7 @@ const device = { function createTestInjector() { const injector = new Yok(); + injector.register("previewAppLogProvider", { on: () => ({}) }); injector.register("devicePlatformsConstants", DevicePlatformsConstants); injector.register("deviceLogProvider", DeveiceLogProviderMock); injector.register("errors", ErrorsStub); diff --git a/test/services/log-parser-service.ts b/test/services/log-parser-service.ts index ec606349db..bd6bde2b5f 100644 --- a/test/services/log-parser-service.ts +++ b/test/services/log-parser-service.ts @@ -20,6 +20,7 @@ class DeveiceLogProviderMock extends EventEmitter { function createTestInjector() { const injector = new Yok(); + injector.register("previewAppLogProvider", { on: () => ({}) }); injector.register("deviceLogProvider", DeveiceLogProviderMock); injector.register("previewSdkService", { on: () => ({})