http-request-manager 18.7.30 → 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);
@@ -1544,7 +2180,7 @@ class RequestService extends WebsocketService {
1544
2180
  this.progress = new BehaviorSubject(0);
1545
2181
  this.progress$ = this.progress.asObservable();
1546
2182
  }
1547
- // Implementation
2183
+ // Implementation
1548
2184
  getRecordRequest(options) {
1549
2185
  const urlPath = this.buildUrlPath(options);
1550
2186
  const headers = this.buildCombinedHeaders(options);
@@ -1558,7 +2194,6 @@ class RequestService extends WebsocketService {
1558
2194
  }).pipe(tap(data => console.log('STREAM DATA', data)), requestStreaming({ streamType: options.streamType || StreamType.AI_STREAMING }), this.requestStreaming(options), this.handleFinalize())
1559
2195
  : this.http.get(urlPath, headers).pipe(this.request(options));
1560
2196
  }
1561
- // Implementation
1562
2197
  createRecordRequest(options, data) {
1563
2198
  const urlPath = this.buildUrlPath(options);
1564
2199
  const headers = this.buildCombinedHeaders(options);
@@ -1724,72 +2359,491 @@ class RequestService extends WebsocketService {
1724
2359
  }
1725
2360
  return fileType;
1726
2361
  }
1727
- combineHeaders(headers, isStreaming) {
1728
- return (isStreaming) ?
1729
- {
1730
- ...headers,
1731
- observe: 'events',
1732
- responseType: 'text',
1733
- reportProgress: true,
1734
- 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);
1735
2821
  }
1736
- : headers;
2822
+ }
2823
+ return skipped;
1737
2824
  }
1738
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
1739
- 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' }); }
1740
2840
  }
1741
- 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: [{
1742
2842
  type: Injectable,
1743
2843
  args: [{
1744
- providedIn: 'root'
2844
+ providedIn: 'root',
1745
2845
  }]
1746
- }] });
1747
-
1748
- function countdown(duration) {
1749
- return defer(() => {
1750
- const currentCount = { current: duration };
1751
- return interval(1000).pipe(map(() => --currentCount.current), takeWhile(count => count >= 0));
1752
- });
1753
- }
1754
-
1755
- const DEFAULT_MAX_RETRIES = 3;
1756
- function delayedRetry(delayMs, maxRetry = DEFAULT_MAX_RETRIES) {
1757
- return (src) => src.pipe(retry({
1758
- count: maxRetry,
1759
- delay: () => timer(delayMs)
1760
- }));
1761
- }
1762
-
1763
- function requestPolling(pollInterval, stopCondition$, isPending$) {
1764
- return (source) => {
1765
- return interval(pollInterval * 1000)
1766
- .pipe(startWith(0), tap(() => {
1767
- try {
1768
- if (isPending$ && typeof isPending$.next === 'function') {
1769
- isPending$.next(true);
1770
- }
1771
- else if (isPending$ && typeof isPending$.set === 'function') {
1772
- isPending$.set(true);
1773
- }
1774
- }
1775
- catch (e) {
1776
- // no-op if setting fails
1777
- }
1778
- }), mergeMap(() => source), tap(() => {
1779
- try {
1780
- if (isPending$ && typeof isPending$.next === 'function') {
1781
- isPending$.next(false);
1782
- }
1783
- else if (isPending$ && typeof isPending$.set === 'function') {
1784
- isPending$.set(false);
1785
- }
1786
- }
1787
- catch (e) {
1788
- // no-op if setting fails
1789
- }
1790
- }), takeUntil(stopCondition$));
1791
- };
1792
- }
2846
+ }], ctorParameters: () => [{ type: WebSocketManagerService }] });
1793
2847
 
1794
2848
  class DatabaseStorage {
1795
2849
  constructor(table = '', expiresIn) {
@@ -1878,6 +2932,12 @@ class HTTPManagerService extends RequestService {
1878
2932
  this.toastMessage = inject(ToastMessageDisplayService);
1879
2933
  this.ng_injector = inject(Injector);
1880
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$;
1881
2941
  this.countdown = new BehaviorSubject(0);
1882
2942
  this.countdown$ = this.countdown.asObservable();
1883
2943
  this.error = new BehaviorSubject(false);
@@ -1888,6 +2948,130 @@ class HTTPManagerService extends RequestService {
1888
2948
  this.config = ApiRequest.adapt();
1889
2949
  this.config = (configOptions) ? ApiRequest.adapt(configOptions.httpRequestOptions) : this.config;
1890
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
+ }
1891
3075
  // REQUESTS
1892
3076
  getRequest(options, params) {
1893
3077
  this.isPending.next(true);
@@ -2417,7 +3601,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2417
3601
  }] }] });
