http-request-manager 18.7.31 → 18.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -383,17 +383,25 @@ class SymmetricalEncryptionService {
|
|
|
383
383
|
let _key = CryptoJS.enc.Utf8.parse(key);
|
|
384
384
|
let _iv = CryptoJS.enc.Utf8.parse(key);
|
|
385
385
|
try {
|
|
386
|
-
|
|
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
|
-
})
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
1268
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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(
|
|
1353
|
+
console.warn(`Cannot subscribe to channel "${channelName}": WebSocket not yet open.`);
|
|
1330
1354
|
}
|
|
1331
1355
|
}
|
|
1332
1356
|
subscribeToChannels(channelNames) {
|
|
@@ -1388,6 +1412,38 @@ class WebsocketService {
|
|
|
1388
1412
|
console.error('WebSocket is not open. Cannot send channel message.');
|
|
1389
1413
|
}
|
|
1390
1414
|
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Send a message to multiple channels at once (batch)
|
|
1417
|
+
* Sends the new batch format and falls back to legacy per-channel messages for compatibility
|
|
1418
|
+
*/
|
|
1419
|
+
sendChannelMessageToChannels(channels, content) {
|
|
1420
|
+
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
1421
|
+
try {
|
|
1422
|
+
// New batch format
|
|
1423
|
+
this.socket.send(JSON.stringify({
|
|
1424
|
+
type: 'channelMessage',
|
|
1425
|
+
channels: channels,
|
|
1426
|
+
data: content
|
|
1427
|
+
}));
|
|
1428
|
+
console.log(`💬 Send channel message to channels [${channels.join(', ')}]:`, content);
|
|
1429
|
+
}
|
|
1430
|
+
catch (error) {
|
|
1431
|
+
console.error('❌ Failed to send channelMessage batch:', error);
|
|
1432
|
+
}
|
|
1433
|
+
// Legacy fallback - send individual messages to each channel
|
|
1434
|
+
try {
|
|
1435
|
+
channels.forEach(channel => {
|
|
1436
|
+
this.socket?.send(JSON.stringify({ type: 'message', subscribedChannel: channel, content }));
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
catch (err) {
|
|
1440
|
+
console.warn('⚠️ Legacy fallback failed sending individual messages', err);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
else {
|
|
1444
|
+
console.error('WebSocket is not open. Cannot send message to channels.');
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1391
1447
|
sendMessageToUser(user, content) {
|
|
1392
1448
|
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
1393
1449
|
this.socket.send(JSON.stringify({ type: 'userMessage', subscribedChannel: user, content }));
|
|
@@ -1533,6 +1589,586 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
1533
1589
|
}]
|
|
1534
1590
|
}] });
|
|
1535
1591
|
|
|
1592
|
+
/**
|
|
1593
|
+
* WebSocketManagerService - Singleton WebSocket connection manager
|
|
1594
|
+
*
|
|
1595
|
+
* This service ensures only ONE WebSocket connection exists across ALL instances
|
|
1596
|
+
* of HTTPManagerStateService. It uses static properties to track connection state
|
|
1597
|
+
* globally, preventing duplicate connections when multiple state services are created.
|
|
1598
|
+
*
|
|
1599
|
+
* Usage:
|
|
1600
|
+
* - Inject into HTTPManagerService (or directly into state services)
|
|
1601
|
+
* - Call connect() with WSOptions - only the first call establishes connection
|
|
1602
|
+
* - Subsequent calls detect existing connection and skip reconnection
|
|
1603
|
+
* - All instances share the same connectionStatus$ and messages$ observables
|
|
1604
|
+
*/
|
|
1605
|
+
class WebSocketManagerService {
|
|
1606
|
+
constructor() {
|
|
1607
|
+
this.messages$ = WebSocketManagerService.messages.asObservable();
|
|
1608
|
+
this.connectionStatus$ = WebSocketManagerService.connectionStatus.asObservable();
|
|
1609
|
+
this.subscribedChannels$ = WebSocketManagerService.subscribedChannels.asObservable();
|
|
1610
|
+
this.onReconnect$ = WebSocketManagerService.onReconnect.asObservable();
|
|
1611
|
+
}
|
|
1612
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1613
|
+
// STATIC SINGLETON STATE (Shared across ALL instances)
|
|
1614
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1615
|
+
static { this.socket = null; }
|
|
1616
|
+
static { this.isConnecting = false; }
|
|
1617
|
+
static { this.connectionInitialized = false; }
|
|
1618
|
+
// Store last options for reconnection
|
|
1619
|
+
static { this.lastOptions = null; }
|
|
1620
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1621
|
+
// INSTANCE OBSERVABLES (Shared across ALL instances via static BehaviorSubjects)
|
|
1622
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1623
|
+
static { this.messages = new BehaviorSubject(null); }
|
|
1624
|
+
static { this.connectionStatus = new BehaviorSubject(false); }
|
|
1625
|
+
static { this.isSubscribed = false; }
|
|
1626
|
+
// Track currently subscribed channels
|
|
1627
|
+
static { this.subscribedChannels = new BehaviorSubject(new Set()); }
|
|
1628
|
+
// Public method to get current subscribed channels
|
|
1629
|
+
static getSubscribedChannels() {
|
|
1630
|
+
return WebSocketManagerService.subscribedChannels.getValue();
|
|
1631
|
+
}
|
|
1632
|
+
// Public method to add a channel to subscribed channels
|
|
1633
|
+
static addSubscribedChannel(channelName) {
|
|
1634
|
+
const current = new Set(WebSocketManagerService.subscribedChannels.value);
|
|
1635
|
+
current.add(channelName);
|
|
1636
|
+
WebSocketManagerService.subscribedChannels.next(current);
|
|
1637
|
+
console.log(`📝 Added ${channelName} to subscribedChannels (now has ${current.size} channels)`);
|
|
1638
|
+
}
|
|
1639
|
+
// Reconnect event (emitted when connection is established/re-established)
|
|
1640
|
+
static { this.onReconnect = new BehaviorSubject(undefined); }
|
|
1641
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1642
|
+
// SESSION MANAGEMENT
|
|
1643
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1644
|
+
getSessionId() {
|
|
1645
|
+
return sessionStorage.getItem('WSID') ?? (() => {
|
|
1646
|
+
const uuid = UUID_STR();
|
|
1647
|
+
sessionStorage.setItem('WSID', uuid);
|
|
1648
|
+
return uuid;
|
|
1649
|
+
})();
|
|
1650
|
+
}
|
|
1651
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1652
|
+
// CONNECTION MANAGEMENT
|
|
1653
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1654
|
+
/**
|
|
1655
|
+
* Connect to WebSocket server
|
|
1656
|
+
*
|
|
1657
|
+
* IMPORTANT: Only the first call across ALL instances will establish a connection.
|
|
1658
|
+
* Subsequent calls will detect the existing connection and return immediately.
|
|
1659
|
+
*
|
|
1660
|
+
* @param options - WebSocket configuration options
|
|
1661
|
+
* @param jwtToken - Optional JWT token for authentication
|
|
1662
|
+
*/
|
|
1663
|
+
connect(options, jwtToken) {
|
|
1664
|
+
// Check if connection already exists and is open
|
|
1665
|
+
if (WebSocketManagerService.socket) {
|
|
1666
|
+
if (WebSocketManagerService.socket.readyState === WebSocket.OPEN) {
|
|
1667
|
+
console.log('✓ WebSocket connection already OPEN (singleton).');
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
if (WebSocketManagerService.socket.readyState === WebSocket.CONNECTING) {
|
|
1671
|
+
console.log('⏳ WebSocket is already CONNECTING (singleton). Waiting for handshake.');
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
// Clean up stale connection
|
|
1675
|
+
if (WebSocketManagerService.socket.readyState === WebSocket.CLOSING ||
|
|
1676
|
+
WebSocketManagerService.socket.readyState === WebSocket.CLOSED) {
|
|
1677
|
+
console.log(`🧹 Cleaning up stale socket (State: ${WebSocketManagerService.socket.readyState}).`);
|
|
1678
|
+
WebSocketManagerService.socket.close();
|
|
1679
|
+
WebSocketManagerService.socket = null;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
// Prevent duplicate connection attempts
|
|
1683
|
+
if (WebSocketManagerService.isConnecting) {
|
|
1684
|
+
console.log('⏳ Connection already in progress...');
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
// Mark as connecting
|
|
1688
|
+
WebSocketManagerService.isConnecting = true;
|
|
1689
|
+
WebSocketManagerService.isSubscribed = false;
|
|
1690
|
+
WebSocketManagerService.subscribedChannels.next(new Set());
|
|
1691
|
+
const sessionId = this.getSessionId();
|
|
1692
|
+
const URL = (jwtToken) ? `${options.wsServer}?token=${jwtToken}&sessionId=${sessionId}` : `${options.wsServer}?sessionId=${sessionId}`;
|
|
1693
|
+
console.log(`🔌 Initiating WebSocket connection to: ${options.wsServer}`);
|
|
1694
|
+
// Create new WebSocket instance (static)
|
|
1695
|
+
WebSocketManagerService.socket = new WebSocket(URL);
|
|
1696
|
+
WebSocketManagerService.socket.onopen = () => {
|
|
1697
|
+
console.log(`📡 Connected to WebSocket`);
|
|
1698
|
+
// Force clear subscribedChannels on new connection - server lost our subscriptions
|
|
1699
|
+
console.log('🧹 Clearing subscribedChannels on connect (was:', WebSocketManagerService.subscribedChannels.value.size, 'channels)');
|
|
1700
|
+
WebSocketManagerService.subscribedChannels.next(new Set());
|
|
1701
|
+
WebSocketManagerService.connectionStatus.next(true);
|
|
1702
|
+
WebSocketManagerService.isConnecting = false;
|
|
1703
|
+
WebSocketManagerService.connectionInitialized = true;
|
|
1704
|
+
WebSocketManagerService.lastOptions = options;
|
|
1705
|
+
// Emit reconnect event - MessageTrackerService will handle subscriptions with lastSeenId
|
|
1706
|
+
console.log(`🔄 Emitting reconnect event for MessageTrackerService`);
|
|
1707
|
+
WebSocketManagerService.onReconnect.next();
|
|
1708
|
+
};
|
|
1709
|
+
WebSocketManagerService.socket.onmessage = (event) => {
|
|
1710
|
+
try {
|
|
1711
|
+
const data = JSON.parse(event.data);
|
|
1712
|
+
if (data.error && data.error === 'JWT_INVALID') {
|
|
1713
|
+
console.error('JWT validation failed. Authentication error!');
|
|
1714
|
+
WebSocketManagerService.messages.next(data);
|
|
1715
|
+
WebSocketManagerService.connectionStatus.next(false);
|
|
1716
|
+
WebSocketManagerService.socket?.close();
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
WebSocketManagerService.messages.next(data);
|
|
1720
|
+
}
|
|
1721
|
+
catch (error) {
|
|
1722
|
+
console.error('Error parsing WebSocket message:', event.data);
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
WebSocketManagerService.socket.onclose = () => {
|
|
1726
|
+
console.log('🔴 WebSocket connection closed');
|
|
1727
|
+
console.log('🧹 Clearing subscribedChannels (was:', WebSocketManagerService.subscribedChannels.value.size, 'channels)');
|
|
1728
|
+
WebSocketManagerService.connectionStatus.next(false);
|
|
1729
|
+
WebSocketManagerService.isConnecting = false;
|
|
1730
|
+
WebSocketManagerService.socket = null;
|
|
1731
|
+
// Clear subscribed channels - server lost our subscriptions
|
|
1732
|
+
WebSocketManagerService.subscribedChannels.next(new Set());
|
|
1733
|
+
console.log('✅ subscribedChannels cleared');
|
|
1734
|
+
};
|
|
1735
|
+
WebSocketManagerService.socket.onerror = (error) => {
|
|
1736
|
+
console.error('❌ WebSocket error:', error);
|
|
1737
|
+
WebSocketManagerService.connectionStatus.next(false);
|
|
1738
|
+
WebSocketManagerService.isConnecting = false;
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Disconnect from WebSocket server
|
|
1743
|
+
*/
|
|
1744
|
+
disconnect() {
|
|
1745
|
+
if (WebSocketManagerService.socket) {
|
|
1746
|
+
console.log('🔌 Disconnecting WebSocket...');
|
|
1747
|
+
WebSocketManagerService.socket.close();
|
|
1748
|
+
WebSocketManagerService.socket = null;
|
|
1749
|
+
WebSocketManagerService.connectionStatus.next(false);
|
|
1750
|
+
WebSocketManagerService.subscribedChannels.next(new Set());
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1754
|
+
// SUBSCRIPTION MANAGEMENT
|
|
1755
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1756
|
+
sendSubscribe(channelName, user) {
|
|
1757
|
+
const alreadySubscribed = WebSocketManagerService.subscribedChannels.value.has(channelName);
|
|
1758
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN && !alreadySubscribed) {
|
|
1759
|
+
const message = {
|
|
1760
|
+
type: 'subscribe',
|
|
1761
|
+
subscribedChannel: channelName,
|
|
1762
|
+
content: {
|
|
1763
|
+
id: this.getSessionId(),
|
|
1764
|
+
...user
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
WebSocketManagerService.socket.send(JSON.stringify(message));
|
|
1768
|
+
// Track this channel as subscribed
|
|
1769
|
+
const current = new Set(WebSocketManagerService.subscribedChannels.value);
|
|
1770
|
+
current.add(channelName);
|
|
1771
|
+
WebSocketManagerService.subscribedChannels.next(current);
|
|
1772
|
+
WebSocketManagerService.isSubscribed = true;
|
|
1773
|
+
console.log(`[CLIENT] Sent initial subscription to: ${channelName}`);
|
|
1774
|
+
}
|
|
1775
|
+
else {
|
|
1776
|
+
console.warn(`[CLIENT] Subscription prevented. Open: ${WebSocketManagerService.socket?.readyState === WebSocket.OPEN}, Already subscribed to ${channelName}: ${alreadySubscribed}`);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Send subscribe with lastSeenId for message sync support
|
|
1781
|
+
* @param channelName - Channel name to subscribe to
|
|
1782
|
+
* @param user - User data
|
|
1783
|
+
* @param lastSeenId - Last message ID seen (for replay)
|
|
1784
|
+
*/
|
|
1785
|
+
sendSubscribeWithLastSeen(channelName, user, lastSeenId) {
|
|
1786
|
+
const alreadySubscribed = WebSocketManagerService.subscribedChannels.value.has(channelName);
|
|
1787
|
+
const isOpen = WebSocketManagerService.socket?.readyState === WebSocket.OPEN;
|
|
1788
|
+
console.log(`🔍 Subscription check for "${channelName}": isOpen=${isOpen}, alreadySubscribed=${alreadySubscribed}`);
|
|
1789
|
+
if (isOpen && !alreadySubscribed && WebSocketManagerService.socket) {
|
|
1790
|
+
const message = {
|
|
1791
|
+
type: 'subscribe',
|
|
1792
|
+
subscribedChannel: channelName,
|
|
1793
|
+
lastSeenId, // NEW: For message replay
|
|
1794
|
+
content: {
|
|
1795
|
+
id: this.getSessionId(),
|
|
1796
|
+
...user
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
WebSocketManagerService.socket.send(JSON.stringify(message));
|
|
1800
|
+
// DON'T track as subscribed yet - wait for server confirmation ('subscribed' message)
|
|
1801
|
+
// This prevents race conditions where we think we're subscribed but server doesn't
|
|
1802
|
+
WebSocketManagerService.isSubscribed = true;
|
|
1803
|
+
console.log(`[CLIENT] Sent subscription with lastSeenId=${lastSeenId} to: ${channelName}`);
|
|
1804
|
+
}
|
|
1805
|
+
else {
|
|
1806
|
+
console.warn(`[CLIENT] Subscription prevented. Open: ${isOpen}, Already subscribed to ${channelName}: ${alreadySubscribed}`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Subscribe to a channel
|
|
1811
|
+
* @param channelName - Channel name to subscribe to
|
|
1812
|
+
*/
|
|
1813
|
+
subscribeToChannel(channelName) {
|
|
1814
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
1815
|
+
try {
|
|
1816
|
+
const message = {
|
|
1817
|
+
type: 'subscribe',
|
|
1818
|
+
subscribedChannel: channelName,
|
|
1819
|
+
content: {}
|
|
1820
|
+
};
|
|
1821
|
+
WebSocketManagerService.socket.send(JSON.stringify(message));
|
|
1822
|
+
// Track locally - create new Set to trigger change detection
|
|
1823
|
+
const current = new Set(WebSocketManagerService.subscribedChannels.value);
|
|
1824
|
+
current.add(channelName);
|
|
1825
|
+
WebSocketManagerService.subscribedChannels.next(current);
|
|
1826
|
+
console.log(`📝 Subscribed to channel: ${channelName}`);
|
|
1827
|
+
}
|
|
1828
|
+
catch (error) {
|
|
1829
|
+
console.error(`❌ Failed to subscribe to channel "${channelName}":`, error);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
else {
|
|
1833
|
+
console.warn(`Cannot subscribe to channel "${channelName}": WebSocket not yet open.`);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Subscribe to multiple channels
|
|
1838
|
+
* @param channelNames - Array of channel names to subscribe to
|
|
1839
|
+
*/
|
|
1840
|
+
subscribeToChannels(channelNames) {
|
|
1841
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
1842
|
+
channelNames.forEach(channel => this.subscribeToChannel(channel));
|
|
1843
|
+
}
|
|
1844
|
+
else {
|
|
1845
|
+
console.warn('Cannot subscribe: WebSocket not yet open.');
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Unsubscribe from a channel
|
|
1850
|
+
* @param channel - Channel name to unsubscribe from
|
|
1851
|
+
*/
|
|
1852
|
+
unsubscribeFromChannel(channel) {
|
|
1853
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
1854
|
+
WebSocketManagerService.socket.send(JSON.stringify({ type: 'unsubscribe', subscribedChannel: channel }));
|
|
1855
|
+
// Remove from local tracking - create new Set to trigger change detection
|
|
1856
|
+
const current = new Set(WebSocketManagerService.subscribedChannels.value);
|
|
1857
|
+
current.delete(channel);
|
|
1858
|
+
WebSocketManagerService.subscribedChannels.next(current);
|
|
1859
|
+
console.log(`💬 Unsubscribed from channel: ${channel}`);
|
|
1860
|
+
}
|
|
1861
|
+
else {
|
|
1862
|
+
console.error('WebSocket is not open. Cannot unsubscribe from channel.');
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Get currently subscribed channels
|
|
1867
|
+
* @returns Set of subscribed channel names
|
|
1868
|
+
*/
|
|
1869
|
+
getSubscribedChannels() {
|
|
1870
|
+
return WebSocketManagerService.subscribedChannels.value;
|
|
1871
|
+
}
|
|
1872
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1873
|
+
// MESSAGE SENDING
|
|
1874
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1875
|
+
/**
|
|
1876
|
+
* Generic send method for message tracking (acks, gap requests, etc.)
|
|
1877
|
+
* @param message - Message object to send
|
|
1878
|
+
*/
|
|
1879
|
+
send(message) {
|
|
1880
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
1881
|
+
WebSocketManagerService.socket.send(JSON.stringify(message));
|
|
1882
|
+
}
|
|
1883
|
+
else {
|
|
1884
|
+
console.warn('⚠️ Cannot send message: WebSocket not open', message);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Send broadcast message
|
|
1889
|
+
* @param content - Message content to broadcast
|
|
1890
|
+
*/
|
|
1891
|
+
sendBroadcast(content) {
|
|
1892
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
1893
|
+
WebSocketManagerService.socket.send(JSON.stringify({ type: 'broadcast', content }));
|
|
1894
|
+
console.log(`📢 Send broadcast: ${content}`);
|
|
1895
|
+
}
|
|
1896
|
+
else {
|
|
1897
|
+
console.error('WebSocket is not open. Cannot send broadcast.');
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Send message in channel (state manager message)
|
|
1902
|
+
* @param channel - Channel name
|
|
1903
|
+
* @param content - Message content
|
|
1904
|
+
*/
|
|
1905
|
+
sendMessageInChannel(channel, content) {
|
|
1906
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
1907
|
+
const message = {
|
|
1908
|
+
type: 'stateMangerMessage',
|
|
1909
|
+
subscribedChannel: channel,
|
|
1910
|
+
content
|
|
1911
|
+
};
|
|
1912
|
+
// DEBUG: Log the exact message being sent
|
|
1913
|
+
console.log('🔍 [DEBUG] sendMessageInChannel:', {
|
|
1914
|
+
channel,
|
|
1915
|
+
channelType: typeof channel,
|
|
1916
|
+
channelEmpty: channel === '' || channel === null || channel === undefined,
|
|
1917
|
+
content,
|
|
1918
|
+
fullMessage: message,
|
|
1919
|
+
jsonString: JSON.stringify(message)
|
|
1920
|
+
});
|
|
1921
|
+
WebSocketManagerService.socket.send(JSON.stringify(message));
|
|
1922
|
+
console.log(`💬 Send message:`, content);
|
|
1923
|
+
}
|
|
1924
|
+
else {
|
|
1925
|
+
console.error('WebSocket is not open. Cannot send message.');
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Send channel message (broadcasts to all subscribers in the channel)
|
|
1930
|
+
* @param channel - Channel name
|
|
1931
|
+
* @param content - Message content
|
|
1932
|
+
*/
|
|
1933
|
+
sendChannelMessage(channel, content) {
|
|
1934
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
1935
|
+
WebSocketManagerService.socket.send(JSON.stringify({
|
|
1936
|
+
type: 'message',
|
|
1937
|
+
subscribedChannel: channel,
|
|
1938
|
+
content
|
|
1939
|
+
}));
|
|
1940
|
+
console.log(`💬 Send channel message to [${channel}]:`, content);
|
|
1941
|
+
}
|
|
1942
|
+
else {
|
|
1943
|
+
console.error('WebSocket is not open. Cannot send channel message.');
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Send message to multiple channels at once (batch)
|
|
1948
|
+
* @param channels - Array of channel names
|
|
1949
|
+
* @param content - Message content
|
|
1950
|
+
*/
|
|
1951
|
+
sendChannelMessageToChannels(channels, content) {
|
|
1952
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
1953
|
+
try {
|
|
1954
|
+
// New batch format
|
|
1955
|
+
WebSocketManagerService.socket.send(JSON.stringify({
|
|
1956
|
+
type: 'channelMessage',
|
|
1957
|
+
channels: channels,
|
|
1958
|
+
data: content
|
|
1959
|
+
}));
|
|
1960
|
+
console.log(`💬 Send channel message to channels [${channels.join(', ')}]:`, content);
|
|
1961
|
+
}
|
|
1962
|
+
catch (error) {
|
|
1963
|
+
console.error('❌ Failed to send channelMessage batch:', error);
|
|
1964
|
+
}
|
|
1965
|
+
// Legacy fallback - send individual messages to each channel
|
|
1966
|
+
try {
|
|
1967
|
+
channels.forEach(channel => {
|
|
1968
|
+
WebSocketManagerService.socket?.send(JSON.stringify({
|
|
1969
|
+
type: 'message',
|
|
1970
|
+
subscribedChannel: channel,
|
|
1971
|
+
content
|
|
1972
|
+
}));
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
catch (err) {
|
|
1976
|
+
console.warn('⚠️ Legacy fallback failed sending individual messages', err);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
else {
|
|
1980
|
+
console.error('WebSocket is not open. Cannot send message to channels.');
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Send message to specific user
|
|
1985
|
+
* @param user - User identifier
|
|
1986
|
+
* @param content - Message content
|
|
1987
|
+
*/
|
|
1988
|
+
sendMessageToUser(user, content) {
|
|
1989
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
1990
|
+
WebSocketManagerService.socket.send(JSON.stringify({
|
|
1991
|
+
type: 'userMessage',
|
|
1992
|
+
subscribedChannel: user,
|
|
1993
|
+
content
|
|
1994
|
+
}));
|
|
1995
|
+
console.log(`💬 Send message:`, content);
|
|
1996
|
+
}
|
|
1997
|
+
else {
|
|
1998
|
+
console.error('WebSocket is not open. Cannot send message.');
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2002
|
+
// CHANNEL MANAGEMENT
|
|
2003
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2004
|
+
/**
|
|
2005
|
+
* Request list of all channels from server
|
|
2006
|
+
*/
|
|
2007
|
+
getAllChannels() {
|
|
2008
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
2009
|
+
WebSocketManagerService.socket.send(JSON.stringify({ type: 'getChannels' }));
|
|
2010
|
+
console.log('🗂️ List of all channels');
|
|
2011
|
+
}
|
|
2012
|
+
else {
|
|
2013
|
+
console.error('WebSocket is not open. Cannot request channels.');
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Create a new channel on server
|
|
2018
|
+
* @param channel - Channel name to create
|
|
2019
|
+
*/
|
|
2020
|
+
createChannel(channel) {
|
|
2021
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
2022
|
+
WebSocketManagerService.socket.send(JSON.stringify({
|
|
2023
|
+
type: 'createChannel',
|
|
2024
|
+
content: channel
|
|
2025
|
+
}));
|
|
2026
|
+
console.log('🗂️ Created channel:', channel);
|
|
2027
|
+
}
|
|
2028
|
+
else {
|
|
2029
|
+
console.error('WebSocket is not open. Cannot request channels.');
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Delete a channel from server
|
|
2034
|
+
* @param channel - Channel name to delete
|
|
2035
|
+
*/
|
|
2036
|
+
deleteChannel(channel) {
|
|
2037
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
2038
|
+
WebSocketManagerService.socket.send(JSON.stringify({
|
|
2039
|
+
type: 'deleteChannel',
|
|
2040
|
+
content: channel
|
|
2041
|
+
}));
|
|
2042
|
+
console.log('🗂️ Delete channel:', channel);
|
|
2043
|
+
}
|
|
2044
|
+
else {
|
|
2045
|
+
console.error('WebSocket is not open. Cannot request channels.');
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Get users in a specific channel
|
|
2050
|
+
* @param channel - Channel name
|
|
2051
|
+
*/
|
|
2052
|
+
getUsersInChannel(channel) {
|
|
2053
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
2054
|
+
WebSocketManagerService.socket.send(JSON.stringify({
|
|
2055
|
+
type: 'getUsers',
|
|
2056
|
+
subscribedChannel: channel
|
|
2057
|
+
}));
|
|
2058
|
+
console.log(`👥 List all users in channel: ${channel}`);
|
|
2059
|
+
}
|
|
2060
|
+
else {
|
|
2061
|
+
console.error('WebSocket is not open. Cannot request users.');
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2065
|
+
// NOTIFICATION CHANNELS (MES- prefix)
|
|
2066
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2067
|
+
/**
|
|
2068
|
+
* Create a notification channel
|
|
2069
|
+
* @param channel - Channel name (should include MES- prefix)
|
|
2070
|
+
*/
|
|
2071
|
+
createNotificationChannel(channel) {
|
|
2072
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
2073
|
+
WebSocketManagerService.socket.send(JSON.stringify({
|
|
2074
|
+
type: 'createNotificationChannel',
|
|
2075
|
+
content: channel
|
|
2076
|
+
}));
|
|
2077
|
+
console.log('📢 Created notification channel:', channel);
|
|
2078
|
+
}
|
|
2079
|
+
else {
|
|
2080
|
+
console.error('WebSocket is not open. Cannot create notification channel.');
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* Get all notification channels (in-memory)
|
|
2085
|
+
*/
|
|
2086
|
+
getNotificationChannels() {
|
|
2087
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
2088
|
+
WebSocketManagerService.socket.send(JSON.stringify({ type: 'getNotificationChannels' }));
|
|
2089
|
+
console.log('📢 Requested notification channels list');
|
|
2090
|
+
}
|
|
2091
|
+
else {
|
|
2092
|
+
console.error('WebSocket is not open. Cannot request notification channels.');
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Get today's notification channels from database
|
|
2097
|
+
*/
|
|
2098
|
+
getTodaysNotificationChannels() {
|
|
2099
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
2100
|
+
WebSocketManagerService.socket.send(JSON.stringify({ type: 'getTodaysNotificationChannels' }));
|
|
2101
|
+
console.log('📢 Requested today\'s notification channels from DB');
|
|
2102
|
+
}
|
|
2103
|
+
else {
|
|
2104
|
+
console.error('WebSocket is not open. Cannot request today\'s notification channels.');
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Subscribe to a notification channel with optional date filters
|
|
2109
|
+
* @param channel - Channel name
|
|
2110
|
+
* @param options - Optional start/end epoch filters
|
|
2111
|
+
* @param user - User information
|
|
2112
|
+
*/
|
|
2113
|
+
subscribeToNotificationChannel(channel, options, user) {
|
|
2114
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
2115
|
+
WebSocketManagerService.socket.send(JSON.stringify({
|
|
2116
|
+
type: 'subscribeNotifications',
|
|
2117
|
+
subscribedChannel: channel,
|
|
2118
|
+
content: {
|
|
2119
|
+
...options,
|
|
2120
|
+
user: user
|
|
2121
|
+
}
|
|
2122
|
+
}));
|
|
2123
|
+
console.log(`📢 Subscribed to notification channel: ${channel}`);
|
|
2124
|
+
}
|
|
2125
|
+
else {
|
|
2126
|
+
console.error('WebSocket is not open. Cannot subscribe to notification channel.');
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Unsubscribe from a notification channel
|
|
2131
|
+
* @param channel - Channel name
|
|
2132
|
+
*/
|
|
2133
|
+
unsubscribeFromNotificationChannel(channel) {
|
|
2134
|
+
if (WebSocketManagerService.socket?.readyState === WebSocket.OPEN) {
|
|
2135
|
+
WebSocketManagerService.socket.send(JSON.stringify({
|
|
2136
|
+
type: 'unsubscribeNotifications',
|
|
2137
|
+
subscribedChannel: channel
|
|
2138
|
+
}));
|
|
2139
|
+
console.log(`📢 Unsubscribed from notification channel: ${channel}`);
|
|
2140
|
+
}
|
|
2141
|
+
else {
|
|
2142
|
+
console.error('WebSocket is not open. Cannot unsubscribe from notification channel.');
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2146
|
+
// CONNECTION STATUS (Static getter for direct access)
|
|
2147
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2148
|
+
/**
|
|
2149
|
+
* Check if WebSocket connection is currently open
|
|
2150
|
+
* @returns true if connected, false otherwise
|
|
2151
|
+
*/
|
|
2152
|
+
static isConnected() {
|
|
2153
|
+
return WebSocketManagerService.socket?.readyState === WebSocket.OPEN;
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Check if connection has been initialized (attempted at least once)
|
|
2157
|
+
* @returns true if connection was initialized, false otherwise
|
|
2158
|
+
*/
|
|
2159
|
+
static isInitialized() {
|
|
2160
|
+
return WebSocketManagerService.connectionInitialized;
|
|
2161
|
+
}
|
|
2162
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WebSocketManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2163
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WebSocketManagerService, providedIn: 'root' }); }
|
|
2164
|
+
}
|
|
2165
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WebSocketManagerService, decorators: [{
|
|
2166
|
+
type: Injectable,
|
|
2167
|
+
args: [{
|
|
2168
|
+
providedIn: 'root',
|
|
2169
|
+
}]
|
|
2170
|
+
}] });
|
|
2171
|
+
|
|
1536
2172
|
class RequestService extends WebsocketService {
|
|
1537
2173
|
constructor() {
|
|
1538
2174
|
super(...arguments);
|
|
@@ -1723,72 +2359,491 @@ class RequestService extends WebsocketService {
|
|
|
1723
2359
|
}
|
|
1724
2360
|
return fileType;
|
|
1725
2361
|
}
|
|
1726
|
-
combineHeaders(headers, isStreaming) {
|
|
1727
|
-
return (isStreaming) ?
|
|
1728
|
-
{
|
|
1729
|
-
...headers,
|
|
1730
|
-
observe: 'events',
|
|
1731
|
-
responseType: 'text',
|
|
1732
|
-
reportProgress: true,
|
|
1733
|
-
Accept: 'text/event-stream'
|
|
2362
|
+
combineHeaders(headers, isStreaming) {
|
|
2363
|
+
return (isStreaming) ?
|
|
2364
|
+
{
|
|
2365
|
+
...headers,
|
|
2366
|
+
observe: 'events',
|
|
2367
|
+
responseType: 'text',
|
|
2368
|
+
reportProgress: true,
|
|
2369
|
+
Accept: 'text/event-stream'
|
|
2370
|
+
}
|
|
2371
|
+
: headers;
|
|
2372
|
+
}
|
|
2373
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2374
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, providedIn: 'root' }); }
|
|
2375
|
+
}
|
|
2376
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, decorators: [{
|
|
2377
|
+
type: Injectable,
|
|
2378
|
+
args: [{
|
|
2379
|
+
providedIn: 'root'
|
|
2380
|
+
}]
|
|
2381
|
+
}] });
|
|
2382
|
+
|
|
2383
|
+
function countdown(duration) {
|
|
2384
|
+
return defer(() => {
|
|
2385
|
+
const currentCount = { current: duration };
|
|
2386
|
+
return interval(1000).pipe(map(() => --currentCount.current), takeWhile(count => count >= 0));
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
2391
|
+
function delayedRetry(delayMs, maxRetry = DEFAULT_MAX_RETRIES) {
|
|
2392
|
+
return (src) => src.pipe(retry({
|
|
2393
|
+
count: maxRetry,
|
|
2394
|
+
delay: () => timer(delayMs)
|
|
2395
|
+
}));
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function requestPolling(pollInterval, stopCondition$, isPending$) {
|
|
2399
|
+
return (source) => {
|
|
2400
|
+
return interval(pollInterval * 1000)
|
|
2401
|
+
.pipe(startWith(0), tap(() => {
|
|
2402
|
+
try {
|
|
2403
|
+
if (isPending$ && typeof isPending$.next === 'function') {
|
|
2404
|
+
isPending$.next(true);
|
|
2405
|
+
}
|
|
2406
|
+
else if (isPending$ && typeof isPending$.set === 'function') {
|
|
2407
|
+
isPending$.set(true);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
catch (e) {
|
|
2411
|
+
// no-op if setting fails
|
|
2412
|
+
}
|
|
2413
|
+
}), mergeMap(() => source), tap(() => {
|
|
2414
|
+
try {
|
|
2415
|
+
if (isPending$ && typeof isPending$.next === 'function') {
|
|
2416
|
+
isPending$.next(false);
|
|
2417
|
+
}
|
|
2418
|
+
else if (isPending$ && typeof isPending$.set === 'function') {
|
|
2419
|
+
isPending$.set(false);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
catch (e) {
|
|
2423
|
+
// no-op if setting fails
|
|
2424
|
+
}
|
|
2425
|
+
}), takeUntil(stopCondition$));
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
/**
|
|
2430
|
+
* Message Tracker Service - Guaranteed Message Delivery
|
|
2431
|
+
*
|
|
2432
|
+
* Tracks message IDs, detects gaps, manages acknowledgments, and handles sync.
|
|
2433
|
+
* Ensures frontend clients never miss messages even during disconnections.
|
|
2434
|
+
*
|
|
2435
|
+
* Features:
|
|
2436
|
+
* - Per-channel message sequence tracking
|
|
2437
|
+
* - Automatic gap detection and recovery
|
|
2438
|
+
* - Batch acknowledgments (configurable interval)
|
|
2439
|
+
* - Reconnection sync with last-seen tracking
|
|
2440
|
+
* - Gap threshold alerting (10 messages before UI notification)
|
|
2441
|
+
*/
|
|
2442
|
+
class MessageTrackerService {
|
|
2443
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2444
|
+
// INITIALIZATION
|
|
2445
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2446
|
+
constructor(wsManager) {
|
|
2447
|
+
this.wsManager = wsManager;
|
|
2448
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2449
|
+
// CONFIGURATION
|
|
2450
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2451
|
+
/** Batch acknowledgment interval in milliseconds */
|
|
2452
|
+
this.BATCH_ACK_INTERVAL_MS = 5000; // 5 seconds
|
|
2453
|
+
/** Maximum gap before alerting UI */
|
|
2454
|
+
this.GAP_THRESHOLD = 10;
|
|
2455
|
+
/** Maximum messages to store in pending acks before forcing batch */
|
|
2456
|
+
this.MAX_PENDING_ACKS = 100;
|
|
2457
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2458
|
+
// STATE TRACKING
|
|
2459
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2460
|
+
/**
|
|
2461
|
+
* Track last seen message ID per channel
|
|
2462
|
+
* Map<channelName, lastMessageId>
|
|
2463
|
+
*/
|
|
2464
|
+
this.lastSeen = new Map();
|
|
2465
|
+
/**
|
|
2466
|
+
* Expected next message ID per channel (for gap detection)
|
|
2467
|
+
* Map<channelName, nextExpectedId>
|
|
2468
|
+
*/
|
|
2469
|
+
this.expectedSequence = new Map();
|
|
2470
|
+
/**
|
|
2471
|
+
* Pending acknowledgments per channel
|
|
2472
|
+
* Map<channelName, Set<messageId>>
|
|
2473
|
+
*/
|
|
2474
|
+
this.pendingAcks = new Map();
|
|
2475
|
+
/**
|
|
2476
|
+
* Gap count per channel (for threshold alerting)
|
|
2477
|
+
* Map<channelName, gapCount>
|
|
2478
|
+
*/
|
|
2479
|
+
this.gapCounts = new Map();
|
|
2480
|
+
/**
|
|
2481
|
+
* Replay mode tracking (per channel)
|
|
2482
|
+
* Used to distinguish replay vs live messages
|
|
2483
|
+
*/
|
|
2484
|
+
this.inReplayMode = new Map();
|
|
2485
|
+
/**
|
|
2486
|
+
* Gap retry count (for retry logic)
|
|
2487
|
+
* Map<channel, retryCount>
|
|
2488
|
+
*/
|
|
2489
|
+
this.gapRetryCount = new Map();
|
|
2490
|
+
/** Batch acknowledgment timer */
|
|
2491
|
+
this.batchAckTimer = null;
|
|
2492
|
+
/** Subscription to WebSocket messages */
|
|
2493
|
+
this.messagesSubscription = null;
|
|
2494
|
+
/**
|
|
2495
|
+
* Track channels we want to be subscribed to (survives disconnect/reconnect)
|
|
2496
|
+
* This is separate from WebSocketManagerService.subscribedChannels which tracks
|
|
2497
|
+
* actual active subscriptions
|
|
2498
|
+
*/
|
|
2499
|
+
this.intendedChannels = new Set();
|
|
2500
|
+
/** Observable to emit processed messages to UI components */
|
|
2501
|
+
this.messagesSubject = new BehaviorSubject(null);
|
|
2502
|
+
this.messages$ = this.messagesSubject.asObservable();
|
|
2503
|
+
// Restore state from sessionStorage (survives page reload)
|
|
2504
|
+
this.restoreLastSeen();
|
|
2505
|
+
this.restoreIntendedChannels();
|
|
2506
|
+
// Start batch acknowledgment timer
|
|
2507
|
+
this.startBatchAckTimer();
|
|
2508
|
+
// Subscribe to all incoming WebSocket messages
|
|
2509
|
+
this.messagesSubscription = this.wsManager.messages$.subscribe(msg => {
|
|
2510
|
+
if (msg) {
|
|
2511
|
+
this.onMessage(msg);
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
// Listen for reconnect events - re-subscribe with lastSeenId for sync
|
|
2515
|
+
this.wsManager.onReconnect$.subscribe(() => {
|
|
2516
|
+
console.log('🔄 Reconnection detected, re-subscribing with lastSeenId...');
|
|
2517
|
+
this.reSubscribeAllChannels();
|
|
2518
|
+
});
|
|
2519
|
+
console.log('✅ MessageTrackerService initialized');
|
|
2520
|
+
}
|
|
2521
|
+
ngOnDestroy() {
|
|
2522
|
+
this.stopBatchAckTimer();
|
|
2523
|
+
this.persistLastSeen(); // This now also persists intendedChannels
|
|
2524
|
+
this.messagesSubscription?.unsubscribe();
|
|
2525
|
+
console.log('🛑 MessageTrackerService destroyed');
|
|
2526
|
+
}
|
|
2527
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2528
|
+
// MESSAGE HANDLING
|
|
2529
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2530
|
+
/**
|
|
2531
|
+
* Process incoming message
|
|
2532
|
+
* Tracks ID, detects gaps, queues acknowledgment
|
|
2533
|
+
*/
|
|
2534
|
+
onMessage(msg) {
|
|
2535
|
+
const { type, channel, messageId, isReplay } = msg;
|
|
2536
|
+
// Only track messages with IDs (ignore legacy messages without messageId)
|
|
2537
|
+
if (messageId === undefined || messageId === null) {
|
|
2538
|
+
console.warn('⚠️ Received message without ID (legacy format):', msg);
|
|
2539
|
+
// Still forward legacy messages to UI
|
|
2540
|
+
this.messagesSubject.next(msg);
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
// Handle replay mode transitions
|
|
2544
|
+
if (isReplay && !this.inReplayMode.get(channel)) {
|
|
2545
|
+
console.log(`🔄 Entering replay mode for channel: ${channel}`);
|
|
2546
|
+
this.inReplayMode.set(channel, true);
|
|
2547
|
+
}
|
|
2548
|
+
else if (!isReplay && this.inReplayMode.get(channel)) {
|
|
2549
|
+
console.log(`✅ Exiting replay mode for channel: ${channel}`);
|
|
2550
|
+
this.inReplayMode.set(channel, false);
|
|
2551
|
+
}
|
|
2552
|
+
// Track message and detect gaps
|
|
2553
|
+
this.trackMessage(channel, messageId, isReplay);
|
|
2554
|
+
// Forward message to UI components
|
|
2555
|
+
this.messagesSubject.next(msg);
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* Track message ID and detect gaps
|
|
2559
|
+
*/
|
|
2560
|
+
trackMessage(channel, messageId, isReplay) {
|
|
2561
|
+
const expected = this.expectedSequence.get(channel) || 1;
|
|
2562
|
+
// Gap detection
|
|
2563
|
+
if (messageId !== expected) {
|
|
2564
|
+
console.warn(`⚠️ Gap detected in channel "${channel}": expected ${expected}, got ${messageId}`);
|
|
2565
|
+
// Increment gap count
|
|
2566
|
+
const currentGapCount = this.gapCounts.get(channel) || 0;
|
|
2567
|
+
const newGapCount = currentGapCount + 1;
|
|
2568
|
+
this.gapCounts.set(channel, newGapCount);
|
|
2569
|
+
// Check threshold
|
|
2570
|
+
if (newGapCount >= this.GAP_THRESHOLD) {
|
|
2571
|
+
console.error(`🚨 Gap threshold exceeded for channel "${channel}": ${newGapCount} gaps`);
|
|
2572
|
+
this.onGapThresholdExceeded(channel, newGapCount);
|
|
2573
|
+
}
|
|
2574
|
+
// Request gap fill
|
|
2575
|
+
this.requestGapFill(channel, expected, messageId - 1);
|
|
2576
|
+
}
|
|
2577
|
+
else {
|
|
2578
|
+
// Reset gap count and retry count on successful sequence
|
|
2579
|
+
this.gapCounts.set(channel, 0);
|
|
2580
|
+
this.gapRetryCount.set(channel, 0);
|
|
2581
|
+
}
|
|
2582
|
+
// Update tracking
|
|
2583
|
+
this.lastSeen.set(channel, messageId);
|
|
2584
|
+
this.expectedSequence.set(channel, messageId + 1);
|
|
2585
|
+
// Persist to sessionStorage (debounced in production)
|
|
2586
|
+
this.persistLastSeen();
|
|
2587
|
+
// Queue for acknowledgment
|
|
2588
|
+
this.queueAck(channel, messageId);
|
|
2589
|
+
// Check if we should force batch ack due to volume
|
|
2590
|
+
const pendingCount = this.pendingAcks.get(channel)?.size || 0;
|
|
2591
|
+
if (pendingCount >= this.MAX_PENDING_ACKS) {
|
|
2592
|
+
console.log(`📦 Forcing batch ack: ${pendingCount} pending messages`);
|
|
2593
|
+
this.sendBatchAck(channel);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2597
|
+
// ACKNOWLEDGMENT MANAGEMENT
|
|
2598
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2599
|
+
/**
|
|
2600
|
+
* Queue message for batch acknowledgment
|
|
2601
|
+
*/
|
|
2602
|
+
queueAck(channel, messageId) {
|
|
2603
|
+
if (!this.pendingAcks.has(channel)) {
|
|
2604
|
+
this.pendingAcks.set(channel, new Set());
|
|
2605
|
+
}
|
|
2606
|
+
this.pendingAcks.get(channel).add(messageId);
|
|
2607
|
+
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Start batch acknowledgment timer
|
|
2610
|
+
*/
|
|
2611
|
+
startBatchAckTimer() {
|
|
2612
|
+
this.batchAckTimer = setInterval(() => {
|
|
2613
|
+
this.sendAllBatchAcks();
|
|
2614
|
+
}, this.BATCH_ACK_INTERVAL_MS);
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Stop batch acknowledgment timer
|
|
2618
|
+
*/
|
|
2619
|
+
stopBatchAckTimer() {
|
|
2620
|
+
if (this.batchAckTimer) {
|
|
2621
|
+
clearInterval(this.batchAckTimer);
|
|
2622
|
+
this.batchAckTimer = null;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Send batch acknowledgments for all channels with pending acks
|
|
2627
|
+
*/
|
|
2628
|
+
sendAllBatchAcks() {
|
|
2629
|
+
this.pendingAcks.forEach((messageIds, channel) => {
|
|
2630
|
+
if (messageIds.size > 0) {
|
|
2631
|
+
this.sendBatchAck(channel);
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Send batch acknowledgment for a specific channel
|
|
2637
|
+
*/
|
|
2638
|
+
sendBatchAck(channel) {
|
|
2639
|
+
const messageIds = this.pendingAcks.get(channel);
|
|
2640
|
+
if (!messageIds || messageIds.size === 0) {
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
// Convert Set to sorted array
|
|
2644
|
+
const sortedIds = Array.from(messageIds).sort((a, b) => a - b);
|
|
2645
|
+
const ackUpTo = sortedIds[sortedIds.length - 1];
|
|
2646
|
+
console.log(`📦 Batch ack for channel "${channel}": ackUpTo=${ackUpTo} (${messageIds.size} messages)`);
|
|
2647
|
+
this.wsManager.send({
|
|
2648
|
+
type: 'messageAckBatch',
|
|
2649
|
+
channel,
|
|
2650
|
+
ackUpTo,
|
|
2651
|
+
// Optional: include skippedIds if you want to report gaps
|
|
2652
|
+
// skippedIds: this.getSkippedIds(channel, ackUpTo)
|
|
2653
|
+
});
|
|
2654
|
+
// Clear pending acks
|
|
2655
|
+
messageIds.clear();
|
|
2656
|
+
}
|
|
2657
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2658
|
+
// GAP RECOVERY
|
|
2659
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2660
|
+
/**
|
|
2661
|
+
* Request missing messages from server
|
|
2662
|
+
*/
|
|
2663
|
+
requestGapFill(channel, fromId, toId) {
|
|
2664
|
+
const retries = this.gapRetryCount.get(channel) || 0;
|
|
2665
|
+
if (retries >= 3) {
|
|
2666
|
+
console.error(`🚨 Gap fill failed after 3 retries for channel "${channel}": ${fromId} to ${toId}`);
|
|
2667
|
+
// Could emit to UI for manual intervention
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
console.log(`🔍 Requesting gap fill for channel "${channel}": ${fromId} to ${toId} (attempt ${retries + 1})`);
|
|
2671
|
+
this.wsManager.send({
|
|
2672
|
+
type: 'gapRequest',
|
|
2673
|
+
channel,
|
|
2674
|
+
fromId,
|
|
2675
|
+
toId
|
|
2676
|
+
});
|
|
2677
|
+
this.gapRetryCount.set(channel, retries + 1);
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Handle gap threshold exceeded (optional: alert UI)
|
|
2681
|
+
*/
|
|
2682
|
+
onGapThresholdExceeded(channel, gapCount) {
|
|
2683
|
+
console.warn(`⚠️ High gap count for channel "${channel}": ${gapCount} missed messages`);
|
|
2684
|
+
// Could emit to a separate observable for UI components to subscribe to
|
|
2685
|
+
// this.gapAlerts.next({ channel, gapCount });
|
|
2686
|
+
}
|
|
2687
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2688
|
+
// SUBSCRIPTION MANAGEMENT
|
|
2689
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2690
|
+
/**
|
|
2691
|
+
* Subscribe to channel with sync support
|
|
2692
|
+
* Automatically includes lastSeenId for replay
|
|
2693
|
+
*/
|
|
2694
|
+
subscribeToChannel(channel, userData) {
|
|
2695
|
+
const lastSeenId = this.lastSeen.get(channel) || 0;
|
|
2696
|
+
console.log(`📥 Subscribing to channel "${channel}" with lastSeenId=${lastSeenId}`);
|
|
2697
|
+
console.log(`📝 Adding to intendedChannels (now has ${this.intendedChannels.size + 1} channels)`);
|
|
2698
|
+
// Track this channel as intended (survives disconnect/reconnect)
|
|
2699
|
+
this.intendedChannels.add(channel);
|
|
2700
|
+
this.persistIntendedChannels();
|
|
2701
|
+
this.wsManager.sendSubscribeWithLastSeen(channel, userData, lastSeenId);
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* Re-subscribe to all previously subscribed channels after reconnect
|
|
2705
|
+
* Includes lastSeenId for each channel to trigger message replay
|
|
2706
|
+
*/
|
|
2707
|
+
reSubscribeAllChannels() {
|
|
2708
|
+
// Use intendedChannels which survives disconnect/reconnect
|
|
2709
|
+
const channels = Array.from(this.intendedChannels);
|
|
2710
|
+
console.log(`🔄 Re-subscribing to ${channels.length} channel(s) with lastSeenId...`);
|
|
2711
|
+
channels.forEach((channel) => {
|
|
2712
|
+
this.subscribeToChannel(channel);
|
|
2713
|
+
});
|
|
2714
|
+
console.log(`✅ Re-subscription complete for ${channels.length} channel(s)`);
|
|
2715
|
+
}
|
|
2716
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2717
|
+
// STATE PERSISTENCE
|
|
2718
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2719
|
+
/**
|
|
2720
|
+
* Persist lastSeen to sessionStorage (survives page reload)
|
|
2721
|
+
*/
|
|
2722
|
+
persistLastSeen() {
|
|
2723
|
+
try {
|
|
2724
|
+
const data = JSON.stringify(Array.from(this.lastSeen.entries()));
|
|
2725
|
+
sessionStorage.setItem('messageLastSeen', data);
|
|
2726
|
+
// Also persist intendedChannels
|
|
2727
|
+
this.persistIntendedChannels();
|
|
2728
|
+
}
|
|
2729
|
+
catch (err) {
|
|
2730
|
+
console.warn('⚠️ Failed to persist lastSeen:', err.message);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
/**
|
|
2734
|
+
* Restore lastSeen from sessionStorage
|
|
2735
|
+
*/
|
|
2736
|
+
restoreLastSeen() {
|
|
2737
|
+
try {
|
|
2738
|
+
const data = sessionStorage.getItem('messageLastSeen');
|
|
2739
|
+
if (data) {
|
|
2740
|
+
const entries = JSON.parse(data);
|
|
2741
|
+
this.lastSeen = new Map(entries);
|
|
2742
|
+
// Rebuild expectedSequence from restored lastSeen
|
|
2743
|
+
this.lastSeen.forEach((lastId, channel) => {
|
|
2744
|
+
this.expectedSequence.set(channel, lastId + 1);
|
|
2745
|
+
});
|
|
2746
|
+
console.log('📥 Restored lastSeen from sessionStorage:', this.lastSeen.size, 'channels');
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
catch (err) {
|
|
2750
|
+
console.warn('⚠️ Failed to restore lastSeen:', err.message);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* Persist intendedChannels to sessionStorage
|
|
2755
|
+
*/
|
|
2756
|
+
persistIntendedChannels() {
|
|
2757
|
+
try {
|
|
2758
|
+
const channels = Array.from(this.intendedChannels);
|
|
2759
|
+
sessionStorage.setItem('intendedChannels', JSON.stringify(channels));
|
|
2760
|
+
}
|
|
2761
|
+
catch (err) {
|
|
2762
|
+
console.warn('⚠️ Failed to persist intendedChannels:', err.message);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Restore intendedChannels from sessionStorage
|
|
2767
|
+
*/
|
|
2768
|
+
restoreIntendedChannels() {
|
|
2769
|
+
try {
|
|
2770
|
+
const data = sessionStorage.getItem('intendedChannels');
|
|
2771
|
+
if (data) {
|
|
2772
|
+
const channels = JSON.parse(data);
|
|
2773
|
+
this.intendedChannels = new Set(channels);
|
|
2774
|
+
console.log('📥 Restored intendedChannels from sessionStorage:', this.intendedChannels.size, 'channels');
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
catch (err) {
|
|
2778
|
+
console.warn('⚠️ Failed to restore intendedChannels:', err.message);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2782
|
+
// PUBLIC ACCESSORS
|
|
2783
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2784
|
+
/**
|
|
2785
|
+
* Get last seen message ID for a channel
|
|
2786
|
+
*/
|
|
2787
|
+
getLastSeenId(channel) {
|
|
2788
|
+
return this.lastSeen.get(channel) || 0;
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Get expected next message ID for a channel
|
|
2792
|
+
*/
|
|
2793
|
+
getExpectedNextId(channel) {
|
|
2794
|
+
return this.expectedSequence.get(channel) || 1;
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Check if currently in replay mode for a channel
|
|
2798
|
+
*/
|
|
2799
|
+
isInReplayMode(channel) {
|
|
2800
|
+
return this.inReplayMode.get(channel) || false;
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Get gap count for a channel
|
|
2804
|
+
*/
|
|
2805
|
+
getGapCount(channel) {
|
|
2806
|
+
return this.gapCounts.get(channel) || 0;
|
|
2807
|
+
}
|
|
2808
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2809
|
+
// UTILITY METHODS
|
|
2810
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2811
|
+
/**
|
|
2812
|
+
* Get skipped IDs between expected and received (for detailed gap reporting)
|
|
2813
|
+
*/
|
|
2814
|
+
getSkippedIds(channel, upToId) {
|
|
2815
|
+
const expected = this.expectedSequence.get(channel) || 1;
|
|
2816
|
+
const skipped = [];
|
|
2817
|
+
for (let i = expected; i < upToId; i++) {
|
|
2818
|
+
const pending = this.pendingAcks.get(channel);
|
|
2819
|
+
if (!pending?.has(i)) {
|
|
2820
|
+
skipped.push(i);
|
|
1734
2821
|
}
|
|
1735
|
-
|
|
2822
|
+
}
|
|
2823
|
+
return skipped;
|
|
1736
2824
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
2825
|
+
/**
|
|
2826
|
+
* Clear all tracking state (useful for debugging or manual reset)
|
|
2827
|
+
*/
|
|
2828
|
+
clearState() {
|
|
2829
|
+
this.lastSeen.clear();
|
|
2830
|
+
this.expectedSequence.clear();
|
|
2831
|
+
this.pendingAcks.clear();
|
|
2832
|
+
this.gapCounts.clear();
|
|
2833
|
+
this.inReplayMode.clear();
|
|
2834
|
+
this.gapRetryCount.clear();
|
|
2835
|
+
this.persistLastSeen();
|
|
2836
|
+
console.log('🧹 MessageTracker state cleared');
|
|
2837
|
+
}
|
|
2838
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageTrackerService, deps: [{ token: WebSocketManagerService }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2839
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageTrackerService, providedIn: 'root' }); }
|
|
1739
2840
|
}
|
|
1740
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
2841
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageTrackerService, decorators: [{
|
|
1741
2842
|
type: Injectable,
|
|
1742
2843
|
args: [{
|
|
1743
|
-
providedIn: 'root'
|
|
2844
|
+
providedIn: 'root',
|
|
1744
2845
|
}]
|
|
1745
|
-
}] });
|
|
1746
|
-
|
|
1747
|
-
function countdown(duration) {
|
|
1748
|
-
return defer(() => {
|
|
1749
|
-
const currentCount = { current: duration };
|
|
1750
|
-
return interval(1000).pipe(map(() => --currentCount.current), takeWhile(count => count >= 0));
|
|
1751
|
-
});
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
const DEFAULT_MAX_RETRIES = 3;
|
|
1755
|
-
function delayedRetry(delayMs, maxRetry = DEFAULT_MAX_RETRIES) {
|
|
1756
|
-
return (src) => src.pipe(retry({
|
|
1757
|
-
count: maxRetry,
|
|
1758
|
-
delay: () => timer(delayMs)
|
|
1759
|
-
}));
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
function requestPolling(pollInterval, stopCondition$, isPending$) {
|
|
1763
|
-
return (source) => {
|
|
1764
|
-
return interval(pollInterval * 1000)
|
|
1765
|
-
.pipe(startWith(0), tap(() => {
|
|
1766
|
-
try {
|
|
1767
|
-
if (isPending$ && typeof isPending$.next === 'function') {
|
|
1768
|
-
isPending$.next(true);
|
|
1769
|
-
}
|
|
1770
|
-
else if (isPending$ && typeof isPending$.set === 'function') {
|
|
1771
|
-
isPending$.set(true);
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
catch (e) {
|
|
1775
|
-
// no-op if setting fails
|
|
1776
|
-
}
|
|
1777
|
-
}), mergeMap(() => source), tap(() => {
|
|
1778
|
-
try {
|
|
1779
|
-
if (isPending$ && typeof isPending$.next === 'function') {
|
|
1780
|
-
isPending$.next(false);
|
|
1781
|
-
}
|
|
1782
|
-
else if (isPending$ && typeof isPending$.set === 'function') {
|
|
1783
|
-
isPending$.set(false);
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
catch (e) {
|
|
1787
|
-
// no-op if setting fails
|
|
1788
|
-
}
|
|
1789
|
-
}), takeUntil(stopCondition$));
|
|
1790
|
-
};
|
|
1791
|
-
}
|
|
2846
|
+
}], ctorParameters: () => [{ type: WebSocketManagerService }] });
|
|
1792
2847
|
|
|
1793
2848
|
class DatabaseStorage {
|
|
1794
2849
|
constructor(table = '', expiresIn) {
|
|
@@ -1877,6 +2932,12 @@ class HTTPManagerService extends RequestService {
|
|
|
1877
2932
|
this.toastMessage = inject(ToastMessageDisplayService);
|
|
1878
2933
|
this.ng_injector = inject(Injector);
|
|
1879
2934
|
this.objectMergerService = inject(ObjectMergerService);
|
|
2935
|
+
this.wsManager = inject(WebSocketManagerService);
|
|
2936
|
+
this.messageTracker = inject(MessageTrackerService);
|
|
2937
|
+
// Delegate WebSocket observables to WebSocketManagerService (singleton)
|
|
2938
|
+
this.connectionStatus$ = this.wsManager.connectionStatus$;
|
|
2939
|
+
this.messages$ = this.messageTracker.messages$; // Messages flow through MessageTrackerService
|
|
2940
|
+
this.subscribedChannels$ = this.wsManager.subscribedChannels$;
|
|
1880
2941
|
this.countdown = new BehaviorSubject(0);
|
|
1881
2942
|
this.countdown$ = this.countdown.asObservable();
|
|
1882
2943
|
this.error = new BehaviorSubject(false);
|
|
@@ -1887,6 +2948,130 @@ class HTTPManagerService extends RequestService {
|
|
|
1887
2948
|
this.config = ApiRequest.adapt();
|
|
1888
2949
|
this.config = (configOptions) ? ApiRequest.adapt(configOptions.httpRequestOptions) : this.config;
|
|
1889
2950
|
}
|
|
2951
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2952
|
+
// WEBSOCKET METHODS (Delegated to WebSocketManagerService singleton)
|
|
2953
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2954
|
+
/**
|
|
2955
|
+
* Connect to WebSocket server
|
|
2956
|
+
* Only the first call across ALL instances will establish a connection
|
|
2957
|
+
*/
|
|
2958
|
+
connect(options, jwtToken) {
|
|
2959
|
+
this.wsManager.connect(options, jwtToken);
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Disconnect from WebSocket server
|
|
2963
|
+
*/
|
|
2964
|
+
disconnect() {
|
|
2965
|
+
this.wsManager.disconnect();
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Subscribe to a channel
|
|
2969
|
+
*/
|
|
2970
|
+
subscribeToChannel(channel, userData) {
|
|
2971
|
+
this.messageTracker.subscribeToChannel(channel, userData);
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Subscribe to multiple channels
|
|
2975
|
+
*/
|
|
2976
|
+
subscribeToChannels(channels, userData) {
|
|
2977
|
+
channels.forEach(channel => this.messageTracker.subscribeToChannel(channel, userData));
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2980
|
+
* Unsubscribe from a channel
|
|
2981
|
+
*/
|
|
2982
|
+
unsubscribeFromChannel(channel) {
|
|
2983
|
+
this.wsManager.unsubscribeFromChannel(channel);
|
|
2984
|
+
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Get currently subscribed channels
|
|
2987
|
+
*/
|
|
2988
|
+
getSubscribedChannels() {
|
|
2989
|
+
return this.wsManager.getSubscribedChannels();
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Send broadcast message
|
|
2993
|
+
*/
|
|
2994
|
+
sendBroadcast(content) {
|
|
2995
|
+
this.wsManager.sendBroadcast(content);
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* Send message in channel (state manager message)
|
|
2999
|
+
*/
|
|
3000
|
+
sendMessageInChannel(channel, content) {
|
|
3001
|
+
this.wsManager.sendMessageInChannel(channel, content);
|
|
3002
|
+
}
|
|
3003
|
+
/**
|
|
3004
|
+
* Send channel message
|
|
3005
|
+
*/
|
|
3006
|
+
sendChannelMessage(channel, content) {
|
|
3007
|
+
this.wsManager.sendChannelMessage(channel, content);
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* Send message to multiple channels
|
|
3011
|
+
*/
|
|
3012
|
+
sendChannelMessageToChannels(channels, content) {
|
|
3013
|
+
this.wsManager.sendChannelMessageToChannels(channels, content);
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
3016
|
+
* Send message to user
|
|
3017
|
+
*/
|
|
3018
|
+
sendMessageToUser(user, content) {
|
|
3019
|
+
this.wsManager.sendMessageToUser(user, content);
|
|
3020
|
+
}
|
|
3021
|
+
/**
|
|
3022
|
+
* Get all channels
|
|
3023
|
+
*/
|
|
3024
|
+
getAllChannels() {
|
|
3025
|
+
this.wsManager.getAllChannels();
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Create channel
|
|
3029
|
+
*/
|
|
3030
|
+
createChannel(channel) {
|
|
3031
|
+
this.wsManager.createChannel(channel);
|
|
3032
|
+
}
|
|
3033
|
+
/**
|
|
3034
|
+
* Delete channel
|
|
3035
|
+
*/
|
|
3036
|
+
deleteChannel(channel) {
|
|
3037
|
+
this.wsManager.deleteChannel(channel);
|
|
3038
|
+
}
|
|
3039
|
+
/**
|
|
3040
|
+
* Get users in channel
|
|
3041
|
+
*/
|
|
3042
|
+
getUsersInChannel(channel) {
|
|
3043
|
+
this.wsManager.getUsersInChannel(channel);
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Create notification channel
|
|
3047
|
+
*/
|
|
3048
|
+
createNotificationChannel(channel) {
|
|
3049
|
+
this.wsManager.createNotificationChannel(channel);
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Get notification channels
|
|
3053
|
+
*/
|
|
3054
|
+
getNotificationChannels() {
|
|
3055
|
+
this.wsManager.getNotificationChannels();
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Get today's notification channels
|
|
3059
|
+
*/
|
|
3060
|
+
getTodaysNotificationChannels() {
|
|
3061
|
+
this.wsManager.getTodaysNotificationChannels();
|
|
3062
|
+
}
|
|
3063
|
+
/**
|
|
3064
|
+
* Subscribe to notification channel
|
|
3065
|
+
*/
|
|
3066
|
+
subscribeToNotificationChannel(channel, options, user) {
|
|
3067
|
+
this.wsManager.subscribeToNotificationChannel(channel, options, user);
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Unsubscribe from notification channel
|
|
3071
|
+
*/
|
|
3072
|
+
unsubscribeFromNotificationChannel(channel) {
|
|
3073
|
+
this.wsManager.unsubscribeFromNotificationChannel(channel);
|
|
3074
|
+
}
|
|
1890
3075
|
// REQUESTS
|
|
1891
3076
|
getRequest(options, params) {
|
|
1892
3077
|
this.isPending.next(true);
|
|
@@ -2416,7 +3601,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2416
3601
|
}] }] });
|
|
2417
3602
|
|
|
2418
3603
|
class ApiRequest {
|
|
2419
|
-
constructor(server = '', path, headers, adapter, mapper, polling, retry, stream, streamType, displayError, saveAs, fileContentHeader, ws) {
|
|
3604
|
+
constructor(server = '', path, headers, adapter, mapper, polling, retry, stream, streamType, displayError, saveAs, fileContentHeader, ws, env) {
|
|
2420
3605
|
this.server = server;
|
|
2421
3606
|
this.path = path;
|
|
2422
3607
|
this.headers = headers;
|
|
@@ -2430,10 +3615,11 @@ class ApiRequest {
|
|
|
2430
3615
|
this.saveAs = saveAs;
|
|
2431
3616
|
this.fileContentHeader = fileContentHeader;
|
|
2432
3617
|
this.ws = ws;
|
|
3618
|
+
this.env = env;
|
|
2433
3619
|
}
|
|
2434
3620
|
static adapt(item) {
|
|
2435
3621
|
const server = Array.isArray(item?.server) ? item.server.join('/') : item?.server || '';
|
|
2436
|
-
return new ApiRequest(server, (item?.path) ? item.path : [], (item?.headers) ? item.headers : {}, item?.adapter, item?.mapper, item?.polling ? Math.floor(item.polling) : 0, item?.retry ? RetryOptions.adapt(item.retry) : RetryOptions.adapt(), (item?.stream) ? item.stream : false, item?.streamType || StreamType.AI_STREAMING, (item?.displayError) ? item.displayError : false, item?.saveAs, item?.fileContentHeader, item?.ws);
|
|
3622
|
+
return new ApiRequest(server, (item?.path) ? item.path : [], (item?.headers) ? item.headers : {}, item?.adapter, item?.mapper, item?.polling ? Math.floor(item.polling) : 0, item?.retry ? RetryOptions.adapt(item.retry) : RetryOptions.adapt(), (item?.stream) ? item.stream : false, item?.streamType || StreamType.AI_STREAMING, (item?.displayError) ? item.displayError : false, item?.saveAs, item?.fileContentHeader, item?.ws, item?.env || 'dev');
|
|
2437
3623
|
}
|
|
2438
3624
|
}
|
|
2439
3625
|
|
|
@@ -2448,17 +3634,18 @@ class RequestOptions {
|
|
|
2448
3634
|
}
|
|
2449
3635
|
|
|
2450
3636
|
class WSOptions {
|
|
2451
|
-
constructor(id = '', wsServer = '', jwtToken = '', permissions, channels, user, retry) {
|
|
3637
|
+
constructor(id = '', wsServer = '', jwtToken = '', permissions, channels, wsUpdateChannels, user, retry) {
|
|
2452
3638
|
this.id = id;
|
|
2453
3639
|
this.wsServer = wsServer;
|
|
2454
3640
|
this.jwtToken = jwtToken;
|
|
2455
3641
|
this.permissions = permissions;
|
|
2456
3642
|
this.channels = channels;
|
|
3643
|
+
this.wsUpdateChannels = wsUpdateChannels;
|
|
2457
3644
|
this.user = user;
|
|
2458
3645
|
this.retry = retry;
|
|
2459
3646
|
}
|
|
2460
3647
|
static adapt(item) {
|
|
2461
|
-
return new WSOptions(item?.id, item?.wsServer, item?.jwtToken, (item?.permissions) ? item.permissions.split(',').map((p) => p.trim()) : [], item?.channels, item?.user, item?.retry);
|
|
3648
|
+
return new WSOptions(item?.id, item?.wsServer, item?.jwtToken, (item?.permissions) ? item.permissions.split(',').map((p) => p.trim()) : [], item?.channels, item?.wsUpdateChannels, item?.user, item?.retry);
|
|
2462
3649
|
}
|
|
2463
3650
|
}
|
|
2464
3651
|
|
|
@@ -2580,8 +3767,24 @@ class LocalStorageManagerService extends ComponentStore {
|
|
|
2580
3767
|
console.warn('No App ID found - AppId not Provided');
|
|
2581
3768
|
return;
|
|
2582
3769
|
}
|
|
2583
|
-
|
|
2584
|
-
|
|
3770
|
+
let storageData = found.data;
|
|
3771
|
+
if (options.encrypted) {
|
|
3772
|
+
const decryptedData = this.encryption.decrypt(found.data, this.app.appID);
|
|
3773
|
+
if (decryptedData !== null) {
|
|
3774
|
+
storageData = decryptedData;
|
|
3775
|
+
}
|
|
3776
|
+
else {
|
|
3777
|
+
console.warn(`Failed to decrypt data for store: ${store}`);
|
|
3778
|
+
storageData = found.data;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
try {
|
|
3782
|
+
return (this.isString(storageData)) ? JSON.parse(storageData) : storageData;
|
|
3783
|
+
}
|
|
3784
|
+
catch (error) {
|
|
3785
|
+
console.warn(`Failed to parse storage data for store: ${store}`, error);
|
|
3786
|
+
return storageData; // Return raw data if parsing fails
|
|
3787
|
+
}
|
|
2585
3788
|
}
|
|
2586
3789
|
else {
|
|
2587
3790
|
return null;
|
|
@@ -2749,10 +3952,39 @@ class LocalStorageManagerService extends ComponentStore {
|
|
|
2749
3952
|
const str = localStorage.getItem(this.storageSettingsName);
|
|
2750
3953
|
const localStr = localStorage.getItem(this.storageName);
|
|
2751
3954
|
const sessionStr = sessionStorage.getItem(this.storageName);
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
3955
|
+
let localData = [];
|
|
3956
|
+
let sessionData = [];
|
|
3957
|
+
let settings = [];
|
|
3958
|
+
try {
|
|
3959
|
+
localData = (localStr) ? JSON.parse(localStr) : [];
|
|
3960
|
+
}
|
|
3961
|
+
catch (error) {
|
|
3962
|
+
console.warn('Failed to parse local storage data:', error);
|
|
3963
|
+
localData = [];
|
|
3964
|
+
}
|
|
3965
|
+
try {
|
|
3966
|
+
sessionData = (sessionStr) ? JSON.parse(sessionStr) : [];
|
|
3967
|
+
}
|
|
3968
|
+
catch (error) {
|
|
3969
|
+
console.warn('Failed to parse session storage data:', error);
|
|
3970
|
+
sessionData = [];
|
|
3971
|
+
}
|
|
3972
|
+
if (str) {
|
|
3973
|
+
const decryptedStr = this.encryption.decrypt(str, this.app.appID);
|
|
3974
|
+
if (decryptedStr) {
|
|
3975
|
+
try {
|
|
3976
|
+
settings = JSON.parse(decryptedStr);
|
|
3977
|
+
}
|
|
3978
|
+
catch (error) {
|
|
3979
|
+
console.warn('Failed to parse decrypted settings:', error);
|
|
3980
|
+
settings = [];
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
else {
|
|
3984
|
+
console.warn('Failed to decrypt settings data');
|
|
3985
|
+
settings = [];
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
2756
3988
|
settings.forEach(store => {
|
|
2757
3989
|
const expired = (store.options?.expires || 0) > 0 && this.utils.hasExpired(store.options?.expires || 0);
|
|
2758
3990
|
if (!expired) {
|
|
@@ -2805,6 +4037,47 @@ class LocalStorageManagerService extends ComponentStore {
|
|
|
2805
4037
|
ngOnDestroy() {
|
|
2806
4038
|
this.persistence$.unsubscribe();
|
|
2807
4039
|
}
|
|
4040
|
+
/**
|
|
4041
|
+
* Clears all stored data from localStorage and sessionStorage
|
|
4042
|
+
* Use this method to recover from corrupted data errors
|
|
4043
|
+
*/
|
|
4044
|
+
clearAllStoredData() {
|
|
4045
|
+
try {
|
|
4046
|
+
localStorage.removeItem(this.storageSettingsName);
|
|
4047
|
+
localStorage.removeItem(this.storageName);
|
|
4048
|
+
sessionStorage.removeItem(this.storageName);
|
|
4049
|
+
console.log('Cleared all stored data');
|
|
4050
|
+
}
|
|
4051
|
+
catch (error) {
|
|
4052
|
+
console.error('Failed to clear stored data:', error);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
/**
|
|
4056
|
+
* Checks if stored data appears to be corrupted
|
|
4057
|
+
* Returns true if data appears corrupted
|
|
4058
|
+
*/
|
|
4059
|
+
checkForCorruptedData() {
|
|
4060
|
+
try {
|
|
4061
|
+
const str = localStorage.getItem(this.storageSettingsName);
|
|
4062
|
+
if (str) {
|
|
4063
|
+
// Try to decrypt if it's encrypted
|
|
4064
|
+
const decryptedStr = this.encryption.decrypt(str, this.app.appID);
|
|
4065
|
+
if (decryptedStr !== null && decryptedStr !== undefined) {
|
|
4066
|
+
// Try to parse the decrypted data
|
|
4067
|
+
JSON.parse(decryptedStr);
|
|
4068
|
+
}
|
|
4069
|
+
else {
|
|
4070
|
+
// Decryption failed, data is likely corrupted
|
|
4071
|
+
return true;
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
return false;
|
|
4075
|
+
}
|
|
4076
|
+
catch (error) {
|
|
4077
|
+
// Parsing failed, data is corrupted
|
|
4078
|
+
return true;
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
2808
4081
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocalStorageManagerService, deps: [{ token: CONFIG_SETTINGS_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2809
4082
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocalStorageManagerService, providedIn: 'root' }); }
|
|
2810
4083
|
}
|
|
@@ -2857,10 +4130,24 @@ class LocalStorageSignalsManagerService {
|
|
|
2857
4130
|
console.warn('No App ID found - AppId not Provided');
|
|
2858
4131
|
return null;
|
|
2859
4132
|
}
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
4133
|
+
let storageData = found.data;
|
|
4134
|
+
if (options.encrypted) {
|
|
4135
|
+
const decryptedData = this.encryption.decrypt(found.data, this.app.appID);
|
|
4136
|
+
if (decryptedData !== null) {
|
|
4137
|
+
storageData = decryptedData;
|
|
4138
|
+
}
|
|
4139
|
+
else {
|
|
4140
|
+
console.warn(`Failed to decrypt data for store: ${store}`);
|
|
4141
|
+
storageData = found.data; // Use undecrypted data as fallback
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
try {
|
|
4145
|
+
return this.isString(storageData) ? JSON.parse(storageData) : storageData;
|
|
4146
|
+
}
|
|
4147
|
+
catch (error) {
|
|
4148
|
+
console.warn(`Failed to parse storage data for store: ${store}`, error);
|
|
4149
|
+
return storageData; // Return raw data if parsing fails
|
|
4150
|
+
}
|
|
2864
4151
|
});
|
|
2865
4152
|
this.settings = computed(() => this.state().settings);
|
|
2866
4153
|
this.setting = (store) => computed(() => this.state().settings.find(item => item.name === store) ?? null);
|
|
@@ -2988,8 +4275,23 @@ class LocalStorageSignalsManagerService {
|
|
|
2988
4275
|
const sessionStr = sessionStorage.getItem(this.storageName);
|
|
2989
4276
|
const localData = localStr ? JSON.parse(localStr) : [];
|
|
2990
4277
|
const sessionData = sessionStr ? JSON.parse(sessionStr) : [];
|
|
2991
|
-
|
|
2992
|
-
|
|
4278
|
+
let settings = [];
|
|
4279
|
+
if (str) {
|
|
4280
|
+
const decryptedStr = this.encryption.decrypt(str, this.app.appID);
|
|
4281
|
+
if (decryptedStr) {
|
|
4282
|
+
try {
|
|
4283
|
+
settings = JSON.parse(decryptedStr);
|
|
4284
|
+
}
|
|
4285
|
+
catch (error) {
|
|
4286
|
+
console.warn('Failed to parse decrypted settings:', error);
|
|
4287
|
+
settings = [];
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
else {
|
|
4291
|
+
console.warn('Failed to decrypt settings data');
|
|
4292
|
+
settings = [];
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
2993
4295
|
settings.forEach(store => {
|
|
2994
4296
|
// normalize options so we compare numbers and compute expires correctly
|
|
2995
4297
|
const options = SettingOptions.adapt(store.options);
|
|
@@ -3448,13 +4750,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3448
4750
|
}], ctorParameters: () => [] });
|
|
3449
4751
|
|
|
3450
4752
|
class ChannelMessage {
|
|
3451
|
-
constructor(sessionId = null, content = null) {
|
|
4753
|
+
constructor(messageId, channel, isReplay, sessionId = null, content = null, timestamp) {
|
|
4754
|
+
this.messageId = messageId;
|
|
4755
|
+
this.channel = channel;
|
|
4756
|
+
this.isReplay = isReplay;
|
|
3452
4757
|
this.sessionId = sessionId;
|
|
3453
4758
|
this.content = content;
|
|
4759
|
+
this.timestamp = timestamp;
|
|
3454
4760
|
}
|
|
3455
4761
|
static adapt(item) {
|
|
3456
|
-
return new ChannelMessage(item?.sessionId || item?.id, // Support both for backward compatibility
|
|
3457
|
-
item?.content);
|
|
4762
|
+
return new ChannelMessage(item?.messageId, item?.channel, item?.isReplay, item?.sessionId || item?.id, // Support both for backward compatibility
|
|
4763
|
+
item?.content, item?.timestamp);
|
|
3458
4764
|
}
|
|
3459
4765
|
}
|
|
3460
4766
|
|
|
@@ -3489,6 +4795,8 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3489
4795
|
getUsersForChannel$(channel) {
|
|
3490
4796
|
return this.userListByChannel$.pipe(map(channelMap => channelMap.get(channel) || []));
|
|
3491
4797
|
}
|
|
4798
|
+
// Message queue for WebSocket communication (processed when connection is established)
|
|
4799
|
+
static { this.wsCommunicationQueue = []; }
|
|
3492
4800
|
constructor(apiOptions = ApiRequest.adapt(), dataType, database) {
|
|
3493
4801
|
super(defaultState);
|
|
3494
4802
|
this.apiOptions = apiOptions;
|
|
@@ -3516,7 +4824,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3516
4824
|
this.messages$ = this.messages.asObservable();
|
|
3517
4825
|
this.userListByChannel = new BehaviorSubject(new Map());
|
|
3518
4826
|
this.userListByChannel$ = this.userListByChannel.asObservable();
|
|
3519
|
-
//
|
|
4827
|
+
// Returns all unique users across all channels
|
|
3520
4828
|
this.userList = new BehaviorSubject([]);
|
|
3521
4829
|
this.userList$ = this.userList.asObservable();
|
|
3522
4830
|
this.user = new BehaviorSubject(null);
|
|
@@ -3540,17 +4848,19 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3540
4848
|
this.latestCommunicationMessages$ = this.latestCommunicationMessages.asObservable();
|
|
3541
4849
|
this.userAction = new BehaviorSubject(null);
|
|
3542
4850
|
this.userAction$ = this.userAction.asObservable();
|
|
3543
|
-
this.wsConnection = false;
|
|
3544
4851
|
this.wsOptions = WSOptions.adapt();
|
|
3545
|
-
// Expose raw WS connection status directly to UI
|
|
4852
|
+
// Expose raw WS connection status directly to UI (from singleton WebSocketManagerService)
|
|
3546
4853
|
this.connectionStatus$ = this.httpManagerService.connectionStatus$;
|
|
3547
4854
|
// WebSocket
|
|
3548
|
-
this.initWS = this.effect((wsOptions$) => wsOptions$.pipe(
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
4855
|
+
this.initWS = this.effect((wsOptions$) => wsOptions$.pipe(switchMap((wsOptions) => merge(this.httpManagerService.connectionStatus$.pipe(tap((isConnected) => {
|
|
4856
|
+
// Process queued wsCommunication calls when connection becomes true
|
|
4857
|
+
if (isConnected && HTTPManagerStateService.wsCommunicationQueue.length > 0) {
|
|
4858
|
+
console.log(`🔄 Processing ${HTTPManagerStateService.wsCommunicationQueue.length} queued WS messages`);
|
|
4859
|
+
while (HTTPManagerStateService.wsCommunicationQueue.length > 0) {
|
|
4860
|
+
const queued = HTTPManagerStateService.wsCommunicationQueue.shift();
|
|
4861
|
+
this.sendWsCommunication(queued.method, queued.path);
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
3554
4864
|
})), this.httpManagerService.messages$.pipe(tap((message) => {
|
|
3555
4865
|
if (!message)
|
|
3556
4866
|
return;
|
|
@@ -3558,6 +4868,8 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3558
4868
|
const currentMessages = this.messages.value;
|
|
3559
4869
|
this.messages.next([...currentMessages, message]);
|
|
3560
4870
|
console.log('Received:', message);
|
|
4871
|
+
// Debug: Log all message types
|
|
4872
|
+
console.log('📨 Message type:', message.type);
|
|
3561
4873
|
if (message.error === 'JWT_INVALID') {
|
|
3562
4874
|
this.shouldRetry = false;
|
|
3563
4875
|
this.httpManagerService.disconnect();
|
|
@@ -3571,16 +4883,22 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3571
4883
|
switch (message.type) {
|
|
3572
4884
|
case 'channelsList':
|
|
3573
4885
|
console.log('💬 Channels:', message.channels);
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
//
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
4886
|
+
console.log('🔍 channelsList received, checking connection status...');
|
|
4887
|
+
console.log('🔍 WebSocket connected:', WebSocketManagerService.isConnected());
|
|
4888
|
+
// Auto-subscribe to all channels from the list
|
|
4889
|
+
if (message.channels && message.channels.length > 0) {
|
|
4890
|
+
console.log('📥 Auto-subscribing to', message.channels.length, 'channel(s)');
|
|
4891
|
+
this.subscribeToChannels(message.channels);
|
|
4892
|
+
}
|
|
4893
|
+
else {
|
|
4894
|
+
console.log('⚠️ No channels to subscribe to');
|
|
4895
|
+
}
|
|
3580
4896
|
this.channels.next(message.channels);
|
|
3581
4897
|
break;
|
|
3582
4898
|
case 'subscribed':
|
|
3583
4899
|
console.log(`✅ Subscription confirmed: ${message.channel}`);
|
|
4900
|
+
// Track as subscribed now that server confirmed
|
|
4901
|
+
WebSocketManagerService.addSubscribedChannel(message.channel);
|
|
3584
4902
|
break;
|
|
3585
4903
|
case 'unsubscribed':
|
|
3586
4904
|
console.log(`🔓 Unsubscription confirmed: ${message.channel}`);
|
|
@@ -3588,6 +4906,11 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3588
4906
|
case 'info':
|
|
3589
4907
|
// Already subscribed or other info messages
|
|
3590
4908
|
console.log(`ℹ️ Info: ${message.message}`);
|
|
4909
|
+
// If it's an "Already subscribed" message, treat it as subscription confirmation
|
|
4910
|
+
if (message.message?.includes('Already subscribed')) {
|
|
4911
|
+
console.log(`✅ Treating info as subscription confirmation for: ${message.data}`);
|
|
4912
|
+
WebSocketManagerService.addSubscribedChannel(message.data);
|
|
4913
|
+
}
|
|
3591
4914
|
break;
|
|
3592
4915
|
case 'stateMangerMessage':
|
|
3593
4916
|
// Compare sender's session ID with current user's ID
|
|
@@ -3606,19 +4929,58 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3606
4929
|
break;
|
|
3607
4930
|
case 'channelMessage':
|
|
3608
4931
|
// Handle channel-based messages (from sendChannelMessage)
|
|
3609
|
-
// Structure: { type: 'channelMessage',
|
|
4932
|
+
// Structure: { type: 'channelMessage', messageId, channel, sessionId, content, timestamp }
|
|
3610
4933
|
// Skip messages from self
|
|
3611
|
-
const senderSessionId = message.sessionId?.id;
|
|
4934
|
+
const senderSessionId = message.sessionId?.id || message.sessionId;
|
|
3612
4935
|
if (senderSessionId === this.user.value?.id) {
|
|
4936
|
+
console.log('🔇 Skipping message from self (sessionId match)');
|
|
3613
4937
|
break;
|
|
3614
4938
|
}
|
|
3615
4939
|
console.log('💬 Channel Message received:', message);
|
|
3616
|
-
|
|
4940
|
+
// Determine which channels this message was sent to
|
|
4941
|
+
const messageChannels = message.channel ? [message.channel] : [];
|
|
4942
|
+
// Construct expected channel path (without env prefix)
|
|
4943
|
+
const myPath = this.apiOptions.path || [];
|
|
4944
|
+
const myPathString = myPath.join('/');
|
|
4945
|
+
// Check if any of the message channels CONTAIN our path
|
|
4946
|
+
const isWsUpdateChannel = messageChannels.some((ch) => {
|
|
4947
|
+
// Strip SYS- prefix if present for comparison
|
|
4948
|
+
const cleanChannel = ch.replace('SYS-', '');
|
|
4949
|
+
// Check if channel contains our path (or starts with it)
|
|
4950
|
+
const matches = cleanChannel === myPathString ||
|
|
4951
|
+
cleanChannel.startsWith(myPathString + '/') ||
|
|
4952
|
+
cleanChannel.includes(myPathString);
|
|
4953
|
+
console.log(`🔍 Channel check: ${ch} contains ${myPathString}? ${matches}`);
|
|
4954
|
+
return matches;
|
|
4955
|
+
});
|
|
4956
|
+
// If it's the expected channel, trigger fetchRecord like stateManagerMessage
|
|
4957
|
+
if (isWsUpdateChannel && message.content?.path) {
|
|
4958
|
+
console.log('🔍 Message received on expected channel (path match):', myPathString);
|
|
4959
|
+
console.log('📄 Content:', message.content);
|
|
4960
|
+
console.log('📥 Fetching record for channel:', myPathString);
|
|
4961
|
+
const path = message.content.path;
|
|
4962
|
+
const method = message.content.method || 'UPDATE';
|
|
4963
|
+
this.userAction.next({ sessionId: message.sessionId, content: message.content });
|
|
4964
|
+
this.fetchRecord(RequestOptions.adapt({ path }), method);
|
|
4965
|
+
}
|
|
4966
|
+
else if (message.content) {
|
|
4967
|
+
// Handle message content directly
|
|
4968
|
+
console.log('📄 Processing message content:', message.content);
|
|
3617
4969
|
this.appendMessages(ChannelMessage.adapt({
|
|
3618
4970
|
sessionId: message.sessionId,
|
|
3619
4971
|
content: message.content,
|
|
3620
4972
|
}));
|
|
3621
4973
|
}
|
|
4974
|
+
else {
|
|
4975
|
+
console.log('⚠️ Message does not contain data.content.path, skipping fetchRecord');
|
|
4976
|
+
}
|
|
4977
|
+
// Keep existing functionality for backward compatibility
|
|
4978
|
+
if (message.data?.content && !isWsUpdateChannel) {
|
|
4979
|
+
this.appendMessages(ChannelMessage.adapt({
|
|
4980
|
+
sessionId: message.data.sessionId,
|
|
4981
|
+
content: message.data.content,
|
|
4982
|
+
}));
|
|
4983
|
+
}
|
|
3622
4984
|
break;
|
|
3623
4985
|
case 'usersInChannel':
|
|
3624
4986
|
console.log(`👥 Users in channel "${message.channel}":`, message.data.users);
|
|
@@ -3834,11 +5196,21 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3834
5196
|
// FETCH RECORD
|
|
3835
5197
|
this.fetchRecord = (options, method) => this.effect(() => of(RequestOptions.adapt(options)).pipe(tap(() => console.log('🔄 fetchRecord effect triggered with path:', options?.path, 'method:', method)), switchMap((options) => {
|
|
3836
5198
|
this.streamedResponse = [];
|
|
5199
|
+
// Temporarily update apiOptions.path with the path from the WebSocket message
|
|
5200
|
+
// This ensures the request goes to the correct endpoint
|
|
5201
|
+
const originalPath = this.apiOptions.path;
|
|
5202
|
+
if (options?.path && Array.isArray(options.path)) {
|
|
5203
|
+
this.apiOptions.path = options.path;
|
|
5204
|
+
console.log('🔧 Temporarily set apiOptions.path to:', options.path);
|
|
5205
|
+
}
|
|
3837
5206
|
const requestOptions = this.updateRequestOptions(options?.headers);
|
|
3838
|
-
console.log('🌐 Making GET request to path:',
|
|
3839
|
-
return this.httpManagerService.getRequest(requestOptions
|
|
5207
|
+
console.log('🌐 Making GET request to path:', this.apiOptions.path);
|
|
5208
|
+
return this.httpManagerService.getRequest(requestOptions)
|
|
3840
5209
|
.pipe(tap((data) => {
|
|
3841
5210
|
console.log('📦 fetchRecord received data:', data);
|
|
5211
|
+
// Restore original path after request completes
|
|
5212
|
+
this.apiOptions.path = originalPath;
|
|
5213
|
+
console.log('🔧 Restored apiOptions.path to:', originalPath);
|
|
3842
5214
|
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
|
|
3843
5215
|
const id = options.path?.length ? options.path[options.path.length - 1] : null;
|
|
3844
5216
|
if (method === 'DELETE') {
|
|
@@ -3860,8 +5232,16 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3860
5232
|
return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, id);
|
|
3861
5233
|
if (method === 'UPDATE' && data)
|
|
3862
5234
|
return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
|
|
3863
|
-
if (method === 'CREATE' && data)
|
|
3864
|
-
|
|
5235
|
+
if (method === 'CREATE' && data) {
|
|
5236
|
+
// Validate that data has a valid id before saving to IndexedDB
|
|
5237
|
+
if (data && (data.id !== undefined && data.id !== null && data.id !== '')) {
|
|
5238
|
+
console.log('💾 Saving to IndexedDB:', { table: this.databaseOptions.table, id: data.id, data });
|
|
5239
|
+
return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
|
|
5240
|
+
}
|
|
5241
|
+
else {
|
|
5242
|
+
console.warn('⚠️ Skipping IndexedDB save: data.id is invalid', data);
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
3865
5245
|
}
|
|
3866
5246
|
return of(data);
|
|
3867
5247
|
}));
|
|
@@ -3874,8 +5254,8 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3874
5254
|
.pipe(tap((data) => {
|
|
3875
5255
|
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
|
|
3876
5256
|
this.addData$(data);
|
|
3877
|
-
if
|
|
3878
|
-
|
|
5257
|
+
// Always call wsCommunication - it will queue if not connected
|
|
5258
|
+
this.wsCommunication('CREATE', [...options?.path || [], data.id]);
|
|
3879
5259
|
}), concatMap((data) => {
|
|
3880
5260
|
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
3881
5261
|
return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
|
|
@@ -3891,8 +5271,8 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3891
5271
|
.pipe(tap((data) => {
|
|
3892
5272
|
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
|
|
3893
5273
|
this.updateData$(data);
|
|
3894
|
-
if
|
|
3895
|
-
|
|
5274
|
+
// Always call wsCommunication - it will queue if not connected
|
|
5275
|
+
this.wsCommunication('UPDATE', [...options?.path || []]);
|
|
3896
5276
|
}), concatMap((data) => {
|
|
3897
5277
|
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
3898
5278
|
return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
|
|
@@ -3908,8 +5288,8 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3908
5288
|
.pipe(tap((data) => {
|
|
3909
5289
|
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
|
|
3910
5290
|
this.deleteData$(data);
|
|
3911
|
-
if
|
|
3912
|
-
|
|
5291
|
+
// Always call wsCommunication - it will queue if not connected
|
|
5292
|
+
this.wsCommunication('DELETE', [...options?.path || []]);
|
|
3913
5293
|
}), concatMap((data) => {
|
|
3914
5294
|
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
3915
5295
|
return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, data.id);
|
|
@@ -3968,23 +5348,34 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3968
5348
|
return of([]);
|
|
3969
5349
|
}));
|
|
3970
5350
|
})));
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
this.
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
5351
|
+
try {
|
|
5352
|
+
this.databaseOptions = database;
|
|
5353
|
+
this.maxRetries = this.apiOptions.ws?.retry?.times || 3;
|
|
5354
|
+
this.retryDelay = (this.apiOptions.ws?.retry?.delay && this.apiOptions.ws.retry.delay * 1000) || 5 * 1000;
|
|
5355
|
+
// Start next retry countdown at 0 to avoid showing 5000 pre-connection
|
|
5356
|
+
this.wsNextRetry = new BehaviorSubject(0);
|
|
5357
|
+
this.wsNextRetry$ = this.wsNextRetry.asObservable();
|
|
5358
|
+
this.setApiRequestOptions(apiOptions, dataType, database);
|
|
5359
|
+
if (this.databaseOptions && this.databaseOptions.table) {
|
|
5360
|
+
this.localStorageManagerService.createStore({
|
|
5361
|
+
name: this.databaseOptions.table,
|
|
5362
|
+
data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions.expiresIn) } },
|
|
5363
|
+
options: SettingOptions.adapt({
|
|
5364
|
+
storage: StorageType.GLOBAL,
|
|
5365
|
+
encrypted: false,
|
|
5366
|
+
})
|
|
5367
|
+
});
|
|
5368
|
+
this.initDBStorage();
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
catch (error) {
|
|
5372
|
+
console.error('Error initializing HTTPManagerStateService:', error);
|
|
5373
|
+
// Initialize with safe defaults
|
|
5374
|
+
this.databaseOptions = undefined;
|
|
5375
|
+
this.maxRetries = 3;
|
|
5376
|
+
this.retryDelay = 5000;
|
|
5377
|
+
this.wsNextRetry = new BehaviorSubject(0);
|
|
5378
|
+
this.wsNextRetry$ = this.wsNextRetry.asObservable();
|
|
3988
5379
|
}
|
|
3989
5380
|
}
|
|
3990
5381
|
/**
|
|
@@ -4041,8 +5432,13 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4041
5432
|
console.error('WSOptions invalid: wsServer is missing or empty');
|
|
4042
5433
|
return;
|
|
4043
5434
|
}
|
|
5435
|
+
// Clean up previous subscription to prevent duplicate handlers
|
|
5436
|
+
if (this.connectionStatusSubscription) {
|
|
5437
|
+
this.connectionStatusSubscription.unsubscribe();
|
|
5438
|
+
this.connectionStatusSubscription = undefined;
|
|
5439
|
+
}
|
|
4044
5440
|
// Setup connection status monitoring (internal subscription to drive retry counters)
|
|
4045
|
-
this.setupConnectionStatus().subscribe();
|
|
5441
|
+
this.connectionStatusSubscription = this.setupConnectionStatus().subscribe();
|
|
4046
5442
|
// Make initial connection attempt
|
|
4047
5443
|
console.log('🔄 Initial WebSocket connection attempt...');
|
|
4048
5444
|
this.httpManagerService.connect(this.apiOptions.ws, this.apiOptions.ws.jwtToken || '');
|
|
@@ -4174,10 +5570,46 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4174
5570
|
}
|
|
4175
5571
|
// WEBSOCKET COMMUNICATION (STATE MANAGER)
|
|
4176
5572
|
wsCommunication(method, path) {
|
|
4177
|
-
if (this.
|
|
5573
|
+
if (!this.apiOptions.ws) {
|
|
5574
|
+
console.warn('wsCommunication called but no WebSocket options configured');
|
|
5575
|
+
return;
|
|
5576
|
+
}
|
|
5577
|
+
// If connected, send immediately (check singleton WebSocketManagerService)
|
|
5578
|
+
if (WebSocketManagerService.isConnected()) {
|
|
5579
|
+
this.sendWsCommunication(method, path);
|
|
5580
|
+
}
|
|
5581
|
+
else {
|
|
5582
|
+
// Queue the message to be sent when connection is established
|
|
5583
|
+
console.log(`⏳ Queuing WS message (not connected): ${method} ${path ? JSON.stringify(path) : ''}`);
|
|
5584
|
+
HTTPManagerStateService.wsCommunicationQueue.push({ method, path });
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
/**
|
|
5588
|
+
* Actually send the WebSocket message (called when connected or from queue)
|
|
5589
|
+
*/
|
|
5590
|
+
sendWsCommunication(method, path) {
|
|
5591
|
+
if (this.apiOptions.ws) {
|
|
4178
5592
|
const wsServer = this.apiOptions.ws.id;
|
|
5593
|
+
// Guard: Don't send if channel is empty
|
|
5594
|
+
if (!wsServer || wsServer === '') {
|
|
5595
|
+
console.error('❌ Cannot send WS message: Channel ID is empty!');
|
|
5596
|
+
return;
|
|
5597
|
+
}
|
|
5598
|
+
// DEBUG: Log what we're sending
|
|
5599
|
+
console.log('🔍 [DEBUG] sendWsCommunication called:', {
|
|
5600
|
+
wsServer,
|
|
5601
|
+
wsServerType: typeof wsServer,
|
|
5602
|
+
wsServerEmpty: wsServer === '' || wsServer === null || wsServer === undefined,
|
|
5603
|
+
method,
|
|
5604
|
+
path,
|
|
5605
|
+
user: this.apiOptions.ws.user,
|
|
5606
|
+
fullPayload: { method, path, user: this.apiOptions.ws.user }
|
|
5607
|
+
});
|
|
4179
5608
|
this.httpManagerService.sendMessageInChannel(wsServer, { method, path, user: this.apiOptions.ws.user });
|
|
4180
5609
|
}
|
|
5610
|
+
else {
|
|
5611
|
+
console.error('❌ [DEBUG] sendWsCommunication: apiOptions.ws is undefined!');
|
|
5612
|
+
}
|
|
4181
5613
|
}
|
|
4182
5614
|
/**
|
|
4183
5615
|
* Send a message to channel(s)
|
|
@@ -4189,19 +5621,18 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4189
5621
|
const user = this.user.value;
|
|
4190
5622
|
const messageInfo = ChannelMessage.adapt({ ...message, fromUser: user });
|
|
4191
5623
|
console.log('📤 wsMessaging called with channels:', channels);
|
|
4192
|
-
if (
|
|
5624
|
+
if (WebSocketManagerService.isConnected() && this.apiOptions.ws) {
|
|
4193
5625
|
// If specific channels provided, send to each channel
|
|
4194
5626
|
// Channels are passed as-is - caller is responsible for including the correct prefix
|
|
4195
5627
|
if (channels && channels.length > 0) {
|
|
4196
5628
|
console.log(`📤 Sending to ${channels.length} channel(s):`, channels);
|
|
4197
|
-
channels
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
});
|
|
5629
|
+
// Send single message with all channels (more efficient)
|
|
5630
|
+
const user = this.user.value;
|
|
5631
|
+
const messageData = {
|
|
5632
|
+
sessionId: user,
|
|
5633
|
+
content: messageInfo
|
|
5634
|
+
};
|
|
5635
|
+
this.httpManagerService.sendChannelMessageToChannels(channels, messageData);
|
|
4205
5636
|
}
|
|
4206
5637
|
else {
|
|
4207
5638
|
// Fallback to the primary WS channel (already prefixed with SYS-)
|
|
@@ -4216,7 +5647,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4216
5647
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4217
5648
|
*/
|
|
4218
5649
|
subscribeToMessageChannel(channel) {
|
|
4219
|
-
if (
|
|
5650
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4220
5651
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
|
|
4221
5652
|
this.httpManagerService.subscribeToChannel(prefixedChannel);
|
|
4222
5653
|
console.log(`💬 Subscribed to message channel: ${prefixedChannel}`);
|
|
@@ -4230,7 +5661,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4230
5661
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4231
5662
|
*/
|
|
4232
5663
|
unsubscribeFromMessageChannel(channel) {
|
|
4233
|
-
if (
|
|
5664
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4234
5665
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
|
|
4235
5666
|
this.httpManagerService.unsubscribeFromChannel(prefixedChannel);
|
|
4236
5667
|
console.log(`💬 Unsubscribed from message channel: ${prefixedChannel}`);
|
|
@@ -4246,8 +5677,11 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4246
5677
|
* Use subscribeToMessageChannel() for MES- prefixed channels
|
|
4247
5678
|
*/
|
|
4248
5679
|
subscribeToChannel(channel) {
|
|
4249
|
-
if (
|
|
4250
|
-
|
|
5680
|
+
if (WebSocketManagerService.isConnected()) {
|
|
5681
|
+
// Get current user data to send with subscription
|
|
5682
|
+
const currentUser = this.user.value;
|
|
5683
|
+
console.log('👤 Subscribing with user:', currentUser);
|
|
5684
|
+
this.httpManagerService.subscribeToChannel(channel, currentUser);
|
|
4251
5685
|
}
|
|
4252
5686
|
else {
|
|
4253
5687
|
console.warn('Cannot subscribe: WebSocket not connected.');
|
|
@@ -4257,8 +5691,11 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4257
5691
|
* Subscribe to multiple channels at once
|
|
4258
5692
|
*/
|
|
4259
5693
|
subscribeToChannels(channels) {
|
|
4260
|
-
if (
|
|
4261
|
-
|
|
5694
|
+
if (WebSocketManagerService.isConnected()) {
|
|
5695
|
+
// Get current user data to send with subscription
|
|
5696
|
+
const currentUser = this.user.value;
|
|
5697
|
+
console.log('👤 Subscribing to', channels.length, 'channels with user:', currentUser);
|
|
5698
|
+
this.httpManagerService.subscribeToChannels(channels, currentUser);
|
|
4262
5699
|
}
|
|
4263
5700
|
else {
|
|
4264
5701
|
console.warn('Cannot subscribe: WebSocket not connected.');
|
|
@@ -4268,7 +5705,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4268
5705
|
* Unsubscribe from a channel
|
|
4269
5706
|
*/
|
|
4270
5707
|
unsubscribeFromChannel(channel) {
|
|
4271
|
-
if (
|
|
5708
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4272
5709
|
this.httpManagerService.unsubscribeFromChannel(channel);
|
|
4273
5710
|
}
|
|
4274
5711
|
else {
|
|
@@ -4291,7 +5728,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4291
5728
|
* Create a new channel on the server
|
|
4292
5729
|
*/
|
|
4293
5730
|
createChannel(channel) {
|
|
4294
|
-
if (
|
|
5731
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4295
5732
|
this.httpManagerService.createChannel(channel);
|
|
4296
5733
|
}
|
|
4297
5734
|
else {
|
|
@@ -4302,7 +5739,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4302
5739
|
* Delete a channel from the server
|
|
4303
5740
|
*/
|
|
4304
5741
|
deleteChannel(channel) {
|
|
4305
|
-
if (
|
|
5742
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4306
5743
|
this.httpManagerService.deleteChannel(channel);
|
|
4307
5744
|
}
|
|
4308
5745
|
else {
|
|
@@ -4313,7 +5750,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4313
5750
|
* Request list of all channels from server
|
|
4314
5751
|
*/
|
|
4315
5752
|
getAllChannels() {
|
|
4316
|
-
if (
|
|
5753
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4317
5754
|
this.httpManagerService.getAllChannels();
|
|
4318
5755
|
}
|
|
4319
5756
|
else {
|
|
@@ -4324,7 +5761,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4324
5761
|
* Get users in a specific channel
|
|
4325
5762
|
*/
|
|
4326
5763
|
getUsersInChannel(channel) {
|
|
4327
|
-
if (
|
|
5764
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4328
5765
|
this.httpManagerService.getUsersInChannel(channel);
|
|
4329
5766
|
}
|
|
4330
5767
|
else {
|
|
@@ -4338,7 +5775,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4338
5775
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4339
5776
|
*/
|
|
4340
5777
|
createNotificationChannel(channel) {
|
|
4341
|
-
if (
|
|
5778
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4342
5779
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
4343
5780
|
this.httpManagerService.createNotificationChannel(prefixedChannel);
|
|
4344
5781
|
console.log(`📢 Creating notification channel: ${prefixedChannel}`);
|
|
@@ -4351,7 +5788,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4351
5788
|
* Request list of all notification channels from server (in-memory)
|
|
4352
5789
|
*/
|
|
4353
5790
|
getNotificationChannels() {
|
|
4354
|
-
if (
|
|
5791
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4355
5792
|
this.httpManagerService.getNotificationChannels();
|
|
4356
5793
|
}
|
|
4357
5794
|
else {
|
|
@@ -4363,7 +5800,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4363
5800
|
* Returns unique channels that have notifications posted today
|
|
4364
5801
|
*/
|
|
4365
5802
|
getTodaysNotificationChannels() {
|
|
4366
|
-
if (
|
|
5803
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4367
5804
|
this.httpManagerService.getTodaysNotificationChannels();
|
|
4368
5805
|
}
|
|
4369
5806
|
else {
|
|
@@ -4375,7 +5812,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4375
5812
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4376
5813
|
*/
|
|
4377
5814
|
subscribeToNotificationChannel(channel, options, user) {
|
|
4378
|
-
if (
|
|
5815
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4379
5816
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
4380
5817
|
this.httpManagerService.subscribeToNotificationChannel(prefixedChannel, options, user);
|
|
4381
5818
|
console.log(`📢 Subscribing to notification channel: ${prefixedChannel}`);
|
|
@@ -4389,7 +5826,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4389
5826
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4390
5827
|
*/
|
|
4391
5828
|
unsubscribeFromNotificationChannel(channel) {
|
|
4392
|
-
if (
|
|
5829
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4393
5830
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
4394
5831
|
this.httpManagerService.unsubscribeFromNotificationChannel(prefixedChannel);
|
|
4395
5832
|
console.log(`📢 Unsubscribing from notification channel: ${prefixedChannel}`);
|
|
@@ -4403,7 +5840,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4403
5840
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4404
5841
|
*/
|
|
4405
5842
|
sendNotification(channel, content) {
|
|
4406
|
-
if (
|
|
5843
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4407
5844
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
4408
5845
|
this.httpManagerService.sendNotification(prefixedChannel, content);
|
|
4409
5846
|
console.log(`📢 Sending notification to channel: ${prefixedChannel}`);
|
|
@@ -5822,17 +7259,25 @@ class StateServiceDemo extends HTTPManagerStateService {
|
|
|
5822
7259
|
}
|
|
5823
7260
|
/**
|
|
5824
7261
|
* Initialize WebSocket connection with server configuration
|
|
7262
|
+
* @param server - Backend server URL
|
|
7263
|
+
* @param wsServer - WebSocket server URL
|
|
7264
|
+
* @param jwtToken - JWT authentication token
|
|
7265
|
+
* @param user - User information
|
|
7266
|
+
* @param path - Path for constructing channel name (e.g., ['ai','tests'])
|
|
5825
7267
|
*/
|
|
5826
|
-
updateConnection(server, wsServer, jwtToken, user) {
|
|
7268
|
+
updateConnection(server, wsServer, jwtToken, user, path = ['ai', 'tests']) {
|
|
7269
|
+
// Construct channel name from path: ['ai','tests'] → 'ai/tests'
|
|
7270
|
+
const channelId = path.join('/');
|
|
5827
7271
|
this.setApiRequestOptions({
|
|
5828
7272
|
server,
|
|
7273
|
+
path, // Set the path for HTTP requests
|
|
5829
7274
|
retry: RetryOptions.adapt({
|
|
5830
7275
|
times: 3,
|
|
5831
7276
|
delay: 1,
|
|
5832
7277
|
}),
|
|
5833
7278
|
adapter: OIDCClient.adapt,
|
|
5834
7279
|
ws: {
|
|
5835
|
-
id: 'USERS123'
|
|
7280
|
+
id: channelId, // Use path-based channel ID instead of hardcoded 'USERS123'
|
|
5836
7281
|
wsServer,
|
|
5837
7282
|
jwtToken,
|
|
5838
7283
|
user,
|
|
@@ -6157,7 +7602,7 @@ class StateDataRequestService extends HTTPManagerStateService {
|
|
|
6157
7602
|
this.nextRetry$ = this.wsNextRetry$;
|
|
6158
7603
|
this.path = ['ai', 'tests'];
|
|
6159
7604
|
}
|
|
6160
|
-
updateConnection(server, wsServer, jwtToken, user, path = []
|
|
7605
|
+
updateConnection(server, wsServer, jwtToken, user, path = []) {
|
|
6161
7606
|
this.path = path;
|
|
6162
7607
|
this.setApiRequestOptions({
|
|
6163
7608
|
server,
|
|
@@ -6167,7 +7612,7 @@ class StateDataRequestService extends HTTPManagerStateService {
|
|
|
6167
7612
|
}),
|
|
6168
7613
|
adapter: OIDCClient.adapt,
|
|
6169
7614
|
ws: {
|
|
6170
|
-
id:
|
|
7615
|
+
id: this.path.join('/'),
|
|
6171
7616
|
wsServer,
|
|
6172
7617
|
jwtToken,
|
|
6173
7618
|
user, // general info about user
|
|
@@ -6218,7 +7663,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
6218
7663
|
class WsDataControlComponent {
|
|
6219
7664
|
constructor() {
|
|
6220
7665
|
this.path = ['ai', 'tests'];
|
|
6221
|
-
this.wsChannel = '';
|
|
6222
7666
|
this.stateDataRequestService = inject(StateDataRequestService);
|
|
6223
7667
|
this.user$ = this.stateDataRequestService.user$;
|
|
6224
7668
|
this.users$ = this.stateDataRequestService.userList$;
|
|
@@ -6230,7 +7674,7 @@ class WsDataControlComponent {
|
|
|
6230
7674
|
};
|
|
6231
7675
|
}
|
|
6232
7676
|
ngOnInit() {
|
|
6233
|
-
this.stateDataRequestService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path
|
|
7677
|
+
this.stateDataRequestService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
|
|
6234
7678
|
this.stateDataRequestService.getData();
|
|
6235
7679
|
}
|
|
6236
7680
|
onGetData() {
|
|
@@ -6255,11 +7699,11 @@ class WsDataControlComponent {
|
|
|
6255
7699
|
this.stateDataRequestService.deleteData(lastRec);
|
|
6256
7700
|
}
|
|
6257
7701
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsDataControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6258
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsDataControlComponent, selector: "app-ws-data-control", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path"
|
|
7702
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsDataControlComponent, selector: "app-ws-data-control", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path" }, ngImport: i0, template: "@if ((data$ | async); as data) {\n <div style=\"margin: 1rem;\">\n @if ((users$ |async); as users) {\n <div>\n @if (users.length > 0) {\n <h3 style=\"margin: 0;\">Connected Users</h3>\n } @else {\n <h3 style=\"margin: 0;\">No Users</h3>\n }\n <mat-chip-set>\n @for (user of users; track $index) {\n <mat-chip\n [class.user-chip--primary]=\"isUser(user, (user$ | async))\"\n [style.color]=\"isUser(user, (user$ | async)) ? '#fff' : null\"\n [disableRipple]=\"true\"\n >\n {{ user.name || user.ldap || user.id || 'Anonymous' }}\n </mat-chip>\n }\n </mat-chip-set>\n </div>\n }\n <div style=\"margin-top: 1rem; margin-bottom: 1rem;\">\n <mat-divider></mat-divider>\n </div>\n\n <div class=\"box\" style=\"margin-bottom: 1rem;\" *ngIf=\"(userAction$ | async) as userAction\">\n {{ userAction?.content?.user?.name }} has {{ userAction?.content?.method }}\n </div>\n\n <h3 style=\"margin: 0;\">Data Actions</h3>\n <div style=\"display: flex; gap: 1rem; margin-bottom: 1rem;\">\n <button mat-stroked-button (click)=\"onGetData()\">Get Data</button>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button color=\"accent\" (click)=\"onUpdateData(data)\">Update Data</button>\n <button mat-stroked-button color=\"warn\" (click)=\"onRemoveData(data)\">Remove Data</button>\n <button mat-stroked-button color=\"primary\" (click)=\"onAddData()\">Add Data</button>\n </div>\n @if (data.length > 0) {\n <div>\n <table mat-table [dataSource]=\"data\" style=\"border: 1px solid grey;\">\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef> ID </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.id}} </td>\n </ng-container>\n <ng-container matColumnDef=\"spiffe\">\n <th mat-header-cell *matHeaderCellDef> Spiffe </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.spiffe}} </td>\n </ng-container>\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef> Name </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.first_name}} {{element.last_name}}</td>\n </ng-container>\n <ng-container matColumnDef=\"email\">\n <th mat-header-cell *matHeaderCellDef> Email </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.email}} </td>\n </ng-container>\n <tr mat-header-row *matHeaderRowDef=\"['id', 'spiffe', 'name', 'email']\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: ['id', 'spiffe', 'name', 'email'];\"></tr>\n </table>\n <div style=\"border: 1px solid grey; padding: .5rem; border-top: none;\">\n <h3 style=\"margin: 0;\">Total Records {{ data.length }}</h3>\n </div>\n </div>\n } @else {\n <div style=\"margin-top: 1rem; font-style: italic;\">\n No Data Available\n </div>\n }\n </div>\n}\n\n", styles: [".user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}.user-chip--primary :is(.mdc-evolution-chip__text-label,.mdc-evolution-chip__action,.mdc-evolution-chip__cell,.mat-mdc-chip-action-label){color:#fff!important}.user-chip--primary,.user-chip--primary *{color:#fff!important}:host ::ng-deep .user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}:host ::ng-deep .user-chip--primary .mdc-evolution-chip__text-label,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__action,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__cell,:host ::ng-deep .user-chip--primary .mat-mdc-chip-action-label,:host ::ng-deep .user-chip--primary *{color:#fff!important}.box{padding:.5rem;border:1px solid rgb(174,174,13);background-color:#ececaf}\n"], dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$1.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "component", type: i3$1.MatChipSet, selector: "mat-chip-set", inputs: ["disabled", "role", "tabIndex"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
|
|
6259
7703
|
}
|
|
6260
7704
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsDataControlComponent, decorators: [{
|
|
6261
7705
|
type: Component,
|
|
6262
|
-
args: [{ selector: 'app-ws-data-control', standalone: false, template: "@if ((data$ | async); as data) {\n <div style=\"margin: 1rem;\">\n @if ((users$ |async); as users) {\n <div>\n @if (users.length > 0) {\n <h3 style=\"margin: 0;\">Connected Users</h3>\n } @else {\n <h3 style=\"margin: 0;\">No Users</h3>\n }\n <mat-chip-set>\n @for (user of users; track
|
|
7706
|
+
args: [{ selector: 'app-ws-data-control', standalone: false, template: "@if ((data$ | async); as data) {\n <div style=\"margin: 1rem;\">\n @if ((users$ |async); as users) {\n <div>\n @if (users.length > 0) {\n <h3 style=\"margin: 0;\">Connected Users</h3>\n } @else {\n <h3 style=\"margin: 0;\">No Users</h3>\n }\n <mat-chip-set>\n @for (user of users; track $index) {\n <mat-chip\n [class.user-chip--primary]=\"isUser(user, (user$ | async))\"\n [style.color]=\"isUser(user, (user$ | async)) ? '#fff' : null\"\n [disableRipple]=\"true\"\n >\n {{ user.name || user.ldap || user.id || 'Anonymous' }}\n </mat-chip>\n }\n </mat-chip-set>\n </div>\n }\n <div style=\"margin-top: 1rem; margin-bottom: 1rem;\">\n <mat-divider></mat-divider>\n </div>\n\n <div class=\"box\" style=\"margin-bottom: 1rem;\" *ngIf=\"(userAction$ | async) as userAction\">\n {{ userAction?.content?.user?.name }} has {{ userAction?.content?.method }}\n </div>\n\n <h3 style=\"margin: 0;\">Data Actions</h3>\n <div style=\"display: flex; gap: 1rem; margin-bottom: 1rem;\">\n <button mat-stroked-button (click)=\"onGetData()\">Get Data</button>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button color=\"accent\" (click)=\"onUpdateData(data)\">Update Data</button>\n <button mat-stroked-button color=\"warn\" (click)=\"onRemoveData(data)\">Remove Data</button>\n <button mat-stroked-button color=\"primary\" (click)=\"onAddData()\">Add Data</button>\n </div>\n @if (data.length > 0) {\n <div>\n <table mat-table [dataSource]=\"data\" style=\"border: 1px solid grey;\">\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef> ID </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.id}} </td>\n </ng-container>\n <ng-container matColumnDef=\"spiffe\">\n <th mat-header-cell *matHeaderCellDef> Spiffe </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.spiffe}} </td>\n </ng-container>\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef> Name </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.first_name}} {{element.last_name}}</td>\n </ng-container>\n <ng-container matColumnDef=\"email\">\n <th mat-header-cell *matHeaderCellDef> Email </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.email}} </td>\n </ng-container>\n <tr mat-header-row *matHeaderRowDef=\"['id', 'spiffe', 'name', 'email']\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: ['id', 'spiffe', 'name', 'email'];\"></tr>\n </table>\n <div style=\"border: 1px solid grey; padding: .5rem; border-top: none;\">\n <h3 style=\"margin: 0;\">Total Records {{ data.length }}</h3>\n </div>\n </div>\n } @else {\n <div style=\"margin-top: 1rem; font-style: italic;\">\n No Data Available\n </div>\n }\n </div>\n}\n\n", styles: [".user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}.user-chip--primary :is(.mdc-evolution-chip__text-label,.mdc-evolution-chip__action,.mdc-evolution-chip__cell,.mat-mdc-chip-action-label){color:#fff!important}.user-chip--primary,.user-chip--primary *{color:#fff!important}:host ::ng-deep .user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}:host ::ng-deep .user-chip--primary .mdc-evolution-chip__text-label,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__action,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__cell,:host ::ng-deep .user-chip--primary .mat-mdc-chip-action-label,:host ::ng-deep .user-chip--primary *{color:#fff!important}.box{padding:.5rem;border:1px solid rgb(174,174,13);background-color:#ececaf}\n"] }]
|
|
6263
7707
|
}], propDecorators: { server: [{
|
|
6264
7708
|
type: Input
|
|
6265
7709
|
}], wsServer: [{
|
|
@@ -6270,12 +7714,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
6270
7714
|
type: Input
|
|
6271
7715
|
}], path: [{
|
|
6272
7716
|
type: Input
|
|
6273
|
-
}], wsChannel: [{
|
|
6274
|
-
type: Input
|
|
6275
7717
|
}] } });
|
|
6276
7718
|
|
|
6277
7719
|
class WsMessagingComponent {
|
|
6278
7720
|
constructor() {
|
|
7721
|
+
this.path = ['ai', 'tests']; // Default path for channel name
|
|
6279
7722
|
this.destroy$ = new Subject();
|
|
6280
7723
|
this.fb = inject(FormBuilder);
|
|
6281
7724
|
this.messageService = inject(MessageServiceDemo);
|
|
@@ -6320,11 +7763,14 @@ class WsMessagingComponent {
|
|
|
6320
7763
|
return this.messages.get('content');
|
|
6321
7764
|
}
|
|
6322
7765
|
ngOnInit() {
|
|
6323
|
-
this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user);
|
|
7766
|
+
this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
|
|
6324
7767
|
// Only trigger once when connection becomes true
|
|
6325
7768
|
this.connectionStatus$.pipe(filter$1(status => status === true), take$1(1), takeUntil$1(this.destroy$)).subscribe(() => {
|
|
6326
|
-
//
|
|
6327
|
-
|
|
7769
|
+
// Wait a moment for subscription to be processed, then fetch channels
|
|
7770
|
+
setTimeout(() => {
|
|
7771
|
+
console.log('📋 Fetching channels after connection...');
|
|
7772
|
+
this.messageService.getAllChannels();
|
|
7773
|
+
}, 500); // 500ms delay to ensure subscription is processed
|
|
6328
7774
|
});
|
|
6329
7775
|
// Subscribe to latest messages and show toast notification
|
|
6330
7776
|
this.latestCommunicationMessages$.pipe(filter$1(message => !!message), takeUntil$1(this.destroy$)).subscribe((message) => {
|
|
@@ -6428,11 +7874,11 @@ class WsMessagingComponent {
|
|
|
6428
7874
|
this.content.reset();
|
|
6429
7875
|
}
|
|
6430
7876
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsMessagingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6431
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsMessagingComponent, selector: "app-ws-messaging", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user" }, ngImport: i0, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"], dependencies: [{ kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "directive", type: i5.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i3$1.MatChipListbox, selector: "mat-chip-listbox", inputs: ["multiple", "aria-orientation", "selectable", "compareWith", "required", "hideSingleSelectionIndicator", "value"], outputs: ["change"] }, { kind: "component", type: i3$1.MatChipOption, selector: "mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]", inputs: ["selectable", "selected"], outputs: ["selectionChange"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
|
|
7877
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsMessagingComponent, selector: "app-ws-messaging", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path" }, ngImport: i0, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel; let i = $index) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel; let i = $index) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"], dependencies: [{ kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "directive", type: i5.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i3$1.MatChipListbox, selector: "mat-chip-listbox", inputs: ["multiple", "aria-orientation", "selectable", "compareWith", "required", "hideSingleSelectionIndicator", "value"], outputs: ["change"] }, { kind: "component", type: i3$1.MatChipOption, selector: "mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]", inputs: ["selectable", "selected"], outputs: ["selectionChange"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
|
|
6432
7878
|
}
|
|
6433
7879
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsMessagingComponent, decorators: [{
|
|
6434
7880
|
type: Component,
|
|
6435
|
-
args: [{ selector: 'app-ws-messaging', standalone: false, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"] }]
|
|
7881
|
+
args: [{ selector: 'app-ws-messaging', standalone: false, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel; let i = $index) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel; let i = $index) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"] }]
|
|
6436
7882
|
}], propDecorators: { server: [{
|
|
6437
7883
|
type: Input
|
|
6438
7884
|
}], wsServer: [{
|
|
@@ -6441,6 +7887,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
6441
7887
|
type: Input
|
|
6442
7888
|
}], user: [{
|
|
6443
7889
|
type: Input
|
|
7890
|
+
}], path: [{
|
|
7891
|
+
type: Input
|
|
6444
7892
|
}] } });
|
|
6445
7893
|
|
|
6446
7894
|
class WsNotificationsComponent {
|
|
@@ -6667,7 +8115,6 @@ class RequestManagerWsDemoComponent {
|
|
|
6667
8115
|
this.stateService = inject(StateServiceDemo);
|
|
6668
8116
|
this.fb = inject(FormBuilder);
|
|
6669
8117
|
this.path = ['ai', 'tests'];
|
|
6670
|
-
this.wsChannel = '';
|
|
6671
8118
|
this.user$ = this.stateService.user$;
|
|
6672
8119
|
this.attempts$ = this.stateService.wsRetryAttempts$;
|
|
6673
8120
|
this.nextRetry$ = this.stateService.wsNextRetry$;
|
|
@@ -6676,14 +8123,14 @@ class RequestManagerWsDemoComponent {
|
|
|
6676
8123
|
this.isPending$ = this.stateService.isPending$;
|
|
6677
8124
|
}
|
|
6678
8125
|
ngOnInit() {
|
|
6679
|
-
this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user);
|
|
8126
|
+
this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
|
|
6680
8127
|
}
|
|
6681
8128
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerWsDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6682
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerWsDemoComponent, selector: "app-request-manager-ws-demo", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path"
|
|
8129
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerWsDemoComponent, selector: "app-request-manager-ws-demo", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path" }, ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2 style=\"display: flex;\">\n <span style=\"flex:1\">HTTP Request State Manager - Websockets</span>\n @if ((connectionStatus$ | async); as connected) {\n <span>\n WS -\n <span style=\"color: green;\">Connected</span>\n </span>\n } @else {\n <span style=\"color: red;\">Disconnected {{ attempts$ | async }} - {{ nextRetry$ |async }}</span>\n }\n </h2>\n\n <div>\n\n @if ((user$ | async); as userInfo) {\n <div>\n <mat-toolbar>\n <div style=\"display: flex; flex:1\">\n <div style=\"flex:1\">{{ userInfo.name }}</div>\n <div>({{ userInfo.ldap }})</div>\n </div>\n </mat-toolbar>\n </div>\n }\n\n @if ((isPending$ | async)) {\n <div>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </div>\n }\n\n <mat-tab-group animationDuration=\"0ms\" [selectedIndex]=\"1\">\n\n <mat-tab label=\"WS - Data Control\">\n <!-- DATA CONTROL -->\n <app-ws-data-control\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-data-control>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Messaging\">\n <!-- MESSAGING -->\n <app-ws-messaging\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-messaging>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Notifications\">\n <!-- WS - Notifications -->\n <app-ws-notifications\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n ></app-ws-notifications>\n </mat-tab>\n\n <mat-tab label=\"WS - Chats\" [disabled]=\"true\">\n <!-- WS - Chats -->\n <app-ws-chats></app-ws-chats>\n </mat-tab>\n\n <mat-tab label=\"WS - AI Messaging\" [disabled]=\"true\">\n <!-- WS - AI Messaging -->\n <app-ws-ai-messaging></app-ws-ai-messaging>\n </mat-tab>\n\n </mat-tab-group>\n</div>\n\n</div>\n\n", styles: [""], dependencies: [{ kind: "component", type: i1$2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i1$2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: i10.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i3$2.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "component", type: WsDataControlComponent, selector: "app-ws-data-control", inputs: ["server", "wsServer", "jwtToken", "user", "path"] }, { kind: "component", type: WsMessagingComponent, selector: "app-ws-messaging", inputs: ["server", "wsServer", "jwtToken", "user", "path"] }, { kind: "component", type: WsNotificationsComponent, selector: "app-ws-notifications", inputs: ["server", "wsServer", "jwtToken", "user"] }, { kind: "component", type: WsAiMessagingComponent, selector: "app-ws-ai-messaging" }, { kind: "component", type: WsChatsComponent, selector: "app-ws-chats" }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
|
|
6683
8130
|
}
|
|
6684
8131
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerWsDemoComponent, decorators: [{
|
|
6685
8132
|
type: Component,
|
|
6686
|
-
args: [{ selector: 'app-request-manager-ws-demo', standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2 style=\"display: flex;\">\n <span style=\"flex:1\">HTTP Request State Manager - Websockets</span>\n @if ((connectionStatus$ | async); as connected) {\n <span>\n WS -\n <span style=\"color: green;\">Connected</span>\n </span>\n } @else {\n <span style=\"color: red;\">Disconnected {{ attempts$ | async }} - {{ nextRetry$ |async }}</span>\n }\n </h2>\n\n <div>\n\n @if ((user$ | async); as userInfo) {\n <div>\n <mat-toolbar>\n <div style=\"display: flex; flex:1\">\n <div style=\"flex:1\">{{ userInfo.name }}</div>\n <div>({{ userInfo.ldap }})</div>\n </div>\n </mat-toolbar>\n </div>\n }\n\n @if ((isPending$ | async)) {\n <div>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </div>\n }\n\n <mat-tab-group animationDuration=\"0ms\" [selectedIndex]=\"1\">\n\n <mat-tab label=\"WS - Data Control\">\n <!-- DATA CONTROL -->\n <app-ws-data-control\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n
|
|
8133
|
+
args: [{ selector: 'app-request-manager-ws-demo', standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2 style=\"display: flex;\">\n <span style=\"flex:1\">HTTP Request State Manager - Websockets</span>\n @if ((connectionStatus$ | async); as connected) {\n <span>\n WS -\n <span style=\"color: green;\">Connected</span>\n </span>\n } @else {\n <span style=\"color: red;\">Disconnected {{ attempts$ | async }} - {{ nextRetry$ |async }}</span>\n }\n </h2>\n\n <div>\n\n @if ((user$ | async); as userInfo) {\n <div>\n <mat-toolbar>\n <div style=\"display: flex; flex:1\">\n <div style=\"flex:1\">{{ userInfo.name }}</div>\n <div>({{ userInfo.ldap }})</div>\n </div>\n </mat-toolbar>\n </div>\n }\n\n @if ((isPending$ | async)) {\n <div>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </div>\n }\n\n <mat-tab-group animationDuration=\"0ms\" [selectedIndex]=\"1\">\n\n <mat-tab label=\"WS - Data Control\">\n <!-- DATA CONTROL -->\n <app-ws-data-control\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-data-control>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Messaging\">\n <!-- MESSAGING -->\n <app-ws-messaging\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-messaging>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Notifications\">\n <!-- WS - Notifications -->\n <app-ws-notifications\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n ></app-ws-notifications>\n </mat-tab>\n\n <mat-tab label=\"WS - Chats\" [disabled]=\"true\">\n <!-- WS - Chats -->\n <app-ws-chats></app-ws-chats>\n </mat-tab>\n\n <mat-tab label=\"WS - AI Messaging\" [disabled]=\"true\">\n <!-- WS - AI Messaging -->\n <app-ws-ai-messaging></app-ws-ai-messaging>\n </mat-tab>\n\n </mat-tab-group>\n</div>\n\n</div>\n\n" }]
|
|
6687
8134
|
}], propDecorators: { server: [{
|
|
6688
8135
|
type: Input
|
|
6689
8136
|
}], wsServer: [{
|
|
@@ -6694,8 +8141,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
6694
8141
|
type: Input
|
|
6695
8142
|
}], path: [{
|
|
6696
8143
|
type: Input
|
|
6697
|
-
}], wsChannel: [{
|
|
6698
|
-
type: Input
|
|
6699
8144
|
}] } });
|
|
6700
8145
|
|
|
6701
8146
|
class Settings {
|
|
@@ -6916,7 +8361,6 @@ class HttpRequestServicesDemoComponent {
|
|
|
6916
8361
|
this.jwtToken = '';
|
|
6917
8362
|
this.server = 'http:';
|
|
6918
8363
|
this.path = ['ai', 'tests'];
|
|
6919
|
-
this.wsChannel = '';
|
|
6920
8364
|
this.requestTypes = [
|
|
6921
8365
|
{ name: "Http Service", value: 'http_service' },
|
|
6922
8366
|
// { name: "Http Signals Service", value: 'http_signals_service', new: true },
|
|
@@ -6937,11 +8381,11 @@ class HttpRequestServicesDemoComponent {
|
|
|
6937
8381
|
this.selectedService = this.requestTypes[type].value;
|
|
6938
8382
|
}
|
|
6939
8383
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HttpRequestServicesDemoComponent, deps: [{ token: CONFIG_SETTINGS_TOKEN }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6940
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: HttpRequestServicesDemoComponent, selector: "app-http-request-services-demo", inputs: { wsServer: "wsServer", jwtToken: "jwtToken", server: "server", user: "user", path: "path",
|
|
8384
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: HttpRequestServicesDemoComponent, selector: "app-http-request-services-demo", inputs: { wsServer: "wsServer", jwtToken: "jwtToken", server: "server", user: "user", path: "path", adapter: "adapter", mapper: "mapper" }, ngImport: i0, template: "<mat-toolbar style=\"display:flex\">\n <div>Http Request Manager Services</div>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button [matMenuTriggerFor]=\"menu\">Services</button>\n <mat-menu #menu=\"matMenu\">\n @for (type of requestTypes; track type; let i = $index) {\n @if (type?.divider) {\n <div\n style=\"margin-top: .5rem; margin-bottom: .5rem;\"\n >\n <mat-divider></mat-divider>\n </div>\n }\n <button\n mat-menu-item\n (click)=\"onSelected(i)\"\n [disabled]=\"type.disabled\"\n >\n {{ type.name }}\n </button>\n }\n\n </mat-menu>\n</mat-toolbar>\n\n<span>\n @switch (selectedService) {\n @case ('http_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'http_signals_service'\">\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-signals-manager-demo></app-request-signals-manager-demo>\n </p> -->\n @case ('http_state_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-state-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-state-demo>\n </p>\n }\n @case ('http_state_service_ws') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-ws-demo\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-request-manager-ws-demo>\n </p>\n }\n @case ('database_service') {\n <p>\n <app-database-data-demo></app-database-data-demo>\n </p>\n }\n @case ('local_storage_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-demo></app-local-storage-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'local_storage_signals_service'\">\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-signals-demo></app-local-storage-signals-demo>\n</p> -->\n@case ('store_state_manager') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-store-state-manager-demo></app-store-state-manager-demo>\n </p>\n}\n@default {\n <p>\n Other\n </p>\n}\n}\n</span>\n\n<ng-template #HTTP_OPTIONS>\n @if (injectionOptions?.httpRequestOptions) {\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - HTTP Options</h3>\n {{ injectionOptions?.httpRequestOptions| json }}\n </div>\n }\n</ng-template>\n\n<ng-template #LOCAL_OPTIONS>\n @if (injectionOptions?.LocalStorageOptions) {\n <ng-container class=\"box\">\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - LocalStorage Options</h3>\n {{ injectionOptions?.LocalStorageOptions| json }}\n </div>\n </ng-container>\n }\n</ng-template>\n\n\n", styles: [".box{padding:1rem;background-color:#f5f5f5;border:thin gray solid;margin-top:1rem}\n"], dependencies: [{ kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "component", type: i3$2.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "component", type: RequestManagerStateDemoComponent, selector: "app-request-manager-state-demo", inputs: ["server", "adapter", "mapper"] }, { kind: "component", type: RequestManagerDemoComponent, selector: "app-request-manager-demo", inputs: ["server", "adapter", "mapper"] }, { kind: "component", type: LocalStorageDemoComponent, selector: "app-local-storage-demo" }, { kind: "component", type: RequestManagerWsDemoComponent, selector: "app-request-manager-ws-demo", inputs: ["server", "wsServer", "jwtToken", "user", "path"] }, { kind: "component", type: StoreStateManagerDemoComponent, selector: "app-store-state-manager-demo" }, { kind: "component", type: DatabaseDataDemoComponent, selector: "app-database-data-demo" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }] }); }
|
|
6941
8385
|
}
|
|
6942
8386
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HttpRequestServicesDemoComponent, decorators: [{
|
|
6943
8387
|
type: Component,
|
|
6944
|
-
args: [{ selector: 'app-http-request-services-demo', standalone: false, template: "<mat-toolbar style=\"display:flex\">\n <div>Http Request Manager Services</div>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button [matMenuTriggerFor]=\"menu\">Services</button>\n <mat-menu #menu=\"matMenu\">\n @for (type of requestTypes; track type; let i = $index) {\n @if (type?.divider) {\n <div\n style=\"margin-top: .5rem; margin-bottom: .5rem;\"\n >\n <mat-divider></mat-divider>\n </div>\n }\n <button\n mat-menu-item\n (click)=\"onSelected(i)\"\n [disabled]=\"type.disabled\"\n >\n {{ type.name }}\n </button>\n }\n\n </mat-menu>\n</mat-toolbar>\n\n<span>\n @switch (selectedService) {\n @case ('http_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'http_signals_service'\">\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-signals-manager-demo></app-request-signals-manager-demo>\n </p> -->\n @case ('http_state_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-state-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-state-demo>\n </p>\n }\n @case ('http_state_service_ws') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-ws-demo\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [
|
|
8388
|
+
args: [{ selector: 'app-http-request-services-demo', standalone: false, template: "<mat-toolbar style=\"display:flex\">\n <div>Http Request Manager Services</div>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button [matMenuTriggerFor]=\"menu\">Services</button>\n <mat-menu #menu=\"matMenu\">\n @for (type of requestTypes; track type; let i = $index) {\n @if (type?.divider) {\n <div\n style=\"margin-top: .5rem; margin-bottom: .5rem;\"\n >\n <mat-divider></mat-divider>\n </div>\n }\n <button\n mat-menu-item\n (click)=\"onSelected(i)\"\n [disabled]=\"type.disabled\"\n >\n {{ type.name }}\n </button>\n }\n\n </mat-menu>\n</mat-toolbar>\n\n<span>\n @switch (selectedService) {\n @case ('http_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'http_signals_service'\">\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-signals-manager-demo></app-request-signals-manager-demo>\n </p> -->\n @case ('http_state_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-state-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-state-demo>\n </p>\n }\n @case ('http_state_service_ws') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-ws-demo\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-request-manager-ws-demo>\n </p>\n }\n @case ('database_service') {\n <p>\n <app-database-data-demo></app-database-data-demo>\n </p>\n }\n @case ('local_storage_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-demo></app-local-storage-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'local_storage_signals_service'\">\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-signals-demo></app-local-storage-signals-demo>\n</p> -->\n@case ('store_state_manager') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-store-state-manager-demo></app-store-state-manager-demo>\n </p>\n}\n@default {\n <p>\n Other\n </p>\n}\n}\n</span>\n\n<ng-template #HTTP_OPTIONS>\n @if (injectionOptions?.httpRequestOptions) {\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - HTTP Options</h3>\n {{ injectionOptions?.httpRequestOptions| json }}\n </div>\n }\n</ng-template>\n\n<ng-template #LOCAL_OPTIONS>\n @if (injectionOptions?.LocalStorageOptions) {\n <ng-container class=\"box\">\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - LocalStorage Options</h3>\n {{ injectionOptions?.LocalStorageOptions| json }}\n </div>\n </ng-container>\n }\n</ng-template>\n\n\n", styles: [".box{padding:1rem;background-color:#f5f5f5;border:thin gray solid;margin-top:1rem}\n"] }]
|
|
6945
8389
|
}], ctorParameters: () => [{ type: ConfigOptions, decorators: [{
|
|
6946
8390
|
type: Inject,
|
|
6947
8391
|
args: [CONFIG_SETTINGS_TOKEN]
|
|
@@ -6955,8 +8399,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
6955
8399
|
type: Input
|
|
6956
8400
|
}], path: [{
|
|
6957
8401
|
type: Input
|
|
6958
|
-
}], wsChannel: [{
|
|
6959
|
-
type: Input
|
|
6960
8402
|
}], adapter: [{
|
|
6961
8403
|
type: Input
|
|
6962
8404
|
}], mapper: [{
|
|
@@ -7628,5 +9070,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
7628
9070
|
* Generated bundle index. Do not edit.
|
|
7629
9071
|
*/
|
|
7630
9072
|
|
|
7631
|
-
export { ApiRequest, AppService, AsymmetricalEncryptionService, CONFIG_SETTINGS_TOKEN, ChannelInfo, ChannelType, CommunicationType, ConfigHTTPOptions, ConfigOptions, DataType, DatabaseDataDemoComponent, DatabaseManagerService, DatabaseStorage, DbService, ErrorDisplaySettings, GlobalStoreOptions, HTTPManagerService, HTTPManagerSignalsService, HTTPManagerStateService, HeadersService, HttpRequestManagerModule, HttpRequestServicesDemoComponent, LocalStorageDemoComponent, LocalStorageManagerService, LocalStorageOptions, LocalStorageSignalsManagerService, PathQueryService, Random, RandomHSLColor, RandomHexColor, RandomNumber, RandomNumbers, RandomNumbersUnique, RandomPaletteColor, RandomSignature, RandomStr, RandomVisibleColor, RequestErrorInterceptor, RequestHeadersInterceptor, RequestManagerDemoComponent, RequestManagerStateDemoComponent, RequestOptions, RequestService, RequestSignalsService, RetryOptions, SettingOptions, StateStorageOptions, StorageData, StorageOption, StorageType, StoreStateManagerService, StreamType, SymmetricalEncryptionService, TableSchemaDef, UUID, UUID_STR, UserData, UtilsService, WSOptions, WSUser, WebsocketService, WithCredentialsInterceptor, countdown, createChannelName, delayedRetry, requestPolling, requestStreaming, streamAI, streamAuto, streamEvents, streamJSON, streamNDJSON };
|
|
9073
|
+
export { ApiRequest, AppService, AsymmetricalEncryptionService, CONFIG_SETTINGS_TOKEN, ChannelInfo, ChannelType, CommunicationType, ConfigHTTPOptions, ConfigOptions, DataType, DatabaseDataDemoComponent, DatabaseManagerService, DatabaseStorage, DbService, ErrorDisplaySettings, GlobalStoreOptions, HTTPManagerService, HTTPManagerSignalsService, HTTPManagerStateService, HeadersService, HttpRequestManagerModule, HttpRequestServicesDemoComponent, LocalStorageDemoComponent, LocalStorageManagerService, LocalStorageOptions, LocalStorageSignalsManagerService, PathQueryService, Random, RandomHSLColor, RandomHexColor, RandomNumber, RandomNumbers, RandomNumbersUnique, RandomPaletteColor, RandomSignature, RandomStr, RandomVisibleColor, RequestErrorInterceptor, RequestHeadersInterceptor, RequestManagerDemoComponent, RequestManagerStateDemoComponent, RequestOptions, RequestService, RequestSignalsService, RetryOptions, SettingOptions, StateStorageOptions, StorageData, StorageOption, StorageType, StoreStateManagerService, StreamType, SymmetricalEncryptionService, TableSchemaDef, UUID, UUID_STR, UserData, UtilsService, WSOptions, WSUser, WebSocketManagerService, WebsocketService, WithCredentialsInterceptor, countdown, createChannelName, delayedRetry, requestPolling, requestStreaming, streamAI, streamAuto, streamEvents, streamJSON, streamNDJSON };
|
|
7632
9074
|
//# sourceMappingURL=http-request-manager.mjs.map
|