Summary: Migrating Server, Client and UninitializedClient to TypeScript Reviewed By: passy Differential Revision: D16687855 fbshipit-source-id: 402e4dbcd5d283d3e280d4d8b312662829457886
389 lines
10 KiB
TypeScript
389 lines
10 KiB
TypeScript
/**
|
|
* Copyright 2018-present Facebook.
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
* @format
|
|
*/
|
|
|
|
import BaseDevice from '../devices/BaseDevice';
|
|
import MacDevice from '../devices/MacDevice';
|
|
import Client from '../Client';
|
|
import {UninitializedClient} from '../UninitializedClient';
|
|
import {isEqual} from 'lodash';
|
|
import iosUtil from '../fb-stubs/iOSContainerUtility';
|
|
import {performance} from 'perf_hooks';
|
|
|
|
export type State = {
|
|
devices: Array<BaseDevice>;
|
|
androidEmulators: Array<string>;
|
|
selectedDevice: null | BaseDevice;
|
|
selectedPlugin: null | string;
|
|
selectedApp: null | string;
|
|
userPreferredDevice: null | string;
|
|
userPreferredPlugin: null | string;
|
|
userPreferredApp: null | string;
|
|
error: null | string;
|
|
clients: Array<Client>;
|
|
uninitializedClients: Array<{
|
|
client: UninitializedClient;
|
|
deviceId?: string;
|
|
errorMessage?: string;
|
|
}>;
|
|
deepLinkPayload: null | string;
|
|
};
|
|
|
|
export type Action =
|
|
| {
|
|
type: 'UNREGISTER_DEVICES';
|
|
payload: Set<string>;
|
|
}
|
|
| {
|
|
type: 'REGISTER_DEVICE';
|
|
payload: BaseDevice;
|
|
}
|
|
| {
|
|
type: 'REGISTER_ANDROID_EMULATORS';
|
|
payload: Array<string>;
|
|
}
|
|
| {
|
|
type: 'SELECT_DEVICE';
|
|
payload: BaseDevice;
|
|
}
|
|
| {
|
|
type: 'SELECT_PLUGIN';
|
|
payload: {
|
|
selectedPlugin: null | string;
|
|
selectedApp?: null | string;
|
|
deepLinkPayload: null | string;
|
|
};
|
|
}
|
|
| {
|
|
type: 'SELECT_USER_PREFERRED_PLUGIN';
|
|
payload: string;
|
|
}
|
|
| {
|
|
type: 'SERVER_ERROR';
|
|
payload: null | string;
|
|
}
|
|
| {
|
|
type: 'NEW_CLIENT';
|
|
payload: Client;
|
|
}
|
|
| {
|
|
type: 'NEW_CLIENT_SANITY_CHECK';
|
|
payload: Client;
|
|
}
|
|
| {
|
|
type: 'CLIENT_REMOVED';
|
|
payload: string;
|
|
}
|
|
| {
|
|
type: 'PREFER_DEVICE';
|
|
payload: string;
|
|
}
|
|
| {
|
|
type: 'START_CLIENT_SETUP';
|
|
payload: UninitializedClient;
|
|
}
|
|
| {
|
|
type: 'FINISH_CLIENT_SETUP';
|
|
payload: {client: UninitializedClient; deviceId: string};
|
|
}
|
|
| {
|
|
type: 'CLIENT_SETUP_ERROR';
|
|
payload: {client: UninitializedClient; error: Error};
|
|
};
|
|
|
|
const DEFAULT_PLUGIN = 'DeviceLogs';
|
|
const DEFAULT_DEVICE_BLACKLIST = [MacDevice];
|
|
|
|
const INITAL_STATE: State = {
|
|
devices: [],
|
|
androidEmulators: [],
|
|
selectedDevice: null,
|
|
selectedApp: null,
|
|
selectedPlugin: DEFAULT_PLUGIN,
|
|
userPreferredDevice: null,
|
|
userPreferredPlugin: null,
|
|
userPreferredApp: null,
|
|
error: null,
|
|
clients: [],
|
|
uninitializedClients: [],
|
|
deepLinkPayload: null,
|
|
};
|
|
|
|
const reducer = (state: State = INITAL_STATE, action: Action): State => {
|
|
switch (action.type) {
|
|
case 'SELECT_DEVICE': {
|
|
const {payload} = action;
|
|
return {
|
|
...state,
|
|
selectedApp: null,
|
|
selectedPlugin: DEFAULT_PLUGIN,
|
|
selectedDevice: payload,
|
|
userPreferredDevice: payload.title,
|
|
};
|
|
}
|
|
case 'REGISTER_ANDROID_EMULATORS': {
|
|
const {payload} = action;
|
|
return {
|
|
...state,
|
|
androidEmulators: payload,
|
|
};
|
|
}
|
|
case 'REGISTER_DEVICE': {
|
|
const {payload} = action;
|
|
const devices = state.devices.concat(payload);
|
|
let {selectedDevice, selectedPlugin} = state;
|
|
|
|
// select the default plugin
|
|
let selection: Partial<State> = {
|
|
selectedApp: null,
|
|
selectedPlugin: DEFAULT_PLUGIN,
|
|
};
|
|
|
|
const canBeDefaultDevice = !DEFAULT_DEVICE_BLACKLIST.some(
|
|
blacklistedDevice => payload instanceof blacklistedDevice,
|
|
);
|
|
|
|
if (!selectedDevice && canBeDefaultDevice) {
|
|
selectedDevice = payload;
|
|
if (selectedPlugin) {
|
|
// We already had a plugin selected, but no device. This is happening
|
|
// when the Client connected before the Device.
|
|
selection = {};
|
|
}
|
|
} else if (payload.title === state.userPreferredDevice) {
|
|
selectedDevice = payload;
|
|
} else {
|
|
// We didn't select the newly connected device, so we don't want to
|
|
// change the plugin.
|
|
selection = {};
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
devices,
|
|
// select device if none was selected before
|
|
selectedDevice,
|
|
...selection,
|
|
};
|
|
}
|
|
case 'UNREGISTER_DEVICES': {
|
|
const {payload} = action;
|
|
const {selectedDevice} = state;
|
|
let selectedDeviceWasRemoved = false;
|
|
const devices = state.devices.filter((device: BaseDevice) => {
|
|
if (payload.has(device.serial)) {
|
|
if (selectedDevice === device) {
|
|
// removed device is the selected
|
|
selectedDeviceWasRemoved = true;
|
|
}
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
let selection = {};
|
|
if (selectedDeviceWasRemoved) {
|
|
selection = {
|
|
selectedDevice: devices[devices.length - 1] || null,
|
|
selectedApp: null,
|
|
selectedPlugin: DEFAULT_PLUGIN,
|
|
};
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
devices,
|
|
...selection,
|
|
};
|
|
}
|
|
case 'SELECT_PLUGIN': {
|
|
const {payload} = action;
|
|
const {selectedPlugin, selectedApp} = payload;
|
|
if (selectedPlugin) {
|
|
performance.mark(`activePlugin-${selectedPlugin}`);
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
...payload,
|
|
userPreferredApp: selectedApp || state.userPreferredApp,
|
|
userPreferredPlugin: selectedPlugin,
|
|
};
|
|
}
|
|
case 'SELECT_USER_PREFERRED_PLUGIN': {
|
|
const {payload} = action;
|
|
return {...state, userPreferredPlugin: payload};
|
|
}
|
|
case 'NEW_CLIENT': {
|
|
const {payload} = action;
|
|
const {userPreferredApp, userPreferredPlugin} = state;
|
|
let {selectedApp, selectedPlugin} = state;
|
|
|
|
if (
|
|
userPreferredApp &&
|
|
userPreferredPlugin &&
|
|
payload.id === userPreferredApp &&
|
|
payload.plugins.includes(userPreferredPlugin)
|
|
) {
|
|
// user preferred client did reconnect, so let's select it
|
|
selectedApp = userPreferredApp;
|
|
selectedPlugin = userPreferredPlugin;
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
clients: state.clients.concat(payload),
|
|
uninitializedClients: state.uninitializedClients.filter(c => {
|
|
return (
|
|
c.deviceId !== payload.query.device_id ||
|
|
c.client.appName !== payload.query.app
|
|
);
|
|
}),
|
|
selectedApp,
|
|
selectedPlugin,
|
|
};
|
|
}
|
|
case 'NEW_CLIENT_SANITY_CHECK': {
|
|
const {payload} = action;
|
|
// Check for clients initialised when there is no matching device
|
|
const clientIsStillConnected = state.clients.filter(
|
|
client => client.id == payload.query.device_id,
|
|
);
|
|
if (clientIsStillConnected) {
|
|
const matchingDeviceForClient = state.devices.filter(
|
|
device => payload.query.device_id === device.serial,
|
|
);
|
|
if (matchingDeviceForClient.length === 0) {
|
|
console.error(
|
|
`Client initialised for non-displayed device: ${payload.id}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return state;
|
|
}
|
|
case 'CLIENT_REMOVED': {
|
|
const {payload} = action;
|
|
|
|
const selected: Partial<State> = {};
|
|
if (state.selectedApp === payload) {
|
|
selected.selectedApp = null;
|
|
selected.selectedPlugin = DEFAULT_PLUGIN;
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
...selected,
|
|
clients: state.clients.filter(
|
|
(client: Client) => client.id !== payload,
|
|
),
|
|
};
|
|
}
|
|
case 'PREFER_DEVICE': {
|
|
const {payload: userPreferredDevice} = action;
|
|
return {...state, userPreferredDevice};
|
|
}
|
|
case 'SERVER_ERROR': {
|
|
const {payload} = action;
|
|
return {...state, error: payload};
|
|
}
|
|
case 'START_CLIENT_SETUP': {
|
|
const {payload} = action;
|
|
return {
|
|
...state,
|
|
uninitializedClients: state.uninitializedClients
|
|
.filter(entry => !isEqual(entry.client, payload))
|
|
.concat([{client: payload}])
|
|
.sort((a, b) => a.client.appName.localeCompare(b.client.appName)),
|
|
};
|
|
}
|
|
case 'FINISH_CLIENT_SETUP': {
|
|
const {payload} = action;
|
|
return {
|
|
...state,
|
|
uninitializedClients: state.uninitializedClients
|
|
.map(c =>
|
|
isEqual(c.client, payload.client)
|
|
? {...c, deviceId: payload.deviceId}
|
|
: c,
|
|
)
|
|
.sort((a, b) => a.client.appName.localeCompare(b.client.appName)),
|
|
};
|
|
}
|
|
case 'CLIENT_SETUP_ERROR': {
|
|
const {payload} = action;
|
|
|
|
const errorMessage =
|
|
payload.error instanceof Error ? payload.error.message : payload.error;
|
|
console.error(
|
|
`Client setup error: ${errorMessage} while setting up client: ${
|
|
payload.client.os
|
|
}:${payload.client.deviceName}:${payload.client.appName}`,
|
|
);
|
|
return {
|
|
...state,
|
|
uninitializedClients: state.uninitializedClients
|
|
.map(c =>
|
|
isEqual(c.client, payload.client)
|
|
? {...c, errorMessage: errorMessage}
|
|
: c,
|
|
)
|
|
.sort((a, b) => a.client.appName.localeCompare(b.client.appName)),
|
|
error: `Client setup error: ${errorMessage}`,
|
|
};
|
|
}
|
|
default:
|
|
return state;
|
|
}
|
|
};
|
|
|
|
export default (state: State = INITAL_STATE, action: Action): State => {
|
|
const nextState = reducer(state, action);
|
|
|
|
if (nextState.selectedDevice) {
|
|
const {selectedDevice} = nextState;
|
|
const deviceNotSupportedError = 'iOS Devices are not yet supported';
|
|
const error =
|
|
selectedDevice.os === 'iOS' &&
|
|
selectedDevice.deviceType === 'physical' &&
|
|
!iosUtil.isAvailable()
|
|
? deviceNotSupportedError
|
|
: null;
|
|
|
|
if (nextState.error === deviceNotSupportedError) {
|
|
nextState.error = error;
|
|
} else {
|
|
nextState.error = error || nextState.error;
|
|
}
|
|
}
|
|
return nextState;
|
|
};
|
|
|
|
export const selectDevice = (payload: BaseDevice): Action => ({
|
|
type: 'SELECT_DEVICE',
|
|
payload,
|
|
});
|
|
|
|
export const preferDevice = (payload: string): Action => ({
|
|
type: 'PREFER_DEVICE',
|
|
payload,
|
|
});
|
|
|
|
export const selectPlugin = (payload: {
|
|
selectedPlugin: null | string;
|
|
selectedApp?: null | string;
|
|
deepLinkPayload: null | string;
|
|
}): Action => ({
|
|
type: 'SELECT_PLUGIN',
|
|
payload,
|
|
});
|
|
|
|
export const userPreferredPlugin = (payload: string): Action => ({
|
|
type: 'SELECT_USER_PREFERRED_PLUGIN',
|
|
payload,
|
|
});
|