| // Copyright (C) 2018 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| export type ControllerAny = Controller</* StateType=*/ unknown>; |
| |
| export interface ControllerFactory<ConstructorArgs> { |
| new (args: ConstructorArgs): ControllerAny; |
| } |
| |
| interface ControllerInitializer<ConstructorArgs> { |
| id: string; |
| factory: ControllerFactory<ConstructorArgs>; |
| args: ConstructorArgs; |
| } |
| |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| export type ControllerInitializerAny = ControllerInitializer<any>; |
| |
| export function Child<ConstructorArgs>( |
| id: string, |
| factory: ControllerFactory<ConstructorArgs>, |
| args: ConstructorArgs, |
| ): ControllerInitializer<ConstructorArgs> { |
| return {id, factory, args}; |
| } |
| |
| export type Children = ControllerInitializerAny[]; |
| |
| export abstract class Controller<StateType> { |
| // This is about the local FSM state, has nothing to do with the global |
| // app state. |
| private _stateChanged = false; |
| private _inRunner = false; |
| private _state: StateType; |
| private _children = new Map<string, ControllerAny>(); |
| |
| constructor(initialState: StateType) { |
| this._state = initialState; |
| } |
| |
| abstract run(): Children | void; |
| onDestroy(): void {} |
| |
| // Invokes the current controller subtree, recursing into children. |
| // While doing so handles lifecycle of child controllers. |
| // This method should be called only by the runControllers() method in |
| // globals.ts. Exposed publicly for testing. |
| invoke(): boolean { |
| if (this._inRunner) throw new Error('Reentrancy in Controller'); |
| this._stateChanged = false; |
| this._inRunner = true; |
| const resArray = this.run(); |
| let triggerAnotherRun = this._stateChanged; |
| this._stateChanged = false; |
| |
| const nextChildren = new Map<string, ControllerInitializerAny>(); |
| if (resArray !== undefined) { |
| for (const childConfig of resArray) { |
| if (nextChildren.has(childConfig.id)) { |
| throw new Error(`Duplicate children controller ${childConfig.id}`); |
| } |
| nextChildren.set(childConfig.id, childConfig); |
| } |
| } |
| const dtors = new Array<() => void>(); |
| const runners = new Array<() => boolean>(); |
| for (const key of this._children.keys()) { |
| if (nextChildren.has(key)) continue; |
| const instance = this._children.get(key)!; |
| this._children.delete(key); |
| dtors.push(() => instance.onDestroy()); |
| } |
| for (const nextChild of nextChildren.values()) { |
| if (!this._children.has(nextChild.id)) { |
| const instance = new nextChild.factory(nextChild.args); |
| this._children.set(nextChild.id, instance); |
| } |
| const instance = this._children.get(nextChild.id)!; |
| runners.push(() => instance.invoke()); |
| } |
| |
| for (const dtor of dtors) dtor(); // Invoke all onDestroy()s. |
| |
| // Invoke all runner()s. |
| for (const runner of runners) { |
| const recursiveRes = runner(); |
| triggerAnotherRun = triggerAnotherRun || recursiveRes; |
| } |
| |
| this._inRunner = false; |
| return triggerAnotherRun; |
| } |
| |
| setState(state: StateType) { |
| if (!this._inRunner) { |
| throw new Error('Cannot setState() outside of the run() method'); |
| } |
| this._stateChanged = state !== this._state; |
| this._state = state; |
| } |
| |
| get state(): StateType { |
| return this._state; |
| } |
| } |