src/lib/base.data-source.ts
BaseDefinition
Properties |
|
Methods |
|
Accessors |
constructor(metadata: Metadata | null)
|
||||||
|
Defined in src/lib/base.data-source.ts:94
|
||||||
|
Parameters :
|
| Protected _connect | ||||||
_connect(viewer: BaseDataSourceViewer)
|
||||||
|
Defined in src/lib/base.data-source.ts:323
|
||||||
|
Parameters :
Returns :
| Observable
|
| Protected _disconnect | ||||||
_disconnect(viewerId: DataSourceViewerId)
|
||||||
|
Defined in src/lib/base.data-source.ts:308
|
||||||
|
Parameters :
Returns :
void
|
| Public attach | ||||||
attach(viewerId: DataSourceViewerId)
|
||||||
|
Defined in src/lib/base.data-source.ts:210
|
||||||
|
Parameters :
Returns :
Observable<Data>
|
| Public connect | ||||||
connect(viewerOrString: Viewer | DataSourceViewerId)
|
||||||
|
Defined in src/lib/base.data-source.ts:124
|
||||||
|
Parameters :
Returns :
Observable<Data>
|
| Public derive | ||||||||||||
derive(id: string, metadata: Partial<BaseDataSourceMetadata>)
|
||||||||||||
|
Defined in src/lib/base.data-source.ts:270
|
||||||||||||
|
Parameters :
Returns :
BaseDataSource<any>
|
| Public disconnect | ||||||
disconnect(viewerOrId: Viewer | DataSourceViewerId)
|
||||||
|
Defined in src/lib/base.data-source.ts:225
|
||||||
|
Parameters :
Returns :
void
|
| Protected genericRetryFunction |
genericRetryFunction(error: any, retryCount: number)
|
|
Defined in src/lib/base.data-source.ts:311
|
|
Returns :
Observable<any>
|
| Public getViewerId | ||||||
getViewerId(viewer: Viewer)
|
||||||
|
Defined in src/lib/base.data-source.ts:114
|
||||||
|
Parameters :
Returns :
string
|
| Protected handelError | ||||||
handelError(error: any)
|
||||||
|
Defined in src/lib/base.data-source.ts:317
|
||||||
|
Parameters :
Returns :
void
|
| Public isConnected | ||||||
isConnected(viewerOrId: Viewer | DataSourceViewerId)
|
||||||
|
Defined in src/lib/base.data-source.ts:217
|
||||||
|
Parameters :
Returns :
boolean
|
| Public refresh |
refresh()
|
|
Defined in src/lib/base.data-source.ts:290
|
|
Returns :
any
|
| Public reset |
reset()
|
|
Defined in src/lib/base.data-source.ts:297
|
|
Returns :
any
|
| Public retry |
retry()
|
|
Defined in src/lib/base.data-source.ts:293
|
|
Returns :
any
|
| Public toJSON |
toJSON()
|
|
Defined in src/lib/base.data-source.ts:280
|
|
Returns :
object
|
| Public Async toPromise | ||||||
toPromise(viewer: Viewer)
|
||||||
|
Defined in src/lib/base.data-source.ts:264
|
||||||
|
Creates a connection to tha data source and converts the Observable into a promise and then disconnects the viewer
Parameters :
Returns :
Promise<Data>
|
| Protected _connectedViewer |
Default value : new Map<DataSourceViewerId, Observable<Data>>()
|
|
Defined in src/lib/base.data-source.ts:78
|
| Protected _connectedViewerTeardown |
Default value : new Map<
DataSourceViewerId,
TeardownLogic
>()
|
|
Defined in src/lib/base.data-source.ts:79
|
| Protected Optional _data |
Type : Data
|
|
Defined in src/lib/base.data-source.ts:104
|
| Protected _data$ |
Type : Observable<Data>
|
Default value : EMPTY
|
|
Defined in src/lib/base.data-source.ts:83
|
| Protected _lastRefreshed |
Type : Date | null
|
Default value : null
|
|
Defined in src/lib/base.data-source.ts:77
|
| Protected _retry$ |
Default value : new Subject<void>()
|
|
Defined in src/lib/base.data-source.ts:90
|
| Protected _viewerIds |
Default value : new Map<Viewer, string>()
|
|
Defined in src/lib/base.data-source.ts:89
|
|
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>()
|
|
Defined in src/lib/base.data-source.ts:67
|
| Public Readonly error$ |
Default value : new ReplaySubject<Error>(1)
|
|
Defined in src/lib/base.data-source.ts:73
|
| Public Optional hasError |
Type : Signal<boolean>
|
|
Defined in src/lib/base.data-source.ts:94
|
| Public Readonly hasError$ |
Default value : new ToggleSubject()
|
|
Defined in src/lib/base.data-source.ts:72
|
| Public Optional loading |
Type : Signal<boolean>
|
|
Defined in src/lib/base.data-source.ts:92
|
| Public loading$ |
Type : Observable<boolean>
|
Default value : EMPTY
|
|
Defined in src/lib/base.data-source.ts:71
|
|
Indicates weather the data source is currently loading new data |
| lastRefreshed |
getlastRefreshed()
|
|
Defined in src/lib/base.data-source.ts:74
|
| data |
getdata()
|
|
Defined in src/lib/base.data-source.ts:106
|
| hasConnections |
gethasConnections()
|
|
Defined in src/lib/base.data-source.ts:110
|
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);
};
}