http-request-manager 18.15.34 → 18.16.1
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/fesm2022/http-request-manager.mjs +13688 -0
- package/fesm2022/http-request-manager.mjs.map +1 -0
- package/http-request-manager-18.16.0.tgz +0 -0
- package/package.json +15 -6
- package/types/http-request-manager.d.ts +3968 -0
- package/TEST_COVERAGE_SUMMARY.md +0 -458
- package/ng-package.json +0 -8
- package/src/docs/ADVANCED_WEBSOCKET.md +0 -633
- package/src/docs/ARCHITECTURE.md +0 -633
- package/src/docs/BATCH_REQUEST_README.md +0 -467
- package/src/docs/COMPLETE_API_REFERENCE.md +0 -1037
- package/src/docs/DATABASE_README.md +0 -1195
- package/src/docs/ENCRYPTION_README.md +0 -403
- package/src/docs/HTTP_MANAGER_README.md +0 -628
- package/src/docs/HTTP_SINGNALS_MANAGER_README.md +0 -654
- package/src/docs/HTTP_STATE_MANAGER_README.md +0 -1391
- package/src/docs/INTERCEPTOR_README.md +0 -549
- package/src/docs/LOCAL_STORAGE_README.md +0 -1056
- package/src/docs/LOCAL_STORAGE_SIGNALS_README.md +0 -338
- package/src/docs/LOGGER_README.md +0 -310
- package/src/docs/MESSAGE_TRACKER_README.md +0 -518
- package/src/docs/MESSAGE_TRACKER_SIGNALS_README.md +0 -563
- package/src/docs/MODELS_README.md +0 -1264
- package/src/docs/SIGNAL_SERVICES_README.md +0 -238
- package/src/docs/SQL_DIXIE_README.md +0 -574
- package/src/docs/STORE_STATE_MANAGER_README.md +0 -556
- package/src/docs/STORE_STATE_SIGNALS_README.md +0 -600
- package/src/docs/UPLOAD_REQUEST_README.md +0 -324
- package/src/docs/UTILS_README.md +0 -1604
- package/src/docs/WEBSOCKET_MESSAGE_SERVICE.md +0 -799
- package/src/docs/WEBSOCKET_SIGNALS_README.md +0 -641
- package/src/docs/WEBSOCKET_SINGLETON_REFACTORING.md +0 -201
- package/src/docs/WS_MANAGER_README.md +0 -613
- package/src/lib/http-request-manager.module.ts +0 -147
- package/src/lib/http-request-services-demo/database-data-demo/database-data-demo.component.html +0 -116
- 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 +0 -255
- package/src/lib/http-request-services-demo/http-request-services-demo.component.html +0 -123
- package/src/lib/http-request-services-demo/http-request-services-demo.component.scss +0 -6
- package/src/lib/http-request-services-demo/http-request-services-demo.component.ts +0 -53
- package/src/lib/http-request-services-demo/local-storage-demo/local-storage-demo.component.html +0 -195
- package/src/lib/http-request-services-demo/local-storage-demo/local-storage-demo.component.scss +0 -17
- package/src/lib/http-request-services-demo/local-storage-demo/local-storage-demo.component.ts +0 -208
- package/src/lib/http-request-services-demo/local-storage-signals-demo/local-storage-signals-demo.component.html +0 -200
- package/src/lib/http-request-services-demo/local-storage-signals-demo/local-storage-signals-demo.component.scss +0 -17
- package/src/lib/http-request-services-demo/local-storage-signals-demo/local-storage-signals-demo.component.ts +0 -214
- package/src/lib/http-request-services-demo/request-manager-basic-demo/file-downloader/download-file/download-file.component.html +0 -53
- package/src/lib/http-request-services-demo/request-manager-basic-demo/file-downloader/download-file/download-file.component.scss +0 -60
- package/src/lib/http-request-services-demo/request-manager-basic-demo/file-downloader/download-file/download-file.component.ts +0 -72
- package/src/lib/http-request-services-demo/request-manager-basic-demo/file-downloader/file-download.module.ts +0 -28
- package/src/lib/http-request-services-demo/request-manager-basic-demo/file-downloader/file-downloader.component.html +0 -10
- package/src/lib/http-request-services-demo/request-manager-basic-demo/file-downloader/file-downloader.component.scss +0 -29
- package/src/lib/http-request-services-demo/request-manager-basic-demo/file-downloader/file-downloader.component.ts +0 -100
- package/src/lib/http-request-services-demo/request-manager-basic-demo/file-downloader/models/download-labels-model.ts +0 -22
- package/src/lib/http-request-services-demo/request-manager-basic-demo/file-downloader/spinner/spinner.component.html +0 -8
- package/src/lib/http-request-services-demo/request-manager-basic-demo/file-downloader/spinner/spinner.component.scss +0 -19
- package/src/lib/http-request-services-demo/request-manager-basic-demo/file-downloader/spinner/spinner.component.ts +0 -26
- package/src/lib/http-request-services-demo/request-manager-basic-demo/models/app-session.model.ts +0 -30
- package/src/lib/http-request-services-demo/request-manager-basic-demo/models/app.model.ts +0 -19
- package/src/lib/http-request-services-demo/request-manager-basic-demo/models/get-sample.model.ts +0 -25
- package/src/lib/http-request-services-demo/request-manager-basic-demo/models/sample-ai-prompt.ts +0 -19
- package/src/lib/http-request-services-demo/request-manager-basic-demo/models/sample-client-details.ts +0 -24
- package/src/lib/http-request-services-demo/request-manager-basic-demo/models/sample-client-info.ts +0 -30
- package/src/lib/http-request-services-demo/request-manager-basic-demo/models/sample-client.model.ts +0 -49
- package/src/lib/http-request-services-demo/request-manager-basic-demo/models/sample-mapper-client-info.ts +0 -33
- package/src/lib/http-request-services-demo/request-manager-basic-demo/request-manager-basic-demo.component.html +0 -279
- package/src/lib/http-request-services-demo/request-manager-basic-demo/request-manager-basic-demo.component.scss +0 -24
- package/src/lib/http-request-services-demo/request-manager-basic-demo/request-manager-basic-demo.component.ts +0 -461
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/download-file/download-file.component.html +0 -53
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/download-file/download-file.component.scss +0 -60
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/download-file/download-file.component.ts +0 -72
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-download.module.ts +0 -28
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-downloader.component.html +0 -10
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-downloader.component.scss +0 -29
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/file-downloader.component.ts +0 -100
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/models/download-labels-model.ts +0 -22
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/spinner/spinner.component.html +0 -8
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/spinner/spinner.component.scss +0 -19
- package/src/lib/http-request-services-demo/request-manager-demo/file-downloader/spinner/spinner.component.ts +0 -26
- package/src/lib/http-request-services-demo/request-manager-demo/models/app-session.model.ts +0 -30
- package/src/lib/http-request-services-demo/request-manager-demo/models/app.model.ts +0 -19
- package/src/lib/http-request-services-demo/request-manager-demo/models/get-sample.model.ts +0 -25
- package/src/lib/http-request-services-demo/request-manager-demo/models/sample-ai-prompt.ts +0 -19
- package/src/lib/http-request-services-demo/request-manager-demo/models/sample-client-details.ts +0 -24
- package/src/lib/http-request-services-demo/request-manager-demo/models/sample-client-info.ts +0 -30
- package/src/lib/http-request-services-demo/request-manager-demo/models/sample-client.model.ts +0 -49
- package/src/lib/http-request-services-demo/request-manager-demo/models/sample-mapper-client-info.ts +0 -33
- package/src/lib/http-request-services-demo/request-manager-demo/request-manager-demo.component.html +0 -622
- package/src/lib/http-request-services-demo/request-manager-demo/request-manager-demo.component.scss +0 -106
- package/src/lib/http-request-services-demo/request-manager-demo/request-manager-demo.component.ts +0 -687
- package/src/lib/http-request-services-demo/request-manager-state-demo/request-manager-state-demo.component.html +0 -418
- package/src/lib/http-request-services-demo/request-manager-state-demo/request-manager-state-demo.component.scss +0 -24
- package/src/lib/http-request-services-demo/request-manager-state-demo/request-manager-state-demo.component.ts +0 -576
- package/src/lib/http-request-services-demo/request-manager-state-demo/services/state-manager-demo.service.ts +0 -89
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/services/state-data-request.service.ts +0 -119
- 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 +0 -3
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-ai-messaging/ws-ai-messaging.component.ts +0 -16
- 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 +0 -3
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-chats/ws-chats.component.ts +0 -16
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.css +0 -31
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.html +0 -94
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.scss +0 -41
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.spec.ts +0 -203
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-data-control/ws-data-control.component.ts +0 -144
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.css +0 -11
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.html +0 -102
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.spec.ts +0 -40
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-messaging/ws-messaging.component.ts +0 -230
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.css +0 -30
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.html +0 -172
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.spec.ts +0 -31
- package/src/lib/http-request-services-demo/request-manager-ws-demo/components/ws-notifications/ws-notifications.component.ts +0 -239
- package/src/lib/http-request-services-demo/request-manager-ws-demo/models/oidc-client.model.ts +0 -31
- package/src/lib/http-request-services-demo/request-manager-ws-demo/models/user-data.model.ts +0 -32
- 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 +0 -84
- package/src/lib/http-request-services-demo/request-manager-ws-demo/request-manager-ws-demo.component.ts +0 -40
- package/src/lib/http-request-services-demo/request-manager-ws-demo/services/index.ts +0 -3
- package/src/lib/http-request-services-demo/request-manager-ws-demo/services/jwt-token.service.ts +0 -62
- package/src/lib/http-request-services-demo/request-manager-ws-demo/services/message-service-demo.service.ts +0 -83
- package/src/lib/http-request-services-demo/request-manager-ws-demo/services/notification-service-demo.service.ts +0 -147
- package/src/lib/http-request-services-demo/request-manager-ws-demo/services/state-service-demo.service.ts +0 -168
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/download-file/download-file.component.html +0 -53
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/download-file/download-file.component.scss +0 -60
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/download-file/download-file.component.ts +0 -72
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-download.module.ts +0 -28
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-downloader.component.html +0 -10
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-downloader.component.scss +0 -29
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/file-downloader.component.ts +0 -100
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/models/download-labels-model.ts +0 -22
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/spinner/spinner.component.html +0 -8
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/spinner/spinner.component.scss +0 -19
- package/src/lib/http-request-services-demo/request-signals-manager-demo/file-downloader/spinner/spinner.component.ts +0 -26
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/app-session.model.ts +0 -30
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/app.model.ts +0 -19
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/get-sample.model.ts +0 -25
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-ai-prompt.ts +0 -19
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-client-details.ts +0 -24
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-client-info.ts +0 -30
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-client.model.ts +0 -49
- package/src/lib/http-request-services-demo/request-signals-manager-demo/models/sample-mapper-client-info.ts +0 -33
- package/src/lib/http-request-services-demo/request-signals-manager-demo/request-signals-manager-demo.component.html +0 -380
- package/src/lib/http-request-services-demo/request-signals-manager-demo/request-signals-manager-demo.component.scss +0 -24
- package/src/lib/http-request-services-demo/request-signals-manager-demo/request-signals-manager-demo.component.ts +0 -410
- package/src/lib/http-request-services-demo/store-state-manager-demo/models/settings.model.ts +0 -28
- package/src/lib/http-request-services-demo/store-state-manager-demo/services/settings-state.service.ts +0 -49
- 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 +0 -23
- package/src/lib/http-request-services-demo/store-state-manager-demo/store-state-manager-demo.component.ts +0 -36
- package/src/lib/http-request-services-demo/store-state-signals-demo/store-state-signals-demo.component.ts +0 -161
- package/src/lib/http-request-services-demo/upload-demo/models/index.ts +0 -1
- package/src/lib/http-request-services-demo/upload-demo/models/upload-state.model.ts +0 -30
- package/src/lib/http-request-services-demo/upload-demo/upload-demo.component.html +0 -89
- package/src/lib/http-request-services-demo/upload-demo/upload-demo.component.scss +0 -160
- package/src/lib/http-request-services-demo/upload-demo/upload-demo.component.spec.ts +0 -101
- package/src/lib/http-request-services-demo/upload-demo/upload-demo.component.ts +0 -136
- package/src/lib/index.ts +0 -3
- package/src/lib/interceptors/credentials.interceptor.ts +0 -16
- package/src/lib/interceptors/index.ts +0 -6
- package/src/lib/interceptors/models/error-settings.model.ts +0 -22
- package/src/lib/interceptors/models/index.ts +0 -2
- package/src/lib/interceptors/proxy-debugger.interceptor.ts +0 -46
- package/src/lib/interceptors/request-error.interceptor.ts +0 -65
- package/src/lib/interceptors/request-header.interceptor.ts +0 -56
- package/src/lib/message-display/README.md +0 -509
- package/src/lib/message-display/index.ts +0 -4
- package/src/lib/message-display/models/action.model.ts +0 -27
- package/src/lib/message-display/models/communication-message.model.ts +0 -77
- package/src/lib/message-display/models/display-config.model.ts +0 -35
- package/src/lib/message-display/models/display-rule.interface.ts +0 -28
- package/src/lib/message-display/models/display-strategy.interface.ts +0 -8
- package/src/lib/message-display/models/index.ts +0 -6
- package/src/lib/message-display/models/slide.model.ts +0 -24
- package/src/lib/message-display/rules/default-display-rules.ts +0 -35
- package/src/lib/message-display/services/message-display-router.service.ts +0 -63
- package/src/lib/message-display/strategies/snackbar.strategy.ts +0 -46
- package/src/lib/models/batch-options.model.ts +0 -33
- package/src/lib/models/batch-progress.model.ts +0 -19
- package/src/lib/models/batch-request-state.model.ts +0 -40
- package/src/lib/models/batch-result.model.ts +0 -30
- package/src/lib/models/config-http-options.model.ts +0 -45
- package/src/lib/models/config-local-storage-options.model.ts +0 -27
- package/src/lib/models/config-options.model.ts +0 -27
- package/src/lib/models/config-token.model.ts +0 -9
- package/src/lib/models/data-type.enum.ts +0 -5
- package/src/lib/models/database-storage.model.ts +0 -24
- package/src/lib/models/index.ts +0 -16
- package/src/lib/models/retry-options.model.ts +0 -22
- package/src/lib/models/upload-validation-error.model.ts +0 -46
- package/src/lib/services/SQL-DixieJS service/dexie-query-executor.ts +0 -246
- package/src/lib/services/SQL-DixieJS service/dexie-sql.service.ts +0 -31
- package/src/lib/services/SQL-DixieJS service/index.ts +0 -4
- package/src/lib/services/SQL-DixieJS service/models/execution-plan.model.ts +0 -52
- package/src/lib/services/SQL-DixieJS service/models/index.ts +0 -3
- package/src/lib/services/SQL-DixieJS service/models/sql-errors.model.ts +0 -13
- package/src/lib/services/SQL-DixieJS service/models/sql-options.model.ts +0 -3
- package/src/lib/services/SQL-DixieJS service/query-planner.ts +0 -284
- package/src/lib/services/SQL-DixieJS service/schema-validator.ts +0 -217
- package/src/lib/services/SQL-DixieJS service/sql-parser.ts +0 -35
- package/src/lib/services/database-manager-service/database.manager.service.ts +0 -384
- package/src/lib/services/database-manager-service/db.storage.service.ts +0 -240
- package/src/lib/services/database-manager-service/index.ts +0 -4
- package/src/lib/services/database-manager-service/models/index.ts +0 -2
- package/src/lib/services/database-manager-service/models/table-schema.ts +0 -33
- package/src/lib/services/index.ts +0 -20
- package/src/lib/services/local-storage-manager-service/index.ts +0 -4
- package/src/lib/services/local-storage-manager-service/local-storage-manager.service.spec.ts +0 -71
- package/src/lib/services/local-storage-manager-service/local-storage-manager.service.ts +0 -567
- package/src/lib/services/local-storage-manager-service/local-storage-signals-manager.service.spec.ts +0 -67
- package/src/lib/services/local-storage-manager-service/local-storage-signals-manager.service.ts +0 -437
- package/src/lib/services/local-storage-manager-service/models/global-store-options.model.ts +0 -30
- package/src/lib/services/local-storage-manager-service/models/index.ts +0 -6
- package/src/lib/services/local-storage-manager-service/models/setting-options.model.ts +0 -35
- package/src/lib/services/local-storage-manager-service/models/storage-data.model.ts +0 -24
- package/src/lib/services/local-storage-manager-service/models/storage-option.model.ts +0 -32
- package/src/lib/services/local-storage-manager-service/models/storage-type.enum.ts +0 -5
- package/src/lib/services/request-manager-services/README.md +0 -282
- package/src/lib/services/request-manager-services/http-manager-signals.service.ts +0 -674
- package/src/lib/services/request-manager-services/http-manager.service.spec.ts +0 -353
- package/src/lib/services/request-manager-services/http-manager.service.ts +0 -727
- package/src/lib/services/request-manager-services/index.ts +0 -8
- package/src/lib/services/request-manager-services/request-signals.service.ts +0 -372
- package/src/lib/services/request-manager-services/request.service.ts +0 -435
- package/src/lib/services/request-manager-services/rxjs-operators/countdown.ts +0 -17
- package/src/lib/services/request-manager-services/rxjs-operators/delay-retry.ts +0 -16
- package/src/lib/services/request-manager-services/rxjs-operators/index.ts +0 -4
- package/src/lib/services/request-manager-services/rxjs-operators/request-polling.ts +0 -35
- package/src/lib/services/request-manager-services/rxjs-operators/request-streaming.ts +0 -468
- package/src/lib/services/request-manager-state-service/http-manager-state.store.spec.ts +0 -665
- package/src/lib/services/request-manager-state-service/http-manager-state.store.ts +0 -2395
- package/src/lib/services/request-manager-state-service/index.ts +0 -3
- package/src/lib/services/request-manager-state-service/models/api-request.model.ts +0 -86
- package/src/lib/services/request-manager-state-service/models/index.ts +0 -14
- package/src/lib/services/request-manager-state-service/models/operation-result.model.ts +0 -18
- package/src/lib/services/request-manager-state-service/models/parsing-result.model.ts +0 -21
- package/src/lib/services/request-manager-state-service/models/request-options.model.ts +0 -37
- package/src/lib/services/request-manager-state-service/models/stream-config.model.ts +0 -20
- package/src/lib/services/request-manager-state-service/models/stream-event-metadata.model.ts +0 -23
- package/src/lib/services/request-manager-state-service/models/stream-event.model.ts +0 -23
- package/src/lib/services/request-manager-state-service/models/stream-output.model.ts +0 -23
- package/src/lib/services/request-manager-state-service/models/stream-progress.model.ts +0 -24
- package/src/lib/services/request-manager-state-service/models/stream-type.enum.ts +0 -13
- package/src/lib/services/request-manager-state-service/models/ws-options.model.ts +0 -42
- package/src/lib/services/store-state-manager-service/index.ts +0 -4
- package/src/lib/services/store-state-manager-service/models/index.ts +0 -3
- package/src/lib/services/store-state-manager-service/models/state-operation-result.model.ts +0 -30
- package/src/lib/services/store-state-manager-service/models/state-storage-options.model.ts +0 -24
- package/src/lib/services/store-state-manager-service/store-state-manager-signals.service.ts +0 -169
- package/src/lib/services/store-state-manager-service/store-state-manager.service.ts +0 -153
- package/src/lib/services/utils/app.service.spec.ts +0 -25
- package/src/lib/services/utils/app.service.ts +0 -21
- package/src/lib/services/utils/encryption/README.md +0 -79
- package/src/lib/services/utils/encryption/asymmetrical-encryption.service.ts +0 -282
- package/src/lib/services/utils/encryption/encryption-test.service.ts +0 -39
- package/src/lib/services/utils/encryption/index.ts +0 -5
- package/src/lib/services/utils/encryption/random.ts +0 -81
- package/src/lib/services/utils/encryption/symmetrical-encryption.service.ts +0 -106
- package/src/lib/services/utils/headers.service.spec.ts +0 -80
- package/src/lib/services/utils/headers.service.ts +0 -18
- package/src/lib/services/utils/index.ts +0 -9
- package/src/lib/services/utils/logger.service.ts +0 -90
- package/src/lib/services/utils/models/index.ts +0 -4
- package/src/lib/services/utils/models/normalized-request-options.model.ts +0 -24
- package/src/lib/services/utils/models/path-tracker-state.model.ts +0 -20
- package/src/lib/services/utils/models/query-params-tracker-options.model.ts +0 -24
- package/src/lib/services/utils/models/query-tracker-state.model.ts +0 -23
- package/src/lib/services/utils/object-merger.service.spec.ts +0 -18
- package/src/lib/services/utils/object-merger.service.ts +0 -78
- package/src/lib/services/utils/path-query.service.spec.ts +0 -117
- package/src/lib/services/utils/path-query.service.ts +0 -69
- package/src/lib/services/utils/query-params-tracker.service.ts +0 -442
- package/src/lib/services/utils/random-color.utils.ts +0 -83
- package/src/lib/services/utils/utils.service.spec.ts +0 -165
- package/src/lib/services/utils/utils.service.ts +0 -192
- package/src/lib/services/ws-manager-service/index.ts +0 -13
- package/src/lib/services/ws-manager-service/message-tracker-signals.service.ts +0 -147
- package/src/lib/services/ws-manager-service/message-tracker.service.ts +0 -477
- package/src/lib/services/ws-manager-service/models/channel-info.model.ts +0 -29
- package/src/lib/services/ws-manager-service/models/channel-message-data.model.ts +0 -24
- package/src/lib/services/ws-manager-service/models/channel-message.model.ts +0 -36
- package/src/lib/services/ws-manager-service/models/channel-type.enum.ts +0 -6
- package/src/lib/services/ws-manager-service/models/communication-type.enum.ts +0 -5
- package/src/lib/services/ws-manager-service/models/index.ts +0 -10
- package/src/lib/services/ws-manager-service/models/notification-message.model.ts +0 -29
- package/src/lib/services/ws-manager-service/models/public-message.model.ts +0 -18
- package/src/lib/services/ws-manager-service/models/state-message.model.ts +0 -18
- package/src/lib/services/ws-manager-service/models/ws-user.model.ts +0 -38
- package/src/lib/services/ws-manager-service/services/index.ts +0 -4
- package/src/lib/services/ws-manager-service/services/websocket-message.service.ts +0 -129
- package/src/lib/services/ws-manager-service/services/websocket.service.ts +0 -434
- package/src/lib/services/ws-manager-service/websocket-service/index.ts +0 -1
- package/src/lib/services/ws-manager-service/websocket-service/websocket-manager.service.ts +0 -716
- package/src/lib/services/ws-manager-service/websocket-services-complete.spec.ts +0 -596
- package/src/lib/services/ws-manager-service/websocket-signals-manager.service.ts +0 -141
- package/src/public-api.ts +0 -19
- package/tsconfig.lib.json +0 -34
- package/tsconfig.lib.prod.json +0 -10
- package/tsconfig.spec.json +0 -14
|
@@ -1,2395 +0,0 @@
|
|
|
1
|
-
import { inject, Inject, Injectable, InjectionToken } from '@angular/core';
|
|
2
|
-
import { ComponentStore } from '@ngrx/component-store';
|
|
3
|
-
|
|
4
|
-
import { BehaviorSubject, Observable, of, Subject, merge, Subscription } from 'rxjs';
|
|
5
|
-
import { tap, switchMap, concatMap, scan, delay, take, map, distinctUntilChanged, filter, catchError, finalize } from 'rxjs/operators';
|
|
6
|
-
import { DatabaseStorage } from '../../models/database-storage.model';
|
|
7
|
-
import { ApiRequest, RequestOptions, WSOptions, OperationResultModel } 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
|
-
import { WebSocketManagerService } from '../ws-manager-service/websocket-service';
|
|
17
|
-
import { LoggerService } from '../../services/utils/logger.service';
|
|
18
|
-
// import { QueryParamsTrackerService } from '../utils/query-params-tracker.service';
|
|
19
|
-
|
|
20
|
-
const API_OPTS = new InjectionToken<ApiRequest>('API_OPTS');
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Channel type enum for different communication purposes
|
|
24
|
-
* - STATE: Private channels for state synchronization (SYS- prefix)
|
|
25
|
-
* - MESSAGE: Public messaging/communication channels (PUB- prefix)
|
|
26
|
-
* - NOTIFICATION: Notification channels with DB persistence (MES- prefix)
|
|
27
|
-
*/
|
|
28
|
-
export enum ChannelType {
|
|
29
|
-
STATE = 'SYS',
|
|
30
|
-
MESSAGE = 'PUB',
|
|
31
|
-
NOTIFICATION = 'MES'
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Utility function to create prefixed channel name
|
|
36
|
-
* @param channelType - The type of channel
|
|
37
|
-
* @param channelName - The base channel name
|
|
38
|
-
* @returns Prefixed channel name (e.g., 'SYS-USERS123')
|
|
39
|
-
*/
|
|
40
|
-
export function createChannelName(channelType: ChannelType, channelName: string): string {
|
|
41
|
-
return `${channelType}-${channelName}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface APIStateManagerData<T> {
|
|
45
|
-
data: T[]
|
|
46
|
-
dataObject: T | null
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const defaultState: APIStateManagerData<any> = {
|
|
50
|
-
data: [],
|
|
51
|
-
dataObject: null,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
@Injectable()
|
|
55
|
-
export class HTTPManagerStateService<T extends { id: number|string }> extends ComponentStore<APIStateManagerData<T>> {
|
|
56
|
-
|
|
57
|
-
httpManagerService = inject(HTTPManagerService)
|
|
58
|
-
dbManagerService = inject(DatabaseManagerService)
|
|
59
|
-
localStorageManagerService = inject(LocalStorageManagerService)
|
|
60
|
-
utils = inject(UtilsService)
|
|
61
|
-
logger = inject(LoggerService)
|
|
62
|
-
// private queryParamsTrackerService = inject(QueryParamsTrackerService)
|
|
63
|
-
|
|
64
|
-
error$ = this.httpManagerService.error$
|
|
65
|
-
isPending$ = this.httpManagerService.isPending$.pipe(delay(1))
|
|
66
|
-
|
|
67
|
-
private operationSuccess = new BehaviorSubject<OperationResultModel | null>(null)
|
|
68
|
-
operationSuccess$ = this.operationSuccess.asObservable()
|
|
69
|
-
|
|
70
|
-
// PAGINATION
|
|
71
|
-
private page = new BehaviorSubject<number>(0)
|
|
72
|
-
page$ = this.page.asObservable()
|
|
73
|
-
|
|
74
|
-
private totalPages = new BehaviorSubject<number>(0)
|
|
75
|
-
totalPages$ = this.totalPages.asObservable()
|
|
76
|
-
|
|
77
|
-
private percentage = new BehaviorSubject<number>(0)
|
|
78
|
-
percentage$ = this.percentage.asObservable()
|
|
79
|
-
|
|
80
|
-
private hasDatabase = false
|
|
81
|
-
|
|
82
|
-
streamedResponse: any[] = []
|
|
83
|
-
|
|
84
|
-
// WS
|
|
85
|
-
private maxRetries: number
|
|
86
|
-
private retryDelay: number
|
|
87
|
-
private shouldRetry = true
|
|
88
|
-
|
|
89
|
-
// Track connection status subscription to prevent duplicates
|
|
90
|
-
private connectionStatusSubscription?: Subscription
|
|
91
|
-
|
|
92
|
-
private databaseOptions?: DatabaseStorage
|
|
93
|
-
|
|
94
|
-
private readonly volatileHeaders = new Set([
|
|
95
|
-
'authorization',
|
|
96
|
-
'x-request-id',
|
|
97
|
-
'x-correlation-id',
|
|
98
|
-
'x-trace-id',
|
|
99
|
-
'x-amzn-trace-id',
|
|
100
|
-
'date',
|
|
101
|
-
'if-none-match'
|
|
102
|
-
])
|
|
103
|
-
|
|
104
|
-
private requestSignatureCache: Record<string, { GET?: string; STREAM?: string }> = {}
|
|
105
|
-
private inFlightRequestSignatures = new Set<string>()
|
|
106
|
-
private _requestCachePaths = new Map<string, string[]>()
|
|
107
|
-
|
|
108
|
-
private wsRetryAttempts = new BehaviorSubject<number>(0)
|
|
109
|
-
wsRetryAttempts$ = this.wsRetryAttempts.asObservable()
|
|
110
|
-
|
|
111
|
-
private wsNextRetry: BehaviorSubject<number>
|
|
112
|
-
wsNextRetry$: Observable<number>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
private messages = new BehaviorSubject<any[]>([])
|
|
116
|
-
messages$ = this.messages.asObservable()
|
|
117
|
-
|
|
118
|
-
private userListByChannel = new BehaviorSubject<Map<string, any[]>>(new Map())
|
|
119
|
-
userListByChannel$ = this.userListByChannel.asObservable()
|
|
120
|
-
|
|
121
|
-
// Convenience observable that returns users for a specific channel
|
|
122
|
-
getUsersForChannel$(channel: string) {
|
|
123
|
-
return this.userListByChannel$.pipe(
|
|
124
|
-
map(channelMap => channelMap.get(channel) || [])
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Returns all unique users across all channels
|
|
129
|
-
private userList = new BehaviorSubject<any[]>([])
|
|
130
|
-
userList$ = this.userList.asObservable()
|
|
131
|
-
|
|
132
|
-
private user = new BehaviorSubject<WSUser|null>(null)
|
|
133
|
-
user$ = this.user.asObservable()
|
|
134
|
-
|
|
135
|
-
private channels = new BehaviorSubject<string[]|null>(null)
|
|
136
|
-
channels$ = this.channels.asObservable()
|
|
137
|
-
|
|
138
|
-
// In-memory notification channels (from server)
|
|
139
|
-
private notificationChannels = new BehaviorSubject<string[]>([])
|
|
140
|
-
notificationChannels$ = this.notificationChannels.asObservable()
|
|
141
|
-
|
|
142
|
-
// Today's notification channels (from database - channels with data for today)
|
|
143
|
-
private todaysNotificationChannels = new BehaviorSubject<string[]>([])
|
|
144
|
-
todaysNotificationChannels$ = this.todaysNotificationChannels.asObservable()
|
|
145
|
-
|
|
146
|
-
// Notification messages (MES- channels)
|
|
147
|
-
private notificationMessages = new BehaviorSubject<any[]>([])
|
|
148
|
-
notificationMessages$ = this.notificationMessages.asObservable()
|
|
149
|
-
|
|
150
|
-
private latestNotification = new Subject<any>()
|
|
151
|
-
latestNotification$ = this.latestNotification.asObservable()
|
|
152
|
-
|
|
153
|
-
private communicationMessages = new BehaviorSubject<any[]>([])
|
|
154
|
-
communicationMessages$ = this.communicationMessages.asObservable()
|
|
155
|
-
|
|
156
|
-
private latestCommunicationMessages = new BehaviorSubject<any|null>(null)
|
|
157
|
-
latestCommunicationMessages$ = this.latestCommunicationMessages.asObservable()
|
|
158
|
-
|
|
159
|
-
private userAction = new BehaviorSubject<any|null>(null)
|
|
160
|
-
userAction$ = this.userAction.asObservable()
|
|
161
|
-
|
|
162
|
-
// Message queue for WebSocket communication (processed when connection is established)
|
|
163
|
-
private static wsCommunicationQueue: Array<{method: string, path?: string|number[]}> = [];
|
|
164
|
-
|
|
165
|
-
// Store our own sessionId for filtering out own messages
|
|
166
|
-
private ownSessionId: string | null = null;
|
|
167
|
-
|
|
168
|
-
wsOptions = WSOptions.adapt()
|
|
169
|
-
|
|
170
|
-
// Expose raw WS connection status directly to UI (from singleton WebSocketManagerService)
|
|
171
|
-
public connectionStatus$: Observable<boolean> = this.httpManagerService.connectionStatus$
|
|
172
|
-
public wsRetryCount$: Observable<number> = this.httpManagerService.retryCount$
|
|
173
|
-
public wsMaxRetries$: Observable<number> = this.httpManagerService.maxRetries$
|
|
174
|
-
|
|
175
|
-
constructor(
|
|
176
|
-
@Inject(API_OPTS) private apiOptions = ApiRequest.adapt(),
|
|
177
|
-
@Inject("dataType") private dataType: DataType | undefined,
|
|
178
|
-
database?: DatabaseStorage
|
|
179
|
-
) {
|
|
180
|
-
|
|
181
|
-
super(defaultState);
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
this.databaseOptions = database
|
|
185
|
-
this.hasDatabase = (database?.table) ? true : false
|
|
186
|
-
this.maxRetries = this.apiOptions.ws?.retry?.times || 3
|
|
187
|
-
this.retryDelay = (this.apiOptions.ws?.retry?.delay && this.apiOptions.ws.retry.delay * 1000) || 5 * 1000
|
|
188
|
-
|
|
189
|
-
// Start next retry countdown at 0 to avoid showing 5000 pre-connection
|
|
190
|
-
this.wsNextRetry = new BehaviorSubject<number>(0)
|
|
191
|
-
this.wsNextRetry$ = this.wsNextRetry.asObservable()
|
|
192
|
-
|
|
193
|
-
this.setApiRequestOptions(apiOptions, dataType, database)
|
|
194
|
-
|
|
195
|
-
if (this.databaseOptions && this.databaseOptions.table) {
|
|
196
|
-
|
|
197
|
-
this.localStorageManagerService.createStore({
|
|
198
|
-
name: this.databaseOptions.table,
|
|
199
|
-
data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions.expiresIn)} },
|
|
200
|
-
options: SettingOptions.adapt({
|
|
201
|
-
storage: StorageType.GLOBAL,
|
|
202
|
-
encrypted: false,
|
|
203
|
-
})
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
// Use initDBStorageAsync directly - the effect initDBStorage requires subscription
|
|
207
|
-
this.initDBStorageAsync().subscribe({
|
|
208
|
-
next: () => console.log('[Constructor] Database storage initialized'),
|
|
209
|
-
error: (err: any) => console.error('[Constructor] Database storage initialization failed:', err)
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
} catch (error) {
|
|
214
|
-
this.logger.error('HTTPManagerStateService', 'Error initializing', error);
|
|
215
|
-
// Initialize with safe defaults
|
|
216
|
-
this.databaseOptions = undefined;
|
|
217
|
-
this.maxRetries = 3;
|
|
218
|
-
this.retryDelay = 5000;
|
|
219
|
-
this.wsNextRetry = new BehaviorSubject<number>(0);
|
|
220
|
-
this.wsNextRetry$ = this.wsNextRetry.asObservable();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Add appropriate prefix to a channel name if not already present
|
|
227
|
-
*/
|
|
228
|
-
private prefixChannel(channel: string, type: ChannelType): string {
|
|
229
|
-
const prefix = `${type}-`;
|
|
230
|
-
if (channel.startsWith(prefix)) {
|
|
231
|
-
return channel;
|
|
232
|
-
}
|
|
233
|
-
// Remove any other known prefix before adding the correct one
|
|
234
|
-
const cleanChannel = this.stripChannelPrefix(channel);
|
|
235
|
-
return `${type}-${cleanChannel}`;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Remove any known prefix from a channel name
|
|
240
|
-
*/
|
|
241
|
-
private stripChannelPrefix(channel: string): string {
|
|
242
|
-
for (const type of Object.values(ChannelType)) {
|
|
243
|
-
const prefix = `${type}-`;
|
|
244
|
-
if (channel.startsWith(prefix)) {
|
|
245
|
-
return channel.slice(prefix.length);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return channel;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Get the base channel name without prefix (for display/user reference)
|
|
253
|
-
*/
|
|
254
|
-
getBaseChannelName(channel: string): string {
|
|
255
|
-
return this.stripChannelPrefix(channel);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
setApiRequestOptions(apiOptions?: ApiRequest, dataType?: DataType, database?: DatabaseStorage) {
|
|
259
|
-
|
|
260
|
-
this.logger.debug('StateStore', '🔧 setApiRequestOptions called', {
|
|
261
|
-
hasWs: !!apiOptions?.ws,
|
|
262
|
-
wsId: apiOptions?.ws?.id,
|
|
263
|
-
wsServer: apiOptions?.ws?.wsServer,
|
|
264
|
-
path: apiOptions?.path
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
this.apiOptions = ApiRequest.adapt(apiOptions)
|
|
268
|
-
this.dataType = (dataType) ? dataType : DataType.ARRAY
|
|
269
|
-
|
|
270
|
-
// Only update database options if a database parameter is explicitly provided
|
|
271
|
-
if (database !== undefined) {
|
|
272
|
-
console.log('[setApiRequestOptions] Database config:', {
|
|
273
|
-
database,
|
|
274
|
-
hasTable: !!database?.table,
|
|
275
|
-
tableValue: database?.table
|
|
276
|
-
})
|
|
277
|
-
this.hasDatabase = (database?.table) ? true : false
|
|
278
|
-
const adapted = DatabaseStorage.adapt(database)
|
|
279
|
-
console.log('[setApiRequestOptions] DatabaseStorage.adapt result:', adapted)
|
|
280
|
-
this.databaseOptions = (this.hasDatabase) ? adapted : undefined
|
|
281
|
-
|
|
282
|
-
// Trigger database table creation if table is configured
|
|
283
|
-
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
284
|
-
this.localStorageManagerService.createStore({
|
|
285
|
-
name: this.databaseOptions.table,
|
|
286
|
-
data: {
|
|
287
|
-
...this.databaseOptions,
|
|
288
|
-
expires: this.utils.expires(this.databaseOptions.expiresIn),
|
|
289
|
-
},
|
|
290
|
-
options: SettingOptions.adapt({
|
|
291
|
-
storage: StorageType.GLOBAL,
|
|
292
|
-
encrypted: false,
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
console.log('[setApiRequestOptions] Initializing database storage for table:', this.databaseOptions.table)
|
|
297
|
-
// Use initDBStorageAsync directly instead of the effect - effects need subscription to run
|
|
298
|
-
this.initDBStorageAsync().subscribe({
|
|
299
|
-
next: () => console.log('[setApiRequestOptions] Database storage initialized successfully'),
|
|
300
|
-
error: (err: any) => console.error('[setApiRequestOptions] Database storage initialization failed:', err)
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
// Pre-populate _requestCachePaths so clearRequestCacheMetadata works after browser refresh
|
|
304
|
-
// (before any GET request has run to populate it via saveRequestCacheMetadata)
|
|
305
|
-
this._requestCachePaths.set(
|
|
306
|
-
this.databaseOptions.table,
|
|
307
|
-
this.resolvePath().filter(p => typeof p === 'string' || typeof p === 'number').map(String)
|
|
308
|
-
)
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if(this.apiOptions.ws && this.apiOptions.ws.id !== '') {
|
|
313
|
-
|
|
314
|
-
// Auto-prefix channel ID for private state manager channels
|
|
315
|
-
// This ensures state manager channels are separate from user-defined channels
|
|
316
|
-
this.apiOptions.ws.id = this.prefixChannel(this.apiOptions.ws.id, ChannelType.STATE);
|
|
317
|
-
this.logger.debug('StateStore', `🔒 Private state channel configured`, { channelId: this.apiOptions.ws.id });
|
|
318
|
-
|
|
319
|
-
// Store our own sessionId for filtering incoming messages
|
|
320
|
-
this.ownSessionId = sessionStorage.getItem('WSID') || null;
|
|
321
|
-
this.logger.debug('StateStore', `🆔 Stored own sessionId for message filtering`, { sessionId: this.ownSessionId });
|
|
322
|
-
this.logger.debug('StateStore', `🔍 WebSocket configuration`, {
|
|
323
|
-
channelId: this.apiOptions.ws.id,
|
|
324
|
-
wsServer: this.apiOptions.ws.wsServer,
|
|
325
|
-
sessionId: this.ownSessionId,
|
|
326
|
-
path: this.apiOptions.path
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// Initialize WebSocket connection if not already connected
|
|
330
|
-
this.logger.debug('StateStore', '🔌 Checking WebSocket connection status...');
|
|
331
|
-
this.logger.debug('StateStore', '🔌 Is connected?', { connected: WebSocketManagerService.isConnected() });
|
|
332
|
-
|
|
333
|
-
if (!WebSocketManagerService.isConnected()) {
|
|
334
|
-
this.logger.debug('StateStore', '🔌 WebSocket not connected, will initialize via effect');
|
|
335
|
-
// initWS is an effect that triggers when setApiRequestOptions is called with ws config
|
|
336
|
-
// The effect is already triggered by calling setApiRequestOptions above
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (this.apiOptions.ws?.retry) {
|
|
340
|
-
this.maxRetries = this.apiOptions.ws.retry.times || 3
|
|
341
|
-
this.retryDelay = (this.apiOptions.ws.retry.delay && this.apiOptions.ws.retry.delay * 1000) || 5 * 1000
|
|
342
|
-
this.wsNextRetry.next(this.retryDelay)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Validate wsServer before attempting connection
|
|
346
|
-
if (!this.apiOptions.ws.wsServer || this.apiOptions.ws.wsServer === '') {
|
|
347
|
-
this.logger.error('StateStore', 'WSOptions invalid: wsServer is missing or empty');
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Clean up previous subscription to prevent duplicate handlers
|
|
352
|
-
if (this.connectionStatusSubscription) {
|
|
353
|
-
this.connectionStatusSubscription.unsubscribe();
|
|
354
|
-
this.connectionStatusSubscription = undefined;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Setup connection status monitoring (internal subscription to drive retry counters)
|
|
358
|
-
this.connectionStatusSubscription = this.setupConnectionStatus().subscribe();
|
|
359
|
-
|
|
360
|
-
// Make initial connection attempt
|
|
361
|
-
this.logger.debug('StateStore', '🔄 Initial WebSocket connection attempt...');
|
|
362
|
-
this.httpManagerService.connect(this.apiOptions.ws as WSOptions, this.apiOptions.ws.jwtToken || '');
|
|
363
|
-
|
|
364
|
-
// Initialize WS effect to handle messages
|
|
365
|
-
this.initWS(this.apiOptions.ws as WSOptions);
|
|
366
|
-
|
|
367
|
-
} else {
|
|
368
|
-
this.logger.warn('StateStore', 'WSOptions invalid Id: empty');
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// WebSocket
|
|
373
|
-
private setupConnectionStatus(): Observable<boolean> {
|
|
374
|
-
return this.httpManagerService.connectionStatus$.pipe(
|
|
375
|
-
distinctUntilChanged(),
|
|
376
|
-
tap(status => {
|
|
377
|
-
if (status === true) {
|
|
378
|
-
this.logger.debug('StateStore', '🟢 WebSocket connection is open')
|
|
379
|
-
} else {
|
|
380
|
-
this.logger.debug('StateStore', '🔴 WebSocket connection is closed')
|
|
381
|
-
}
|
|
382
|
-
}),
|
|
383
|
-
map(status => {
|
|
384
|
-
|
|
385
|
-
if (status === true) {
|
|
386
|
-
this.shouldRetry = true
|
|
387
|
-
this.wsRetryAttempts.next(0)
|
|
388
|
-
this.wsNextRetry.next(0)
|
|
389
|
-
return true
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Retry orchestration is centralized in WebSocketManagerService.connect/onclose
|
|
393
|
-
// and uses ws.retry from request options. Keep this stream as status/state only.
|
|
394
|
-
if (!this.shouldRetry) {
|
|
395
|
-
this.wsRetryAttempts.next(0)
|
|
396
|
-
this.wsNextRetry.next(0)
|
|
397
|
-
return false
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const nextAttempt = Math.min(this.wsRetryAttempts.value + 1, this.maxRetries)
|
|
401
|
-
this.wsRetryAttempts.next(nextAttempt)
|
|
402
|
-
this.wsNextRetry.next(nextAttempt >= this.maxRetries ? 0 : this.retryDelay / 1000)
|
|
403
|
-
return false
|
|
404
|
-
})
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// WebSocket
|
|
409
|
-
readonly initWS = this.effect<WSOptions>((wsOptions$) =>
|
|
410
|
-
wsOptions$.pipe(
|
|
411
|
-
switchMap((wsOptions) =>
|
|
412
|
-
merge(
|
|
413
|
-
this.httpManagerService.connectionStatus$.pipe(
|
|
414
|
-
tap((isConnected: any) => {
|
|
415
|
-
if (isConnected) {
|
|
416
|
-
// Subscribe to our private SYS- state channel so we receive stateMangerMessage broadcasts
|
|
417
|
-
// This is the critical subscription that enables multi-client state syncing
|
|
418
|
-
const stateChannel = wsOptions?.id || this.apiOptions.ws?.id;
|
|
419
|
-
if (stateChannel) {
|
|
420
|
-
console.log(`🔒 [STATE STORE] Subscribing to state channel: ${stateChannel}`);
|
|
421
|
-
this.httpManagerService.subscribeToChannel(stateChannel);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Process queued wsCommunication calls when connection becomes true
|
|
425
|
-
if (HTTPManagerStateService.wsCommunicationQueue.length > 0) {
|
|
426
|
-
console.log(`🔄 Processing ${HTTPManagerStateService.wsCommunicationQueue.length} queued WS messages`);
|
|
427
|
-
while (HTTPManagerStateService.wsCommunicationQueue.length > 0) {
|
|
428
|
-
const queued = HTTPManagerStateService.wsCommunicationQueue.shift()!;
|
|
429
|
-
this.sendWsCommunication(queued.method, queued.path);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
})
|
|
434
|
-
),
|
|
435
|
-
this.httpManagerService.messages$.pipe(
|
|
436
|
-
tap((message: any) => {
|
|
437
|
-
if (!message) return
|
|
438
|
-
|
|
439
|
-
// CRITICAL DEBUG: Log EVERY message received
|
|
440
|
-
console.log('🔍 [STATE STORE] Received WebSocket message:', {
|
|
441
|
-
type: message.type,
|
|
442
|
-
channel: message.channel,
|
|
443
|
-
sessionId: message.data?.sessionId?.id || message.sessionId?.id,
|
|
444
|
-
content: message.content || message.data?.content,
|
|
445
|
-
timestamp: new Date().toISOString()
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
// Add message to messages array
|
|
449
|
-
const currentMessages = this.messages.value
|
|
450
|
-
this.messages.next([...currentMessages, message])
|
|
451
|
-
|
|
452
|
-
console.log('Received:', message)
|
|
453
|
-
|
|
454
|
-
// Debug: Log all message types
|
|
455
|
-
this.logger.debug('StateStore', '📨 Message type', { type: message.type })
|
|
456
|
-
|
|
457
|
-
if (message.error === 'JWT_INVALID' || message.error === 'AUTH_BLOCKED') {
|
|
458
|
-
this.shouldRetry = false
|
|
459
|
-
this.httpManagerService.disconnect()
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (message.type === 'success') {
|
|
463
|
-
if(message.data.id !== this.user.value?.id) {
|
|
464
|
-
const user = WSUser.adapt(message.data)
|
|
465
|
-
this.user.next(user)
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
switch (message.type) {
|
|
470
|
-
case 'channelsList':
|
|
471
|
-
|
|
472
|
-
this.logger.debug('StateStore', '💬 Channels', { channels: message.channels })
|
|
473
|
-
this.logger.debug('StateStore', '🔍 channelsList received, checking connection status...')
|
|
474
|
-
this.logger.debug('StateStore', '🔍 WebSocket connected', { connected: WebSocketManagerService.isConnected() })
|
|
475
|
-
|
|
476
|
-
// Extract channel names from metadata objects (new format) or use strings directly (old format)
|
|
477
|
-
const channelNames = message.channels.map((c: any) => {
|
|
478
|
-
// Handle new format: {name: string, canSubscribe: boolean}
|
|
479
|
-
if (typeof c === 'object' && c.name) {
|
|
480
|
-
return c.name;
|
|
481
|
-
}
|
|
482
|
-
// Handle old format: string
|
|
483
|
-
return c;
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
// Auto-subscribe to all channels from the list
|
|
487
|
-
if (channelNames && channelNames.length > 0) {
|
|
488
|
-
this.logger.debug('StateStore', '📥 Auto-subscribing to channels', { count: channelNames.length })
|
|
489
|
-
this.subscribeToChannels(channelNames)
|
|
490
|
-
} else {
|
|
491
|
-
this.logger.warn('StateStore', '⚠️ No channels to subscribe to')
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
this.channels.next(channelNames)
|
|
495
|
-
|
|
496
|
-
break;
|
|
497
|
-
|
|
498
|
-
case 'success':
|
|
499
|
-
// Success messages - check for subscription confirmation
|
|
500
|
-
this.logger.info('StateStore', `✅ Success`, { message: message.message })
|
|
501
|
-
if (message.message?.includes('Subscribed to channel:')) {
|
|
502
|
-
// Extract channel name from message: "Subscribed to channel: PUB-chat"
|
|
503
|
-
const channelName = message.message.split('Subscribed to channel:')[1]?.trim()
|
|
504
|
-
this.logger.debug('StateStore', `✅ Subscription confirmed for channel`, { channel: channelName })
|
|
505
|
-
WebSocketManagerService.addSubscribedChannel(channelName)
|
|
506
|
-
}
|
|
507
|
-
break;
|
|
508
|
-
|
|
509
|
-
case 'subscribed':
|
|
510
|
-
this.logger.debug('StateStore', `✅ Subscription confirmed`, { channel: message.channel })
|
|
511
|
-
// Track as subscribed now that server confirmed
|
|
512
|
-
WebSocketManagerService.addSubscribedChannel(message.channel);
|
|
513
|
-
break;
|
|
514
|
-
|
|
515
|
-
case 'unsubscribed':
|
|
516
|
-
this.logger.debug('StateStore', `🔓 Unsubscription confirmed`, { channel: message.channel })
|
|
517
|
-
break;
|
|
518
|
-
|
|
519
|
-
case 'info':
|
|
520
|
-
// Already subscribed or other info messages
|
|
521
|
-
this.logger.info('StateStore', `ℹ️ Info`, { message: message.message })
|
|
522
|
-
// If it's an "Already subscribed" message, treat it as subscription confirmation
|
|
523
|
-
if (message.message?.includes('Already subscribed to channel:')) {
|
|
524
|
-
// Extract channel name from message: "Already subscribed to channel: PUB-chat"
|
|
525
|
-
const channelName = message.message.split('Already subscribed to channel:')[1]?.trim()
|
|
526
|
-
console.log(`✅ Treating info as subscription confirmation for channel: ${channelName}`)
|
|
527
|
-
WebSocketManagerService.addSubscribedChannel(channelName)
|
|
528
|
-
}
|
|
529
|
-
break;
|
|
530
|
-
|
|
531
|
-
case 'statemanagerMessage':
|
|
532
|
-
case 'stateMangerMessage':
|
|
533
|
-
// CRITICAL DEBUG: Log channel comparison
|
|
534
|
-
console.log('🔍 [STATE STORE] stateMangerMessage received:', {
|
|
535
|
-
messageChannel: message.channel,
|
|
536
|
-
ourChannel: this.apiOptions.ws?.id,
|
|
537
|
-
channelsMatch: message.channel === this.apiOptions.ws?.id,
|
|
538
|
-
senderSessionId: message.data.sessionId?.id,
|
|
539
|
-
ourSessionId: this.ownSessionId
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
// Compare sender's session ID with our own sessionId
|
|
543
|
-
// message.data.sessionId is an object with 'id' property from server
|
|
544
|
-
const stateManagerSenderId = message.data.sessionId?.id || message.data.sessionId;
|
|
545
|
-
console.log('🔍 State Manager: Sender sessionId:', stateManagerSenderId, 'Own sessionId:', this.ownSessionId);
|
|
546
|
-
if(stateManagerSenderId !== this.ownSessionId) {
|
|
547
|
-
console.log('💬 State Manager Message:', message.data)
|
|
548
|
-
console.log('📥 Fetching record with path:', message.data.content.path, 'method:', message.data.content.method)
|
|
549
|
-
this.userAction.next(message.data)
|
|
550
|
-
this.fetchRecord(RequestOptions.adapt({ path: message.data.content.path }), message.data.content.method)
|
|
551
|
-
} else {
|
|
552
|
-
console.log('⏭️ Skipping own message (sessionId match)');
|
|
553
|
-
}
|
|
554
|
-
break;
|
|
555
|
-
|
|
556
|
-
case 'channelMessage':
|
|
557
|
-
// Handle channel-based messages (from sendChannelMessage)
|
|
558
|
-
// Structure: { type: 'channelMessage', messageId, channel, sessionId, content, timestamp }
|
|
559
|
-
// Skip messages from self
|
|
560
|
-
const senderSessionId = message.sessionId?.id || message.sessionId;
|
|
561
|
-
if (senderSessionId === this.ownSessionId) {
|
|
562
|
-
console.log('🔇 Skipping message from self (sessionId match)');
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
console.log('💬 Channel Message received:', message);
|
|
567
|
-
|
|
568
|
-
// Determine which channels this message was sent to
|
|
569
|
-
const messageChannels = message.channel ? [message.channel] : [];
|
|
570
|
-
|
|
571
|
-
// Construct expected channel path (without env prefix)
|
|
572
|
-
const myPath = this.apiOptions.path || [];
|
|
573
|
-
const myPathString = myPath.join('/');
|
|
574
|
-
|
|
575
|
-
// Check if any of the message channels CONTAIN our path
|
|
576
|
-
const isWsUpdateChannel = messageChannels.some((ch: string) => {
|
|
577
|
-
// Strip SYS- prefix if present for comparison
|
|
578
|
-
const cleanChannel = ch.replace('SYS-', '');
|
|
579
|
-
// Check if channel contains our path (or starts with it)
|
|
580
|
-
const matches = cleanChannel === myPathString ||
|
|
581
|
-
cleanChannel.startsWith(myPathString + '/') ||
|
|
582
|
-
cleanChannel.includes(myPathString);
|
|
583
|
-
console.log(`🔍 Channel check: ${ch} contains ${myPathString}? ${matches}`);
|
|
584
|
-
return matches;
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
// If it's the expected channel, trigger fetchRecord like stateManagerMessage
|
|
588
|
-
if (isWsUpdateChannel && message.content?.path) {
|
|
589
|
-
console.log('🔍 Message received on expected channel (path match):', myPathString);
|
|
590
|
-
console.log('📄 Content:', message.content);
|
|
591
|
-
|
|
592
|
-
console.log('📥 Fetching record for channel:', myPathString);
|
|
593
|
-
const path = message.content.path;
|
|
594
|
-
const method = message.content.method || 'UPDATE';
|
|
595
|
-
this.userAction.next({ sessionId: message.sessionId, content: message.content });
|
|
596
|
-
this.fetchRecord(RequestOptions.adapt({ path }), method);
|
|
597
|
-
|
|
598
|
-
} else if (message.content) {
|
|
599
|
-
// Handle message content directly
|
|
600
|
-
console.log('📄 Processing message content:', message.content);
|
|
601
|
-
this.appendMessages(ChannelMessage.adapt({
|
|
602
|
-
sessionId: message.sessionId,
|
|
603
|
-
content: message.content,
|
|
604
|
-
}));
|
|
605
|
-
|
|
606
|
-
} else {
|
|
607
|
-
console.log('⚠️ Message does not contain data.content.path, skipping fetchRecord');
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Keep existing functionality for backward compatibility
|
|
611
|
-
if (message.data?.content && !isWsUpdateChannel) {
|
|
612
|
-
this.appendMessages(ChannelMessage.adapt({
|
|
613
|
-
sessionId: message.data.sessionId,
|
|
614
|
-
content: message.data.content,
|
|
615
|
-
}));
|
|
616
|
-
}
|
|
617
|
-
break;
|
|
618
|
-
|
|
619
|
-
case 'usersInChannel':
|
|
620
|
-
console.log(`👥 Users in channel "${message.channel}":`, message.data.users)
|
|
621
|
-
|
|
622
|
-
// Update channel-specific user list
|
|
623
|
-
const currentMap = new Map(this.userListByChannel.value)
|
|
624
|
-
currentMap.set(message.channel, message.data.users)
|
|
625
|
-
this.userListByChannel.next(currentMap)
|
|
626
|
-
|
|
627
|
-
// Update legacy userList with unique users across all channels
|
|
628
|
-
const allUsers = Array.from(currentMap.values()).flat()
|
|
629
|
-
const uniqueUsers = allUsers.filter((user, index, self) =>
|
|
630
|
-
index === self.findIndex(u => u.id === user.id)
|
|
631
|
-
)
|
|
632
|
-
this.userList.next(uniqueUsers)
|
|
633
|
-
break;
|
|
634
|
-
|
|
635
|
-
case 'notificationChannelsList':
|
|
636
|
-
console.log('📢 Notification Channels (in-memory):', message.channels)
|
|
637
|
-
// Extract channel names from metadata objects (new format) or use strings directly (old format)
|
|
638
|
-
const notifyChannelNames = message.channels.map((c: any) => {
|
|
639
|
-
if (typeof c === 'object' && c.name) {
|
|
640
|
-
return c.name;
|
|
641
|
-
}
|
|
642
|
-
return c;
|
|
643
|
-
});
|
|
644
|
-
this.notificationChannels.next(notifyChannelNames || [])
|
|
645
|
-
break;
|
|
646
|
-
|
|
647
|
-
case 'todaysNotificationChannelsList':
|
|
648
|
-
console.log('📢 Today\'s Notification Channels (from DB):', message.channels)
|
|
649
|
-
// Extract channel names from metadata objects (new format) or use strings directly (old format)
|
|
650
|
-
const todaysNotifyChannelNames = message.channels.map((c: any) => {
|
|
651
|
-
if (typeof c === 'object' && c.name) {
|
|
652
|
-
return c.name;
|
|
653
|
-
}
|
|
654
|
-
return c;
|
|
655
|
-
});
|
|
656
|
-
this.todaysNotificationChannels.next(todaysNotifyChannelNames || [])
|
|
657
|
-
break;
|
|
658
|
-
|
|
659
|
-
case 'notificationSubscribed':
|
|
660
|
-
console.log(`📢 Notification subscription confirmed: ${message.channel}`, message.notifications)
|
|
661
|
-
// Set historical notifications from subscription
|
|
662
|
-
if (message.notifications && Array.isArray(message.notifications)) {
|
|
663
|
-
this.notificationMessages.next(message.notifications)
|
|
664
|
-
}
|
|
665
|
-
break;
|
|
666
|
-
|
|
667
|
-
case 'notificationUnsubscribed':
|
|
668
|
-
console.log(`📢 Notification unsubscription confirmed: ${message.channel}`)
|
|
669
|
-
break;
|
|
670
|
-
|
|
671
|
-
case 'notification':
|
|
672
|
-
console.log('📢 Notification received:', message)
|
|
673
|
-
// Add to notifications array
|
|
674
|
-
const currentNotifications = this.notificationMessages.value
|
|
675
|
-
this.notificationMessages.next([...currentNotifications, message])
|
|
676
|
-
// Emit as latest notification
|
|
677
|
-
this.latestNotification.next(message)
|
|
678
|
-
break;
|
|
679
|
-
|
|
680
|
-
default:
|
|
681
|
-
// Messages are already added at the beginning of the tap
|
|
682
|
-
break
|
|
683
|
-
}
|
|
684
|
-
})
|
|
685
|
-
)
|
|
686
|
-
)
|
|
687
|
-
)
|
|
688
|
-
)
|
|
689
|
-
);
|
|
690
|
-
|
|
691
|
-
appendMessages(message: any) {
|
|
692
|
-
const currentMessages = this.communicationMessages.value
|
|
693
|
-
this.communicationMessages.next([...currentMessages, message])
|
|
694
|
-
this.latestMessage()
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
latestMessage() {
|
|
698
|
-
const messages = this.communicationMessages.value
|
|
699
|
-
const latestMessage = messages[messages.length -1]
|
|
700
|
-
this.latestCommunicationMessages.next(latestMessage)
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
clearMessages() {
|
|
704
|
-
this.communicationMessages.next([])
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
get ApiRequestOptions() {
|
|
708
|
-
return this.apiOptions
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
readonly initDBStorage = this.effect<void>((trigger$) =>
|
|
712
|
-
trigger$.pipe(
|
|
713
|
-
tap(() => {
|
|
714
|
-
console.log('[initDBStorage effect] Triggered, checking conditions:', {
|
|
715
|
-
dataType: this.dataType,
|
|
716
|
-
isARRAY: this.dataType === DataType.ARRAY,
|
|
717
|
-
hasAdapter: !!this.apiOptions?.adapter,
|
|
718
|
-
hasTable: !!this.databaseOptions?.table,
|
|
719
|
-
tableValue: this.databaseOptions?.table
|
|
720
|
-
})
|
|
721
|
-
if (this.dataType !== DataType.ARRAY) console.warn('Database storage requires dataType to be ARRAY')
|
|
722
|
-
if (!this.apiOptions.adapter) console.warn('Database storage adapter missing, using minimal or inferred schema')
|
|
723
|
-
if (this.databaseOptions && this.databaseOptions?.table === '') console.warn('Database storage requires a table name')
|
|
724
|
-
}),
|
|
725
|
-
filter(() => {
|
|
726
|
-
const shouldProceed = this.dataType === DataType.ARRAY && !!this.databaseOptions?.table
|
|
727
|
-
console.log('[initDBStorage effect] Filter result:', shouldProceed)
|
|
728
|
-
return shouldProceed
|
|
729
|
-
}),
|
|
730
|
-
switchMap(() => {
|
|
731
|
-
const schema = this.buildSchemaFromAdapter()
|
|
732
|
-
|
|
733
|
-
const tableDef = TableSchemaDef.adapt({
|
|
734
|
-
table: this.databaseOptions?.table,
|
|
735
|
-
schema: schema
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
return this.dbManagerService.createDatabaseTable(tableDef)
|
|
739
|
-
})
|
|
740
|
-
)
|
|
741
|
-
)
|
|
742
|
-
|
|
743
|
-
initializeState(data: any) {
|
|
744
|
-
this.setData$(data)
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// --------------------------------------------------------------------------------------------------
|
|
748
|
-
// SELECTORS
|
|
749
|
-
readonly data$ = this.select(({ data, dataObject }) => {
|
|
750
|
-
const isArray =( this.dataType === DataType.ARRAY) ? true : false
|
|
751
|
-
return (isArray) ? data : dataObject
|
|
752
|
-
})
|
|
753
|
-
|
|
754
|
-
readonly selectRecord$ = (id: number) => this.select(
|
|
755
|
-
this.data$,
|
|
756
|
-
(data) => {
|
|
757
|
-
if (this.dataType === DataType.ARRAY && Array.isArray(data)) {
|
|
758
|
-
return data.find(item => item.id === id) as T | null
|
|
759
|
-
} else {
|
|
760
|
-
return (data as T).id === id ? data : null
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
)
|
|
764
|
-
|
|
765
|
-
// --------------------------------------------------------------------------------------------------
|
|
766
|
-
|
|
767
|
-
// UPDATERS
|
|
768
|
-
private readonly setData$ = this.updater((state: APIStateManagerData<T>, data: T | T[] | any) => {
|
|
769
|
-
|
|
770
|
-
if (!data) return state;
|
|
771
|
-
|
|
772
|
-
if (this.dataType === DataType.ARRAY) {
|
|
773
|
-
|
|
774
|
-
const dataArray = Array.isArray(data) ? data : [data]
|
|
775
|
-
|
|
776
|
-
const stateDataSample = (state.data.length > 0) ? Object.keys(state.data[0]) : []
|
|
777
|
-
const newDataSample = (dataArray.length > 0) ? Object.keys(dataArray[0]) : []
|
|
778
|
-
|
|
779
|
-
const isSame = (state.data.length === 0) ? false : stateDataSample.every((value, index) => value === newDataSample[index])
|
|
780
|
-
const updatedData = (!isSame && dataArray.length !== 0) ? this.updateArrayState([], dataArray) : this.updateArrayState(state.data, dataArray)
|
|
781
|
-
|
|
782
|
-
return { ...state, data: updatedData, dataObject: null } as APIStateManagerData<T>;
|
|
783
|
-
|
|
784
|
-
} else {
|
|
785
|
-
const dataObject = this.isEmpty(data) ? null : data;
|
|
786
|
-
return { ...state, data: [], dataObject: dataObject } as APIStateManagerData<T>;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
})
|
|
790
|
-
|
|
791
|
-
private updateArrayState(currentData: any[], newData: any[]): any[] {
|
|
792
|
-
|
|
793
|
-
// For non-streaming requests (GET), REPLACE data entirely
|
|
794
|
-
// For streaming requests, MERGE with existing data incrementally
|
|
795
|
-
if (this.streamedResponse.length === 0) {
|
|
796
|
-
// GET request: return new data as-is (replace)
|
|
797
|
-
return newData
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Streaming: merge with existing data
|
|
801
|
-
const filterCurrentData = () => {
|
|
802
|
-
const ids = this.streamedResponse.map((obj: any) => obj.id)
|
|
803
|
-
return currentData.filter(obj => (obj.id) ? ids.includes(obj.id) : obj)
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
const filteredCurrentData = (this.httpManagerService.isPending.value)
|
|
807
|
-
? currentData
|
|
808
|
-
: filterCurrentData()
|
|
809
|
-
|
|
810
|
-
const updatedData = filteredCurrentData.map(item => {
|
|
811
|
-
const newItem = newData.find(newItem => {
|
|
812
|
-
const hasId = (newItem?.id && item?.id) ? true : false
|
|
813
|
-
return (hasId) ? newItem.id === item.id : JSON.stringify(newItem) === JSON.stringify(item)
|
|
814
|
-
})
|
|
815
|
-
return (newItem) ? { ...item, ...newItem } : item
|
|
816
|
-
})
|
|
817
|
-
|
|
818
|
-
const addedData = newData.filter(newItem => {
|
|
819
|
-
return !filteredCurrentData.some(item => {
|
|
820
|
-
const hasId = (newItem?.id && item?.id) ? true : false
|
|
821
|
-
return (hasId) ? item.id === newItem.id : JSON.stringify(newItem) === JSON.stringify(item)
|
|
822
|
-
})
|
|
823
|
-
})
|
|
824
|
-
|
|
825
|
-
return [...updatedData, ...addedData]
|
|
826
|
-
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
private readonly addData$ = this.updater((state: APIStateManagerData<T>, data: T) => {
|
|
830
|
-
|
|
831
|
-
if (this.dataType === DataType.ARRAY) {
|
|
832
|
-
const exists = state.data.some(item => item.id === data.id);
|
|
833
|
-
|
|
834
|
-
if (exists) {
|
|
835
|
-
const updatedData = state.data.map(item =>
|
|
836
|
-
item.id === data.id ? data : item
|
|
837
|
-
);
|
|
838
|
-
|
|
839
|
-
return { ...state, data: updatedData };
|
|
840
|
-
} else {
|
|
841
|
-
const newState = [...state.data, data];
|
|
842
|
-
return { ...state, data: newState };
|
|
843
|
-
}
|
|
844
|
-
} else {
|
|
845
|
-
return { ...state, dataObject: data };
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
})
|
|
849
|
-
|
|
850
|
-
private readonly deleteData$ = this.updater((state: APIStateManagerData<T>, data: T & { id: number }) => {
|
|
851
|
-
if (this.dataType === DataType.ARRAY) {
|
|
852
|
-
const newState = state.data.filter(item => item.id !== data.id)
|
|
853
|
-
return { ...state, ...{ data: newState } }
|
|
854
|
-
} else {
|
|
855
|
-
return { ...state, ...{ dataObject: null } }
|
|
856
|
-
}
|
|
857
|
-
})
|
|
858
|
-
|
|
859
|
-
private readonly updateData$ = this.updater((state: APIStateManagerData<T>, data: T) => {
|
|
860
|
-
|
|
861
|
-
if (this.dataType === DataType.ARRAY) {
|
|
862
|
-
|
|
863
|
-
const objIndex = state.data.findIndex(item => item.id === data.id)
|
|
864
|
-
|
|
865
|
-
if (objIndex > -1) {
|
|
866
|
-
const newState = [...state.data]
|
|
867
|
-
newState[objIndex] = data
|
|
868
|
-
return { ...state, ...{ data: newState } }
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
return state
|
|
872
|
-
|
|
873
|
-
} else {
|
|
874
|
-
return { ...state, ...{ dataObject: data } }
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
})
|
|
878
|
-
|
|
879
|
-
// --------------------------------------------------------------------------------------------------
|
|
880
|
-
// EFFECTS
|
|
881
|
-
readonly clearRecords = this.effect(data =>
|
|
882
|
-
data.pipe(
|
|
883
|
-
|
|
884
|
-
tap(() => {
|
|
885
|
-
|
|
886
|
-
if (this.dataType === DataType.ARRAY) {
|
|
887
|
-
this.setData$([])
|
|
888
|
-
} else {
|
|
889
|
-
this.setData$({})
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
}),
|
|
893
|
-
concatMap(() => {
|
|
894
|
-
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
895
|
-
|
|
896
|
-
this.clearRequestCacheMetadata(this.databaseOptions.table)
|
|
897
|
-
|
|
898
|
-
return this.dbManagerService.clearTable(this.databaseOptions.table);
|
|
899
|
-
|
|
900
|
-
}
|
|
901
|
-
return of(null);
|
|
902
|
-
})
|
|
903
|
-
))
|
|
904
|
-
|
|
905
|
-
// --------------------------------------------------------------------------------------------------
|
|
906
|
-
// CRUD OPERATIONS
|
|
907
|
-
|
|
908
|
-
// FETCH RECORDS
|
|
909
|
-
readonly fetchRecords = (options?: RequestOptions) =>
|
|
910
|
-
this.effect<any>(() =>
|
|
911
|
-
of(RequestOptions.adapt(options)).pipe(
|
|
912
|
-
switchMap(() => {
|
|
913
|
-
|
|
914
|
-
this.streamedResponse = []
|
|
915
|
-
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
916
|
-
const effectiveParams = this.getEffectiveParams(options?.path)
|
|
917
|
-
const requestSignature = this.buildRequestSignature('GET', requestOptions, effectiveParams)
|
|
918
|
-
|
|
919
|
-
const fetchFromAPI = () => {
|
|
920
|
-
|
|
921
|
-
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
922
|
-
this.setCachedRequestSignature(this.databaseOptions.table, 'GET', requestSignature)
|
|
923
|
-
|
|
924
|
-
if (!this.tryBeginInFlightRequest(requestSignature)) {
|
|
925
|
-
const currentStateData = this.get()?.data
|
|
926
|
-
if (Array.isArray(currentStateData) && currentStateData.length > 0) {
|
|
927
|
-
return of(currentStateData)
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
if (
|
|
931
|
-
currentStateData &&
|
|
932
|
-
!Array.isArray(currentStateData) &&
|
|
933
|
-
Object.keys(currentStateData).length > 0
|
|
934
|
-
) {
|
|
935
|
-
return of(currentStateData)
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
return of(this.dataType === DataType.ARRAY ? [] : {})
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
return this.httpManagerService.getRequest(requestOptions, effectiveParams).pipe(
|
|
943
|
-
tap((data: any) => {
|
|
944
|
-
// Extract array from paginated response if needed
|
|
945
|
-
const arrayData = (data?.results && Array.isArray(data.results)) ? data.results : data
|
|
946
|
-
data = (!arrayData) ? (this.dataType === DataType.ARRAY) ? [] : {} : arrayData
|
|
947
|
-
this.setData$(data)
|
|
948
|
-
}),
|
|
949
|
-
concatMap((data: any) => {
|
|
950
|
-
// Extract array from paginated response for database storage
|
|
951
|
-
const dbData = (data?.results && Array.isArray(data.results)) ? data.results : data
|
|
952
|
-
if (this.hasDatabase && this.databaseOptions?.table && Array.isArray(dbData) && dbData.length > 0) {
|
|
953
|
-
const tableName = this.databaseOptions.table
|
|
954
|
-
|
|
955
|
-
const schema = this.buildSchemaFromSample(dbData[0]);
|
|
956
|
-
const tableDef = TableSchemaDef.adapt({ table: tableName, schema });
|
|
957
|
-
const schemaSignature = this.buildSchemaSignature(schema)
|
|
958
|
-
|
|
959
|
-
// Always ensure table exists immediately before writing to avoid stale schema/store races.
|
|
960
|
-
return this.localStorageManagerService.store$(tableName).pipe(
|
|
961
|
-
take(1),
|
|
962
|
-
switchMap((storeData: any) => {
|
|
963
|
-
this.localStorageManagerService.updateStore({
|
|
964
|
-
name: tableName,
|
|
965
|
-
data: { ...(storeData || {}), ...this.databaseOptions, expires: this.utils.expires(this.databaseOptions!.expiresIn) }
|
|
966
|
-
})
|
|
967
|
-
|
|
968
|
-
const ensureTable$ = this.dbManagerService.createDatabaseTable(tableDef)
|
|
969
|
-
|
|
970
|
-
return ensureTable$.pipe(
|
|
971
|
-
switchMap((created) => {
|
|
972
|
-
if (!created) {
|
|
973
|
-
console.warn('[DB STORAGE] Table create/open not ready, skipping DB write for this payload:', { table: tableName });
|
|
974
|
-
return of(data);
|
|
975
|
-
}
|
|
976
|
-
return this.dbManagerService.createTableRecords(tableName, dbData).pipe(
|
|
977
|
-
tap(() => this.saveRequestCacheMetadata(tableName, 'GET', requestSignature, schemaSignature, options))
|
|
978
|
-
);
|
|
979
|
-
})
|
|
980
|
-
)
|
|
981
|
-
}),
|
|
982
|
-
catchError((error) => {
|
|
983
|
-
console.error('[DB STORAGE] Failed to ensure table and write records:', { table: tableName, schema, error });
|
|
984
|
-
return of(data);
|
|
985
|
-
})
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
return of(data);
|
|
989
|
-
}),
|
|
990
|
-
finalize(() => {
|
|
991
|
-
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
992
|
-
this.endInFlightRequest(requestSignature)
|
|
993
|
-
}
|
|
994
|
-
})
|
|
995
|
-
);
|
|
996
|
-
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// console.log('[DB STORAGE] Checking database storage:', {
|
|
1000
|
-
// hasDatabase: this.hasDatabase,
|
|
1001
|
-
// table: this.databaseOptions?.table,
|
|
1002
|
-
// databaseOptions: this.databaseOptions
|
|
1003
|
-
// })
|
|
1004
|
-
|
|
1005
|
-
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
1006
|
-
|
|
1007
|
-
return this.dbManagerService.databaseExists().pipe(
|
|
1008
|
-
switchMap((dbExists: boolean) => {
|
|
1009
|
-
|
|
1010
|
-
if (!dbExists) {
|
|
1011
|
-
const initObs: Observable<any> = this.initDBStorageAsync();
|
|
1012
|
-
return initObs.pipe(
|
|
1013
|
-
switchMap(() => fetchFromAPI()),
|
|
1014
|
-
catchError((error) => {
|
|
1015
|
-
console.warn('[DB STORAGE] initDBStorageAsync failed when DB did not exist, continuing with API:', error);
|
|
1016
|
-
return fetchFromAPI();
|
|
1017
|
-
})
|
|
1018
|
-
);
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
return this.dbManagerService.hasDatabaseTable(this.databaseOptions!.table).pipe(
|
|
1022
|
-
switchMap((tableExists: boolean): Observable<any> => {
|
|
1023
|
-
|
|
1024
|
-
if (!tableExists) {
|
|
1025
|
-
const initObs: Observable<any> = this.initDBStorageAsync();
|
|
1026
|
-
return initObs.pipe(
|
|
1027
|
-
switchMap(() => fetchFromAPI()),
|
|
1028
|
-
catchError((error) => {
|
|
1029
|
-
console.warn('[DB STORAGE] initDBStorageAsync failed when table missing, continuing with API:', error);
|
|
1030
|
-
return fetchFromAPI();
|
|
1031
|
-
})
|
|
1032
|
-
);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
return this.localStorageManagerService.store$(this.databaseOptions!.table).pipe(
|
|
1036
|
-
take(1),
|
|
1037
|
-
switchMap((storeData: any) => {
|
|
1038
|
-
// console.log('[CacheDebug] storeData for table:', this.databaseOptions!.table, { storeData, requestCache: storeData?.requestCache, expires: storeData?.expires })
|
|
1039
|
-
|
|
1040
|
-
const forceRefresh = !!(options as any)?.forceRefresh
|
|
1041
|
-
const storedSchemaSignature = this.getStoredSchemaSignature(storeData)
|
|
1042
|
-
|
|
1043
|
-
const expires = storeData?.expires || 0;
|
|
1044
|
-
const hasExpired = expires > 0 && this.utils.hasExpired(expires);
|
|
1045
|
-
|
|
1046
|
-
if (forceRefresh) {
|
|
1047
|
-
return fetchFromAPI();
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
if (hasExpired) {
|
|
1051
|
-
return this.dbManagerService.clearTable(this.databaseOptions!.table).pipe(
|
|
1052
|
-
switchMap(() => fetchFromAPI())
|
|
1053
|
-
);
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
const expectedSchema = this.buildSchemaFromAdapter()
|
|
1057
|
-
const expectedSchemaSignature = this.buildSchemaSignature(expectedSchema)
|
|
1058
|
-
// console.log('[CacheDebug] schema check:', { tableName: this.databaseOptions!.table, storedSchemaSignature, expectedSchemaSignature, mismatch: storedSchemaSignature && storedSchemaSignature !== expectedSchemaSignature })
|
|
1059
|
-
if (storedSchemaSignature && storedSchemaSignature !== expectedSchemaSignature) {
|
|
1060
|
-
const tableDef = TableSchemaDef.adapt({ table: this.databaseOptions!.table, schema: expectedSchema })
|
|
1061
|
-
return this.dbManagerService.clearTable(this.databaseOptions!.table).pipe(
|
|
1062
|
-
switchMap(() => this.dbManagerService.createDatabaseTable(tableDef)),
|
|
1063
|
-
switchMap(() => fetchFromAPI())
|
|
1064
|
-
)
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
return this.checkTrackerAllowsRequest(
|
|
1068
|
-
this.databaseOptions!.table,
|
|
1069
|
-
'GET',
|
|
1070
|
-
this.resolvePath(effectiveParams),
|
|
1071
|
-
options,
|
|
1072
|
-
storeData
|
|
1073
|
-
).pipe(
|
|
1074
|
-
switchMap((trackerAllowsRequest: boolean) => {
|
|
1075
|
-
if (trackerAllowsRequest) {
|
|
1076
|
-
const normalizedPath = this.trackerNormalizePath(this.resolvePath(effectiveParams));
|
|
1077
|
-
|
|
1078
|
-
if (!normalizedPath.hasQuery) {
|
|
1079
|
-
return this.dbManagerService.getTableRecords(this.databaseOptions!.table).pipe(
|
|
1080
|
-
switchMap((dbData: any) => {
|
|
1081
|
-
if (Array.isArray(dbData) && dbData.length > 0) {
|
|
1082
|
-
this.setData$(dbData);
|
|
1083
|
-
// Save cache metadata for both GET and STREAM request types so
|
|
1084
|
-
// tracker/check logic can detect cached responses regardless
|
|
1085
|
-
// of whether a subsequent call is a normal GET or a streaming GET.
|
|
1086
|
-
const getSignature = requestSignature;
|
|
1087
|
-
const streamRequestOptions = { ...(requestOptions || {}), stream: true } as any;
|
|
1088
|
-
const streamSignature = this.buildRequestSignature('STREAM', streamRequestOptions, effectiveParams);
|
|
1089
|
-
this.saveRequestCacheMetadata(
|
|
1090
|
-
this.databaseOptions!.table,
|
|
1091
|
-
'GET',
|
|
1092
|
-
getSignature,
|
|
1093
|
-
storedSchemaSignature ?? expectedSchemaSignature ?? undefined,
|
|
1094
|
-
options
|
|
1095
|
-
);
|
|
1096
|
-
this.saveRequestCacheMetadata(
|
|
1097
|
-
this.databaseOptions!.table,
|
|
1098
|
-
'STREAM',
|
|
1099
|
-
streamSignature,
|
|
1100
|
-
storedSchemaSignature ?? expectedSchemaSignature ?? undefined,
|
|
1101
|
-
options
|
|
1102
|
-
);
|
|
1103
|
-
return of({ data: dbData, fromCache: true });
|
|
1104
|
-
}
|
|
1105
|
-
return fetchFromAPI();
|
|
1106
|
-
}),
|
|
1107
|
-
catchError((error) => {
|
|
1108
|
-
const tableName = this.databaseOptions!.table;
|
|
1109
|
-
console.warn('[DB STORAGE] fallback cache read failed, falling back to API:', { table: tableName, error });
|
|
1110
|
-
return fetchFromAPI();
|
|
1111
|
-
})
|
|
1112
|
-
);
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
return fetchFromAPI();
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
return this.dbManagerService.getTableRecords(this.databaseOptions!.table).pipe(
|
|
1119
|
-
switchMap((dbData: any) => {
|
|
1120
|
-
|
|
1121
|
-
if (Array.isArray(dbData) && dbData.length > 0) {
|
|
1122
|
-
this.setData$(dbData);
|
|
1123
|
-
this.saveRequestCacheMetadata(
|
|
1124
|
-
this.databaseOptions!.table,
|
|
1125
|
-
'GET',
|
|
1126
|
-
requestSignature,
|
|
1127
|
-
storedSchemaSignature ?? expectedSchemaSignature ?? undefined,
|
|
1128
|
-
options
|
|
1129
|
-
);
|
|
1130
|
-
return of(dbData);
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
return fetchFromAPI();
|
|
1134
|
-
|
|
1135
|
-
})
|
|
1136
|
-
,
|
|
1137
|
-
catchError((error) => {
|
|
1138
|
-
const tableName = this.databaseOptions!.table;
|
|
1139
|
-
console.warn('[DB STORAGE] getTableRecords failed, recreating table and falling back to API:', { table: tableName, error });
|
|
1140
|
-
const schema = this.buildSchemaFromAdapter();
|
|
1141
|
-
const tableDef = TableSchemaDef.adapt({ table: tableName, schema });
|
|
1142
|
-
return this.dbManagerService.createDatabaseTable(tableDef).pipe(
|
|
1143
|
-
switchMap(() => fetchFromAPI()),
|
|
1144
|
-
catchError((recreateError) => {
|
|
1145
|
-
console.error('[DB STORAGE] Failed to recreate table after read error, continuing with API only:', recreateError);
|
|
1146
|
-
return fetchFromAPI();
|
|
1147
|
-
})
|
|
1148
|
-
);
|
|
1149
|
-
})
|
|
1150
|
-
);
|
|
1151
|
-
})
|
|
1152
|
-
);
|
|
1153
|
-
})
|
|
1154
|
-
);
|
|
1155
|
-
|
|
1156
|
-
})
|
|
1157
|
-
);
|
|
1158
|
-
})
|
|
1159
|
-
);
|
|
1160
|
-
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
return fetchFromAPI();
|
|
1164
|
-
|
|
1165
|
-
})
|
|
1166
|
-
)
|
|
1167
|
-
)
|
|
1168
|
-
|
|
1169
|
-
private initDBStorageAsync(): Observable<any> {
|
|
1170
|
-
|
|
1171
|
-
console.log('[initDBStorageAsync] Starting initialization:', {
|
|
1172
|
-
dataType: this.dataType,
|
|
1173
|
-
hasAdapter: !!this.apiOptions?.adapter,
|
|
1174
|
-
table: this.databaseOptions?.table,
|
|
1175
|
-
databaseOptions: this.databaseOptions
|
|
1176
|
-
})
|
|
1177
|
-
|
|
1178
|
-
if (this.dataType !== DataType.ARRAY) {
|
|
1179
|
-
console.warn('Database storage requires dataType to be ARRAY');
|
|
1180
|
-
return of(null);
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
if (!this.apiOptions.adapter) {
|
|
1184
|
-
console.warn('Database storage adapter missing, using minimal or inferred schema');
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
if (!this.databaseOptions?.table) {
|
|
1188
|
-
console.warn('Database storage requires a table name');
|
|
1189
|
-
return of(null);
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
const schema = this.buildSchemaFromAdapter();
|
|
1193
|
-
|
|
1194
|
-
const tableDef = TableSchemaDef.adapt({
|
|
1195
|
-
table: this.databaseOptions?.table,
|
|
1196
|
-
schema: schema
|
|
1197
|
-
});
|
|
1198
|
-
|
|
1199
|
-
return this.dbManagerService.createDatabaseTable(tableDef).pipe(
|
|
1200
|
-
tap((created) => {
|
|
1201
|
-
if (created && this.databaseOptions?.table) {
|
|
1202
|
-
this.saveSchemaSignature(this.databaseOptions.table, this.buildSchemaSignature(schema))
|
|
1203
|
-
}
|
|
1204
|
-
})
|
|
1205
|
-
);
|
|
1206
|
-
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
private buildSchemaFromAdapter(): string {
|
|
1210
|
-
const sampleData = this.apiOptions.adapter?.({}) || {};
|
|
1211
|
-
return this.buildSchemaFromSample(sampleData);
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
private buildSchemaFromSample(sampleData: any): string {
|
|
1215
|
-
const schemaKeys = Object.keys(sampleData || {}).filter(key => sampleData[key] !== undefined);
|
|
1216
|
-
let schema = '++id';
|
|
1217
|
-
|
|
1218
|
-
if (schemaKeys.length > 0) {
|
|
1219
|
-
const otherKeys = schemaKeys.filter((k) => k !== 'id').sort();
|
|
1220
|
-
if (otherKeys.length > 0) {
|
|
1221
|
-
schema += ', ' + otherKeys.join(', ');
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
return schema;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
private persistStreamDataToDb(payload: any, streamOptions?: RequestOptions): Observable<any> {
|
|
1229
|
-
if (!this.hasDatabase || !this.databaseOptions?.table) {
|
|
1230
|
-
return of(payload);
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
const dbData = (payload?.results && Array.isArray(payload.results))
|
|
1234
|
-
? payload.results
|
|
1235
|
-
: Array.isArray(payload)
|
|
1236
|
-
? payload
|
|
1237
|
-
: payload
|
|
1238
|
-
? [payload]
|
|
1239
|
-
: [];
|
|
1240
|
-
|
|
1241
|
-
if (dbData.length === 0) {
|
|
1242
|
-
return of(payload);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
const tableName = this.databaseOptions.table;
|
|
1246
|
-
const requestOptions = this.updateRequestOptions(streamOptions?.headers)
|
|
1247
|
-
requestOptions.stream = true
|
|
1248
|
-
const effectiveParams = this.getEffectiveParams(streamOptions?.path)
|
|
1249
|
-
const requestSignature = this.buildRequestSignature('STREAM', requestOptions, effectiveParams)
|
|
1250
|
-
|
|
1251
|
-
const schema = this.buildSchemaFromSample(dbData[0]);
|
|
1252
|
-
const schemaSignature = this.buildSchemaSignature(schema)
|
|
1253
|
-
const tableDef = TableSchemaDef.adapt({ table: tableName, schema });
|
|
1254
|
-
|
|
1255
|
-
return this.localStorageManagerService.store$(tableName).pipe(
|
|
1256
|
-
take(1),
|
|
1257
|
-
switchMap((storeData: any) => {
|
|
1258
|
-
this.localStorageManagerService.updateStore({
|
|
1259
|
-
name: tableName,
|
|
1260
|
-
data: { ...(storeData || {}), ...this.databaseOptions, expires: this.utils.expires(this.databaseOptions!.expiresIn) }
|
|
1261
|
-
});
|
|
1262
|
-
|
|
1263
|
-
const ensureTable$ = this.dbManagerService.createDatabaseTable(tableDef)
|
|
1264
|
-
|
|
1265
|
-
return ensureTable$.pipe(
|
|
1266
|
-
switchMap((created) => {
|
|
1267
|
-
if (!created) {
|
|
1268
|
-
console.warn('[DB STORAGE] Stream table create/open not ready, skipping DB write for this chunk:', { table: tableName });
|
|
1269
|
-
return of(payload);
|
|
1270
|
-
}
|
|
1271
|
-
return this.dbManagerService.createTableRecords(tableName, dbData).pipe(
|
|
1272
|
-
tap(() => this.saveRequestCacheMetadata(tableName, 'STREAM', requestSignature, schemaSignature, streamOptions)),
|
|
1273
|
-
map(() => payload)
|
|
1274
|
-
);
|
|
1275
|
-
})
|
|
1276
|
-
)
|
|
1277
|
-
}),
|
|
1278
|
-
map(() => payload),
|
|
1279
|
-
catchError((error) => {
|
|
1280
|
-
console.error('[DB STORAGE] Failed to persist streaming payload:', { table: tableName, schema, error });
|
|
1281
|
-
return of(payload);
|
|
1282
|
-
})
|
|
1283
|
-
);
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// FETCH RECORD
|
|
1287
|
-
readonly fetchRecord = (options: RequestOptions, method: string) =>
|
|
1288
|
-
this.effect<any>(() =>
|
|
1289
|
-
of(RequestOptions.adapt(options)).pipe(
|
|
1290
|
-
tap(() => console.log('🔄 fetchRecord effect triggered with path:', options?.path, 'method:', method)),
|
|
1291
|
-
switchMap((options) => {
|
|
1292
|
-
|
|
1293
|
-
this.streamedResponse = []
|
|
1294
|
-
|
|
1295
|
-
// Temporarily update apiOptions.path with the path from the WebSocket message
|
|
1296
|
-
// This ensures the request goes to the correct endpoint
|
|
1297
|
-
const originalPath = this.apiOptions.path;
|
|
1298
|
-
if (options?.path && Array.isArray(options.path)) {
|
|
1299
|
-
this.apiOptions.path = options.path;
|
|
1300
|
-
console.log('🔧 Temporarily set apiOptions.path to:', options.path);
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
1304
|
-
|
|
1305
|
-
console.log('🌐 Making GET request to path:', this.apiOptions.path)
|
|
1306
|
-
return this.httpManagerService.getRequest(requestOptions)
|
|
1307
|
-
.pipe(
|
|
1308
|
-
tap((data: any) => {
|
|
1309
|
-
console.log('📦 fetchRecord received data:', data)
|
|
1310
|
-
|
|
1311
|
-
// Restore original path after request completes
|
|
1312
|
-
this.apiOptions.path = originalPath;
|
|
1313
|
-
console.log('🔧 Restored apiOptions.path to:', originalPath);
|
|
1314
|
-
|
|
1315
|
-
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data
|
|
1316
|
-
|
|
1317
|
-
const id = (options as any).path?.length ? options.path[options.path.length -1] : null
|
|
1318
|
-
|
|
1319
|
-
if(method === 'DELETE') {
|
|
1320
|
-
console.log('🗑️ Deleting record with id:', id)
|
|
1321
|
-
this.deleteData$({ id } as T & { id: number })
|
|
1322
|
-
}
|
|
1323
|
-
if(method === 'UPDATE') {
|
|
1324
|
-
console.log('✏️ Updating record:', data)
|
|
1325
|
-
this.updateData$(data)
|
|
1326
|
-
}
|
|
1327
|
-
if(method === 'CREATE') {
|
|
1328
|
-
console.log('➕ Adding record:', data)
|
|
1329
|
-
this.addData$(data)
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
}),
|
|
1333
|
-
concatMap((data: any) => {
|
|
1334
|
-
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
1335
|
-
const id = (options as any).path?.length ? options.path[options.path.length -1] : null
|
|
1336
|
-
|
|
1337
|
-
if(method === 'DELETE' && id) return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, id)
|
|
1338
|
-
if(method === 'UPDATE' && data) return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data)
|
|
1339
|
-
if(method === 'CREATE' && data) {
|
|
1340
|
-
// Validate that data has a valid id before saving to IndexedDB
|
|
1341
|
-
if (data && (data.id !== undefined && data.id !== null && data.id !== '')) {
|
|
1342
|
-
console.log('💾 Saving to IndexedDB:', { table: this.databaseOptions.table, id: data.id, data });
|
|
1343
|
-
return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
|
|
1344
|
-
} else {
|
|
1345
|
-
console.warn('⚠️ Skipping IndexedDB save: data.id is invalid', data);
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
return of(data)
|
|
1350
|
-
})
|
|
1351
|
-
)
|
|
1352
|
-
|
|
1353
|
-
})
|
|
1354
|
-
)
|
|
1355
|
-
)
|
|
1356
|
-
|
|
1357
|
-
// CREATE RECORD
|
|
1358
|
-
readonly createRecord = (data: any|null, options?: RequestOptions) =>
|
|
1359
|
-
this.effect<any>(() =>
|
|
1360
|
-
of(data).pipe(
|
|
1361
|
-
switchMap((data: any) => {
|
|
1362
|
-
|
|
1363
|
-
this.streamedResponse = []
|
|
1364
|
-
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
1365
|
-
|
|
1366
|
-
return this.httpManagerService.postRequest(data, requestOptions, options?.path)
|
|
1367
|
-
.pipe(
|
|
1368
|
-
tap((data: any) => {
|
|
1369
|
-
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data
|
|
1370
|
-
this.addData$(data)
|
|
1371
|
-
this.operationSuccess.next(OperationResultModel.adapt({ success: true, operation: 'CREATE' }))
|
|
1372
|
-
// Always call wsCommunication - it will queue if not connected
|
|
1373
|
-
this.wsCommunication('CREATE', [...options?.path || [], data.id])
|
|
1374
|
-
}),
|
|
1375
|
-
concatMap((data: any) => {
|
|
1376
|
-
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
1377
|
-
return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
|
|
1378
|
-
}
|
|
1379
|
-
return of(data);
|
|
1380
|
-
})
|
|
1381
|
-
)
|
|
1382
|
-
|
|
1383
|
-
})
|
|
1384
|
-
)
|
|
1385
|
-
)
|
|
1386
|
-
|
|
1387
|
-
// UPDATE RECORD
|
|
1388
|
-
readonly updateRecord = (data: any|null, options?: RequestOptions) =>
|
|
1389
|
-
this.effect<any>(() =>
|
|
1390
|
-
of(data).pipe(
|
|
1391
|
-
concatMap((data: any) => {
|
|
1392
|
-
|
|
1393
|
-
this.streamedResponse = []
|
|
1394
|
-
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
1395
|
-
|
|
1396
|
-
return this.httpManagerService.putRequest(data, requestOptions, options?.path)
|
|
1397
|
-
.pipe(
|
|
1398
|
-
tap((data: any) => {
|
|
1399
|
-
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data
|
|
1400
|
-
this.updateData$(data)
|
|
1401
|
-
this.operationSuccess.next(OperationResultModel.adapt({ success: true, operation: 'UPDATE' }))
|
|
1402
|
-
// Always call wsCommunication - it will queue if not connected
|
|
1403
|
-
this.wsCommunication('UPDATE', [...options?.path || []])
|
|
1404
|
-
}),
|
|
1405
|
-
concatMap((data: any) => {
|
|
1406
|
-
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
1407
|
-
return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
|
|
1408
|
-
}
|
|
1409
|
-
return of(data);
|
|
1410
|
-
})
|
|
1411
|
-
)
|
|
1412
|
-
|
|
1413
|
-
})
|
|
1414
|
-
)
|
|
1415
|
-
)
|
|
1416
|
-
|
|
1417
|
-
// DELETE RECORD
|
|
1418
|
-
readonly deleteRecord = (options?: RequestOptions) =>
|
|
1419
|
-
this.effect<any>(() =>
|
|
1420
|
-
of(options).pipe(
|
|
1421
|
-
concatMap((data: any) => {
|
|
1422
|
-
|
|
1423
|
-
this.streamedResponse = []
|
|
1424
|
-
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
1425
|
-
|
|
1426
|
-
return this.httpManagerService.deleteRequest(requestOptions, options?.path)
|
|
1427
|
-
.pipe(
|
|
1428
|
-
tap((data: any) => {
|
|
1429
|
-
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data
|
|
1430
|
-
this.deleteData$(data)
|
|
1431
|
-
this.operationSuccess.next(OperationResultModel.adapt({ success: true, operation: 'DELETE' }))
|
|
1432
|
-
// Always call wsCommunication - it will queue if not connected
|
|
1433
|
-
this.wsCommunication('DELETE', [...options?.path || []])
|
|
1434
|
-
}),
|
|
1435
|
-
concatMap((data: any) => {
|
|
1436
|
-
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
1437
|
-
return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, data.id);
|
|
1438
|
-
}
|
|
1439
|
-
return of(data);
|
|
1440
|
-
})
|
|
1441
|
-
)
|
|
1442
|
-
|
|
1443
|
-
})
|
|
1444
|
-
)
|
|
1445
|
-
)
|
|
1446
|
-
|
|
1447
|
-
// --------------------------------------------------------------------------------------------------
|
|
1448
|
-
// FETCH STREAM
|
|
1449
|
-
readonly createStream = (data: any|null, options?: RequestOptions) =>
|
|
1450
|
-
this.effect<any>(() =>
|
|
1451
|
-
of(data).pipe(
|
|
1452
|
-
tap(() => this.httpManagerService.isPending.next(true)),
|
|
1453
|
-
switchMap((data: any) => {
|
|
1454
|
-
|
|
1455
|
-
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
1456
|
-
|
|
1457
|
-
return this.httpManagerService.postRequest(data, requestOptions, options?.path)
|
|
1458
|
-
.pipe(
|
|
1459
|
-
tap((res: any) => {
|
|
1460
|
-
if(res.length > 0) this.setData$(res)
|
|
1461
|
-
this.streamedResponse = res
|
|
1462
|
-
}),
|
|
1463
|
-
concatMap((res: any) => this.persistStreamDataToDb(res, options)),
|
|
1464
|
-
scan((acc, res: any) => {
|
|
1465
|
-
|
|
1466
|
-
const previous = acc.current
|
|
1467
|
-
const current = res
|
|
1468
|
-
return { previous, current };
|
|
1469
|
-
|
|
1470
|
-
}, { previous: null, current: null }),
|
|
1471
|
-
tap(({ previous, current }) => {
|
|
1472
|
-
|
|
1473
|
-
if(previous && JSON.stringify(previous) === JSON.stringify(current)) {
|
|
1474
|
-
this.httpManagerService.isPending.next(false)
|
|
1475
|
-
this.setData$([])
|
|
1476
|
-
} else {
|
|
1477
|
-
this.httpManagerService.isPending.next(true)
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
})
|
|
1481
|
-
)
|
|
1482
|
-
|
|
1483
|
-
})
|
|
1484
|
-
)
|
|
1485
|
-
|
|
1486
|
-
)
|
|
1487
|
-
|
|
1488
|
-
readonly fetchStream = (options?: RequestOptions) =>
|
|
1489
|
-
this.effect<any>(() =>
|
|
1490
|
-
of(options).pipe(
|
|
1491
|
-
tap(() => {
|
|
1492
|
-
// console.log('[DEBUG] fetchStream called')
|
|
1493
|
-
this.streamedResponse = []
|
|
1494
|
-
this.httpManagerService.isPending.next(true)
|
|
1495
|
-
}),
|
|
1496
|
-
switchMap((options: any) => {
|
|
1497
|
-
|
|
1498
|
-
const requestOptions = this.updateRequestOptions(options?.headers)
|
|
1499
|
-
requestOptions.stream = true
|
|
1500
|
-
const effectiveParams = this.getEffectiveParams(options?.path)
|
|
1501
|
-
|
|
1502
|
-
// console.log('[DEBUG] Making streaming request:', requestOptions)
|
|
1503
|
-
|
|
1504
|
-
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
1505
|
-
const requestSignature = this.buildRequestSignature('STREAM', requestOptions, effectiveParams)
|
|
1506
|
-
const fetchStreamFromAPI = () => {
|
|
1507
|
-
if (!this.tryBeginInFlightRequest(requestSignature)) {
|
|
1508
|
-
const currentStateData = this.get()?.data
|
|
1509
|
-
if (Array.isArray(currentStateData) && currentStateData.length > 0) {
|
|
1510
|
-
return of({ data: currentStateData, fromCache: true })
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
if (
|
|
1514
|
-
currentStateData &&
|
|
1515
|
-
!Array.isArray(currentStateData) &&
|
|
1516
|
-
Object.keys(currentStateData).length > 0
|
|
1517
|
-
) {
|
|
1518
|
-
return of({ data: currentStateData, fromCache: true })
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
return of({ data: this.dataType === DataType.ARRAY ? [] : {}, fromCache: true })
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
this.setCachedRequestSignature(this.databaseOptions!.table, 'STREAM', requestSignature)
|
|
1525
|
-
return this.httpManagerService.getRequest(requestOptions, effectiveParams).pipe(
|
|
1526
|
-
map((apiData: any) => ({ data: apiData, fromCache: false })),
|
|
1527
|
-
finalize(() => this.endInFlightRequest(requestSignature))
|
|
1528
|
-
)
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
return this.localStorageManagerService.store$(this.databaseOptions.table).pipe(
|
|
1532
|
-
take(1),
|
|
1533
|
-
switchMap((storeData: any) => {
|
|
1534
|
-
const forceRefresh = !!(options as any)?.forceRefresh
|
|
1535
|
-
const expires = storeData?.expires || 0
|
|
1536
|
-
const hasExpired = expires > 0 && this.utils.hasExpired(expires)
|
|
1537
|
-
|
|
1538
|
-
if (forceRefresh) {
|
|
1539
|
-
return fetchStreamFromAPI()
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
if (hasExpired) {
|
|
1543
|
-
return this.dbManagerService.clearTable(this.databaseOptions!.table).pipe(
|
|
1544
|
-
switchMap(() => fetchStreamFromAPI())
|
|
1545
|
-
)
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
const storedSchemaSignature = this.getStoredSchemaSignature(storeData)
|
|
1549
|
-
const expectedSchema = this.buildSchemaFromAdapter()
|
|
1550
|
-
const expectedSchemaSignature = this.buildSchemaSignature(expectedSchema)
|
|
1551
|
-
if (storedSchemaSignature && storedSchemaSignature !== expectedSchemaSignature) {
|
|
1552
|
-
const tableDef = TableSchemaDef.adapt({ table: this.databaseOptions!.table, schema: expectedSchema })
|
|
1553
|
-
return this.dbManagerService.clearTable(this.databaseOptions!.table).pipe(
|
|
1554
|
-
switchMap(() => this.dbManagerService.createDatabaseTable(tableDef)),
|
|
1555
|
-
switchMap(() => fetchStreamFromAPI())
|
|
1556
|
-
)
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
return this.checkTrackerAllowsRequest(
|
|
1560
|
-
this.databaseOptions!.table,
|
|
1561
|
-
'STREAM',
|
|
1562
|
-
this.resolvePath(effectiveParams),
|
|
1563
|
-
options,
|
|
1564
|
-
storeData
|
|
1565
|
-
).pipe(
|
|
1566
|
-
switchMap((trackerAllowsRequest: boolean) => {
|
|
1567
|
-
if (!trackerAllowsRequest) {
|
|
1568
|
-
return this.dbManagerService.getTableRecords(this.databaseOptions!.table).pipe(
|
|
1569
|
-
switchMap((dbData: any) => {
|
|
1570
|
-
if (Array.isArray(dbData) && dbData.length > 0) {
|
|
1571
|
-
return of({ data: dbData, fromCache: true })
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
const currentStateData = this.get()?.data
|
|
1575
|
-
if (Array.isArray(currentStateData) && currentStateData.length > 0) {
|
|
1576
|
-
return of({ data: currentStateData, fromCache: true })
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
if (
|
|
1580
|
-
currentStateData &&
|
|
1581
|
-
!Array.isArray(currentStateData) &&
|
|
1582
|
-
Object.keys(currentStateData).length > 0
|
|
1583
|
-
) {
|
|
1584
|
-
return of({ data: currentStateData, fromCache: true })
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
return of({ data: this.dataType === DataType.ARRAY ? [] : {}, fromCache: true })
|
|
1588
|
-
})
|
|
1589
|
-
)
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
return fetchStreamFromAPI()
|
|
1593
|
-
})
|
|
1594
|
-
)
|
|
1595
|
-
})
|
|
1596
|
-
).pipe(
|
|
1597
|
-
tap((packet: any) => {
|
|
1598
|
-
const res = packet?.data
|
|
1599
|
-
// console.log('[DEBUG] Streaming response received:', res)
|
|
1600
|
-
|
|
1601
|
-
if (res && res.length > 0) {
|
|
1602
|
-
// console.log('[DEBUG] Updating state with streaming data:', res)
|
|
1603
|
-
this.setData$(res)
|
|
1604
|
-
this.streamedResponse = [...this.streamedResponse, ...res]
|
|
1605
|
-
}
|
|
1606
|
-
}),
|
|
1607
|
-
concatMap((packet: any) => {
|
|
1608
|
-
if (packet?.fromCache) {
|
|
1609
|
-
return of(packet?.data)
|
|
1610
|
-
}
|
|
1611
|
-
return this.persistStreamDataToDb(packet?.data, options)
|
|
1612
|
-
}),
|
|
1613
|
-
map((res: any) => {
|
|
1614
|
-
// console.log('[DEBUG] Returning data to subscribers:', res)
|
|
1615
|
-
return res
|
|
1616
|
-
}),
|
|
1617
|
-
catchError((error) => {
|
|
1618
|
-
console.error('[DEBUG] Streaming error:', error)
|
|
1619
|
-
return of([])
|
|
1620
|
-
}),
|
|
1621
|
-
finalize(() => this.httpManagerService.isPending.next(false))
|
|
1622
|
-
)
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
// If no database is configured, always call the API (do not check tracker)
|
|
1626
|
-
return this.httpManagerService.getRequest(requestOptions, effectiveParams)
|
|
1627
|
-
.pipe(
|
|
1628
|
-
tap((res: any) => {
|
|
1629
|
-
console.log('[DEBUG] Streaming response received:', res)
|
|
1630
|
-
|
|
1631
|
-
// Always update state with streaming data
|
|
1632
|
-
if (res && res.length > 0) {
|
|
1633
|
-
// console.log('[DEBUG] Updating state with streaming data:', res)
|
|
1634
|
-
this.setData$(res)
|
|
1635
|
-
this.streamedResponse = [...this.streamedResponse, ...res]
|
|
1636
|
-
} else {
|
|
1637
|
-
// console.log('[DEBUG] No streaming data or empty array:', res)
|
|
1638
|
-
}
|
|
1639
|
-
}),
|
|
1640
|
-
concatMap((res: any) => this.persistStreamDataToDb(res, options)),
|
|
1641
|
-
map((res: any) => {
|
|
1642
|
-
// console.log('[DEBUG] Returning data to subscribers:', res)
|
|
1643
|
-
return res // Return the data so subscribers can receive it
|
|
1644
|
-
}),
|
|
1645
|
-
catchError((error) => {
|
|
1646
|
-
// console.error('[DEBUG] Streaming error:', error)
|
|
1647
|
-
return of([])
|
|
1648
|
-
}),
|
|
1649
|
-
finalize(() => this.httpManagerService.isPending.next(false))
|
|
1650
|
-
)
|
|
1651
|
-
|
|
1652
|
-
}),
|
|
1653
|
-
)
|
|
1654
|
-
)
|
|
1655
|
-
|
|
1656
|
-
// WEBSOCKET COMMUNICATION (STATE MANAGER)
|
|
1657
|
-
private wsCommunication(method: string, path?: string|number[]) {
|
|
1658
|
-
if (!this.apiOptions.ws) {
|
|
1659
|
-
console.warn('wsCommunication called but no WebSocket options configured');
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
// If connected, send immediately (check singleton WebSocketManagerService)
|
|
1664
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1665
|
-
this.sendWsCommunication(method, path);
|
|
1666
|
-
} else {
|
|
1667
|
-
// Queue the message to be sent when connection is established
|
|
1668
|
-
console.log(`⏳ Queuing WS message (not connected): ${method} ${path ? JSON.stringify(path) : ''}`);
|
|
1669
|
-
HTTPManagerStateService.wsCommunicationQueue.push({ method, path });
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
/**
|
|
1674
|
-
* Actually send the WebSocket message (called when connected or from queue)
|
|
1675
|
-
*/
|
|
1676
|
-
private sendWsCommunication(method: string, path?: string|number[]) {
|
|
1677
|
-
if (this.apiOptions.ws) {
|
|
1678
|
-
const wsServer = this.apiOptions.ws.id;
|
|
1679
|
-
|
|
1680
|
-
// Guard: Don't send if channel is empty
|
|
1681
|
-
if (!wsServer || wsServer === '') {
|
|
1682
|
-
console.error('❌ Cannot send WS message: Channel ID is empty!');
|
|
1683
|
-
return;
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
// DEBUG: Log what we're sending
|
|
1687
|
-
console.log('🔍 [DEBUG] sendWsCommunication called:', {
|
|
1688
|
-
wsServer,
|
|
1689
|
-
wsServerType: typeof wsServer,
|
|
1690
|
-
wsServerEmpty: wsServer === '' || wsServer === null || wsServer === undefined,
|
|
1691
|
-
method,
|
|
1692
|
-
path,
|
|
1693
|
-
user: this.apiOptions.ws.user,
|
|
1694
|
-
fullPayload: { method, path, user: this.apiOptions.ws.user }
|
|
1695
|
-
});
|
|
1696
|
-
|
|
1697
|
-
this.httpManagerService.sendMessageInChannel(wsServer, { method, path, user: this.apiOptions.ws.user });
|
|
1698
|
-
} else {
|
|
1699
|
-
console.error('❌ [DEBUG] sendWsCommunication: apiOptions.ws is undefined!');
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
/**
|
|
1704
|
-
* Send a message to channel(s)
|
|
1705
|
-
* @param message - The message content
|
|
1706
|
-
* @param channels - Optional array of channel names (passed as-is, caller should include prefix)
|
|
1707
|
-
* Use 'allChannels' to broadcast to all
|
|
1708
|
-
*/
|
|
1709
|
-
wsMessaging(message: ChannelMessage, channels?: string[]) {
|
|
1710
|
-
|
|
1711
|
-
const user = this.user.value
|
|
1712
|
-
const messageInfo = ChannelMessage.adapt({ ...message, fromUser: user })
|
|
1713
|
-
|
|
1714
|
-
console.log('📤 wsMessaging called with channels:', channels);
|
|
1715
|
-
|
|
1716
|
-
if (WebSocketManagerService.isConnected() && this.apiOptions.ws) {
|
|
1717
|
-
|
|
1718
|
-
// If specific channels provided, send to each channel
|
|
1719
|
-
// Channels are passed as-is - caller is responsible for including the correct prefix
|
|
1720
|
-
if (channels && channels.length > 0) {
|
|
1721
|
-
console.log(`📤 Sending to ${channels.length} channel(s):`, channels);
|
|
1722
|
-
|
|
1723
|
-
// Send single message with all channels (more efficient)
|
|
1724
|
-
const user = this.user.value;
|
|
1725
|
-
const messageData = {
|
|
1726
|
-
sessionId: user,
|
|
1727
|
-
content: messageInfo
|
|
1728
|
-
};
|
|
1729
|
-
|
|
1730
|
-
this.httpManagerService.sendChannelMessageToChannels(channels, messageData);
|
|
1731
|
-
} else {
|
|
1732
|
-
// Fallback to the primary WS channel (already prefixed with SYS-)
|
|
1733
|
-
const wsChannel = this.apiOptions.ws.id;
|
|
1734
|
-
console.log(`📤 Sending to state channel:`, wsChannel);
|
|
1735
|
-
this.httpManagerService.sendChannelMessage(wsChannel, messageInfo);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
/**
|
|
1743
|
-
* Subscribe to a messaging channel
|
|
1744
|
-
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1745
|
-
*/
|
|
1746
|
-
subscribeToMessageChannel(channel: string) {
|
|
1747
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1748
|
-
const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
|
|
1749
|
-
this.httpManagerService.subscribeToChannel(prefixedChannel);
|
|
1750
|
-
console.log(`💬 Subscribed to message channel: ${prefixedChannel}`);
|
|
1751
|
-
} else {
|
|
1752
|
-
console.warn('Cannot subscribe to message channel: WebSocket not connected.');
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
/**
|
|
1757
|
-
* Unsubscribe from a messaging channel
|
|
1758
|
-
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1759
|
-
*/
|
|
1760
|
-
unsubscribeFromMessageChannel(channel: string) {
|
|
1761
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1762
|
-
const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
|
|
1763
|
-
this.httpManagerService.unsubscribeFromChannel(prefixedChannel);
|
|
1764
|
-
console.log(`💬 Unsubscribed from message channel: ${prefixedChannel}`);
|
|
1765
|
-
} else {
|
|
1766
|
-
console.warn('Cannot unsubscribe from message channel: WebSocket not connected.');
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
// --------------------------------------------------------------------------------------------------
|
|
1771
|
-
// CHANNEL MANAGEMENT (Raw channels - no automatic prefix)
|
|
1772
|
-
|
|
1773
|
-
/**
|
|
1774
|
-
* Subscribe to a single channel (no automatic prefix)
|
|
1775
|
-
* Use subscribeToMessageChannel() for MES- prefixed channels
|
|
1776
|
-
*/
|
|
1777
|
-
subscribeToChannel(channel: string) {
|
|
1778
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1779
|
-
// Get current user data to send with subscription
|
|
1780
|
-
const currentUser = this.user.value;
|
|
1781
|
-
console.log('👤 Subscribing with user:', currentUser)
|
|
1782
|
-
this.httpManagerService.subscribeToChannel(channel, currentUser);
|
|
1783
|
-
} else {
|
|
1784
|
-
console.warn('Cannot subscribe: WebSocket not connected.');
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
/**
|
|
1789
|
-
* Subscribe to multiple channels at once
|
|
1790
|
-
*/
|
|
1791
|
-
subscribeToChannels(channels: string[]) {
|
|
1792
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1793
|
-
// Get current user data to send with subscription
|
|
1794
|
-
const currentUser = this.user.value;
|
|
1795
|
-
console.log('👤 Subscribing to', channels.length, 'channels with user:', currentUser)
|
|
1796
|
-
this.httpManagerService.subscribeToChannels(channels, currentUser);
|
|
1797
|
-
} else {
|
|
1798
|
-
console.warn('Cannot subscribe: WebSocket not connected.');
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
/**
|
|
1803
|
-
* Unsubscribe from a channel
|
|
1804
|
-
*/
|
|
1805
|
-
unsubscribeFromChannel(channel: string) {
|
|
1806
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1807
|
-
this.httpManagerService.unsubscribeFromChannel(channel);
|
|
1808
|
-
} else {
|
|
1809
|
-
console.warn('Cannot unsubscribe: WebSocket not connected.');
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
/**
|
|
1814
|
-
* Get observable of currently subscribed channels
|
|
1815
|
-
*/
|
|
1816
|
-
get subscribedChannels$(): Observable<Set<string>> {
|
|
1817
|
-
return this.httpManagerService.subscribedChannels$;
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
/**
|
|
1821
|
-
* Get current subscribed channels synchronously
|
|
1822
|
-
*/
|
|
1823
|
-
getSubscribedChannels(): Set<string> {
|
|
1824
|
-
return this.httpManagerService.getSubscribedChannels();
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
/**
|
|
1828
|
-
* Create a new channel on the server
|
|
1829
|
-
*/
|
|
1830
|
-
createChannel(channel: string) {
|
|
1831
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1832
|
-
this.httpManagerService.createChannel(channel);
|
|
1833
|
-
} else {
|
|
1834
|
-
console.warn('Cannot create channel: WebSocket not connected.');
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
/**
|
|
1839
|
-
* Delete a channel from the server
|
|
1840
|
-
*/
|
|
1841
|
-
deleteChannel(channel: string) {
|
|
1842
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1843
|
-
this.httpManagerService.deleteChannel(channel);
|
|
1844
|
-
} else {
|
|
1845
|
-
console.warn('Cannot delete channel: WebSocket not connected.');
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
/**
|
|
1850
|
-
* Request list of all channels from server
|
|
1851
|
-
*/
|
|
1852
|
-
getAllChannels() {
|
|
1853
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1854
|
-
this.httpManagerService.getAllChannels();
|
|
1855
|
-
} else {
|
|
1856
|
-
console.warn('Cannot get channels: WebSocket not connected.');
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
/**
|
|
1861
|
-
* Get users in a specific channel
|
|
1862
|
-
*/
|
|
1863
|
-
getUsersInChannel(channel: string) {
|
|
1864
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1865
|
-
this.httpManagerService.getUsersInChannel(channel);
|
|
1866
|
-
} else {
|
|
1867
|
-
console.warn('Cannot get users: WebSocket not connected.');
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
// --------------------------------------------------------------------------------------------------
|
|
1872
|
-
// NOTIFICATION CHANNELS (MES- prefix - managed automatically)
|
|
1873
|
-
|
|
1874
|
-
/**
|
|
1875
|
-
* Create a notification channel on the server
|
|
1876
|
-
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1877
|
-
*/
|
|
1878
|
-
createNotificationChannel(channel: string) {
|
|
1879
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1880
|
-
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
1881
|
-
this.httpManagerService.createNotificationChannel(prefixedChannel);
|
|
1882
|
-
console.log(`📢 Creating notification channel: ${prefixedChannel}`);
|
|
1883
|
-
} else {
|
|
1884
|
-
console.warn('Cannot create notification channel: WebSocket not connected.');
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
/**
|
|
1889
|
-
* Request list of all notification channels from server (in-memory)
|
|
1890
|
-
*/
|
|
1891
|
-
getNotificationChannels() {
|
|
1892
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1893
|
-
this.httpManagerService.getNotificationChannels();
|
|
1894
|
-
} else {
|
|
1895
|
-
console.warn('Cannot get notification channels: WebSocket not connected.');
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
/**
|
|
1900
|
-
* Request list of today's notification channels from database
|
|
1901
|
-
* Returns unique channels that have notifications posted today
|
|
1902
|
-
*/
|
|
1903
|
-
getTodaysNotificationChannels() {
|
|
1904
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1905
|
-
this.httpManagerService.getTodaysNotificationChannels();
|
|
1906
|
-
} else {
|
|
1907
|
-
console.warn('Cannot get today\'s notification channels: WebSocket not connected.');
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
/**
|
|
1912
|
-
* Subscribe to a notification channel with optional date filters
|
|
1913
|
-
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1914
|
-
*/
|
|
1915
|
-
subscribeToNotificationChannel(channel: string, options?: { startEpoch?: number; endEpoch?: number }, user?: any) {
|
|
1916
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1917
|
-
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
1918
|
-
this.httpManagerService.subscribeToNotificationChannel(prefixedChannel, options, user);
|
|
1919
|
-
console.log(`📢 Subscribing to notification channel: ${prefixedChannel}`);
|
|
1920
|
-
} else {
|
|
1921
|
-
console.warn('Cannot subscribe to notification channel: WebSocket not connected.');
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
/**
|
|
1926
|
-
* Unsubscribe from a notification channel
|
|
1927
|
-
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1928
|
-
*/
|
|
1929
|
-
unsubscribeFromNotificationChannel(channel: string) {
|
|
1930
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1931
|
-
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
1932
|
-
this.httpManagerService.unsubscribeFromNotificationChannel(prefixedChannel);
|
|
1933
|
-
console.log(`📢 Unsubscribing from notification channel: ${prefixedChannel}`);
|
|
1934
|
-
} else {
|
|
1935
|
-
console.warn('Cannot unsubscribe from notification channel: WebSocket not connected.');
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
/**
|
|
1940
|
-
* Send a notification to a channel
|
|
1941
|
-
* @param channel - Base channel name (MES- prefix added automatically)
|
|
1942
|
-
*/
|
|
1943
|
-
sendNotification(channel: string, content: any) {
|
|
1944
|
-
if (WebSocketManagerService.isConnected()) {
|
|
1945
|
-
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
1946
|
-
this.httpManagerService.sendNotification(prefixedChannel, content);
|
|
1947
|
-
console.log(`📢 Sending notification to channel: ${prefixedChannel}`);
|
|
1948
|
-
} else {
|
|
1949
|
-
console.warn('Cannot send notification: WebSocket not connected.');
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
// --------------------------------------------------------------------------------------------------
|
|
1954
|
-
// MISC
|
|
1955
|
-
|
|
1956
|
-
/**
|
|
1957
|
-
* Clear/flush all records from the database table
|
|
1958
|
-
* Does not clear localStorage metadata (expires info)
|
|
1959
|
-
* Does not re-fetch from API - leaves state empty for manual refresh
|
|
1960
|
-
*/
|
|
1961
|
-
clearDatabase() {
|
|
1962
|
-
|
|
1963
|
-
if (!this.hasDatabase || !this.databaseOptions?.table) return;
|
|
1964
|
-
|
|
1965
|
-
const tableName = this.databaseOptions.table;
|
|
1966
|
-
|
|
1967
|
-
// Reset localStorage tracking SYNCHRONOUSLY before the async DB clear so any
|
|
1968
|
-
// concurrent request that fires during clearTable() sees a clean store immediately
|
|
1969
|
-
// and is not blocked by stale queryParams from the previous session.
|
|
1970
|
-
this.localStorageManagerService.updateStore({
|
|
1971
|
-
name: tableName,
|
|
1972
|
-
data: { ...this.databaseOptions, expires: 0 }
|
|
1973
|
-
});
|
|
1974
|
-
this._requestCachePaths.delete(tableName);
|
|
1975
|
-
|
|
1976
|
-
this.dbManagerService.clearTable(tableName).subscribe({
|
|
1977
|
-
next: () => {
|
|
1978
|
-
if (this.dataType === DataType.ARRAY) {
|
|
1979
|
-
this.setData$([]);
|
|
1980
|
-
} else {
|
|
1981
|
-
this.setData$({});
|
|
1982
|
-
}
|
|
1983
|
-
},
|
|
1984
|
-
error: (err) => {
|
|
1985
|
-
console.error(`❌ Error clearing table ${tableName}:`, err);
|
|
1986
|
-
}
|
|
1987
|
-
});
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
private isEmpty(obj: any) {
|
|
1991
|
-
return Object.keys(obj).length === 0
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
private updateRequestOptions(headers?: any): ApiRequest {
|
|
1995
|
-
|
|
1996
|
-
const options = ApiRequest.adapt({ ...this.apiOptions })
|
|
1997
|
-
|
|
1998
|
-
options.headers = (headers)
|
|
1999
|
-
? { ...options.headers, ...headers }
|
|
2000
|
-
: { ...options.headers }
|
|
2001
|
-
|
|
2002
|
-
return options
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
private normalizeObject(value: any): any {
|
|
2006
|
-
if (Array.isArray(value)) {
|
|
2007
|
-
return value.map((item) => this.normalizeObject(item))
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
if (value && typeof value === 'object') {
|
|
2011
|
-
return Object.keys(value)
|
|
2012
|
-
.sort()
|
|
2013
|
-
.reduce((acc: any, key: string) => {
|
|
2014
|
-
acc[key] = this.normalizeObject(value[key])
|
|
2015
|
-
return acc
|
|
2016
|
-
}, {})
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
return value
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
private filterHeaders(headers?: any): Record<string, any> {
|
|
2023
|
-
const source = headers || {}
|
|
2024
|
-
return Object.keys(source).reduce((acc: Record<string, any>, key: string) => {
|
|
2025
|
-
if (!this.volatileHeaders.has(key.toLowerCase())) {
|
|
2026
|
-
acc[key] = source[key]
|
|
2027
|
-
}
|
|
2028
|
-
return acc
|
|
2029
|
-
}, {})
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
private resolvePath(params?: any[]): any[] {
|
|
2033
|
-
const basePath = Array.isArray(this.apiOptions.path) ? this.apiOptions.path : []
|
|
2034
|
-
const effective = this.getEffectiveParams(params)
|
|
2035
|
-
return effective ? [...basePath, ...effective] : [...basePath]
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
private getEffectiveParams(params?: any[]): any[] | undefined {
|
|
2039
|
-
if (!Array.isArray(params) || params.length === 0) {
|
|
2040
|
-
return undefined
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
const basePath = Array.isArray(this.apiOptions.path) ? this.apiOptions.path : []
|
|
2044
|
-
|
|
2045
|
-
if (basePath.length !== params.length) {
|
|
2046
|
-
return params
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
const normalizePart = (value: any): string => {
|
|
2050
|
-
if (value && typeof value === 'object') {
|
|
2051
|
-
return JSON.stringify(this.normalizeObject(value))
|
|
2052
|
-
}
|
|
2053
|
-
return String(value)
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
const samePath = params.every((part, index) => normalizePart(part) === normalizePart(basePath[index]))
|
|
2057
|
-
return samePath ? undefined : params
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
private buildRequestSignature(method: 'GET'|'STREAM', requestOptions: ApiRequest, params?: any[]): string {
|
|
2061
|
-
const signaturePayload = {
|
|
2062
|
-
method,
|
|
2063
|
-
server: requestOptions.server,
|
|
2064
|
-
path: this.resolvePath(params),
|
|
2065
|
-
headers: this.filterHeaders(requestOptions.headers),
|
|
2066
|
-
stream: !!requestOptions.stream,
|
|
2067
|
-
streamType: requestOptions.streamType || null
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
return JSON.stringify(this.normalizeObject(signaturePayload))
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
private buildSchemaSignature(schema: string): string {
|
|
2074
|
-
return JSON.stringify(this.normalizeObject(schema.split(',').map((part) => part.trim()).filter(Boolean)))
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
private setCachedRequestSignature(tableName: string, type: 'GET'|'STREAM', signature: string): void {
|
|
2078
|
-
const existing = this.requestSignatureCache[tableName] || {}
|
|
2079
|
-
this.requestSignatureCache[tableName] = {
|
|
2080
|
-
...existing,
|
|
2081
|
-
[type]: signature,
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
private tryBeginInFlightRequest(signature: string): boolean {
|
|
2086
|
-
if (this.inFlightRequestSignatures.has(signature)) {
|
|
2087
|
-
return false
|
|
2088
|
-
}
|
|
2089
|
-
this.inFlightRequestSignatures.add(signature)
|
|
2090
|
-
return true
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
private endInFlightRequest(signature: string): void {
|
|
2094
|
-
this.inFlightRequestSignatures.delete(signature)
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
private getRequestCacheMetadata(storeData: any, type: 'GET'|'STREAM'): any {
|
|
2098
|
-
return storeData?.requestCache?.[type] || null
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
private getStoredSchemaSignature(storeData: any): string | null {
|
|
2102
|
-
return storeData?.schemaSignature || null
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
private saveSchemaSignature(tableName: string, schemaSignature: string) {
|
|
2106
|
-
this.localStorageManagerService.store$(tableName).pipe(
|
|
2107
|
-
take(1),
|
|
2108
|
-
tap((storeData: any) => {
|
|
2109
|
-
this.localStorageManagerService.updateStore({
|
|
2110
|
-
name: tableName,
|
|
2111
|
-
data: {
|
|
2112
|
-
...(storeData || {}),
|
|
2113
|
-
schemaSignature,
|
|
2114
|
-
}
|
|
2115
|
-
})
|
|
2116
|
-
})
|
|
2117
|
-
).subscribe()
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
private saveRequestCacheMetadata(tableName: string, type: 'GET'|'STREAM', signature: string, schemaSignature?: string, options?: RequestOptions) {
|
|
2121
|
-
this.setCachedRequestSignature(tableName, type, signature)
|
|
2122
|
-
this._requestCachePaths.set(tableName, this.resolvePath(options?.path).filter(p => typeof p === 'string' || typeof p === 'number').map(String))
|
|
2123
|
-
|
|
2124
|
-
this.localStorageManagerService.store$(tableName).pipe(
|
|
2125
|
-
take(1),
|
|
2126
|
-
tap((storeData: any) => {
|
|
2127
|
-
const currentCache = storeData?.requestCache || {}
|
|
2128
|
-
const currentEntry = currentCache[type] || {}
|
|
2129
|
-
this.localStorageManagerService.updateStore({
|
|
2130
|
-
name: tableName,
|
|
2131
|
-
data: {
|
|
2132
|
-
...(storeData || {}),
|
|
2133
|
-
requestCache: {
|
|
2134
|
-
...currentCache,
|
|
2135
|
-
[type]: {
|
|
2136
|
-
...currentEntry,
|
|
2137
|
-
signature,
|
|
2138
|
-
savedAt: Date.now(),
|
|
2139
|
-
path: this.resolvePath(options?.path),
|
|
2140
|
-
headers: this.filterHeaders(options?.headers),
|
|
2141
|
-
queryParams: currentEntry.queryParams,
|
|
2142
|
-
queryParamsExpires: currentEntry.queryParamsExpires,
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
})
|
|
2147
|
-
})
|
|
2148
|
-
).subscribe()
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
private clearRequestCacheMetadata(tableName: string) {
|
|
2152
|
-
this.localStorageManagerService.store$(tableName).pipe(
|
|
2153
|
-
take(1),
|
|
2154
|
-
tap((storeData: any) => {
|
|
2155
|
-
let pathArray = this._requestCachePaths.get(tableName)
|
|
2156
|
-
if (!Array.isArray(pathArray) || pathArray.length === 0) {
|
|
2157
|
-
const requestCache = storeData?.requestCache
|
|
2158
|
-
if (requestCache) {
|
|
2159
|
-
const cached = requestCache['GET'] || requestCache['STREAM']
|
|
2160
|
-
if (Array.isArray(cached?.path) && cached.path.length > 0) {
|
|
2161
|
-
pathArray = cached.path.filter((p: any) => typeof p === 'string' || typeof p === 'number').map(String)
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
if (Array.isArray(pathArray) && pathArray.length > 0) {
|
|
2166
|
-
// this.queryParamsTrackerService.clearTrackingForPath(pathArray.join('/'))
|
|
2167
|
-
this._requestCachePaths.delete(tableName)
|
|
2168
|
-
}
|
|
2169
|
-
if (!storeData) return
|
|
2170
|
-
const updated = { ...(storeData || {}) }
|
|
2171
|
-
delete updated.requestCache
|
|
2172
|
-
delete updated.tracker
|
|
2173
|
-
this.localStorageManagerService.updateStore({
|
|
2174
|
-
name: tableName,
|
|
2175
|
-
data: updated
|
|
2176
|
-
})
|
|
2177
|
-
})
|
|
2178
|
-
).subscribe()
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
private trackerNormalizePath(path: any[]): { pathKey: string; query: Record<string, string>; hasQuery: boolean } {
|
|
2182
|
-
const pathSegments: string[] = []
|
|
2183
|
-
const query: Record<string, string> = {}
|
|
2184
|
-
path.forEach((segment) => {
|
|
2185
|
-
if (segment && typeof segment === 'object' && !Array.isArray(segment)) {
|
|
2186
|
-
Object.keys(segment).forEach((key) => {
|
|
2187
|
-
query[this.trackerNormalizeParamKey(key)] = this.trackerNormalizeParamValue(segment[key])
|
|
2188
|
-
})
|
|
2189
|
-
} else {
|
|
2190
|
-
const parsed = this.trackerParsePathSegment(String(segment))
|
|
2191
|
-
pathSegments.push(parsed.pathSegment)
|
|
2192
|
-
Object.keys(parsed.queryObject).forEach((key) => {
|
|
2193
|
-
query[this.trackerNormalizeParamKey(key)] = this.trackerNormalizeParamValue(parsed.queryObject[key])
|
|
2194
|
-
})
|
|
2195
|
-
}
|
|
2196
|
-
})
|
|
2197
|
-
return {
|
|
2198
|
-
pathKey: this.trackerNormalizePathSegments(pathSegments),
|
|
2199
|
-
query,
|
|
2200
|
-
hasQuery: Object.keys(query).length > 0,
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
private trackerParsePathSegment(rawSegment: string): { pathSegment: string; queryObject: Record<string, any> } {
|
|
2205
|
-
const [pathPart, ...queryParts] = String(rawSegment).split('?')
|
|
2206
|
-
const queryString = queryParts.join('?')
|
|
2207
|
-
const queryObject: Record<string, any> = {}
|
|
2208
|
-
if (queryString) {
|
|
2209
|
-
queryString.split('&').forEach((pair) => {
|
|
2210
|
-
if (!pair) return
|
|
2211
|
-
const [rawKey, ...rawValueParts] = pair.split('=')
|
|
2212
|
-
const key = this.trackerSafeDecode(rawKey).trim()
|
|
2213
|
-
const value = this.trackerSafeDecode(rawValueParts.join('=')).trim()
|
|
2214
|
-
if (!key) return
|
|
2215
|
-
queryObject[key] = value
|
|
2216
|
-
})
|
|
2217
|
-
}
|
|
2218
|
-
return { pathSegment: pathPart, queryObject }
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
private trackerSafeDecode(value: string | undefined): string {
|
|
2222
|
-
if (typeof value === 'undefined') return ''
|
|
2223
|
-
try { return decodeURIComponent(value) } catch { return String(value) }
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
private trackerNormalizePathSegments(pathSegments: string[]): string {
|
|
2227
|
-
return pathSegments
|
|
2228
|
-
.map((segment) => String(segment).trim())
|
|
2229
|
-
.filter((segment) => segment.length > 0)
|
|
2230
|
-
.join('/')
|
|
2231
|
-
.replace(/([^:]\/+)\/+/g, '$1')
|
|
2232
|
-
.replace(/^\//, '')
|
|
2233
|
-
.replace(/\/$/, '')
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
private trackerNormalizeParamKey(key: string): string {
|
|
2237
|
-
return String(key).trim().toLowerCase()
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
private trackerNormalizeParamValue(value: any): string {
|
|
2241
|
-
if (Array.isArray(value)) {
|
|
2242
|
-
return value.map((item) => this.trackerNormalizeParamValue(item)).join(',')
|
|
2243
|
-
}
|
|
2244
|
-
if (Object.prototype.toString.call(value) === '[object Object]') {
|
|
2245
|
-
return JSON.stringify(
|
|
2246
|
-
Object.keys(value).sort().reduce((acc: any, k) => {
|
|
2247
|
-
acc[this.trackerNormalizeParamKey(k)] = this.trackerNormalizeParamValue(value[k])
|
|
2248
|
-
return acc
|
|
2249
|
-
}, {})
|
|
2250
|
-
)
|
|
2251
|
-
}
|
|
2252
|
-
return String(value).trim().toLowerCase()
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
private trackerFilterQuery(query: Record<string, string>, ignoreQueryParams?: string[]): Record<string, string> {
|
|
2256
|
-
if (!Array.isArray(ignoreQueryParams) || ignoreQueryParams.length === 0) return { ...query }
|
|
2257
|
-
const normalizedIgnore = ignoreQueryParams.map(p => this.trackerNormalizeParamKey(p))
|
|
2258
|
-
const result: Record<string, string> = {}
|
|
2259
|
-
Object.keys(query).forEach((key) => {
|
|
2260
|
-
if (!normalizedIgnore.includes(key)) {
|
|
2261
|
-
result[key] = query[key]
|
|
2262
|
-
}
|
|
2263
|
-
})
|
|
2264
|
-
return result
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
private trackerBuildExpiryEpoch(expireIn?: string | number): number | null {
|
|
2268
|
-
if (typeof expireIn === 'undefined' || expireIn === null || expireIn === '') return null
|
|
2269
|
-
if (typeof expireIn === 'number' && Number.isFinite(expireIn) && expireIn > 0) {
|
|
2270
|
-
return Math.floor(Date.now() / 1000) + Math.floor(expireIn)
|
|
2271
|
-
}
|
|
2272
|
-
const raw = String(expireIn).trim().toLowerCase().replace(/\s+/g, '')
|
|
2273
|
-
const parsed = raw.match(/^(\d+)(y|w|d|hr|h|mn|min|m|s)$/)
|
|
2274
|
-
if (!parsed) return null
|
|
2275
|
-
const toSeconds: Record<string, number> = { y: 31556926, w: 604800, d: 86400, hr: 3600, h: 3600, mn: 60, min: 60, m: 60, s: 1 }
|
|
2276
|
-
const seconds = toSeconds[parsed[2]]
|
|
2277
|
-
if (!seconds) return null
|
|
2278
|
-
return Math.floor(Date.now() / 1000) + Number(parsed[1]) * seconds
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
private checkTrackerAllowsRequest(tableName: string, type: 'GET'|'STREAM', path: any[], options: RequestOptions | undefined, storeData: any): Observable<boolean> {
|
|
2282
|
-
const normalized = this.trackerNormalizePath(path)
|
|
2283
|
-
const ignoreQueryParams = Array.isArray(options?.ignoreQueryParams) ? options!.ignoreQueryParams : []
|
|
2284
|
-
|
|
2285
|
-
if (!normalized.hasQuery) {
|
|
2286
|
-
const meta = this.getRequestCacheMetadata(storeData, type)
|
|
2287
|
-
if (!meta) {
|
|
2288
|
-
// No prior cache entry — record that we're tracking this request
|
|
2289
|
-
this.setCachedRequestSignature(tableName, type, '')
|
|
2290
|
-
this.localStorageManagerService.store$(tableName).pipe(
|
|
2291
|
-
take(1),
|
|
2292
|
-
tap((s: any) => {
|
|
2293
|
-
const currentCache = s?.requestCache || {}
|
|
2294
|
-
this.localStorageManagerService.updateStore({
|
|
2295
|
-
name: tableName,
|
|
2296
|
-
data: {
|
|
2297
|
-
...(s || {}),
|
|
2298
|
-
requestCache: {
|
|
2299
|
-
...currentCache,
|
|
2300
|
-
[type]: {
|
|
2301
|
-
...(currentCache[type] || {}),
|
|
2302
|
-
active: true,
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
})
|
|
2307
|
-
})
|
|
2308
|
-
).subscribe()
|
|
2309
|
-
}
|
|
2310
|
-
return of(!meta)
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
const filtered = this.trackerFilterQuery(normalized.query, ignoreQueryParams)
|
|
2314
|
-
const keys = Object.keys(filtered)
|
|
2315
|
-
|
|
2316
|
-
if (keys.length === 0) {
|
|
2317
|
-
// All query params were filtered out — check if we have any prior entry
|
|
2318
|
-
const meta = this.getRequestCacheMetadata(storeData, type)
|
|
2319
|
-
if (!meta) {
|
|
2320
|
-
this.setCachedRequestSignature(tableName, type, '')
|
|
2321
|
-
this.localStorageManagerService.store$(tableName).pipe(
|
|
2322
|
-
take(1),
|
|
2323
|
-
tap((s: any) => {
|
|
2324
|
-
const currentCache = s?.requestCache || {}
|
|
2325
|
-
this.localStorageManagerService.updateStore({
|
|
2326
|
-
name: tableName,
|
|
2327
|
-
data: {
|
|
2328
|
-
...(s || {}),
|
|
2329
|
-
requestCache: {
|
|
2330
|
-
...currentCache,
|
|
2331
|
-
[type]: {
|
|
2332
|
-
...(currentCache[type] || {}),
|
|
2333
|
-
active: true,
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
})
|
|
2338
|
-
})
|
|
2339
|
-
).subscribe()
|
|
2340
|
-
}
|
|
2341
|
-
return of(!meta)
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
const meta = this.getRequestCacheMetadata(storeData, type) || {}
|
|
2345
|
-
const now = Math.floor(Date.now() / 1000)
|
|
2346
|
-
let queryParams: Record<string, string[]> = { ...(meta.queryParams || {}) }
|
|
2347
|
-
let queryParamsExpires: number | null = meta.queryParamsExpires ?? null
|
|
2348
|
-
|
|
2349
|
-
if (queryParamsExpires !== null && queryParamsExpires <= now) {
|
|
2350
|
-
queryParams = {}
|
|
2351
|
-
queryParamsExpires = null
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
let accepted = false
|
|
2355
|
-
keys.forEach(key => {
|
|
2356
|
-
const value = filtered[key]
|
|
2357
|
-
const consumed = queryParams[key] || []
|
|
2358
|
-
if (!consumed.includes(value)) {
|
|
2359
|
-
queryParams[key] = [...consumed, value]
|
|
2360
|
-
accepted = true
|
|
2361
|
-
}
|
|
2362
|
-
})
|
|
2363
|
-
|
|
2364
|
-
if (accepted) {
|
|
2365
|
-
const newExpiry = this.trackerBuildExpiryEpoch(options?.queryParamsExpiresIn)
|
|
2366
|
-
if (newExpiry !== null) {
|
|
2367
|
-
queryParamsExpires = newExpiry
|
|
2368
|
-
}
|
|
2369
|
-
this.localStorageManagerService.store$(tableName).pipe(
|
|
2370
|
-
take(1),
|
|
2371
|
-
tap((latestStoreData: any) => {
|
|
2372
|
-
const currentCache = latestStoreData?.requestCache || {}
|
|
2373
|
-
const currentEntry = currentCache[type] || {}
|
|
2374
|
-
this.localStorageManagerService.updateStore({
|
|
2375
|
-
name: tableName,
|
|
2376
|
-
data: {
|
|
2377
|
-
...(latestStoreData || {}),
|
|
2378
|
-
requestCache: {
|
|
2379
|
-
...currentCache,
|
|
2380
|
-
[type]: {
|
|
2381
|
-
...currentEntry,
|
|
2382
|
-
queryParams,
|
|
2383
|
-
queryParamsExpires,
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
})
|
|
2388
|
-
})
|
|
2389
|
-
).subscribe()
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
return of(accepted)
|
|
2393
|
-
}
|
|
2394
|
-
|
|
2395
|
-
}
|