2418
3602
 
2419
3603
  class ApiRequest {
2420
- 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) {
2421
3605
  this.server = server;
2422
3606
  this.path = path;
2423
3607
  this.headers = headers;
@@ -2431,10 +3615,11 @@ class ApiRequest {
2431
3615
  this.saveAs = saveAs;
2432
3616
  this.fileContentHeader = fileContentHeader;
2433
3617
  this.ws = ws;
3618
+ this.env = env;
2434
3619
  }
2435
3620
  static adapt(item) {
2436
3621
  const server = Array.isArray(item?.server) ? item.server.join('/') : item?.server || '';
2437
- 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');
2438
3623
  }
2439
3624
  }
2440
3625
 
@@ -2449,17 +3634,18 @@ class RequestOptions {
2449
3634
  }
2450
3635
 
2451
3636
  class WSOptions {
2452
- constructor(id = '', wsServer = '', jwtToken = '', permissions, channels, user, retry) {
3637
+ constructor(id = '', wsServer = '', jwtToken = '', permissions, channels, wsUpdateChannels, user, retry) {
2453
3638
  this.id = id;
2454
3639
  this.wsServer = wsServer;
2455
3640
  this.jwtToken = jwtToken;
2456
3641
  this.permissions = permissions;
2457
3642
  this.channels = channels;
3643
+ this.wsUpdateChannels = wsUpdateChannels;
2458
3644
  this.user = user;
2459
3645
  this.retry = retry;
2460
3646
  }
2461
3647
  static adapt(item) {
2462
- 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);
2463
3649
  }
2464
3650
  }
2465
3651
 
@@ -2581,8 +3767,24 @@ class LocalStorageManagerService extends ComponentStore {
2581
3767
  console.warn('No App ID found - AppId not Provided');
2582
3768
  return;
2583
3769
  }
2584
- const storageData = (options.encrypted) ? this.encryption.decrypt(found.data, this.app.appID) : found.data;
2585
- 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
+ }
2586
3788
  }
2587
3789
  else {
2588
3790
  return null;
@@ -2750,10 +3952,39 @@ class LocalStorageManagerService extends ComponentStore {
2750
3952
  const str = localStorage.getItem(this.storageSettingsName);
2751
3953
  const localStr = localStorage.getItem(this.storageName);
2752
3954
  const sessionStr = sessionStorage.getItem(this.storageName);
2753
- const localData = (localStr) ? JSON.parse(localStr) : null;
2754
- const sessionData = (sessionStr) ? JSON.parse(sessionStr) : null;
2755
- const decryptedStr = str ? this.encryption.decrypt(str, this.app.appID) : null;
2756
- 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
+ }
2757
3988
  settings.forEach(store => {
2758
3989
  const expired = (store.options?.expires || 0) > 0 && this.utils.hasExpired(store.options?.expires || 0);
2759
3990
  if (!expired) {
@@ -2806,6 +4037,47 @@ class LocalStorageManagerService extends ComponentStore {
2806
4037
  ngOnDestroy() {
2807
4038
  this.persistence$.unsubscribe();
2808
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
+ }
2809
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 }); }
2810
4082
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocalStorageManagerService, providedIn: 'root' }); }
2811
4083
  }
@@ -2858,10 +4130,24 @@ class LocalStorageSignalsManagerService {
2858
4130
  console.warn('No App ID found - AppId not Provided');
2859
4131
  return null;
2860
4132
  }
2861
- const storageData = options.encrypted
2862
- ? this.encryption.decrypt(found.data, this.app.appID)
2863
- : found.data;
2864
- 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
+ }
2865
4151
  });
2866
4152
  this.settings = computed(() => this.state().settings);
2867
4153
  this.setting = (store) => computed(() => this.state().settings.find(item => item.name === store) ?? null);
@@ -2989,8 +4275,23 @@ class LocalStorageSignalsManagerService {
2989
4275
  const sessionStr = sessionStorage.getItem(this.storageName);
2990
4276
  const localData = localStr ? JSON.parse(localStr) : [];
2991
4277
  const sessionData = sessionStr ? JSON.parse(sessionStr) : [];
2992
- const decryptedStr = str ? this.encryption.decrypt(str, this.app.appID) : null;
2993
- 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
+ }
2994
4295
  settings.forEach(store => {
2995
4296
  // normalize options so we compare numbers and compute expires correctly
2996
4297
  const options = SettingOptions.adapt(store.options);
@@ -3449,13 +4750,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3449
4750
  }], ctorParameters: () => [] });
3450
4751
 
3451
4752
  class ChannelMessage {
3452
- 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;
3453
4757
  this.sessionId = sessionId;
3454
4758
  this.content = content;
4759
+ this.timestamp = timestamp;
3455
4760
  }
