http-request-manager 18.7.31 → 18.9.0

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.
@@ -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);
@@ -1723,72 +2359,491 @@ class RequestService extends WebsocketService {
1723
2359
  }
1724
2360
  return fileType;
1725
2361
  }
1726
- combineHeaders(headers, isStreaming) {
1727
- return (isStreaming) ?
1728
- {
1729
- ...headers,
1730
- observe: 'events',
1731
- responseType: 'text',
1732
- reportProgress: true,
1733
- Accept: 'text/event-stream'
2362
+ combineHeaders(headers, isStreaming) {
2363
+ return (isStreaming) ?
2364
+ {
2365
+ ...headers,
2366
+ observe: 'events',
2367
+ responseType: 'text',
2368
+ reportProgress: true,
2369
+ Accept: 'text/event-stream'
2370
+ }
2371
+ : headers;
2372
+ }
2373
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
2374
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, providedIn: 'root' }); }
2375
+ }
2376
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, decorators: [{
2377
+ type: Injectable,
2378
+ args: [{
2379
+ providedIn: 'root'
2380
+ }]
2381
+ }] });
2382
+
2383
+ function countdown(duration) {
2384
+ return defer(() => {
2385
+ const currentCount = { current: duration };
2386
+ return interval(1000).pipe(map(() => --currentCount.current), takeWhile(count => count >= 0));
2387
+ });
2388
+ }
2389
+
2390
+ const DEFAULT_MAX_RETRIES = 3;
2391
+ function delayedRetry(delayMs, maxRetry = DEFAULT_MAX_RETRIES) {
2392
+ return (src) => src.pipe(retry({
2393
+ count: maxRetry,
2394
+ delay: () => timer(delayMs)
2395
+ }));
2396
+ }
2397
+
2398
+ function requestPolling(pollInterval, stopCondition$, isPending$) {
2399
+ return (source) => {
2400
+ return interval(pollInterval * 1000)
2401
+ .pipe(startWith(0), tap(() => {
2402
+ try {
2403
+ if (isPending$ && typeof isPending$.next === 'function') {
2404
+ isPending$.next(true);
2405
+ }
2406
+ else if (isPending$ && typeof isPending$.set === 'function') {
2407
+ isPending$.set(true);
2408
+ }
2409
+ }
2410
+ catch (e) {
2411
+ // no-op if setting fails
2412
+ }
2413
+ }), mergeMap(() => source), tap(() => {
2414
+ try {
2415
+ if (isPending$ && typeof isPending$.next === 'function') {
2416
+ isPending$.next(false);
2417
+ }
2418
+ else if (isPending$ && typeof isPending$.set === 'function') {
2419
+ isPending$.set(false);
2420
+ }
2421
+ }
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);
1734
2821
  }
1735
- : headers;
2822
+ }
2823
+ return skipped;
1736
2824
  }
1737
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
1738
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, providedIn: 'root' }); }
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' }); }
1739
2840
  }
1740
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, decorators: [{
2841
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageTrackerService, decorators: [{
1741
2842
  type: Injectable,
1742
2843
  args: [{
1743
- providedIn: 'root'
2844
+ providedIn: 'root',
1744
2845
  }]
1745
- }] });
1746
-
1747
- function countdown(duration) {
1748
- return defer(() => {
1749
- const currentCount = { current: duration };
1750
- return interval(1000).pipe(map(() => --currentCount.current), takeWhile(count => count >= 0));
1751
- });
1752
- }
1753
-
1754
- const DEFAULT_MAX_RETRIES = 3;
1755
- function delayedRetry(delayMs, maxRetry = DEFAULT_MAX_RETRIES) {
1756
- return (src) => src.pipe(retry({
1757
- count: maxRetry,
1758
- delay: () => timer(delayMs)
1759
- }));
1760
- }
1761
-
1762
- function requestPolling(pollInterval, stopCondition$, isPending$) {
1763
- return (source) => {
1764
- return interval(pollInterval * 1000)
1765
- .pipe(startWith(0), tap(() => {
1766
- try {
1767
- if (isPending$ && typeof isPending$.next === 'function') {
1768
- isPending$.next(true);
1769
- }
1770
- else if (isPending$ && typeof isPending$.set === 'function') {
1771
- isPending$.set(true);
1772
- }
1773
- }
1774
- catch (e) {
1775
- // no-op if setting fails
1776
- }
1777
- }), mergeMap(() => source), tap(() => {
1778
- try {
1779
- if (isPending$ && typeof isPending$.next === 'function') {
1780
- isPending$.next(false);
1781
- }
1782
- else if (isPending$ && typeof isPending$.set === 'function') {
1783
- isPending$.set(false);
1784
- }
1785
- }
1786
- catch (e) {
1787
- // no-op if setting fails
1788
- }
1789
- }), takeUntil(stopCondition$));
1790
- };
1791
- }
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);
@@ -3448,13 +4750,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3448
4750
  }], ctorParameters: () => [] });
3449
4751
 
