@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
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<View></code>
</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<i>Type : </i> <code>Observable<View></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 '@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);
};
}
</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>