Files
flipper/desktop/flipper-plugin/src/state/atom.tsx
Michel Weststrate c43049d881 Preserve device state after disconnect
Summary:
This diff stack introduces support for keeping devices and clients around after they have disconnected. This is a pretty important debugging improvement, that will allow inspecting a device / app after it crashed for example.

This feature existed partially before, but only supported Android, and only support plugins with persisted state; as it replace the current device with an archived version of the same device. In practice this didn't work really well, as most plugins would not be available, and all non-persisted state would be lost.

This diff makes sure we can keep devices around after disconnecting, the next one will keep the clients around as well. And explain some code choices in more detail.

Note that `Device.isArchived` was an overloaded term before, and even more now (both representing imported and disconnected devices), will address that in a later diff.

https://github.com/facebook/flipper/issues/1460
https://github.com/facebook/flipper/issues/812
https://github.com/facebook/flipper/issues/1487

Changelog: iOS and Android devices will preserve their state after being disconnected

Reviewed By: nikoant

Differential Revision: D26224310

fbshipit-source-id: 7dfc93c2a109a51c2880ec212a00463bc8d32041
2021-02-09 04:16:24 -08:00

111 lines
2.7 KiB
TypeScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {produce, Draft, enableMapSet} from 'immer';
import {useState, useEffect} from 'react';
import {getCurrentPluginInstance} from '../plugin/PluginBase';
enableMapSet();
export type Atom<T> = {
get(): T;
set(newValue: T): void;
update(recipe: (draft: Draft<T>) => void): void;
};
class AtomValue<T> implements Atom<T> {
value: T;
listeners: ((value: T) => void)[] = [];
constructor(initialValue: T) {
this.value = initialValue;
}
get() {
return this.value;
}
set(nextValue: T) {
if (nextValue !== this.value) {
this.value = nextValue;
this.notifyChanged();
}
}
update(recipe: (draft: Draft<T>) => void) {
this.set(produce(this.value, recipe));
}
notifyChanged() {
// TODO: add scheduling
this.listeners.slice().forEach((l) => l(this.value));
}
subscribe(listener: (value: T) => void) {
this.listeners.push(listener);
}
unsubscribe(listener: (value: T) => void) {
const idx = this.listeners.indexOf(listener);
if (idx !== -1) {
this.listeners.splice(idx, 1);
}
}
}
type StateOptions = {
/**
* Should this state persist when exporting a plugin?
* If set, the atom will be saved / loaded under the key provided
*/
persist?: string;
};
export function createState<T>(
initialValue: T,
options: StateOptions = {},
): Atom<T> {
const atom = new AtomValue<T>(initialValue);
if (getCurrentPluginInstance() && options.persist) {
const {rootStates} = getCurrentPluginInstance()!;
if (rootStates[options.persist]) {
throw new Error(
`Some other state is already persisting with key "${options.persist}"`,
);
}
rootStates[options.persist] = atom;
}
return atom;
}
export function useValue<T>(atom: Atom<T>): T;
export function useValue<T>(atom: Atom<T> | undefined, defaultValue: T): T;
export function useValue<T>(atom: Atom<T> | undefined, defaultValue?: T): T {
const [localValue, setLocalValue] = useState<T>(
atom ? atom.get() : defaultValue!,
);
useEffect(() => {
if (!atom) {
return;
}
// atom might have changed between mounting and effect setup
// in that case, this will cause a re-render, otherwise not
setLocalValue(atom.get());
(atom as AtomValue<T>).subscribe(setLocalValue);
return () => {
(atom as AtomValue<T>).unsubscribe(setLocalValue);
};
}, [atom]);
return localValue;
}
export function isAtom(value: any): value is Atom<any> {
return value instanceof AtomValue;
}