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.
Files changed (198) hide show
  1. package/ARCHITECTURE.md +483 -0
  2. package/DATABASE_README.md +1176 -0
  3. package/HTTP_MANAGER_README.md +579 -0
  4. package/HTTP_SINGNALS_MANAGER_README.md +654 -0
  5. package/HTTP_STATE_MANAGER_README.md +948 -0
  6. package/INTERCEPTOR_README.md +549 -0
  7. package/LOCAL_STORAGE_README.md +1056 -0
  8. package/STORE_STATE_MANAGER_README.md +1322 -0
  9. package/UTILS_README.md +1186 -0
  10. package/WS_MANAGER_README.md +613 -0
  11. package/ng-package.json +8 -0
  12. package/package.json +1 -12
  13. package/src/lib/http-request-manager.module.ts +132 -0
  14. package/src/lib/http-request-services-demo/database-data-demo/database-data-demo.component.html +65 -0
  15. package/src/lib/http-request-services-demo/database-data-demo/database-data-demo.component.scss +0 -0
  16. package/src/lib/http-request-services-demo/database-data-demo/database-data-demo.component.ts +224 -0
  17. package/src/lib/http-request-services-demo/http-request-services-demo.component.html +114 -0
  18. package/src/lib/http-request-services-demo/http-request-services-demo.component.scss +6 -0
  19. package/src/lib/http-request-services-demo/http-request-services-demo.component.ts +52 -0
  20. package/src/lib/http-request-services-demo/local-storage-demo/local-storage-demo.component.html +195 -0
  21. package/src/lib/http-request-services-demo/local-storage-demo/local-storage-demo.component.scss +17 -0
  22. package/src/lib/http-request-services-demo/local-storage-demo/local-storage-demo.component.ts +206 -0
  23. package/src/lib/http-request-services-demo/local-storage-signals-demo/local-storage-signals-demo.component.html +200 -0
  24. package/src/lib/http-request-services-demo/local-storage-signals-demo/local-storage-signals-demo.component.scss +17 -0
  25. package/src/lib/http-request-services-demo/local-storage-signals-demo/local-storage-signals-demo.component.ts +212 -0
  26. package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/download-file/download-file.component.html +53 -0
  27. package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/download-file/download-file.component.scss +60 -0
  28. package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/download-file/download-file.component.ts +72 -0
  29. package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-download.module.ts +28 -0
  30. package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-downloader.component.html +10 -0
  31. package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-downloader.component.scss +29 -0
  32. package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-downloader.component.ts +100 -0
  33. package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/models/download-labels-model.ts +22 -0
  34. package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/spinner/spinner.component.html +8 -0
  35. package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/spinner/spinner.component.scss +19 -0
  36. package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/spinner/spinner.component.ts +26 -0
  37. package/src/lib/http-request-services-demo/request-manager-demo/models/app-session.model.ts +30 -0
  38. package/src/lib/http-request-services-demo/request-manager-demo/models/app.model.ts +19 -0
  39. package/src/lib/http-request-services-demo/request-manager-demo/models/get-sample.model.ts +25 -0
  40. package/src/lib/http-request-services-demo/request-manager-demo/models/sample-ai-prompt.ts +19 -0
  41. package/src/lib/http-request-services-demo/request-manager-demo/models/sample-client-details.ts +24 -0
  42. package/src/lib/http-request-services-demo/request-manager-demo/models/sample-client-info.ts +30 -0
  43. package/src/lib/http-request-services-demo/request-manager-demo/models/sample-client.model.ts +49 -0
  44. package/src/lib/http-request-services-demo/request-manager-demo/models/sample-mapper-client-info.ts +33 -0
  45. package/src/lib/http-request-services-demo/request-manager-demo/request-manager-demo.component.html +392 -0
  46. package/src/lib/http-request-services-demo/request-manager-demo/request-manager-demo.component.scss +24 -0
  47. package/src/lib/http-request-services-demo/request-manager-demo/request-manager-demo.component.ts +461 -0
  48. package/src/lib/http-request-services-demo/request-manager-state-demo/request-manager-state-demo.component.html +393 -0
  49. package/src/lib/http-request-services-demo/request-manager-state-demo/request-manager-state-demo.component.scss +24 -0
  50. package/src/lib/http-request-services-demo/request-manager-state-demo/request-manager-state-demo.component.ts +421 -0
  51. package/src/lib/http-request-services-demo/request-manager-state-demo/services/state-manager-demo.service.ts +87 -0
  52. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/services/state-data-request.service.ts +120 -0
  53. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-ai-messaging/ws-ai-messaging.component.css +0 -0
  54. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-ai-messaging/ws-ai-messaging.component.html +3 -0
  55. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-ai-messaging/ws-ai-messaging.component.ts +16 -0
  56. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-chats/ws-chats.component.css +0 -0
  57. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-chats/ws-chats.component.html +3 -0
  58. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-chats/ws-chats.component.ts +16 -0
  59. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.css +31 -0
  60. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.html +72 -0
  61. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.scss +41 -0
  62. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.spec.ts +205 -0
  63. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.ts +77 -0
  64. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.css +11 -0
  65. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.html +96 -0
  66. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.spec.ts +31 -0
  67. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.ts +229 -0
  68. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.css +30 -0
  69. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.html +172 -0
  70. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.spec.ts +31 -0
  71. package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.ts +239 -0
  72. package/src/lib/http-request-services-demo/request-manager-ws-demo/models/oidc-client.model.ts +31 -0
  73. package/src/lib/http-request-services-demo/request-manager-ws-demo/models/user-data.model.ts +32 -0
  74. package/src/lib/http-request-services-demo/request-manager-ws-demo/request-manager-ws-demo.component.css +0 -0
  75. package/src/lib/http-request-services-demo/request-manager-ws-demo/request-manager-ws-demo.component.html +84 -0
  76. package/src/lib/http-request-services-demo/request-manager-ws-demo/request-manager-ws-demo.component.ts +41 -0
  77. package/src/lib/http-request-services-demo/request-manager-ws-demo/services/index.ts +3 -0
  78. package/src/lib/http-request-services-demo/request-manager-ws-demo/services/message-service-demo.service.ts +83 -0
  79. package/src/lib/http-request-services-demo/request-manager-ws-demo/services/notification-service-demo.service.ts +147 -0
  80. package/src/lib/http-request-services-demo/request-manager-ws-demo/services/state-service-demo.service.ts +158 -0
  81. package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/download-file/download-file.component.html +53 -0
  82. package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/download-file/download-file.component.scss +60 -0
  83. package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/download-file/download-file.component.ts +72 -0
  84. package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-download.module.ts +28 -0
  85. package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-downloader.component.html +10 -0
  86. package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-downloader.component.scss +29 -0
  87. package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-downloader.component.ts +100 -0
  88. package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/models/download-labels-model.ts +22 -0
  89. package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/spinner/spinner.component.html +8 -0
  90. package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/spinner/spinner.component.scss +19 -0
  91. package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/spinner/spinner.component.ts +26 -0
  92. package/src/lib/http-request-services-demo/request-signals-manager-demo/models/app-session.model.ts +30 -0
  93. package/src/lib/http-request-services-demo/request-signals-manager-demo/models/app.model.ts +19 -0
  94. package/src/lib/http-request-services-demo/request-signals-manager-demo/models/get-sample.model.ts +25 -0
  95. package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-ai-prompt.ts +19 -0
  96. package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-client-details.ts +24 -0
  97. package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-client-info.ts +30 -0
  98. package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-client.model.ts +49 -0
  99. package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-mapper-client-info.ts +33 -0
  100. package/src/lib/http-request-services-demo/request-signals-manager-demo/request-signals-manager-demo.component.html +380 -0
  101. package/src/lib/http-request-services-demo/request-signals-manager-demo/request-signals-manager-demo.component.scss +24 -0
  102. package/src/lib/http-request-services-demo/request-signals-manager-demo/request-signals-manager-demo.component.ts +410 -0
  103. package/src/lib/http-request-services-demo/store-state-manager-demo/models/settings.model.ts +28 -0
  104. package/src/lib/http-request-services-demo/store-state-manager-demo/services/settings-state.service.ts +48 -0
  105. package/src/lib/http-request-services-demo/store-state-manager-demo/store-state-manager-demo.component.css +0 -0
  106. package/src/lib/http-request-services-demo/store-state-manager-demo/store-state-manager-demo.component.html +23 -0
  107. package/src/lib/http-request-services-demo/store-state-manager-demo/store-state-manager-demo.component.ts +36 -0
  108. package/src/lib/index.ts +3 -0
  109. package/src/lib/interceptors/credentials.interceptor.ts +16 -0
  110. package/src/lib/interceptors/index.ts +6 -0
  111. package/src/lib/interceptors/models/error-settings.model.ts +22 -0
  112. package/src/lib/interceptors/models/index.ts +2 -0
  113. package/src/lib/interceptors/proxy-debugger.interceptor.ts +46 -0
  114. package/src/lib/interceptors/request-error.interceptor.ts +65 -0
  115. package/src/lib/interceptors/request-header.interceptor.ts +53 -0
  116. package/src/lib/models/config-http-options.model.ts +42 -0
  117. package/src/lib/models/config-local-storage-options.model.ts +27 -0
  118. package/src/lib/models/config-options.model.ts +27 -0
  119. package/src/lib/models/config-token.model.ts +9 -0
  120. package/src/lib/models/data-type.enum.ts +5 -0
  121. package/src/lib/models/database-storage.model.ts +24 -0
  122. package/src/lib/models/index.ts +12 -0
  123. package/src/lib/models/retry-options.model.ts +22 -0
  124. package/src/lib/services/database-manager-service/database.manager.service.ts +262 -0
  125. package/src/lib/services/database-manager-service/db.storage.service.ts +207 -0
  126. package/src/lib/services/database-manager-service/index.ts +4 -0
  127. package/src/lib/services/database-manager-service/models/index.ts +2 -0
  128. package/src/lib/services/database-manager-service/models/table-schema.ts +33 -0
  129. package/src/lib/services/index.ts +12 -0
  130. package/src/lib/services/local-storage-manager-service/index.ts +4 -0
  131. package/src/lib/services/local-storage-manager-service/local-storage-manager.service.spec.ts +71 -0
  132. package/src/lib/services/local-storage-manager-service/local-storage-manager.service.ts +426 -0
  133. package/src/lib/services/local-storage-manager-service/local-storage-signals-manager.service.spec.ts +67 -0
  134. package/src/lib/services/local-storage-manager-service/local-storage-signals-manager.service.ts +345 -0
  135. package/src/lib/services/local-storage-manager-service/models/global-store-options.model.ts +30 -0
  136. package/src/lib/services/local-storage-manager-service/models/index.ts +6 -0
  137. package/src/lib/services/local-storage-manager-service/models/setting-options.model.ts +35 -0
  138. package/src/lib/services/local-storage-manager-service/models/storage-data.model.ts +24 -0
  139. package/src/lib/services/local-storage-manager-service/models/storage-option.model.ts +32 -0
  140. package/src/lib/services/local-storage-manager-service/models/storage-type.enum.ts +5 -0
  141. package/src/lib/services/request-manager-services/README.md +268 -0
  142. package/src/lib/services/request-manager-services/http-manager-signals.service.ts +246 -0
  143. package/src/lib/services/request-manager-services/http-manager.service.spec.ts +232 -0
  144. package/src/lib/services/request-manager-services/http-manager.service.ts +274 -0
  145. package/src/lib/services/request-manager-services/index.ts +8 -0
  146. package/src/lib/services/request-manager-services/request-signals.service.ts +214 -0
  147. package/src/lib/services/request-manager-services/request.service.ts +309 -0
  148. package/src/lib/services/request-manager-services/rxjs-operators/countdown.ts +17 -0
  149. package/src/lib/services/request-manager-services/rxjs-operators/delay-retry.ts +16 -0
  150. package/src/lib/services/request-manager-services/rxjs-operators/index.ts +4 -0
  151. package/src/lib/services/request-manager-services/rxjs-operators/request-polling.ts +35 -0
  152. package/src/lib/services/request-manager-services/rxjs-operators/request-streaming.ts +436 -0
  153. package/src/lib/services/request-manager-state-service/http-manager-state.store.ts +1321 -0
  154. package/src/lib/services/request-manager-state-service/index.ts +3 -0
  155. package/src/lib/services/request-manager-state-service/models/api-request.model.ts +61 -0
  156. package/src/lib/services/request-manager-state-service/models/index.ts +6 -0
  157. package/src/lib/services/request-manager-state-service/models/request-options.model.ts +22 -0
  158. package/src/lib/services/request-manager-state-service/models/stream-type.enum.ts +13 -0
  159. package/src/lib/services/request-manager-state-service/models/ws-options.model.ts +39 -0
  160. package/src/lib/services/store-state-manager-service/index.ts +3 -0
  161. package/src/lib/services/store-state-manager-service/models/index.ts +2 -0
  162. package/src/lib/services/store-state-manager-service/models/state-storage-options.model.ts +24 -0
  163. package/src/lib/services/store-state-manager-service/store-state-manager.service.ts +88 -0
  164. package/src/lib/services/utils/app.service.spec.ts +25 -0
  165. package/src/lib/services/utils/app.service.ts +21 -0
  166. package/src/lib/services/utils/encryption/README.md +79 -0
  167. package/src/lib/services/utils/encryption/asymmetrical-encryption.service.ts +282 -0
  168. package/src/lib/services/utils/encryption/encryption-test.service.ts +39 -0
  169. package/src/lib/services/utils/encryption/index.ts +5 -0
  170. package/src/lib/services/utils/encryption/random.ts +81 -0
  171. package/src/lib/services/utils/encryption/symmetrical-encryption.service.ts +93 -0
  172. package/src/lib/services/utils/headers.service.spec.ts +80 -0
  173. package/src/lib/services/utils/headers.service.ts +18 -0
  174. package/src/lib/services/utils/index.ts +7 -0
  175. package/src/lib/services/utils/object-merger.service.spec.ts +18 -0
  176. package/src/lib/services/utils/object-merger.service.ts +78 -0
  177. package/src/lib/services/utils/path-query.service.spec.ts +117 -0
  178. package/src/lib/services/utils/path-query.service.ts +69 -0
  179. package/src/lib/services/utils/random-color.utils.ts +83 -0
  180. package/src/lib/services/utils/utils.service.spec.ts +165 -0
  181. package/src/lib/services/utils/utils.service.ts +192 -0
  182. package/src/lib/services/ws-manager-service/index.ts +4 -0
  183. package/src/lib/services/ws-manager-service/models/channel-info.model.ts +24 -0
  184. package/src/lib/services/ws-manager-service/models/channel-message-data.model.ts +24 -0
  185. package/src/lib/services/ws-manager-service/models/channel-message.model.ts +24 -0
  186. package/src/lib/services/ws-manager-service/models/communication-type.enum.ts +5 -0
  187. package/src/lib/services/ws-manager-service/models/index.ts +5 -0
  188. package/src/lib/services/ws-manager-service/models/ws-user.model.ts +38 -0
  189. package/src/lib/services/ws-manager-service/services/index.ts +3 -0
  190. package/src/lib/services/ws-manager-service/services/websocket.service.ts +392 -0
  191. package/src/public-api.ts +14 -0
  192. package/tsconfig.lib.json +32 -0
  193. package/tsconfig.lib.prod.json +10 -0
  194. package/tsconfig.spec.json +14 -0
  195. package/fesm2022/http-request-manager.mjs +0 -7634
  196. package/fesm2022/http-request-manager.mjs.map +0 -1
  197. package/http-request-manager-18.7.19.tgz +0 -0
  198. 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
+ }