http-request-manager 18.7.19 → 18.7.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +483 -0
- package/DATABASE_README.md +1176 -0
- package/HTTP_MANAGER_README.md +579 -0
- package/HTTP_SINGNALS_MANAGER_README.md +654 -0
- package/HTTP_STATE_MANAGER_README.md +948 -0
- package/INTERCEPTOR_README.md +549 -0
- package/LOCAL_STORAGE_README.md +1056 -0
- package/STORE_STATE_MANAGER_README.md +1322 -0
- package/UTILS_README.md +1186 -0
- package/WS_MANAGER_README.md +613 -0
- package/ng-package.json +8 -0
- package/package.json +1 -12
- package/src/lib/http-request-manager.module.ts +132 -0
- package/src/lib/http-request-services-demo/database-data-demo/database-data-demo.component.html +65 -0
- package/src/lib/http-request-services-demo/database-data-demo/database-data-demo.component.scss +0 -0
- package/src/lib/http-request-services-demo/database-data-demo/database-data-demo.component.ts +224 -0
- package/src/lib/http-request-services-demo/http-request-services-demo.component.html +114 -0
- package/src/lib/http-request-services-demo/http-request-services-demo.component.scss +6 -0
- package/src/lib/http-request-services-demo/http-request-services-demo.component.ts +52 -0
- package/src/lib/http-request-services-demo/local-storage-demo/local-storage-demo.component.html +195 -0
- package/src/lib/http-request-services-demo/local-storage-demo/local-storage-demo.component.scss +17 -0
- package/src/lib/http-request-services-demo/local-storage-demo/local-storage-demo.component.ts +206 -0
- package/src/lib/http-request-services-demo/local-storage-signals-demo/local-storage-signals-demo.component.html +200 -0
- package/src/lib/http-request-services-demo/local-storage-signals-demo/local-storage-signals-demo.component.scss +17 -0
- package/src/lib/http-request-services-demo/local-storage-signals-demo/local-storage-signals-demo.component.ts +212 -0
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/download-file/download-file.component.html +53 -0
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/download-file/download-file.component.scss +60 -0
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/download-file/download-file.component.ts +72 -0
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-download.module.ts +28 -0
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-downloader.component.html +10 -0
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-downloader.component.scss +29 -0
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-downloader.component.ts +100 -0
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/models/download-labels-model.ts +22 -0
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/spinner/spinner.component.html +8 -0
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/spinner/spinner.component.scss +19 -0
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/spinner/spinner.component.ts +26 -0
- package/src/lib/http-request-services-demo/request-manager-demo/models/app-session.model.ts +30 -0
- package/src/lib/http-request-services-demo/request-manager-demo/models/app.model.ts +19 -0
- package/src/lib/http-request-services-demo/request-manager-demo/models/get-sample.model.ts +25 -0
- package/src/lib/http-request-services-demo/request-manager-demo/models/sample-ai-prompt.ts +19 -0
- package/src/lib/http-request-services-demo/request-manager-demo/models/sample-client-details.ts +24 -0
- package/src/lib/http-request-services-demo/request-manager-demo/models/sample-client-info.ts +30 -0
- package/src/lib/http-request-services-demo/request-manager-demo/models/sample-client.model.ts +49 -0
- package/src/lib/http-request-services-demo/request-manager-demo/models/sample-mapper-client-info.ts +33 -0
- package/src/lib/http-request-services-demo/request-manager-demo/request-manager-demo.component.html +392 -0
- package/src/lib/http-request-services-demo/request-manager-demo/request-manager-demo.component.scss +24 -0
- package/src/lib/http-request-services-demo/request-manager-demo/request-manager-demo.component.ts +461 -0
- package/src/lib/http-request-services-demo/request-manager-state-demo/request-manager-state-demo.component.html +393 -0
- package/src/lib/http-request-services-demo/request-manager-state-demo/request-manager-state-demo.component.scss +24 -0
- package/src/lib/http-request-services-demo/request-manager-state-demo/request-manager-state-demo.component.ts +421 -0
- package/src/lib/http-request-services-demo/request-manager-state-demo/services/state-manager-demo.service.ts +87 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/services/state-data-request.service.ts +120 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-ai-messaging/ws-ai-messaging.component.css +0 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-ai-messaging/ws-ai-messaging.component.html +3 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-ai-messaging/ws-ai-messaging.component.ts +16 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-chats/ws-chats.component.css +0 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-chats/ws-chats.component.html +3 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-chats/ws-chats.component.ts +16 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.css +31 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.html +72 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.scss +41 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.spec.ts +205 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.ts +77 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.css +11 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.html +96 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.spec.ts +31 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.ts +229 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.css +30 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.html +172 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.spec.ts +31 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.ts +239 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/models/oidc-client.model.ts +31 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/models/user-data.model.ts +32 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/request-manager-ws-demo.component.css +0 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/request-manager-ws-demo.component.html +84 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/request-manager-ws-demo.component.ts +41 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/services/index.ts +3 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/services/message-service-demo.service.ts +83 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/services/notification-service-demo.service.ts +147 -0
- package/src/lib/http-request-services-demo/request-manager-ws-demo/services/state-service-demo.service.ts +158 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/download-file/download-file.component.html +53 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/download-file/download-file.component.scss +60 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/download-file/download-file.component.ts +72 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-download.module.ts +28 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-downloader.component.html +10 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-downloader.component.scss +29 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-downloader.component.ts +100 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/models/download-labels-model.ts +22 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/spinner/spinner.component.html +8 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/spinner/spinner.component.scss +19 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/spinner/spinner.component.ts +26 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/app-session.model.ts +30 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/app.model.ts +19 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/get-sample.model.ts +25 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-ai-prompt.ts +19 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-client-details.ts +24 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-client-info.ts +30 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-client.model.ts +49 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-mapper-client-info.ts +33 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/request-signals-manager-demo.component.html +380 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/request-signals-manager-demo.component.scss +24 -0
- package/src/lib/http-request-services-demo/request-signals-manager-demo/request-signals-manager-demo.component.ts +410 -0
- package/src/lib/http-request-services-demo/store-state-manager-demo/models/settings.model.ts +28 -0
- package/src/lib/http-request-services-demo/store-state-manager-demo/services/settings-state.service.ts +48 -0
- package/src/lib/http-request-services-demo/store-state-manager-demo/store-state-manager-demo.component.css +0 -0
- package/src/lib/http-request-services-demo/store-state-manager-demo/store-state-manager-demo.component.html +23 -0
- package/src/lib/http-request-services-demo/store-state-manager-demo/store-state-manager-demo.component.ts +36 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/interceptors/credentials.interceptor.ts +16 -0
- package/src/lib/interceptors/index.ts +6 -0
- package/src/lib/interceptors/models/error-settings.model.ts +22 -0
- package/src/lib/interceptors/models/index.ts +2 -0
- package/src/lib/interceptors/proxy-debugger.interceptor.ts +46 -0
- package/src/lib/interceptors/request-error.interceptor.ts +65 -0
- package/src/lib/interceptors/request-header.interceptor.ts +53 -0
- package/src/lib/models/config-http-options.model.ts +42 -0
- package/src/lib/models/config-local-storage-options.model.ts +27 -0
- package/src/lib/models/config-options.model.ts +27 -0
- package/src/lib/models/config-token.model.ts +9 -0
- package/src/lib/models/data-type.enum.ts +5 -0
- package/src/lib/models/database-storage.model.ts +24 -0
- package/src/lib/models/index.ts +12 -0
- package/src/lib/models/retry-options.model.ts +22 -0
- package/src/lib/services/database-manager-service/database.manager.service.ts +262 -0
- package/src/lib/services/database-manager-service/db.storage.service.ts +207 -0
- package/src/lib/services/database-manager-service/index.ts +4 -0
- package/src/lib/services/database-manager-service/models/index.ts +2 -0
- package/src/lib/services/database-manager-service/models/table-schema.ts +33 -0
- package/src/lib/services/index.ts +12 -0
- package/src/lib/services/local-storage-manager-service/index.ts +4 -0
- package/src/lib/services/local-storage-manager-service/local-storage-manager.service.spec.ts +71 -0
- package/src/lib/services/local-storage-manager-service/local-storage-manager.service.ts +426 -0
- package/src/lib/services/local-storage-manager-service/local-storage-signals-manager.service.spec.ts +67 -0
- package/src/lib/services/local-storage-manager-service/local-storage-signals-manager.service.ts +345 -0
- package/src/lib/services/local-storage-manager-service/models/global-store-options.model.ts +30 -0
- package/src/lib/services/local-storage-manager-service/models/index.ts +6 -0
- package/src/lib/services/local-storage-manager-service/models/setting-options.model.ts +35 -0
- package/src/lib/services/local-storage-manager-service/models/storage-data.model.ts +24 -0
- package/src/lib/services/local-storage-manager-service/models/storage-option.model.ts +32 -0
- package/src/lib/services/local-storage-manager-service/models/storage-type.enum.ts +5 -0
- package/src/lib/services/request-manager-services/README.md +268 -0
- package/src/lib/services/request-manager-services/http-manager-signals.service.ts +246 -0
- package/src/lib/services/request-manager-services/http-manager.service.spec.ts +232 -0
- package/src/lib/services/request-manager-services/http-manager.service.ts +274 -0
- package/src/lib/services/request-manager-services/index.ts +8 -0
- package/src/lib/services/request-manager-services/request-signals.service.ts +214 -0
- package/src/lib/services/request-manager-services/request.service.ts +309 -0
- package/src/lib/services/request-manager-services/rxjs-operators/countdown.ts +17 -0
- package/src/lib/services/request-manager-services/rxjs-operators/delay-retry.ts +16 -0
- package/src/lib/services/request-manager-services/rxjs-operators/index.ts +4 -0
- package/src/lib/services/request-manager-services/rxjs-operators/request-polling.ts +35 -0
- package/src/lib/services/request-manager-services/rxjs-operators/request-streaming.ts +436 -0
- package/src/lib/services/request-manager-state-service/http-manager-state.store.ts +1321 -0
- package/src/lib/services/request-manager-state-service/index.ts +3 -0
- package/src/lib/services/request-manager-state-service/models/api-request.model.ts +61 -0
- package/src/lib/services/request-manager-state-service/models/index.ts +6 -0
- package/src/lib/services/request-manager-state-service/models/request-options.model.ts +22 -0
- package/src/lib/services/request-manager-state-service/models/stream-type.enum.ts +13 -0
- package/src/lib/services/request-manager-state-service/models/ws-options.model.ts +39 -0
- package/src/lib/services/store-state-manager-service/index.ts +3 -0
- package/src/lib/services/store-state-manager-service/models/index.ts +2 -0
- package/src/lib/services/store-state-manager-service/models/state-storage-options.model.ts +24 -0
- package/src/lib/services/store-state-manager-service/store-state-manager.service.ts +88 -0
- package/src/lib/services/utils/app.service.spec.ts +25 -0
- package/src/lib/services/utils/app.service.ts +21 -0
- package/src/lib/services/utils/encryption/README.md +79 -0
- package/src/lib/services/utils/encryption/asymmetrical-encryption.service.ts +282 -0
- package/src/lib/services/utils/encryption/encryption-test.service.ts +39 -0
- package/src/lib/services/utils/encryption/index.ts +5 -0
- package/src/lib/services/utils/encryption/random.ts +81 -0
- package/src/lib/services/utils/encryption/symmetrical-encryption.service.ts +93 -0
- package/src/lib/services/utils/headers.service.spec.ts +80 -0
- package/src/lib/services/utils/headers.service.ts +18 -0
- package/src/lib/services/utils/index.ts +7 -0
- package/src/lib/services/utils/object-merger.service.spec.ts +18 -0
- package/src/lib/services/utils/object-merger.service.ts +78 -0
- package/src/lib/services/utils/path-query.service.spec.ts +117 -0
- package/src/lib/services/utils/path-query.service.ts +69 -0
- package/src/lib/services/utils/random-color.utils.ts +83 -0
- package/src/lib/services/utils/utils.service.spec.ts +165 -0
- package/src/lib/services/utils/utils.service.ts +192 -0
- package/src/lib/services/ws-manager-service/index.ts +4 -0
- package/src/lib/services/ws-manager-service/models/channel-info.model.ts +24 -0
- package/src/lib/services/ws-manager-service/models/channel-message-data.model.ts +24 -0
- package/src/lib/services/ws-manager-service/models/channel-message.model.ts +24 -0
- package/src/lib/services/ws-manager-service/models/communication-type.enum.ts +5 -0
- package/src/lib/services/ws-manager-service/models/index.ts +5 -0
- package/src/lib/services/ws-manager-service/models/ws-user.model.ts +38 -0
- package/src/lib/services/ws-manager-service/services/index.ts +3 -0
- package/src/lib/services/ws-manager-service/services/websocket.service.ts +392 -0
- package/src/public-api.ts +14 -0
- package/tsconfig.lib.json +32 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +14 -0
- package/fesm2022/http-request-manager.mjs +0 -7634
- package/fesm2022/http-request-manager.mjs.map +0 -1
- package/http-request-manager-18.7.19.tgz +0 -0
- package/types/http-request-manager.d.ts +0 -2278
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
import { inject, Inject, Injectable, InjectionToken } from '@angular/core';
|
|
2
|
+
import { ComponentStore } from '@ngrx/component-store';
|
|
3
|
+
|
|
4
|
+
import { BehaviorSubject, Observable, of, Subject, timer, merge } from 'rxjs';
|
|
5
|
+
import { tap, switchMap, concatMap, scan, delay, take, map, distinctUntilChanged, takeUntil, takeWhile, filter, catchError } from 'rxjs/operators';
|
|
6
|
+
import { DatabaseStorage } from '../../models/database-storage.model';
|
|
7
|
+
import { ApiRequest, RequestOptions, WSOptions } from './models';
|
|
8
|
+
import { WSUser } from '../ws-manager-service/models';
|
|
9
|
+
import { HTTPManagerService, DataType } from '../request-manager-services';
|
|
10
|
+
import { DatabaseManagerService } from '../database-manager-service';
|
|
11
|
+
import { TableSchemaDef } from '../database-manager-service/models/table-schema';
|
|
12
|
+
import { LocalStorageManagerService, StorageType } from '../local-storage-manager-service';
|
|
13
|
+
import { SettingOptions } from '../local-storage-manager-service/models/setting-options.model';
|
|
14
|
+
import { UtilsService } from '../..';
|
|
15
|
+
import { ChannelMessage } from '../ws-manager-service/models/channel-message.model';
|
|
16
|
+
|
|
17
|
+
const API_OPTS = new InjectionToken<ApiRequest>('API_OPTS');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Channel type enum for different communication purposes
|
|
21
|
+
* - STATE: Private channels for state synchronization (SYS- prefix)
|
|
22
|
+
* - MESSAGE: Public messaging/communication channels (PUB- prefix)
|
|
23
|
+
* - NOTIFICATION: Notification channels with DB persistence (MES- prefix)
|
|
24
|
+
*/
|
|
25
|
+
export enum ChannelType {
|
|
26
|
+
STATE = 'SYS',
|
|
27
|
+
MESSAGE = 'PUB',
|
|
28
|
+
NOTIFICATION = 'MES'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Utility function to create prefixed channel name
|
|
33
|
+
* @param channelType - The type of channel
|
|
34
|
+
* @param channelName - The base channel name
|
|
35
|
+
* @returns Prefixed channel name (e.g., 'SYS-USERS123')
|
|
36
|
+
*/
|
|
37
|
+
export function createChannelName(channelType: ChannelType, channelName: string): string {
|
|
38
|
+
return `${channelType}-${channelName}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface APIStateManagerData<T> {
|
|
42
|
+
data: T[]
|
|
43
|
+
dataObject: T | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const defaultState: APIStateManagerData<any> = {
|
|
47
|
+
data: [],
|
|
48
|
+
dataObject: null,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
@Injectable()
|
|
52
|
+
export class HTTPManagerStateService<T extends { id: number|string }> extends ComponentStore<APIStateManagerData<T>> {
|
|
53
|
+
|
|
54
|
+
httpManagerService = inject(HTTPManagerService)
|
|
55
|
+
dbManagerService = inject(DatabaseManagerService)
|
|
56
|
+
localStorageManagerService = inject(LocalStorageManagerService)
|
|
57
|
+
utils = inject(UtilsService)
|
|
58
|
+
|
|
59
|
+
error$ = this.httpManagerService.error$
|
|
60
|
+
isPending$ = this.httpManagerService.isPending$.pipe(delay(1))
|
|
61
|
+
|
|
62
|
+
// PAGINATION
|
|
63
|
+
private page = new BehaviorSubject<number>(0)
|
|
64
|
+
page$ = this.page.asObservable()
|
|
65
|
+
|
|
66
|
+
private totalPages = new BehaviorSubject<number>(0)
|
|
67
|
+
totalPages$ = this.totalPages.asObservable()
|
|
68
|
+
|
|
69
|
+
private percentage = new BehaviorSubject<number>(0)
|
|
70
|
+
percentage$ = this.percentage.asObservable()
|
|
71
|
+
|
|
72
|
+
private hasDatabase = false
|
|
73
|
+
|
|
74
|
+
streamedResponse = []
|
|
75
|
+
|
|
76
|
+
// WS
|
|
77
|
+
private maxRetries: number
|
|
78
|
+
private retryDelay: number
|
|
79
|
+
private shouldRetry = true
|
|
80
|
+
|
|
81
|
+
private databaseOptions?: DatabaseStorage
|
|
82
|
+
|
|
83
|
+
private wsRetryAttempts = new BehaviorSubject<number>(0)
|
|
84
|
+
wsRetryAttempts$ = this.wsRetryAttempts.asObservable()
|
|
85
|
+
|
|
86
|
+
private wsNextRetry: BehaviorSubject<number>
|
|
87
|
+
wsNextRetry$: Observable<number>
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
private messages = new BehaviorSubject<any[]>([])
|
|
91
|
+
messages$ = this.messages.asObservable()
|
|
92
|
+
|
|
93
|
+
private userListByChannel = new BehaviorSubject<Map<string, any[]>>(new Map())
|
|
94
|
+
userListByChannel$ = this.userListByChannel.asObservable()
|
|
95
|
+
|
|
96
|
+
// Convenience observable that returns users for a specific channel
|
|
97
|
+
getUsersForChannel$(channel: string) {
|
|
98
|
+
return this.userListByChannel$.pipe(
|
|
99
|
+
map(channelMap => channelMap.get(channel) || [])
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Legacy support - returns all unique users across all channels
|
|
104
|
+
private userList = new BehaviorSubject<any[]>([])
|
|
105
|
+
userList$ = this.userList.asObservable()
|
|
106
|
+
|
|
107
|
+
private user = new BehaviorSubject<WSUser|null>(null)
|
|
108
|
+
user$ = this.user.asObservable()
|
|
109
|
+
|
|
110
|
+
private channels = new BehaviorSubject<string[]|null>(null)
|
|
111
|
+
channels$ = this.channels.asObservable()
|
|
112
|
+
|
|
113
|
+
// In-memory notification channels (from server)
|
|
114
|
+
private notificationChannels = new BehaviorSubject<string[]>([])
|
|
115
|
+
notificationChannels$ = this.notificationChannels.asObservable()
|
|
116
|
+
|
|
117
|
+
// Today's notification channels (from database - channels with data for today)
|
|
118
|
+
private todaysNotificationChannels = new BehaviorSubject<string[]>([])
|
|
119
|
+
todaysNotificationChannels$ = this.todaysNotificationChannels.asObservable()
|
|
120
|
+
|
|
121
|
+
// Notification messages (MES- channels)
|
|
122
|
+
private notificationMessages = new BehaviorSubject<any[]>([])
|
|
123
|
+
notificationMessages$ = this.notificationMessages.asObservable()
|
|
124
|
+
|
|
125
|
+
private latestNotification = new Subject<any>()
|
|
126
|
+
latestNotification$ = this.latestNotification.asObservable()
|
|
127
|
+
|
|
128
|
+
private communicationMessages = new BehaviorSubject<any[]>([])
|
|
129
|
+
communicationMessages$ = this.communicationMessages.asObservable()
|
|
130
|
+
|
|
131
|
+
private latestCommunicationMessages = new BehaviorSubject<any|null>(null)
|
|
132
|
+
latestCommunicationMessages$ = this.latestCommunicationMessages.asObservable()
|
|
133
|
+
|
|
134
|
+
private userAction = new BehaviorSubject<any|null>(null)
|
|
135
|
+
userAction$ = this.userAction.asObservable()
|
|
136
|
+
|
|
137
|
+
wsConnection = false
|
|
138
|
+
wsOptions = WSOptions.adapt()
|
|
139
|
+
|
|
140
|
+
// Expose raw WS connection status directly to UI
|
|
141
|
+
public connectionStatus$: Observable<boolean> = this.httpManagerService.connectionStatus$
|
|
142
|
+
|
|
143
|
+
constructor(
|
|
144
|
+
@Inject(API_OPTS) private apiOptions = ApiRequest.adapt(),
|
|
145
|
+
@Inject("dataType") private dataType: DataType | undefined,
|
|
146
|
+
@Inject("database") private database?: DatabaseStorage | undefined
|
|
147
|
+
) {
|
|
148
|
+
|
|
149
|
+
super(defaultState);
|
|
150
|
+
|
|
151
|
+
this.databaseOptions = database
|
|
152
|
+
this.maxRetries = this.apiOptions.ws?.retry?.times || 3
|
|
153
|
+
this.retryDelay = (this.apiOptions.ws?.retry?.delay && this.apiOptions.ws.retry.delay * 1000) || 5 * 1000
|
|
154
|
+
// Start next retry countdown at 0 to avoid showing 5000 pre-connection
|
|
155
|
+
this.wsNextRetry = new BehaviorSubject<number>(0)
|
|
156
|
+
this.wsNextRetry$ = this.wsNextRetry.asObservable()
|
|
157
|
+
|
|
158
|
+
this.setApiRequestOptions(apiOptions, dataType, database)
|
|
159
|
+
|
|
160
|
+
if (this.databaseOptions && this.databaseOptions.table) {
|
|
161
|
+
|
|
162
|
+
this.localStorageManagerService.createStore({
|
|
163
|
+
name: this.databaseOptions.table,
|
|
164
|
+
data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions.expiresIn)} },
|
|
165
|
+
options: SettingOptions.adapt({
|
|
166
|
+
storage: StorageType.GLOBAL,
|
|
167
|
+
encrypted: false,
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
this.initDBStorage()
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Add appropriate prefix to a channel name if not already present
|
|
179
|
+
*/
|
|
180
|
+
private prefixChannel(channel: string, type: ChannelType): string {
|
|
181
|
+
const prefix = `${type}-`;
|
|
182
|
+
if (channel.startsWith(prefix)) {
|
|
183
|
+
return channel;
|
|
184
|
+
}
|
|
185
|
+
// Remove any other known prefix before adding the correct one
|
|
186
|
+
const cleanChannel = this.stripChannelPrefix(channel);
|
|
187
|
+
return `${type}-${cleanChannel}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Remove any known prefix from a channel name
|
|
192
|
+
*/
|
|
193
|
+
private stripChannelPrefix(channel: string): string {
|
|
194
|
+
for (const type of Object.values(ChannelType)) {
|
|
195
|
+
const prefix = `${type}-`;
|
|
196
|
+
if (channel.startsWith(prefix)) {
|
|
197
|
+
return channel.slice(prefix.length);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return channel;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get the base channel name without prefix (for display/user reference)
|
|
205
|
+
*/
|
|
206
|
+
getBaseChannelName(channel: string): string {
|
|
207
|
+
return this.stripChannelPrefix(channel);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
setApiRequestOptions(apiOptions?: ApiRequest, dataType?: DataType, database?: DatabaseStorage) {
|
|
211
|
+
|
|
212
|
+
this.apiOptions = ApiRequest.adapt(apiOptions)
|
|
213
|
+
this.dataType = (dataType) ? dataType : DataType.ARRAY
|
|
214
|
+
|
|
215
|
+
// Only update database options if a database parameter is explicitly provided
|
|
216
|
+
if (database !== undefined) {
|
|
217
|
+
this.hasDatabase = (database?.table) ? true : false
|
|
218
|
+
this.databaseOptions = (this.hasDatabase) ? DatabaseStorage.adapt(database) : undefined
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if(this.apiOptions.ws && this.apiOptions.ws.id !== '') {
|
|
222
|
+
|
|
223
|
+
// Auto-prefix channel ID for private state manager channels
|
|
224
|
+
// This ensures state manager channels are separate from user-defined channels
|
|
225
|
+
this.apiOptions.ws.id = this.prefixChannel(this.apiOptions.ws.id, ChannelType.STATE);
|
|
226
|
+
console.log(`🔒 Private state channel: ${this.apiOptions.ws.id}`);
|
|
227
|
+
|
|
228
|
+
// Update WebSocket retry settings when options change
|
|
229
|
+
if (this.apiOptions.ws?.retry) {
|
|
230
|
+
this.maxRetries = this.apiOptions.ws.retry.times || 3
|
|
231
|
+
this.retryDelay = (this.apiOptions.ws.retry.delay && this.apiOptions.ws.retry.delay * 1000) || 5 * 1000
|
|
232
|
+
this.wsNextRetry.next(this.retryDelay)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Validate wsServer before attempting connection
|
|
236
|
+
if (!this.apiOptions.ws.wsServer || this.apiOptions.ws.wsServer === '') {
|
|
237
|
+
console.error('WSOptions invalid: wsServer is missing or empty');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Setup connection status monitoring (internal subscription to drive retry counters)
|
|
242
|
+
this.setupConnectionStatus().subscribe();
|
|
243
|
+
|
|
244
|
+
// Make initial connection attempt
|
|
245
|
+
console.log('🔄 Initial WebSocket connection attempt...');
|
|
246
|
+
this.httpManagerService.connect(this.apiOptions.ws as WSOptions, this.apiOptions.ws.jwtToken || '');
|
|
247
|
+
|
|
248
|
+
// Initialize WS effect to handle messages
|
|
249
|
+
this.initWS(this.apiOptions.ws as WSOptions)
|
|
250
|
+
|
|
251
|
+
} else {
|
|
252
|
+
console.warn('WSOptions invalid Id: empty')
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// WebSocket
|
|
258
|
+
private setupConnectionStatus(): Observable<boolean> {
|
|
259
|
+
return this.httpManagerService.connectionStatus$.pipe(
|
|
260
|
+
distinctUntilChanged(),
|
|
261
|
+
tap(status => {
|
|
262
|
+
if (status === true) {
|
|
263
|
+
console.log('🟢 WebSocket connection is open.')
|
|
264
|
+
} else {
|
|
265
|
+
console.log('🔴 WebSocket connection is closed.')
|
|
266
|
+
}
|
|
267
|
+
}),
|
|
268
|
+
switchMap(status => {
|
|
269
|
+
|
|
270
|
+
if (status === true) {
|
|
271
|
+
this.shouldRetry = true
|
|
272
|
+
this.wsRetryAttempts.next(0);
|
|
273
|
+
this.wsNextRetry.next(0);
|
|
274
|
+
return of(true);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!this.shouldRetry) return of(false)
|
|
278
|
+
|
|
279
|
+
const countdownEnder$ = new Subject<void>();
|
|
280
|
+
|
|
281
|
+
// Immediately reflect upcoming retry delay in seconds on UI
|
|
282
|
+
const seconds = this.retryDelay / 1000;
|
|
283
|
+
this.wsNextRetry.next(seconds);
|
|
284
|
+
|
|
285
|
+
return timer(0, this.retryDelay)
|
|
286
|
+
.pipe(
|
|
287
|
+
take(this.maxRetries),
|
|
288
|
+
tap(i => {
|
|
289
|
+
|
|
290
|
+
const attempt = i + 1;
|
|
291
|
+
this.wsRetryAttempts.next(attempt);
|
|
292
|
+
countdownEnder$.next();
|
|
293
|
+
|
|
294
|
+
// Validate WS options; abort retries if invalid
|
|
295
|
+
const hasValidWS = !!(this.apiOptions.ws && this.apiOptions.ws.wsServer && this.apiOptions.ws.wsServer !== '');
|
|
296
|
+
|
|
297
|
+
if (!hasValidWS) {
|
|
298
|
+
this.shouldRetry = false;
|
|
299
|
+
this.wsNextRetry.next(0);
|
|
300
|
+
this.wsRetryAttempts.next(0);
|
|
301
|
+
return; // Skip connect and countdown
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log(`🔄 Retry attempt #${attempt}/${this.maxRetries}`);
|
|
305
|
+
this.httpManagerService.connect(this.apiOptions.ws as WSOptions, this.apiOptions.ws!.jwtToken || '');
|
|
306
|
+
|
|
307
|
+
if (attempt === this.maxRetries) {
|
|
308
|
+
this.wsNextRetry.next(0)
|
|
309
|
+
// console.error(`🚨 FAILED CONNECTION: Tried #${attempt} times`);
|
|
310
|
+
} else {
|
|
311
|
+
// console.log(`⚠️ Retry Attempt #${attempt}: Retrying in ${this.retryDelay / 1000}s`);
|
|
312
|
+
const seconds = this.retryDelay / 1000;
|
|
313
|
+
|
|
314
|
+
timer(0, 1000).pipe(
|
|
315
|
+
map(tick => seconds - tick),
|
|
316
|
+
takeWhile(val => val >= 0),
|
|
317
|
+
takeUntil(countdownEnder$)
|
|
318
|
+
).subscribe(remaining => {
|
|
319
|
+
this.wsNextRetry.next(remaining);
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
}),
|
|
325
|
+
map(() => false)
|
|
326
|
+
)
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// WebSocket
|
|
332
|
+
readonly initWS = this.effect<WSOptions>((wsOptions$) =>
|
|
333
|
+
wsOptions$.pipe(
|
|
334
|
+
// tap((wsOptions) => { debugger
|
|
335
|
+
// this.wsOptions = wsOptions
|
|
336
|
+
// }),
|
|
337
|
+
switchMap((wsOptions) =>
|
|
338
|
+
merge(
|
|
339
|
+
this.httpManagerService.connectionStatus$.pipe(
|
|
340
|
+
tap((isConnected: any) => {
|
|
341
|
+
this.wsConnection = isConnected
|
|
342
|
+
})
|
|
343
|
+
),
|
|
344
|
+
this.httpManagerService.messages$.pipe(
|
|
345
|
+
tap((message: any) => {
|
|
346
|
+
if (!message) return
|
|
347
|
+
|
|
348
|
+
// Add message to messages array
|
|
349
|
+
const currentMessages = this.messages.value
|
|
350
|
+
this.messages.next([...currentMessages, message])
|
|
351
|
+
|
|
352
|
+
console.log('Received:', message)
|
|
353
|
+
|
|
354
|
+
if (message.error === 'JWT_INVALID') {
|
|
355
|
+
this.shouldRetry = false
|
|
356
|
+
this.httpManagerService.disconnect()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (message.type === 'success') {
|
|
360
|
+
if(message.data.id !== this.user.value?.id) {
|
|
361
|
+
const user = WSUser.adapt(message.data)
|
|
362
|
+
this.user.next(user)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
switch (message.type) {
|
|
367
|
+
case 'channelsList':
|
|
368
|
+
|
|
369
|
+
console.log('💬 Channels:', message.channels)
|
|
370
|
+
|
|
371
|
+
// this.channelList = message.channels
|
|
372
|
+
|
|
373
|
+
// if (this.channelList.includes(wsOptions.id)) {
|
|
374
|
+
// this.httpManagerService.subscribeToChannel(wsOptions.id)
|
|
375
|
+
// } else {
|
|
376
|
+
// this.httpManagerService.createChannel(wsOptions.id)
|
|
377
|
+
// }
|
|
378
|
+
|
|
379
|
+
this.channels.next(message.channels)
|
|
380
|
+
|
|
381
|
+
break;
|
|
382
|
+
|
|
383
|
+
case 'subscribed':
|
|
384
|
+
console.log(`✅ Subscription confirmed: ${message.channel}`)
|
|
385
|
+
break;
|
|
386
|
+
|
|
387
|
+
case 'unsubscribed':
|
|
388
|
+
console.log(`🔓 Unsubscription confirmed: ${message.channel}`)
|
|
389
|
+
break;
|
|
390
|
+
|
|
391
|
+
case 'info':
|
|
392
|
+
// Already subscribed or other info messages
|
|
393
|
+
console.log(`ℹ️ Info: ${message.message}`)
|
|
394
|
+
break;
|
|
395
|
+
|
|
396
|
+
case 'stateMangerMessage':
|
|
397
|
+
// Compare sender's session ID with current user's ID
|
|
398
|
+
// message.data.sessionId is an object with 'id' property from server
|
|
399
|
+
const stateManagerSenderId = message.data.sessionId?.id || message.data.sessionId;
|
|
400
|
+
console.log('🔍 State Manager: Sender ID:', stateManagerSenderId, 'Current User ID:', this.user.value?.id);
|
|
401
|
+
if(stateManagerSenderId !== this.user.value?.id) {
|
|
402
|
+
console.log('💬 State Manager Message:', message.data)
|
|
403
|
+
console.log('📥 Fetching record with path:', message.data.content.path, 'method:', message.data.content.method)
|
|
404
|
+
this.userAction.next(message.data)
|
|
405
|
+
this.fetchRecord(RequestOptions.adapt({ path: message.data.content.path }), message.data.content.method)
|
|
406
|
+
} else {
|
|
407
|
+
console.log('⏭️ Skipping own message');
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case 'channelMessage':
|
|
412
|
+
// Handle channel-based messages (from sendChannelMessage)
|
|
413
|
+
// Structure: { type: 'channelMessage', channels: [...], sessionId: {id, ldap, name, email}, content: {message payload} }
|
|
414
|
+
// Skip messages from self
|
|
415
|
+
const senderSessionId = message.sessionId?.id;
|
|
416
|
+
if (senderSessionId === this.user.value?.id) {
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
console.log('💬 Channel Message received:', message);
|
|
420
|
+
if (message.content) {
|
|
421
|
+
this.appendMessages(ChannelMessage.adapt({
|
|
422
|
+
sessionId: message.sessionId,
|
|
423
|
+
content: message.content,
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
case 'usersInChannel':
|
|
429
|
+
console.log(`👥 Users in channel "${message.channel}":`, message.data.users)
|
|
430
|
+
|
|
431
|
+
// Update channel-specific user list
|
|
432
|
+
const currentMap = new Map(this.userListByChannel.value)
|
|
433
|
+
currentMap.set(message.channel, message.data.users)
|
|
434
|
+
this.userListByChannel.next(currentMap)
|
|
435
|
+
|
|
436
|
+
// Update legacy userList with unique users across all channels
|
|
437
|
+
const allUsers = Array.from(currentMap.values()).flat()
|
|
438
|
+
const uniqueUsers = allUsers.filter((user, index, self) =>
|
|
439
|
+
index === self.findIndex(u => u.id === user.id)
|
|
440
|
+
)
|
|
441
|
+
this.userList.next(uniqueUsers)
|
|
442
|
+
break;
|
|
443
|
+
|
|
444
|
+
case 'notificationChannelsList':
|
|
445
|
+
console.log('📢 Notification Channels (in-memory):', message.channels)
|
|
446
|
+
this.notificationChannels.next(message.channels || [])
|
|
447
|
+
break;
|
|
448
|
+
|
|
449
|
+
case 'todaysNotificationChannelsList':
|
|
450
|
+
console.log('📢 Today\'s Notification Channels (from DB):', message.channels)
|
|
451
|
+
this.todaysNotificationChannels.next(message.channels || [])
|
|
452
|
+
break;
|
|
453
|
+
|
|
454
|
+
case 'notificationSubscribed':
|
|
455
|
+
console.log(`📢 Notification subscription confirmed: ${message.channel}`, message.notifications)
|
|
456
|
+
// Set historical notifications from subscription
|
|
457
|
+
if (message.notifications && Array.isArray(message.notifications)) {
|
|
458
|
+
this.notificationMessages.next(message.notifications)
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
|
|
462
|
+
case 'notificationUnsubscribed':
|
|
463
|
+
console.log(`📢 Notification unsubscription confirmed: ${message.channel}`)
|
|
464
|
+
break;
|
|
465
|
+
|
|
466
|
+
case 'notification':
|
|
467
|
+
console.log('📢 Notification received:', message)
|
|
468
|
+
// Add to notifications array
|
|
469
|
+
const currentNotifications = this.notificationMessages.value
|
|
470
|
+
this.notificationMessages.next([...currentNotifications, message])
|
|
471
|
+
// Emit as latest notification
|
|
472
|
+
this.latestNotification.next(message)
|
|
473
|
+
break;
|
|
474
|
+
|
|
475
|
+
default:
|
|
476
|
+
// Messages are already added at the beginning of the tap
|
|
477
|
+
break
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
appendMessages(message: any) {
|
|
487
|
+
const currentMessages = this.communicationMessages.value
|
|
488
|
+
this.communicationMessages.next([...currentMessages, message])
|
|
489
|
+
this.latestMessage()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
latestMessage() {
|
|
493
|
+
const messages = this.communicationMessages.value
|
|
494
|
+
const latestMessage = messages[messages.length -1]
|
|
495
|
+
this.latestCommunicationMessages.next(latestMessage)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
clearMessages() {
|
|
499
|
+
this.communicationMessages.next([])
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
get ApiRequestOptions() {
|
|
503
|
+
return this.apiOptions
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
readonly initDBStorage = this.effect<void>((trigger$) =>
|
|
507
|
+
trigger$.pipe(
|
|
508
|
+
tap(() => {
|
|
509
|
+
if (this.dataType !== DataType.ARRAY) console.warn('Database storage requires dataType to be ARRAY')
|
|
510
|
+
if (!this.apiOptions.adapter) console.warn('Database storage requires an adapter to define the data shape')
|
|
511
|
+
if (this.databaseOptions && this.databaseOptions?.table === '') console.warn('Database storage requires a table name')
|
|
512
|
+
}),
|
|
513
|
+
filter(() => this.dataType === DataType.ARRAY && !!this.apiOptions.adapter && !!this.databaseOptions?.table),
|
|
514
|
+
switchMap(() => {
|
|
515
|
+
const sampleData = this.apiOptions.adapter?.({}) || {}
|
|
516
|
+
const schemaKeys = Object.keys(sampleData).filter(key => sampleData[key] !== undefined)
|
|
517
|
+
|
|
518
|
+
let schema = '++id'
|
|
519
|
+
|
|
520
|
+
if (schemaKeys.length > 0) {
|
|
521
|
+
const otherKeys = schemaKeys.filter(k => k !== 'id')
|
|
522
|
+
if (otherKeys.length > 0) {
|
|
523
|
+
schema += ', ' + otherKeys.join(', ')
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const tableDef = TableSchemaDef.adapt({
|
|
528
|
+
table: this.databaseOptions?.table,
|
|
529
|
+
schema: schema
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
return this.dbManagerService.createDatabaseTable(tableDef)
|
|
533
|
+
})
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
initializeState(data: any) {
|
|
538
|
+
this.setData$(data)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// --------------------------------------------------------------------------------------------------
|
|
542
|
+
// SELECTORS
|
|
543
|
+
readonly data$ = this.select(({ data, dataObject }) => {
|
|
544
|
+
const isArray =( this.dataType === DataType.ARRAY) ? true : false
|
|
545
|
+
return (isArray) ? data : dataObject
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
readonly selectRecord$ = (id: number) => this.select(
|
|
549
|
+
this.data$,
|
|
550
|
+
(data) => {
|
|
551
|
+
if (this.dataType === DataType.ARRAY && Array.isArray(data)) {
|
|
552
|
+
return data.find(item => item.id === id) as T | null
|
|
553
|
+
} else {
|
|
554
|
+
return (data as T).id === id ? data : null
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
// --------------------------------------------------------------------------------------------------
|
|
560
|
+
|
|
561
|
+
// UPDATERS
|
|
562
|
+
private readonly setData$ = this.updater((state: APIStateManagerData<T>, data: T | T[] | any) => {
|
|
563
|
+
|
|
564
|
+
if (!data) return state;
|
|
565
|
+
|
|
566
|
+
if (this.dataType === DataType.ARRAY) {
|
|
567
|
+
|
|
568
|
+
const dataArray = Array.isArray(data) ? data : [data]
|
|
569
|
+
|
|
570
|
+
const stateDataSample = (state.data.length > 0) ? Object.keys(state.data[0]) : []
|
|
571
|
+
const newDataSample = (dataArray.length > 0) ? Object.keys(dataArray[0]) : []
|
|
572
|
+
|
|
573
|
+
const isSame = (state.data.length === 0) ? false : stateDataSample.every((value, index) => value === newDataSample[index])
|
|
574
|
+
const updatedData = (!isSame && dataArray.length !== 0) ? this.updateArrayState([], dataArray) : this.updateArrayState(state.data, dataArray)
|
|
575
|
+
|
|
576
|
+
return { ...state, data: updatedData, dataObject: null } as APIStateManagerData<T>;
|
|
577
|
+
|
|
578
|
+
} else {
|
|
579
|
+
const dataObject = this.isEmpty(data) ? null : data;
|
|
580
|
+
return { ...state, data: [], dataObject: dataObject } as APIStateManagerData<T>;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
private updateArrayState(currentData: any[], newData: any[]): any[] {
|
|
586
|
+
|
|
587
|
+
const filterCurrentData = () => {
|
|
588
|
+
const ids = this.streamedResponse.map((obj: any) => obj.id)
|
|
589
|
+
return currentData.filter(obj => (obj.id) ? ids.includes(obj.id) : obj)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const filteredCurrentData = (this.httpManagerService.isPending.value) ? currentData : filterCurrentData()
|
|
593
|
+
|
|
594
|
+
const updatedData = filteredCurrentData.map(item => {
|
|
595
|
+
const newItem = newData.find(newItem => {
|
|
596
|
+
const hasId = (newItem?.id && item?.id) ? true : false
|
|
597
|
+
return (hasId) ? newItem.id === item.id : JSON.stringify(newItem) === JSON.stringify(item)
|
|
598
|
+
})
|
|
599
|
+
return (newItem) ? { ...item, ...newItem } : item
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
const addedData = newData.filter(newItem => {
|
|
603
|
+
return !filteredCurrentData.some(item => {
|
|
604
|
+
const hasId = (newItem?.id && item?.id) ? true : false
|
|
605
|
+
return (hasId) ? item.id === newItem.id : JSON.stringify(newItem) === JSON.stringify(item)
|
|
606
|
+
})
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
return [...updatedData, ...addedData]
|
|
610
|
+
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private readonly addData$ = this.updater((state: APIStateManagerData<T>, data: T) => {
|
|
614
|
+
|
|
615
|
+
if (this.dataType === DataType.ARRAY) {
|
|
616
|
+
const exists = state.data.some(item => item.id === data.id);
|
|
617
|
+
|
|
618
|
+
if (exists) {
|
|
619
|
+
const updatedData = state.data.map(item =>
|
|
620
|
+
item.id === data.id ? data : item
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
return { ...state, data: updatedData };
|
|
624
|
+
} else {
|
|
625
|
+
const newState = [...state.data, data];
|
|
626
|
+
return { ...state, data: newState };
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
return { ...state, dataObject: data };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
private readonly deleteData$ = this.updater((state: APIStateManagerData<T>, data: T & { id: number }) => {
|
|
635
|
+
if (this.dataType === DataType.ARRAY) {
|
|
636
|
+
const newState = state.data.filter(item => item.id !== data.id)
|
|
637
|
+
return { ...state, ...{ data: newState } }
|
|
638
|
+
} else {
|
|
639
|
+
return { ...state, ...{ dataObject: null } }
|
|
640
|
+
}
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
private readonly updateData$ = this.updater((state: APIStateManagerData<T>, data: T) => {
|
|
644
|
+
|
|
645
|
+
if (this.dataType === DataType.ARRAY) {
|
|
646
|
+
|
|
647
|
+
const objIndex = state.data.findIndex(item => item.id === data.id)
|
|
648
|
+
|
|
649
|
+
if (objIndex > -1) {
|
|
650
|
+
const newState = [...state.data]
|
|
651
|
+
newState[objIndex] = data
|
|
652
|
+
return { ...state, ...{ data: newState } }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return state
|
|
656
|
+
|
|
657
|
+
} else {
|
|
658
|
+
return { ...state, ...{ dataObject: data } }
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
// --------------------------------------------------------------------------------------------------
|
|
664
|
+
// EFFECTS
|
|
665
|
+
|
|
666
|
+
readonly clearRecords = this.effect(data =>
|
|
667
|
+
data.pipe(
|
|
668
|
+
|
|
669
|
+
tap(() => {
|
|
670
|
+
|
|
671
|
+
if (this.dataType === DataType.ARRAY) {
|
|
672
|
+
this.setData$([])
|
|
673
|
+
} else {
|
|
674
|
+
this.setData$({})
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
}),
|
|
678
|
+
concatMap(() => {
|
|
679
|
+
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
680
|
+
|
|
681
|
+
const currentData = this.get()?.data;
|
|
682
|
+
const idsToDelete = Array.isArray(currentData) ? currentData.map((r: any) => r.id) : [];
|
|
683
|
+
|
|
684
|
+
if (idsToDelete.length > 0) {
|
|
685
|
+
return this.dbManagerService.deleteTableRecords(this.databaseOptions.table, idsToDelete);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
}
|
|
689
|
+
return of(null);
|
|
690
|
+
})
|
|
691
|
+
))
|
|
692
|
+
|
|
693
|
+
// --------------------------------------------------------------------------------------------------
|
|
694
|
+
// CRUD OPERATIONS
|
|
695
|
+
|
|
696
|
+
// FETCH RECORDS
|
|
697
|
+
readonly fetchRecords = (options?: RequestOptions) =>
|
|
698
|
+
this.effect<any>(() =>
|
|
699
|
+
of(RequestOptions.adapt(options)).pipe(
|
|
700
|
+
switchMap(() => {
|
|
701
|
+
|
|
702
|
+
this.streamedResponse = []
|
|
703
|
+
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
704
|
+
|
|
705
|
+
const fetchFromAPI = () => {
|
|
706
|
+
|
|
707
|
+
return this.httpManagerService.getRequest(requestOptions, options?.path).pipe(
|
|
708
|
+
tap((data: any) => {
|
|
709
|
+
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data
|
|
710
|
+
this.setData$(data)
|
|
711
|
+
}),
|
|
712
|
+
concatMap((data: any) => {
|
|
713
|
+
if (this.hasDatabase && this.databaseOptions?.table && Array.isArray(data) && data.length > 0) {
|
|
714
|
+
|
|
715
|
+
this.localStorageManagerService.updateStore({
|
|
716
|
+
name: this.databaseOptions!.table,
|
|
717
|
+
data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions!.expiresIn)} }
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
return this.dbManagerService.createTableRecords(this.databaseOptions.table, data);
|
|
721
|
+
}
|
|
722
|
+
return of(data);
|
|
723
|
+
})
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
729
|
+
|
|
730
|
+
return this.dbManagerService.databaseExists().pipe(
|
|
731
|
+
switchMap((dbExists: boolean) => {
|
|
732
|
+
|
|
733
|
+
if (!dbExists) {
|
|
734
|
+
const initObs: Observable<any> = this.initDBStorageAsync();
|
|
735
|
+
return initObs.pipe(
|
|
736
|
+
switchMap(() => fetchFromAPI())
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return this.dbManagerService.hasDatabaseTable(this.databaseOptions!.table).pipe(
|
|
741
|
+
switchMap((tableExists: boolean): Observable<any> => {
|
|
742
|
+
|
|
743
|
+
if (!tableExists) {
|
|
744
|
+
const initObs: Observable<any> = this.initDBStorageAsync();
|
|
745
|
+
return initObs.pipe(
|
|
746
|
+
switchMap(() => fetchFromAPI())
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return this.localStorageManagerService.store$(this.databaseOptions!.table).pipe(
|
|
751
|
+
take(1),
|
|
752
|
+
switchMap((storeData: any) => {
|
|
753
|
+
|
|
754
|
+
const expires = storeData?.expires || 0;
|
|
755
|
+
const hasExpired = expires > 0 && this.utils.hasExpired(expires);
|
|
756
|
+
|
|
757
|
+
if (hasExpired) {
|
|
758
|
+
return this.dbManagerService.clearTable(this.databaseOptions!.table).pipe(
|
|
759
|
+
switchMap(() => fetchFromAPI()),
|
|
760
|
+
tap(() => {
|
|
761
|
+
this.localStorageManagerService.updateStore({
|
|
762
|
+
name: this.databaseOptions!.table,
|
|
763
|
+
data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions!.expiresIn)} }
|
|
764
|
+
})
|
|
765
|
+
})
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return this.dbManagerService.getTableRecords(this.databaseOptions!.table).pipe(
|
|
770
|
+
switchMap((dbData: any) => {
|
|
771
|
+
|
|
772
|
+
if (Array.isArray(dbData) && dbData.length > 0) {
|
|
773
|
+
this.setData$(dbData);
|
|
774
|
+
return of(dbData);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return fetchFromAPI();
|
|
778
|
+
|
|
779
|
+
})
|
|
780
|
+
);
|
|
781
|
+
})
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
})
|
|
785
|
+
);
|
|
786
|
+
})
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return fetchFromAPI();
|
|
792
|
+
|
|
793
|
+
})
|
|
794
|
+
)
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
private initDBStorageAsync() {
|
|
798
|
+
|
|
799
|
+
if (this.dataType !== DataType.ARRAY) {
|
|
800
|
+
console.warn('Database storage requires dataType to be ARRAY');
|
|
801
|
+
return of(null);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (!this.apiOptions.adapter) {
|
|
805
|
+
console.warn('Database storage requires an adapter to define the data shape');
|
|
806
|
+
return of(null);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (!this.databaseOptions?.table) {
|
|
810
|
+
console.warn('Database storage requires a table name');
|
|
811
|
+
return of(null);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const sampleData = this.apiOptions.adapter?.({}) || {};
|
|
815
|
+
const schemaKeys = Object.keys(sampleData).filter(key => sampleData[key] !== undefined);
|
|
816
|
+
|
|
817
|
+
let schema = '++id';
|
|
818
|
+
|
|
819
|
+
if (schemaKeys.length > 0) {
|
|
820
|
+
const otherKeys = schemaKeys.filter(k => k !== 'id');
|
|
821
|
+
if (otherKeys.length > 0) {
|
|
822
|
+
schema += ', ' + otherKeys.join(', ');
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const tableDef = TableSchemaDef.adapt({
|
|
827
|
+
table: this.databaseOptions?.table,
|
|
828
|
+
schema: schema
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
return this.dbManagerService.createDatabaseTable(tableDef);
|
|
832
|
+
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// FETCH RECORD
|
|
836
|
+
readonly fetchRecord = (options: RequestOptions, method: string) =>
|
|
837
|
+
this.effect<any>(() =>
|
|
838
|
+
of(RequestOptions.adapt(options)).pipe(
|
|
839
|
+
tap(() => console.log('🔄 fetchRecord effect triggered with path:', options?.path, 'method:', method)),
|
|
840
|
+
switchMap((options) => {
|
|
841
|
+
|
|
842
|
+
this.streamedResponse = []
|
|
843
|
+
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
844
|
+
|
|
845
|
+
console.log('🌐 Making GET request to path:', options?.path)
|
|
846
|
+
return this.httpManagerService.getRequest(requestOptions, options?.path)
|
|
847
|
+
.pipe(
|
|
848
|
+
tap((data: any) => {
|
|
849
|
+
console.log('📦 fetchRecord received data:', data)
|
|
850
|
+
|
|
851
|
+
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data
|
|
852
|
+
|
|
853
|
+
const id = (options as any).path?.length ? options.path[options.path.length -1] : null
|
|
854
|
+
|
|
855
|
+
if(method === 'DELETE') {
|
|
856
|
+
console.log('🗑️ Deleting record with id:', id)
|
|
857
|
+
this.deleteData$({ id } as T & { id: number })
|
|
858
|
+
}
|
|
859
|
+
if(method === 'UPDATE') {
|
|
860
|
+
console.log('✏️ Updating record:', data)
|
|
861
|
+
this.updateData$(data)
|
|
862
|
+
}
|
|
863
|
+
if(method === 'CREATE') {
|
|
864
|
+
console.log('➕ Adding record:', data)
|
|
865
|
+
this.addData$(data)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
}),
|
|
869
|
+
concatMap((data: any) => {
|
|
870
|
+
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
871
|
+
const id = (options as any).path?.length ? options.path[options.path.length -1] : null
|
|
872
|
+
|
|
873
|
+
if(method === 'DELETE' && id) return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, id)
|
|
874
|
+
if(method === 'UPDATE' && data) return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data)
|
|
875
|
+
if(method === 'CREATE' && data) return this.dbManagerService.createTableRecord(this.databaseOptions.table, data)
|
|
876
|
+
}
|
|
877
|
+
return of(data)
|
|
878
|
+
})
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
})
|
|
882
|
+
)
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
// CREATE RECORD
|
|
886
|
+
readonly createRecord = (data: any|null, options?: RequestOptions) =>
|
|
887
|
+
this.effect<any>(() =>
|
|
888
|
+
of(data).pipe(
|
|
889
|
+
switchMap((data: any) => {
|
|
890
|
+
|
|
891
|
+
this.streamedResponse = []
|
|
892
|
+
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
893
|
+
|
|
894
|
+
return this.httpManagerService.postRequest(data, requestOptions, options?.path)
|
|
895
|
+
.pipe(
|
|
896
|
+
tap((data: any) => {
|
|
897
|
+
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data
|
|
898
|
+
this.addData$(data)
|
|
899
|
+
if(this.wsConnection) this.wsCommunication('CREATE', [...options?.path || [], data.id])
|
|
900
|
+
}),
|
|
901
|
+
concatMap((data: any) => {
|
|
902
|
+
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
903
|
+
return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
|
|
904
|
+
}
|
|
905
|
+
return of(data);
|
|
906
|
+
})
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
})
|
|
910
|
+
)
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
// UPDATE RECORD
|
|
914
|
+
readonly updateRecord = (data: any|null, options?: RequestOptions) =>
|
|
915
|
+
this.effect<any>(() =>
|
|
916
|
+
of(data).pipe(
|
|
917
|
+
concatMap((data: any) => {
|
|
918
|
+
|
|
919
|
+
this.streamedResponse = []
|
|
920
|
+
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
921
|
+
|
|
922
|
+
return this.httpManagerService.putRequest(data, requestOptions, options?.path)
|
|
923
|
+
.pipe(
|
|
924
|
+
tap((data: any) => {
|
|
925
|
+
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data
|
|
926
|
+
this.updateData$(data)
|
|
927
|
+
if(this.wsConnection) this.wsCommunication('UPDATE', [...options?.path || []])
|
|
928
|
+
}),
|
|
929
|
+
concatMap((data: any) => {
|
|
930
|
+
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
931
|
+
return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
|
|
932
|
+
}
|
|
933
|
+
return of(data);
|
|
934
|
+
})
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
})
|
|
938
|
+
)
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
// DELETE RECORD
|
|
942
|
+
readonly deleteRecord = (options?: RequestOptions) =>
|
|
943
|
+
this.effect<any>(() =>
|
|
944
|
+
of(options).pipe(
|
|
945
|
+
concatMap((data: any) => {
|
|
946
|
+
|
|
947
|
+
this.streamedResponse = []
|
|
948
|
+
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
949
|
+
|
|
950
|
+
return this.httpManagerService.deleteRequest(requestOptions, options?.path)
|
|
951
|
+
.pipe(
|
|
952
|
+
tap((data: any) => {
|
|
953
|
+
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data
|
|
954
|
+
this.deleteData$(data)
|
|
955
|
+
if(this.wsConnection) this.wsCommunication('DELETE', [...options?.path || []])
|
|
956
|
+
}),
|
|
957
|
+
concatMap((data: any) => {
|
|
958
|
+
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
959
|
+
return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, data.id);
|
|
960
|
+
}
|
|
961
|
+
return of(data);
|
|
962
|
+
})
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
})
|
|
966
|
+
)
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
// --------------------------------------------------------------------------------------------------
|
|
970
|
+
// FETCH STREAM
|
|
971
|
+
readonly createStream = (data: any|null, options?: RequestOptions) =>
|
|
972
|
+
this.effect<any>(() =>
|
|
973
|
+
of(data).pipe(
|
|
974
|
+
tap(() => this.httpManagerService.isPending.next(true)),
|
|
975
|
+
switchMap((data: any) => {
|
|
976
|
+
|
|
977
|
+
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
978
|
+
|
|
979
|
+
return this.httpManagerService.postRequest(data, requestOptions, options?.path)
|
|
980
|
+
.pipe(
|
|
981
|
+
tap((res: any) => {
|
|
982
|
+
if(res.length > 0) this.setData$(res)
|
|
983
|
+
this.streamedResponse = res
|
|
984
|
+
}),
|
|
985
|
+
scan((acc, res: any) => {
|
|
986
|
+
|
|
987
|
+
const previous = acc.current
|
|
988
|
+
const current = res
|
|
989
|
+
return { previous, current };
|
|
990
|
+
|
|
991
|
+
}, { previous: null, current: null }),
|
|
992
|
+
tap(({ previous, current }) => {
|
|
993
|
+
|
|
994
|
+
if(previous && JSON.stringify(previous) === JSON.stringify(current)) {
|
|
995
|
+
this.httpManagerService.isPending.next(false)
|
|
996
|
+
this.setData$([])
|
|
997
|
+
} else {
|
|
998
|
+
this.httpManagerService.isPending.next(true)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
})
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
})
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
readonly fetchStream = (options?: RequestOptions) =>
|
|
1010
|
+
this.effect<any>(() =>
|
|
1011
|
+
of(options).pipe(
|
|
1012
|
+
tap(() => {
|
|
1013
|
+
console.log('[DEBUG] fetchStream called')
|
|
1014
|
+
this.httpManagerService.isPending.next(true)
|
|
1015
|
+
}),
|
|
1016
|
+
switchMap((options: any) => {
|
|
1017
|
+
|
|
1018
|
+
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
1019
|
+
requestOptions.stream = true
|
|
1020
|
+
|
|
1021
|
+
console.log('[DEBUG] Making streaming request:', requestOptions)
|
|
1022
|
+
|
|
1023
|
+
return this.httpManagerService.getRequest(requestOptions, options?.path)
|
|
1024
|
+
.pipe(
|
|
1025
|
+
tap((res: any) => {
|
|
1026
|
+
console.log('[DEBUG] Streaming response received:', res)
|
|
1027
|
+
|
|
1028
|
+
// Always update state with streaming data
|
|
1029
|
+
if (res && res.length > 0) {
|
|
1030
|
+
console.log('[DEBUG] Updating state with streaming data:', res)
|
|
1031
|
+
this.setData$(res)
|
|
1032
|
+
this.streamedResponse = res
|
|
1033
|
+
} else {
|
|
1034
|
+
console.log('[DEBUG] No streaming data or empty array:', res)
|
|
1035
|
+
}
|
|
1036
|
+
}),
|
|
1037
|
+
map((res: any) => {
|
|
1038
|
+
console.log('[DEBUG] Returning data to subscribers:', res)
|
|
1039
|
+
return res // Return the data so subscribers can receive it
|
|
1040
|
+
}),
|
|
1041
|
+
catchError((error) => {
|
|
1042
|
+
console.error('[DEBUG] Streaming error:', error)
|
|
1043
|
+
this.httpManagerService.isPending.next(false)
|
|
1044
|
+
return of([])
|
|
1045
|
+
})
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
}),
|
|
1049
|
+
)
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
// WEBSOCKET COMMUNICATION (STATE MANAGER)
|
|
1053
|
+
private wsCommunication(method: string, path?: string|number[]) {
|
|
1054
|
+
if(this.wsConnection && this.apiOptions.ws) {
|
|
1055
|
+
const wsServer = this.apiOptions.ws.id
|
|
1056
|
+
this.httpManagerService.sendMessageInChannel(wsServer, { method, path, user: this.apiOptions.ws.user })
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Send a message to channel(s)
|
|
1063
|
+
* @param message - The message content
|
|
1064
|
+
* @param channels - Optional array of channel names (passed as-is, caller should include prefix)
|
|
1065
|
+
* Use 'allChannels' to broadcast to all
|
|
1066
|
+
*/
|
|
1067
|
+
wsMessaging(message: ChannelMessage, channels?: string[]) {
|
|
1068
|
+
|
|
1069
|
+
const user = this.user.value
|
|
1070
|
+
const messageInfo = ChannelMessage.adapt({ ...message, fromUser: user })
|
|
1071
|
+
|
|
1072
|
+
console.log('📤 wsMessaging called with channels:', channels);
|
|
1073
|
+
|
|
1074
|
+
if(this.wsConnection && this.apiOptions.ws) {
|
|
1075
|
+
|
|
1076
|
+
// If specific channels provided, send to each channel
|
|
1077
|
+
// Channels are passed as-is - caller is responsible for including the correct prefix
|
|
1078
|
+
if (channels && channels.length > 0) {
|
|
1079
|
+
console.log(`📤 Sending to ${channels.length} channel(s):`, channels);
|
|
1080
|
+
channels.forEach(channel => {
|
|
1081
|
+
if (channel === 'allChannels') {
|
|
1082
|
+
this.httpManagerService.sendBroadcast(messageInfo);
|
|
1083
|
+
} else {
|
|
1084
|
+
this.httpManagerService.sendChannelMessage(channel, messageInfo);
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
} else {
|
|
1088
|
+
// Fallback to the primary WS channel (already prefixed with SYS-)
|
|
1089
|
+
const wsChannel = this.apiOptions.ws.id;
|
|
1090
|
+
console.log(`📤 Sending to state channel:`, wsChannel);
|
|
1091
|
+
this.httpManagerService.sendChannelMessage(wsChannel, messageInfo);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Subscribe to a messaging channel
|
|
1100
|
+
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1101
|
+
*/
|
|
1102
|
+
subscribeToMessageChannel(channel: string) {
|
|
1103
|
+
if (this.wsConnection) {
|
|
1104
|
+
const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
|
|
1105
|
+
this.httpManagerService.subscribeToChannel(prefixedChannel);
|
|
1106
|
+
console.log(`💬 Subscribed to message channel: ${prefixedChannel}`);
|
|
1107
|
+
} else {
|
|
1108
|
+
console.warn('Cannot subscribe to message channel: WebSocket not connected.');
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Unsubscribe from a messaging channel
|
|
1114
|
+
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1115
|
+
*/
|
|
1116
|
+
unsubscribeFromMessageChannel(channel: string) {
|
|
1117
|
+
if (this.wsConnection) {
|
|
1118
|
+
const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
|
|
1119
|
+
this.httpManagerService.unsubscribeFromChannel(prefixedChannel);
|
|
1120
|
+
console.log(`💬 Unsubscribed from message channel: ${prefixedChannel}`);
|
|
1121
|
+
} else {
|
|
1122
|
+
console.warn('Cannot unsubscribe from message channel: WebSocket not connected.');
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// --------------------------------------------------------------------------------------------------
|
|
1127
|
+
// CHANNEL MANAGEMENT (Raw channels - no automatic prefix)
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Subscribe to a single channel (no automatic prefix)
|
|
1131
|
+
* Use subscribeToMessageChannel() for MES- prefixed channels
|
|
1132
|
+
*/
|
|
1133
|
+
subscribeToChannel(channel: string) {
|
|
1134
|
+
if (this.wsConnection) {
|
|
1135
|
+
this.httpManagerService.subscribeToChannel(channel);
|
|
1136
|
+
} else {
|
|
1137
|
+
console.warn('Cannot subscribe: WebSocket not connected.');
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Subscribe to multiple channels at once
|
|
1143
|
+
*/
|
|
1144
|
+
subscribeToChannels(channels: string[]) {
|
|
1145
|
+
if (this.wsConnection) {
|
|
1146
|
+
this.httpManagerService.subscribeToChannels(channels);
|
|
1147
|
+
} else {
|
|
1148
|
+
console.warn('Cannot subscribe: WebSocket not connected.');
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Unsubscribe from a channel
|
|
1154
|
+
*/
|
|
1155
|
+
unsubscribeFromChannel(channel: string) {
|
|
1156
|
+
if (this.wsConnection) {
|
|
1157
|
+
this.httpManagerService.unsubscribeFromChannel(channel);
|
|
1158
|
+
} else {
|
|
1159
|
+
console.warn('Cannot unsubscribe: WebSocket not connected.');
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Get observable of currently subscribed channels
|
|
1165
|
+
*/
|
|
1166
|
+
get subscribedChannels$(): Observable<Set<string>> {
|
|
1167
|
+
return this.httpManagerService.subscribedChannels$;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Get current subscribed channels synchronously
|
|
1172
|
+
*/
|
|
1173
|
+
getSubscribedChannels(): Set<string> {
|
|
1174
|
+
return this.httpManagerService.getSubscribedChannels();
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Create a new channel on the server
|
|
1179
|
+
*/
|
|
1180
|
+
createChannel(channel: string) {
|
|
1181
|
+
if (this.wsConnection) {
|
|
1182
|
+
this.httpManagerService.createChannel(channel);
|
|
1183
|
+
} else {
|
|
1184
|
+
console.warn('Cannot create channel: WebSocket not connected.');
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Delete a channel from the server
|
|
1190
|
+
*/
|
|
1191
|
+
deleteChannel(channel: string) {
|
|
1192
|
+
if (this.wsConnection) {
|
|
1193
|
+
this.httpManagerService.deleteChannel(channel);
|
|
1194
|
+
} else {
|
|
1195
|
+
console.warn('Cannot delete channel: WebSocket not connected.');
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Request list of all channels from server
|
|
1201
|
+
*/
|
|
1202
|
+
getAllChannels() {
|
|
1203
|
+
if (this.wsConnection) {
|
|
1204
|
+
this.httpManagerService.getAllChannels();
|
|
1205
|
+
} else {
|
|
1206
|
+
console.warn('Cannot get channels: WebSocket not connected.');
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Get users in a specific channel
|
|
1212
|
+
*/
|
|
1213
|
+
getUsersInChannel(channel: string) {
|
|
1214
|
+
if (this.wsConnection) {
|
|
1215
|
+
this.httpManagerService.getUsersInChannel(channel);
|
|
1216
|
+
} else {
|
|
1217
|
+
console.warn('Cannot get users: WebSocket not connected.');
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// --------------------------------------------------------------------------------------------------
|
|
1222
|
+
// NOTIFICATION CHANNELS (MES- prefix - managed automatically)
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Create a notification channel on the server
|
|
1226
|
+
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1227
|
+
*/
|
|
1228
|
+
createNotificationChannel(channel: string) {
|
|
1229
|
+
if (this.wsConnection) {
|
|
1230
|
+
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
1231
|
+
this.httpManagerService.createNotificationChannel(prefixedChannel);
|
|
1232
|
+
console.log(`📢 Creating notification channel: ${prefixedChannel}`);
|
|
1233
|
+
} else {
|
|
1234
|
+
console.warn('Cannot create notification channel: WebSocket not connected.');
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Request list of all notification channels from server (in-memory)
|
|
1240
|
+
*/
|
|
1241
|
+
getNotificationChannels() {
|
|
1242
|
+
if (this.wsConnection) {
|
|
1243
|
+
this.httpManagerService.getNotificationChannels();
|
|
1244
|
+
} else {
|
|
1245
|
+
console.warn('Cannot get notification channels: WebSocket not connected.');
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Request list of today's notification channels from database
|
|
1251
|
+
* Returns unique channels that have notifications posted today
|
|
1252
|
+
*/
|
|
1253
|
+
getTodaysNotificationChannels() {
|
|
1254
|
+
if (this.wsConnection) {
|
|
1255
|
+
this.httpManagerService.getTodaysNotificationChannels();
|
|
1256
|
+
} else {
|
|
1257
|
+
console.warn('Cannot get today\'s notification channels: WebSocket not connected.');
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Subscribe to a notification channel with optional date filters
|
|
1263
|
+
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1264
|
+
*/
|
|
1265
|
+
subscribeToNotificationChannel(channel: string, options?: { startEpoch?: number; endEpoch?: number }, user?: any) {
|
|
1266
|
+
if (this.wsConnection) {
|
|
1267
|
+
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
1268
|
+
this.httpManagerService.subscribeToNotificationChannel(prefixedChannel, options, user);
|
|
1269
|
+
console.log(`📢 Subscribing to notification channel: ${prefixedChannel}`);
|
|
1270
|
+
} else {
|
|
1271
|
+
console.warn('Cannot subscribe to notification channel: WebSocket not connected.');
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Unsubscribe from a notification channel
|
|
1277
|
+
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1278
|
+
*/
|
|
1279
|
+
unsubscribeFromNotificationChannel(channel: string) {
|
|
1280
|
+
if (this.wsConnection) {
|
|
1281
|
+
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
1282
|
+
this.httpManagerService.unsubscribeFromNotificationChannel(prefixedChannel);
|
|
1283
|
+
console.log(`📢 Unsubscribing from notification channel: ${prefixedChannel}`);
|
|
1284
|
+
} else {
|
|
1285
|
+
console.warn('Cannot unsubscribe from notification channel: WebSocket not connected.');
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Send a notification to a channel
|
|
1291
|
+
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1292
|
+
*/
|
|
1293
|
+
sendNotification(channel: string, content: any) {
|
|
1294
|
+
if (this.wsConnection) {
|
|
1295
|
+
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
1296
|
+
this.httpManagerService.sendNotification(prefixedChannel, content);
|
|
1297
|
+
console.log(`📢 Sending notification to channel: ${prefixedChannel}`);
|
|
1298
|
+
} else {
|
|
1299
|
+
console.warn('Cannot send notification: WebSocket not connected.');
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// --------------------------------------------------------------------------------------------------
|
|
1304
|
+
// MISC
|
|
1305
|
+
|
|
1306
|
+
private isEmpty(obj: any) {
|
|
1307
|
+
return Object.keys(obj).length === 0
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
private updateRequestOptions(headers?: any): ApiRequest {
|
|
1311
|
+
|
|
1312
|
+
const options = ApiRequest.adapt({ ...this.apiOptions })
|
|
1313
|
+
|
|
1314
|
+
options.headers = (headers)
|
|
1315
|
+
? { ...options.headers, ...headers }
|
|
1316
|
+
: { ...options.headers }
|
|
1317
|
+
|
|
1318
|
+
return options
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
}
|