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:
committed by
Facebook GitHub Bot
parent
616341f649
commit
dd7a9f5195
@@ -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,
|
||||||
|
|||||||
@@ -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],
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user