### File

`src/lib/base.data-source.ts`

### Index

###### Properties

- Optional id
- Optional restore
- Optional viewChange

### Indexable

`[key: string]: `any``
Defined in src/lib/base.data-source.ts:49

### Properties

id
`id: `DataSourceViewerId``
Type : `DataSourceViewerId`
Optional

restore
`restore: `boolean``
Type : `boolean`
Optional
Indicates weather the data source should restore the last value from local storage

viewChange
`viewChange: `Observable<View>``
Type : `Observable<View>`
Optional

    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.warn(
            `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 ""