File

src/app/shared/widgets/configuration/app.service.ts

Index

Properties

Properties

Signature : [observable: Observable<any>]
Returns : Observable<any>
import {
  BehaviorSubject,
  defer as observableDefer,
  EMPTY,
  Observable,
  of as observableOf,
  ReplaySubject,
  Subject,
  Subscription,
} from "rxjs";

import { map, startWith, switchMap } from "rxjs/operators";
import { Injectable, Injector } from "@angular/core";
import { Configuration, Widget } from "./app.configuration";
import {
  AppController,
  AppControllerFactory,
  AppParams,
  ControllerRegistry,
} from "./app.controller";
import { HttpClient } from "@angular/common/http";
import {
  AppGlobalChannelsService,
  GlobalChannel,
  GlobalChannelRegistry,
} from "./app.global-channels.service";
import { ChannelDescriptor, WidgetMetadata } from "../widget.descriptor";
import { WidgetConfig } from "../widget.configuration";
import { Link } from "../../components/hal/hal";
import { NmModuleDescriptor } from "../../../nm.module-descriptor";

const NG_ON_INIT = "ngOnInit";

export function appServiceFactory(
  http: HttpClient,
  channels: AppGlobalChannelsService,
  injector: Injector
): AppService {
  return new AppService(http, channels, injector);
}

