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