http-request-manager 18.7.31 → 18.9.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.
@@ -4,7 +4,7 @@ import { ComponentStore } from '@ngrx/component-store';
4
4
  import { map, catchError, filter, tap, finalize, takeWhile, retry, startWith, mergeMap, takeUntil, withLatestFrom, switchMap, delay, concatMap, take, scan, distinctUntilChanged } from 'rxjs/operators';
5
5
  import { HttpClient, HttpHeaders, HttpEventType, HttpHeaderResponse, HttpErrorResponse, HTTP_INTERCEPTORS } from '@angular/common/http';
6
6
  import * as CryptoJS from 'crypto-js';
7
- import { from, BehaviorSubject, EMPTY, throwError, defer, interval, timer, Subject, merge, of, Subscription, take as take$1, catchError as catchError$1, map as map$1, tap as tap$1, switchMap as switchMap$1, startWith as startWith$1, distinctUntilChanged as distinctUntilChanged$1, combineLatest, filter as filter$1, takeUntil as takeUntil$1, ReplaySubject } from 'rxjs';
7
+ import { from, BehaviorSubject, EMPTY, throwError, defer, interval, timer, Subject, of, merge, Subscription, take as take$1, catchError as catchError$1, map as map$1, tap as tap$1, switchMap as switchMap$1, startWith as startWith$1, distinctUntilChanged as distinctUntilChanged$1, combineLatest, filter as filter$1, takeUntil as takeUntil$1, ReplaySubject } from 'rxjs';
8
8
  import { ToastMessageDisplayService, ToastDisplay, ToastColors, ToastMessageDisplayModule } from 'toast-message-display';
9
9
  import Dexie from 'dexie';
10
10
  import * as i1 from '@ngx-translate/core';
@@ -383,17 +383,25 @@ class SymmetricalEncryptionService {
383
383
  let _key = CryptoJS.enc.Utf8.parse(key);
384
384
  let _iv = CryptoJS.enc.Utf8.parse(key);
385
385
  try {
386
- return CryptoJS.AES.decrypt(str, _key, {
386
+ const decrypted = CryptoJS.AES.decrypt(str, _key, {
387
387
  keySize: 16,
388
388
  iv: _iv,
389
389
  mode: CryptoJS.mode.ECB,
390
390
  padding: CryptoJS.pad.Pkcs7
391
- }).toString(CryptoJS.enc.Utf8);
391
+ });
392
+ // Check if the decrypted data is valid before converting to UTF-8
393
+ const decryptedStr = decrypted.toString(CryptoJS.enc.Utf8);
394
+ if (!decryptedStr) {
395
+ console.warn('Decryption resulted in empty string');
396
+ return null;
397
+ }
398
+ return decryptedStr;
392
399
  }
393
400
  catch (error) {
394
- console.log(error);
401
+ console.error('Decryption failed:', error.message);
402
+ return null;
395
403
  }
396
- return;
404
+ return null;
397
405
  }
