UNPKG

@rxap/data-source

Version:

Provides a set of classes and decorators for creating and managing data sources in Angular applications, including base classes, static data sources, observable data sources, and method data sources. It also includes a component for displaying data source

740 lines (627 loc) 25.4 kB
<!doctype html> <html class="no-js" lang=""> <head> <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <title>angular-data-source</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="../images/favicon.ico"> <link rel="stylesheet" href="../styles/style.css"> <link rel="stylesheet" href="../styles/dark.css"> </head> <body> <script> // Blocking script to avoid flickering dark mode // Dark mode toggle button var useDark = window.matchMedia('(prefers-color-scheme: dark)'); var darkModeState = useDark.matches; var $darkModeToggleSwitchers = document.querySelectorAll('.dark-mode-switch input'); var $darkModeToggles = document.querySelectorAll('.dark-mode-switch'); var darkModeStateLocal = localStorage.getItem('compodoc_darkmode-state'); function checkToggle(check) { for (var i = 0; i < $darkModeToggleSwitchers.length; i++) { $darkModeToggleSwitchers[i].checked = check; } } function toggleDarkMode(state) { if (window.localStorage) { localStorage.setItem('compodoc_darkmode-state', state); } checkToggle(state); const hasClass = document.body.classList.contains('dark'); if (state) { for (var i = 0; i < $darkModeToggles.length; i++) { $darkModeToggles[i].classList.add('dark'); } if (!hasClass) { document.body.classList.add('dark'); } } else { for (var i = 0; i < $darkModeToggles.length; i++) { $darkModeToggles[i].classList.remove('dark'); } if (hasClass) { document.body.classList.remove('dark'); } } } useDark.addEventListener('change', function (evt) { toggleDarkMode(evt.matches); }); if (darkModeStateLocal) { darkModeState = darkModeStateLocal === 'true'; } toggleDarkMode(darkModeState); </script> <div class="navbar navbar-default navbar-fixed-top d-md-none p-0"> <div class="d-flex"> <a href="../" class="navbar-brand">angular-data-source</a> <button type="button" class="btn btn-default btn-menu ion-ios-menu" id="btn-menu"></button> </div> </div> <div class="xs-menu menu" id="mobile-menu"> <div id="book-search-input" role="search"><input type="text" placeholder="Type to search"></div> <compodoc-menu></compodoc-menu> </div> <div class="container-fluid main"> <div class="row main"> <div class="d-none d-md-block menu"> <compodoc-menu mode="normal"></compodoc-menu> </div> <!-- START CONTENT --> <div class="content interface"> <div class="content-data"> <ol class="breadcrumb"> <li class="breadcrumb-item">Interfaces</li> <li class="breadcrumb-item" > BaseDataSourceViewer</li> </ol> <ul class="nav nav-tabs" role="tablist"> <li class="nav-item"> <a href="#info" class="nav-link" class="nav-link active" role="tab" id="info-tab" data-bs-toggle="tab" data-link="info">Info</a> </li> <li class="nav-item"> <a href="#source" class="nav-link" role="tab" id="source-tab" data-bs-toggle="tab" data-link="source">Source</a> </li> </ul> <div class="tab-content"> <div class="tab-pane fade active in" id="info"> <p class="comment"> <h3>File</h3> </p> <p class="comment"> <code>src/lib/base.data-source.ts</code> </p> <section data-compodoc="block-index"> <h3 id="index">Index</h3> <table class="table table-sm table-bordered index-table"> <tbody> <tr> <td class="col-md-4"> <h6><b>Properties</b></h6> </td> </tr> <tr> <td class="col-md-4"> <ul class="index-list"> <li> <span class="modifier">Optional</span> <a href="#id" > id </a> </li> <li> <span class="modifier">Optional</span> <a href="#restore" > restore </a> </li> <li> <span class="modifier">Optional</span> <a href="#viewChange" > viewChange </a> </li> </ul> </td> </tr> </tbody> </table> </section> <section data-compodoc="block-indexables"> <h3 id="inputs">Indexable</h3> <table class="table table-sm table-bordered"> <tbody> <tr> <td class="col-md-4"> <code>[key: string]: <code><a href="https://www.typescriptlang.org/docs/handbook/basic-types.html" target="_blank" >any</a></code> </code> </td> </tr> <tr> <td class="col-md-4"> <div class="io-line">Defined in <a href="" data-line="49" class="link-to-prism">src/lib/base.data-source.ts:49</a></div> </td> </tr> </tbody> </table> </section> <section data-compodoc="block-properties"> <h3 id="inputs">Properties</h3> <table class="table table-sm table-bordered"> <tbody> <tr> <td class="col-md-4"> <a name="id"></a> <span class="name "><b>id</b> <a href="#id"> <span class="icon ion-ios-link"></span> </a> </span> </td> </tr> <tr> <td class="col-md-4"> <code>id: <code><a href="../miscellaneous/typealiases.html#DataSourceViewerId" target="_self" >DataSourceViewerId</a></code> </code> </td> </tr> <tr> <td class="col-md-4"> <i>Type : </i> <code><a href="../miscellaneous/typealiases.html#DataSourceViewerId" target="_self" >DataSourceViewerId</a></code> </td> </tr> <tr> <td class="col-md-4"> <i>Optional</i> </td> </tr> </tbody> </table> <table class="table table-sm table-bordered"> <tbody> <tr> <td class="col-md-4"> <a name="restore"></a> <span class="name "><b>restore</b> <a href="#restore"> <span class="icon ion-ios-link"></span> </a> </span> </td> </tr> <tr> <td class="col-md-4"> <code>restore: <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/boolean" target="_blank" >boolean</a></code> </code> </td> </tr> <tr> <td class="col-md-4"> <i>Type : </i> <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/boolean" target="_blank" >boolean</a></code> </td> </tr> <tr> <td class="col-md-4"> <i>Optional</i> </td> </tr> <tr> <td class="col-md-4"> <div class="io-description"><p>Indicates weather the data source should restore the last value from local storage</p> </div> </td> </tr> </tbody> </table> <table class="table table-sm table-bordered"> <tbody> <tr> <td class="col-md-4"> <a name="viewChange"></a> <span class="name "><b>viewChange</b> <a href="#viewChange"> <span class="icon ion-ios-link"></span> </a> </span> </td> </tr> <tr> <td class="col-md-4"> <code>viewChange: <code>Observable&lt;View&gt;</code> </code> </td> </tr> <tr> <td class="col-md-4"> <i>Type : </i> <code>Observable&lt;View&gt;</code> </td> </tr> <tr> <td class="col-md-4"> <i>Optional</i> </td> </tr> </tbody> </table> </section> </div> <div class="tab-pane fade tab-source-code" id="source"> <pre class="line-numbers compodoc-sourcecode"><code class="language-typescript">import { Inject, Injectable, isDevMode, Optional, Signal, } from &#x27;@angular/core&#x27;; import { toSignal } from &#x27;@angular/core/rxjs-interop&#x27;; import { BaseDefinition, BaseDefinitionMetadata, DefinitionMetadata, RXAP_DEFINITION_METADATA, } from &#x27;@rxap/definition&#x27;; import { CloneObservable, ToggleSubject, } from &#x27;@rxap/rxjs&#x27;; import { clone, Constructor, deepMerge, GenerateRandomString, } from &#x27;@rxap/utilities&#x27;; import { EMPTY, firstValueFrom, Observable, ReplaySubject, Subject, TeardownLogic, } from &#x27;rxjs&#x27;; import { finalize, startWith, take, takeUntil, tap, } from &#x27;rxjs/operators&#x27;; export type DataSourceViewerId &#x3D; string; export interface BaseDataSourceViewer&lt;View &#x3D; any&gt; { id?: DataSourceViewerId; viewChange?: Observable&lt;View&gt;; /** * 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&lt; Data &#x3D; any, Metadata extends BaseDataSourceMetadata &#x3D; BaseDataSourceMetadata, Viewer extends BaseDataSourceViewer &#x3D; BaseDataSourceViewer &gt; extends BaseDefinition&lt;Metadata&gt; { public readonly change$ &#x3D; new Subject&lt;Data&gt;(); /** * Indicates weather the data source is currently loading new data */ public loading$: Observable&lt;boolean&gt; &#x3D; EMPTY; public readonly hasError$ &#x3D; new ToggleSubject(); public readonly error$ &#x3D; new ReplaySubject&lt;Error&gt;(1); public get lastRefreshed(): Date | null { return this._lastRefreshed; } protected _lastRefreshed: Date | null &#x3D; null; protected _connectedViewer &#x3D; new Map&lt;DataSourceViewerId, Observable&lt;Data&gt;&gt;(); protected _connectedViewerTeardown &#x3D; new Map&lt; DataSourceViewerId, TeardownLogic &gt;(); protected _data$: Observable&lt;Data&gt; &#x3D; EMPTY; /** * a map of viewer to view id. * Allows to create a view id from the viewer object reference * @protected */ protected _viewerIds &#x3D; new Map&lt;Viewer, string&gt;(); protected _retry$ &#x3D; new Subject&lt;void&gt;(); public loading?: Signal&lt;boolean&gt;; public hasError?: Signal&lt;boolean&gt;; constructor( @Optional() @Inject(RXAP_DEFINITION_METADATA) metadata: Metadata | null &#x3D; null, ) { super(metadata); } protected _data?: Data; public get data() { return clone(this._data); } public get hasConnections(): boolean { return this._connectedViewer.size &gt; 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&lt;Data&gt; { let viewer: Viewer; if (typeof viewerOrString &#x3D;&#x3D;&#x3D; &#x27;string&#x27;) { viewer &#x3D; { id: viewerOrString } as Viewer; } else { viewer &#x3D; viewerOrString; } if (!viewer.id) { viewer.id &#x3D; this.getViewerId(viewer); } if (this.isConnected(viewer)) { return this._connectedViewer.get(viewer.id)!; } if (!viewer.viewChange) { viewer.viewChange &#x3D; EMPTY; } const _connection &#x3D; this._connect(viewer); let connection: Observable&lt;Data&gt;; let teardownLogic: TeardownLogic | null &#x3D; null; if (Array.isArray(_connection)) { if (_connection.length !&#x3D;&#x3D; 2) { throw new Error( &#x27;if this._connect returns an array. The array should have two items&#x27;, ); } connection &#x3D; _connection[0]; teardownLogic &#x3D; _connection[1]; } else { connection &#x3D; _connection; } const destroy$ &#x3D; new Subject&lt;void&gt;(); const cacheKey &#x3D; [ &#x27;rxap&#x27;, &#x27;data-source&#x27;, this.constructor.name, this.id ].join(&#x27;_&#x27;); const restore &#x3D; viewer.restore ?? this.metadata.restore ?? false; connection &#x3D; connection.pipe( tap((data) &#x3D;&gt; { this._data &#x3D; data; if (restore) { try { const cache &#x3D; JSON.stringify(data); localStorage.setItem(cacheKey, cache); } catch (e: any) { console.warn( &#x60;Failed to store data source &#x27;${ this.id }&#x27; data in local storage&#x60;, ); } } }), tap((data) &#x3D;&gt; this.change$.next(data)), tap(() &#x3D;&gt; this._lastRefreshed &#x3D; new Date()), finalize(() &#x3D;&gt; this.disconnect(viewer)), takeUntil(destroy$), ); if (restore) { const data &#x3D; localStorage.getItem(cacheKey); if (data) { this._data &#x3D; JSON.parse(data) as Data; connection &#x3D; connection.pipe(startWith(this._data)); } } this._connectedViewer.set(viewer.id, connection); if (teardownLogic) { const tl &#x3D; teardownLogic; teardownLogic &#x3D; () &#x3D;&gt; { destroy$.next(); if (typeof tl &#x3D;&#x3D;&#x3D; &#x27;function&#x27;) { tl(); } else { tl.unsubscribe(); } }; } else { teardownLogic &#x3D; () &#x3D;&gt; { destroy$.next(); }; } this._connectedViewerTeardown.set(viewer.id, teardownLogic); return connection; } public attach(viewerId: DataSourceViewerId): Observable&lt;Data&gt; { if (this.isConnected(viewerId)) { return CloneObservable(this._connectedViewer.get(viewerId)!); } throw new Error(&#x60;No active connection with viewer id &#x27;${ viewerId }&#x27; found&#x60;); } public isConnected(viewerOrId: Viewer | DataSourceViewerId): boolean { const viewerId &#x3D; typeof viewerOrId &#x3D;&#x3D;&#x3D; &#x27;string&#x27; ? viewerOrId : viewerOrId.id ?? this.getViewerId(viewerOrId); return this._connectedViewer.has(viewerId); } public disconnect(viewerOrId: Viewer | DataSourceViewerId) { const viewerId &#x3D; typeof viewerOrId &#x3D;&#x3D;&#x3D; &#x27;string&#x27; ? viewerOrId : viewerOrId.id ?? this.getViewerId(viewerOrId); if (this.isConnected(viewerId)) { this._disconnect(viewerId); this._connectedViewer.delete(viewerId); if (this._connectedViewerTeardown.has(viewerId)) { const teardownLogic &#x3D; this._connectedViewerTeardown.get(viewerId)!; if (teardownLogic) { if (typeof teardownLogic &#x3D;&#x3D;&#x3D; &#x27;function&#x27;) { teardownLogic(); } else { teardownLogic.unsubscribe(); } } } } else { console.debug( &#x60;Connection with viewer id &#x27;${ viewerId }&#x27; is not connected to the data source &#x27;${ this.id }&#x27;&#x60;, ); } // TODO : find better cleanup solution if (typeof viewerOrId &#x3D;&#x3D;&#x3D; &#x27;string&#x27;) { for (const [ viewer, id ] of this._viewerIds.entries()) { if (viewerOrId &#x3D;&#x3D;&#x3D; 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&lt;Data&gt; { const result &#x3D; await firstValueFrom(this.connect(viewer).pipe(take(1))); this.disconnect(viewer); return result; } public derive( id: string, metadata: Partial&lt;BaseDataSourceMetadata&gt; &#x3D; this.metadata, ): BaseDataSource&lt;any&gt; { return new BaseDataSource&lt;any&gt;({ ...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(&#x27;context&#x27;)) { delete this.metadata[&#x27;context&#x27;]; } } // eslint-disable-next-line @typescript-eslint/no-empty-function protected _disconnect(viewerId: DataSourceViewerId): void { } protected genericRetryFunction(error: any, retryCount: number): Observable&lt;any&gt; { return this._retry$.asObservable().pipe( tap(() &#x3D;&gt; this.hasError$.disable()), ); } protected handelError(error: any) { if (isDevMode()) { console.log(&#x60;DataSource &#x27;${ this.id }&#x27; has an error:&#x60;, error); } } protected _connect( viewer: BaseDataSourceViewer, ): [ Observable&lt;Data&gt;, TeardownLogic ] | Observable&lt;Data&gt; { this.init(); return this._data$; } } export function RxapDataSource&lt;Metadata extends BaseDataSourceMetadata&gt;( dataSourceIdOrMetadata: string | Metadata, className &#x3D; &#x27;BaseDataSource&#x27;, packageName &#x3D; &#x27;@rxap/data-source&#x27;, ) { return function (target: Constructor&lt;BaseDataSource&gt;) { DefinitionMetadata(dataSourceIdOrMetadata, className, packageName)(target); }; } </code></pre> </div> </div> </div><div class="search-results"> <div class="has-results"> <h1 class="search-results-title"><span class='search-results-count'></span> results matching "<span class='search-query'></span>"</h1> <ul class="search-results-list"></ul> </div> <div class="no-results"> <h1 class="search-results-title">No results matching "<span class='search-query'></span>"</h1> </div> </div> </div> <!-- END CONTENT --> </div> </div> <label class="dark-mode-switch"> <input type="checkbox"> <span class="slider"> <svg class="slider-icon" viewBox="0 0 24 24" fill="none" height="20" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" width="20" xmlns="http://www.w3.org/2000/svg"> <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"></path> </svg> </span> </label> <script> var COMPODOC_CURRENT_PAGE_DEPTH = 1; var COMPODOC_CURRENT_PAGE_CONTEXT = 'interface'; var COMPODOC_CURRENT_PAGE_URL = 'BaseDataSourceViewer.html'; var MAX_SEARCH_RESULTS = 15; </script> <script> $darkModeToggleSwitchers = document.querySelectorAll('.dark-mode-switch input'); checkToggle(darkModeState); if ($darkModeToggleSwitchers.length > 0) { for (var i = 0; i < $darkModeToggleSwitchers.length; i++) { $darkModeToggleSwitchers[i].addEventListener('change', function (event) { darkModeState = !darkModeState; toggleDarkMode(darkModeState); }); } } </script> <script src="../js/libs/custom-elements.min.js"></script> <script src="../js/libs/lit-html.js"></script> <script src="../js/menu-wc.js" defer></script> <script nomodule src="../js/menu-wc_es5.js" defer></script> <script src="../js/libs/bootstrap-native.js"></script> <script src="../js/libs/es6-shim.min.js"></script> <script src="../js/libs/EventDispatcher.js"></script> <script src="../js/libs/promise.min.js"></script> <script src="../js/libs/zepto.min.js"></script> <script src="../js/compodoc.js"></script> <script src="../js/tabs.js"></script> <script src="../js/menu.js"></script> <script src="../js/libs/clipboard.min.js"></script> <script src="../js/libs/prism.js"></script> <script src="../js/sourceCode.js"></script> <script src="../js/search/search.js"></script> <script src="../js/search/lunr.min.js"></script> <script src="../js/search/search-lunr.js"></script> <script src="../js/search/search_index.js"></script> <script src="../js/lazy-load-graphs.js"></script> </body> </html>