export function uuid() {
  return ("" + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (r) => {
    let c = Number(r);
    return (
      c ^
      (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
    ).toString(16);
  });
}

export interface DefaultChannelOptions {
  createRelay: () => Subject<any>;
}

export interface ChannelOptions {
  relay?: Subject<any>;
  subscribeTargetsFirst?: boolean;
}

export type SubjectOrOptions =
  | ChannelOptions
  | Subject<any>
  | ReplaySubject<any>
  | BehaviorSubject<any>;

export interface WidgetChannel {
  widget: string;
  channel: string;
}

export interface Supplier {
  (): any;
}

export interface Consumer {
  (input: any): any;
}

export type SourceChannel =
  | Observable<any>
  | WidgetChannel
  | GlobalChannel
  | Supplier
  | (Observable<any> | WidgetChannel | GlobalChannel | Supplier)[];

export type TargetChannel =
  | Subject<any>
  | WidgetChannel
  | GlobalChannel
  | Consumer
  | (Subject<any> | WidgetChannel | GlobalChannel | Consumer)[];

export interface Transformation {
  (observable: Observable<any>): Observable<any>;
}

const DEFAULT_TRANSFORMATION: Transformation = (observable) => observable;

export interface ChannelRef {
  subscription: Subscription;
  widgetId?: string;
}

export interface SourceChannelRef extends ChannelRef {
  observable: Observable<any>;
}

export interface TargetChannelRef extends ChannelRef {
  subject: Subject<any>;
}

export interface ChannelConfig {
  transformation: Transformation;
}

export interface SourceChannelConfig extends ChannelConfig {
  channel: SourceChannel;
  refs: SourceChannelRef | Array<SourceChannelRef>;
}

export interface TargetChannelConfig extends ChannelConfig {
  channel: TargetChannel;
  refs: TargetChannelRef | Array<TargetChannelRef>;
}

type ExtractRefs<C> = C extends SourceChannelConfig
  ? SourceChannelRef
  : TargetChannelRef;

export interface Bridge {
  name: string;
  sources: SourceChannelConfig[];
  targets: TargetChannelConfig[];

  relay: Subject<any>;

  initialized: boolean;
  options: ChannelOptions;
}

export interface ChannelBridgeBuilder {
  from(
    channel: SourceChannel,
    transformation?: Transformation
  ): ChannelBridgeBuilder;

  to(
    channel: TargetChannel,
    transformation?: Transformation
  ): ChannelBridgeBuilder;
}

export class ChannelBridgeBuilderImpl implements ChannelBridgeBuilder {
  readonly bridge: Bridge;

  constructor(
    name: string,
    private config: Configuration,
    private options: ChannelOptions
  ) {
    if (!name) {
      throw new Error("name is required");
    }

    this.bridge = {
      name: name,
      sources: [],
      targets: [],
      relay: options.relay,
      initialized: false,
      options,
    };

    this.bridge.toString = function (): string {
      return this.name;
    };
  }

  public from(
    channel: SourceChannel,
    transformation: Transformation = DEFAULT_TRANSFORMATION
  ): ChannelBridgeBuilder {
    if (!this.canCreateBridge(channel)) {
      return this;
    }
    let channels: Array<SourceChannel> = [];
    channels = channels.concat(channel);

    channels.forEach((ch) => {
      this.bridge.sources.push({
        channel: ch,
        transformation: transformation,
        refs: null,
      });
    });

    return this;
  }

  public to(
    channel: TargetChannel,
    transformation: Transformation = DEFAULT_TRANSFORMATION
  ): ChannelBridgeBuilder {
    // If this is a widget-channel try to check if the widget exists. If not dont create a new target-bridge
    if (!this.canCreateBridge(channel)) {
      return this;
    }
    let channels: Array<TargetChannel> = [];
    channels = channels.concat(channel);

    channels.forEach((ch) => {
      this.bridge.targets.push({
        channel: ch,
        transformation: transformation,
        refs: null,
      });
    });

    return this;
  }

  private canCreateBridge(channel: SourceChannel | TargetChannel): boolean {
    if (isWidgetChannel(channel)) {
      let exists = this.config.widgets.hasOwnProperty(channel.widget);
      if (!exists) {
        console.debug(
          `Unable to find widget ${channel.widget} the bridge to the channel ${channel.channel} will not be created`
        );
        return false;
      }
    }
    return true;
  }
}

export interface WidgetData {
  widgetId: string;
  id: string;
  widget: any;
  descriptor: WidgetMetadata;
  initializeChannels: () => void;
}

function isChannelOptions(options: any): options is ChannelOptions {
  return (
    options.hasOwnProperty("relay") ||
    options.hasOwnProperty("subscribeTargetsFirst")
  );
}

function isGlobalChannel(channel: any): channel is GlobalChannel {
  return channel.hasOwnProperty("channel") && !channel.hasOwnProperty("widget");
}

function isWidgetChannel(channel: any): channel is WidgetChannel {
  return channel.hasOwnProperty("widget") && channel.hasOwnProperty("channel");
}

function isSubject(channel: any): channel is Subject<any> {
  return channel instanceof Subject;
}

function isObservable(channel: any): channel is Observable<any> {
  return channel instanceof Observable;
}

function isConsumer(channel: TargetChannel): channel is Consumer {
  return typeof channel == "function" && channel.length == 1;
}

function isSupplier(channel: SourceChannel): channel is Supplier {
  return typeof channel == "function" && channel.length == 0;
}

export interface ControllerService {
  registerController(name: string): void;
}

export interface ChannelService {
  connectDefaults(defaults: DefaultChannelOptions): void;

  connect(
    name: string,
    subjectOrOptions?: SubjectOrOptions
  ): ChannelBridgeBuilder;
}

declare var lload: any;
declare var contextPath: any;
declare var nmLoader: any;

@Injectable()
export class AppService implements ControllerService, ChannelService {
  private config: Configuration = null;
  private widgetConfigs: Map<string, WidgetConfig>;

  private channels: Set<Bridge>;
  private controllers: Set<AppController>;

  private _detached = new ReplaySubject<any>(1);
  private _detachedObs = this._detached.asObservable().pipe(startWith(false));
  private _activeContext = new ReplaySubject<any>(1);
  private _initialized = new ReplaySubject<any>(1);
  private _componentInitialized = new ReplaySubject<any>(1);

  private global: GlobalChannelRegistry;

  private connectDefaultOpts: DefaultChannelOptions = {
    createRelay: () => new ReplaySubject(1),
  };

  public get configurations() {
    return this.config;
  }

  public constructor(
    private http: HttpClient,
    globalChannelsService: AppGlobalChannelsService,
    private injector: Injector
  ) {
    this.initialize();

    // clone the global channel registry, so that we can register page specific
    // global channels here
    this.global = globalChannelsService.clone();
    this.global.registerSource("initialized", this._initialized);
    this.global.registerSource(
      "component-initialized",
      this._componentInitialized
    );
    this.global.registerSource(
      "initialized-active-context",
      this._activeContext.pipe(
        switchMap((ctx) => this._initialized.pipe(map((_) => ctx)))
      ),
      (params, prop) => (params && prop ? params[prop] : params)
    );
    this.global.registerSource(
      "active-context",
      this._activeContext,
      (params, prop) => (params && prop ? params[prop] : params)
    );
  }

  /**
   * detaches all active channels
   *
   * incoming events from source channels will be buffered in the relay "ReplaySubject", which
   * keeps the latest event from all it's sources.
   */
  public detach() {
    this._detached.next(true);
  }

  /**
   * re-attaches all channels
   *
   * all target channels will re-subscribe to the relay "ReplaySubjects", which automatically
   * re-emits the latest value from it's source channels
   */
  public attach() {
    this._detached.next(false);
  }

  set activeContext(param: any) {
    this._activeContext.next(param);
  }

  public initialized() {
    this._initialized.next(true);
    this._initialized.complete();
  }

  public componentInitialized(id: string) {
    this._componentInitialized.next(id);
  }

  /**
   * @deprecated use ControllerRegistry.registerController directly
   */
  public static registerAppController(
    name: string,
    factory: AppControllerFactory
  ) {
    ControllerRegistry.registerController(name, factory);
  }

  public loadPage(params: AppParams): Observable<Configuration> {
    return this.http.get(params.href).pipe(
      map((response) => <Configuration>response),
      map((config) => this.configurationLoaded(<Configuration>config))
    );
  }

  private configurationLoaded(config: Configuration): Configuration {
    this.config = config;
    if (config.messages) {
      console.log(
        `CONFIGURATION MESSAGES for ${config.module}:${config.identifier}:${config.profile}`
      );
      config.messages.forEach((msg) =>
        console.log(msg.level, msg.path, msg.message)
      );
    }

    let controllerId = config["root-controller"];
    if (!controllerId) {
      throw new Error("no root controller configured");
    }

    let widgetId = config["root-widget"];
    if (!widgetId) {
      throw new Error("no root widget configured");
    }

    // initializes the root controller and allows the root controller to register sub-controllers if required
    // after any initialization of the controller, the required channel connections can be registered
    this.registerController(controllerId);
    return config;
  }

  public registerController(controllerId: string) {
    let controllerConfig = this.config.controllers[controllerId];
    if (!controllerConfig) {
      throw new Error(
        `no configuration for controller "${controllerId}" found`
      );
    }

    let controllerName = controllerConfig.controller;
    let factory = ControllerRegistry.get(controllerName);

    if (!factory) {
      throw new Error(
        `no factory for controller "${controllerId}:${controllerName}" found`
      );
    }

    let controller = factory.create(this.injector);

    this.controllers.add(controller);

    controller.initialize(controllerConfig, this);
    controller.connect(this);
  }

  public connectDefaults(defaults: DefaultChannelOptions): void {
    this.connectDefaultOpts = defaults;
  }

  public connect(
    name: string,
    subjectOrOptions?: SubjectOrOptions
  ): ChannelBridgeBuilder {
    let options: ChannelOptions;

    if (subjectOrOptions == null) {
      options = { relay: this.connectDefaultOpts.createRelay() };
    } else if (isSubject(subjectOrOptions)) {
      let subject = subjectOrOptions as Subject<any>;
      options = { relay: subject };
    } else if (isChannelOptions(subjectOrOptions)) {
      options = subjectOrOptions;

      if (options.relay == null) {
        options.relay = this.connectDefaultOpts.createRelay();
      }

      if (options.subscribeTargetsFirst == null) {
        options.subscribeTargetsFirst = false;
      }
    }

    let builder = new ChannelBridgeBuilderImpl(name, this.config, options);

    this.channels.add(builder.bridge);

    return builder;
  }

  public configure(
    id: string,
    widget: any,
    descriptor: WidgetMetadata,
    configuration: WidgetConfig,
    beforeInitCallback?: (widget) => void
  ): WidgetData {
    let data: WidgetData = {
      widgetId: uuid(),
      id: id,
      widget: widget,
      descriptor: descriptor,
      initializeChannels: () => {
        try {
          if (this.channels) {
            this.channels.forEach((bridge) =>
              this.initializeBridge(bridge, data)
            );
          }
        } catch (err) {
          console.error(err);
          throw new Error(
            "exception during onInit or connectInput/-Output " +
              JSON.stringify(err)
          );
        }
      },
    };

    if (descriptor.propertyConfiguration) {
      widget[descriptor.propertyConfiguration] = configuration;
    }

    if (descriptor.propertyId) {
      widget[descriptor.propertyId] = id;
    }

    if (descriptor.methodConfigure) {
      widget[descriptor.methodConfigure](configuration);
    }

    if (beforeInitCallback) {
      beforeInitCallback(widget);
    }

    return data;
  }

  private static unsubscribeRef(
    channel: SourceChannelConfig,
    predicate: (ref: SourceChannelRef) => boolean
  ): boolean;
  private static unsubscribeRef(
    channel: TargetChannelConfig,
    predicate: (ref: TargetChannelRef) => boolean
  ): boolean;
  private static unsubscribeRef(
    channel: any,
    predicate: (ref: any) => boolean
  ): boolean {
    if (!channel.refs) {
      return;
    }

    let reset = false;

    let refs = Array.isArray(channel.refs) ? channel.refs : [channel.refs];
    refs = refs.filter((ref) => {
      if (ref == null) {
        return false;
      }

      let unsubscribe = predicate(ref);
      if (unsubscribe) {
        ref.subscription.unsubscribe();
        reset = true;
      }

      return !unsubscribe;
    });

    channel.refs = AppService.toRefs(refs);

    return reset;
  }

  public destroyComponent(id: string) {
    this.channels.forEach((bridge) => {
      let reset = false;
      bridge.sources
        .filter((source) => isWidgetChannel(source.channel))
        .forEach((source) => {
          reset = AppService.unsubscribeRef(
            source,
            (ref) => ref.widgetId && ref.widgetId == id && !!ref.subscription
          );
        });

      bridge.targets
        .filter((target) => isWidgetChannel(target.channel))
        .forEach((target) => {
          reset = AppService.unsubscribeRef(
            target,
            (ref) => ref.widgetId && ref.widgetId == id && !!ref.subscription
          );
        });

      if (reset) {
        bridge.initialized = false;
      }
    });
  }

  private initializeBridge(bridge: Bridge, widget: WidgetData) {
    if (bridge.initialized) {
      return;
    }
    const subscribeTargetsFirst = bridge.options.subscribeTargetsFirst;

    let targetsReady;

    // check, if we want to subscribe to targets first
    // to ensure the connection is established, before events from any sources
    // can be emitted
    if (subscribeTargetsFirst) {
      targetsReady = this.resolveTargets(bridge, widget);
    }

    // try to resolve all sources
    let sourcesReady = this.resolveSources(bridge, widget);

    // .. and targets, if they weren't already resolve above
    if (!subscribeTargetsFirst) {
      targetsReady = this.resolveTargets(bridge, widget);
    }

    if (sourcesReady && targetsReady) {
      console.log(`bridge "${bridge.name}" fully initialized`);
      bridge.initialized = true;
    }
  }

  private resolveTargets(bridge: Bridge, widget: WidgetData) {
    let targetsReady = true;
    for (let target of bridge.targets) {
      let isResolved = this.resolveTarget(target, widget, bridge);
      if (!isResolved) {
        targetsReady = false;
      }
    }
    return targetsReady;
  }

  private resolveSources(bridge: Bridge, widget: WidgetData) {
    let sourcesReady = true;
    for (let source of bridge.sources) {
      let isResolved = this.resolveSource(source, widget, bridge);
      if (!isResolved) {
        sourcesReady = false;
      }
    }
    return sourcesReady;
  }

  /**
   * build a refs property. if the result is only on element, that element will be returned.
   * If more than one ref exist, return an array
   *
   * @param refs array of references
   * @param newRef reference to add
   */
  static toRefs<T>(refs: T | Array<T>, newRef: T = null): T | Array<T> {
    if (refs == null) {
      return newRef;
    }

    if (newRef == null) {
      if (Array.isArray(refs)) {
        if (refs.length == 0) {
          return null;
        }
        if (refs.length == 1) {
          return refs[0];
        }
      }

      return refs;
    }

    if (Array.isArray(refs)) {
      return [].concat(...refs, newRef);
    }

    return [refs, newRef];
  }

  private static subscribeSource(
    config: SourceChannelConfig,
    observable: Observable<any>,
    relay: Subject<any>,
    widgetId: string = null
  ) {
    if (!observable) {
      return;
    }

    observable = config.transformation(observable);
    let newRef = {
      observable,
      widgetId,
      subscription: observable.subscribe(relay),
    };

    config.refs = AppService.toRefs(config.refs, newRef);
  }

  private static subscribeTarget(
    config: TargetChannelConfig,
    subject: Subject<any>,
    relay: Subject<any>,
    detached,
    widgetId: string = null
  ) {
    if (!subject) {
      return;
    }

    let observable = relay.asObservable();
    let source = detached.pipe(
      switchMap((isDetached) => (isDetached ? EMPTY : observable))
    );

    let newRef = {
      subject,
      widgetId,
      subscription: config.transformation(source).subscribe(subject),
    };

    config.refs = AppService.toRefs(config.refs, newRef);
  }

  private resolveSource(
    config: SourceChannelConfig,
    data: WidgetData,
    bridge: Bridge
  ): boolean {
    let channel = config.channel;

    if (isWidgetChannel(channel)) {
      let ch = <WidgetChannel>channel;
      if (ch.widget !== data.id) {
        return false;
      }

      let descriptor = data.descriptor;
      let channelDescriptor: ChannelDescriptor = descriptor.outputs[ch.channel];
      if (channelDescriptor) {
        let property = channelDescriptor.property;

        if (!data.widget[property]) {
          data.widget[property] = new Subject<any>();
        }

        let observable = data.widget[property];
        AppService.subscribeSource(
          config,
          observable,
          bridge.relay,
          data.widgetId
        );

        // no return here. we may want to subscribe multiple instances of the same widget
      }

      return false;
    }

    // fast return if non-widget channel already has a channel-ref
    // the channel was already resolved
    if (config.refs) {
      return true;
    }

    let observable: Observable<any>;
    if (isObservable(channel)) {
      observable = channel;
    } else if (isSupplier(channel)) {
      observable = observableDefer(() => observableOf((<Supplier>channel)()));
    } else if (isGlobalChannel(channel)) {
      observable = this.global.getSource(channel);
    } else {
      throw new Error("unknown channel type");
    }

    if (observable) {
      AppService.subscribeSource(config, observable, bridge.relay);
      return true;
    }

    return false;
  }

  private resolveTarget(
    config: TargetChannelConfig,
    data: WidgetData,
    bridge: Bridge
  ): boolean {
    let channel = config.channel;

    if (isWidgetChannel(channel)) {
      let ch = <WidgetChannel>channel;
      if (ch.widget !== data.id) {
        return false;
      }

      let descriptor = data.descriptor;
      let channelDescriptor: ChannelDescriptor = descriptor.inputs[ch.channel];
      if (channelDescriptor) {
        let property = channelDescriptor.property;

        if (!data.widget[property]) {
          data.widget[property] = new Subject<any>();
        }

        let subject = data.widget[property];
        AppService.subscribeTarget(
          config,
          subject,
          bridge.relay,
          this._detachedObs,
          data.widgetId
        );

        // no return here. we may want to subscribe multiple instances of the same widget
      }

      return false;
    }

    // fast return if non-widget channel already has a channel-ref
    // the channel was already resolved
    if (config.refs) {
      return true;
    }

    let subject: Subject<any>;
    if (isSubject(channel)) {
      subject = channel;
    } else if (isConsumer(channel)) {
      subject = new Subject();
      subject.subscribe((data) => (<Consumer>channel)(data));
    } else if (isGlobalChannel(channel)) {
      subject = this.global.getTarget(channel);
    } else {
      throw new Error("unknown channel type");
    }

    if (subject) {
      AppService.subscribeTarget(
        config,
        subject,
        bridge.relay,
        this._detachedObs,
        null
      );
      return true;
    }

    return false;
  }

  public hasWidget(id: string): boolean {
    return this.widgetConfigs.has(id);
  }

  public registerWidget(widget: WidgetConfig) {
    this.widgetConfigs.set(widget.id, widget);
  }

  public toWidgetConfig(id: string): WidgetConfig {
    let result: WidgetConfig = this.widgetConfigs.get(id);
    if (result) {
      return result;
    }

    let widget: Widget = this.config.widgets[id];
    if (widget == undefined) {
      // console.debug(`Widget with id ${id} could not be found`);
      return undefined;
    }
    result = {
      id: id,
      component: widget.component,
      configuration: widget.configuration,
      hostClass: widget.hostClass,
      hostClassModifiers: widget.hostClassModifiers,
      elementClass: widget.elementClass,
      elementClassModifiers: widget.elementClassModifiers,
    };

    if (widget.links) {
      result._links = {};

      for (let link in widget.links) {
        result._links[link] = <Link>{
          href: widget.links[link],
        };
      }
    }

    if (widget.header) {
      result.header = this.toWidgetConfig(widget.header);
    }

    if (widget.actions) {
      result.actions = {
        alignment: widget.actions.alignment,
        elements: widget.actions.elements.map((entry) =>
          this.toWidgetConfig(entry)
        ),
      };
    }

    if (widget.footer) {
      result.footer = this.toWidgetConfig(widget.footer);
    }

    this.widgetConfigs.set(id, result);

    return result;
  }

  private initialize() {
    this.widgetConfigs = new Map<string, WidgetConfig>();
    this.channels = new Set<Bridge>();
    this.controllers = new Set<AppController>();
  }

  public reset() {
    this.disposeChannels();
    this.initialize();
  }

  private disposeChannels() {
    if (this.controllers) {
      this.controllers.forEach((controller) => controller.dispose());
    }

    if (this.channels) {
      this.channels.forEach((channel) => {
        channel.targets
          .filter((target) => !!target.refs)
          .map((target) => target.refs)
          .forEach((refs) => {
            refs = Array.isArray(refs) ? refs : [refs];
            refs
              .filter((ref) => !!ref.subscription)
              .forEach((ref) => ref.subscription.unsubscribe());
          });

        channel.sources
          .filter((source) => !!source.refs)
          .map((source) => source.refs)
          .forEach((refs) => {
            refs = Array.isArray(refs) ? refs : [refs];
            refs
              .filter((ref) => !!ref.subscription)
              .forEach((ref) => ref.subscription.unsubscribe());
          });
      });
    }
  }

  public getWidgetConfig(key: string) {
    return this.config.widgets[key];
  }

  public dispose() {
    this._activeContext.unsubscribe();
    this._initialized.unsubscribe();
    this._componentInitialized.unsubscribe();
    this.disposeChannels();
  }
}

export function loadController(module: NmModuleDescriptor) {
  return Observable.create((observer) => {
    let path = `${contextPath}/${module.identifier}/extensions/js/controller.js`;
    lload(path, (err) => {
      if (err) {
        console.warn(`controller.js not found here: ${path}`);
      } else {
        let loadedModule = nmLoader.registry["custom-controller"];
        let result = loadedModule["APP"];

        if (result && result.controllers) {
          let controllers: { [id: string]: AppControllerFactory } =
            result.controllers;
          for (let name in controllers) {
            ControllerRegistry.registerController(name, controllers[name]);
          }
        }
      }
      observer.complete();
    });
  }).toPromise();
}

results matching ""

    No results matching ""