398
406
  createSignature(url, len = 16) {
399
407
  const sig = CryptoJS.SHA256(url).toString(CryptoJS.enc.Hex);
@@ -1261,19 +1269,30 @@ class WebsocketService {
1261
1269
  this.lastOptions = options;
1262
1270
  // Subscribe to primary channel
1263
1271
  this.sendSubscribe(options.id, options.user);
1264
- // Auto-subscribe to additional channels from options.channels[]
1272
+ // Build unified set of all channels to subscribe to (with deduplication)
1273
+ const allChannels = new Set();
1274
+ // Add channels from options.channels[]
1265
1275
  if (options.channels && options.channels.length > 0) {
1266
- options.channels.forEach(channel => {
1267
- if (channel !== options.id) {
1268
- this.subscribeToChannel(channel);
1269
- }
1276
+ options.channels.forEach(channel => allChannels.add(channel));
1277
+ }
1278
+ // Add channels from options.wsUpdateChannels[] with MES- prefix
1279
+ if (options.wsUpdateChannels && options.wsUpdateChannels.length > 0) {
1280
+ options.wsUpdateChannels.forEach(ch => {
1281
+ const prefixed = ch.startsWith('MES-') ? ch : `MES-${ch}`;
1282
+ allChannels.add(prefixed);
1270
1283
  });
1271
1284
  }
1272
- // Re-subscribe to any previously subscribed channels (reconnection scenario)
1285
+ // Subscribe to all channels (excluding primary channel)
1286
+ allChannels.forEach(channel => {
1287
+ if (channel !== options.id) {
1288
+ this.subscribeToChannel(channel);
1289
+ }
1290
+ });
1291
+ // Re-subscribe to any previously subscribed channels not in current options
1273
1292
  const previousChannels = this.subscribedChannels.value;
1274
1293
  if (previousChannels.size > 0) {
1275
1294
  previousChannels.forEach(channel => {
1276
- if (channel !== options.id && (!options.channels || !options.channels.includes(channel))) {
1295
+ if (!allChannels.has(channel) && channel !== options.id) {
1277
1296
  this.subscribeToChannel(channel);
1278
1297
  }
1279
1298
  });
@@ -1313,20 +1332,25 @@ class WebsocketService {
1313
1332
  }
1314
1333
  subscribeToChannel(channelName) {
1315
1334
  if (this.socket?.readyState === WebSocket.OPEN) {
1316
- const message = {
1317
- type: 'subscribe',
1318
- subscribedChannel: channelName,
1319
- content: {}
1320
- };
1321
- this.socket.send(JSON.stringify(message));
1322
- // Track locally - create new Set to trigger change detection
1323
- const current = new Set(this.subscribedChannels.value);
1324
- current.add(channelName);
1325
- this.subscribedChannels.next(current);
1326
- console.log(`📝 Subscribed to channel: ${channelName}`);
1335
+ try {
1336
+ const message = {
1337
+ type: 'subscribe',
1338
+ subscribedChannel: channelName,
1339
+ content: {}
1340
+ };
1341
+ this.socket.send(JSON.stringify(message));
1342
+ // Track locally - create new Set to trigger change detection
1343
+ const current = new Set(this.subscribedChannels.value);
1344
+ current.add(channelName);
1345
+ this.subscribedChannels.next(current);
1346
+ console.log(`📝 Subscribed to channel: ${channelName}`);
1347
+ }
1348
+ catch (error) {
1349
+ console.error(`❌ Failed to subscribe to channel "${channelName}":`, error);
1350
+ }
1327
1351
  }
1328
1352
  else {
1329
- console.warn('Cannot subscribe: WebSocket not yet open.');
1353
+ console.warn(`Cannot subscribe to channel "${channelName}": WebSocket not yet open.`);
1330
1354
  }
1331
1355
  }
1332
1356
  subscribeToChannels(channelNames) {
@@ -1388,6 +1412,38 @@ class WebsocketService {
1388
1412
  console.error('WebSocket is not open. Cannot send channel message.');
1389
1413
  }
1390
1414
  }
1415
+ /**
1416
+ * Send a message to multiple channels at once (batch)
1417
+ * Sends the new batch format and falls back to legacy per-channel messages for compatibility
1418
+ */
1419
+ sendChannelMessageToChannels(channels, content) {
1420
+ if (this.socket?.readyState === WebSocket.OPEN) {
1421
+ try {
1422
+ // New batch format
1423
+ this.socket.send(JSON.stringify({
1424
+ type: 'channelMessage',
1425
+ channels: channels,
1426
+ data: content
1427
+ }));
1428
+ console.log(`💬 Send channel message to channels [${channels.join(', ')}]:`, content);
1429
+ }
1430
+ catch (error) {
1431
+ console.error('❌ Failed to send channelMessage batch:', error);
1432
+ }
1433
+ // Legacy fallback - send individual messages to each channel
1434
+ try {
1435
+ channels.forEach(channel => {
1436
+ this.socket?.send(JSON.stringify({ type: 'message', subscribedChannel: channel, content }));
1437
+ });
1438
+ }
1439
+ catch (err) {
1440
+ console.warn('⚠️ Legacy fallback failed sending individual messages', err);
1441
+ }
1442
+ }
1443
+ else {
1444
+ console.error('WebSocket is not open. Cannot send message to channels.');
1445
+ }
1446
+ }
1391
1447
  sendMessageToUser(user, content) {
1392
1448
  if (this.socket?.readyState === WebSocket.OPEN) {
1393
1449
  this.socket.send(JSON.stringify({ type: 'userMessage', subscribedChannel: user, content }));
@@ -1533,6 +1589,586 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
1533
1589
  }]
1534
1590
  }] });
1535
1591
 
1592
+ /**
1593
+ * WebSocketManagerService - Singleton WebSocket connection manager
1594
+ *
1595
+ * This service ensures only ONE WebSocket connection exists across ALL instances
1596
+ * of HTTPManagerStateService. It uses static properties to track connection state
1597
+ * globally, preventing duplicate connections when multiple state services are created.
1598
+ *
1599
+ * Usage:
1600
+ * - Inject into HTTPManagerService (or directly into state services)
1601
+ * - Call connect() with WSOptions - only the first call establishes connection
1602
+ * - Subsequent calls detect existing connection and skip reconnection
1603
+ * - All instances share the same connectionStatus$ and messages$ observables
1604
+ */
1605
+ class WebSocketManagerService {
1606
+ constructor() {
1607
+ this.messages$ = WebSocketManagerService.messages.asObservable();
1608
+ this.connectionStatus$ = WebSocketManagerService.connectionStatus.asObservable();
1609
+ this.subscribedChannels$ = WebSocketManagerService.subscribedChannels.asObservable();
1610
+ this.onReconnect$ = WebSocketManagerService.onReconnect.asObservable();
1611
+ }
1612
+ // ═══════════════════════════════════════════════════════════════════════════
1613
+ // STATIC SINGLETON STATE (Shared across ALL instances)
1614
+ // ═══════════════════════════════════════════════════════════════════════════
1615
+ static { this.socket = null; }
1616
+ static { this.isConnecting = false; }
1617
+ static { this.connectionInitialized = false; }
1618
+ // Store last options for reconnection
1619
+ static { this.lastOptions = null; }
1620
+ // ═══════════════════════════════════════════════════════════════════════════
1621
+ // INSTANCE OBSERVABLES (Shared across ALL instances via static BehaviorSubjects)
1622
+ // ═══════════════════════════════════════════════════════════════════════════
1623
+ static { this.messages = new BehaviorSubject(null); }
1624
+ static { this.connectionStatus = new BehaviorSubject(false); }
1625
+ static { this.isSubscribed = false; }
1626
+ // Track currently subscribed channels
1627
+ static { this.subscribedChannels = new BehaviorSubject(new Set()); }
1628
+ // Public method to get current subscribed channels
1629
+ static getSubscribedChannels() {
1630
+ return WebSocketManagerService.subscribedChannels.getValue();
1631
+ }
1632
+ // Public method to add a channel to subscribed channels
1633
+ static addSubscribedChannel(channelName) {
1634
+ const current = new Set(WebSocketManagerService.subscribedChannels.value);
1635
+ current.add(channelName);
1636
+ WebSocketManagerService.subscribedChannels.next(current);
1637
+ console.log(`📝 Added ${channelName} to subscribedChannels (now has ${current.size} channels)`);
1638
+ }
1639
+ // Reconnect event (emitted when connection is established/re-established)
1640
+ static { this.onReconnect = new BehaviorSubject(undefined); }
1641
+ // ═══════════════════════════════════════════════════════════════════════════
1642
+ // SESSION MANAGEMENT
1643
+ // ═══════════════════════════════════════════════════════════════════════════
1644
+ getSessionId() {
1645
+ return sessionStorage.getItem('WSID') ?? (() => {
1646
+ const uuid = UUID_STR();
1647
+ sessionStorage.setItem('WSID', uuid);
1648
+ return uuid;
1649
+ })();
1650
+ }
1651
+ // ═══════════════════════════════════════════════════════════════════════════
1652
+ // CONNECTION MANAGEMENT
1653
+ // ═══════════════════════════════════════════════════════════════════════════
1654
+ /**
1655
+ * Connect to WebSocket server
1656
+ *
1657
+ * IMPORTANT: Only the first call across ALL instances will establish a connection.
1658
+ * Subsequent calls will detect the existing connection and return immediately.
1659
+ *
1660
+ * @param options - WebSocket configuration options
1661
+ * @param jwtToken - Optional JWT token for authentication
1662
+ */
1663
+ connect(options, jwtToken) {
1664
+ // Check if connection already exists and is open
1665
+ if (WebSocketManagerService.socket) {
1666
+ if (WebSocketManagerService.socket.readyState === WebSocket.OPEN) {
1667
+ console.log('✓ WebSocket connection already OPEN (singleton).');
1668
+ return;
1669
+ }
1670
+ if (WebSocketManagerService.socket.readyState === WebSocket.CONNECTING) {
1671
+ console.log('⏳ WebSocket is already CONNECTING (singleton). Waiting for handshake.');
1672
+ return;
1673
+ }
1674
+ // Clean up stale connection
1675
+ if (WebSocketManagerService.socket.readyState === WebSocket.CLOSING ||
1676
+ WebSocketManagerService.socket.readyState === WebSocket.CLOSED) {
1677
+ console.log(`🧹 Cleaning up stale socket (State: ${WebSocketManagerService.socket.readyState}).`);
1678
+ WebSocketManagerService.socket.close();
1679
+ WebSocketManagerService.socket = null;
1680
+ }
1681
+ }
1682
+ // Prevent duplicate connection attempts
1683
+ if (WebSocketManagerService.isConnecting) {
1684
+ console.log('⏳ Connection already in progress...');
1685
+ return;
1686
+ }
1687
+ // Mark as connecting
1688
+ WebSocketManagerService.isConnecting = true;
1689
+ WebSocketManagerService.isSubscribed = false;
1690
+ WebSocketManagerService.subscribedChannels.next(new Set());
1691
+ const sessionId = this.getSessionId();
1692
+ const URL = (jwtToken) ? `${options.wsServer}?token=${jwtToken}&sessionId=${sessionId}` : `${options.wsServer}?sessionId=${sessionId}`;
1693
+ console.log(`🔌 Initiating WebSocket connection to: ${options.wsServer}`);
1694
+ // Create new WebSocket instance (static)
1695
+ WebSocketManagerService.socket = new WebSocket(URL);
1696
+ WebSocketManagerService.socket.onopen = () => {
1697
+ console.log(`📡 Connected to WebSocket`);
1698
+ // Force clear subscribedChannels on new connection - server lost our subscriptions
1699
+ console.log('🧹 Clearing subscribedChannels on connect (was:', WebSocketManagerService.subscribedChannels.value.size, 'channels)');
1700
+ WebSocketManagerService.subscribedChannels.next(new Set());
1701
+ WebSocketManagerService.connectionStatus.next(true);
1702
+ WebSocketManagerService.isConnecting = false;
1703
+ WebSocketManagerService.connectionInitialized = true;
1704
+ WebSocketManagerService.lastOptions = options;
1705
+ // Emit reconnect event - MessageTrackerService will handle subscriptions with lastSeenId
1706
+ console.log(`🔄 Emitting reconnect event for MessageTrackerService`);
1707
+ WebSocketManagerService.onReconnect.next();
1708
+ };
1709
+ WebSocketManagerService.socket.onmessage = (event) => {
1710
+ try {
1711
+ const data = JSON.parse(event.data);
1712
+ if (data.error && data.error === 'JWT_INVALID') {
1713
+ console.error('JWT validation failed. Authentication error!');
1714
+ WebSocketManagerService.messages.next(data);
1715
+ WebSocketManagerService.connectionStatus.next(false);
1716
+ WebSocketManagerService.socket?.close();
1717
+ return;
1718
+ }
1719
+ WebSocketManagerService.messages.next(data);
1720
+ }
1721
+ catch (error) {
1722
+ console.error('Error parsing WebSocket message:', event.data);
1723
+ }
1724
+ };
1725
+ WebSocketManagerService.socket.onclose = () => {
1726
+ console.log('🔴 WebSocket connection closed');
1727
+ console.log('🧹 Clearing subscribedChannels (was:', WebSocketManagerService.subscribedChannels.value.size, 'channels)');
1728
+ WebSocketManagerService.connectionStatus.next(false);
1729
+ WebSocketManagerService.isConnecting = false;
1730
+ WebSocketManagerService.socket = null;
1731
+ // Clear subscribed channels - server lost our subscriptions
1732
+ WebSocketManagerService.subscribedChannels.next(new Set());
1733
+ console.log('✅ subscribedChannels cleared');
1734
+ };
1735
+ WebSocketManagerService.socket.onerror = (error) => {
1736
+ console.error('❌ WebSocket error:', error);
1737
+ WebSocketManagerService.connectionStatus.next(false);
1738
+ WebSocketManagerService.isConnecting = false;
1739
+ };
1740
+ }
1741
+ /**
1742
+ * Disconnect from WebSocket server
1743
+ */
1744
+ disconnect() {
1745
+ if (WebSocketManagerService.socket) {
1746
+ console.log('🔌 Disconnecting WebSocket...');
1747
+ WebSocketManagerService.socket.close();
1748
+ WebSocketManagerService.socket = null;
1749
+ WebSocketManagerService.connectionStatus.next(false);
1750
+ WebSocketManagerService.subscribedChannels.next(new Set());
1751
+ }
1752
+ }
1753
+ // ═══════════════════════════════════════════════════════════════════════════
1754
+ // SUBSCRIPTION MANAGEMENT
1755
+ // ═══════════════════════════════════════════════════════════════════════════
1756
+ sendSubscribe(channelName, user) {
1757
+ const alreadySubscribed = WebSocketManagerService.subscribedChannels.value.has(channelName);
1758
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN && !alreadySubscribed) {
1759
+ const message = {
1760
+ type: 'subscribe',
1761
+ subscribedChannel: channelName,
1762
+ content: {
1763
+ id: this.getSessionId(),
1764
+ ...user
1765
+ }
1766
+ };
1767
+ WebSocketManagerService.socket.send(JSON.stringify(message));
1768
+ // Track this channel as subscribed
1769
+ const current = new Set(WebSocketManagerService.subscribedChannels.value);
1770
+ current.add(channelName);
1771
+ WebSocketManagerService.subscribedChannels.next(current);
1772
+ WebSocketManagerService.isSubscribed = true;
1773
+ console.log(`[CLIENT] Sent initial subscription to: ${channelName}`);
1774
+ }
1775
+ else {
1776
+ console.warn(`[CLIENT] Subscription prevented. Open: ${WebSocketManagerService.socket?.readyState === WebSocket.OPEN}, Already subscribed to ${channelName}: ${alreadySubscribed}`);
1777
+ }
1778
+ }
1779
+ /**
1780
+ * Send subscribe with lastSeenId for message sync support
1781
+ * @param channelName - Channel name to subscribe to
1782
+ * @param user - User data
1783
+ * @param lastSeenId - Last message ID seen (for replay)
1784
+ */
1785
+ sendSubscribeWithLastSeen(channelName, user, lastSeenId) {
1786
+ const alreadySubscribed = WebSocketManagerService.subscribedChannels.value.has(channelName);
1787
+ const isOpen = WebSocketManagerService.socket?.readyState === WebSocket.OPEN;
1788
+ console.log(`🔍 Subscription check for "${channelName}": isOpen=${isOpen}, alreadySubscribed=${alreadySubscribed}`);
1789
+ if (isOpen && !alreadySubscribed && WebSocketManagerService.socket) {
1790
+ const message = {
1791
+ type: 'subscribe',
1792
+ subscribedChannel: channelName,
1793
+ lastSeenId, // NEW: For message replay
1794
+ content: {
1795
+ id: this.getSessionId(),
1796
+ ...user
1797
+ }
1798
+ };
1799
+ WebSocketManagerService.socket.send(JSON.stringify(message));
1800
+ // DON'T track as subscribed yet - wait for server confirmation ('subscribed' message)
1801
+ // This prevents race conditions where we think we're subscribed but server doesn't
1802
+ WebSocketManagerService.isSubscribed = true;
1803
+ console.log(`[CLIENT] Sent subscription with lastSeenId=${lastSeenId} to: ${channelName}`);
1804
+ }
1805
+ else {
1806
+ console.warn(`[CLIENT] Subscription prevented. Open: ${isOpen}, Already subscribed to ${channelName}: ${alreadySubscribed}`);
1807
+ }
1808
+ }
1809
+ /**
1810
+ * Subscribe to a channel
1811
+ * @param channelName - Channel name to subscribe to
1812
+ */
1813
+ subscribeToChannel(channelName) {
1814
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
1815
+ try {
1816
+ const message = {
1817
+ type: 'subscribe',
1818
+ subscribedChannel: channelName,
1819
+ content: {}
1820
+ };
1821
+ WebSocketManagerService.socket.send(JSON.stringify(message));
1822
+ // Track locally - create new Set to trigger change detection
1823
+ const current = new Set(WebSocketManagerService.subscribedChannels.value);
1824
+ current.add(channelName);
1825
+ WebSocketManagerService.subscribedChannels.next(current);
1826
+ console.log(`📝 Subscribed to channel: ${channelName}`);
1827
+ }
1828
+ catch (error) {
1829
+ console.error(`❌ Failed to subscribe to channel "${channelName}":`, error);
1830
+ }
1831
+ }
1832
+ else {
1833
+ console.warn(`Cannot subscribe to channel "${channelName}": WebSocket not yet open.`);
1834
+ }
1835
+ }
1836
+ /**
1837
+ * Subscribe to multiple channels
1838
+ * @param channelNames - Array of channel names to subscribe to
1839
+ */
1840
+ subscribeToChannels(channelNames) {
1841
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
1842
+ channelNames.forEach(channel => this.subscribeToChannel(channel));
1843
+ }
1844
+ else {
1845
+ console.warn('Cannot subscribe: WebSocket not yet open.');
1846
+ }
1847
+ }
1848
+ /**
1849
+ * Unsubscribe from a channel
1850
+ * @param channel - Channel name to unsubscribe from
1851
+ */
1852
+ unsubscribeFromChannel(channel) {
1853
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
1854
+ WebSocketManagerService.socket.send(JSON.stringify({ type: 'unsubscribe', subscribedChannel: channel }));
1855
+ // Remove from local tracking - create new Set to trigger change detection
1856
+ const current = new Set(WebSocketManagerService.subscribedChannels.value);
1857
+ current.delete(channel);
1858
+ WebSocketManagerService.subscribedChannels.next(current);
1859
+ console.log(`💬 Unsubscribed from channel: ${channel}`);
1860
+ }
1861
+ else {
1862
+ console.error('WebSocket is not open. Cannot unsubscribe from channel.');
1863
+ }
1864
+ }
1865
+ /**
1866
+ * Get currently subscribed channels
1867
+ * @returns Set of subscribed channel names
1868
+ */
1869
+ getSubscribedChannels() {
1870
+ return WebSocketManagerService.subscribedChannels.value;
1871
+ }
1872
+ // ═══════════════════════════════════════════════════════════════════════════
1873
+ // MESSAGE SENDING
1874
+ // ═══════════════════════════════════════════════════════════════════════════
1875
+ /**
1876
+ * Generic send method for message tracking (acks, gap requests, etc.)
1877
+ * @param message - Message object to send
1878
+ */
1879
+ send(message) {
1880
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
1881
+ WebSocketManagerService.socket.send(JSON.stringify(message));
1882
+ }
1883
+ else {
1884
+ console.warn('⚠️ Cannot send message: WebSocket not open', message);
1885
+ }
1886
+ }
1887
+ /**
1888
+ * Send broadcast message
1889
+ * @param content - Message content to broadcast
1890
+ */
1891
+ sendBroadcast(content) {
1892
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
1893
+ WebSocketManagerService.socket.send(JSON.stringify({ type: 'broadcast', content }));
1894
+ console.log(`📢 Send broadcast: ${content}`);
1895
+ }
1896
+ else {
1897
+ console.error('WebSocket is not open. Cannot send broadcast.');
1898
+ }
1899
+ }
1900
+ /**
1901
+ * Send message in channel (state manager message)
1902
+ * @param channel - Channel name
1903
+ * @param content - Message content
1904
+ */
1905
+ sendMessageInChannel(channel, content) {
1906
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
1907
+ const message = {
1908
+ type: 'stateMangerMessage',
1909
+ subscribedChannel: channel,
1910
+ content
1911
+ };
1912
+ // DEBUG: Log the exact message being sent
1913
+ console.log('🔍 [DEBUG] sendMessageInChannel:', {
1914
+ channel,
1915
+ channelType: typeof channel,
1916
+ channelEmpty: channel === '' || channel === null || channel === undefined,
1917
+ content,
1918
+ fullMessage: message,
1919
+ jsonString: JSON.stringify(message)
1920
+ });
1921
+ WebSocketManagerService.socket.send(JSON.stringify(message));
1922
+ console.log(`💬 Send message:`, content);
1923
+ }
1924
+ else {
1925
+ console.error('WebSocket is not open. Cannot send message.');
1926
+ }
1927
+ }
1928
+ /**
1929
+ * Send channel message (broadcasts to all subscribers in the channel)
1930
+ * @param channel - Channel name
1931
+ * @param content - Message content
1932
+ */
1933
+ sendChannelMessage(channel, content) {
1934
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
1935
+ WebSocketManagerService.socket.send(JSON.stringify({
1936
+ type: 'message',
1937
+ subscribedChannel: channel,
1938
+ content
1939
+ }));
1940
+ console.log(`💬 Send channel message to [${channel}]:`, content);
1941
+ }
1942
+ else {
1943
+ console.error('WebSocket is not open. Cannot send channel message.');
1944
+ }
1945
+ }
1946
+ /**
1947
+ * Send message to multiple channels at once (batch)
1948
+ * @param channels - Array of channel names
1949
+ * @param content - Message content
1950
+ */
1951
+ sendChannelMessageToChannels(channels, content) {
1952
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
1953
+ try {
1954
+ // New batch format
1955
+ WebSocketManagerService.socket.send(JSON.stringify({
1956
+ type: 'channelMessage',
1957
+ channels: channels,
1958
+ data: content
1959
+ }));
1960
+ console.log(`💬 Send channel message to channels [${channels.join(', ')}]:`, content);
1961
+ }
1962
+ catch (error) {
1963
+ console.error('❌ Failed to send channelMessage batch:', error);
1964
+ }
1965
+ // Legacy fallback - send individual messages to each channel
1966
+ try {
1967
+ channels.forEach(channel => {
1968
+ WebSocketManagerService.socket?.send(JSON.stringify({
1969
+ type: 'message',
1970
+ subscribedChannel: channel,
1971
+ content
1972
+ }));
1973
+ });
1974
+ }
1975
+ catch (err) {
1976
+ console.warn('⚠️ Legacy fallback failed sending individual messages', err);
1977
+ }
1978
+ }
1979
+ else {
1980
+ console.error('WebSocket is not open. Cannot send message to channels.');
1981
+ }
1982
+ }
1983
+ /**
1984
+ * Send message to specific user
1985
+ * @param user - User identifier
1986
+ * @param content - Message content
1987
+ */
1988
+ sendMessageToUser(user, content) {
1989
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
1990
+ WebSocketManagerService.socket.send(JSON.stringify({
1991
+ type: 'userMessage',
1992
+ subscribedChannel: user,
1993
+ content
1994
+ }));
1995
+ console.log(`💬 Send message:`, content);
1996
+ }
1997
+ else {
1998
+ console.error('WebSocket is not open. Cannot send message.');
1999
+ }
2000
+ }
2001
+ // ═══════════════════════════════════════════════════════════════════════════
2002
+ // CHANNEL MANAGEMENT
2003
+ // ═══════════════════════════════════════════════════════════════════════════
2004
+ /**
2005
+ * Request list of all channels from server
2006
+ */
2007
+ getAllChannels() {
2008
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
2009
+ WebSocketManagerService.socket.send(JSON.stringify({ type: 'getChannels' }));
2010
+ console.log('🗂️ List of all channels');
2011
+ }
2012
+ else {
2013
+ console.error('WebSocket is not open. Cannot request channels.');
2014
+ }
2015
+ }
2016
+ /**
2017
+ * Create a new channel on server
2018
+ * @param channel - Channel name to create
2019
+ */
2020
+ createChannel(channel) {
2021
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
2022
+ WebSocketManagerService.socket.send(JSON.stringify({
2023
+ type: 'createChannel',
2024
+ content: channel
2025
+ }));
2026
+ console.log('🗂️ Created channel:', channel);
2027
+ }
2028
+ else {
2029
+ console.error('WebSocket is not open. Cannot request channels.');
2030
+ }
2031
+ }
2032
+ /**
2033
+ * Delete a channel from server
2034
+ * @param channel - Channel name to delete
2035
+ */
2036
+ deleteChannel(channel) {
2037
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
2038
+ WebSocketManagerService.socket.send(JSON.stringify({
2039
+ type: 'deleteChannel',
2040
+ content: channel
2041
+ }));
2042
+ console.log('🗂️ Delete channel:', channel);
2043
+ }
2044
+ else {
2045
+ console.error('WebSocket is not open. Cannot request channels.');
2046
+ }
2047
+ }
2048
+ /**
2049
+ * Get users in a specific channel
2050
+ * @param channel - Channel name
2051
+ */
2052
+ getUsersInChannel(channel) {
2053
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
2054
+ WebSocketManagerService.socket.send(JSON.stringify({
2055
+ type: 'getUsers',
2056
+ subscribedChannel: channel
2057
+ }));
2058
+ console.log(`👥 List all users in channel: ${channel}`);
2059
+ }
2060
+ else {
2061
+ console.error('WebSocket is not open. Cannot request users.');
2062
+ }
2063
+ }
2064
+ // ═══════════════════════════════════════════════════════════════════════════
2065
+ // NOTIFICATION CHANNELS (MES- prefix)
2066
+ // ═══════════════════════════════════════════════════════════════════════════
2067
+ /**
2068
+ * Create a notification channel
2069
+ * @param channel - Channel name (should include MES- prefix)
2070
+ */
2071
+ createNotificationChannel(channel) {
2072
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
2073
+ WebSocketManagerService.socket.send(JSON.stringify({
2074
+ type: 'createNotificationChannel',
2075
+ content: channel
2076
+ }));
2077
+ console.log('📢 Created notification channel:', channel);
2078
+ }
2079
+ else {
2080
+ console.error('WebSocket is not open. Cannot create notification channel.');
2081
+ }
2082
+ }
2083
+ /**
2084
+ * Get all notification channels (in-memory)
2085
+ */
2086
+ getNotificationChannels() {
2087
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
2088
+ WebSocketManagerService.socket.send(JSON.stringify({ type: 'getNotificationChannels' }));
2089
+ console.log('📢 Requested notification channels list');
2090
+ }
2091
+ else {
2092
+ console.error('WebSocket is not open. Cannot request notification channels.');
2093
+ }
2094
+ }
2095
+ /**
2096
+ * Get today's notification channels from database
2097
+ */
2098
+ getTodaysNotificationChannels() {
2099
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
2100
+ WebSocketManagerService.socket.send(JSON.stringify({ type: 'getTodaysNotificationChannels' }));
2101
+ console.log('📢 Requested today\'s notification channels from DB');
2102
+ }
2103
+ else {
2104
+ console.error('WebSocket is not open. Cannot request today\'s notification channels.');
2105
+ }
2106
+ }
2107
+ /**
2108
+ * Subscribe to a notification channel with optional date filters
2109
+ * @param channel - Channel name
2110
+ * @param options - Optional start/end epoch filters
2111
+ * @param user - User information
2112
+ */
2113
+ subscribeToNotificationChannel(channel, options, user) {
2114
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
2115
+ WebSocketManagerService.socket.send(JSON.stringify({
2116
+ type: 'subscribeNotifications',
2117
+ subscribedChannel: channel,
2118
+ content: {
2119
+ ...options,
2120
+ user: user
2121
+ }
2122
+ }));
2123
+ console.log(`📢 Subscribed to notification channel: ${channel}`);
2124
+ }
2125
+ else {
2126
+ console.error('WebSocket is not open. Cannot subscribe to notification channel.');
2127
+ }
2128
+ }
2129
+ /**
2130
+ * Unsubscribe from a notification channel
2131
+ * @param channel - Channel name
2132
+ */
2133
+ unsubscribeFromNotificationChannel(channel) {
2134
+ if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
2135
+ WebSocketManagerService.socket.send(JSON.stringify({
2136
+ type: 'unsubscribeNotifications',
2137
+ subscribedChannel: channel
2138
+ }));
2139
+ console.log(`📢 Unsubscribed from notification channel: ${channel}`);
2140
+ }
2141
+ else {
2142
+ console.error('WebSocket is not open. Cannot unsubscribe from notification channel.');
2143
+ }
2144
+ }
2145
+ // ═══════════════════════════════════════════════════════════════════════════
2146
+ // CONNECTION STATUS (Static getter for direct access)
2147
+ // ═══════════════════════════════════════════════════════════════════════════
2148
+ /**
2149
+ * Check if WebSocket connection is currently open
2150
+ * @returns true if connected, false otherwise
2151
+ */
2152
+ static isConnected() {
2153
+ return WebSocketManagerService.socket?.readyState === WebSocket.OPEN;
2154
+ }
2155
+ /**
2156
+ * Check if connection has been initialized (attempted at least once)
2157
+ * @returns true if connection was initialized, false otherwise
2158
+ */
2159
+ static isInitialized() {
2160
+ return WebSocketManagerService.connectionInitialized;
2161
+ }
2162
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WebSocketManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2163
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WebSocketManagerService, providedIn: 'root' }); }
2164
+ }
2165
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WebSocketManagerService, decorators: [{
2166
+ type: Injectable,
2167
+ args: [{
2168
+ providedIn: 'root',
2169
+ }]
2170
+ }] });
2171
+
1536
2172
  class RequestService extends WebsocketService {
1537
2173
  constructor() {
1538
2174
  super(...arguments);
@@ -1783,12 +2419,431 @@ function requestPolling(pollInterval, stopCondition$, isPending$) {
1783
2419
  isPending$.set(false);
1784
2420
  }
1785
2421
  }
1786
- catch (e) {
1787
- // no-op if setting fails
2422
+ catch (e) {
2423
+ // no-op if setting fails
2424
+ }
2425
+ }), takeUntil(stopCondition$));
2426
+ };
2427
+ }
2428
+
2429
+ /**
2430
+ * Message Tracker Service - Guaranteed Message Delivery
2431
+ *
2432
+ * Tracks message IDs, detects gaps, manages acknowledgments, and handles sync.
2433
+ * Ensures frontend clients never miss messages even during disconnections.
2434
+ *
2435
+ * Features:
2436
+ * - Per-channel message sequence tracking
2437
+ * - Automatic gap detection and recovery
2438
+ * - Batch acknowledgments (configurable interval)
2439
+ * - Reconnection sync with last-seen tracking
2440
+ * - Gap threshold alerting (10 messages before UI notification)
2441
+ */
2442
+ class MessageTrackerService {
2443
+ // ═══════════════════════════════════════════════════════════════════════════
2444
+ // INITIALIZATION
2445
+ // ═══════════════════════════════════════════════════════════════════════════
2446
+ constructor(wsManager) {
2447
+ this.wsManager = wsManager;
2448
+ // ═══════════════════════════════════════════════════════════════════════════
2449
+ // CONFIGURATION
2450
+ // ═══════════════════════════════════════════════════════════════════════════
2451
+ /** Batch acknowledgment interval in milliseconds */
2452
+ this.BATCH_ACK_INTERVAL_MS = 5000; // 5 seconds
2453
+ /** Maximum gap before alerting UI */
2454
+ this.GAP_THRESHOLD = 10;
2455
+ /** Maximum messages to store in pending acks before forcing batch */
2456
+ this.MAX_PENDING_ACKS = 100;
2457
+ // ═══════════════════════════════════════════════════════════════════════════
2458
+ // STATE TRACKING
2459
+ // ═══════════════════════════════════════════════════════════════════════════
2460
+ /**
2461
+ * Track last seen message ID per channel
2462
+ * Map<channelName, lastMessageId>
2463
+ */
2464
+ this.lastSeen = new Map();
2465
+ /**
2466
+ * Expected next message ID per channel (for gap detection)
2467
+ * Map<channelName, nextExpectedId>
2468
+ */
2469
+ this.expectedSequence = new Map();
2470
+ /**
2471
+ * Pending acknowledgments per channel
2472
+ * Map<channelName, Set<messageId>>
2473
+ */
2474
+ this.pendingAcks = new Map();
2475
+ /**
2476
+ * Gap count per channel (for threshold alerting)
2477
+ * Map<channelName, gapCount>
2478
+ */
2479
+ this.gapCounts = new Map();
2480
+ /**
2481
+ * Replay mode tracking (per channel)
2482
+ * Used to distinguish replay vs live messages
2483
+ */
2484
+ this.inReplayMode = new Map();
2485
+ /**
2486
+ * Gap retry count (for retry logic)
2487
+ * Map<channel, retryCount>
2488
+ */
2489
+ this.gapRetryCount = new Map();
2490
+ /** Batch acknowledgment timer */
2491
+ this.batchAckTimer = null;
2492
+ /** Subscription to WebSocket messages */
2493
+ this.messagesSubscription = null;
2494
+ /**
2495
+ * Track channels we want to be subscribed to (survives disconnect/reconnect)
2496
+ * This is separate from WebSocketManagerService.subscribedChannels which tracks
2497
+ * actual active subscriptions
2498
+ */
2499
+ this.intendedChannels = new Set();
2500
+ /** Observable to emit processed messages to UI components */
2501
+ this.messagesSubject = new BehaviorSubject(null);
2502
+ this.messages$ = this.messagesSubject.asObservable();
2503
+ // Restore state from sessionStorage (survives page reload)
2504
+ this.restoreLastSeen();
2505
+ this.restoreIntendedChannels();
2506
+ // Start batch acknowledgment timer
2507
+ this.startBatchAckTimer();
2508
+ // Subscribe to all incoming WebSocket messages
2509
+ this.messagesSubscription = this.wsManager.messages$.subscribe(msg => {
2510
+ if (msg) {
2511
+ this.onMessage(msg);
2512
+ }
2513
+ });
2514
+ // Listen for reconnect events - re-subscribe with lastSeenId for sync
2515
+ this.wsManager.onReconnect$.subscribe(() => {
2516
+ console.log('🔄 Reconnection detected, re-subscribing with lastSeenId...');
2517
+ this.reSubscribeAllChannels();
2518
+ });
2519
+ console.log('✅ MessageTrackerService initialized');
2520
+ }
2521
+ ngOnDestroy() {
2522
+ this.stopBatchAckTimer();
2523
+ this.persistLastSeen(); // This now also persists intendedChannels
2524
+ this.messagesSubscription?.unsubscribe();
2525
+ console.log('🛑 MessageTrackerService destroyed');
2526
+ }
2527
+ // ═══════════════════════════════════════════════════════════════════════════
2528
+ // MESSAGE HANDLING
2529
+ // ═══════════════════════════════════════════════════════════════════════════
2530
+ /**
2531
+ * Process incoming message
2532
+ * Tracks ID, detects gaps, queues acknowledgment
2533
+ */
2534
+ onMessage(msg) {
2535
+ const { type, channel, messageId, isReplay } = msg;
2536
+ // Only track messages with IDs (ignore legacy messages without messageId)
2537
+ if (messageId === undefined || messageId === null) {
2538
+ console.warn('⚠️ Received message without ID (legacy format):', msg);
2539
+ // Still forward legacy messages to UI
2540
+ this.messagesSubject.next(msg);
2541
+ return;
2542
+ }
2543
+ // Handle replay mode transitions
2544
+ if (isReplay && !this.inReplayMode.get(channel)) {
2545
+ console.log(`🔄 Entering replay mode for channel: ${channel}`);
2546
+ this.inReplayMode.set(channel, true);
2547
+ }
2548
+ else if (!isReplay && this.inReplayMode.get(channel)) {
2549
+ console.log(`✅ Exiting replay mode for channel: ${channel}`);
2550
+ this.inReplayMode.set(channel, false);
2551
+ }
2552
+ // Track message and detect gaps
2553
+ this.trackMessage(channel, messageId, isReplay);
2554
+ // Forward message to UI components
2555
+ this.messagesSubject.next(msg);
2556
+ }
2557
+ /**
2558
+ * Track message ID and detect gaps
2559
+ */
2560
+ trackMessage(channel, messageId, isReplay) {
2561
+ const expected = this.expectedSequence.get(channel) || 1;
2562
+ // Gap detection
2563
+ if (messageId !== expected) {
2564
+ console.warn(`⚠️ Gap detected in channel "${channel}": expected ${expected}, got ${messageId}`);
2565
+ // Increment gap count
2566
+ const currentGapCount = this.gapCounts.get(channel) || 0;
2567
+ const newGapCount = currentGapCount + 1;
2568
+ this.gapCounts.set(channel, newGapCount);
2569
+ // Check threshold
2570
+ if (newGapCount >= this.GAP_THRESHOLD) {
2571
+ console.error(`🚨 Gap threshold exceeded for channel "${channel}": ${newGapCount} gaps`);
2572
+ this.onGapThresholdExceeded(channel, newGapCount);
2573
+ }
2574
+ // Request gap fill
2575
+ this.requestGapFill(channel, expected, messageId - 1);
2576
+ }
2577
+ else {
2578
+ // Reset gap count and retry count on successful sequence
2579
+ this.gapCounts.set(channel, 0);
2580
+ this.gapRetryCount.set(channel, 0);
2581
+ }
2582
+ // Update tracking
2583
+ this.lastSeen.set(channel, messageId);
2584
+ this.expectedSequence.set(channel, messageId + 1);
2585
+ // Persist to sessionStorage (debounced in production)
2586
+ this.persistLastSeen();
2587
+ // Queue for acknowledgment
2588
+ this.queueAck(channel, messageId);
2589
+ // Check if we should force batch ack due to volume
2590
+ const pendingCount = this.pendingAcks.get(channel)?.size || 0;
2591
+ if (pendingCount >= this.MAX_PENDING_ACKS) {
2592
+ console.log(`📦 Forcing batch ack: ${pendingCount} pending messages`);
2593
+ this.sendBatchAck(channel);
2594
+ }
2595
+ }
2596
+ // ═══════════════════════════════════════════════════════════════════════════
2597
+ // ACKNOWLEDGMENT MANAGEMENT
2598
+ // ═══════════════════════════════════════════════════════════════════════════
2599
+ /**
2600
+ * Queue message for batch acknowledgment
2601
+ */
2602
+ queueAck(channel, messageId) {
2603
+ if (!this.pendingAcks.has(channel)) {
2604
+ this.pendingAcks.set(channel, new Set());
2605
+ }
2606
+ this.pendingAcks.get(channel).add(messageId);
2607
+ }
2608
+ /**
2609
+ * Start batch acknowledgment timer
2610
+ */
2611
+ startBatchAckTimer() {
2612
+ this.batchAckTimer = setInterval(() => {
2613
+ this.sendAllBatchAcks();
2614
+ }, this.BATCH_ACK_INTERVAL_MS);
2615
+ }
2616
+ /**
2617
+ * Stop batch acknowledgment timer
2618
+ */
2619
+ stopBatchAckTimer() {
2620
+ if (this.batchAckTimer) {
2621
+ clearInterval(this.batchAckTimer);
2622
+ this.batchAckTimer = null;
2623
+ }
2624
+ }
2625
+ /**
2626
+ * Send batch acknowledgments for all channels with pending acks
2627
+ */
2628
+ sendAllBatchAcks() {
2629
+ this.pendingAcks.forEach((messageIds, channel) => {
2630
+ if (messageIds.size > 0) {
2631
+ this.sendBatchAck(channel);
2632
+ }
2633
+ });
2634
+ }
2635
+ /**
2636
+ * Send batch acknowledgment for a specific channel
2637
+ */
2638
+ sendBatchAck(channel) {
2639
+ const messageIds = this.pendingAcks.get(channel);
2640
+ if (!messageIds || messageIds.size === 0) {
2641
+ return;
2642
+ }
2643
+ // Convert Set to sorted array
2644
+ const sortedIds = Array.from(messageIds).sort((a, b) => a - b);
2645
+ const ackUpTo = sortedIds[sortedIds.length - 1];
2646
+ console.log(`📦 Batch ack for channel "${channel}": ackUpTo=${ackUpTo} (${messageIds.size} messages)`);
2647
+ this.wsManager.send({
2648
+ type: 'messageAckBatch',
2649
+ channel,
2650
+ ackUpTo,
2651
+ // Optional: include skippedIds if you want to report gaps
2652
+ // skippedIds: this.getSkippedIds(channel, ackUpTo)
2653
+ });
2654
+ // Clear pending acks
2655
+ messageIds.clear();
2656
+ }
2657
+ // ═══════════════════════════════════════════════════════════════════════════
2658
+ // GAP RECOVERY
2659
+ // ═══════════════════════════════════════════════════════════════════════════
2660
+ /**
2661
+ * Request missing messages from server
2662
+ */
2663
+ requestGapFill(channel, fromId, toId) {
2664
+ const retries = this.gapRetryCount.get(channel) || 0;
2665
+ if (retries >= 3) {
2666
+ console.error(`🚨 Gap fill failed after 3 retries for channel "${channel}": ${fromId} to ${toId}`);
2667
+ // Could emit to UI for manual intervention
2668
+ return;
2669
+ }
2670
+ console.log(`🔍 Requesting gap fill for channel "${channel}": ${fromId} to ${toId} (attempt ${retries + 1})`);
2671
+ this.wsManager.send({
2672
+ type: 'gapRequest',
2673
+ channel,
2674
+ fromId,
2675
+ toId
2676
+ });
2677
+ this.gapRetryCount.set(channel, retries + 1);
2678
+ }
2679
+ /**
2680
+ * Handle gap threshold exceeded (optional: alert UI)
2681
+ */
2682
+ onGapThresholdExceeded(channel, gapCount) {
2683
+ console.warn(`⚠️ High gap count for channel "${channel}": ${gapCount} missed messages`);
2684
+ // Could emit to a separate observable for UI components to subscribe to
2685
+ // this.gapAlerts.next({ channel, gapCount });
2686
+ }
2687
+ // ═══════════════════════════════════════════════════════════════════════════
2688
+ // SUBSCRIPTION MANAGEMENT
2689
+ // ═══════════════════════════════════════════════════════════════════════════
2690
+ /**
2691
+ * Subscribe to channel with sync support
2692
+ * Automatically includes lastSeenId for replay
2693
+ */
2694
+ subscribeToChannel(channel, userData) {
2695
+ const lastSeenId = this.lastSeen.get(channel) || 0;
2696
+ console.log(`📥 Subscribing to channel "${channel}" with lastSeenId=${lastSeenId}`);
2697
+ console.log(`📝 Adding to intendedChannels (now has ${this.intendedChannels.size + 1} channels)`);
2698
+ // Track this channel as intended (survives disconnect/reconnect)
2699
+ this.intendedChannels.add(channel);
2700
+ this.persistIntendedChannels();
2701
+ this.wsManager.sendSubscribeWithLastSeen(channel, userData, lastSeenId);
2702
+ }
2703
+ /**
2704
+ * Re-subscribe to all previously subscribed channels after reconnect
2705
+ * Includes lastSeenId for each channel to trigger message replay
2706
+ */
2707
+ reSubscribeAllChannels() {
2708
+ // Use intendedChannels which survives disconnect/reconnect
2709
+ const channels = Array.from(this.intendedChannels);
2710
+ console.log(`🔄 Re-subscribing to ${channels.length} channel(s) with lastSeenId...`);
2711
+ channels.forEach((channel) => {
2712
+ this.subscribeToChannel(channel);
2713
+ });
2714
+ console.log(`✅ Re-subscription complete for ${channels.length} channel(s)`);
2715
+ }
2716
+ // ═══════════════════════════════════════════════════════════════════════════
2717
+ // STATE PERSISTENCE
2718
+ // ═══════════════════════════════════════════════════════════════════════════
2719
+ /**
2720
+ * Persist lastSeen to sessionStorage (survives page reload)
2721
+ */
2722
+ persistLastSeen() {
2723
+ try {
2724
+ const data = JSON.stringify(Array.from(this.lastSeen.entries()));
2725
+ sessionStorage.setItem('messageLastSeen', data);
2726
+ // Also persist intendedChannels
2727
+ this.persistIntendedChannels();
2728
+ }
2729
+ catch (err) {
2730
+ console.warn('⚠️ Failed to persist lastSeen:', err.message);
2731
+ }
2732
+ }
2733
+ /**
2734
+ * Restore lastSeen from sessionStorage
2735
+ */
2736
+ restoreLastSeen() {
2737
+ try {
2738
+ const data = sessionStorage.getItem('messageLastSeen');
2739
+ if (data) {
2740
+ const entries = JSON.parse(data);
2741
+ this.lastSeen = new Map(entries);
2742
+ // Rebuild expectedSequence from restored lastSeen
2743
+ this.lastSeen.forEach((lastId, channel) => {
2744
+ this.expectedSequence.set(channel, lastId + 1);
2745
+ });
2746
+ console.log('📥 Restored lastSeen from sessionStorage:', this.lastSeen.size, 'channels');
2747
+ }
2748
+ }
2749
+ catch (err) {
2750
+ console.warn('⚠️ Failed to restore lastSeen:', err.message);
2751
+ }
2752
+ }
2753
+ /**
2754
+ * Persist intendedChannels to sessionStorage
2755
+ */
2756
+ persistIntendedChannels() {
2757
+ try {
2758
+ const channels = Array.from(this.intendedChannels);
2759
+ sessionStorage.setItem('intendedChannels', JSON.stringify(channels));
2760
+ }
2761
+ catch (err) {
2762
+ console.warn('⚠️ Failed to persist intendedChannels:', err.message);
2763
+ }
2764
+ }
2765
+ /**
2766
+ * Restore intendedChannels from sessionStorage
2767
+ */
2768
+ restoreIntendedChannels() {
2769
+ try {
2770
+ const data = sessionStorage.getItem('intendedChannels');
2771
+ if (data) {
2772
+ const channels = JSON.parse(data);
2773
+ this.intendedChannels = new Set(channels);
2774
+ console.log('📥 Restored intendedChannels from sessionStorage:', this.intendedChannels.size, 'channels');
2775
+ }
2776
+ }
2777
+ catch (err) {
2778
+ console.warn('⚠️ Failed to restore intendedChannels:', err.message);
2779
+ }
2780
+ }
2781
+ // ═══════════════════════════════════════════════════════════════════════════
2782
+ // PUBLIC ACCESSORS
2783
+ // ═══════════════════════════════════════════════════════════════════════════
2784
+ /**
2785
+ * Get last seen message ID for a channel
2786
+ */
2787
+ getLastSeenId(channel) {
2788
+ return this.lastSeen.get(channel) || 0;
2789
+ }
2790
+ /**
2791
+ * Get expected next message ID for a channel
2792
+ */
2793
+ getExpectedNextId(channel) {
2794
+ return this.expectedSequence.get(channel) || 1;
2795
+ }
2796
+ /**
2797
+ * Check if currently in replay mode for a channel
2798
+ */
2799
+ isInReplayMode(channel) {
2800
+ return this.inReplayMode.get(channel) || false;
2801
+ }
2802
+ /**
2803
+ * Get gap count for a channel
2804
+ */
2805
+ getGapCount(channel) {
2806
+ return this.gapCounts.get(channel) || 0;
2807
+ }
2808
+ // ═══════════════════════════════════════════════════════════════════════════
2809
+ // UTILITY METHODS
2810
+ // ═══════════════════════════════════════════════════════════════════════════
2811
+ /**
2812
+ * Get skipped IDs between expected and received (for detailed gap reporting)
2813
+ */
2814
+ getSkippedIds(channel, upToId) {
2815
+ const expected = this.expectedSequence.get(channel) || 1;
2816
+ const skipped = [];
2817
+ for (let i = expected; i < upToId; i++) {
2818
+ const pending = this.pendingAcks.get(channel);
2819
+ if (!pending?.has(i)) {
2820
+ skipped.push(i);
1788
2821
  }
1789
- }), takeUntil(stopCondition$));
1790
- };
2822
+ }
2823
+ return skipped;
2824
+ }
2825
+ /**
2826
+ * Clear all tracking state (useful for debugging or manual reset)
2827
+ */
2828
+ clearState() {
2829
+ this.lastSeen.clear();
2830
+ this.expectedSequence.clear();
2831
+ this.pendingAcks.clear();
2832
+ this.gapCounts.clear();
2833
+ this.inReplayMode.clear();
2834
+ this.gapRetryCount.clear();
2835
+ this.persistLastSeen();
2836
+ console.log('🧹 MessageTracker state cleared');
2837
+ }
2838
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageTrackerService, deps: [{ token: WebSocketManagerService }], target: i0.ɵɵFactoryTarget.Injectable }); }
2839
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageTrackerService, providedIn: 'root' }); }
1791
2840
  }
