Fix potential race conditions for starting/stopping server add-ons
Reviewed By: mweststrate Differential Revision: D34301593 fbshipit-source-id: 2950de8a8567318cd3e87eff176657df5ba8fd1b
This commit is contained in:
committed by
Facebook GitHub Bot
parent
bdbf79e3e1
commit
81d0057a8d
@@ -425,10 +425,12 @@ export class FlipperServerImpl implements FlipperServer {
|
|||||||
'plugins-server-add-on-stop': async (pluginName, owner) =>
|
'plugins-server-add-on-stop': async (pluginName, owner) =>
|
||||||
this.pluginManager.stopServerAddOn(pluginName, owner),
|
this.pluginManager.stopServerAddOn(pluginName, owner),
|
||||||
'plugins-server-add-on-request-response': async (payload) => {
|
'plugins-server-add-on-request-response': async (payload) => {
|
||||||
const serverAddOn = this.pluginManager.getServerAddOnForMessage(payload);
|
try {
|
||||||
if (serverAddOn) {
|
const serverAddOn =
|
||||||
return await serverAddOn.connection.sendExpectResponse(payload);
|
this.pluginManager.getServerAddOnForMessage(payload);
|
||||||
}
|
assertNotNull(serverAddOn);
|
||||||
|
return await serverAddOn.sendExpectResponse(payload);
|
||||||
|
} catch {
|
||||||
return {
|
return {
|
||||||
length: 0,
|
length: 0,
|
||||||
error: {
|
error: {
|
||||||
@@ -439,6 +441,7 @@ export class FlipperServerImpl implements FlipperServer {
|
|||||||
stacktrace: '',
|
stacktrace: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'doctor-get-healthchecks': getHealthChecks,
|
'doctor-get-healthchecks': getHealthChecks,
|
||||||
'doctor-run-healthcheck': runHealthcheck,
|
'doctor-run-healthcheck': runHealthcheck,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
getInstalledPlugin,
|
getInstalledPlugin,
|
||||||
installPluginFromNpm,
|
installPluginFromNpm,
|
||||||
} from 'flipper-plugin-lib';
|
} from 'flipper-plugin-lib';
|
||||||
import {ServerAddOn} from './ServerAddOn';
|
import {ServerAddOnManager} from './ServerAddManager';
|
||||||
|
|
||||||
const maxInstalledPluginVersionsToKeep = 2;
|
const maxInstalledPluginVersionsToKeep = 2;
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ const isExecuteMessage = (message: object): message is ExecuteMessage =>
|
|||||||
(message as ExecuteMessage).method === 'execute';
|
(message as ExecuteMessage).method === 'execute';
|
||||||
|
|
||||||
export class PluginManager {
|
export class PluginManager {
|
||||||
private readonly serverAddOns = new Map<string, ServerAddOn>();
|
private readonly serverAddOns = new Map<string, ServerAddOnManager>();
|
||||||
|
|
||||||
constructor(private readonly flipperServer: FlipperServerForServerAddOn) {}
|
constructor(private readonly flipperServer: FlipperServerForServerAddOn) {}
|
||||||
|
|
||||||
@@ -178,43 +178,89 @@ export class PluginManager {
|
|||||||
pluginName: string,
|
pluginName: string,
|
||||||
details: ServerAddOnStartDetails,
|
details: ServerAddOnStartDetails,
|
||||||
owner: string,
|
owner: string,
|
||||||
) {
|
): Promise<void> {
|
||||||
console.debug('PluginManager.startServerAddOn', pluginName);
|
console.debug('PluginManager.startServerAddOn', pluginName);
|
||||||
const existingServerAddOn = this.serverAddOns.get(pluginName);
|
const existingServerAddOn = this.serverAddOns.get(pluginName);
|
||||||
|
|
||||||
if (existingServerAddOn) {
|
if (existingServerAddOn) {
|
||||||
|
if (existingServerAddOn.state.is('stopping')) {
|
||||||
console.debug(
|
console.debug(
|
||||||
'PluginManager.startServerAddOn -> already started, adding an owner',
|
'PluginManager.startServerAddOn -> currently stropping',
|
||||||
pluginName,
|
pluginName,
|
||||||
owner,
|
owner,
|
||||||
|
existingServerAddOn.state.currentState,
|
||||||
);
|
);
|
||||||
existingServerAddOn.addOwner(owner);
|
await existingServerAddOn.state.wait(['inactive', 'zombie']);
|
||||||
|
return this.startServerAddOn(pluginName, details, owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(
|
||||||
|
'PluginManager.startServerAddOn -> already started',
|
||||||
|
pluginName,
|
||||||
|
owner,
|
||||||
|
existingServerAddOn.state.currentState,
|
||||||
|
);
|
||||||
|
await existingServerAddOn.addOwner(owner);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newServerAddOn = await ServerAddOn.start(
|
const newServerAddOn = new ServerAddOnManager(
|
||||||
pluginName,
|
pluginName,
|
||||||
details,
|
details,
|
||||||
owner,
|
owner,
|
||||||
() => this.serverAddOns.delete(pluginName),
|
|
||||||
this.flipperServer,
|
this.flipperServer,
|
||||||
);
|
);
|
||||||
this.serverAddOns.set(pluginName, newServerAddOn);
|
this.serverAddOns.set(pluginName, newServerAddOn);
|
||||||
|
|
||||||
|
newServerAddOn.state.once(['fatal', 'zombie', 'inactive'], () => {
|
||||||
|
this.serverAddOns.delete(pluginName);
|
||||||
|
});
|
||||||
|
|
||||||
|
await newServerAddOn.state.wait(['active', 'fatal']);
|
||||||
|
|
||||||
|
if (newServerAddOn.state.is('fatal')) {
|
||||||
|
this.serverAddOns.delete(pluginName);
|
||||||
|
throw newServerAddOn.state.error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopServerAddOn(pluginName: string, owner: string) {
|
async stopServerAddOn(pluginName: string, owner: string): Promise<void> {
|
||||||
console.debug('PluginManager.stopServerAddOn', pluginName);
|
console.debug('PluginManager.stopServerAddOn', pluginName);
|
||||||
const serverAddOn = this.serverAddOns.get(pluginName);
|
const serverAddOn = this.serverAddOns.get(pluginName);
|
||||||
|
|
||||||
if (!serverAddOn) {
|
if (!serverAddOn) {
|
||||||
console.debug('PluginManager.stopServerAddOn -> not started', pluginName);
|
console.warn('PluginManager.stopServerAddOn -> not started', pluginName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
serverAddOn.removeOwner(owner);
|
|
||||||
|
try {
|
||||||
|
await serverAddOn.removeOwner(owner);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
'PluginManager.stopServerAddOn -> error',
|
||||||
|
pluginName,
|
||||||
|
owner,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
this.serverAddOns.delete(pluginName);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopAllServerAddOns(owner: string) {
|
stopAllServerAddOns(owner: string) {
|
||||||
console.debug('PluginManager.stopAllServerAddOns');
|
console.debug('PluginManager.stopAllServerAddOns', owner);
|
||||||
this.serverAddOns.forEach((serverAddOn) => {
|
this.serverAddOns.forEach(async (serverAddOnPromise) => {
|
||||||
|
try {
|
||||||
|
const serverAddOn = await serverAddOnPromise;
|
||||||
serverAddOn.removeOwner(owner);
|
serverAddOn.removeOwner(owner);
|
||||||
|
} catch (e) {
|
||||||
|
// It is OK to use a debug level here because any failure would be logged in "stopServerAddOn"
|
||||||
|
console.debug(
|
||||||
|
'PluginManager.stopAllServerAddOns -> failed to remove owner',
|
||||||
|
owner,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
desktop/flipper-server-core/src/plugins/ServerAddManager.tsx
Normal file
147
desktop/flipper-server-core/src/plugins/ServerAddManager.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and 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 {
|
||||||
|
ClientResponseType,
|
||||||
|
ExecuteMessage,
|
||||||
|
FlipperServerForServerAddOn,
|
||||||
|
ServerAddOnStartDetails,
|
||||||
|
} from 'flipper-common';
|
||||||
|
import {assertNotNull} from '../comms/Utilities';
|
||||||
|
import {StateMachine} from '../utils/StateMachine';
|
||||||
|
import {ServerAddOn} from './ServerAddOn';
|
||||||
|
|
||||||
|
type TState =
|
||||||
|
| 'inactive'
|
||||||
|
| 'starting'
|
||||||
|
| 'active'
|
||||||
|
| 'fatal'
|
||||||
|
| 'stopping'
|
||||||
|
| 'zombie';
|
||||||
|
|
||||||
|
export class ServerAddOnManager {
|
||||||
|
public readonly state = new StateMachine<TState, 'fatal'>('inactive');
|
||||||
|
private _serverAddOn?: ServerAddOn;
|
||||||
|
constructor(
|
||||||
|
public readonly pluginName: string,
|
||||||
|
details: ServerAddOnStartDetails,
|
||||||
|
initialOwner: string,
|
||||||
|
flipperServer: FlipperServerForServerAddOn,
|
||||||
|
) {
|
||||||
|
this.startServerAddOn(details, initialOwner, flipperServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendExpectResponse(message: ExecuteMessage): Promise<ClientResponseType> {
|
||||||
|
if (!this.state.is('active')) {
|
||||||
|
console.info(
|
||||||
|
'StateAddOnManager.sendExpectResponse -> error: server add-on is not active, Current state:',
|
||||||
|
this.state.currentState,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
'StateAddOnManager.sendExpectResponse -> error: server add-on is not active',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assertNotNull(
|
||||||
|
this._serverAddOn,
|
||||||
|
'StateAddOnManager.sendExpectResponse -> _serverAddOn is undefined',
|
||||||
|
);
|
||||||
|
return this._serverAddOn.connection.sendExpectResponse(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addOwner(owner: string) {
|
||||||
|
if (this.state.is('starting')) {
|
||||||
|
await this.state.wait(['active', 'fatal']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.is('active')) {
|
||||||
|
console.info(
|
||||||
|
'StateAddOnManager.addOwner -> error: server add-on is not active, Current state:',
|
||||||
|
this.state.currentState,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
'StateAddOnManager.addOwner -> error: server add-on is not active',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assertNotNull(
|
||||||
|
this._serverAddOn,
|
||||||
|
'StateAddOnManager.addOwner -> _serverAddOn is undefined',
|
||||||
|
);
|
||||||
|
|
||||||
|
this._serverAddOn.addOwner(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeOwner(owner: string) {
|
||||||
|
if (this.state.is(['stopping', 'inactive'])) {
|
||||||
|
return this.state.wait(['zombie', 'inactive']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.is('starting')) {
|
||||||
|
await this.state.wait(['active', 'fatal']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.is('active')) {
|
||||||
|
console.debug(
|
||||||
|
'StateAddOnManager.removeOwner -> error: server add-on failed to start, Current state:',
|
||||||
|
this.state.currentState,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(
|
||||||
|
this._serverAddOn,
|
||||||
|
'StateAddOnManager.addOwner -> _serverAddOn is undefined',
|
||||||
|
);
|
||||||
|
|
||||||
|
const stopping = this._serverAddOn.removeOwner(owner);
|
||||||
|
|
||||||
|
if (stopping) {
|
||||||
|
this.state.set('stopping');
|
||||||
|
try {
|
||||||
|
await stopping;
|
||||||
|
this.state.set('inactive');
|
||||||
|
} catch (e) {
|
||||||
|
this.state.set('zombie');
|
||||||
|
console.error(
|
||||||
|
'ServerAddOnManager.removeOwner -> server add-on failed to clean up',
|
||||||
|
this.pluginName,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startServerAddOn(
|
||||||
|
details: ServerAddOnStartDetails,
|
||||||
|
initialOwner: string,
|
||||||
|
flipperServer: FlipperServerForServerAddOn,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
this.state.set('starting');
|
||||||
|
|
||||||
|
this._serverAddOn = await ServerAddOn.start(
|
||||||
|
this.pluginName,
|
||||||
|
details,
|
||||||
|
initialOwner,
|
||||||
|
flipperServer,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.state.set('active');
|
||||||
|
} catch (e) {
|
||||||
|
this.state.set('fatal', e);
|
||||||
|
console.error(
|
||||||
|
'StateAddOnManager.startServerAddOn -> error',
|
||||||
|
this.pluginName,
|
||||||
|
details,
|
||||||
|
initialOwner,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,7 +49,6 @@ const loadPlugin = (
|
|||||||
return serverAddOnModule;
|
return serverAddOnModule;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Fix potential race conditions when starting/stopping concurrently
|
|
||||||
export class ServerAddOn {
|
export class ServerAddOn {
|
||||||
private owners: Set<string>;
|
private owners: Set<string>;
|
||||||
|
|
||||||
@@ -66,7 +65,6 @@ export class ServerAddOn {
|
|||||||
pluginName: string,
|
pluginName: string,
|
||||||
details: ServerAddOnStartDetails,
|
details: ServerAddOnStartDetails,
|
||||||
initialOwner: string,
|
initialOwner: string,
|
||||||
onStop: () => void,
|
|
||||||
flipperServer: FlipperServerForServerAddOn,
|
flipperServer: FlipperServerForServerAddOn,
|
||||||
): Promise<ServerAddOn> {
|
): Promise<ServerAddOn> {
|
||||||
console.info('ServerAddOn.start', pluginName, details);
|
console.info('ServerAddOn.start', pluginName, details);
|
||||||
@@ -89,11 +87,6 @@ export class ServerAddOn {
|
|||||||
`ServerAddOn ${pluginName} must return a clean up function, instead it returned ${typeof cleanup}.`,
|
`ServerAddOn ${pluginName} must return a clean up function, instead it returned ${typeof cleanup}.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const onStopCombined = async () => {
|
|
||||||
onStop();
|
|
||||||
await cleanup();
|
|
||||||
};
|
|
||||||
|
|
||||||
const desktopToModuleConnection = new ServerAddOnDesktopToModuleConnection(
|
const desktopToModuleConnection = new ServerAddOnDesktopToModuleConnection(
|
||||||
serverAddOnModuleToDesktopConnection,
|
serverAddOnModuleToDesktopConnection,
|
||||||
flipperServer,
|
flipperServer,
|
||||||
@@ -101,7 +94,7 @@ export class ServerAddOn {
|
|||||||
|
|
||||||
return new ServerAddOn(
|
return new ServerAddOn(
|
||||||
pluginName,
|
pluginName,
|
||||||
onStopCombined,
|
cleanup,
|
||||||
desktopToModuleConnection,
|
desktopToModuleConnection,
|
||||||
initialOwner,
|
initialOwner,
|
||||||
);
|
);
|
||||||
@@ -115,22 +108,12 @@ export class ServerAddOn {
|
|||||||
const ownerExisted = this.owners.delete(owner);
|
const ownerExisted = this.owners.delete(owner);
|
||||||
|
|
||||||
if (!this.owners.size && ownerExisted) {
|
if (!this.owners.size && ownerExisted) {
|
||||||
this.stop().catch((e) => {
|
return this.stop();
|
||||||
console.error(
|
|
||||||
'ServerAddOn.removeOwner -> failed to stop automatically when no owners left',
|
|
||||||
this.pluginName,
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async stop() {
|
async stop() {
|
||||||
console.info('ServerAddOn.stop', this.pluginName);
|
console.info('ServerAddOn.stop', this.pluginName);
|
||||||
try {
|
|
||||||
await this.cleanup();
|
await this.cleanup();
|
||||||
} catch (e) {
|
|
||||||
console.error('ServerAddOn.stop -> failed to clean up', this.pluginName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import EventEmitter from 'events';
|
|
||||||
import {sleep} from 'flipper-common';
|
import {sleep} from 'flipper-common';
|
||||||
import {assertNotNull} from '../comms/Utilities';
|
import {assertNotNull} from '../comms/Utilities';
|
||||||
|
import {StateMachine} from './StateMachine';
|
||||||
|
|
||||||
export const RESTART_CNT = 3;
|
export const RESTART_CNT = 3;
|
||||||
const RESTART_SLEEP = 100;
|
const RESTART_SLEEP = 100;
|
||||||
@@ -21,84 +21,13 @@ export type DeviceLogListenerState =
|
|||||||
| 'inactive'
|
| 'inactive'
|
||||||
| 'fatal'
|
| 'fatal'
|
||||||
| 'zombie';
|
| 'zombie';
|
||||||
class State {
|
|
||||||
private _currentState: DeviceLogListenerState = 'inactive';
|
|
||||||
private _error?: Error;
|
|
||||||
private valueEmitter = new EventEmitter();
|
|
||||||
|
|
||||||
get error() {
|
|
||||||
return this._error;
|
|
||||||
}
|
|
||||||
|
|
||||||
get currentState() {
|
|
||||||
return this._currentState;
|
|
||||||
}
|
|
||||||
|
|
||||||
set<T extends DeviceLogListenerState>(
|
|
||||||
...[newState, error]: T extends 'fatal' | 'zombie' ? [T, Error] : [T]
|
|
||||||
) {
|
|
||||||
this._currentState = newState;
|
|
||||||
this._error = error;
|
|
||||||
this.valueEmitter.emit(newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
once(
|
|
||||||
state: DeviceLogListenerState | DeviceLogListenerState[],
|
|
||||||
cb: () => void,
|
|
||||||
): () => void {
|
|
||||||
return this.subscribe(state, cb, {once: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
on(
|
|
||||||
state: DeviceLogListenerState | DeviceLogListenerState[],
|
|
||||||
cb: () => void,
|
|
||||||
): () => void {
|
|
||||||
return this.subscribe(state, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
is(targetState: DeviceLogListenerState | DeviceLogListenerState[]) {
|
|
||||||
if (!Array.isArray(targetState)) {
|
|
||||||
targetState = [targetState];
|
|
||||||
}
|
|
||||||
return targetState.includes(this._currentState);
|
|
||||||
}
|
|
||||||
|
|
||||||
private subscribe(
|
|
||||||
state: DeviceLogListenerState | DeviceLogListenerState[],
|
|
||||||
cb: () => void,
|
|
||||||
{once}: {once?: boolean} = {},
|
|
||||||
): () => void {
|
|
||||||
const statesNormalized = Array.isArray(state) ? state : [state];
|
|
||||||
|
|
||||||
if (statesNormalized.includes(this._currentState)) {
|
|
||||||
cb();
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
let executed = false;
|
|
||||||
const wrappedCb = () => {
|
|
||||||
if (!executed) {
|
|
||||||
executed = true;
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fn = once ? 'once' : 'on';
|
|
||||||
statesNormalized.forEach((item) => {
|
|
||||||
this.valueEmitter[fn](item, wrappedCb);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
statesNormalized.forEach((item) => {
|
|
||||||
this.valueEmitter.off(item, wrappedCb);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class DeviceListener {
|
export abstract class DeviceListener {
|
||||||
private name: string = this.constructor.name;
|
private name: string = this.constructor.name;
|
||||||
protected _state = new State();
|
protected _state = new StateMachine<
|
||||||
|
DeviceLogListenerState,
|
||||||
|
'fatal' | 'zombie'
|
||||||
|
>('inactive');
|
||||||
|
|
||||||
private stopLogListener?: () => Promise<void> | void;
|
private stopLogListener?: () => Promise<void> | void;
|
||||||
|
|
||||||
|
|||||||
86
desktop/flipper-server-core/src/utils/StateMachine.tsx
Normal file
86
desktop/flipper-server-core/src/utils/StateMachine.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and 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 {EventEmitter} from 'events';
|
||||||
|
|
||||||
|
export class StateMachine<TState extends string, TError extends TState> {
|
||||||
|
private _error?: Error;
|
||||||
|
private valueEmitter = new EventEmitter();
|
||||||
|
|
||||||
|
constructor(private _currentState: TState) {}
|
||||||
|
|
||||||
|
get error() {
|
||||||
|
return this._error;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentState() {
|
||||||
|
return this._currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
set<T extends TState>(
|
||||||
|
...[newState, error]: T extends TError ? [T, Error] : [T]
|
||||||
|
) {
|
||||||
|
this._currentState = newState as TState;
|
||||||
|
this._error = error;
|
||||||
|
this.valueEmitter.emit(newState as TState);
|
||||||
|
}
|
||||||
|
|
||||||
|
wait<T extends TState | TState[]>(state: T): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.once(state, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
once(state: TState | TState[], cb: () => void): () => void {
|
||||||
|
return this.subscribe(state, cb, {once: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
on(state: TState | TState[], cb: () => void): () => void {
|
||||||
|
return this.subscribe(state, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
is(targetState: TState | TState[]) {
|
||||||
|
if (!Array.isArray(targetState)) {
|
||||||
|
targetState = [targetState];
|
||||||
|
}
|
||||||
|
return targetState.includes(this._currentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribe(
|
||||||
|
state: TState | TState[],
|
||||||
|
cb: () => void,
|
||||||
|
{once}: {once?: boolean} = {},
|
||||||
|
): () => void {
|
||||||
|
const statesNormalized = Array.isArray(state) ? state : [state];
|
||||||
|
|
||||||
|
if (statesNormalized.includes(this._currentState)) {
|
||||||
|
cb();
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let executed = false;
|
||||||
|
const wrappedCb = () => {
|
||||||
|
if (!executed) {
|
||||||
|
executed = true;
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fn = once ? 'once' : 'on';
|
||||||
|
statesNormalized.forEach((item) => {
|
||||||
|
this.valueEmitter[fn](item, wrappedCb);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
statesNormalized.forEach((item) => {
|
||||||
|
this.valueEmitter.off(item, wrappedCb);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user