3450
4752
  class ChannelMessage {
3451
- constructor(sessionId = null, content = null) {
4753
+ constructor(messageId, channel, isReplay, sessionId = null, content = null, timestamp) {
4754
+ this.messageId = messageId;
4755
+ this.channel = channel;
4756
+ this.isReplay = isReplay;
3452
4757
  this.sessionId = sessionId;
3453
4758
  this.content = content;
4759
+ this.timestamp = timestamp;
3454
4760
  }
3455
4761
  static adapt(item) {
3456
- return new ChannelMessage(item?.sessionId || item?.id, // Support both for backward compatibility
3457
- item?.content);
4762
+ return new ChannelMessage(item?.messageId, item?.channel, item?.isReplay, item?.sessionId || item?.id, // Support both for backward compatibility
4763
+ item?.content, item?.timestamp);
3458
4764
  }
3459
4765
  }
3460
4766
 
@@ -3489,6 +4795,8 @@ class HTTPManagerStateService extends ComponentStore {
3489
4795
  getUsersForChannel$(channel) {
3490
4796
  return this.userListByChannel$.pipe(map(channelMap => channelMap.get(channel) || []));
3491
4797
  }
4798
+ // Message queue for WebSocket communication (processed when connection is established)
4799
+ static { this.wsCommunicationQueue = []; }
3492
4800
  constructor(apiOptions = ApiRequest.adapt(), dataType, database) {
3493
4801
  super(defaultState);
3494
4802
  this.apiOptions = apiOptions;
@@ -3516,7 +4824,7 @@ class HTTPManagerStateService extends ComponentStore {
3516
4824
  this.messages$ = this.messages.asObservable();
3517
4825
  this.userListByChannel = new BehaviorSubject(new Map());
3518
4826
  this.userListByChannel$ = this.userListByChannel.asObservable();
3519
- // Legacy support - returns all unique users across all channels
4827
+ // Returns all unique users across all channels
3520
4828
  this.userList = new BehaviorSubject([]);
3521
4829
  this.userList$ = this.userList.asObservable();
3522
4830
  this.user = new BehaviorSubject(null);
@@ -3540,17 +4848,19 @@ class HTTPManagerStateService extends ComponentStore {
3540
4848
  this.latestCommunicationMessages$ = this.latestCommunicationMessages.asObservable();
3541
4849
  this.userAction = new BehaviorSubject(null);
3542
4850
  this.userAction$ = this.userAction.asObservable();
3543
- this.wsConnection = false;
3544
4851
  this.wsOptions = WSOptions.adapt();
3545
- // Expose raw WS connection status directly to UI
4852
+ // Expose raw WS connection status directly to UI (from singleton WebSocketManagerService)
3546
4853
  this.connectionStatus$ = this.httpManagerService.connectionStatus$;
3547
4854
  // 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;
4855
+ this.initWS = this.effect((wsOptions$) => wsOptions$.pipe(switchMap((wsOptions) => merge(this.httpManagerService.connectionStatus$.pipe(tap((isConnected) => {
4856
+ // Process queued wsCommunication calls when connection becomes true
4857
+ if (isConnected && HTTPManagerStateService.wsCommunicationQueue.length > 0) {
4858
+ console.log(`🔄 Processing ${HTTPManagerStateService.wsCommunicationQueue.length} queued WS messages`);
4859
+ while (HTTPManagerStateService.wsCommunicationQueue.length > 0) {
4860
+ const queued = HTTPManagerStateService.wsCommunicationQueue.shift();
4861
+ this.sendWsCommunication(queued.method, queued.path);
4862
+ }
4863
+ }
3554
4864
  })), this.httpManagerService.messages$.pipe(tap((message) => {
3555
4865
  if (!message)
3556
4866
  return;
@@ -3558,6 +4868,8 @@ class HTTPManagerStateService extends ComponentStore {
3558
4868
  const currentMessages = this.messages.value;
3559
4869
  this.messages.next([...currentMessages, message]);
3560
4870
  console.log('Received:', message);
4871
+ // Debug: Log all message types
4872
+ console.log('📨 Message type:', message.type);
3561
4873
  if (message.error === 'JWT_INVALID') {
3562
4874
  this.shouldRetry = false;
3563
4875
  this.httpManagerService.disconnect();
@@ -3571,16 +4883,22 @@ class HTTPManagerStateService extends ComponentStore {
3571
4883
  switch (message.type) {
3572
4884
  case 'channelsList':
3573
4885
  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
- // }
4886
+ console.log('🔍 channelsList received, checking connection status...');
4887
+ console.log('🔍 WebSocket connected:', WebSocketManagerService.isConnected());
4888
+ // Auto-subscribe to all channels from the list
4889
+ if (message.channels && message.channels.length > 0) {
4890
+ console.log('📥 Auto-subscribing to', message.channels.length, 'channel(s)');
4891
+ this.subscribeToChannels(message.channels);
4892
+ }
4893
+ else {
4894
+ console.log('⚠️ No channels to subscribe to');
4895
+ }
3580
4896
  this.channels.next(message.channels);
3581
4897
  break;
3582
4898
  case 'subscribed':
3583
4899
  console.log(`✅ Subscription confirmed: ${message.channel}`);
4900
+ // Track as subscribed now that server confirmed
4901
+ WebSocketManagerService.addSubscribedChannel(message.channel);
3584
4902
  break;
3585
4903
  case 'unsubscribed':
3586
4904
  console.log(`🔓 Unsubscription confirmed: ${message.channel}`);
@@ -3588,6 +4906,11 @@ class HTTPManagerStateService extends ComponentStore {
3588
4906
  case 'info':
3589
4907
  // Already subscribed or other info messages
3590
4908
  console.log(`ℹ️ Info: ${message.message}`);
4909
+ // If it's an "Already subscribed" message, treat it as subscription confirmation
4910
+ if (message.message?.includes('Already subscribed')) {
4911
+ console.log(`✅ Treating info as subscription confirmation for: ${message.data}`);
4912
+ WebSocketManagerService.addSubscribedChannel(message.data);
4913
+ }
3591
4914
  break;
3592
4915
  case 'stateMangerMessage':
3593
4916
  // Compare sender's session ID with current user's ID
@@ -3606,19 +4929,58 @@ class HTTPManagerStateService extends ComponentStore {
3606
4929
  break;
3607
4930
  case 'channelMessage':
3608
4931
  // Handle channel-based messages (from sendChannelMessage)
3609
- // Structure: { type: 'channelMessage', channels: [...], sessionId: {id, ldap, name, email}, content: {message payload} }
4932
+ // Structure: { type: 'channelMessage', messageId, channel, sessionId, content, timestamp }
3610
4933
  // Skip messages from self
3611
- const senderSessionId = message.sessionId?.id;
4934
+ const senderSessionId = message.sessionId?.id || message.sessionId;
3612
4935
  if (senderSessionId === this.user.value?.id) {
4936
+ console.log('🔇 Skipping message from self (sessionId match)');
3613
4937
  break;
3614
4938
  }
3615
4939
  console.log('💬 Channel Message received:', message);
3616
- if (message.content) {
4940
+ // Determine which channels this message was sent to
4941
+ const messageChannels = message.channel ? [message.channel] : [];
4942
+ // Construct expected channel path (without env prefix)
4943
+ const myPath = this.apiOptions.path || [];
4944
+ const myPathString = myPath.join('/');
4945
+ // Check if any of the message channels CONTAIN our path
4946
+ const isWsUpdateChannel = messageChannels.some((ch) => {
4947
+ // Strip SYS- prefix if present for comparison
4948
+ const cleanChannel = ch.replace('SYS-', '');
4949
+ // Check if channel contains our path (or starts with it)
4950
+ const matches = cleanChannel === myPathString ||
4951
+ cleanChannel.startsWith(myPathString + '/') ||
4952
+ cleanChannel.includes(myPathString);
4953
+ console.log(`🔍 Channel check: ${ch} contains ${myPathString}? ${matches}`);
4954
+ return matches;
4955
+ });
4956
+ // If it's the expected channel, trigger fetchRecord like stateManagerMessage
4957
+ if (isWsUpdateChannel && message.content?.path) {
4958
+ console.log('🔍 Message received on expected channel (path match):', myPathString);
4959
+ console.log('📄 Content:', message.content);
4960
+ console.log('📥 Fetching record for channel:', myPathString);
4961
+ const path = message.content.path;
4962
+ const method = message.content.method || 'UPDATE';
4963
+ this.userAction.next({ sessionId: message.sessionId, content: message.content });
4964
+ this.fetchRecord(RequestOptions.adapt({ path }), method);
4965
+ }
4966
+ else if (message.content) {
4967
+ // Handle message content directly
4968
+ console.log('📄 Processing message content:', message.content);
3617
4969
  this.appendMessages(ChannelMessage.adapt({
3618
4970
  sessionId: message.sessionId,
3619
4971
  content: message.content,
3620
4972
  }));
3621
4973
  }
4974
+ else {
4975
+ console.log('⚠️ Message does not contain data.content.path, skipping fetchRecord');
4976
+ }
4977
+ // Keep existing functionality for backward compatibility
4978
+ if (message.data?.content && !isWsUpdateChannel) {
4979
+ this.appendMessages(ChannelMessage.adapt({
4980
+ sessionId: message.data.sessionId,
4981
+ content: message.data.content,
4982
+ }));
4983
+ }
3622
4984
  break;
3623
4985
  case 'usersInChannel':
3624
4986
  console.log(`👥 Users in channel "${message.channel}":`, message.data.users);
@@ -3834,11 +5196,21 @@ class HTTPManagerStateService extends ComponentStore {
3834
5196
  // FETCH RECORD
3835
5197
  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
5198
  this.streamedResponse = [];
5199
+ // Temporarily update apiOptions.path with the path from the WebSocket message
5200
+ // This ensures the request goes to the correct endpoint
5201
+ const originalPath = this.apiOptions.path;
5202
+ if (options?.path && Array.isArray(options.path)) {
5203
+ this.apiOptions.path = options.path;
5204
+ console.log('🔧 Temporarily set apiOptions.path to:', options.path);
5205
+ }
3837
5206
  const requestOptions = this.updateRequestOptions(options?.headers);
3838
- console.log('🌐 Making GET request to path:', options?.path);
3839
- return this.httpManagerService.getRequest(requestOptions, options?.path)
5207
+ console.log('🌐 Making GET request to path:', this.apiOptions.path);
5208
+ return this.httpManagerService.getRequest(requestOptions)
3840
5209
  .pipe(tap((data) => {
3841
5210
  console.log('📦 fetchRecord received data:', data);
5211
+ // Restore original path after request completes
5212
+ this.apiOptions.path = originalPath;
5213
+ console.log('🔧 Restored apiOptions.path to:', originalPath);
3842
5214
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3843
5215
  const id = options.path?.length ? options.path[options.path.length - 1] : null;
3844
5216
  if (method === 'DELETE') {
@@ -3860,8 +5232,16 @@ class HTTPManagerStateService extends ComponentStore {
3860
5232
  return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, id);
3861
5233
  if (method === 'UPDATE' && data)
3862
5234
  return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
3863
- if (method === 'CREATE' && data)
3864
- return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
5235
+ if (method === 'CREATE' && data) {
5236
+ // Validate that data has a valid id before saving to IndexedDB
5237
+ if (data && (data.id !== undefined && data.id !== null && data.id !== '')) {
5238
+ console.log('💾 Saving to IndexedDB:', { table: this.databaseOptions.table, id: data.id, data });
5239
+ return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
5240
+ }
5241
+ else {
5242
+ console.warn('⚠️ Skipping IndexedDB save: data.id is invalid', data);
5243
+ }
5244
+ }
3865
5245
  }
3866
5246
  return of(data);
3867
5247
  }));
@@ -3874,8 +5254,8 @@ class HTTPManagerStateService extends ComponentStore {
3874
5254
  .pipe(tap((data) => {
3875
5255
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3876
5256
  this.addData$(data);
3877
- if (this.wsConnection)
3878
- this.wsCommunication('CREATE', [...options?.path || [], data.id]);
5257
+ // Always call wsCommunication - it will queue if not connected
5258
+ this.wsCommunication('CREATE', [...options?.path || [], data.id]);
3879
5259
  }), concatMap((data) => {
3880
5260
  if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
3881
5261
  return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
@@ -3891,8 +5271,8 @@ class HTTPManagerStateService extends ComponentStore {
3891
5271
  .pipe(tap((data) => {
3892
5272
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3893
5273
  this.updateData$(data);
3894
- if (this.wsConnection)
3895
- this.wsCommunication('UPDATE', [...options?.path || []]);
5274
+ // Always call wsCommunication - it will queue if not connected
5275
+ this.wsCommunication('UPDATE', [...options?.path || []]);
3896
5276
  }), concatMap((data) => {
3897
5277
  if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
3898
5278
  return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
@@ -3908,8 +5288,8 @@ class HTTPManagerStateService extends ComponentStore {
3908
5288
  .pipe(tap((data) => {
3909
5289
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3910
5290
  this.deleteData$(data);
3911
- if (this.wsConnection)
3912
- this.wsCommunication('DELETE', [...options?.path || []]);
5291
+ // Always call wsCommunication - it will queue if not connected
5292
+ this.wsCommunication('DELETE', [...options?.path || []]);
3913
5293
  }), concatMap((data) => {
3914
5294
  if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
3915
5295
  return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, data.id);
@@ -3968,23 +5348,34 @@ class HTTPManagerStateService extends ComponentStore {
3968
5348
  return of([]);
3969
5349
  }));
3970
5350
  })));
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();
5351
+ try {
5352
+ this.databaseOptions = database;
5353
+ this.maxRetries = this.apiOptions.ws?.retry?.times || 3;
5354
+ this.retryDelay = (this.apiOptions.ws?.retry?.delay && this.apiOptions.ws.retry.delay * 1000) || 5 * 1000;
5355
+ // Start next retry countdown at 0 to avoid showing 5000 pre-connection
5356
+ this.wsNextRetry = new BehaviorSubject(0);
5357
+ this.wsNextRetry$ = this.wsNextRetry.asObservable();
5358
+ this.setApiRequestOptions(apiOptions, dataType, database);
5359
+ if (this.databaseOptions && this.databaseOptions.table) {
5360
+ this.localStorageManagerService.createStore({
5361
+ name: this.databaseOptions.table,
5362
+ data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions.expiresIn) } },
5363
+ options: SettingOptions.adapt({
5364
+ storage: StorageType.GLOBAL,
5365
+ encrypted: false,
5366
+ })
5367
+ });
5368
+ this.initDBStorage();
5369
+ }
5370
+ }
5371
+ catch (error) {
5372
+ console.error('Error initializing HTTPManagerStateService:', error);
5373
+ // Initialize with safe defaults
5374
+ this.databaseOptions = undefined;
5375
+ this.maxRetries = 3;
5376
+ this.retryDelay = 5000;
5377
+ this.wsNextRetry = new BehaviorSubject(0);
5378
+ this.wsNextRetry$ = this.wsNextRetry.asObservable();
3988
5379
  }
3989
5380
  }
3990
5381
  /**
@@ -4041,8 +5432,13 @@ class HTTPManagerStateService extends ComponentStore {
4041
5432
  console.error('WSOptions invalid: wsServer is missing or empty');
4042
5433
  return;
4043
5434
  }
5435
+ // Clean up previous subscription to prevent duplicate handlers
5436
+ if (this.connectionStatusSubscription) {
5437
+ this.connectionStatusSubscription.unsubscribe();
5438
+ this.connectionStatusSubscription = undefined;
5439
+ }
4044
5440
  // Setup connection status monitoring (internal subscription to drive retry counters)
4045
- this.setupConnectionStatus().subscribe();
5441
+ this.connectionStatusSubscription = this.setupConnectionStatus().subscribe();
4046
5442
  // Make initial connection attempt
4047
5443
  console.log('🔄 Initial WebSocket connection attempt...');
4048
5444
  this.httpManagerService.connect(this.apiOptions.ws, this.apiOptions.ws.jwtToken || '');
@@ -4174,10 +5570,46 @@ class HTTPManagerStateService extends ComponentStore {
4174
5570
  }
4175
5571
  // WEBSOCKET COMMUNICATION (STATE MANAGER)
4176
5572
  wsCommunication(method, path) {
4177
- if (this.wsConnection && this.apiOptions.ws) {
5573
+ if (!this.apiOptions.ws) {
5574
+ console.warn('wsCommunication called but no WebSocket options configured');
5575
+ return;
5576
+ }
5577
+ // If connected, send immediately (check singleton WebSocketManagerService)
5578
+ if (WebSocketManagerService.isConnected()) {
5579
+ this.sendWsCommunication(method, path);
5580
+ }
5581
+ else {
5582
+ // Queue the message to be sent when connection is established
5583
+ console.log(`⏳ Queuing WS message (not connected): ${method} ${path ? JSON.stringify(path) : ''}`);
5584
+ HTTPManagerStateService.wsCommunicationQueue.push({ method, path });
5585
+ }
5586
+ }
5587
+ /**
5588
+ * Actually send the WebSocket message (called when connected or from queue)
5589
+ */
5590
+ sendWsCommunication(method, path) {
5591
+ if (this.apiOptions.ws) {
4178
5592
  const wsServer = this.apiOptions.ws.id;
5593
+ // Guard: Don't send if channel is empty
5594
+ if (!wsServer || wsServer === '') {
5595
+ console.error('❌ Cannot send WS message: Channel ID is empty!');
5596
+ return;
5597
+ }
5598
+ // DEBUG: Log what we're sending
5599
+ console.log('🔍 [DEBUG] sendWsCommunication called:', {
5600
+ wsServer,
5601
+ wsServerType: typeof wsServer,
5602
+ wsServerEmpty: wsServer === '' || wsServer === null || wsServer === undefined,
5603
+ method,
5604
+ path,
5605
+ user: this.apiOptions.ws.user,
5606
+ fullPayload: { method, path, user: this.apiOptions.ws.user }
5607
+ });
4179
5608
  this.httpManagerService.sendMessageInChannel(wsServer, { method, path, user: this.apiOptions.ws.user });
4180
5609
  }
5610
+ else {
5611
+ console.error('❌ [DEBUG] sendWsCommunication: apiOptions.ws is undefined!');
5612
+ }
4181
5613
  }
4182
5614
  /**
4183
5615
  * Send a message to channel(s)
@@ -4189,19 +5621,18 @@ class HTTPManagerStateService extends ComponentStore {
4189
5621
  const user = this.user.value;
4190
5622
  const messageInfo = ChannelMessage.adapt({ ...message, fromUser: user });
4191
5623
  console.log('📤 wsMessaging called with channels:', channels);
4192
- if (this.wsConnection && this.apiOptions.ws) {
5624
+ if (WebSocketManagerService.isConnected() && this.apiOptions.ws) {
4193
5625
  // If specific channels provided, send to each channel
4194
5626
  // Channels are passed as-is - caller is responsible for including the correct prefix
4195
5627
  if (channels && channels.length > 0) {
4196
5628
  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
- });
5629
+ // Send single message with all channels (more efficient)
5630
+ const user = this.user.value;
5631
+ const messageData = {
5632
+ sessionId: user,
5633
+ content: messageInfo
5634
+ };
5635
+ this.httpManagerService.sendChannelMessageToChannels(channels, messageData);
4205
5636
  }
4206
5637
  else {
4207
5638
  // Fallback to the primary WS channel (already prefixed with SYS-)
@@ -4216,7 +5647,7 @@ class HTTPManagerStateService extends ComponentStore {
4216
5647
  * @param channel - Base channel name (MES- prefix added automatically)
4217
5648
  */
4218
5649
  subscribeToMessageChannel(channel) {
4219
- if (this.wsConnection) {
5650
+ if (WebSocketManagerService.isConnected()) {
4220
5651
  const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
4221
5652
  this.httpManagerService.subscribeToChannel(prefixedChannel);
4222
5653
  console.log(`💬 Subscribed to message channel: ${prefixedChannel}`);
@@ -4230,7 +5661,7 @@ class HTTPManagerStateService extends ComponentStore {
4230
5661
  * @param channel - Base channel name (MES- prefix added automatically)
4231
5662
  */
4232
5663
  unsubscribeFromMessageChannel(channel) {
4233
- if (this.wsConnection) {
5664
+ if (WebSocketManagerService.isConnected()) {
4234
5665
  const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
4235
5666
  this.httpManagerService.unsubscribeFromChannel(prefixedChannel);
4236
5667
  console.log(`💬 Unsubscribed from message channel: ${prefixedChannel}`);
@@ -4246,8 +5677,11 @@ class HTTPManagerStateService extends ComponentStore {
4246
5677
  * Use subscribeToMessageChannel() for MES- prefixed channels
4247
5678
  */
4248
5679
  subscribeToChannel(channel) {
4249
- if (this.wsConnection) {
4250
- this.httpManagerService.subscribeToChannel(channel);
5680
+ if (WebSocketManagerService.isConnected()) {
5681
+ // Get current user data to send with subscription
5682
+ const currentUser = this.user.value;
5683
+ console.log('👤 Subscribing with user:', currentUser);
5684
+ this.httpManagerService.subscribeToChannel(channel, currentUser);
4251
5685
  }
4252
5686
  else {
4253
5687
  console.warn('Cannot subscribe: WebSocket not connected.');
@@ -4257,8 +5691,11 @@ class HTTPManagerStateService extends ComponentStore {
4257
5691
  * Subscribe to multiple channels at once
4258
5692
  */
4259
5693
  subscribeToChannels(channels) {
4260
- if (this.wsConnection) {
4261
- this.httpManagerService.subscribeToChannels(channels);
5694
+ if (WebSocketManagerService.isConnected()) {
5695
+ // Get current user data to send with subscription
5696
+ const currentUser = this.user.value;
5697
+ console.log('👤 Subscribing to', channels.length, 'channels with user:', currentUser);
5698
+ this.httpManagerService.subscribeToChannels(channels, currentUser);
4262
5699
  }
4263
5700
  else {
4264
5701
  console.warn('Cannot subscribe: WebSocket not connected.');
@@ -4268,7 +5705,7 @@ class HTTPManagerStateService extends ComponentStore {
4268
5705
  * Unsubscribe from a channel
4269
5706
  */
4270
5707
  unsubscribeFromChannel(channel) {
4271
- if (this.wsConnection) {
5708
+ if (WebSocketManagerService.isConnected()) {
4272
5709
  this.httpManagerService.unsubscribeFromChannel(channel);
4273
5710
  }
4274
5711
  else {
@@ -4291,7 +5728,7 @@ class HTTPManagerStateService extends ComponentStore {
4291
5728
  * Create a new channel on the server
4292
5729
  */
4293
5730
  createChannel(channel) {
4294
- if (this.wsConnection) {
5731
+ if (WebSocketManagerService.isConnected()) {
4295
5732
  this.httpManagerService.createChannel(channel);
4296
5733
  }
4297
5734
  else {
@@ -4302,7 +5739,7 @@ class HTTPManagerStateService extends ComponentStore {
4302
5739
  * Delete a channel from the server
4303
5740
  */
4304
5741
  deleteChannel(channel) {
4305
- if (this.wsConnection) {
5742
+ if (WebSocketManagerService.isConnected()) {
4306
5743
  this.httpManagerService.deleteChannel(channel);
4307
5744
  }
4308
5745
  else {
@@ -4313,7 +5750,7 @@ class HTTPManagerStateService extends ComponentStore {
4313
5750
  * Request list of all channels from server
4314
5751
  */
4315
5752
  getAllChannels() {
4316
- if (this.wsConnection) {
5753
+ if (WebSocketManagerService.isConnected()) {
4317
5754
  this.httpManagerService.getAllChannels();
4318
5755
  }
4319
5756
  else {
@@ -4324,7 +5761,7 @@ class HTTPManagerStateService extends ComponentStore {
4324
5761
  * Get users in a specific channel
4325
5762
  */
4326
5763
  getUsersInChannel(channel) {
4327
- if (this.wsConnection) {
5764
+ if (WebSocketManagerService.isConnected()) {
4328
5765
  this.httpManagerService.getUsersInChannel(channel);
4329
5766
  }
4330
5767
  else {
@@ -4338,7 +5775,7 @@ class HTTPManagerStateService extends ComponentStore {
4338
5775
  * @param channel - Base channel name (MES- prefix added automatically)
4339
5776
  */
4340
5777
  createNotificationChannel(channel) {
4341
- if (this.wsConnection) {
5778
+ if (WebSocketManagerService.isConnected()) {
4342
5779
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4343
5780
  this.httpManagerService.createNotificationChannel(prefixedChannel);
4344
5781
  console.log(`📢 Creating notification channel: ${prefixedChannel}`);
@@ -4351,7 +5788,7 @@ class HTTPManagerStateService extends ComponentStore {
4351
5788
  * Request list of all notification channels from server (in-memory)
4352
5789
  */
4353
5790
  getNotificationChannels() {
4354
- if (this.wsConnection) {
5791
+ if (WebSocketManagerService.isConnected()) {
4355
5792
  this.httpManagerService.getNotificationChannels();
4356
5793
  }
4357
5794
  else {
@@ -4363,7 +5800,7 @@ class HTTPManagerStateService extends ComponentStore {
4363
5800
  * Returns unique channels that have notifications posted today
4364
5801
  */
4365
5802
  getTodaysNotificationChannels() {
4366
- if (this.wsConnection) {
5803
+ if (WebSocketManagerService.isConnected()) {
4367
5804
  this.httpManagerService.getTodaysNotificationChannels();
4368
5805
  }
4369
5806
  else {
@@ -4375,7 +5812,7 @@ class HTTPManagerStateService extends ComponentStore {
4375
5812
  * @param channel - Base channel name (MES- prefix added automatically)
4376
5813
  */
4377
5814
  subscribeToNotificationChannel(channel, options, user) {
4378
- if (this.wsConnection) {
5815
+ if (WebSocketManagerService.isConnected()) {
4379
5816
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4380
5817
  this.httpManagerService.subscribeToNotificationChannel(prefixedChannel, options, user);
4381
5818
  console.log(`📢 Subscribing to notification channel: ${prefixedChannel}`);
@@ -4389,7 +5826,7 @@ class HTTPManagerStateService extends ComponentStore {
4389
5826
  * @param channel - Base channel name (MES- prefix added automatically)
4390
5827
  */
4391
5828
  unsubscribeFromNotificationChannel(channel) {
4392
- if (this.wsConnection) {
5829
+ if (WebSocketManagerService.isConnected()) {
4393
5830
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4394
5831
  this.httpManagerService.unsubscribeFromNotificationChannel(prefixedChannel);
4395
5832
  console.log(`📢 Unsubscribing from notification channel: ${prefixedChannel}`);
@@ -4403,7 +5840,7 @@ class HTTPManagerStateService extends ComponentStore {
4403
5840
  * @param channel - Base channel name (MES- prefix added automatically)
4404
5841
  */
4405
5842
  sendNotification(channel, content) {
4406
- if (this.wsConnection) {
5843
+ if (WebSocketManagerService.isConnected()) {
4407
5844
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4408
5845
  this.httpManagerService.sendNotification(prefixedChannel, content);
4409
5846
  console.log(`📢 Sending notification to channel: ${prefixedChannel}`);
@@ -5822,17 +7259,25 @@ class StateServiceDemo extends HTTPManagerStateService {
5822
7259
  }
5823
7260
  /**
5824
7261
  * Initialize WebSocket connection with server configuration
7262
+ * @param server - Backend server URL
7263
+ * @param wsServer - WebSocket server URL
7264
+ * @param jwtToken - JWT authentication token
7265
+ * @param user - User information
7266
+ * @param path - Path for constructing channel name (e.g., ['ai','tests'])
5825
7267
  */
5826
- updateConnection(server, wsServer, jwtToken, user) {
7268
+ updateConnection(server, wsServer, jwtToken, user, path = ['ai', 'tests']) {
7269
+ // Construct channel name from path: ['ai','tests'] → 'ai/tests'
7270
+ const channelId = path.join('/');
5827
7271
  this.setApiRequestOptions({
5828
7272
  server,
7273
+ path, // Set the path for HTTP requests
5829
7274
  retry: RetryOptions.adapt({
5830
7275
  times: 3,
5831
7276
  delay: 1,
5832
7277
  }),
5833
7278
  adapter: OIDCClient.adapt,
5834
7279
  ws: {
5835
- id: 'USERS123',
7280
+ id: channelId, // Use path-based channel ID instead of hardcoded 'USERS123'
5836
7281
  wsServer,
5837
7282
  jwtToken,
5838
7283
  user,
@@ -6157,7 +7602,7 @@ class StateDataRequestService extends HTTPManagerStateService {
6157
7602
  this.nextRetry$ = this.wsNextRetry$;
6158
7603
  this.path = ['ai', 'tests'];
6159
7604
  }
6160
- updateConnection(server, wsServer, jwtToken, user, path = [], wsChannel = '') {
7605
+ updateConnection(server, wsServer, jwtToken, user, path = []) {
6161
7606
  this.path = path;
6162
7607
  this.setApiRequestOptions({
6163
7608
  server,
@@ -6167,7 +7612,7 @@ class StateDataRequestService extends HTTPManagerStateService {
6167
7612
  }),
6168
7613
  adapter: OIDCClient.adapt,
6169
7614
  ws: {
6170
- id: wsChannel,
7615
+ id: this.path.join('/'),
6171
7616
  wsServer,
6172
7617
  jwtToken,
6173
7618
  user, // general info about user
@@ -6218,7 +7663,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6218
7663
  class WsDataControlComponent {
6219
7664
  constructor() {
6220
7665
  this.path = ['ai', 'tests'];
6221
- this.wsChannel = '';
6222
7666
  this.stateDataRequestService = inject(StateDataRequestService);
6223
7667
  this.user$ = this.stateDataRequestService.user$;
6224
7668
  this.users$ = this.stateDataRequestService.userList$;
@@ -6230,7 +7674,7 @@ class WsDataControlComponent {
6230
7674
  };
6231
7675
  }
6232
7676
  ngOnInit() {
6233
- this.stateDataRequestService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path, this.wsChannel);
7677
+ this.stateDataRequestService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
6234
7678
  this.stateDataRequestService.getData();
6235
7679
  }
6236
7680
  onGetData() {
@@ -6255,11 +7699,11 @@ class WsDataControlComponent {
6255
7699
  this.stateDataRequestService.deleteData(lastRec);
6256
7700
  }
6257
7701
  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" }] }); }
7702
+ 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
7703
  }
6260
7704
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsDataControlComponent, decorators: [{
6261
7705
  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"] }]
7706
+ 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
7707
  }], propDecorators: { server: [{
6264
7708
  type: Input
6265
7709
  }], wsServer: [{
@@ -6270,12 +7714,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6270
7714
  type: Input
6271
7715
  }], path: [{
6272
7716
  type: Input
6273
- }], wsChannel: [{
6274
- type: Input
6275
7717
  }] } });
6276
7718
 
6277
7719
  class WsMessagingComponent {
6278
7720
  constructor() {
7721
+ this.path = ['ai', 'tests']; // Default path for channel name
6279
7722
  this.destroy$ = new Subject();
6280
7723
  this.fb = inject(FormBuilder);
6281
7724
  this.messageService = inject(MessageServiceDemo);
@@ -6320,11 +7763,14 @@ class WsMessagingComponent {
6320
7763
  return this.messages.get('content');
6321
7764
  }
6322
7765
  ngOnInit() {
6323
- this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user);
7766
+ this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
6324
7767
  // Only trigger once when connection becomes true
6325
7768
  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();
7769
+ // Wait a moment for subscription to be processed, then fetch channels
7770
+ setTimeout(() => {
7771
+ console.log('📋 Fetching channels after connection...');
7772
+ this.messageService.getAllChannels();
7773
+ }, 500); // 500ms delay to ensure subscription is processed
6328
7774
  });
6329
7775
  // Subscribe to latest messages and show toast notification
6330
7776
  this.latestCommunicationMessages$.pipe(filter$1(message => !!message), takeUntil$1(this.destroy$)).subscribe((message) => {
@@ -6428,11 +7874,11 @@ class WsMessagingComponent {
6428
7874
  this.content.reset();
6429
7875
  }
6430
7876
  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" }] }); }
7877
+ 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
7878
  }
6433
7879
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsMessagingComponent, decorators: [{
6434
7880
  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"] }]
7881
+ 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
7882
  }], propDecorators: { server: [{
6437
7883
  type: Input
6438
7884
  }], wsServer: [{
@@ -6441,6 +7887,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6441
7887
  type: Input
6442
7888
  }], user: [{
6443
7889
  type: Input
7890
+ }], path: [{
7891
+ type: Input
6444
7892
  }] } });
6445
7893
 
6446
7894
  class WsNotificationsComponent {
@@ -6667,7 +8115,6 @@ class RequestManagerWsDemoComponent {
6667
8115
  this.stateService = inject(StateServiceDemo);
6668
8116
  this.fb = inject(FormBuilder);
6669
8117
  this.path = ['ai', 'tests'];
6670
- this.wsChannel = '';
6671
8118
  this.user$ = this.stateService.user$;
6672
8119
  this.attempts$ = this.stateService.wsRetryAttempts$;
6673
8120
  this.nextRetry$ = this.stateService.wsNextRetry$;
@@ -6676,14 +8123,14 @@ class RequestManagerWsDemoComponent {
6676
8123
  this.isPending$ = this.stateService.isPending$;
6677
8124
  }
6678
8125
  ngOnInit() {
6679
- this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user);
8126
+ this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
6680
8127
  }
6681
8128
  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" }] }); }
8129
+ 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
8130
  }
6684
8131
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerWsDemoComponent, decorators: [{
6685
8132
  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" }]
8133
+ 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
8134
  }], propDecorators: { server: [{
6688
8135
  type: Input
6689
8136
  }], wsServer: [{
@@ -6694,8 +8141,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6694
8141
  type: Input
6695
8142
  }], path: [{
6696
8143
  type: Input
6697
- }], wsChannel: [{
6698
- type: Input
6699
8144
  }] } });
6700
8145
 
6701
8146
  class Settings {
@@ -6916,7 +8361,6 @@ class HttpRequestServicesDemoComponent {
6916
8361
  this.jwtToken = '';
6917
8362
  this.server = 'http:';
6918
8363
  this.path = ['ai', 'tests'];
6919
- this.wsChannel = '';
6920
8364
  this.requestTypes = [
6921
8365
  { name: "Http Service", value: 'http_service' },
6922
8366
  // { name: "Http Signals Service", value: 'http_signals_service', new: true },
@@ -6937,11 +8381,11 @@ class HttpRequestServicesDemoComponent {
6937
8381
  this.selectedService = this.requestTypes[type].value;
6938
8382
  }
6939
8383
  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" }] }); }
8384
+ 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
8385
  }
6942
8386
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HttpRequestServicesDemoComponent, decorators: [{
6943
8387
  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"] }]
8388
+ 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
8389
  }], ctorParameters: () => [{ type: ConfigOptions, decorators: [{
6946
8390
  type: Inject,
6947
8391
  args: [CONFIG_SETTINGS_TOKEN]
@@ -6955,8 +8399,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6955
8399
  type: Input
6956
8400
  }], path: [{
6957
8401
  type: Input
6958
- }], wsChannel: [{
6959
- type: Input
6960
8402
  }], adapter: [{
6961
8403
  type: Input
6962
8404
  }], mapper: [{
@@ -7628,5 +9070,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
7628
9070
  * Generated bundle index. Do not edit.
7629
9071
  */
7630
9072
 
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 };
9073
+ 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
9074
  //# sourceMappingURL=http-request-manager.mjs.map