2841
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageTrackerService, decorators: [{
2842
+ type: Injectable,
2843
+ args: [{
2844
+ providedIn: 'root',
2845
+ }]
2846
+ }], ctorParameters: () => [{ type: WebSocketManagerService }] });
1792
2847
 
1793
2848
  class DatabaseStorage {
1794
2849
  constructor(table = '', expiresIn) {
@@ -1877,6 +2932,12 @@ class HTTPManagerService extends RequestService {
1877
2932
  this.toastMessage = inject(ToastMessageDisplayService);
1878
2933
  this.ng_injector = inject(Injector);
1879
2934
  this.objectMergerService = inject(ObjectMergerService);
2935
+ this.wsManager = inject(WebSocketManagerService);
2936
+ this.messageTracker = inject(MessageTrackerService);
2937
+ // Delegate WebSocket observables to WebSocketManagerService (singleton)
2938
+ this.connectionStatus$ = this.wsManager.connectionStatus$;
2939
+ this.messages$ = this.messageTracker.messages$; // Messages flow through MessageTrackerService
2940
+ this.subscribedChannels$ = this.wsManager.subscribedChannels$;
1880
2941
  this.countdown = new BehaviorSubject(0);
1881
2942
  this.countdown$ = this.countdown.asObservable();
1882
2943
  this.error = new BehaviorSubject(false);
@@ -1887,6 +2948,130 @@ class HTTPManagerService extends RequestService {
1887
2948
  this.config = ApiRequest.adapt();
1888
2949
  this.config = (configOptions) ? ApiRequest.adapt(configOptions.httpRequestOptions) : this.config;
1889
2950
  }
2951
+ // ═══════════════════════════════════════════════════════════════════════════
2952
+ // WEBSOCKET METHODS (Delegated to WebSocketManagerService singleton)
2953
+ // ═══════════════════════════════════════════════════════════════════════════
2954
+ /**
2955
+ * Connect to WebSocket server
2956
+ * Only the first call across ALL instances will establish a connection
2957
+ */
2958
+ connect(options, jwtToken) {
2959
+ this.wsManager.connect(options, jwtToken);
2960
+ }
2961
+ /**
2962
+ * Disconnect from WebSocket server
2963
+ */
2964
+ disconnect() {
2965
+ this.wsManager.disconnect();
2966
+ }
2967
+ /**
2968
+ * Subscribe to a channel
2969
+ */
2970
+ subscribeToChannel(channel, userData) {
2971
+ this.messageTracker.subscribeToChannel(channel, userData);
2972
+ }
2973
+ /**
2974
+ * Subscribe to multiple channels
2975
+ */
2976
+ subscribeToChannels(channels, userData) {
2977
+ channels.forEach(channel => this.messageTracker.subscribeToChannel(channel, userData));
2978
+ }
2979
+ /**
2980
+ * Unsubscribe from a channel
2981
+ */
2982
+ unsubscribeFromChannel(channel) {
2983
+ this.wsManager.unsubscribeFromChannel(channel);
2984
+ }
2985
+ /**
2986
+ * Get currently subscribed channels
2987
+ */
2988
+ getSubscribedChannels() {
2989
+ return this.wsManager.getSubscribedChannels();
2990
+ }
2991
+ /**
2992
+ * Send broadcast message
2993
+ */
2994
+ sendBroadcast(content) {
2995
+ this.wsManager.sendBroadcast(content);
2996
+ }
2997
+ /**
2998
+ * Send message in channel (state manager message)
2999
+ */
3000
+ sendMessageInChannel(channel, content) {
3001
+ this.wsManager.sendMessageInChannel(channel, content);
3002
+ }
3003
+ /**
3004
+ * Send channel message
3005
+ */
3006
+ sendChannelMessage(channel, content) {
3007
+ this.wsManager.sendChannelMessage(channel, content);
3008
+ }
3009
+ /**
3010
+ * Send message to multiple channels
3011
+ */
3012
+ sendChannelMessageToChannels(channels, content) {
3013
+ this.wsManager.sendChannelMessageToChannels(channels, content);
3014
+ }
3015
+ /**
3016
+ * Send message to user
3017
+ */
3018
+ sendMessageToUser(user, content) {
3019
+ this.wsManager.sendMessageToUser(user, content);
3020
+ }
3021
+ /**
3022
+ * Get all channels
3023
+ */
3024
+ getAllChannels() {
3025
+ this.wsManager.getAllChannels();
3026
+ }
3027
+ /**
3028
+ * Create channel
3029
+ */
3030
+ createChannel(channel) {
3031
+ this.wsManager.createChannel(channel);
3032
+ }
3033
+ /**
3034
+ * Delete channel
3035
+ */
3036
+ deleteChannel(channel) {
3037
+ this.wsManager.deleteChannel(channel);
3038
+ }
3039
+ /**
3040
+ * Get users in channel
3041
+ */
3042
+ getUsersInChannel(channel) {
3043
+ this.wsManager.getUsersInChannel(channel);
3044
+ }
3045
+ /**
3046
+ * Create notification channel
3047
+ */
3048
+ createNotificationChannel(channel) {
3049
+ this.wsManager.createNotificationChannel(channel);
3050
+ }
3051
+ /**
3052
+ * Get notification channels
3053
+ */
3054
+ getNotificationChannels() {
3055
+ this.wsManager.getNotificationChannels();
3056
+ }
3057
+ /**
3058
+ * Get today's notification channels
3059
+ */
3060
+ getTodaysNotificationChannels() {
3061
+ this.wsManager.getTodaysNotificationChannels();
3062
+ }
3063
+ /**
3064
+ * Subscribe to notification channel
3065
+ */
3066
+ subscribeToNotificationChannel(channel, options, user) {
3067
+ this.wsManager.subscribeToNotificationChannel(channel, options, user);
3068
+ }
3069
+ /**
3070
+ * Unsubscribe from notification channel
3071
+ */
3072
+ unsubscribeFromNotificationChannel(channel) {
3073
+ this.wsManager.unsubscribeFromNotificationChannel(channel);
3074
+ }
1890
3075
  // REQUESTS
1891
3076
  getRequest(options, params) {
1892
3077
  this.isPending.next(true);
@@ -2416,7 +3601,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2416
3601
  }] }] });
