File

src/lib/base.data-source.ts

Extends

BaseDefinition

Index

Properties
Methods
Accessors

Constructor

constructor(metadata: Metadata | null)
Parameters :
Name Type Optional
metadata Metadata | null No

Methods

Protected _connect
_connect(viewer: BaseDataSourceViewer)
Parameters :
Name Type Optional
viewer BaseDataSourceViewer No
Returns : | Observable
Protected _disconnect
_disconnect(viewerId: DataSourceViewerId)
Parameters :
Name Type Optional
viewerId DataSourceViewerId No
Returns : void
Public attach
attach(viewerId: DataSourceViewerId)
Parameters :
Name Type Optional
viewerId DataSourceViewerId No
Returns : Observable<Data>
Public connect
connect(viewerOrString: Viewer | DataSourceViewerId)
Parameters :
Name Type Optional
viewerOrString Viewer | DataSourceViewerId No
Returns : Observable<Data>
Public derive
derive(id: string, metadata: Partial<BaseDataSourceMetadata>)
Parameters :
Name Type Optional Default value
id string No
metadata Partial<BaseDataSourceMetadata> No this.metadata
Public disconnect
disconnect(viewerOrId: Viewer | DataSourceViewerId)
Parameters :
Name Type Optional
viewerOrId Viewer | DataSourceViewerId No
Returns : void
Protected genericRetryFunction
genericRetryFunction(error: any, retryCount: number)
Parameters :
Name Type Optional
error any No
retryCount number No
Returns : Observable<any>
Public getViewerId
getViewerId(viewer: Viewer)
Parameters :
Name Type Optional
viewer Viewer No
Returns : string
Protected handelError
handelError(error: any)
Parameters :
Name Type Optional
error any No
Returns : void
Public isConnected
isConnected(viewerOrId: Viewer | DataSourceViewerId)
Parameters :
Name Type Optional
viewerOrId Viewer | DataSourceViewerId No
Returns : boolean
Public refresh
refresh()
Returns : any
Public reset
reset()
Returns : any
Public retry
retry()
Returns : any
Public toJSON
toJSON()
Returns : object
Public Async toPromise
toPromise(viewer: Viewer)

Creates a connection to tha data source and converts the Observable into a promise and then disconnects the viewer

Parameters :
Name Type Optional
viewer Viewer No
Returns : Promise<Data>

Properties

Protected _connectedViewer
Default value : new Map<DataSourceViewerId, Observable<Data>>()
Protected _connectedViewerTeardown
Default value : new Map< DataSourceViewerId, TeardownLogic >()
Protected Optional _data
Type : Data
Protected _data$
Type : Observable<Data>
Default value : EMPTY
Protected _lastRefreshed
Type : Date | null
Default value : null
Protected _retry$
Default value : new Subject<void>()
Protected _viewerIds
Default value : new Map<Viewer, string>()

a map of viewer to view id. Allows to create a view id from the viewer object reference

Public Readonly change$
Default value : new Subject<Data>()
Public Readonly error$
Default value : new ReplaySubject<Error>(1)
Public Optional hasError
Type : Signal<boolean>
Public Readonly hasError$
Default value : new ToggleSubject()
Public Optional loading
Type : Signal<boolean>
Public loading$
Type : Observable<boolean>
Default value : EMPTY

Indicates weather the data source is currently loading new data

Accessors

