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 15// tslint:disable-next-line no-any 16export type ControllerAny = Controller</*StateType=*/any>; 17 18export interface ControllerFactory<ConstructorArgs> { 19 new(args: ConstructorArgs): ControllerAny; 20} 21 22interface ControllerInitializer<ConstructorArgs> { 23 id: string; 24 factory: ControllerFactory<ConstructorArgs>; 25 args: ConstructorArgs; 26} 27 28// tslint:disable-next-line no-any 29export type ControllerInitializerAny = ControllerInitializer<any>; 30 31export function Child<ConstructorArgs>( 32 id: string, 33 factory: ControllerFactory<ConstructorArgs>, 34 args: ConstructorArgs): ControllerInitializer<ConstructorArgs> { 35 return {id, factory, args}; 36} 37 38export type Children = ControllerInitializerAny[]; 39 40export abstract class Controller<StateType> { 41 // This is about the local FSM state, has nothing to do with the global 42 // app state. 43 private _stateChanged = false; 44 private _inRunner = false; 45 private _state: StateType; 46 private _children = new Map<string, ControllerAny>(); 47 48 constructor(initialState: StateType) { 49 this._state = initialState; 50 } 51 52 abstract run(): Children|void; 53 onDestroy(): void {} 54 55 // Invokes the current controller subtree, recursing into children. 56 // While doing so handles lifecycle of child controllers. 57 // This method should be called only by the runControllers() method in 58 // globals.ts. Exposed publicly for testing. 59 invoke(): boolean { 60 if (this._inRunner) throw new Error('Reentrancy in Controller'); 61 this._stateChanged = false; 62 this._inRunner = true; 63 const resArray = this.run(); 64 let triggerAnotherRun = this._stateChanged; 65 this._stateChanged = false; 66 67 const nextChildren = new Map<string, ControllerInitializerAny>(); 68 if (resArray !== undefined) { 69 for (const childConfig of resArray) { 70 if (nextChildren.has(childConfig.id)) { 71 throw new Error(`Duplicate children controller ${childConfig.id}`); 72 } 73 nextChildren.set(childConfig.id, childConfig); 74 } 75 } 76 const dtors = new Array<(() => void)>(); 77 const runners = new Array<(() => boolean)>(); 78 for (const key of this._children.keys()) { 79 if (nextChildren.has(key)) continue; 80 const instance = this._children.get(key)!; 81 this._children.delete(key); 82 dtors.push(() => instance.onDestroy()); 83 } 84 for (const nextChild of nextChildren.values()) { 85 if (!this._children.has(nextChild.id)) { 86 const instance = new nextChild.factory(nextChild.args); 87 this._children.set(nextChild.id, instance); 88 } 89 const instance = this._children.get(nextChild.id)!; 90 runners.push(() => instance.invoke()); 91 } 92 93 for (const dtor of dtors) dtor(); // Invoke all onDestroy()s. 94 95 // Invoke all runner()s. 96 for (const runner of runners) { 97 const recursiveRes = runner(); 98 triggerAnotherRun = triggerAnotherRun || recursiveRes; 99 } 100 101 this._inRunner = false; 102 return triggerAnotherRun; 103 } 104 105 setState(state: StateType) { 106 if (!this._inRunner) { 107 throw new Error('Cannot setState() outside of the run() method'); 108 } 109 this._stateChanged = state !== this._state; 110 this._state = state; 111 } 112 113 get state(): StateType { 114 return this._state; 115 } 116} 117