introduce onReady life-cycle

Summary: Flipper Sandy plugins didn't have an event to hook into that is run _after_ any state snapshot is loaded, which was needed by the graphQL plugin, as they do some post processing when a data snapshot is restored.

Reviewed By: passy

Differential Revision: D28189573

fbshipit-source-id: 4ef992f3fafc32787eab3bc235059f2c41396c80
This commit is contained in:
Michel Weststrate
2021-05-04 12:51:32 -07:00
committed by Facebook GitHub Bot
parent 616341f649
commit dd7a9f5195
5 changed files with 57 additions and 19 deletions

View File

@@ -28,6 +28,7 @@ export function plugin(client: PluginClient<Events, Methods>) {
const activateStub = jest.fn(); const activateStub = jest.fn();
const deactivateStub = jest.fn(); const deactivateStub = jest.fn();
const destroyStub = jest.fn(); const destroyStub = jest.fn();
const readyStub = jest.fn();
const state = createState( const state = createState(
{ {
count: 0, count: 0,
@@ -43,6 +44,7 @@ export function plugin(client: PluginClient<Events, Methods>) {
client.onActivate(activateStub); client.onActivate(activateStub);
client.onDeactivate(deactivateStub); client.onDeactivate(deactivateStub);
client.onDestroy(destroyStub); client.onDestroy(destroyStub);
client.onReady(readyStub);
client.onMessage('inc', ({delta}) => { client.onMessage('inc', ({delta}) => {
state.update((draft) => { state.update((draft) => {
draft.count += delta; draft.count += delta;
@@ -81,6 +83,7 @@ export function plugin(client: PluginClient<Events, Methods>) {
connectStub, connectStub,
destroyStub, destroyStub,
disconnectStub, disconnectStub,
readyStub,
getCurrentState, getCurrentState,
state, state,
unhandledMessages, unhandledMessages,

View File

@@ -58,6 +58,7 @@ test('it can start a plugin and lifecycle events', () => {
expect(instance.activateStub).toBeCalledTimes(2); expect(instance.activateStub).toBeCalledTimes(2);
expect(instance.deactivateStub).toBeCalledTimes(2); expect(instance.deactivateStub).toBeCalledTimes(2);
expect(instance.destroyStub).toBeCalledTimes(1); expect(instance.destroyStub).toBeCalledTimes(1);
expect(instance.readyStub).toBeCalledTimes(1);
expect(instance.appName).toBe('TestApplication'); expect(instance.appName).toBe('TestApplication');
expect(instance.appId).toBe('TestApplication#Android#TestDevice#serial-000'); expect(instance.appId).toBe('TestApplication#Android#TestDevice#serial-000');
@@ -220,12 +221,14 @@ test('plugins support non-serializable state', async () => {
}); });
test('plugins support restoring state', async () => { test('plugins support restoring state', async () => {
const readyFn = jest.fn();
const {exportState, instance} = TestUtils.startPlugin( const {exportState, instance} = TestUtils.startPlugin(
{ {
plugin() { plugin(c: PluginClient<{}, {}>) {
const field1 = createState(1, {persist: 'field1'}); const field1 = createState(1, {persist: 'field1'});
const field2 = createState(2); const field2 = createState(2);
const field3 = createState(3, {persist: 'field3'}); const field3 = createState(3, {persist: 'field3'});
c.onReady(readyFn);
return { return {
field1, field1,
field2, field2,
@@ -247,6 +250,7 @@ test('plugins support restoring state', async () => {
expect(field3.get()).toBe('b'); expect(field3.get()).toBe('b');
expect(exportState()).toEqual({field1: 'a', field3: 'b'}); expect(exportState()).toEqual({field1: 'a', field3: 'b'});
expect(readyFn).toBeCalledTimes(1);
}); });
test('plugins cannot use a persist key twice', async () => { test('plugins cannot use a persist key twice', async () => {
@@ -267,6 +271,8 @@ test('plugins cannot use a persist key twice', async () => {
}); });
test('plugins can have custom import handler', () => { test('plugins can have custom import handler', () => {
const readyFn = jest.fn();
const {instance} = TestUtils.startPlugin( const {instance} = TestUtils.startPlugin(
{ {
plugin(client: PluginClient) { plugin(client: PluginClient) {
@@ -277,6 +283,7 @@ test('plugins can have custom import handler', () => {
field1.set(data.a); field1.set(data.a);
field2.set(data.b); field2.set(data.b);
}); });
client.onReady(readyFn);
return {field1, field2}; return {field1, field2};
}, },
@@ -293,6 +300,7 @@ test('plugins can have custom import handler', () => {
); );
expect(instance.field1.get()).toBe(1); expect(instance.field1.get()).toBe(1);
expect(instance.field2.get()).toBe(2); expect(instance.field2.get()).toBe(2);
expect(readyFn).toBeCalledTimes(1);
}); });
test('plugins cannot combine import handler with persist option', async () => { test('plugins cannot combine import handler with persist option', async () => {
@@ -344,7 +352,7 @@ test('plugins can handle import errors', async () => {
expect(console.error.mock.calls).toMatchInlineSnapshot(` expect(console.error.mock.calls).toMatchInlineSnapshot(`
Array [ Array [
Array [ Array [
"Error occurred when importing date for plugin 'TestPlugin': 'Error: Oops", "An error occurred when importing data for plugin 'TestPlugin': 'Error: Oops",
[Error: Oops], [Error: Oops],
], ],
] ]

View File

@@ -66,6 +66,13 @@ export interface BasePluginClient {
*/ */
onImport<T = any>(handler: StateImportHandler<T>): void; onImport<T = any>(handler: StateImportHandler<T>): void;
/**
* The `onReady` event is triggered immediately after a plugin has been initialized and any pending state was restored.
* This event fires after `onImport` / the interpretation of any `persist` flags and indicates that the initialization process has finished.
* This event does not signal that the plugin is loaded in the UI yet (see `onActivated`) and does fire before deeplinks (see `onDeeplink`) are handled.
*/
onReady(handler: () => void): void;
/** /**
* Register menu entries in the Flipper toolbar * Register menu entries in the Flipper toolbar
*/ */
@@ -225,31 +232,40 @@ export abstract class BasePluginInstance {
); );
} }
if (this.initialStates) { if (this.initialStates) {
if (this.importHandler) { try {
try { if (this.importHandler) {
batched(this.importHandler)(this.initialStates); batched(this.importHandler)(this.initialStates);
} catch (e) { } else {
const msg = `Error occurred when importing date for plugin '${this.definition.id}': '${e}`; for (const key in this.rootStates) {
// msg is already specific if (key in this.initialStates) {
// eslint-disable-next-line this.rootStates[key].deserialize(this.initialStates[key]);
console.error(msg, e); } else {
message.error(msg); console.warn(
} `Tried to initialize plugin with existing data, however data for "${key}" is missing. Was the export created with a different Flipper version?`,
} else { );
for (const key in this.rootStates) { }
if (key in this.initialStates) {
this.rootStates[key].deserialize(this.initialStates[key]);
} else {
console.warn(
`Tried to initialize plugin with existing data, however data for "${key}" is missing. Was the export created with a different Flipper version?`,
);
} }
} }
} catch (e) {
const msg = `An error occurred when importing data for plugin '${this.definition.id}': '${e}`;
// msg is already specific
// eslint-disable-next-line
console.error(msg, e);
message.error(msg);
} }
} }
this.initialStates = undefined; this.initialStates = undefined;
setCurrentPluginInstance(undefined); setCurrentPluginInstance(undefined);
} }
try {
this.events.emit('ready');
} catch (e) {
const msg = `An error occurred when initializing plugin '${this.definition.id}': '${e}`;
// msg is already specific
// eslint-disable-next-line
console.error(msg, e);
message.error(msg);
}
} }
protected createBasePluginClient(): BasePluginClient { protected createBasePluginClient(): BasePluginClient {
@@ -280,6 +296,9 @@ export abstract class BasePluginInstance {
} }
this.importHandler = cb; this.importHandler = cb;
}, },
onReady: (cb) => {
this.events.on('ready', batched(cb));
},
addMenuEntry: (...entries) => { addMenuEntry: (...entries) => {
for (const entry of entries) { for (const entry of entries) {
const normalized = normalizeMenuEntry(entry); const normalized = normalizeMenuEntry(entry);

View File

@@ -192,6 +192,13 @@ export function plugin(client: PluginClient) {
} }
``` ```
### `onReady`
The `onReady` event is triggered immediately after a plugin has been initialized and any pending state was restored.
This event fires after `onImport` / the interpretation of any `persist` flags and indicates that the initialization process has finished.
This event does not signal that the plugin is loaded in the UI yet (see `onActivated`) and does fire before deeplinks (see `onDeeplink`) are handled.
If a plugin has complex initialization logic it is recommended to put it in the `onReady` hook, as an error in the onReady hook won't cause the plugin not to be loaded.
### Methods ### Methods
#### `send` #### `send`

View File

@@ -96,6 +96,7 @@ Some abstractions that used to be (for example) static methods on `FlipperPlugin
| `exportPersistedState` | Use the `client.onExport` hook | | `exportPersistedState` | Use the `client.onExport` hook |
| `getActiveNotifications` | Use `client.showNotification` for persistent notifications, or `message` / `notification` from `antd` for one-off notifications. | `getActiveNotifications` | Use `client.showNotification` for persistent notifications, or `message` / `notification` from `antd` for one-off notifications.
| `createTablePlugin` | TBD, so these conversions can be skipped for now | | `createTablePlugin` | TBD, so these conversions can be skipped for now |
| `init` | `client.onReady` |
## Using Sandy / Ant.design to organise the plugin UI ## Using Sandy / Ant.design to organise the plugin UI