3456
4761
  static adapt(item) {
3457
- return new ChannelMessage(item?.sessionId || item?.id, // Support both for backward compatibility
3458
- 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);
3459
4764
  }
3460
4765
  }
3461
4766
 
@@ -3490,6 +4795,8 @@ class HTTPManagerStateService extends ComponentStore {
3490
4795
  getUsersForChannel$(channel) {
3491
4796
  return this.userListByChannel$.pipe(map(channelMap => channelMap.get(channel) || []));
3492
4797
  }
4798
+ // Message queue for WebSocket communication (processed when connection is established)
4799
+ static { this.wsCommunicationQueue = []; }
3493
4800
  constructor(apiOptions = ApiRequest.adapt(), dataType, database) {
3494
4801
  super(defaultState);
3495
4802
  this.apiOptions = apiOptions;
@@ -3517,7 +4824,7 @@ class HTTPManagerStateService extends ComponentStore {
3517
4824
  this.messages$ = this.messages.asObservable();
3518
4825
  this.userListByChannel = new BehaviorSubject(new Map());
3519
4826
  this.userListByChannel$ = this.userListByChannel.asObservable();
3520
- // Legacy support - returns all unique users across all channels
4827
+ // Returns all unique users across all channels
3521
4828
  this.userList = new BehaviorSubject([]);
3522
4829
  this.userList$ = this.userList.asObservable();
3523
4830
  this.user = new BehaviorSubject(null);
@@ -3541,17 +4848,19 @@ class HTTPManagerStateService extends ComponentStore {
3541
4848
  this.latestCommunicationMessages$ = this.latestCommunicationMessages.asObservable();
3542
4849
  this.userAction = new BehaviorSubject(null);
3543
4850
  this.userAction$ = this.userAction.asObservable();
3544
- this.wsConnection = false;
3545
4851
  this.wsOptions = WSOptions.adapt();
3546
- // Expose raw WS connection status directly to UI
4852
+ // Expose raw WS connection status directly to UI (from singleton WebSocketManagerService)
3547
4853
  this.connectionStatus$ = this.httpManagerService.connectionStatus$;
3548
4854
  // WebSocket
3549
- this.initWS = this.effect((wsOptions$) => wsOptions$.pipe(
3550
- // tap((wsOptions) => { debugger
3551
- // this.wsOptions = wsOptions
3552
- // }),
3553
- switchMap((wsOptions) => merge(this.httpManagerService.connectionStatus$.pipe(tap((isConnected) => {
3554
- 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
+ }
3555
4864
  })), this.httpManagerService.messages$.pipe(tap((message) => {
3556
4865
  if (!message)
3557
4866
  return;
@@ -3559,6 +4868,8 @@ class HTTPManagerStateService extends ComponentStore {
3559
4868
  const currentMessages = this.messages.value;
3560
4869
  this.messages.next([...currentMessages, message]);
3561
4870
  console.log('Received:', message);
4871
+ // Debug: Log all message types
4872
+ console.log('📨 Message type:', message.type);
3562
4873
  if (message.error === 'JWT_INVALID') {
3563
4874
  this.shouldRetry = false;
3564
4875
  this.httpManagerService.disconnect();
@@ -3572,16 +4883,22 @@ class HTTPManagerStateService extends ComponentStore {
3572
4883
  switch (message.type) {
3573
4884
  case 'channelsList':
3574
4885
  console.log('💬 Channels:', message.channels);
3575
- // this.channelList = message.channels
3576
- // if (this.channelList.includes(wsOptions.id)) {
3577
- // this.httpManagerService.subscribeToChannel(wsOptions.id)
3578
- // } else {
3579
- // this.httpManagerService.createChannel(wsOptions.id)
3580
- // }
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
+ }
3581
4896
  this.channels.next(message.channels);
3582
4897
  break;
3583
4898
  case 'subscribed':
3584
4899
  console.log(`✅ Subscription confirmed: ${message.channel}`);
4900
+ // Track as subscribed now that server confirmed
4901
+ WebSocketManagerService.addSubscribedChannel(message.channel);
3585
4902
  break;
3586
4903
  case 'unsubscribed':
3587
4904
  console.log(`🔓 Unsubscription confirmed: ${message.channel}`);
@@ -3589,6 +4906,11 @@ class HTTPManagerStateService extends ComponentStore {
3589
4906
  case 'info':
3590
4907
  // Already subscribed or other info messages
3591
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
+ }
3592
4914
  break;
3593
4915
  case 'stateMangerMessage':
3594
4916
  // Compare sender's session ID with current user's ID
@@ -3607,19 +4929,58 @@ class HTTPManagerStateService extends ComponentStore {
3607
4929
  break;
3608
4930
  case 'channelMessage':
3609
4931
  // Handle channel-based messages (from sendChannelMessage)
3610
- // Structure: { type: 'channelMessage', channels: [...], sessionId: {id, ldap, name, email}, content: {message payload} }
4932
+ // Structure: { type: 'channelMessage', messageId, channel, sessionId, content, timestamp }
3611
4933
  // Skip messages from self
3612
- const senderSessionId = message.sessionId?.id;
4934
+ const senderSessionId = message.sessionId?.id || message.sessionId;
3613
4935
  if (senderSessionId === this.user.value?.id) {
4936
+ console.log('🔇 Skipping message from self (sessionId match)');
3614
4937
  break;
3615
4938
  }
3616
4939
  console.log('💬 Channel Message received:', message);
3617
- 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);
3618
4969
  this.appendMessages(ChannelMessage.adapt({
3619
4970
  sessionId: message.sessionId,
3620
4971
  content: message.content,
3621
4972
  }));
3622
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
+ }
3623
4984
  break;
3624
4985
  case 'usersInChannel':
3625
4986
  console.log(`👥 Users in channel "${message.channel}":`, message.data.users);
@@ -3835,11 +5196,21 @@ class HTTPManagerStateService extends ComponentStore {
3835
5196
  // FETCH RECORD
3836
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) => {
3837
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
+ }
3838
5206
  const requestOptions = this.updateRequestOptions(options?.headers);
3839
- console.log('🌐 Making GET request to path:', options?.path);
3840
- return this.httpManagerService.getRequest(requestOptions, options?.path)
5207
+ console.log('🌐 Making GET request to path:', this.apiOptions.path);
5208
+ return this.httpManagerService.getRequest(requestOptions)
3841
5209
  .pipe(tap((data) => {
3842
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);
3843
5214
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3844
5215
  const id = options.path?.length ? options.path[options.path.length - 1] : null;
3845
5216
  if (method === 'DELETE') {
@@ -3861,8 +5232,16 @@ class HTTPManagerStateService extends ComponentStore {
3861
5232
  return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, id);
3862
5233
  if (method === 'UPDATE' && data)
3863
5234
  return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
3864
- if (method === 'CREATE' && data)
3865
- 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
+ }
3866
5245
  }
3867
5246
  return of(data);
3868
5247
  }));
@@ -3875,8 +5254,8 @@ class HTTPManagerStateService extends ComponentStore {
3875
5254
  .pipe(tap((data) => {
3876
5255
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3877
5256
  this.addData$(data);
3878
- if (this.wsConnection)
3879
- 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]);
3880
5259
  }), concatMap((data) => {
3881
5260
  if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
3882
5261
  return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
@@ -3892,8 +5271,8 @@ class HTTPManagerStateService extends ComponentStore {
3892
5271
  .pipe(tap((data) => {
3893
5272
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3894
5273
  this.updateData$(data);
3895
- if (this.wsConnection)
3896
- this.wsCommunication('UPDATE', [...options?.path || []]);
5274
+ // Always call wsCommunication - it will queue if not connected
5275
+ this.wsCommunication('UPDATE', [...options?.path || []]);
3897
5276
  }), concatMap((data) => {
3898
5277
  if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
3899
5278
  return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
@@ -3909,8 +5288,8 @@ class HTTPManagerStateService extends ComponentStore {
3909
5288
  .pipe(tap((data) => {
3910
5289
  data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
3911
5290
  this.deleteData$(data);
3912
- if (this.wsConnection)
3913
- this.wsCommunication('DELETE', [...options?.path || []]);
5291
+ // Always call wsCommunication - it will queue if not connected
5292
+ this.wsCommunication('DELETE', [...options?.path || []]);
3914
5293
  }), concatMap((data) => {
3915
5294
  if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
3916
5295
  return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, data.id);
@@ -3969,23 +5348,34 @@ class HTTPManagerStateService extends ComponentStore {
3969
5348
  return of([]);
3970
5349
  }));
3971
5350
  })));
3972
- this.databaseOptions = database;
3973
- this.maxRetries = this.apiOptions.ws?.retry?.times || 3;
3974
- this.retryDelay = (this.apiOptions.ws?.retry?.delay && this.apiOptions.ws.retry.delay * 1000) || 5 * 1000;
3975
- // Start next retry countdown at 0 to avoid showing 5000 pre-connection
3976
- this.wsNextRetry = new BehaviorSubject(0);
3977
- this.wsNextRetry$ = this.wsNextRetry.asObservable();
3978
- this.setApiRequestOptions(apiOptions, dataType, database);
3979
- if (this.databaseOptions && this.databaseOptions.table) {
3980
- this.localStorageManagerService.createStore({
3981
- name: this.databaseOptions.table,
3982
- data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions.expiresIn) } },
3983
- options: SettingOptions.adapt({
3984
- storage: StorageType.GLOBAL,
3985
- encrypted: false,
3986
- })
3987
- });
3988
- 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();
3989
5379
  }
3990
5380
  }
3991
5381
  /**
@@ -4042,8 +5432,13 @@ class HTTPManagerStateService extends ComponentStore {
4042
5432
  console.error('WSOptions invalid: wsServer is missing or empty');
4043
5433
  return;
4044
5434
  }
5435
+ // Clean up previous subscription to prevent duplicate handlers
5436
+ if (this.connectionStatusSubscription) {
5437
+ this.connectionStatusSubscription.unsubscribe();
5438
+ this.connectionStatusSubscription = undefined;
5439
+ }
4045
5440
  // Setup connection status monitoring (internal subscription to drive retry counters)
4046
- this.setupConnectionStatus().subscribe();
5441
+ this.connectionStatusSubscription = this.setupConnectionStatus().subscribe();
4047
5442
  // Make initial connection attempt
4048
5443
  console.log('🔄 Initial WebSocket connection attempt...');
4049
5444
  this.httpManagerService.connect(this.apiOptions.ws, this.apiOptions.ws.jwtToken || '');
@@ -4175,10 +5570,46 @@ class HTTPManagerStateService extends ComponentStore {
4175
5570
  }
4176
5571
  // WEBSOCKET COMMUNICATION (STATE MANAGER)
4177
5572
  wsCommunication(method, path) {
4178
- 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) {
4179
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
+ });
4180
5608
  this.httpManagerService.sendMessageInChannel(wsServer, { method, path, user: this.apiOptions.ws.user });
4181
5609
  }
5610
+ else {
5611
+ console.error('❌ [DEBUG] sendWsCommunication: apiOptions.ws is undefined!');
5612
+ }
4182
5613
  }
4183
5614
  /**
4184
5615
  * Send a message to channel(s)
@@ -4190,19 +5621,18 @@ class HTTPManagerStateService extends ComponentStore {
4190
5621
  const user = this.user.value;
4191
5622
  const messageInfo = ChannelMessage.adapt({ ...message, fromUser: user });
4192
5623
  console.log('📤 wsMessaging called with channels:', channels);
4193
- if (this.wsConnection && this.apiOptions.ws) {
5624
+ if (WebSocketManagerService.isConnected() && this.apiOptions.ws) {
4194
5625
  // If specific channels provided, send to each channel
4195
5626
  // Channels are passed as-is - caller is responsible for including the correct prefix
4196
5627
  if (channels && channels.length > 0) {
4197
5628
  console.log(`📤 Sending to ${channels.length} channel(s):`, channels);
4198
- channels.forEach(channel => {
4199
- if (channel === 'allChannels') {
4200
- this.httpManagerService.sendBroadcast(messageInfo);
4201
- }
4202
- else {
4203
- this.httpManagerService.sendChannelMessage(channel, messageInfo);
4204
- }
4205
- });
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);
4206
5636
  }
4207
5637
  else {
4208
5638
  // Fallback to the primary WS channel (already prefixed with SYS-)
@@ -4217,7 +5647,7 @@ class HTTPManagerStateService extends ComponentStore {
4217
5647
  * @param channel - Base channel name (MES- prefix added automatically)
4218
5648
  */
4219
5649
  subscribeToMessageChannel(channel) {
4220
- if (this.wsConnection) {
5650
+ if (WebSocketManagerService.isConnected()) {
4221
5651
  const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
4222
5652
  this.httpManagerService.subscribeToChannel(prefixedChannel);
4223
5653
  console.log(`💬 Subscribed to message channel: ${prefixedChannel}`);
@@ -4231,7 +5661,7 @@ class HTTPManagerStateService extends ComponentStore {
4231
5661
  * @param channel - Base channel name (MES- prefix added automatically)
4232
5662
  */
4233
5663
  unsubscribeFromMessageChannel(channel) {
4234
- if (this.wsConnection) {
5664
+ if (WebSocketManagerService.isConnected()) {
4235
5665
  const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
4236
5666
  this.httpManagerService.unsubscribeFromChannel(prefixedChannel);
4237
5667
  console.log(`💬 Unsubscribed from message channel: ${prefixedChannel}`);
@@ -4247,8 +5677,11 @@ class HTTPManagerStateService extends ComponentStore {
4247
5677
  * Use subscribeToMessageChannel() for MES- prefixed channels
4248
5678
  */
4249
5679
  subscribeToChannel(channel) {
4250
- if (this.wsConnection) {
4251
- 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);
4252
5685
  }
4253
5686
  else {
4254
5687
  console.warn('Cannot subscribe: WebSocket not connected.');
@@ -4258,8 +5691,11 @@ class HTTPManagerStateService extends ComponentStore {
4258
5691
  * Subscribe to multiple channels at once
4259
5692
  */
4260
5693
  subscribeToChannels(channels) {
4261
- if (this.wsConnection) {
4262
- 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);
4263
5699
  }
4264
5700
  else {
4265
5701
  console.warn('Cannot subscribe: WebSocket not connected.');
@@ -4269,7 +5705,7 @@ class HTTPManagerStateService extends ComponentStore {
4269
5705
  * Unsubscribe from a channel
4270
5706
  */
4271
5707
  unsubscribeFromChannel(channel) {
4272
- if (this.wsConnection) {
5708
+ if (WebSocketManagerService.isConnected()) {
4273
5709
  this.httpManagerService.unsubscribeFromChannel(channel);
4274
5710
  }
4275
5711
  else {
@@ -4292,7 +5728,7 @@ class HTTPManagerStateService extends ComponentStore {
4292
5728
  * Create a new channel on the server
4293
5729
  */
4294
5730
  createChannel(channel) {
4295
- if (this.wsConnection) {
5731
+ if (WebSocketManagerService.isConnected()) {
4296
5732
  this.httpManagerService.createChannel(channel);
4297
5733
  }
4298
5734
  else {
@@ -4303,7 +5739,7 @@ class HTTPManagerStateService extends ComponentStore {
4303
5739
  * Delete a channel from the server
4304
5740
  */
4305
5741
  deleteChannel(channel) {
4306
- if (this.wsConnection) {
5742
+ if (WebSocketManagerService.isConnected()) {
4307
5743
  this.httpManagerService.deleteChannel(channel);
4308
5744
  }
4309
5745
  else {
@@ -4314,7 +5750,7 @@ class HTTPManagerStateService extends ComponentStore {
4314
5750
  * Request list of all channels from server
4315
5751
  */
4316
5752
  getAllChannels() {
4317
- if (this.wsConnection) {
5753
+ if (WebSocketManagerService.isConnected()) {
4318
5754
  this.httpManagerService.getAllChannels();
4319
5755
  }
4320
5756
  else {
@@ -4325,7 +5761,7 @@ class HTTPManagerStateService extends ComponentStore {
4325
5761
  * Get users in a specific channel
4326
5762
  */
4327
5763
  getUsersInChannel(channel) {
4328
- if (this.wsConnection) {
5764
+ if (WebSocketManagerService.isConnected()) {
4329
5765
  this.httpManagerService.getUsersInChannel(channel);
4330
5766
  }
4331
5767
  else {
@@ -4339,7 +5775,7 @@ class HTTPManagerStateService extends ComponentStore {
4339
5775
  * @param channel - Base channel name (MES- prefix added automatically)
4340
5776
  */
4341
5777
  createNotificationChannel(channel) {
4342
- if (this.wsConnection) {
5778
+ if (WebSocketManagerService.isConnected()) {
4343
5779
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4344
5780
  this.httpManagerService.createNotificationChannel(prefixedChannel);
4345
5781
  console.log(`📢 Creating notification channel: ${prefixedChannel}`);
@@ -4352,7 +5788,7 @@ class HTTPManagerStateService extends ComponentStore {
4352
5788
  * Request list of all notification channels from server (in-memory)
4353
5789
  */
4354
5790
  getNotificationChannels() {
4355
- if (this.wsConnection) {
5791
+ if (WebSocketManagerService.isConnected()) {
4356
5792
  this.httpManagerService.getNotificationChannels();
4357
5793
  }
4358
5794
  else {
@@ -4364,7 +5800,7 @@ class HTTPManagerStateService extends ComponentStore {
4364
5800
  * Returns unique channels that have notifications posted today
4365
5801
  */
4366
5802
  getTodaysNotificationChannels() {
4367
- if (this.wsConnection) {
5803
+ if (WebSocketManagerService.isConnected()) {
4368
5804
  this.httpManagerService.getTodaysNotificationChannels();
4369
5805
  }
4370
5806
  else {
@@ -4376,7 +5812,7 @@ class HTTPManagerStateService extends ComponentStore {
4376
5812
  * @param channel - Base channel name (MES- prefix added automatically)
4377
5813
  */
4378
5814
  subscribeToNotificationChannel(channel, options, user) {
4379
- if (this.wsConnection) {
5815
+ if (WebSocketManagerService.isConnected()) {
4380
5816
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4381
5817
  this.httpManagerService.subscribeToNotificationChannel(prefixedChannel, options, user);
4382
5818
  console.log(`📢 Subscribing to notification channel: ${prefixedChannel}`);
@@ -4390,7 +5826,7 @@ class HTTPManagerStateService extends ComponentStore {
4390
5826
  * @param channel - Base channel name (MES- prefix added automatically)
4391
5827
  */
4392
5828
  unsubscribeFromNotificationChannel(channel) {
4393
- if (this.wsConnection) {
5829
+ if (WebSocketManagerService.isConnected()) {
4394
5830
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4395
5831
  this.httpManagerService.unsubscribeFromNotificationChannel(prefixedChannel);
4396
5832
  console.log(`📢 Unsubscribing from notification channel: ${prefixedChannel}`);
@@ -4404,7 +5840,7 @@ class HTTPManagerStateService extends ComponentStore {
4404
5840
  * @param channel - Base channel name (MES- prefix added automatically)
4405
5841
  */
4406
5842
  sendNotification(channel, content) {
4407
- if (this.wsConnection) {
5843
+ if (WebSocketManagerService.isConnected()) {
4408
5844
  const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
4409
5845
  this.httpManagerService.sendNotification(prefixedChannel, content);
4410
5846
  console.log(`📢 Sending notification to channel: ${prefixedChannel}`);
@@ -5823,17 +7259,25 @@ class StateServiceDemo extends HTTPManagerStateService {
5823
7259
  }
5824
7260
  /**
5825
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'])
5826
7267
  */
5827
- 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('/');
5828
7271
  this.setApiRequestOptions({
5829
7272
  server,
7273
+ path, // Set the path for HTTP requests
5830
7274
  retry: RetryOptions.adapt({
5831
7275
  times: 3,
5832
7276
  delay: 1,
5833
7277
  }),
5834
7278
  adapter: OIDCClient.adapt,
5835
7279
  ws: {
5836
- id: 'USERS123',
7280
+ id: channelId, // Use path-based channel ID instead of hardcoded 'USERS123'
5837
7281
  wsServer,
5838
7282
  jwtToken,
5839
7283
  user,
@@ -6158,7 +7602,7 @@ class StateDataRequestService extends HTTPManagerStateService {
6158
7602
  this.nextRetry$ = this.wsNextRetry$;
6159
7603
  this.path = ['ai', 'tests'];
6160
7604
  }
6161
- updateConnection(server, wsServer, jwtToken, user, path = [], wsChannel = '') {
7605
+ updateConnection(server, wsServer, jwtToken, user, path = []) {
6162
7606
  this.path = path;
6163
7607
  this.setApiRequestOptions({
6164
7608
  server,
@@ -6168,7 +7612,7 @@ class StateDataRequestService extends HTTPManagerStateService {
6168
7612
  }),
6169
7613
  adapter: OIDCClient.adapt,
6170
7614
  ws: {
6171
- id: wsChannel,
7615
+ id: this.path.join('/'),
6172
7616
  wsServer,
6173
7617
  jwtToken,
6174
7618
  user, // general info about user
@@ -6219,7 +7663,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6219
7663
  class WsDataControlComponent {
6220
7664
  constructor() {
6221
7665
  this.path = ['ai', 'tests'];
6222
- this.wsChannel = '';
6223
7666
  this.stateDataRequestService = inject(StateDataRequestService);
6224
7667
  this.user$ = this.stateDataRequestService.user$;
6225
7668
  this.users$ = this.stateDataRequestService.userList$;
@@ -6231,7 +7674,7 @@ class WsDataControlComponent {
6231
7674
  };
6232
7675
  }
6233
7676
  ngOnInit() {
6234
- 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);
6235
7678
  this.stateDataRequestService.getData();
6236
7679
  }
6237
7680
  onGetData() {
@@ -6256,11 +7699,11 @@ class WsDataControlComponent {
6256
7699
  this.stateDataRequestService.deleteData(lastRec);
6257
7700
  }
6258
7701
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsDataControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6259
- 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" }] }); }
6260
7703
  }
6261
7704
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsDataControlComponent, decorators: [{
6262
7705
  type: Component,
6263
- 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"] }]
6264
7707
  }], propDecorators: { server: [{
6265
7708
  type: Input
6266
7709
  }], wsServer: [{
@@ -6271,12 +7714,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6271
7714
  type: Input
6272
7715
  }], path: [{
6273
7716
  type: Input
6274
- }], wsChannel: [{
6275
- type: Input
6276
7717
  }] } });
6277
7718
 
6278
7719
  class WsMessagingComponent {
6279
7720
  constructor() {
7721
+ this.path = ['ai', 'tests']; // Default path for channel name
6280
7722
  this.destroy$ = new Subject();
6281
7723
  this.fb = inject(FormBuilder);
6282
7724
  this.messageService = inject(MessageServiceDemo);
@@ -6321,11 +7763,14 @@ class WsMessagingComponent {
6321
7763
  return this.messages.get('content');
6322
7764
  }
6323
7765
  ngOnInit() {
6324
- 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);
6325
7767
  // Only trigger once when connection becomes true
6326
7768
  this.connectionStatus$.pipe(filter$1(status => status === true), take$1(1), takeUntil$1(this.destroy$)).subscribe(() => {
6327
- // Fetch existing channels when connected (user will create channels in demo)
6328
- 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
6329
7774
  });
6330
7775
  // Subscribe to latest messages and show toast notification
6331
7776
  this.latestCommunicationMessages$.pipe(filter$1(message => !!message), takeUntil$1(this.destroy$)).subscribe((message) => {
@@ -6429,11 +7874,11 @@ class WsMessagingComponent {
6429
7874
  this.content.reset();
6430
7875
  }
6431
7876
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsMessagingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6432
- 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" }] }); }
6433
7878
  }
6434
7879
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsMessagingComponent, decorators: [{
6435
7880
  type: Component,
6436
- 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"] }]
6437
7882
  }], propDecorators: { server: [{
6438
7883
  type: Input
6439
7884
  }], wsServer: [{
@@ -6442,6 +7887,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6442
7887
  type: Input
6443
7888
  }], user: [{
6444
7889
  type: Input
7890
+ }], path: [{
7891
+ type: Input
6445
7892
  }] } });
6446
7893
 
6447
7894
  class WsNotificationsComponent {
@@ -6668,7 +8115,6 @@ class RequestManagerWsDemoComponent {
6668
8115
  this.stateService = inject(StateServiceDemo);
6669
8116
  this.fb = inject(FormBuilder);
6670
8117
  this.path = ['ai', 'tests'];
6671
- this.wsChannel = '';
6672
8118
  this.user$ = this.stateService.user$;
6673
8119
  this.attempts$ = this.stateService.wsRetryAttempts$;
6674
8120
  this.nextRetry$ = this.stateService.wsNextRetry$;
@@ -6677,14 +8123,14 @@ class RequestManagerWsDemoComponent {
6677
8123
  this.isPending$ = this.stateService.isPending$;
6678
8124
  }
6679
8125
  ngOnInit() {
6680
- 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);
6681
8127
  }
6682
8128
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerWsDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6683
- 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" }] }); }
6684
8130
  }
6685
8131
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerWsDemoComponent, decorators: [{
6686
8132
  type: Component,
6687
- 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" }]
6688
8134
  }], propDecorators: { server: [{
6689
8135
  type: Input
6690
8136
  }], wsServer: [{
@@ -6695,8 +8141,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6695
8141
  type: Input
6696
8142
  }], path: [{
6697
8143
  type: Input
6698
- }], wsChannel: [{
6699
- type: Input
6700
8144
  }] } });
6701
8145
 
6702
8146
  class Settings {
@@ -6917,7 +8361,6 @@ class HttpRequestServicesDemoComponent {
6917
8361
  this.jwtToken = '';
6918
8362
  this.server = 'http:';
6919
8363
  this.path = ['ai', 'tests'];
6920
- this.wsChannel = '';
6921
8364
  this.requestTypes = [
6922
8365
  { name: "Http Service", value: 'http_service' },
6923
8366
  // { name: "Http Signals Service", value: 'http_signals_service', new: true },
@@ -6938,11 +8381,11 @@ class HttpRequestServicesDemoComponent {
6938
8381
  this.selectedService = this.requestTypes[type].value;
6939
8382
  }
6940
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 }); }
6941
- 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" }] }); }
6942
8385
  }
6943
8386
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HttpRequestServicesDemoComponent, decorators: [{
6944
8387
  type: Component,
6945
- 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"] }]
6946
8389
  }], ctorParameters: () => [{ type: ConfigOptions, decorators: [{
6947
8390
  type: Inject,
6948
8391
  args: [CONFIG_SETTINGS_TOKEN]
@@ -6956,8 +8399,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6956
8399
  type: Input
6957
8400
  }], path: [{
6958
8401
  type: Input
6959
- }], wsChannel: [{
6960
- type: Input
6961
8402
  }], adapter: [{
6962
8403
  type: Input
6963
8404
  }], mapper: [{
@@ -7629,5 +9070,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
7629
9070
  * Generated bundle index. Do not edit.
7630
9071
  */
7631
9072
 
7632
- 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 };
7633
9074
  //# sourceMappingURL=http-request-manager.mjs.map