lastRefreshed
getlastRefreshed()
data
getdata()
hasConnections
gethasConnections()
import {
  Inject,
  Injectable,
  isDevMode,
  Optional,
  Signal,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import {
  BaseDefinition,
  BaseDefinitionMetadata,
  DefinitionMetadata,
  RXAP_DEFINITION_METADATA,
} from '@rxap/definition';
import {
  CloneObservable,
  ToggleSubject,
} from '@rxap/rxjs';
import {
  clone,
  Constructor,
  deepMerge,
  GenerateRandomString,
} from '@rxap/utilities';
import {
  EMPTY,
  firstValueFrom,
  Observable,
  ReplaySubject,
  Subject,
  TeardownLogic,
} from 'rxjs';
import {
  finalize,
  startWith,
  take,
  takeUntil,
  tap,
} from 'rxjs/operators';

export type DataSourceViewerId = string;

export interface BaseDataSourceViewer<View = any> {
  id?: DataSourceViewerId;
  viewChange?: Observable<View>;
  /**
   * Indicates weather the data source should restore the last value from local storage
   */
  restore?: boolean;

  [key: string]: any;
}

export interface BaseDataSourceMetadata extends BaseDefinitionMetadata {
  /**
   * Indicates weather the data source should restore the last value from local storage
   */
  restore?: boolean;
}

@Injectable()
export class BaseDataSource<
  Data = any,
  Metadata extends BaseDataSourceMetadata = BaseDataSourceMetadata,
  Viewer extends BaseDataSourceViewer = BaseDataSourceViewer
> extends BaseDefinition<Metadata> {
  public readonly change$ = new Subject<Data>();
  /**
   * Indicates weather the data source is currently loading new data
   */
  public loading$: Observable<boolean> = EMPTY;
  public readonly hasError$ = new ToggleSubject();
  public readonly error$ = new ReplaySubject<Error>(1);
  public get lastRefreshed(): Date | null {
    return this._lastRefreshed;
  }
  protected _lastRefreshed: Date | null = null;
  protected _connectedViewer = new Map<DataSourceViewerId, Observable<Data>>();
  protected _connectedViewerTeardown = new Map<
    DataSourceViewerId,
    TeardownLogic
  >();
  protected _data$: Observable<Data> = EMPTY;
  /**
   * a map of viewer to view id.
   * Allows to create a view id from the viewer object reference
   * @protected
   */
  protected _viewerIds = new Map<Viewer, string>();
  protected _retry$ = new Subject<void>();

  public loading?: Signal<boolean>;

  public hasError?: Signal<boolean>;

  constructor(
    @Optional()
    @Inject(RXAP_DEFINITION_METADATA)
      metadata: Metadata | null = null,
  ) {
    super(metadata);
  }

  protected _data?: Data;

  public get data() {
    return clone(this._data);
  }

  public get hasConnections(): boolean {
    return this._connectedViewer.size > 0;
  }

  public getViewerId(viewer: Viewer): string {
    if (viewer.id) {
      return viewer.id;
    }
    if (!this._viewerIds.has(viewer)) {
      this._viewerIds.set(viewer, GenerateRandomString());
    }
    return this._viewerIds.get(viewer)!;
  }

  public connect(viewerOrString: Viewer | DataSourceViewerId): Observable<Data> {
    let viewer: Viewer;
    if (typeof viewerOrString === 'string') {
      viewer = { id: viewerOrString } as Viewer;
    } else {
      viewer = viewerOrString;
    }
    if (!viewer.id) {
      viewer.id = this.getViewerId(viewer);
    }
    if (this.isConnected(viewer)) {
      return this._connectedViewer.get(viewer.id)!;
    }
    if (!viewer.viewChange) {
      viewer.viewChange = EMPTY;
    }
    const _connection = this._connect(viewer);
    let connection: Observable<Data>;
    let teardownLogic: TeardownLogic | null = null;
    if (Array.isArray(_connection)) {
      if (_connection.length !== 2) {
        throw new Error(
          'if this._connect returns an array. The array should have two items',
        );
      }
      connection = _connection[0];
      teardownLogic = _connection[1];
    } else {
      connection = _connection;
    }

    const destroy$ = new Subject<void>();

    const cacheKey = [ 'rxap', 'data-source', this.constructor.name, this.id ].join('_');
    const restore = viewer.restore ?? this.metadata.restore ?? false;

    connection = connection.pipe(
      tap((data) => {
        this._data = data;
        if (restore) {
          try {
            const cache = JSON.stringify(data);
            localStorage.setItem(cacheKey, cache);
          } catch (e: any) {
            console.warn(
              `Failed to store data source '${ this.id }' data in local storage`,
            );
          }
        }
      }),
      tap((data) => this.change$.next(data)),
      tap(() => this._lastRefreshed = new Date()),
      finalize(() => this.disconnect(viewer)),
      takeUntil(destroy$),
    );

    if (restore) {
      const data = localStorage.getItem(cacheKey);
      if (data) {
        this._data = JSON.parse(data) as Data;
        connection = connection.pipe(startWith(this._data));
      }
    }

    this._connectedViewer.set(viewer.id, connection);

    if (teardownLogic) {
      const tl = teardownLogic;
      teardownLogic = () => {
        destroy$.next();
        if (typeof tl === 'function') {
          tl();
        } else {
          tl.unsubscribe();
        }
      };
    } else {
      teardownLogic = () => {
        destroy$.next();
      };
    }

    this._connectedViewerTeardown.set(viewer.id, teardownLogic);
    return connection;
  }

  public attach(viewerId: DataSourceViewerId): Observable<Data> {
    if (this.isConnected(viewerId)) {
      return CloneObservable(this._connectedViewer.get(viewerId)!);
    }
    throw new Error(`No active connection with viewer id '${ viewerId }' found`);
  }

  public isConnected(viewerOrId: Viewer | DataSourceViewerId): boolean {
    const viewerId =
      typeof viewerOrId === 'string'
        ? viewerOrId
        : viewerOrId.id ?? this.getViewerId(viewerOrId);
    return this._connectedViewer.has(viewerId);
  }

  public disconnect(viewerOrId: Viewer | DataSourceViewerId) {
    const viewerId =
      typeof viewerOrId === 'string'
        ? viewerOrId
        : viewerOrId.id ?? this.getViewerId(viewerOrId);
    if (this.isConnected(viewerId)) {
      this._disconnect(viewerId);
      this._connectedViewer.delete(viewerId);
      if (this._connectedViewerTeardown.has(viewerId)) {
        const teardownLogic = this._connectedViewerTeardown.get(viewerId)!;
        if (teardownLogic) {
          if (typeof teardownLogic === 'function') {
            teardownLogic();
          } else {
            teardownLogic.unsubscribe();
          }
        }
      }
    } else {
      console.debug(
        `Connection with viewer id '${ viewerId }' is not connected to the data source '${ this.id }'`,
      );
    }
    // TODO : find better cleanup solution
    if (typeof viewerOrId === 'string') {
      for (const [ viewer, id ] of this._viewerIds.entries()) {
        if (viewerOrId === id) {
          this._viewerIds.delete(viewer);
        }
      }
    } else {
      this._viewerIds.delete(viewerOrId);
    }
  }

  /**
   * Creates a connection to tha data source and converts the Observable into a
   * promise and then disconnects the viewer
   */
  public async toPromise(viewer: Viewer): Promise<Data> {
    const result = await firstValueFrom(this.connect(viewer).pipe(take(1)));
    this.disconnect(viewer);
    return result;
  }

  public derive(
    id: string,
    metadata: Partial<BaseDataSourceMetadata> = this.metadata,
  ): BaseDataSource<any> {
    return new BaseDataSource<any>({
      ...deepMerge(this.metadata, metadata),
      id,
    });
  }

  public toJSON(): object {
    return {
      id: this.id,
      metadata: this.metadata,
      connected: this._connectedViewer.keys(),
      data: this._data,
    };
  }

  // eslint-disable-next-line @typescript-eslint/no-empty-function
  public refresh(): any {
  }

  public retry(): any {
    this._retry$.next();
  }

  public reset(): any {
    for (const viewerId of this._connectedViewer.keys()) {
      this.disconnect(viewerId);
    }
    // eslint-disable-next-line no-prototype-builtins
    if (this.metadata.hasOwnProperty('context')) {
      delete this.metadata['context'];
    }
  }

  // eslint-disable-next-line @typescript-eslint/no-empty-function
  protected _disconnect(viewerId: DataSourceViewerId): void {
  }

  protected genericRetryFunction(error: any, retryCount: number): Observable<any> {
    return this._retry$.asObservable().pipe(
      tap(() => this.hasError$.disable()),
    );
  }

  protected handelError(error: any) {
    if (isDevMode()) {
      console.log(`DataSource '${ this.id }' has an error:`, error);
    }
  }

  protected _connect(
    viewer: BaseDataSourceViewer,
  ): [ Observable<Data>, TeardownLogic ] | Observable<Data> {
    this.init();
    return this._data$;
  }
}

export function RxapDataSource<Metadata extends BaseDataSourceMetadata>(
  dataSourceIdOrMetadata: string | Metadata,
  className = 'BaseDataSource',
  packageName = '@rxap/data-source',
) {
  return function (target: Constructor<BaseDataSource>) {
    DefinitionMetadata(dataSourceIdOrMetadata, className, packageName)(target);
  };
}

results matching ""

    No results matching ""