2417
3602
 
2418
3603
  class ApiRequest {
2419
- constructor(server = '', path, headers, adapter, mapper, polling, retry, stream, streamType, displayError, saveAs, fileContentHeader, ws) {
3604
+ constructor(server = '', path, headers, adapter, mapper, polling, retry, stream, streamType, displayError, saveAs, fileContentHeader, ws, env) {
2420
3605
  this.server = server;
2421
3606
  this.path = path;
2422
3607
  this.headers = headers;
@@ -2430,10 +3615,11 @@ class ApiRequest {
2430
3615
  this.saveAs = saveAs;
2431
3616
  this.fileContentHeader = fileContentHeader;
2432
3617
  this.ws = ws;
3618
+ this.env = env;
2433
3619
  }
2434
3620
  static adapt(item) {
2435
3621
  const server = Array.isArray(item?.server) ? item.server.join('/') : item?.server || '';
2436
- return new ApiRequest(server, (item?.path) ? item.path : [], (item?.headers) ? item.headers : {}, item?.adapter, item?.mapper, item?.polling ? Math.floor(item.polling) : 0, item?.retry ? RetryOptions.adapt(item.retry) : RetryOptions.adapt(), (item?.stream) ? item.stream : false, item?.streamType || StreamType.AI_STREAMING, (item?.displayError) ? item.displayError : false, item?.saveAs, item?.fileContentHeader, item?.ws);
3622
+ return new ApiRequest(server, (item?.path) ? item.path : [], (item?.headers) ? item.headers : {}, item?.adapter, item?.mapper, item?.polling ? Math.floor(item.polling) : 0, item?.retry ? RetryOptions.adapt(item.retry) : RetryOptions.adapt(), (item?.stream) ? item.stream : false, item?.streamType || StreamType.AI_STREAMING, (item?.displayError) ? item.displayError : false, item?.saveAs, item?.fileContentHeader, item?.ws, item?.env || 'dev');
2437
3623
  }
2438
3624
  }
2439
3625
 
@@ -2448,17 +3634,18 @@ class RequestOptions {
2448
3634
  }
2449
3635
 
2450
3636
  class WSOptions {
2451
- constructor(id = '', wsServer = '', jwtToken = '', permissions, channels, user, retry) {
3637
+ constructor(id = '', wsServer = '', jwtToken = '', permissions, channels, wsUpdateChannels, user, retry) {
2452
3638
  this.id = id;
2453
3639
  this.wsServer = wsServer;
2454
3640
  this.jwtToken = jwtToken;
2455
3641
  this.permissions = permissions;
2456
3642
  this.channels = channels;
3643
+ this.wsUpdateChannels = wsUpdateChannels;
2457
3644
  this.user = user;
2458
3645
  this.retry = retry;
2459
3646
  }
2460
3647
  static adapt(item) {
2461
- return new WSOptions(item?.id, item?.wsServer, item?.jwtToken, (item?.permissions) ? item.permissions.split(',').map((p) => p.trim()) : [], item?.channels, item?.user, item?.retry);
3648
+ return new WSOptions(item?.id, item?.wsServer, item?.jwtToken, (item?.permissions) ? item.permissions.split(',').map((p) => p.trim()) : [], item?.channels, item?.wsUpdateChannels, item?.user, item?.retry);
2462
3649
  }
2463
3650
  }
2464
3651
 
@@ -2580,8 +3767,24 @@ class LocalStorageManagerService extends ComponentStore {
2580
3767
  console.warn('No App ID found - AppId not Provided');
2581
3768
  return;
2582
3769
  }
2583
- const storageData = (options.encrypted) ? this.encryption.decrypt(found.data, this.app.appID) : found.data;
2584
- return (this.isString(storageData)) ? JSON.parse(storageData) : storageData;
3770
+ let storageData = found.data;
3771
+ if (options.encrypted) {
3772
+ const decryptedData = this.encryption.decrypt(found.data, this.app.appID);
3773
+ if (decryptedData !== null) {
3774
+ storageData = decryptedData;
3775
+ }
3776
+ else {
3777
+ console.warn(`Failed to decrypt data for store: ${store}`);
3778
+ storageData = found.data;
3779
+ }
3780
+ }
3781
+ try {
3782
+ return (this.isString(storageData)) ? JSON.parse(storageData) : storageData;
3783
+ }
3784
+ catch (error) {
3785
+ console.warn(`Failed to parse storage data for store: ${store}`, error);
3786
+ return storageData; // Return raw data if parsing fails
3787
+ }
2585
3788
  }
2586
3789
  else {
2587
3790
  return null;
@@ -2749,10 +3952,39 @@ class LocalStorageManagerService extends ComponentStore {
2749
3952
  const str = localStorage.getItem(this.storageSettingsName);
2750
3953
  const localStr = localStorage.getItem(this.storageName);
2751
3954
  const sessionStr = sessionStorage.getItem(this.storageName);
2752
- const localData = (localStr) ? JSON.parse(localStr) : null;
2753
- const sessionData = (sessionStr) ? JSON.parse(sessionStr) : null;
2754
- const decryptedStr = str ? this.encryption.decrypt(str, this.app.appID) : null;
2755
- const settings = (decryptedStr && decryptedStr !== null) ? JSON.parse(decryptedStr) : [];
3955
+ let localData = [];
3956
+ let sessionData = [];
3957
+ let settings = [];
3958
+ try {
3959
+ localData = (localStr) ? JSON.parse(localStr) : [];
3960
+ }
3961
+ catch (error) {
3962
+ console.warn('Failed to parse local storage data:', error);
3963
+ localData = [];
3964
+ }
3965
+ try {
3966
+ sessionData = (sessionStr) ? JSON.parse(sessionStr) : [];
3967
+ }
3968
+ catch (error) {
3969
+ console.warn('Failed to parse session storage data:', error);
3970
+ sessionData = [];
3971
+ }
3972
+ if (str) {
3973
+ const decryptedStr = this.encryption.decrypt(str, this.app.appID);
3974
+ if (decryptedStr) {
3975
+ try {
3976
+ settings = JSON.parse(decryptedStr);
3977
+ }
3978
+ catch (error) {
3979
+ console.warn('Failed to parse decrypted settings:', error);
3980
+ settings = [];
3981
+ }
3982
+ }
3983
+ else {
3984
+ console.warn('Failed to decrypt settings data');
3985
+ settings = [];
3986
+ }
3987
+ }
2756
3988
  settings.forEach(store => {
2757
3989
  const expired = (store.options?.expires || 0) > 0 && this.utils.hasExpired(store.options?.expires || 0);
2758
3990
  if (!expired) {
@@ -2805,6 +4037,47 @@ class LocalStorageManagerService extends ComponentStore {
2805
4037
  ngOnDestroy() {
2806
4038
  this.persistence$.unsubscribe();
2807
4039
  }
4040
+ /**
4041
+ * Clears all stored data from localStorage and sessionStorage
4042
+ * Use this method to recover from corrupted data errors
4043
+ */
4044
+ clearAllStoredData() {
4045
+ try {
4046
+ localStorage.removeItem(this.storageSettingsName);
4047
+ localStorage.removeItem(this.storageName);
4048
+ sessionStorage.removeItem(this.storageName);
4049
+ console.log('Cleared all stored data');
4050
+ }
4051
+ catch (error) {
4052
+ console.error('Failed to clear stored data:', error);
4053
+ }
4054
+ }
4055
+ /**
4056
+ * Checks if stored data appears to be corrupted
4057
+ * Returns true if data appears corrupted
4058
+ */
4059
+ checkForCorruptedData() {
4060
+ try {
4061
+ const str = localStorage.getItem(this.storageSettingsName);
4062
+ if (str) {
4063
+ // Try to decrypt if it's encrypted
4064
+ const decryptedStr = this.encryption.decrypt(str, this.app.appID);
4065
+ if (decryptedStr !== null && decryptedStr !== undefined) {
4066
+ // Try to parse the decrypted data
4067
+ JSON.parse(decryptedStr);
4068
+ }
4069
+ else {
4070
+ // Decryption failed, data is likely corrupted
4071
+ return true;
4072
+ }
4073
+ }
4074
+ return false;
4075
+ }
4076
+ catch (error) {
4077
+ // Parsing failed, data is corrupted
4078
+ return true;
4079
+ }
4080
+ }
2808
4081
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocalStorageManagerService, deps: [{ token: CONFIG_SETTINGS_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
2809
4082
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocalStorageManagerService, providedIn: 'root' }); }
2810
4083
  }
@@ -2857,10 +4130,24 @@ class LocalStorageSignalsManagerService {
2857
4130
  console.warn('No App ID found - AppId not Provided');
2858
4131
  return null;
2859
4132
  }
2860
- const storageData = options.encrypted
2861
- ? this.encryption.decrypt(found.data, this.app.appID)
2862
- : found.data;
2863
- return this.isString(storageData) ? JSON.parse(storageData) : storageData;
4133
+ let storageData = found.data;
4134
+ if (options.encrypted) {
4135
+ const decryptedData = this.encryption.decrypt(found.data, this.app.appID);
4136
+ if (decryptedData !== null) {
4137
+ storageData = decryptedData;
4138
+ }
4139
+ else {
4140
+ console.warn(`Failed to decrypt data for store: ${store}`);
4141
+ storageData = found.data; // Use undecrypted data as fallback
4142
+ }
4143
+ }
4144
+ try {
4145
+ return this.isString(storageData) ? JSON.parse(storageData) : storageData;
4146
+ }
4147
+ catch (error) {
4148
+ console.warn(`Failed to parse storage data for store: ${store}`, error);
4149
+ return storageData; // Return raw data if parsing fails
4150
+ }
2864
4151
  });
2865
4152
  this.settings = computed(() => this.state().settings);
2866
4153
  this.setting = (store) => computed(() => this.state().settings.find(item => item.name === store) ?? null);
@@ -2988,8 +4275,23 @@ class LocalStorageSignalsManagerService {
2988
4275
  const sessionStr = sessionStorage.getItem(this.storageName);
2989
4276
  const localData = localStr ? JSON.parse(localStr) : [];
2990
4277
  const sessionData = sessionStr ? JSON.parse(sessionStr) : [];
2991
- const decryptedStr = str ? this.encryption.decrypt(str, this.app.appID) : null;
2992
- const settings = decryptedStr ? JSON.parse(decryptedStr) : [];
4278
+ let settings = [];
4279
+ if (str) {
4280
+ const decryptedStr = this.encryption.decrypt(str, this.app.appID);
4281
+ if (decryptedStr) {
4282
+ try {
4283
+ settings = JSON.parse(decryptedStr);
4284
+ }
4285
+ catch (error) {
4286
+ console.warn('Failed to parse decrypted settings:', error);
4287
+ settings = [];
4288
+ }
4289
+ }
4290
+ else {
4291
+ console.warn('Failed to decrypt settings data');
4292
+ settings = [];
4293
+ }
4294
+ }
2993
4295
  settings.forEach(store => {
2994
4296
  // normalize options so we compare numbers and compute expires correctly
2995
4297
  const options = SettingOptions.adapt(store.options);
@@ -3428,14 +4730,23 @@ class DatabaseManagerService extends DbService {
3428
4730
  }
3429
4731
  clearTable(table) {
3430
4732
  const tableName = this.cleanTableName(table);
3431
- return this.getDatabaseTable(tableName).pipe(switchMap((tableData) => {
3432
- if (!tableData) {
4733
+ console.log(`clearTable: Starting clear for table: ${tableName}`);
4734
+ try {
4735
+ const tableInstance = this.table(tableName);
4736
+ if (!tableInstance) {
3433
4737
  console.warn(`clearTable: Table '${tableName}' not found`);
3434
- return from([]);
4738
+ return of([]);
3435
4739
  }
3436
- console.log(`clearTable: Clearing all records from ${tableName}`);
3437
- return from(tableData.clear());
3438
- }));
4740
+ console.log(`clearTable: Clearing table '${tableName}'...`);
4741
+ // Use table.clear() directly and wrap in Observable
4742
+ return from(Promise.resolve().then(() => {
4743
+ return tableInstance.clear();
4744
+ })).pipe(tap(() => console.log(`clearTable: ✅ Table '${tableName}' cleared successfully`)), map(() => []));
4745
+ }
4746
+ catch (error) {
4747
+ console.error(`clearTable: ❌ Error:`, error);
4748
+ return of([]);
4749
+ }
3439
4750
  }
3440
4751
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DatabaseManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3441
4752
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DatabaseManagerService, providedIn: 'root' }); }
@@ -3448,13 +4759,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3448
4759
  }], ctorParameters: () => [] });
3449
4760
 
3450
4761
  class ChannelMessage {
3451
- constructor(sessionId = null, content = null) {
4762
+ constructor(messageId, channel, isReplay, sessionId = null, content = null, timestamp) {
4763
+ this.messageId = messageId;
4764
+ this.channel = channel;
4765
+ this.isReplay = isReplay;
3452
4766
  this.sessionId = sessionId;
3453
4767
  this.content = content;
4768
+ this.timestamp = timestamp;
3454
4769
  }
3455
4770
  static adapt(item) {
3456
- return new ChannelMessage(item?.sessionId || item?.id, // Support both for backward compatibility
3457
- item?.content);
4771
+ return new ChannelMessage(item?.messageId, item?.channel, item?.isReplay, item?.sessionId || item?.id, // Support both for backward compatibility
4772
+ item?.content, item?.timestamp);
3458
4773
  }
3459
4774
  }
3460
4775
 
@@ -3489,11 +4804,12 @@ class HTTPManagerStateService extends ComponentStore {
3489
4804
  getUsersForChannel$(channel) {
3490
4805
  return this.userListByChannel$.pipe(map(channelMap => channelMap.get(channel) || []));
3491
4806
  }
4807
+ // Message queue for WebSocket communication (processed when connection is established)
4808
+ static { this.wsCommunicationQueue = []; }
3492
4809
  constructor(apiOptions = ApiRequest.adapt(), dataType, database) {
3493
4810
  super(defaultState);
3494
4811
  this.apiOptions = apiOptions;
3495
4812
  this.dataType = dataType;
3496
- this.database = database;
3497
4813
  this.httpManagerService = inject(HTTPManagerService);
3498
4814
  this.dbManagerService = inject(DatabaseManagerService);
3499
4815
  this.localStorageManagerService = inject(LocalStorageManagerService);
@@ -3516,7 +4832,7 @@ class HTTPManagerStateService extends ComponentStore {
3516
4832
  this.messages$ = this.messages.asObservable();
3517
4833
  this.userListByChannel = new BehaviorSubject(new Map());
3518
4834
  this.userListByChannel$ = this.userListByChannel.asObservable();
3519
- // Legacy support - returns all unique users across all channels
4835
+ // Returns all unique users across all channels
3520
4836
  this.userList = new BehaviorSubject([]);
3521
4837
  this.userList$ = this.userList.asObservable();
3522
4838
  this.user = new BehaviorSubject(null);
@@ -3540,17 +4856,19 @@ class HTTPManagerStateService extends ComponentStore {
3540
4856
  this.latestCommunicationMessages$ = this.latestCommunicationMessages.asObservable();
3541
4857
  this.userAction = new BehaviorSubject(null);
3542
4858
  this.userAction$ = this.userAction.asObservable();
3543
- this.wsConnection = false;
3544
4859
  this.wsOptions = WSOptions.adapt();
3545
- // Expose raw WS connection status directly to UI
4860
+ // Expose raw WS connection status directly to UI (from singleton WebSocketManagerService)
3546
4861
  this.connectionStatus$ = this.httpManagerService.connectionStatus$;
3547
4862
  // WebSocket
3548
- this.initWS = this.effect((wsOptions$) => wsOptions$.pipe(
3549
- // tap((wsOptions) => { debugger
3550
- // this.wsOptions = wsOptions
3551
- // }),
3552
- switchMap((wsOptions) => merge(this.httpManagerService.connectionStatus$.pipe(tap((isConnected) => {
3553
- this.wsConnection = isConnected;
4863
+ this.initWS = this.effect((wsOptions$) => wsOptions$.pipe(switchMap((wsOptions) => merge(this.httpManagerService.connectionStatus$.pipe(tap((isConnected) => {
4864
+ // Process queued wsCommunication calls when connection becomes true
4865
+ if (isConnected && HTTPManagerStateService.wsCommunicationQueue.length > 0) {
4866
+ console.log(`🔄 Processing ${HTTPManagerStateService.wsCommunicationQueue.length} queued WS messages`);
4867
+ while (HTTPManagerStateService.wsCommunicationQueue.length > 0) {
4868
+ const queued = HTTPManagerStateService.wsCommunicationQueue.shift();
4869
+ this.sendWsCommunication(queued.method, queued.path);
4870
+ }
4871
+ }
3554
4872
  })), this.httpManagerService.messages$.pipe(tap((message) => {
3555
4873
  if (!message)
3556
4874
  return;
@@ -3558,6 +4876,8 @@ class HTTPManagerStateService extends ComponentStore {
3558
4876
  const currentMessages = this.messages.value;
3559
4877
  this.messages.next([...currentMessages, message]);
3560
4878
  console.log('Received:', message);
4879
+ // Debug: Log all message types
4880
+ console.log('📨 Message type:', message.type);
3561
4881
  if (message.error === 'JWT_INVALID') {
3562
4882
  this.shouldRetry = false;
3563
4883
  this.httpManagerService.disconnect();
@@ -3571,16 +4891,22 @@ class HTTPManagerStateService extends ComponentStore {
3571
4891
  switch (message.type) {
3572
4892
  case 'channelsList':
3573
4893
  console.log('💬 Channels:', message.channels);
3574
- // this.channelList = message.channels
3575
- // if (this.channelList.includes(wsOptions.id)) {
3576
- // this.httpManagerService.subscribeToChannel(wsOptions.id)
3577
- // } else {
3578
- // this.httpManagerService.createChannel(wsOptions.id)
3579
- // }
4894
+ console.log('🔍 channelsList received, checking connection status...');
4895
+ console.log('🔍 WebSocket connected:', WebSocketManagerService.isConnected());
4896
+ // Auto-subscribe to all channels from the list
4897
+ if (message.channels && message.channels.length > 0) {
4898
+ console.log('📥 Auto-subscribing to', message.channels.length, 'channel(s)');
4899
+ this.subscribeToChannels(message.channels);
4900
+ }
4901
+ else {
4902
+ console.log('⚠️ No channels to subscribe to');
4903
+ }
3580
4904
  this.channels.next(message.channels);
3581
4905
  break;
3582
4906
  case 'subscribed':
3583
4907
  console.log(`✅ Subscription confirmed: ${message.channel}`);
4908
+ // Track as subscribed now that server confirmed
4909
+ WebSocketManagerService.addSubscribedChannel(message.channel);
3584
4910
  break;
3585
4911
  case 'unsubscribed':
3586
4912
  console.log(`🔓 Unsubscription confirmed: ${message.channel}`);
@@ -3588,6 +4914,11 @@ class HTTPManagerStateService extends ComponentStore {
3588
4914
  case 'info':
3589
4915
  // Already subscribed or other info messages
3590
4916
  console.log(`ℹ️ Info: ${message.message}`);
4917
+ // If it's an "Already subscribed" message, treat it as subscription confirmation
4918
+ if (message.message?.includes('Already subscribed')) {
4919
+ console.log(`✅ Treating info as subscription confirmation for: ${message.data}`);
4920
+ WebSocketManagerService.addSubscribedChannel(message.data);
4921
+ }
3591
4922
  break;
3592
4923
  case 'stateMangerMessage':
3593
4924
  // Compare sender's session ID with current user's ID
@@ -3606,19 +4937,58 @@ class HTTPManagerStateService extends ComponentStore {
3606
4937
  break;
3607
4938
  case 'channelMessage':
3608
4939
  // Handle channel-based messages (from sendChannelMessage)
3609
- // Structure: { type: 'channelMessage', channels: [...], sessionId: {id, ldap, name, email}, content: {message payload} }
4940
+ // Structure: { type: 'channelMessage', messageId, channel, sessionId, content, timestamp }
3610
4941
  // Skip messages from self
3611
- const senderSessionId = message.sessionId?.id;
4942
+ const senderSessionId = message.sessionId?.id || message.sessionId;
3612
4943
  if (senderSessionId === this.user.value?.id) {
4944
+ console.log('🔇 Skipping message from self (sessionId match)');
3613
4945
  break;
3614
4946
  }
3615
4947
  console.log('💬 Channel Message received:', message);
3616
- if (message.content) {
4948
+ // Determine which channels this message was sent to
4949
+ const messageChannels = message.channel ? [message.channel] : [];
4950
+ // Construct expected channel path (without env prefix)
4951
+ const myPath = this.apiOptions.path || [];
4952
+ const myPathString = myPath.join('/');
4953
+ // Check if any of the message channels CONTAIN our path
4954
+ const isWsUpdateChannel = messageChannels.some((ch) => {
4955
+ // Strip SYS- prefix if present for comparison
4956
+ const cleanChannel = ch.replace('SYS-', '');
4957
+ // Check if channel contains our path (or starts with it)
4958
+ const matches = cleanChannel === myPathString ||
4959
+ cleanChannel.startsWith(myPathString + '/') ||
4960
+ cleanChannel.includes(myPathString);
4961
+ console.log(`🔍 Channel check: ${ch} contains ${myPathString}? ${matches}`);
4962
+ return matches;
4963
+ });
4964
+ // If it's the expected channel, trigger fetchRecord like stateManagerMessage
4965
+ if (isWsUpdateChannel && message.content?.path) {
4966
+ console.log('🔍 Message received on expected channel (path match):', myPathString);
4967
+ console.log('📄 Content:', message.content);
4968
+ console.log('📥 Fetching record for channel:', myPathString);
4969
+ const path = message.content.path;
4970
+ const method = message.content.method || 'UPDATE';
4971
+ this.userAction.next({ sessionId: message.sessionId, content: message.content });
4972
+ this.fetchRecord(RequestOptions.adapt({ path }), method);
4973
+ }
4974
+ else if (message.content) {
4975
+ // Handle message content directly
4976
+ console.log('📄 Processing message content:', message.content);
3617
4977
  this.appendMessages(ChannelMessage.adapt({
3618
4978
  sessionId: message.sessionId,
3619
4979
  content: message.content,
3620
4980
  }));
3621
4981
  }
4982
+ else {
4983
+ console.log('⚠️ Message does not contain data.content.path, skipping fetchRecord');
4984
+ }
4985
+ // Keep existing functionality for backward compatibility
4986
+ if (message.data?.content && !isWsUpdateChannel) {
4987
+ this.appendMessages(ChannelMessage.adapt({
4988
+ sessionId: message.data.sessionId,
4989
+ content: message.data.content,
4990
+ }));
4991
+ }
3622
4992
  break;
3623
4993
  case 'usersInChannel':
3624
4994
  console.log(`👥 Users in channel "${message.channel}":`, message.data.users);
@@ -3834,11 +5204,21 @@ class HTTPManagerStateService extends ComponentStore {
3834
5204
  // FETCH RECORD
3835
5205
  this.fetchRecord = (options, method) => this.effect(() => of(RequestOptions.adapt(options)).pipe(tap(() => console.log('🔄 fetchRecord effect triggered with path:', options?.path, 'method:', method)), switchMap((options) => {
3836
5206
  this.streamedResponse = [];
5207
+ // Temporarily update apiOptions.path with the path from the WebSocket message
5208
+ // This ensures the request goes to the correct endpoint
5209
+ const originalPath = this.apiOptions.path;
5210
+ if (options?.path && Array.isArray(options.path)) {
5211
+ this.apiOptions.path = options.path;
5212
+ console.log('🔧 Temporarily set apiOptions.path to:', options.path);
5213
+ }
3837
5214
  const requestOptions = this.updateRequestOptions(options?.headers);
3838
- console.log('🌐 Making GET request to path:', options?.path);
3839
- return this.httpManagerService.getRequest(requestOptions, options?.path)
5215
+ console.log('🌐 Making GET request to path:', this.apiOptions.path);
5216
+ return this.httpManagerService.getRequest(requestOptions)
3840
5217
  .pipe(tap((data) => {
3841
5218
  console.log('📦 fetchRecord received data:', data);
5219
+ // Restore original path after request completes
5220
+ this.apiOptions.path = originalPath;
5221
+ console.log('🔧 Restored apiOptions.path to:', originalPath);
3842
5222
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3843
5223
  const id = options.path?.length ? options.path[options.path.length - 1] : null;
3844
5224
  if (method === 'DELETE') {
@@ -3860,8 +5240,16 @@ class HTTPManagerStateService extends ComponentStore {
3860
5240
  return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, id);
3861
5241
  if (method === 'UPDATE' && data)
3862
5242
  return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
3863
- if (method === 'CREATE' && data)
3864
- return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
5243
+ if (method === 'CREATE' && data) {
5244
+ // Validate that data has a valid id before saving to IndexedDB
5245
+ if (data && (data.id !== undefined && data.id !== null && data.id !== '')) {
5246
+ console.log('💾 Saving to IndexedDB:', { table: this.databaseOptions.table, id: data.id, data });
5247
+ return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
5248
+ }
5249
+ else {
5250
+ console.warn('⚠️ Skipping IndexedDB save: data.id is invalid', data);
5251
+ }
5252
+ }
3865
5253
  }
3866
5254
  return of(data);
3867
5255
  }));
@@ -3874,8 +5262,8 @@ class HTTPManagerStateService extends ComponentStore {
3874
5262
  .pipe(tap((data) => {
3875
5263
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3876
5264
  this.addData$(data);
3877
- if (this.wsConnection)
3878
- this.wsCommunication('CREATE', [...options?.path || [], data.id]);
5265
+ // Always call wsCommunication - it will queue if not connected
5266
+ this.wsCommunication('CREATE', [...options?.path || [], data.id]);
3879
5267
  }), concatMap((data) => {
3880
5268
  if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
3881
5269
  return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
@@ -3891,8 +5279,8 @@ class HTTPManagerStateService extends ComponentStore {
3891
5279
  .pipe(tap((data) => {
3892
5280
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3893
5281
  this.updateData$(data);
3894
- if (this.wsConnection)
3895
- this.wsCommunication('UPDATE', [...options?.path || []]);
5282
+ // Always call wsCommunication - it will queue if not connected
5283
+ this.wsCommunication('UPDATE', [...options?.path || []]);
3896
5284
  }), concatMap((data) => {
3897
5285
  if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
3898
5286
  return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
@@ -3908,8 +5296,8 @@ class HTTPManagerStateService extends ComponentStore {
3908
5296
  .pipe(tap((data) => {
3909
5297
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3910
5298
  this.deleteData$(data);
3911
- if (this.wsConnection)
3912
- this.wsCommunication('DELETE', [...options?.path || []]);
5299
+ // Always call wsCommunication - it will queue if not connected
5300
+ this.wsCommunication('DELETE', [...options?.path || []]);
3913
5301
  }), concatMap((data) => {
3914
5302
  if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
3915
5303
  return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, data.id);
@@ -3968,23 +5356,35 @@ class HTTPManagerStateService extends ComponentStore {
3968
5356
  return of([]);
3969
5357
  }));
3970
5358
  })));
3971
- this.databaseOptions = database;
3972
- this.maxRetries = this.apiOptions.ws?.retry?.times || 3;
3973
- this.retryDelay = (this.apiOptions.ws?.retry?.delay && this.apiOptions.ws.retry.delay * 1000) || 5 * 1000;
3974
- // Start next retry countdown at 0 to avoid showing 5000 pre-connection
3975
- this.wsNextRetry = new BehaviorSubject(0);
3976
- this.wsNextRetry$ = this.wsNextRetry.asObservable();
3977
- this.setApiRequestOptions(apiOptions, dataType, database);
3978
- if (this.databaseOptions && this.databaseOptions.table) {
3979
- this.localStorageManagerService.createStore({
3980
- name: this.databaseOptions.table,
3981
- data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions.expiresIn) } },
3982
- options: SettingOptions.adapt({
3983
- storage: StorageType.GLOBAL,
3984
- encrypted: false,
3985
- })
3986
- });
3987
- this.initDBStorage();
5359
+ try {
5360
+ this.databaseOptions = database;
5361
+ this.hasDatabase = (database?.table) ? true : false;
5362
+ this.maxRetries = this.apiOptions.ws?.retry?.times || 3;
5363
+ this.retryDelay = (this.apiOptions.ws?.retry?.delay && this.apiOptions.ws.retry.delay * 1000) || 5 * 1000;
5364
+ // Start next retry countdown at 0 to avoid showing 5000 pre-connection
5365
+ this.wsNextRetry = new BehaviorSubject(0);
5366
+ this.wsNextRetry$ = this.wsNextRetry.asObservable();
5367
+ this.setApiRequestOptions(apiOptions, dataType, database);
5368
+ if (this.databaseOptions && this.databaseOptions.table) {
5369
+ this.localStorageManagerService.createStore({
5370
+ name: this.databaseOptions.table,
5371
+ data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions.expiresIn) } },
5372
+ options: SettingOptions.adapt({
5373
+ storage: StorageType.GLOBAL,
5374
+ encrypted: false,
5375
+ })
5376
+ });
5377
+ this.initDBStorage();
5378
+ }
5379
+ }
5380
+ catch (error) {
5381
+ console.error('Error initializing HTTPManagerStateService:', error);
5382
+ // Initialize with safe defaults
5383
+ this.databaseOptions = undefined;
5384
+ this.maxRetries = 3;
5385
+ this.retryDelay = 5000;
5386
+ this.wsNextRetry = new BehaviorSubject(0);
5387
+ this.wsNextRetry$ = this.wsNextRetry.asObservable();
3988
5388
  }
3989
5389
  }
3990
5390
  /**
@@ -4041,8 +5441,13 @@ class HTTPManagerStateService extends ComponentStore {
4041
5441
  console.error('WSOptions invalid: wsServer is missing or empty');
4042
5442
  return;
4043
5443
  }
5444
+ // Clean up previous subscription to prevent duplicate handlers
5445
+ if (this.connectionStatusSubscription) {
5446
+ this.connectionStatusSubscription.unsubscribe();
5447
+ this.connectionStatusSubscription = undefined;
5448
+ }
4044
5449
  // Setup connection status monitoring (internal subscription to drive retry counters)
4045
- this.setupConnectionStatus().subscribe();
5450
+ this.connectionStatusSubscription = this.setupConnectionStatus().subscribe();
4046
5451
  // Make initial connection attempt
4047
5452
  console.log('🔄 Initial WebSocket connection attempt...');
4048
5453
  this.httpManagerService.connect(this.apiOptions.ws, this.apiOptions.ws.jwtToken || '');
@@ -4174,10 +5579,46 @@ class HTTPManagerStateService extends ComponentStore {
4174
5579
  }
4175
5580
  // WEBSOCKET COMMUNICATION (STATE MANAGER)
4176
5581
  wsCommunication(method, path) {
4177
- if (this.wsConnection && this.apiOptions.ws) {
5582
+ if (!this.apiOptions.ws) {
5583
+ console.warn('wsCommunication called but no WebSocket options configured');
5584
+ return;
5585
+ }
5586
+ // If connected, send immediately (check singleton WebSocketManagerService)
5587
+ if (WebSocketManagerService.isConnected()) {
5588
+ this.sendWsCommunication(method, path);
5589
+ }
5590
+ else {
5591
+ // Queue the message to be sent when connection is established
5592
+ console.log(`⏳ Queuing WS message (not connected): ${method} ${path ? JSON.stringify(path) : ''}`);
5593
+ HTTPManagerStateService.wsCommunicationQueue.push({ method, path });
5594
+ }
5595
+ }
5596
+ /**
5597
+ * Actually send the WebSocket message (called when connected or from queue)
5598
+ */
5599
+ sendWsCommunication(method, path) {
5600
+ if (this.apiOptions.ws) {
4178
5601
  const wsServer = this.apiOptions.ws.id;
5602
+ // Guard: Don't send if channel is empty
5603
+ if (!wsServer || wsServer === '') {
5604
+ console.error('❌ Cannot send WS message: Channel ID is empty!');
5605
+ return;
5606
+ }
5607
+ // DEBUG: Log what we're sending
5608
+ console.log('🔍 [DEBUG] sendWsCommunication called:', {
5609
+ wsServer,
5610
+ wsServerType: typeof wsServer,
5611
+ wsServerEmpty: wsServer === '' || wsServer === null || wsServer === undefined,
5612
+ method,
5613
+ path,
5614
+ user: this.apiOptions.ws.user,
5615
+ fullPayload: { method, path, user: this.apiOptions.ws.user }
5616
+ });
4179
5617
  this.httpManagerService.sendMessageInChannel(wsServer, { method, path, user: this.apiOptions.ws.user });
4180
5618
  }
5619
+ else {
5620
+ console.error('❌ [DEBUG] sendWsCommunication: apiOptions.ws is undefined!');
5621
+ }
4181
5622
  }
4182
5623
  /**
4183
5624
  * Send a message to channel(s)
@@ -4189,19 +5630,18 @@ class HTTPManagerStateService extends ComponentStore {
4189
5630
  const user = this.user.value;
4190
5631
  const messageInfo = ChannelMessage.adapt({ ...message, fromUser: user });
4191
5632
  console.log('📤 wsMessaging called with channels:', channels);
4192
- if (this.wsConnection && this.apiOptions.ws) {
5633
+ if (WebSocketManagerService.isConnected() && this.apiOptions.ws) {
4193
5634
  // If specific channels provided, send to each channel
4194
5635
  // Channels are passed as-is - caller is responsible for including the correct prefix
4195
5636
  if (channels && channels.length > 0) {
4196
5637
  console.log(`📤 Sending to ${channels.length} channel(s):`, channels);
4197
- channels.forEach(channel => {
4198
- if (channel === 'allChannels') {
4199
- this.httpManagerService.sendBroadcast(messageInfo);
4200
- }
4201
- else {
4202
- this.httpManagerService.sendChannelMessage(channel, messageInfo);
4203
- }
4204
- });
5638
+ // Send single message with all channels (more efficient)
5639
+ const user = this.user.value;
5640
+ const messageData = {
5641
+ sessionId: user,
5642
+ content: messageInfo
5643
+ };
5644
+ this.httpManagerService.sendChannelMessageToChannels(channels, messageData);
4205
5645
  }
4206
5646
  else {
4207
5647
  // Fallback to the primary WS channel (already prefixed with SYS-)
@@ -4216,7 +5656,7 @@ class HTTPManagerStateService extends ComponentStore {
4216
5656
  * @param channel - Base channel name (MES- prefix added automatically)
4217
5657
  */
4218
5658
  subscribeToMessageChannel(channel) {
4219
- if (this.wsConnection) {
5659
+ if (WebSocketManagerService.isConnected()) {
4220
5660
  const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
4221
5661
  this.httpManagerService.subscribeToChannel(prefixedChannel);
4222
5662
  console.log(`💬 Subscribed to message channel: ${prefixedChannel}`);
@@ -4230,7 +5670,7 @@ class HTTPManagerStateService extends ComponentStore {
4230
5670
  * @param channel - Base channel name (MES- prefix added automatically)
4231
5671
  */
4232
5672
  unsubscribeFromMessageChannel(channel) {
4233
- if (this.wsConnection) {
5673
+ if (WebSocketManagerService.isConnected()) {
4234
5674
  const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
4235
5675
  this.httpManagerService.unsubscribeFromChannel(prefixedChannel);
4236
5676
  console.log(`💬 Unsubscribed from message channel: ${prefixedChannel}`);
@@ -4246,8 +5686,11 @@ class HTTPManagerStateService extends ComponentStore {
4246
5686
  * Use subscribeToMessageChannel() for MES- prefixed channels
4247
5687
  */
4248
5688
  subscribeToChannel(channel) {
4249
- if (this.wsConnection) {
4250
- this.httpManagerService.subscribeToChannel(channel);
5689
+ if (WebSocketManagerService.isConnected()) {
5690
+ // Get current user data to send with subscription
5691
+ const currentUser = this.user.value;
5692
+ console.log('👤 Subscribing with user:', currentUser);
5693
+ this.httpManagerService.subscribeToChannel(channel, currentUser);
4251
5694
  }
4252
5695
  else {
4253
5696
  console.warn('Cannot subscribe: WebSocket not connected.');
@@ -4257,8 +5700,11 @@ class HTTPManagerStateService extends ComponentStore {
4257
5700
  * Subscribe to multiple channels at once
4258
5701
  */
4259
5702
  subscribeToChannels(channels) {
4260
- if (this.wsConnection) {
4261
- this.httpManagerService.subscribeToChannels(channels);
5703
+ if (WebSocketManagerService.isConnected()) {
5704
+ // Get current user data to send with subscription
5705
+ const currentUser = this.user.value;
5706
+ console.log('👤 Subscribing to', channels.length, 'channels with user:', currentUser);
5707
+ this.httpManagerService.subscribeToChannels(channels, currentUser);
4262
5708
  }
4263
5709
  else {
4264
5710
  console.warn('Cannot subscribe: WebSocket not connected.');
@@ -4268,7 +5714,7 @@ class HTTPManagerStateService extends ComponentStore {
4268
5714
  * Unsubscribe from a channel
4269
5715
  */
4270
5716
  unsubscribeFromChannel(channel) {
4271
- if (this.wsConnection) {
5717
+ if (WebSocketManagerService.isConnected()) {
4272
5718
  this.httpManagerService.unsubscribeFromChannel(channel);
4273
5719
  }
4274
5720
  else {
@@ -4291,7 +5737,7 @@ class HTTPManagerStateService extends ComponentStore {
4291
5737
  * Create a new channel on the server
4292
5738
  */
4293
5739
  createChannel(channel) {
4294
- if (this.wsConnection) {
5740
+ if (WebSocketManagerService.isConnected()) {
4295
5741
  this.httpManagerService.createChannel(channel);
4296
5742
  }
4297
5743
  else {
@@ -4302,7 +5748,7 @@ class HTTPManagerStateService extends ComponentStore {
4302
5748
  * Delete a channel from the server
4303
5749
  */
4304
5750
  deleteChannel(channel) {
4305
- if (this.wsConnection) {
5751
+ if (WebSocketManagerService.isConnected()) {
4306
5752
  this.httpManagerService.deleteChannel(channel);
4307
5753
  }
4308
5754
  else {
@@ -4313,7 +5759,7 @@ class HTTPManagerStateService extends ComponentStore {
4313
5759
  * Request list of all channels from server
4314
5760
  */
4315
5761
  getAllChannels() {
4316
- if (this.wsConnection) {
5762
+ if (WebSocketManagerService.isConnected()) {
4317
5763
  this.httpManagerService.getAllChannels();
4318
5764
  }
4319
5765
  else {
@@ -4324,7 +5770,7 @@ class HTTPManagerStateService extends ComponentStore {
4324
5770
  * Get users in a specific channel
4325
5771
  */
4326
5772
  getUsersInChannel(channel) {
4327
- if (this.wsConnection) {
5773
+ if (WebSocketManagerService.isConnected()) {
4328
5774
  this.httpManagerService.getUsersInChannel(channel);
4329
5775
  }
4330
5776
  else {
@@ -4338,7 +5784,7 @@ class HTTPManagerStateService extends ComponentStore {
4338
5784
  * @param channel - Base channel name (MES- prefix added automatically)
4339
5785
  */
4340
5786
  createNotificationChannel(channel) {
4341
- if (this.wsConnection) {
5787
+ if (WebSocketManagerService.isConnected()) {
4342
5788
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4343
5789
  this.httpManagerService.createNotificationChannel(prefixedChannel);
4344
5790
  console.log(`📢 Creating notification channel: ${prefixedChannel}`);
@@ -4351,7 +5797,7 @@ class HTTPManagerStateService extends ComponentStore {
4351
5797
  * Request list of all notification channels from server (in-memory)
4352
5798
  */
4353
5799
  getNotificationChannels() {
4354
- if (this.wsConnection) {
5800
+ if (WebSocketManagerService.isConnected()) {
4355
5801
  this.httpManagerService.getNotificationChannels();
4356
5802
  }
4357
5803
  else {
@@ -4363,7 +5809,7 @@ class HTTPManagerStateService extends ComponentStore {
4363
5809
  * Returns unique channels that have notifications posted today
4364
5810
  */
4365
5811
  getTodaysNotificationChannels() {
4366
- if (this.wsConnection) {
5812
+ if (WebSocketManagerService.isConnected()) {
4367
5813
  this.httpManagerService.getTodaysNotificationChannels();
4368
5814
  }
4369
5815
  else {
@@ -4375,7 +5821,7 @@ class HTTPManagerStateService extends ComponentStore {
4375
5821
  * @param channel - Base channel name (MES- prefix added automatically)
4376
5822
  */
4377
5823
  subscribeToNotificationChannel(channel, options, user) {
4378
- if (this.wsConnection) {
5824
+ if (WebSocketManagerService.isConnected()) {
4379
5825
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4380
5826
  this.httpManagerService.subscribeToNotificationChannel(prefixedChannel, options, user);
4381
5827
  console.log(`📢 Subscribing to notification channel: ${prefixedChannel}`);
@@ -4389,7 +5835,7 @@ class HTTPManagerStateService extends ComponentStore {
4389
5835
  * @param channel - Base channel name (MES- prefix added automatically)
4390
5836
  */
4391
5837
  unsubscribeFromNotificationChannel(channel) {
4392
- if (this.wsConnection) {
5838
+ if (WebSocketManagerService.isConnected()) {
4393
5839
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4394
5840
  this.httpManagerService.unsubscribeFromNotificationChannel(prefixedChannel);
4395
5841
  console.log(`📢 Unsubscribing from notification channel: ${prefixedChannel}`);
@@ -4403,7 +5849,7 @@ class HTTPManagerStateService extends ComponentStore {
4403
5849
  * @param channel - Base channel name (MES- prefix added automatically)
4404
5850
  */
4405
5851
  sendNotification(channel, content) {
4406
- if (this.wsConnection) {
5852
+ if (WebSocketManagerService.isConnected()) {
4407
5853
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4408
5854
  this.httpManagerService.sendNotification(prefixedChannel, content);
4409
5855
  console.log(`📢 Sending notification to channel: ${prefixedChannel}`);
@@ -4414,6 +5860,29 @@ class HTTPManagerStateService extends ComponentStore {
4414
5860
  }
4415
5861
  // --------------------------------------------------------------------------------------------------
4416
5862
  // MISC
5863
+ /**
5864
+ * Clear/flush all records from the database table
5865
+ * Does not clear localStorage metadata (expires info)
5866
+ * Does not re-fetch from API - leaves state empty for manual refresh
5867
+ */
5868
+ clearDatabase() {
5869
+ if (!this.hasDatabase || !this.databaseOptions?.table)
5870
+ return;
5871
+ const tableName = this.databaseOptions.table;
5872
+ this.dbManagerService.clearTable(tableName).subscribe({
5873
+ next: () => {
5874
+ if (this.dataType === DataType.ARRAY) {
5875
+ this.setData$([]);
5876
+ }
5877
+ else {
5878
+ this.setData$({});
5879
+ }
5880
+ },
5881
+ error: (err) => {
5882
+ console.error(`❌ Error clearing table ${tableName}:`, err);
5883
+ }
5884
+ });
5885
+ }
4417
5886
  isEmpty(obj) {
4418
5887
  return Object.keys(obj).length === 0;
4419
5888
  }
@@ -4424,7 +5893,7 @@ class HTTPManagerStateService extends ComponentStore {
4424
5893
  : { ...options.headers };
4425
5894
  return options;
4426
5895
  }
4427
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HTTPManagerStateService, deps: [{ token: API_OPTS }, { token: "dataType" }, { token: "database" }], target: i0.ɵɵFactoryTarget.Injectable }); }
5896
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HTTPManagerStateService, deps: [{ token: API_OPTS }, { token: "dataType" }, { token: DatabaseStorage }], target: i0.ɵɵFactoryTarget.Injectable }); }
4428
5897
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HTTPManagerStateService }); }
4429
5898
  }
4430
5899
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HTTPManagerStateService, decorators: [{
@@ -4435,10 +5904,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4435
5904
  }] }, { type: undefined, decorators: [{
4436
5905
  type: Inject,
4437
5906
  args: ["dataType"]
4438
- }] }, { type: undefined, decorators: [{
4439
- type: Inject,
4440
- args: ["database"]
4441
- }] }] });
5907
+ }] }, { type: DatabaseStorage }] });
4442
5908
 
4443
5909
  class StateStorageOptions {
4444
5910
  constructor(store, options, model) {
@@ -5274,11 +6740,11 @@ class RequestManagerStateDemoComponent {
5274
6740
  this.prompts = [];
5275
6741
  }
5276
6742
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerStateDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5277
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerStateDemoComponent, selector: "app-request-manager-state-demo", inputs: { server: "server", adapter: "adapter", mapper: "mapper" }, providers: [StateManagerDemoService, HTTPManagerService], viewQueries: [{ propertyName: "failedState", first: true, predicate: ["failedState"], descendants: true, static: true }, { propertyName: "pollingState", first: true, predicate: ["pollingState"], descendants: true, static: true }], ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem\" formGroupName=\"database\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\" [disabled]=\"data === null && data?.length === 0\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track item) {\n <mat-option [value]=\"item\">\n {{item.name || (item.first_name) | titlecase}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n \n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\"> \n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n \n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n \n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Data received\n </div>\n </div>\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i2$1.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i10.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i11.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.TitleCasePipe, name: "titlecase" }] }); }
6743
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerStateDemoComponent, selector: "app-request-manager-state-demo", inputs: { server: "server", adapter: "adapter", mapper: "mapper" }, providers: [StateManagerDemoService, HTTPManagerService], viewQueries: [{ propertyName: "failedState", first: true, predicate: ["failedState"], descendants: true, static: true }, { propertyName: "pollingState", first: true, predicate: ["pollingState"], descendants: true, static: true }], ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem\" formGroupName=\"database\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\" [disabled]=\"data === null && data?.length === 0\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track item) {\n <mat-option [value]=\"item\">\n {{item.name || (item.first_name) | titlecase}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Data received\n </div>\n </div>\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i2$1.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i10.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i11.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.TitleCasePipe, name: "titlecase" }] }); }
5278
6744
  }
5279
6745
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerStateDemoComponent, decorators: [{
5280
6746
  type: Component,
5281
- args: [{ selector: 'app-request-manager-state-demo', providers: [StateManagerDemoService, HTTPManagerService], standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem\" formGroupName=\"database\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\" [disabled]=\"data === null && data?.length === 0\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track item) {\n <mat-option [value]=\"item\">\n {{item.name || (item.first_name) | titlecase}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n \n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\"> \n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n \n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n \n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Data received\n </div>\n </div>\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"] }]
6747
+ args: [{ selector: 'app-request-manager-state-demo', providers: [StateManagerDemoService, HTTPManagerService], standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem\" formGroupName=\"database\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\" [disabled]=\"data === null && data?.length === 0\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track item) {\n <mat-option [value]=\"item\">\n {{item.name || (item.first_name) | titlecase}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Data received\n </div>\n </div>\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"] }]
5282
6748
  }], ctorParameters: () => [], propDecorators: { server: [{
5283
6749
  type: Input
5284
6750
  }], adapter: [{
@@ -5822,17 +7288,25 @@ class StateServiceDemo extends HTTPManagerStateService {
5822
7288
  }
5823
7289
  /**
5824
7290
  * Initialize WebSocket connection with server configuration
7291
+ * @param server - Backend server URL
7292
+ * @param wsServer - WebSocket server URL
7293
+ * @param jwtToken - JWT authentication token
7294
+ * @param user - User information
7295
+ * @param path - Path for constructing channel name (e.g., ['ai','tests'])
5825
7296
  */
5826
- updateConnection(server, wsServer, jwtToken, user) {
7297
+ updateConnection(server, wsServer, jwtToken, user, path = ['ai', 'tests']) {
7298
+ // Construct channel name from path: ['ai','tests'] → 'ai/tests'
7299
+ const channelId = path.join('/');
5827
7300
  this.setApiRequestOptions({
5828
7301
  server,
7302
+ path, // Set the path for HTTP requests
5829
7303
  retry: RetryOptions.adapt({
5830
7304
  times: 3,
5831
7305
  delay: 1,
5832
7306
  }),
5833
7307
  adapter: OIDCClient.adapt,
5834
7308
  ws: {
5835
- id: 'USERS123',
7309
+ id: channelId, // Use path-based channel ID instead of hardcoded 'USERS123'
5836
7310
  wsServer,
5837
7311
  jwtToken,
5838
7312
  user,
@@ -6157,7 +7631,7 @@ class StateDataRequestService extends HTTPManagerStateService {
6157
7631
  this.nextRetry$ = this.wsNextRetry$;
6158
7632
  this.path = ['ai', 'tests'];
6159
7633
  }
6160
- updateConnection(server, wsServer, jwtToken, user, path = [], wsChannel = '') {
7634
+ updateConnection(server, wsServer, jwtToken, user, path = []) {
6161
7635
  this.path = path;
6162
7636
  this.setApiRequestOptions({
6163
7637
  server,
@@ -6167,7 +7641,7 @@ class StateDataRequestService extends HTTPManagerStateService {
6167
7641
  }),
6168
7642
  adapter: OIDCClient.adapt,
6169
7643
  ws: {
6170
- id: wsChannel,
7644
+ id: this.path.join('/'),
6171
7645
  wsServer,
6172
7646
  jwtToken,
6173
7647
  user, // general info about user
@@ -6218,7 +7692,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6218
7692
  class WsDataControlComponent {
6219
7693
  constructor() {
6220
7694
  this.path = ['ai', 'tests'];
6221
- this.wsChannel = '';
6222
7695
  this.stateDataRequestService = inject(StateDataRequestService);
6223
7696
  this.user$ = this.stateDataRequestService.user$;
6224
7697
  this.users$ = this.stateDataRequestService.userList$;
@@ -6230,7 +7703,7 @@ class WsDataControlComponent {
6230
7703
  };
6231
7704
  }
6232
7705
  ngOnInit() {
6233
- this.stateDataRequestService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path, this.wsChannel);
7706
+ this.stateDataRequestService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
6234
7707
  this.stateDataRequestService.getData();
6235
7708
  }
6236
7709
  onGetData() {
@@ -6255,11 +7728,11 @@ class WsDataControlComponent {
6255
7728
  this.stateDataRequestService.deleteData(lastRec);
6256
7729
  }
6257
7730
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsDataControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6258
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsDataControlComponent, selector: "app-ws-data-control", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path", wsChannel: "wsChannel" }, ngImport: i0, template: "@if ((data$ | async); as data) {\n <div style=\"margin: 1rem;\">\n @if ((users$ |async); as users) {\n <div>\n @if (users.length > 0) {\n <h3 style=\"margin: 0;\">Connected Users</h3>\n } @else {\n <h3 style=\"margin: 0;\">No Users</h3>\n }\n <mat-chip-set>\n @for (user of users; track user.sessionId) {\n <mat-chip\n [class.user-chip--primary]=\"isUser(user, (user$ | async))\"\n [style.color]=\"isUser(user, (user$ | async)) ? '#fff' : null\"\n [disableRipple]=\"true\"\n >\n {{ user.name }}\n </mat-chip>\n }\n </mat-chip-set>\n </div>\n }\n <div style=\"margin-top: 1rem; margin-bottom: 1rem;\">\n <mat-divider></mat-divider>\n </div>\n\n <div class=\"box\" style=\"margin-bottom: 1rem;\" *ngIf=\"(userAction$ | async) as userAction\">\n {{ userAction?.content?.user?.name }} has {{ userAction?.content?.method }}\n </div>\n\n <h3 style=\"margin: 0;\">Data Actions</h3>\n <div style=\"display: flex; gap: 1rem; margin-bottom: 1rem;\">\n <button mat-stroked-button (click)=\"onGetData()\">Get Data</button>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button color=\"accent\" (click)=\"onUpdateData(data)\">Update Data</button>\n <button mat-stroked-button color=\"warn\" (click)=\"onRemoveData(data)\">Remove Data</button>\n <button mat-stroked-button color=\"primary\" (click)=\"onAddData()\">Add Data</button>\n </div>\n @if (data.length > 0) {\n <div>\n <table mat-table [dataSource]=\"data\" style=\"border: 1px solid grey;\">\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef> ID </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.id}} </td>\n </ng-container>\n <ng-container matColumnDef=\"spiffe\">\n <th mat-header-cell *matHeaderCellDef> Spiffe </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.spiffe}} </td>\n </ng-container>\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef> Name </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.first_name}} {{element.last_name}}</td>\n </ng-container>\n <ng-container matColumnDef=\"email\">\n <th mat-header-cell *matHeaderCellDef> Email </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.email}} </td>\n </ng-container>\n <tr mat-header-row *matHeaderRowDef=\"['id', 'spiffe', 'name', 'email']\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: ['id', 'spiffe', 'name', 'email'];\"></tr>\n </table>\n <div style=\"border: 1px solid grey; padding: .5rem; border-top: none;\">\n <h3 style=\"margin: 0;\">Total Records {{ data.length }}</h3>\n </div>\n </div>\n } @else {\n <div style=\"margin-top: 1rem; font-style: italic;\">\n No Data Available\n </div>\n }\n </div>\n}\n\n", styles: [".user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}.user-chip--primary :is(.mdc-evolution-chip__text-label,.mdc-evolution-chip__action,.mdc-evolution-chip__cell,.mat-mdc-chip-action-label){color:#fff!important}.user-chip--primary,.user-chip--primary *{color:#fff!important}:host ::ng-deep .user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}:host ::ng-deep .user-chip--primary .mdc-evolution-chip__text-label,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__action,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__cell,:host ::ng-deep .user-chip--primary .mat-mdc-chip-action-label,:host ::ng-deep .user-chip--primary *{color:#fff!important}.box{padding:.5rem;border:1px solid rgb(174,174,13);background-color:#ececaf}\n"], dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$1.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "component", type: i3$1.MatChipSet, selector: "mat-chip-set", inputs: ["disabled", "role", "tabIndex"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
7731
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsDataControlComponent, selector: "app-ws-data-control", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path" }, ngImport: i0, template: "@if ((data$ | async); as data) {\n <div style=\"margin: 1rem;\">\n @if ((users$ |async); as users) {\n <div>\n @if (users.length > 0) {\n <h3 style=\"margin: 0;\">Connected Users</h3>\n } @else {\n <h3 style=\"margin: 0;\">No Users</h3>\n }\n <mat-chip-set>\n @for (user of users; track $index) {\n <mat-chip\n [class.user-chip--primary]=\"isUser(user, (user$ | async))\"\n [style.color]=\"isUser(user, (user$ | async)) ? '#fff' : null\"\n [disableRipple]=\"true\"\n >\n {{ user.name || user.ldap || user.id || 'Anonymous' }}\n </mat-chip>\n }\n </mat-chip-set>\n </div>\n }\n <div style=\"margin-top: 1rem; margin-bottom: 1rem;\">\n <mat-divider></mat-divider>\n </div>\n\n <div class=\"box\" style=\"margin-bottom: 1rem;\" *ngIf=\"(userAction$ | async) as userAction\">\n {{ userAction?.content?.user?.name }} has {{ userAction?.content?.method }}\n </div>\n\n <h3 style=\"margin: 0;\">Data Actions</h3>\n <div style=\"display: flex; gap: 1rem; margin-bottom: 1rem;\">\n <button mat-stroked-button (click)=\"onGetData()\">Get Data</button>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button color=\"accent\" (click)=\"onUpdateData(data)\">Update Data</button>\n <button mat-stroked-button color=\"warn\" (click)=\"onRemoveData(data)\">Remove Data</button>\n <button mat-stroked-button color=\"primary\" (click)=\"onAddData()\">Add Data</button>\n </div>\n @if (data.length > 0) {\n <div>\n <table mat-table [dataSource]=\"data\" style=\"border: 1px solid grey;\">\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef> ID </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.id}} </td>\n </ng-container>\n <ng-container matColumnDef=\"spiffe\">\n <th mat-header-cell *matHeaderCellDef> Spiffe </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.spiffe}} </td>\n </ng-container>\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef> Name </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.first_name}} {{element.last_name}}</td>\n </ng-container>\n <ng-container matColumnDef=\"email\">\n <th mat-header-cell *matHeaderCellDef> Email </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.email}} </td>\n </ng-container>\n <tr mat-header-row *matHeaderRowDef=\"['id', 'spiffe', 'name', 'email']\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: ['id', 'spiffe', 'name', 'email'];\"></tr>\n </table>\n <div style=\"border: 1px solid grey; padding: .5rem; border-top: none;\">\n <h3 style=\"margin: 0;\">Total Records {{ data.length }}</h3>\n </div>\n </div>\n } @else {\n <div style=\"margin-top: 1rem; font-style: italic;\">\n No Data Available\n </div>\n }\n </div>\n}\n\n", styles: [".user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}.user-chip--primary :is(.mdc-evolution-chip__text-label,.mdc-evolution-chip__action,.mdc-evolution-chip__cell,.mat-mdc-chip-action-label){color:#fff!important}.user-chip--primary,.user-chip--primary *{color:#fff!important}:host ::ng-deep .user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}:host ::ng-deep .user-chip--primary .mdc-evolution-chip__text-label,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__action,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__cell,:host ::ng-deep .user-chip--primary .mat-mdc-chip-action-label,:host ::ng-deep .user-chip--primary *{color:#fff!important}.box{padding:.5rem;border:1px solid rgb(174,174,13);background-color:#ececaf}\n"], dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$1.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "component", type: i3$1.MatChipSet, selector: "mat-chip-set", inputs: ["disabled", "role", "tabIndex"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
6259
7732
  }
6260
7733
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsDataControlComponent, decorators: [{
6261
7734
  type: Component,
6262
- args: [{ selector: 'app-ws-data-control', standalone: false, template: "@if ((data$ | async); as data) {\n <div style=\"margin: 1rem;\">\n @if ((users$ |async); as users) {\n <div>\n @if (users.length > 0) {\n <h3 style=\"margin: 0;\">Connected Users</h3>\n } @else {\n <h3 style=\"margin: 0;\">No Users</h3>\n }\n <mat-chip-set>\n @for (user of users; track user.sessionId) {\n <mat-chip\n [class.user-chip--primary]=\"isUser(user, (user$ | async))\"\n [style.color]=\"isUser(user, (user$ | async)) ? '#fff' : null\"\n [disableRipple]=\"true\"\n >\n {{ user.name }}\n </mat-chip>\n }\n </mat-chip-set>\n </div>\n }\n <div style=\"margin-top: 1rem; margin-bottom: 1rem;\">\n <mat-divider></mat-divider>\n </div>\n\n <div class=\"box\" style=\"margin-bottom: 1rem;\" *ngIf=\"(userAction$ | async) as userAction\">\n {{ userAction?.content?.user?.name }} has {{ userAction?.content?.method }}\n </div>\n\n <h3 style=\"margin: 0;\">Data Actions</h3>\n <div style=\"display: flex; gap: 1rem; margin-bottom: 1rem;\">\n <button mat-stroked-button (click)=\"onGetData()\">Get Data</button>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button color=\"accent\" (click)=\"onUpdateData(data)\">Update Data</button>\n <button mat-stroked-button color=\"warn\" (click)=\"onRemoveData(data)\">Remove Data</button>\n <button mat-stroked-button color=\"primary\" (click)=\"onAddData()\">Add Data</button>\n </div>\n @if (data.length > 0) {\n <div>\n <table mat-table [dataSource]=\"data\" style=\"border: 1px solid grey;\">\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef> ID </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.id}} </td>\n </ng-container>\n <ng-container matColumnDef=\"spiffe\">\n <th mat-header-cell *matHeaderCellDef> Spiffe </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.spiffe}} </td>\n </ng-container>\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef> Name </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.first_name}} {{element.last_name}}</td>\n </ng-container>\n <ng-container matColumnDef=\"email\">\n <th mat-header-cell *matHeaderCellDef> Email </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.email}} </td>\n </ng-container>\n <tr mat-header-row *matHeaderRowDef=\"['id', 'spiffe', 'name', 'email']\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: ['id', 'spiffe', 'name', 'email'];\"></tr>\n </table>\n <div style=\"border: 1px solid grey; padding: .5rem; border-top: none;\">\n <h3 style=\"margin: 0;\">Total Records {{ data.length }}</h3>\n </div>\n </div>\n } @else {\n <div style=\"margin-top: 1rem; font-style: italic;\">\n No Data Available\n </div>\n }\n </div>\n}\n\n", styles: [".user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}.user-chip--primary :is(.mdc-evolution-chip__text-label,.mdc-evolution-chip__action,.mdc-evolution-chip__cell,.mat-mdc-chip-action-label){color:#fff!important}.user-chip--primary,.user-chip--primary *{color:#fff!important}:host ::ng-deep .user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}:host ::ng-deep .user-chip--primary .mdc-evolution-chip__text-label,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__action,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__cell,:host ::ng-deep .user-chip--primary .mat-mdc-chip-action-label,:host ::ng-deep .user-chip--primary *{color:#fff!important}.box{padding:.5rem;border:1px solid rgb(174,174,13);background-color:#ececaf}\n"] }]
7735
+ args: [{ selector: 'app-ws-data-control', standalone: false, template: "@if ((data$ | async); as data) {\n <div style=\"margin: 1rem;\">\n @if ((users$ |async); as users) {\n <div>\n @if (users.length > 0) {\n <h3 style=\"margin: 0;\">Connected Users</h3>\n } @else {\n <h3 style=\"margin: 0;\">No Users</h3>\n }\n <mat-chip-set>\n @for (user of users; track $index) {\n <mat-chip\n [class.user-chip--primary]=\"isUser(user, (user$ | async))\"\n [style.color]=\"isUser(user, (user$ | async)) ? '#fff' : null\"\n [disableRipple]=\"true\"\n >\n {{ user.name || user.ldap || user.id || 'Anonymous' }}\n </mat-chip>\n }\n </mat-chip-set>\n </div>\n }\n <div style=\"margin-top: 1rem; margin-bottom: 1rem;\">\n <mat-divider></mat-divider>\n </div>\n\n <div class=\"box\" style=\"margin-bottom: 1rem;\" *ngIf=\"(userAction$ | async) as userAction\">\n {{ userAction?.content?.user?.name }} has {{ userAction?.content?.method }}\n </div>\n\n <h3 style=\"margin: 0;\">Data Actions</h3>\n <div style=\"display: flex; gap: 1rem; margin-bottom: 1rem;\">\n <button mat-stroked-button (click)=\"onGetData()\">Get Data</button>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button color=\"accent\" (click)=\"onUpdateData(data)\">Update Data</button>\n <button mat-stroked-button color=\"warn\" (click)=\"onRemoveData(data)\">Remove Data</button>\n <button mat-stroked-button color=\"primary\" (click)=\"onAddData()\">Add Data</button>\n </div>\n @if (data.length > 0) {\n <div>\n <table mat-table [dataSource]=\"data\" style=\"border: 1px solid grey;\">\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef> ID </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.id}} </td>\n </ng-container>\n <ng-container matColumnDef=\"spiffe\">\n <th mat-header-cell *matHeaderCellDef> Spiffe </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.spiffe}} </td>\n </ng-container>\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef> Name </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.first_name}} {{element.last_name}}</td>\n </ng-container>\n <ng-container matColumnDef=\"email\">\n <th mat-header-cell *matHeaderCellDef> Email </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.email}} </td>\n </ng-container>\n <tr mat-header-row *matHeaderRowDef=\"['id', 'spiffe', 'name', 'email']\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: ['id', 'spiffe', 'name', 'email'];\"></tr>\n </table>\n <div style=\"border: 1px solid grey; padding: .5rem; border-top: none;\">\n <h3 style=\"margin: 0;\">Total Records {{ data.length }}</h3>\n </div>\n </div>\n } @else {\n <div style=\"margin-top: 1rem; font-style: italic;\">\n No Data Available\n </div>\n }\n </div>\n}\n\n", styles: [".user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}.user-chip--primary :is(.mdc-evolution-chip__text-label,.mdc-evolution-chip__action,.mdc-evolution-chip__cell,.mat-mdc-chip-action-label){color:#fff!important}.user-chip--primary,.user-chip--primary *{color:#fff!important}:host ::ng-deep .user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}:host ::ng-deep .user-chip--primary .mdc-evolution-chip__text-label,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__action,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__cell,:host ::ng-deep .user-chip--primary .mat-mdc-chip-action-label,:host ::ng-deep .user-chip--primary *{color:#fff!important}.box{padding:.5rem;border:1px solid rgb(174,174,13);background-color:#ececaf}\n"] }]
6263
7736
  }], propDecorators: { server: [{
6264
7737
  type: Input
6265
7738
  }], wsServer: [{
@@ -6270,12 +7743,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6270
7743
  type: Input
6271
7744
  }], path: [{
6272
7745
  type: Input
6273
- }], wsChannel: [{
6274
- type: Input
6275
7746
  }] } });
6276
7747
 
6277
7748
  class WsMessagingComponent {
6278
7749
  constructor() {
7750
+ this.path = ['ai', 'tests']; // Default path for channel name
6279
7751
  this.destroy$ = new Subject();
6280
7752
  this.fb = inject(FormBuilder);
6281
7753
  this.messageService = inject(MessageServiceDemo);
@@ -6320,11 +7792,14 @@ class WsMessagingComponent {
6320
7792
  return this.messages.get('content');
6321
7793
  }
6322
7794
  ngOnInit() {
6323
- this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user);
7795
+ this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
6324
7796
  // Only trigger once when connection becomes true
6325
7797
  this.connectionStatus$.pipe(filter$1(status => status === true), take$1(1), takeUntil$1(this.destroy$)).subscribe(() => {
6326
- // Fetch existing channels when connected (user will create channels in demo)
6327
- this.messageService.getAllChannels();
7798
+ // Wait a moment for subscription to be processed, then fetch channels
7799
+ setTimeout(() => {
7800
+ console.log('📋 Fetching channels after connection...');
7801
+ this.messageService.getAllChannels();
7802
+ }, 500); // 500ms delay to ensure subscription is processed
6328
7803
  });
6329
7804
  // Subscribe to latest messages and show toast notification
6330
7805
  this.latestCommunicationMessages$.pipe(filter$1(message => !!message), takeUntil$1(this.destroy$)).subscribe((message) => {
@@ -6428,11 +7903,11 @@ class WsMessagingComponent {
6428
7903
  this.content.reset();
6429
7904
  }
6430
7905
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsMessagingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6431
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsMessagingComponent, selector: "app-ws-messaging", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user" }, ngImport: i0, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"], dependencies: [{ kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "directive", type: i5.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i3$1.MatChipListbox, selector: "mat-chip-listbox", inputs: ["multiple", "aria-orientation", "selectable", "compareWith", "required", "hideSingleSelectionIndicator", "value"], outputs: ["change"] }, { kind: "component", type: i3$1.MatChipOption, selector: "mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]", inputs: ["selectable", "selected"], outputs: ["selectionChange"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
7906
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsMessagingComponent, selector: "app-ws-messaging", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path" }, ngImport: i0, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel; let i = $index) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel; let i = $index) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"], dependencies: [{ kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "directive", type: i5.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i3$1.MatChipListbox, selector: "mat-chip-listbox", inputs: ["multiple", "aria-orientation", "selectable", "compareWith", "required", "hideSingleSelectionIndicator", "value"], outputs: ["change"] }, { kind: "component", type: i3$1.MatChipOption, selector: "mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]", inputs: ["selectable", "selected"], outputs: ["selectionChange"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
6432
7907
  }
6433
7908
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsMessagingComponent, decorators: [{
6434
7909
  type: Component,
6435
- args: [{ selector: 'app-ws-messaging', standalone: false, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"] }]
7910
+ args: [{ selector: 'app-ws-messaging', standalone: false, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel; let i = $index) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel; let i = $index) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"] }]
6436
7911
  }], propDecorators: { server: [{
6437
7912
  type: Input
6438
7913
  }], wsServer: [{
@@ -6441,6 +7916,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6441
7916
  type: Input
6442
7917
  }], user: [{
6443
7918
  type: Input
7919
+ }], path: [{
7920
+ type: Input
6444
7921
  }] } });
6445
7922
 
6446
7923
  class WsNotificationsComponent {
@@ -6667,7 +8144,6 @@ class RequestManagerWsDemoComponent {
6667
8144
  this.stateService = inject(StateServiceDemo);
6668
8145
  this.fb = inject(FormBuilder);
6669
8146
  this.path = ['ai', 'tests'];
6670
- this.wsChannel = '';
6671
8147
  this.user$ = this.stateService.user$;
6672
8148
  this.attempts$ = this.stateService.wsRetryAttempts$;
6673
8149
  this.nextRetry$ = this.stateService.wsNextRetry$;
@@ -6676,14 +8152,14 @@ class RequestManagerWsDemoComponent {
6676
8152
  this.isPending$ = this.stateService.isPending$;
6677
8153
  }
6678
8154
  ngOnInit() {
6679
- this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user);
8155
+ this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
6680
8156
  }
6681
8157
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerWsDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6682
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerWsDemoComponent, selector: "app-request-manager-ws-demo", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path", wsChannel: "wsChannel" }, ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2 style=\"display: flex;\">\n <span style=\"flex:1\">HTTP Request State Manager - Websockets</span>\n @if ((connectionStatus$ | async); as connected) {\n <span>\n WS -\n <span style=\"color: green;\">Connected</span>\n </span>\n } @else {\n <span style=\"color: red;\">Disconnected {{ attempts$ | async }} - {{ nextRetry$ |async }}</span>\n }\n </h2>\n\n <div>\n\n @if ((user$ | async); as userInfo) {\n <div>\n <mat-toolbar>\n <div style=\"display: flex; flex:1\">\n <div style=\"flex:1\">{{ userInfo.name }}</div>\n <div>({{ userInfo.ldap }})</div>\n </div>\n </mat-toolbar>\n </div>\n }\n\n @if ((isPending$ | async)) {\n <div>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </div>\n }\n\n <mat-tab-group animationDuration=\"0ms\" [selectedIndex]=\"1\">\n\n <mat-tab label=\"WS - Data Control\">\n <!-- DATA CONTROL -->\n <app-ws-data-control\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n [wsChannel]=\"wsChannel\"\n ></app-ws-data-control>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Messaging\">\n <!-- MESSAGING -->\n <app-ws-messaging\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n ></app-ws-messaging>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Notifications\">\n <!-- WS - Notifications -->\n <app-ws-notifications\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n ></app-ws-notifications>\n </mat-tab>\n\n <mat-tab label=\"WS - Chats\" [disabled]=\"true\">\n <!-- WS - Chats -->\n <app-ws-chats></app-ws-chats>\n </mat-tab>\n\n <mat-tab label=\"WS - AI Messaging\" [disabled]=\"true\">\n <!-- WS - AI Messaging -->\n <app-ws-ai-messaging></app-ws-ai-messaging>\n </mat-tab>\n\n </mat-tab-group>\n</div>\n\n</div>\n\n", styles: [""], dependencies: [{ kind: "component", type: i1$2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i1$2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: i10.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i3$2.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "component", type: WsDataControlComponent, selector: "app-ws-data-control", inputs: ["server", "wsServer", "jwtToken", "user", "path", "wsChannel"] }, { kind: "component", type: WsMessagingComponent, selector: "app-ws-messaging", inputs: ["server", "wsServer", "jwtToken", "user"] }, { kind: "component", type: WsNotificationsComponent, selector: "app-ws-notifications", inputs: ["server", "wsServer", "jwtToken", "user"] }, { kind: "component", type: WsAiMessagingComponent, selector: "app-ws-ai-messaging" }, { kind: "component", type: WsChatsComponent, selector: "app-ws-chats" }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
8158
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerWsDemoComponent, selector: "app-request-manager-ws-demo", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path" }, ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2 style=\"display: flex;\">\n <span style=\"flex:1\">HTTP Request State Manager - Websockets</span>\n @if ((connectionStatus$ | async); as connected) {\n <span>\n WS -\n <span style=\"color: green;\">Connected</span>\n </span>\n } @else {\n <span style=\"color: red;\">Disconnected {{ attempts$ | async }} - {{ nextRetry$ |async }}</span>\n }\n </h2>\n\n <div>\n\n @if ((user$ | async); as userInfo) {\n <div>\n <mat-toolbar>\n <div style=\"display: flex; flex:1\">\n <div style=\"flex:1\">{{ userInfo.name }}</div>\n <div>({{ userInfo.ldap }})</div>\n </div>\n </mat-toolbar>\n </div>\n }\n\n @if ((isPending$ | async)) {\n <div>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </div>\n }\n\n <mat-tab-group animationDuration=\"0ms\" [selectedIndex]=\"1\">\n\n <mat-tab label=\"WS - Data Control\">\n <!-- DATA CONTROL -->\n <app-ws-data-control\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-data-control>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Messaging\">\n <!-- MESSAGING -->\n <app-ws-messaging\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-messaging>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Notifications\">\n <!-- WS - Notifications -->\n <app-ws-notifications\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n ></app-ws-notifications>\n </mat-tab>\n\n <mat-tab label=\"WS - Chats\" [disabled]=\"true\">\n <!-- WS - Chats -->\n <app-ws-chats></app-ws-chats>\n </mat-tab>\n\n <mat-tab label=\"WS - AI Messaging\" [disabled]=\"true\">\n <!-- WS - AI Messaging -->\n <app-ws-ai-messaging></app-ws-ai-messaging>\n </mat-tab>\n\n </mat-tab-group>\n</div>\n\n</div>\n\n", styles: [""], dependencies: [{ kind: "component", type: i1$2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i1$2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: i10.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i3$2.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "component", type: WsDataControlComponent, selector: "app-ws-data-control", inputs: ["server", "wsServer", "jwtToken", "user", "path"] }, { kind: "component", type: WsMessagingComponent, selector: "app-ws-messaging", inputs: ["server", "wsServer", "jwtToken", "user", "path"] }, { kind: "component", type: WsNotificationsComponent, selector: "app-ws-notifications", inputs: ["server", "wsServer", "jwtToken", "user"] }, { kind: "component", type: WsAiMessagingComponent, selector: "app-ws-ai-messaging" }, { kind: "component", type: WsChatsComponent, selector: "app-ws-chats" }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
6683
8159
  }
6684
8160
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerWsDemoComponent, decorators: [{
6685
8161
  type: Component,
6686
- args: [{ selector: 'app-request-manager-ws-demo', standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2 style=\"display: flex;\">\n <span style=\"flex:1\">HTTP Request State Manager - Websockets</span>\n @if ((connectionStatus$ | async); as connected) {\n <span>\n WS -\n <span style=\"color: green;\">Connected</span>\n </span>\n } @else {\n <span style=\"color: red;\">Disconnected {{ attempts$ | async }} - {{ nextRetry$ |async }}</span>\n }\n </h2>\n\n <div>\n\n @if ((user$ | async); as userInfo) {\n <div>\n <mat-toolbar>\n <div style=\"display: flex; flex:1\">\n <div style=\"flex:1\">{{ userInfo.name }}</div>\n <div>({{ userInfo.ldap }})</div>\n </div>\n </mat-toolbar>\n </div>\n }\n\n @if ((isPending$ | async)) {\n <div>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </div>\n }\n\n <mat-tab-group animationDuration=\"0ms\" [selectedIndex]=\"1\">\n\n <mat-tab label=\"WS - Data Control\">\n <!-- DATA CONTROL -->\n <app-ws-data-control\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n [wsChannel]=\"wsChannel\"\n ></app-ws-data-control>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Messaging\">\n <!-- MESSAGING -->\n <app-ws-messaging\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n ></app-ws-messaging>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Notifications\">\n <!-- WS - Notifications -->\n <app-ws-notifications\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n ></app-ws-notifications>\n </mat-tab>\n\n <mat-tab label=\"WS - Chats\" [disabled]=\"true\">\n <!-- WS - Chats -->\n <app-ws-chats></app-ws-chats>\n </mat-tab>\n\n <mat-tab label=\"WS - AI Messaging\" [disabled]=\"true\">\n <!-- WS - AI Messaging -->\n <app-ws-ai-messaging></app-ws-ai-messaging>\n </mat-tab>\n\n </mat-tab-group>\n</div>\n\n</div>\n\n" }]
8162
+ args: [{ selector: 'app-request-manager-ws-demo', standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2 style=\"display: flex;\">\n <span style=\"flex:1\">HTTP Request State Manager - Websockets</span>\n @if ((connectionStatus$ | async); as connected) {\n <span>\n WS -\n <span style=\"color: green;\">Connected</span>\n </span>\n } @else {\n <span style=\"color: red;\">Disconnected {{ attempts$ | async }} - {{ nextRetry$ |async }}</span>\n }\n </h2>\n\n <div>\n\n @if ((user$ | async); as userInfo) {\n <div>\n <mat-toolbar>\n <div style=\"display: flex; flex:1\">\n <div style=\"flex:1\">{{ userInfo.name }}</div>\n <div>({{ userInfo.ldap }})</div>\n </div>\n </mat-toolbar>\n </div>\n }\n\n @if ((isPending$ | async)) {\n <div>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </div>\n }\n\n <mat-tab-group animationDuration=\"0ms\" [selectedIndex]=\"1\">\n\n <mat-tab label=\"WS - Data Control\">\n <!-- DATA CONTROL -->\n <app-ws-data-control\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-data-control>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Messaging\">\n <!-- MESSAGING -->\n <app-ws-messaging\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-messaging>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Notifications\">\n <!-- WS - Notifications -->\n <app-ws-notifications\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n ></app-ws-notifications>\n </mat-tab>\n\n <mat-tab label=\"WS - Chats\" [disabled]=\"true\">\n <!-- WS - Chats -->\n <app-ws-chats></app-ws-chats>\n </mat-tab>\n\n <mat-tab label=\"WS - AI Messaging\" [disabled]=\"true\">\n <!-- WS - AI Messaging -->\n <app-ws-ai-messaging></app-ws-ai-messaging>\n </mat-tab>\n\n </mat-tab-group>\n</div>\n\n</div>\n\n" }]
6687
8163
  }], propDecorators: { server: [{
6688
8164
  type: Input
6689
8165
  }], wsServer: [{
@@ -6694,8 +8170,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6694
8170
  type: Input
6695
8171
  }], path: [{
6696
8172
  type: Input
6697
- }], wsChannel: [{
6698
- type: Input
6699
8173
  }] } });
6700
8174
 
6701
8175
  class Settings {
@@ -6916,7 +8390,6 @@ class HttpRequestServicesDemoComponent {
6916
8390
  this.jwtToken = '';
6917
8391
  this.server = 'http:';
6918
8392
  this.path = ['ai', 'tests'];
6919
- this.wsChannel = '';
6920
8393
  this.requestTypes = [
6921
8394
  { name: "Http Service", value: 'http_service' },
6922
8395
  // { name: "Http Signals Service", value: 'http_signals_service', new: true },
@@ -6937,11 +8410,11 @@ class HttpRequestServicesDemoComponent {
6937
8410
  this.selectedService = this.requestTypes[type].value;
6938
8411
  }
6939
8412
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HttpRequestServicesDemoComponent, deps: [{ token: CONFIG_SETTINGS_TOKEN }], target: i0.ɵɵFactoryTarget.Component }); }
6940
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: HttpRequestServicesDemoComponent, selector: "app-http-request-services-demo", inputs: { wsServer: "wsServer", jwtToken: "jwtToken", server: "server", user: "user", path: "path", wsChannel: "wsChannel", adapter: "adapter", mapper: "mapper" }, ngImport: i0, template: "<mat-toolbar style=\"display:flex\">\n <div>Http Request Manager Services</div>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button [matMenuTriggerFor]=\"menu\">Services</button>\n <mat-menu #menu=\"matMenu\">\n @for (type of requestTypes; track type; let i = $index) {\n @if (type?.divider) {\n <div\n style=\"margin-top: .5rem; margin-bottom: .5rem;\"\n >\n <mat-divider></mat-divider>\n </div>\n }\n <button\n mat-menu-item\n (click)=\"onSelected(i)\"\n [disabled]=\"type.disabled\"\n >\n {{ type.name }}\n </button>\n }\n\n </mat-menu>\n</mat-toolbar>\n\n<span>\n @switch (selectedService) {\n @case ('http_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'http_signals_service'\">\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-signals-manager-demo></app-request-signals-manager-demo>\n </p> -->\n @case ('http_state_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-state-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-state-demo>\n </p>\n }\n @case ('http_state_service_ws') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-ws-demo\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [wsChannel]=\"wsChannel\"\n [path]=\"path\"\n ></app-request-manager-ws-demo>\n </p>\n }\n @case ('database_service') {\n <p>\n <app-database-data-demo></app-database-data-demo>\n </p>\n }\n @case ('local_storage_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-demo></app-local-storage-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'local_storage_signals_service'\">\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-signals-demo></app-local-storage-signals-demo>\n</p> -->\n@case ('store_state_manager') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-store-state-manager-demo></app-store-state-manager-demo>\n </p>\n}\n@default {\n <p>\n Other\n </p>\n}\n}\n</span>\n\n<ng-template #HTTP_OPTIONS>\n @if (injectionOptions?.httpRequestOptions) {\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - HTTP Options</h3>\n {{ injectionOptions?.httpRequestOptions| json }}\n </div>\n }\n</ng-template>\n\n<ng-template #LOCAL_OPTIONS>\n @if (injectionOptions?.LocalStorageOptions) {\n <ng-container class=\"box\">\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - LocalStorage Options</h3>\n {{ injectionOptions?.LocalStorageOptions| json }}\n </div>\n </ng-container>\n }\n</ng-template>\n\n\n", styles: [".box{padding:1rem;background-color:#f5f5f5;border:thin gray solid;margin-top:1rem}\n"], dependencies: [{ kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "component", type: i3$2.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "component", type: RequestManagerStateDemoComponent, selector: "app-request-manager-state-demo", inputs: ["server", "adapter", "mapper"] }, { kind: "component", type: RequestManagerDemoComponent, selector: "app-request-manager-demo", inputs: ["server", "adapter", "mapper"] }, { kind: "component", type: LocalStorageDemoComponent, selector: "app-local-storage-demo" }, { kind: "component", type: RequestManagerWsDemoComponent, selector: "app-request-manager-ws-demo", inputs: ["server", "wsServer", "jwtToken", "user", "path", "wsChannel"] }, { kind: "component", type: StoreStateManagerDemoComponent, selector: "app-store-state-manager-demo" }, { kind: "component", type: DatabaseDataDemoComponent, selector: "app-database-data-demo" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }] }); }
8413
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: HttpRequestServicesDemoComponent, selector: "app-http-request-services-demo", inputs: { wsServer: "wsServer", jwtToken: "jwtToken", server: "server", user: "user", path: "path", adapter: "adapter", mapper: "mapper" }, ngImport: i0, template: "<mat-toolbar style=\"display:flex\">\n <div>Http Request Manager Services</div>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button [matMenuTriggerFor]=\"menu\">Services</button>\n <mat-menu #menu=\"matMenu\">\n @for (type of requestTypes; track type; let i = $index) {\n @if (type?.divider) {\n <div\n style=\"margin-top: .5rem; margin-bottom: .5rem;\"\n >\n <mat-divider></mat-divider>\n </div>\n }\n <button\n mat-menu-item\n (click)=\"onSelected(i)\"\n [disabled]=\"type.disabled\"\n >\n {{ type.name }}\n </button>\n }\n\n </mat-menu>\n</mat-toolbar>\n\n<span>\n @switch (selectedService) {\n @case ('http_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'http_signals_service'\">\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-signals-manager-demo></app-request-signals-manager-demo>\n </p> -->\n @case ('http_state_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-state-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-state-demo>\n </p>\n }\n @case ('http_state_service_ws') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-ws-demo\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-request-manager-ws-demo>\n </p>\n }\n @case ('database_service') {\n <p>\n <app-database-data-demo></app-database-data-demo>\n </p>\n }\n @case ('local_storage_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-demo></app-local-storage-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'local_storage_signals_service'\">\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-signals-demo></app-local-storage-signals-demo>\n</p> -->\n@case ('store_state_manager') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-store-state-manager-demo></app-store-state-manager-demo>\n </p>\n}\n@default {\n <p>\n Other\n </p>\n}\n}\n</span>\n\n<ng-template #HTTP_OPTIONS>\n @if (injectionOptions?.httpRequestOptions) {\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - HTTP Options</h3>\n {{ injectionOptions?.httpRequestOptions| json }}\n </div>\n }\n</ng-template>\n\n<ng-template #LOCAL_OPTIONS>\n @if (injectionOptions?.LocalStorageOptions) {\n <ng-container class=\"box\">\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - LocalStorage Options</h3>\n {{ injectionOptions?.LocalStorageOptions| json }}\n </div>\n </ng-container>\n }\n</ng-template>\n\n\n", styles: [".box{padding:1rem;background-color:#f5f5f5;border:thin gray solid;margin-top:1rem}\n"], dependencies: [{ kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "component", type: i3$2.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "component", type: RequestManagerStateDemoComponent, selector: "app-request-manager-state-demo", inputs: ["server", "adapter", "mapper"] }, { kind: "component", type: RequestManagerDemoComponent, selector: "app-request-manager-demo", inputs: ["server", "adapter", "mapper"] }, { kind: "component", type: LocalStorageDemoComponent, selector: "app-local-storage-demo" }, { kind: "component", type: RequestManagerWsDemoComponent, selector: "app-request-manager-ws-demo", inputs: ["server", "wsServer", "jwtToken", "user", "path"] }, { kind: "component", type: StoreStateManagerDemoComponent, selector: "app-store-state-manager-demo" }, { kind: "component", type: DatabaseDataDemoComponent, selector: "app-database-data-demo" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }] }); }
6941
8414
  }
6942
8415
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HttpRequestServicesDemoComponent, decorators: [{
6943
8416
  type: Component,
6944
- args: [{ selector: 'app-http-request-services-demo', standalone: false, template: "<mat-toolbar style=\"display:flex\">\n <div>Http Request Manager Services</div>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button [matMenuTriggerFor]=\"menu\">Services</button>\n <mat-menu #menu=\"matMenu\">\n @for (type of requestTypes; track type; let i = $index) {\n @if (type?.divider) {\n <div\n style=\"margin-top: .5rem; margin-bottom: .5rem;\"\n >\n <mat-divider></mat-divider>\n </div>\n }\n <button\n mat-menu-item\n (click)=\"onSelected(i)\"\n [disabled]=\"type.disabled\"\n >\n {{ type.name }}\n </button>\n }\n\n </mat-menu>\n</mat-toolbar>\n\n<span>\n @switch (selectedService) {\n @case ('http_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'http_signals_service'\">\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-signals-manager-demo></app-request-signals-manager-demo>\n </p> -->\n @case ('http_state_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-state-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-state-demo>\n </p>\n }\n @case ('http_state_service_ws') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-ws-demo\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [wsChannel]=\"wsChannel\"\n [path]=\"path\"\n ></app-request-manager-ws-demo>\n </p>\n }\n @case ('database_service') {\n <p>\n <app-database-data-demo></app-database-data-demo>\n </p>\n }\n @case ('local_storage_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-demo></app-local-storage-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'local_storage_signals_service'\">\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-signals-demo></app-local-storage-signals-demo>\n</p> -->\n@case ('store_state_manager') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-store-state-manager-demo></app-store-state-manager-demo>\n </p>\n}\n@default {\n <p>\n Other\n </p>\n}\n}\n</span>\n\n<ng-template #HTTP_OPTIONS>\n @if (injectionOptions?.httpRequestOptions) {\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - HTTP Options</h3>\n {{ injectionOptions?.httpRequestOptions| json }}\n </div>\n }\n</ng-template>\n\n<ng-template #LOCAL_OPTIONS>\n @if (injectionOptions?.LocalStorageOptions) {\n <ng-container class=\"box\">\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - LocalStorage Options</h3>\n {{ injectionOptions?.LocalStorageOptions| json }}\n </div>\n </ng-container>\n }\n</ng-template>\n\n\n", styles: [".box{padding:1rem;background-color:#f5f5f5;border:thin gray solid;margin-top:1rem}\n"] }]
8417
+ args: [{ selector: 'app-http-request-services-demo', standalone: false, template: "<mat-toolbar style=\"display:flex\">\n <div>Http Request Manager Services</div>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button [matMenuTriggerFor]=\"menu\">Services</button>\n <mat-menu #menu=\"matMenu\">\n @for (type of requestTypes; track type; let i = $index) {\n @if (type?.divider) {\n <div\n style=\"margin-top: .5rem; margin-bottom: .5rem;\"\n >\n <mat-divider></mat-divider>\n </div>\n }\n <button\n mat-menu-item\n (click)=\"onSelected(i)\"\n [disabled]=\"type.disabled\"\n >\n {{ type.name }}\n </button>\n }\n\n </mat-menu>\n</mat-toolbar>\n\n<span>\n @switch (selectedService) {\n @case ('http_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'http_signals_service'\">\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-signals-manager-demo></app-request-signals-manager-demo>\n </p> -->\n @case ('http_state_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-state-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-state-demo>\n </p>\n }\n @case ('http_state_service_ws') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-ws-demo\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-request-manager-ws-demo>\n </p>\n }\n @case ('database_service') {\n <p>\n <app-database-data-demo></app-database-data-demo>\n </p>\n }\n @case ('local_storage_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-demo></app-local-storage-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'local_storage_signals_service'\">\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-signals-demo></app-local-storage-signals-demo>\n</p> -->\n@case ('store_state_manager') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-store-state-manager-demo></app-store-state-manager-demo>\n </p>\n}\n@default {\n <p>\n Other\n </p>\n}\n}\n</span>\n\n<ng-template #HTTP_OPTIONS>\n @if (injectionOptions?.httpRequestOptions) {\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - HTTP Options</h3>\n {{ injectionOptions?.httpRequestOptions| json }}\n </div>\n }\n</ng-template>\n\n<ng-template #LOCAL_OPTIONS>\n @if (injectionOptions?.LocalStorageOptions) {\n <ng-container class=\"box\">\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - LocalStorage Options</h3>\n {{ injectionOptions?.LocalStorageOptions| json }}\n </div>\n </ng-container>\n }\n</ng-template>\n\n\n", styles: [".box{padding:1rem;background-color:#f5f5f5;border:thin gray solid;margin-top:1rem}\n"] }]
6945
8418
  }], ctorParameters: () => [{ type: ConfigOptions, decorators: [{
6946
8419
  type: Inject,
6947
8420
  args: [CONFIG_SETTINGS_TOKEN]
@@ -6955,8 +8428,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6955
8428
  type: Input
6956
8429
  }], path: [{
6957
8430
  type: Input
6958
- }], wsChannel: [{
6959
- type: Input
6960
8431
  }], adapter: [{
6961
8432
  type: Input
6962
8433
  }], mapper: [{
@@ -7628,5 +9099,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
7628
9099
  * Generated bundle index. Do not edit.
7629
9100
  */
7630
9101
 
7631
- export { ApiRequest, AppService, AsymmetricalEncryptionService, CONFIG_SETTINGS_TOKEN, ChannelInfo, ChannelType, CommunicationType, ConfigHTTPOptions, ConfigOptions, DataType, DatabaseDataDemoComponent, DatabaseManagerService, DatabaseStorage, DbService, ErrorDisplaySettings, GlobalStoreOptions, HTTPManagerService, HTTPManagerSignalsService, HTTPManagerStateService, HeadersService, HttpRequestManagerModule, HttpRequestServicesDemoComponent, LocalStorageDemoComponent, LocalStorageManagerService, LocalStorageOptions, LocalStorageSignalsManagerService, PathQueryService, Random, RandomHSLColor, RandomHexColor, RandomNumber, RandomNumbers, RandomNumbersUnique, RandomPaletteColor, RandomSignature, RandomStr, RandomVisibleColor, RequestErrorInterceptor, RequestHeadersInterceptor, RequestManagerDemoComponent, RequestManagerStateDemoComponent, RequestOptions, RequestService, RequestSignalsService, RetryOptions, SettingOptions, StateStorageOptions, StorageData, StorageOption, StorageType, StoreStateManagerService, StreamType, SymmetricalEncryptionService, TableSchemaDef, UUID, UUID_STR, UserData, UtilsService, WSOptions, WSUser, WebsocketService, WithCredentialsInterceptor, countdown, createChannelName, delayedRetry, requestPolling, requestStreaming, streamAI, streamAuto, streamEvents, streamJSON, streamNDJSON };
9102
+ export { ApiRequest, AppService, AsymmetricalEncryptionService, CONFIG_SETTINGS_TOKEN, ChannelInfo, ChannelType, CommunicationType, ConfigHTTPOptions, ConfigOptions, DataType, DatabaseDataDemoComponent, DatabaseManagerService, DatabaseStorage, DbService, ErrorDisplaySettings, GlobalStoreOptions, HTTPManagerService, HTTPManagerSignalsService, HTTPManagerStateService, HeadersService, HttpRequestManagerModule, HttpRequestServicesDemoComponent, LocalStorageDemoComponent, LocalStorageManagerService, LocalStorageOptions, LocalStorageSignalsManagerService, PathQueryService, Random, RandomHSLColor, RandomHexColor, RandomNumber, RandomNumbers, RandomNumbersUnique, RandomPaletteColor, RandomSignature, RandomStr, RandomVisibleColor, RequestErrorInterceptor, RequestHeadersInterceptor, RequestManagerDemoComponent, RequestManagerStateDemoComponent, RequestOptions, RequestService, RequestSignalsService, RetryOptions, SettingOptions, StateStorageOptions, StorageData, StorageOption, StorageType, StoreStateManagerService, StreamType, SymmetricalEncryptionService, TableSchemaDef, UUID, UUID_STR, UserData, UtilsService, WSOptions, WSUser, WebSocketManagerService, WebsocketService, WithCredentialsInterceptor, countdown, createChannelName, delayedRetry, requestPolling, requestStreaming, streamAI, streamAuto, streamEvents, streamJSON, streamNDJSON };
7632
9103
  //# sourceMappingURL=http-request-manager.mjs.map