  if (viewModel) {
        action(viewModel);
      } else {
        this.#attachedViewModels.splice(i, 1);
      }
    }
  }

  *enumerateLines(searchText) {
    for (let source of this.#sources) {
      yield* source.enumerateLines(searchText);
    }
  }

  /**
   *
   * @param {Function} createSourceFn (aggregatorApi) used to create Data Source.
   *                   aggregatorApi is the way for Data Source to push data
   *                   to the Aggregator.
   */
  addSource(createSourceFn) {
    const api = this.#apiForDataSource();
    const source = createSourceFn(api);
    this.#sources.push(source);
  }

  callFunction(dataSource, functionName, ...params) {
    const source = this.#sources.find(
      source => source.constructor.name === dataSource
    );

    if (source && source[functionName]) {
      return source[functionName](params);
    }

    return null;
  }

  /**
   * Exposes interface for a datasource to communicate with Aggregator.
   */
  #apiForDataSource() {
    const aggregator = this;
    return {
      refreshSingleLineOnScreen(line) {
        aggregator.forEachViewModel(vm => vm.refreshSingleLineOnScreen(line));
      },

      refreshAllLinesOnScreen() {
        aggregator.forEachViewModel(vm => vm.refreshAllLinesOnScreen());
      },

      setLayout(layout) {
        aggregator.forEachViewModel(vm => vm.setLayout(layout));
      },

      setNotification(notification) {
        aggregator.forEachViewModel(vm => vm.setNotification(notification));
      },

      setDisplayMode(displayMode) {
        aggregator.forEachViewModel(vm => vm.setDisplayMode(displayMode));
      },

      discardChangesConfirmed() {
        aggregator.forEachViewModel(vm => vm.discardChangesConfirmed());
      },

      setPrimaryPasswordAuthenticated(isAuthenticated) {
        aggregator.forEachViewModel(vm =>
          vm.setPrimaryPasswordAuthenticated(isAuthenticated)
        );
      },
    };
  }
}
PK