Files
flipper/src/reducers/connections.js
John Knox a097e673d8 Back out "[flipper] fix reconnecting clients"
Summary:
Original commit changeset: 1d0e6ce17c89

Backing this out until we can come up with a better way to do it.

The change was introduced so that when a device disconnects / crashes, we don't lose all plugin state, and you can still see what was on screen before the crash..

However, there are some problems with this solution, which get quite complicated. Putting them here for future reference:
* Closing an app results in the plugins staying there, and there's no way to tell it's not actually connected.
* If the app reconnects, the JS plugin doesn't get re-initialized. Even though the client plugin has been.

Reviewed By: bnelo12

Differential Revision: D16280932

fbshipit-source-id: 92585cdd0dace2012924df4106327a1e21ab9f9b
2019-07-16 08:39:05 -07:00

390 lines
10 KiB
JavaScript

/**
* 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 type BaseDevice from '../devices/BaseDevice';
import MacDevice from '../devices/MacDevice';
import type Client from '../Client';
import type {UninitializedClient} from '../UninitializedClient';
import {isEqual} from 'lodash';
import iosUtil from '../fb-stubs/iOSContainerUtility';
// $FlowFixMe perf_hooks is a new API in node
import {performance} from 'perf_hooks';
export type State = {|
devices: Array<BaseDevice>,
androidEmulators: Array<string>,
selectedDevice: ?BaseDevice,
selectedPlugin: ?string,
selectedApp: ?string,
userPreferredDevice: ?string,
userPreferredPlugin: ?string,
userPreferredApp: ?string,
error: ?string,
clients: Array<Client>,
uninitializedClients: Array<{
client: UninitializedClient,
deviceId?: string,
errorMessage?: string,
}>,
deepLinkPayload: ?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: ?string,
selectedApp: ?string,
deepLinkPayload: ?string,
|},
}
| {
type: 'SELECT_USER_PREFERRED_PLUGIN',
payload: string,
}
| {
type: 'SERVER_ERROR',
payload: ?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 = {
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 = {};
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: ?string,
selectedApp?: ?string,
deepLinkPayload: ?string,
|}): Action => ({
type: 'SELECT_PLUGIN',
payload,
});
export const userPreferredPlugin = (payload: string): Action => ({
type: 'SELECT_USER_PREFERRED_PLUGIN',
payload,
});