• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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