1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15export type ControllerAny = Controller</* StateType=*/ any>; 16 17export interface ControllerFactory<ConstructorArgs> { 18 new(args: ConstructorArgs): ControllerAny; 19} 20 21interface ControllerInitializer<ConstructorArgs> { 22 id: string; 23 factory: ControllerFactory<ConstructorArgs>; 24 args: ConstructorArgs; 25} 26 27export type ControllerInitializerAny = ControllerInitializer<any>; 28 29export function Child<ConstructorArgs>( 30 id: string, 31 factory: ControllerFactory<ConstructorArgs>, 32 args: ConstructorArgs): ControllerInitializer<ConstructorArgs> { 33 return {id, factory, args}; 34} 35 36export type Children = ControllerInitializerAny[]; 37 38export abstract class Controller<StateType> { 39 // This is about the local FSM state, has nothing to do with the global 40 // app state. 41 private _stateChanged = false; 42 private _inRunner = false; 43 private _state: StateType; 44 private _children = new Map<string, ControllerAny>(); 45 46 constructor(initialState: StateType) { 47 this._state = initialState; 48 } 49 50 abstract run(): Children|void; 51 onDestroy(): void {} 52 53 // Invokes the current controller subtree, recursing into children. 54 // While doing so handles lifecycle of child controllers. 55 // This method should be called only by the runControllers() method in 56 // globals.ts. Exposed publicly for testing. 57 invoke(): boolean { 58 if (this._inRunner) throw new Error('Reentrancy in Controller'); 59 this._stateChanged = false; 60 this._inRunner = true; 61 const resArray = this.run(); 62 let triggerAnotherRun = this._stateChanged; 63 this._stateChanged = false; 64 65 const nextChildren = new Map<string, ControllerInitializerAny>(); 66 if (resArray !== undefined) { 67 for (const childConfig of resArray) { 68 if (nextChildren.has(childConfig.id)) { 69 throw new Error(`Duplicate children controller ${childConfig.id}`); 70 } 71 nextChildren.set(childConfig.id, childConfig); 72 } 73 } 74 const dtors = new Array<(() => void)>(); 75 const runners = new Array<(() => boolean)>(); 76 for (const key of this._children.keys()) { 77 if (nextChildren.has(key)) continue; 78 const instance = this._children.get(key)!; 79 this._children.delete(key); 80 dtors.push(() => instance.onDestroy()); 81 } 82 for (const nextChild of nextChildren.values()) { 83 if (!this._children.has(nextChild.id)) { 84 const instance = new nextChild.factory(nextChild.args); 85 this._children.set(nextChild.id, instance); 86 } 87 const instance = this._children.get(nextChild.id)!; 88 runners.push(() => instance.invoke()); 89 } 90 91 for (const dtor of dtors) dtor(); // Invoke all onDestroy()s. 92 93 // Invoke all runner()s. 94 for (const runner of runners) { 95 const recursiveRes = runner(); 96 triggerAnotherRun = triggerAnotherRun || recursiveRes; 97 } 98 99 this._inRunner = false; 100 return triggerAnotherRun; 101 } 102 103 setState(state: StateType) { 104 if (!this._inRunner) { 105 throw new Error('Cannot setState() outside of the run() method'); 106 } 107 this._stateChanged = state !== this._state; 108 this._state = state; 109 } 110 111 get state(): StateType { 112 return this._state; 113 } 114} 115