http-request-manager 18.7.30 → 18.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -383,17 +383,25 @@ class SymmetricalEncryptionService {
|
|
|
383
383
|
let _key = CryptoJS.enc.Utf8.parse(key);
|
|
384
384
|
let _iv = CryptoJS.enc.Utf8.parse(key);
|
|
385
385
|
try {
|
|
386
|
-
|
|
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);
|
|
@@ -1544,7 +2180,7 @@ class RequestService extends WebsocketService {
|
|
|
1544
2180
|
this.progress = new BehaviorSubject(0);
|
|
1545
2181
|
this.progress$ = this.progress.asObservable();
|
|
1546
2182
|
}
|
|
1547
|
-
// Implementation
|
|
2183
|
+
// Implementation
|
|
1548
2184
|
getRecordRequest(options) {
|
|
1549
2185
|
const urlPath = this.buildUrlPath(options);
|
|
1550
2186
|
const headers = this.buildCombinedHeaders(options);
|
|
@@ -1558,7 +2194,6 @@ class RequestService extends WebsocketService {
|
|
|
1558
2194
|
}).pipe(tap(data => console.log('STREAM DATA', data)), requestStreaming({ streamType: options.streamType || StreamType.AI_STREAMING }), this.requestStreaming(options), this.handleFinalize())
|
|
1559
2195
|
: this.http.get(urlPath, headers).pipe(this.request(options));
|
|
1560
2196
|
}
|
|
1561
|
-
// Implementation
|
|
1562
2197
|
createRecordRequest(options, data) {
|
|
1563
2198
|
const urlPath = this.buildUrlPath(options);
|
|
1564
2199
|
const headers = this.buildCombinedHeaders(options);
|
|
@@ -1724,72 +2359,491 @@ class RequestService extends WebsocketService {
|
|
|
1724
2359
|
}
|
|
1725
2360
|
return fileType;
|
|
1726
2361
|
}
|
|
1727
|
-
combineHeaders(headers, isStreaming) {
|
|
1728
|
-
return (isStreaming) ?
|
|
1729
|
-
{
|
|
1730
|
-
...headers,
|
|
1731
|
-
observe: 'events',
|
|
1732
|
-
responseType: 'text',
|
|
1733
|
-
reportProgress: true,
|
|
1734
|
-
Accept: 'text/event-stream'
|
|
2362
|
+
combineHeaders(headers, isStreaming) {
|
|
2363
|
+
return (isStreaming) ?
|
|
2364
|
+
{
|
|
2365
|
+
...headers,
|
|
2366
|
+
observe: 'events',
|
|
2367
|
+
responseType: 'text',
|
|
2368
|
+
reportProgress: true,
|
|
2369
|
+
Accept: 'text/event-stream'
|
|
2370
|
+
}
|
|
2371
|
+
: headers;
|
|
2372
|
+
}
|
|
2373
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2374
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, providedIn: 'root' }); }
|
|
2375
|
+
}
|
|
2376
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestService, decorators: [{
|
|
2377
|
+
type: Injectable,
|
|
2378
|
+
args: [{
|
|
2379
|
+
providedIn: 'root'
|
|
2380
|
+
}]
|
|
2381
|
+
}] });
|
|
2382
|
+
|
|
2383
|
+
function countdown(duration) {
|
|
2384
|
+
return defer(() => {
|
|
2385
|
+
const currentCount = { current: duration };
|
|
2386
|
+
return interval(1000).pipe(map(() => --currentCount.current), takeWhile(count => count >= 0));
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
2391
|
+
function delayedRetry(delayMs, maxRetry = DEFAULT_MAX_RETRIES) {
|
|
2392
|
+
return (src) => src.pipe(retry({
|
|
2393
|
+
count: maxRetry,
|
|
2394
|
+
delay: () => timer(delayMs)
|
|
2395
|
+
}));
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function requestPolling(pollInterval, stopCondition$, isPending$) {
|
|
2399
|
+
return (source) => {
|
|
2400
|
+
return interval(pollInterval * 1000)
|
|
2401
|
+
.pipe(startWith(0), tap(() => {
|
|
2402
|
+
try {
|
|
2403
|
+
if (isPending$ && typeof isPending$.next === 'function') {
|
|
2404
|
+
isPending$.next(true);
|
|
2405
|
+
}
|
|
2406
|
+
else if (isPending$ && typeof isPending$.set === 'function') {
|
|
2407
|
+
isPending$.set(true);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
catch (e) {
|
|
2411
|
+
// no-op if setting fails
|
|
2412
|
+
}
|
|
2413
|
+
}), mergeMap(() => source), tap(() => {
|
|
2414
|
+
try {
|
|
2415
|
+
if (isPending$ && typeof isPending$.next === 'function') {
|
|
2416
|
+
isPending$.next(false);
|
|
2417
|
+
}
|
|
2418
|
+
else if (isPending$ && typeof isPending$.set === 'function') {
|
|
2419
|
+
isPending$.set(false);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
catch (e) {
|
|
2423
|
+
// no-op if setting fails
|
|
2424
|
+
}
|
|
2425
|
+
}), takeUntil(stopCondition$));
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
/**
|
|
2430
|
+
* Message Tracker Service - Guaranteed Message Delivery
|
|
2431
|
+
*
|
|
2432
|
+
* Tracks message IDs, detects gaps, manages acknowledgments, and handles sync.
|
|
2433
|
+
* Ensures frontend clients never miss messages even during disconnections.
|
|
2434
|
+
*
|
|
2435
|
+
* Features:
|
|
2436
|
+
* - Per-channel message sequence tracking
|
|
2437
|
+
* - Automatic gap detection and recovery
|
|
2438
|
+
* - Batch acknowledgments (configurable interval)
|
|
2439
|
+
* - Reconnection sync with last-seen tracking
|
|
2440
|
+
* - Gap threshold alerting (10 messages before UI notification)
|
|
2441
|
+
*/
|
|
2442
|
+
class MessageTrackerService {
|
|
2443
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2444
|
+
// INITIALIZATION
|
|
2445
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2446
|
+
constructor(wsManager) {
|
|
2447
|
+
this.wsManager = wsManager;
|
|
2448
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2449
|
+
// CONFIGURATION
|
|
2450
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2451
|
+
/** Batch acknowledgment interval in milliseconds */
|
|
2452
|
+
this.BATCH_ACK_INTERVAL_MS = 5000; // 5 seconds
|
|
2453
|
+
/** Maximum gap before alerting UI */
|
|
2454
|
+
this.GAP_THRESHOLD = 10;
|
|
2455
|
+
/** Maximum messages to store in pending acks before forcing batch */
|
|
2456
|
+
this.MAX_PENDING_ACKS = 100;
|
|
2457
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2458
|
+
// STATE TRACKING
|
|
2459
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2460
|
+
/**
|
|
2461
|
+
* Track last seen message ID per channel
|
|
2462
|
+
* Map<channelName, lastMessageId>
|
|
2463
|
+
*/
|
|
2464
|
+
this.lastSeen = new Map();
|
|
2465
|
+
/**
|
|
2466
|
+
* Expected next message ID per channel (for gap detection)
|
|
2467
|
+
* Map<channelName, nextExpectedId>
|
|
2468
|
+
*/
|
|
2469
|
+
this.expectedSequence = new Map();
|
|
2470
|
+
/**
|
|
2471
|
+
* Pending acknowledgments per channel
|
|
2472
|
+
* Map<channelName, Set<messageId>>
|
|
2473
|
+
*/
|
|
2474
|
+
this.pendingAcks = new Map();
|
|
2475
|
+
/**
|
|
2476
|
+
* Gap count per channel (for threshold alerting)
|
|
2477
|
+
* Map<channelName, gapCount>
|
|
2478
|
+
*/
|
|
2479
|
+
this.gapCounts = new Map();
|
|
2480
|
+
/**
|
|
2481
|
+
* Replay mode tracking (per channel)
|
|
2482
|
+
* Used to distinguish replay vs live messages
|
|
2483
|
+
*/
|
|
2484
|
+
this.inReplayMode = new Map();
|
|
2485
|
+
/**
|
|
2486
|
+
* Gap retry count (for retry logic)
|
|
2487
|
+
* Map<channel, retryCount>
|
|
2488
|
+
*/
|
|
2489
|
+
this.gapRetryCount = new Map();
|
|
2490
|
+
/** Batch acknowledgment timer */
|
|
2491
|
+
this.batchAckTimer = null;
|
|
2492
|
+
/** Subscription to WebSocket messages */
|
|
2493
|
+
this.messagesSubscription = null;
|
|
2494
|
+
/**
|
|
2495
|
+
* Track channels we want to be subscribed to (survives disconnect/reconnect)
|
|
2496
|
+
* This is separate from WebSocketManagerService.subscribedChannels which tracks
|
|
2497
|
+
* actual active subscriptions
|
|
2498
|
+
*/
|
|
2499
|
+
this.intendedChannels = new Set();
|
|
2500
|
+
/** Observable to emit processed messages to UI components */
|
|
2501
|
+
this.messagesSubject = new BehaviorSubject(null);
|
|
2502
|
+
this.messages$ = this.messagesSubject.asObservable();
|
|
2503
|
+
// Restore state from sessionStorage (survives page reload)
|
|
2504
|
+
this.restoreLastSeen();
|
|
2505
|
+
this.restoreIntendedChannels();
|
|
2506
|
+
// Start batch acknowledgment timer
|
|
2507
|
+
this.startBatchAckTimer();
|
|
2508
|
+
// Subscribe to all incoming WebSocket messages
|
|
2509
|
+
this.messagesSubscription = this.wsManager.messages$.subscribe(msg => {
|
|
2510
|
+
if (msg) {
|
|
2511
|
+
this.onMessage(msg);
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
// Listen for reconnect events - re-subscribe with lastSeenId for sync
|
|
2515
|
+
this.wsManager.onReconnect$.subscribe(() => {
|
|
2516
|
+
console.log('🔄 Reconnection detected, re-subscribing with lastSeenId...');
|
|
2517
|
+
this.reSubscribeAllChannels();
|
|
2518
|
+
});
|
|
2519
|
+
console.log('✅ MessageTrackerService initialized');
|
|
2520
|
+
}
|
|
2521
|
+
ngOnDestroy() {
|
|
2522
|
+
this.stopBatchAckTimer();
|
|
2523
|
+
this.persistLastSeen(); // This now also persists intendedChannels
|
|
2524
|
+
this.messagesSubscription?.unsubscribe();
|
|
2525
|
+
console.log('🛑 MessageTrackerService destroyed');
|
|
2526
|
+
}
|
|
2527
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2528
|
+
// MESSAGE HANDLING
|
|
2529
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2530
|
+
/**
|
|
2531
|
+
* Process incoming message
|
|
2532
|
+
* Tracks ID, detects gaps, queues acknowledgment
|
|
2533
|
+
*/
|
|
2534
|
+
onMessage(msg) {
|
|
2535
|
+
const { type, channel, messageId, isReplay } = msg;
|
|
2536
|
+
// Only track messages with IDs (ignore legacy messages without messageId)
|
|
2537
|
+
if (messageId === undefined || messageId === null) {
|
|
2538
|
+
console.warn('⚠️ Received message without ID (legacy format):', msg);
|
|
2539
|
+
// Still forward legacy messages to UI
|
|
2540
|
+
this.messagesSubject.next(msg);
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
// Handle replay mode transitions
|
|
2544
|
+
if (isReplay && !this.inReplayMode.get(channel)) {
|
|
2545
|
+
console.log(`🔄 Entering replay mode for channel: ${channel}`);
|
|
2546
|
+
this.inReplayMode.set(channel, true);
|
|
2547
|
+
}
|
|
2548
|
+
else if (!isReplay && this.inReplayMode.get(channel)) {
|
|
2549
|
+
console.log(`✅ Exiting replay mode for channel: ${channel}`);
|
|
2550
|
+
this.inReplayMode.set(channel, false);
|
|
2551
|
+
}
|
|
2552
|
+
// Track message and detect gaps
|
|
2553
|
+
this.trackMessage(channel, messageId, isReplay);
|
|
2554
|
+
// Forward message to UI components
|
|
2555
|
+
this.messagesSubject.next(msg);
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* Track message ID and detect gaps
|
|
2559
|
+
*/
|
|
2560
|
+
trackMessage(channel, messageId, isReplay) {
|
|
2561
|
+
const expected = this.expectedSequence.get(channel) || 1;
|
|
2562
|
+
// Gap detection
|
|
2563
|
+
if (messageId !== expected) {
|
|
2564
|
+
console.warn(`⚠️ Gap detected in channel "${channel}": expected ${expected}, got ${messageId}`);
|
|
2565
|
+
// Increment gap count
|
|
2566
|
+
const currentGapCount = this.gapCounts.get(channel) || 0;
|
|
2567
|
+
const newGapCount = currentGapCount + 1;
|
|
2568
|
+
this.gapCounts.set(channel, newGapCount);
|
|
2569
|
+
// Check threshold
|
|
2570
|
+
if (newGapCount >= this.GAP_THRESHOLD) {
|
|
2571
|
+
console.error(`🚨 Gap threshold exceeded for channel "${channel}": ${newGapCount} gaps`);
|
|
2572
|
+
this.onGapThresholdExceeded(channel, newGapCount);
|
|
2573
|
+
}
|
|
2574
|
+
// Request gap fill
|
|
2575
|
+
this.requestGapFill(channel, expected, messageId - 1);
|
|
2576
|
+
}
|
|
2577
|
+
else {
|
|
2578
|
+
// Reset gap count and retry count on successful sequence
|
|
2579
|
+
this.gapCounts.set(channel, 0);
|
|
2580
|
+
this.gapRetryCount.set(channel, 0);
|
|
2581
|
+
}
|
|
2582
|
+
// Update tracking
|
|
2583
|
+
this.lastSeen.set(channel, messageId);
|
|
2584
|
+
this.expectedSequence.set(channel, messageId + 1);
|
|
2585
|
+
// Persist to sessionStorage (debounced in production)
|
|
2586
|
+
this.persistLastSeen();
|
|
2587
|
+
// Queue for acknowledgment
|
|
2588
|
+
this.queueAck(channel, messageId);
|
|
2589
|
+
// Check if we should force batch ack due to volume
|
|
2590
|
+
const pendingCount = this.pendingAcks.get(channel)?.size || 0;
|
|
2591
|
+
if (pendingCount >= this.MAX_PENDING_ACKS) {
|
|
2592
|
+
console.log(`📦 Forcing batch ack: ${pendingCount} pending messages`);
|
|
2593
|
+
this.sendBatchAck(channel);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2597
|
+
// ACKNOWLEDGMENT MANAGEMENT
|
|
2598
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2599
|
+
/**
|
|
2600
|
+
* Queue message for batch acknowledgment
|
|
2601
|
+
*/
|
|
2602
|
+
queueAck(channel, messageId) {
|
|
2603
|
+
if (!this.pendingAcks.has(channel)) {
|
|
2604
|
+
this.pendingAcks.set(channel, new Set());
|
|
2605
|
+
}
|
|
2606
|
+
this.pendingAcks.get(channel).add(messageId);
|
|
2607
|
+
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Start batch acknowledgment timer
|
|
2610
|
+
*/
|
|
2611
|
+
startBatchAckTimer() {
|
|
2612
|
+
this.batchAckTimer = setInterval(() => {
|
|
2613
|
+
this.sendAllBatchAcks();
|
|
2614
|
+
}, this.BATCH_ACK_INTERVAL_MS);
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Stop batch acknowledgment timer
|
|
2618
|
+
*/
|
|
2619
|
+
stopBatchAckTimer() {
|
|
2620
|
+
if (this.batchAckTimer) {
|
|
2621
|
+
clearInterval(this.batchAckTimer);
|
|
2622
|
+
this.batchAckTimer = null;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Send batch acknowledgments for all channels with pending acks
|
|
2627
|
+
*/
|
|
2628
|
+
sendAllBatchAcks() {
|
|
2629
|
+
this.pendingAcks.forEach((messageIds, channel) => {
|
|
2630
|
+
if (messageIds.size > 0) {
|
|
2631
|
+
this.sendBatchAck(channel);
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Send batch acknowledgment for a specific channel
|
|
2637
|
+
*/
|
|
2638
|
+
sendBatchAck(channel) {
|
|
2639
|
+
const messageIds = this.pendingAcks.get(channel);
|
|
2640
|
+
if (!messageIds || messageIds.size === 0) {
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
// Convert Set to sorted array
|
|
2644
|
+
const sortedIds = Array.from(messageIds).sort((a, b) => a - b);
|
|
2645
|
+
const ackUpTo = sortedIds[sortedIds.length - 1];
|
|
2646
|
+
console.log(`📦 Batch ack for channel "${channel}": ackUpTo=${ackUpTo} (${messageIds.size} messages)`);
|
|
2647
|
+
this.wsManager.send({
|
|
2648
|
+
type: 'messageAckBatch',
|
|
2649
|
+
channel,
|
|
2650
|
+
ackUpTo,
|
|
2651
|
+
// Optional: include skippedIds if you want to report gaps
|
|
2652
|
+
// skippedIds: this.getSkippedIds(channel, ackUpTo)
|
|
2653
|
+
});
|
|
2654
|
+
// Clear pending acks
|
|
2655
|
+
messageIds.clear();
|
|
2656
|
+
}
|
|
2657
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2658
|
+
// GAP RECOVERY
|
|
2659
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2660
|
+
/**
|
|
2661
|
+
* Request missing messages from server
|
|
2662
|
+
*/
|
|
2663
|
+
requestGapFill(channel, fromId, toId) {
|
|
2664
|
+
const retries = this.gapRetryCount.get(channel) || 0;
|
|
2665
|
+
if (retries >= 3) {
|
|
2666
|
+
console.error(`🚨 Gap fill failed after 3 retries for channel "${channel}": ${fromId} to ${toId}`);
|
|
2667
|
+
// Could emit to UI for manual intervention
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
console.log(`🔍 Requesting gap fill for channel "${channel}": ${fromId} to ${toId} (attempt ${retries + 1})`);
|
|
2671
|
+
this.wsManager.send({
|
|
2672
|
+
type: 'gapRequest',
|
|
2673
|
+
channel,
|
|
2674
|
+
fromId,
|
|
2675
|
+
toId
|
|
2676
|
+
});
|
|
2677
|
+
this.gapRetryCount.set(channel, retries + 1);
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Handle gap threshold exceeded (optional: alert UI)
|
|
2681
|
+
*/
|
|
2682
|
+
onGapThresholdExceeded(channel, gapCount) {
|
|
2683
|
+
console.warn(`⚠️ High gap count for channel "${channel}": ${gapCount} missed messages`);
|
|
2684
|
+
// Could emit to a separate observable for UI components to subscribe to
|
|
2685
|
+
// this.gapAlerts.next({ channel, gapCount });
|
|
2686
|
+
}
|
|
2687
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2688
|
+
// SUBSCRIPTION MANAGEMENT
|
|
2689
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2690
|
+
/**
|
|
2691
|
+
* Subscribe to channel with sync support
|
|
2692
|
+
* Automatically includes lastSeenId for replay
|
|
2693
|
+
*/
|
|
2694
|
+
subscribeToChannel(channel, userData) {
|
|
2695
|
+
const lastSeenId = this.lastSeen.get(channel) || 0;
|
|
2696
|
+
console.log(`📥 Subscribing to channel "${channel}" with lastSeenId=${lastSeenId}`);
|
|
2697
|
+
console.log(`📝 Adding to intendedChannels (now has ${this.intendedChannels.size + 1} channels)`);
|
|
2698
|
+
// Track this channel as intended (survives disconnect/reconnect)
|
|
2699
|
+
this.intendedChannels.add(channel);
|
|
2700
|
+
this.persistIntendedChannels();
|
|
2701
|
+
this.wsManager.sendSubscribeWithLastSeen(channel, userData, lastSeenId);
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* Re-subscribe to all previously subscribed channels after reconnect
|
|
2705
|
+
* Includes lastSeenId for each channel to trigger message replay
|
|
2706
|
+
*/
|
|
2707
|
+
reSubscribeAllChannels() {
|
|
2708
|
+
// Use intendedChannels which survives disconnect/reconnect
|
|
2709
|
+
const channels = Array.from(this.intendedChannels);
|
|
2710
|
+
console.log(`🔄 Re-subscribing to ${channels.length} channel(s) with lastSeenId...`);
|
|
2711
|
+
channels.forEach((channel) => {
|
|
2712
|
+
this.subscribeToChannel(channel);
|
|
2713
|
+
});
|
|
2714
|
+
console.log(`✅ Re-subscription complete for ${channels.length} channel(s)`);
|
|
2715
|
+
}
|
|
2716
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2717
|
+
// STATE PERSISTENCE
|
|
2718
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2719
|
+
/**
|
|
2720
|
+
* Persist lastSeen to sessionStorage (survives page reload)
|
|
2721
|
+
*/
|
|
2722
|
+
persistLastSeen() {
|
|
2723
|
+
try {
|
|
2724
|
+
const data = JSON.stringify(Array.from(this.lastSeen.entries()));
|
|
2725
|
+
sessionStorage.setItem('messageLastSeen', data);
|
|
2726
|
+
// Also persist intendedChannels
|
|
2727
|
+
this.persistIntendedChannels();
|
|
2728
|
+
}
|
|
2729
|
+
catch (err) {
|
|
2730
|
+
console.warn('⚠️ Failed to persist lastSeen:', err.message);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
/**
|
|
2734
|
+
* Restore lastSeen from sessionStorage
|
|
2735
|
+
*/
|
|
2736
|
+
restoreLastSeen() {
|
|
2737
|
+
try {
|
|
2738
|
+
const data = sessionStorage.getItem('messageLastSeen');
|
|
2739
|
+
if (data) {
|
|
2740
|
+
const entries = JSON.parse(data);
|
|
2741
|
+
this.lastSeen = new Map(entries);
|
|
2742
|
+
// Rebuild expectedSequence from restored lastSeen
|
|
2743
|
+
this.lastSeen.forEach((lastId, channel) => {
|
|
2744
|
+
this.expectedSequence.set(channel, lastId + 1);
|
|
2745
|
+
});
|
|
2746
|
+
console.log('📥 Restored lastSeen from sessionStorage:', this.lastSeen.size, 'channels');
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
catch (err) {
|
|
2750
|
+
console.warn('⚠️ Failed to restore lastSeen:', err.message);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* Persist intendedChannels to sessionStorage
|
|
2755
|
+
*/
|
|
2756
|
+
persistIntendedChannels() {
|
|
2757
|
+
try {
|
|
2758
|
+
const channels = Array.from(this.intendedChannels);
|
|
2759
|
+
sessionStorage.setItem('intendedChannels', JSON.stringify(channels));
|
|
2760
|
+
}
|
|
2761
|
+
catch (err) {
|
|
2762
|
+
console.warn('⚠️ Failed to persist intendedChannels:', err.message);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Restore intendedChannels from sessionStorage
|
|
2767
|
+
*/
|
|
2768
|
+
restoreIntendedChannels() {
|
|
2769
|
+
try {
|
|
2770
|
+
const data = sessionStorage.getItem('intendedChannels');
|
|
2771
|
+
if (data) {
|
|
2772
|
+
const channels = JSON.parse(data);
|
|
2773
|
+
this.intendedChannels = new Set(channels);
|
|
2774
|
+
console.log('📥 Restored intendedChannels from sessionStorage:', this.intendedChannels.size, 'channels');
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
catch (err) {
|
|
2778
|
+
console.warn('⚠️ Failed to restore intendedChannels:', err.message);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2782
|
+
// PUBLIC ACCESSORS
|
|
2783
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2784
|
+
/**
|
|
2785
|
+
* Get last seen message ID for a channel
|
|
2786
|
+
*/
|
|
2787
|
+
getLastSeenId(channel) {
|
|
2788
|
+
return this.lastSeen.get(channel) || 0;
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Get expected next message ID for a channel
|
|
2792
|
+
*/
|
|
2793
|
+
getExpectedNextId(channel) {
|
|
2794
|
+
return this.expectedSequence.get(channel) || 1;
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Check if currently in replay mode for a channel
|
|
2798
|
+
*/
|
|
2799
|
+
isInReplayMode(channel) {
|
|
2800
|
+
return this.inReplayMode.get(channel) || false;
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Get gap count for a channel
|
|
2804
|
+
*/
|
|
2805
|
+
getGapCount(channel) {
|
|
2806
|
+
return this.gapCounts.get(channel) || 0;
|
|
2807
|
+
}
|
|
2808
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2809
|
+
// UTILITY METHODS
|
|
2810
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2811
|
+
/**
|
|
2812
|
+
* Get skipped IDs between expected and received (for detailed gap reporting)
|
|
2813
|
+
*/
|
|
2814
|
+
getSkippedIds(channel, upToId) {
|
|
2815
|
+
const expected = this.expectedSequence.get(channel) || 1;
|
|
2816
|
+
const skipped = [];
|
|
2817
|
+
for (let i = expected; i < upToId; i++) {
|
|
2818
|
+
const pending = this.pendingAcks.get(channel);
|
|
2819
|
+
if (!pending?.has(i)) {
|
|
2820
|
+
skipped.push(i);
|
|
1735
2821
|
}
|
|
1736
|
-
|
|
2822
|
+
}
|
|
2823
|
+
return skipped;
|
|
1737
2824
|
}
|
|
1738
|
-
|
|
1739
|
-
|
|
2825
|
+
/**
|
|
2826
|
+
* Clear all tracking state (useful for debugging or manual reset)
|
|
2827
|
+
*/
|
|
2828
|
+
clearState() {
|
|
2829
|
+
this.lastSeen.clear();
|
|
2830
|
+
this.expectedSequence.clear();
|
|
2831
|
+
this.pendingAcks.clear();
|
|
2832
|
+
this.gapCounts.clear();
|
|
2833
|
+
this.inReplayMode.clear();
|
|
2834
|
+
this.gapRetryCount.clear();
|
|
2835
|
+
this.persistLastSeen();
|
|
2836
|
+
console.log('🧹 MessageTracker state cleared');
|
|
2837
|
+
}
|
|
2838
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageTrackerService, deps: [{ token: WebSocketManagerService }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2839
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageTrackerService, providedIn: 'root' }); }
|
|
1740
2840
|
}
|
|
1741
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
2841
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageTrackerService, decorators: [{
|
|
1742
2842
|
type: Injectable,
|
|
1743
2843
|
args: [{
|
|
1744
|
-
providedIn: 'root'
|
|
2844
|
+
providedIn: 'root',
|
|
1745
2845
|
}]
|
|
1746
|
-
}] });
|
|
1747
|
-
|
|
1748
|
-
function countdown(duration) {
|
|
1749
|
-
return defer(() => {
|
|
1750
|
-
const currentCount = { current: duration };
|
|
1751
|
-
return interval(1000).pipe(map(() => --currentCount.current), takeWhile(count => count >= 0));
|
|
1752
|
-
});
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
const DEFAULT_MAX_RETRIES = 3;
|
|
1756
|
-
function delayedRetry(delayMs, maxRetry = DEFAULT_MAX_RETRIES) {
|
|
1757
|
-
return (src) => src.pipe(retry({
|
|
1758
|
-
count: maxRetry,
|
|
1759
|
-
delay: () => timer(delayMs)
|
|
1760
|
-
}));
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
function requestPolling(pollInterval, stopCondition$, isPending$) {
|
|
1764
|
-
return (source) => {
|
|
1765
|
-
return interval(pollInterval * 1000)
|
|
1766
|
-
.pipe(startWith(0), tap(() => {
|
|
1767
|
-
try {
|
|
1768
|
-
if (isPending$ && typeof isPending$.next === 'function') {
|
|
1769
|
-
isPending$.next(true);
|
|
1770
|
-
}
|
|
1771
|
-
else if (isPending$ && typeof isPending$.set === 'function') {
|
|
1772
|
-
isPending$.set(true);
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
catch (e) {
|
|
1776
|
-
// no-op if setting fails
|
|
1777
|
-
}
|
|
1778
|
-
}), mergeMap(() => source), tap(() => {
|
|
1779
|
-
try {
|
|
1780
|
-
if (isPending$ && typeof isPending$.next === 'function') {
|
|
1781
|
-
isPending$.next(false);
|
|
1782
|
-
}
|
|
1783
|
-
else if (isPending$ && typeof isPending$.set === 'function') {
|
|
1784
|
-
isPending$.set(false);
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
catch (e) {
|
|
1788
|
-
// no-op if setting fails
|
|
1789
|
-
}
|
|
1790
|
-
}), takeUntil(stopCondition$));
|
|
1791
|
-
};
|
|
1792
|
-
}
|
|
2846
|
+
}], ctorParameters: () => [{ type: WebSocketManagerService }] });
|
|
1793
2847
|
|
|
1794
2848
|
class DatabaseStorage {
|
|
1795
2849
|
constructor(table = '', expiresIn) {
|
|
@@ -1878,6 +2932,12 @@ class HTTPManagerService extends RequestService {
|
|
|
1878
2932
|
this.toastMessage = inject(ToastMessageDisplayService);
|
|
1879
2933
|
this.ng_injector = inject(Injector);
|
|
1880
2934
|
this.objectMergerService = inject(ObjectMergerService);
|
|
2935
|
+
this.wsManager = inject(WebSocketManagerService);
|
|
2936
|
+
this.messageTracker = inject(MessageTrackerService);
|
|
2937
|
+
// Delegate WebSocket observables to WebSocketManagerService (singleton)
|
|
2938
|
+
this.connectionStatus$ = this.wsManager.connectionStatus$;
|
|
2939
|
+
this.messages$ = this.messageTracker.messages$; // Messages flow through MessageTrackerService
|
|
2940
|
+
this.subscribedChannels$ = this.wsManager.subscribedChannels$;
|
|
1881
2941
|
this.countdown = new BehaviorSubject(0);
|
|
1882
2942
|
this.countdown$ = this.countdown.asObservable();
|
|
1883
2943
|
this.error = new BehaviorSubject(false);
|
|
@@ -1888,6 +2948,130 @@ class HTTPManagerService extends RequestService {
|
|
|
1888
2948
|
this.config = ApiRequest.adapt();
|
|
1889
2949
|
this.config = (configOptions) ? ApiRequest.adapt(configOptions.httpRequestOptions) : this.config;
|
|
1890
2950
|
}
|
|
2951
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2952
|
+
// WEBSOCKET METHODS (Delegated to WebSocketManagerService singleton)
|
|
2953
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2954
|
+
/**
|
|
2955
|
+
* Connect to WebSocket server
|
|
2956
|
+
* Only the first call across ALL instances will establish a connection
|
|
2957
|
+
*/
|
|
2958
|
+
connect(options, jwtToken) {
|
|
2959
|
+
this.wsManager.connect(options, jwtToken);
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Disconnect from WebSocket server
|
|
2963
|
+
*/
|
|
2964
|
+
disconnect() {
|
|
2965
|
+
this.wsManager.disconnect();
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Subscribe to a channel
|
|
2969
|
+
*/
|
|
2970
|
+
subscribeToChannel(channel, userData) {
|
|
2971
|
+
this.messageTracker.subscribeToChannel(channel, userData);
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Subscribe to multiple channels
|
|
2975
|
+
*/
|
|
2976
|
+
subscribeToChannels(channels, userData) {
|
|
2977
|
+
channels.forEach(channel => this.messageTracker.subscribeToChannel(channel, userData));
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2980
|
+
* Unsubscribe from a channel
|
|
2981
|
+
*/
|
|
2982
|
+
unsubscribeFromChannel(channel) {
|
|
2983
|
+
this.wsManager.unsubscribeFromChannel(channel);
|
|
2984
|
+
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Get currently subscribed channels
|
|
2987
|
+
*/
|
|
2988
|
+
getSubscribedChannels() {
|
|
2989
|
+
return this.wsManager.getSubscribedChannels();
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Send broadcast message
|
|
2993
|
+
*/
|
|
2994
|
+
sendBroadcast(content) {
|
|
2995
|
+
this.wsManager.sendBroadcast(content);
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* Send message in channel (state manager message)
|
|
2999
|
+
*/
|
|
3000
|
+
sendMessageInChannel(channel, content) {
|
|
3001
|
+
this.wsManager.sendMessageInChannel(channel, content);
|
|
3002
|
+
}
|
|
3003
|
+
/**
|
|
3004
|
+
* Send channel message
|
|
3005
|
+
*/
|
|
3006
|
+
sendChannelMessage(channel, content) {
|
|
3007
|
+
this.wsManager.sendChannelMessage(channel, content);
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* Send message to multiple channels
|
|
3011
|
+
*/
|
|
3012
|
+
sendChannelMessageToChannels(channels, content) {
|
|
3013
|
+
this.wsManager.sendChannelMessageToChannels(channels, content);
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
3016
|
+
* Send message to user
|
|
3017
|
+
*/
|
|
3018
|
+
sendMessageToUser(user, content) {
|
|
3019
|
+
this.wsManager.sendMessageToUser(user, content);
|
|
3020
|
+
}
|
|
3021
|
+
/**
|
|
3022
|
+
* Get all channels
|
|
3023
|
+
*/
|
|
3024
|
+
getAllChannels() {
|
|
3025
|
+
this.wsManager.getAllChannels();
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Create channel
|
|
3029
|
+
*/
|
|
3030
|
+
createChannel(channel) {
|
|
3031
|
+
this.wsManager.createChannel(channel);
|
|
3032
|
+
}
|
|
3033
|
+
/**
|
|
3034
|
+
* Delete channel
|
|
3035
|
+
*/
|
|
3036
|
+
deleteChannel(channel) {
|
|
3037
|
+
this.wsManager.deleteChannel(channel);
|
|
3038
|
+
}
|
|
3039
|
+
/**
|
|
3040
|
+
* Get users in channel
|
|
3041
|
+
*/
|
|
3042
|
+
getUsersInChannel(channel) {
|
|
3043
|
+
this.wsManager.getUsersInChannel(channel);
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Create notification channel
|
|
3047
|
+
*/
|
|
3048
|
+
createNotificationChannel(channel) {
|
|
3049
|
+
this.wsManager.createNotificationChannel(channel);
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Get notification channels
|
|
3053
|
+
*/
|
|
3054
|
+
getNotificationChannels() {
|
|
3055
|
+
this.wsManager.getNotificationChannels();
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Get today's notification channels
|
|
3059
|
+
*/
|
|
3060
|
+
getTodaysNotificationChannels() {
|
|
3061
|
+
this.wsManager.getTodaysNotificationChannels();
|
|
3062
|
+
}
|
|
3063
|
+
/**
|
|
3064
|
+
* Subscribe to notification channel
|
|
3065
|
+
*/
|
|
3066
|
+
subscribeToNotificationChannel(channel, options, user) {
|
|
3067
|
+
this.wsManager.subscribeToNotificationChannel(channel, options, user);
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Unsubscribe from notification channel
|
|
3071
|
+
*/
|
|
3072
|
+
unsubscribeFromNotificationChannel(channel) {
|
|
3073
|
+
this.wsManager.unsubscribeFromNotificationChannel(channel);
|
|
3074
|
+
}
|
|
1891
3075
|
// REQUESTS
|
|
1892
3076
|
getRequest(options, params) {
|
|
1893
3077
|
this.isPending.next(true);
|
|
@@ -2417,7 +3601,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2417
3601
|
}] }] });
|
|
2418
3602
|
|
|
2419
3603
|
class ApiRequest {
|
|
2420
|
-
constructor(server = '', path, headers, adapter, mapper, polling, retry, stream, streamType, displayError, saveAs, fileContentHeader, ws) {
|
|
3604
|
+
constructor(server = '', path, headers, adapter, mapper, polling, retry, stream, streamType, displayError, saveAs, fileContentHeader, ws, env) {
|
|
2421
3605
|
this.server = server;
|
|
2422
3606
|
this.path = path;
|
|
2423
3607
|
this.headers = headers;
|
|
@@ -2431,10 +3615,11 @@ class ApiRequest {
|
|
|
2431
3615
|
this.saveAs = saveAs;
|
|
2432
3616
|
this.fileContentHeader = fileContentHeader;
|
|
2433
3617
|
this.ws = ws;
|
|
3618
|
+
this.env = env;
|
|
2434
3619
|
}
|
|
2435
3620
|
static adapt(item) {
|
|
2436
3621
|
const server = Array.isArray(item?.server) ? item.server.join('/') : item?.server || '';
|
|
2437
|
-
return new ApiRequest(server, (item?.path) ? item.path : [], (item?.headers) ? item.headers : {}, item?.adapter, item?.mapper, item?.polling ? Math.floor(item.polling) : 0, item?.retry ? RetryOptions.adapt(item.retry) : RetryOptions.adapt(), (item?.stream) ? item.stream : false, item?.streamType || StreamType.AI_STREAMING, (item?.displayError) ? item.displayError : false, item?.saveAs, item?.fileContentHeader, item?.ws);
|
|
3622
|
+
return new ApiRequest(server, (item?.path) ? item.path : [], (item?.headers) ? item.headers : {}, item?.adapter, item?.mapper, item?.polling ? Math.floor(item.polling) : 0, item?.retry ? RetryOptions.adapt(item.retry) : RetryOptions.adapt(), (item?.stream) ? item.stream : false, item?.streamType || StreamType.AI_STREAMING, (item?.displayError) ? item.displayError : false, item?.saveAs, item?.fileContentHeader, item?.ws, item?.env || 'dev');
|
|
2438
3623
|
}
|
|
2439
3624
|
}
|
|
2440
3625
|
|
|
@@ -2449,17 +3634,18 @@ class RequestOptions {
|
|
|
2449
3634
|
}
|
|
2450
3635
|
|
|
2451
3636
|
class WSOptions {
|
|
2452
|
-
constructor(id = '', wsServer = '', jwtToken = '', permissions, channels, user, retry) {
|
|
3637
|
+
constructor(id = '', wsServer = '', jwtToken = '', permissions, channels, wsUpdateChannels, user, retry) {
|
|
2453
3638
|
this.id = id;
|
|
2454
3639
|
this.wsServer = wsServer;
|
|
2455
3640
|
this.jwtToken = jwtToken;
|
|
2456
3641
|
this.permissions = permissions;
|
|
2457
3642
|
this.channels = channels;
|
|
3643
|
+
this.wsUpdateChannels = wsUpdateChannels;
|
|
2458
3644
|
this.user = user;
|
|
2459
3645
|
this.retry = retry;
|
|
2460
3646
|
}
|
|
2461
3647
|
static adapt(item) {
|
|
2462
|
-
return new WSOptions(item?.id, item?.wsServer, item?.jwtToken, (item?.permissions) ? item.permissions.split(',').map((p) => p.trim()) : [], item?.channels, item?.user, item?.retry);
|
|
3648
|
+
return new WSOptions(item?.id, item?.wsServer, item?.jwtToken, (item?.permissions) ? item.permissions.split(',').map((p) => p.trim()) : [], item?.channels, item?.wsUpdateChannels, item?.user, item?.retry);
|
|
2463
3649
|
}
|
|
2464
3650
|
}
|
|
2465
3651
|
|
|
@@ -2581,8 +3767,24 @@ class LocalStorageManagerService extends ComponentStore {
|
|
|
2581
3767
|
console.warn('No App ID found - AppId not Provided');
|
|
2582
3768
|
return;
|
|
2583
3769
|
}
|
|
2584
|
-
|
|
2585
|
-
|
|
3770
|
+
let storageData = found.data;
|
|
3771
|
+
if (options.encrypted) {
|
|
3772
|
+
const decryptedData = this.encryption.decrypt(found.data, this.app.appID);
|
|
3773
|
+
if (decryptedData !== null) {
|
|
3774
|
+
storageData = decryptedData;
|
|
3775
|
+
}
|
|
3776
|
+
else {
|
|
3777
|
+
console.warn(`Failed to decrypt data for store: ${store}`);
|
|
3778
|
+
storageData = found.data;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
try {
|
|
3782
|
+
return (this.isString(storageData)) ? JSON.parse(storageData) : storageData;
|
|
3783
|
+
}
|
|
3784
|
+
catch (error) {
|
|
3785
|
+
console.warn(`Failed to parse storage data for store: ${store}`, error);
|
|
3786
|
+
return storageData; // Return raw data if parsing fails
|
|
3787
|
+
}
|
|
2586
3788
|
}
|
|
2587
3789
|
else {
|
|
2588
3790
|
return null;
|
|
@@ -2750,10 +3952,39 @@ class LocalStorageManagerService extends ComponentStore {
|
|
|
2750
3952
|
const str = localStorage.getItem(this.storageSettingsName);
|
|
2751
3953
|
const localStr = localStorage.getItem(this.storageName);
|
|
2752
3954
|
const sessionStr = sessionStorage.getItem(this.storageName);
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
3955
|
+
let localData = [];
|
|
3956
|
+
let sessionData = [];
|
|
3957
|
+
let settings = [];
|
|
3958
|
+
try {
|
|
3959
|
+
localData = (localStr) ? JSON.parse(localStr) : [];
|
|
3960
|
+
}
|
|
3961
|
+
catch (error) {
|
|
3962
|
+
console.warn('Failed to parse local storage data:', error);
|
|
3963
|
+
localData = [];
|
|
3964
|
+
}
|
|
3965
|
+
try {
|
|
3966
|
+
sessionData = (sessionStr) ? JSON.parse(sessionStr) : [];
|
|
3967
|
+
}
|
|
3968
|
+
catch (error) {
|
|
3969
|
+
console.warn('Failed to parse session storage data:', error);
|
|
3970
|
+
sessionData = [];
|
|
3971
|
+
}
|
|
3972
|
+
if (str) {
|
|
3973
|
+
const decryptedStr = this.encryption.decrypt(str, this.app.appID);
|
|
3974
|
+
if (decryptedStr) {
|
|
3975
|
+
try {
|
|
3976
|
+
settings = JSON.parse(decryptedStr);
|
|
3977
|
+
}
|
|
3978
|
+
catch (error) {
|
|
3979
|
+
console.warn('Failed to parse decrypted settings:', error);
|
|
3980
|
+
settings = [];
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
else {
|
|
3984
|
+
console.warn('Failed to decrypt settings data');
|
|
3985
|
+
settings = [];
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
2757
3988
|
settings.forEach(store => {
|
|
2758
3989
|
const expired = (store.options?.expires || 0) > 0 && this.utils.hasExpired(store.options?.expires || 0);
|
|
2759
3990
|
if (!expired) {
|
|
@@ -2806,6 +4037,47 @@ class LocalStorageManagerService extends ComponentStore {
|
|
|
2806
4037
|
ngOnDestroy() {
|
|
2807
4038
|
this.persistence$.unsubscribe();
|
|
2808
4039
|
}
|
|
4040
|
+
/**
|
|
4041
|
+
* Clears all stored data from localStorage and sessionStorage
|
|
4042
|
+
* Use this method to recover from corrupted data errors
|
|
4043
|
+
*/
|
|
4044
|
+
clearAllStoredData() {
|
|
4045
|
+
try {
|
|
4046
|
+
localStorage.removeItem(this.storageSettingsName);
|
|
4047
|
+
localStorage.removeItem(this.storageName);
|
|
4048
|
+
sessionStorage.removeItem(this.storageName);
|
|
4049
|
+
console.log('Cleared all stored data');
|
|
4050
|
+
}
|
|
4051
|
+
catch (error) {
|
|
4052
|
+
console.error('Failed to clear stored data:', error);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
/**
|
|
4056
|
+
* Checks if stored data appears to be corrupted
|
|
4057
|
+
* Returns true if data appears corrupted
|
|
4058
|
+
*/
|
|
4059
|
+
checkForCorruptedData() {
|
|
4060
|
+
try {
|
|
4061
|
+
const str = localStorage.getItem(this.storageSettingsName);
|
|
4062
|
+
if (str) {
|
|
4063
|
+
// Try to decrypt if it's encrypted
|
|
4064
|
+
const decryptedStr = this.encryption.decrypt(str, this.app.appID);
|
|
4065
|
+
if (decryptedStr !== null && decryptedStr !== undefined) {
|
|
4066
|
+
// Try to parse the decrypted data
|
|
4067
|
+
JSON.parse(decryptedStr);
|
|
4068
|
+
}
|
|
4069
|
+
else {
|
|
4070
|
+
// Decryption failed, data is likely corrupted
|
|
4071
|
+
return true;
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
return false;
|
|
4075
|
+
}
|
|
4076
|
+
catch (error) {
|
|
4077
|
+
// Parsing failed, data is corrupted
|
|
4078
|
+
return true;
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
2809
4081
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocalStorageManagerService, deps: [{ token: CONFIG_SETTINGS_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2810
4082
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocalStorageManagerService, providedIn: 'root' }); }
|
|
2811
4083
|
}
|
|
@@ -2858,10 +4130,24 @@ class LocalStorageSignalsManagerService {
|
|
|
2858
4130
|
console.warn('No App ID found - AppId not Provided');
|
|
2859
4131
|
return null;
|
|
2860
4132
|
}
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
4133
|
+
let storageData = found.data;
|
|
4134
|
+
if (options.encrypted) {
|
|
4135
|
+
const decryptedData = this.encryption.decrypt(found.data, this.app.appID);
|
|
4136
|
+
if (decryptedData !== null) {
|
|
4137
|
+
storageData = decryptedData;
|
|
4138
|
+
}
|
|
4139
|
+
else {
|
|
4140
|
+
console.warn(`Failed to decrypt data for store: ${store}`);
|
|
4141
|
+
storageData = found.data; // Use undecrypted data as fallback
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
try {
|
|
4145
|
+
return this.isString(storageData) ? JSON.parse(storageData) : storageData;
|
|
4146
|
+
}
|
|
4147
|
+
catch (error) {
|
|
4148
|
+
console.warn(`Failed to parse storage data for store: ${store}`, error);
|
|
4149
|
+
return storageData; // Return raw data if parsing fails
|
|
4150
|
+
}
|
|
2865
4151
|
});
|
|
2866
4152
|
this.settings = computed(() => this.state().settings);
|
|
2867
4153
|
this.setting = (store) => computed(() => this.state().settings.find(item => item.name === store) ?? null);
|
|
@@ -2989,8 +4275,23 @@ class LocalStorageSignalsManagerService {
|
|
|
2989
4275
|
const sessionStr = sessionStorage.getItem(this.storageName);
|
|
2990
4276
|
const localData = localStr ? JSON.parse(localStr) : [];
|
|
2991
4277
|
const sessionData = sessionStr ? JSON.parse(sessionStr) : [];
|
|
2992
|
-
|
|
2993
|
-
|
|
4278
|
+
let settings = [];
|
|
4279
|
+
if (str) {
|
|
4280
|
+
const decryptedStr = this.encryption.decrypt(str, this.app.appID);
|
|
4281
|
+
if (decryptedStr) {
|
|
4282
|
+
try {
|
|
4283
|
+
settings = JSON.parse(decryptedStr);
|
|
4284
|
+
}
|
|
4285
|
+
catch (error) {
|
|
4286
|
+
console.warn('Failed to parse decrypted settings:', error);
|
|
4287
|
+
settings = [];
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
else {
|
|
4291
|
+
console.warn('Failed to decrypt settings data');
|
|
4292
|
+
settings = [];
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
2994
4295
|
settings.forEach(store => {
|
|
2995
4296
|
// normalize options so we compare numbers and compute expires correctly
|
|
2996
4297
|
const options = SettingOptions.adapt(store.options);
|
|
@@ -3449,13 +4750,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3449
4750
|
}], ctorParameters: () => [] });
|
|
3450
4751
|
|
|
3451
4752
|
class ChannelMessage {
|
|
3452
|
-
constructor(sessionId = null, content = null) {
|
|
4753
|
+
constructor(messageId, channel, isReplay, sessionId = null, content = null, timestamp) {
|
|
4754
|
+
this.messageId = messageId;
|
|
4755
|
+
this.channel = channel;
|
|
4756
|
+
this.isReplay = isReplay;
|
|
3453
4757
|
this.sessionId = sessionId;
|
|
3454
4758
|
this.content = content;
|
|
4759
|
+
this.timestamp = timestamp;
|
|
3455
4760
|
}
|
|
3456
4761
|
static adapt(item) {
|
|
3457
|
-
return new ChannelMessage(item?.sessionId || item?.id, // Support both for backward compatibility
|
|
3458
|
-
item?.content);
|
|
4762
|
+
return new ChannelMessage(item?.messageId, item?.channel, item?.isReplay, item?.sessionId || item?.id, // Support both for backward compatibility
|
|
4763
|
+
item?.content, item?.timestamp);
|
|
3459
4764
|
}
|
|
3460
4765
|
}
|
|
3461
4766
|
|
|
@@ -3490,6 +4795,8 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3490
4795
|
getUsersForChannel$(channel) {
|
|
3491
4796
|
return this.userListByChannel$.pipe(map(channelMap => channelMap.get(channel) || []));
|
|
3492
4797
|
}
|
|
4798
|
+
// Message queue for WebSocket communication (processed when connection is established)
|
|
4799
|
+
static { this.wsCommunicationQueue = []; }
|
|
3493
4800
|
constructor(apiOptions = ApiRequest.adapt(), dataType, database) {
|
|
3494
4801
|
super(defaultState);
|
|
3495
4802
|
this.apiOptions = apiOptions;
|
|
@@ -3517,7 +4824,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3517
4824
|
this.messages$ = this.messages.asObservable();
|
|
3518
4825
|
this.userListByChannel = new BehaviorSubject(new Map());
|
|
3519
4826
|
this.userListByChannel$ = this.userListByChannel.asObservable();
|
|
3520
|
-
//
|
|
4827
|
+
// Returns all unique users across all channels
|
|
3521
4828
|
this.userList = new BehaviorSubject([]);
|
|
3522
4829
|
this.userList$ = this.userList.asObservable();
|
|
3523
4830
|
this.user = new BehaviorSubject(null);
|
|
@@ -3541,17 +4848,19 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3541
4848
|
this.latestCommunicationMessages$ = this.latestCommunicationMessages.asObservable();
|
|
3542
4849
|
this.userAction = new BehaviorSubject(null);
|
|
3543
4850
|
this.userAction$ = this.userAction.asObservable();
|
|
3544
|
-
this.wsConnection = false;
|
|
3545
4851
|
this.wsOptions = WSOptions.adapt();
|
|
3546
|
-
// Expose raw WS connection status directly to UI
|
|
4852
|
+
// Expose raw WS connection status directly to UI (from singleton WebSocketManagerService)
|
|
3547
4853
|
this.connectionStatus$ = this.httpManagerService.connectionStatus$;
|
|
3548
4854
|
// WebSocket
|
|
3549
|
-
this.initWS = this.effect((wsOptions$) => wsOptions$.pipe(
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
4855
|
+
this.initWS = this.effect((wsOptions$) => wsOptions$.pipe(switchMap((wsOptions) => merge(this.httpManagerService.connectionStatus$.pipe(tap((isConnected) => {
|
|
4856
|
+
// Process queued wsCommunication calls when connection becomes true
|
|
4857
|
+
if (isConnected && HTTPManagerStateService.wsCommunicationQueue.length > 0) {
|
|
4858
|
+
console.log(`🔄 Processing ${HTTPManagerStateService.wsCommunicationQueue.length} queued WS messages`);
|
|
4859
|
+
while (HTTPManagerStateService.wsCommunicationQueue.length > 0) {
|
|
4860
|
+
const queued = HTTPManagerStateService.wsCommunicationQueue.shift();
|
|
4861
|
+
this.sendWsCommunication(queued.method, queued.path);
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
3555
4864
|
})), this.httpManagerService.messages$.pipe(tap((message) => {
|
|
3556
4865
|
if (!message)
|
|
3557
4866
|
return;
|
|
@@ -3559,6 +4868,8 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3559
4868
|
const currentMessages = this.messages.value;
|
|
3560
4869
|
this.messages.next([...currentMessages, message]);
|
|
3561
4870
|
console.log('Received:', message);
|
|
4871
|
+
// Debug: Log all message types
|
|
4872
|
+
console.log('📨 Message type:', message.type);
|
|
3562
4873
|
if (message.error === 'JWT_INVALID') {
|
|
3563
4874
|
this.shouldRetry = false;
|
|
3564
4875
|
this.httpManagerService.disconnect();
|
|
@@ -3572,16 +4883,22 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3572
4883
|
switch (message.type) {
|
|
3573
4884
|
case 'channelsList':
|
|
3574
4885
|
console.log('💬 Channels:', message.channels);
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
//
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
4886
|
+
console.log('🔍 channelsList received, checking connection status...');
|
|
4887
|
+
console.log('🔍 WebSocket connected:', WebSocketManagerService.isConnected());
|
|
4888
|
+
// Auto-subscribe to all channels from the list
|
|
4889
|
+
if (message.channels && message.channels.length > 0) {
|
|
4890
|
+
console.log('📥 Auto-subscribing to', message.channels.length, 'channel(s)');
|
|
4891
|
+
this.subscribeToChannels(message.channels);
|
|
4892
|
+
}
|
|
4893
|
+
else {
|
|
4894
|
+
console.log('⚠️ No channels to subscribe to');
|
|
4895
|
+
}
|
|
3581
4896
|
this.channels.next(message.channels);
|
|
3582
4897
|
break;
|
|
3583
4898
|
case 'subscribed':
|
|
3584
4899
|
console.log(`✅ Subscription confirmed: ${message.channel}`);
|
|
4900
|
+
// Track as subscribed now that server confirmed
|
|
4901
|
+
WebSocketManagerService.addSubscribedChannel(message.channel);
|
|
3585
4902
|
break;
|
|
3586
4903
|
case 'unsubscribed':
|
|
3587
4904
|
console.log(`🔓 Unsubscription confirmed: ${message.channel}`);
|
|
@@ -3589,6 +4906,11 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3589
4906
|
case 'info':
|
|
3590
4907
|
// Already subscribed or other info messages
|
|
3591
4908
|
console.log(`ℹ️ Info: ${message.message}`);
|
|
4909
|
+
// If it's an "Already subscribed" message, treat it as subscription confirmation
|
|
4910
|
+
if (message.message?.includes('Already subscribed')) {
|
|
4911
|
+
console.log(`✅ Treating info as subscription confirmation for: ${message.data}`);
|
|
4912
|
+
WebSocketManagerService.addSubscribedChannel(message.data);
|
|
4913
|
+
}
|
|
3592
4914
|
break;
|
|
3593
4915
|
case 'stateMangerMessage':
|
|
3594
4916
|
// Compare sender's session ID with current user's ID
|
|
@@ -3607,19 +4929,58 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3607
4929
|
break;
|
|
3608
4930
|
case 'channelMessage':
|
|
3609
4931
|
// Handle channel-based messages (from sendChannelMessage)
|
|
3610
|
-
// Structure: { type: 'channelMessage',
|
|
4932
|
+
// Structure: { type: 'channelMessage', messageId, channel, sessionId, content, timestamp }
|
|
3611
4933
|
// Skip messages from self
|
|
3612
|
-
const senderSessionId = message.sessionId?.id;
|
|
4934
|
+
const senderSessionId = message.sessionId?.id || message.sessionId;
|
|
3613
4935
|
if (senderSessionId === this.user.value?.id) {
|
|
4936
|
+
console.log('🔇 Skipping message from self (sessionId match)');
|
|
3614
4937
|
break;
|
|
3615
4938
|
}
|
|
3616
4939
|
console.log('💬 Channel Message received:', message);
|
|
3617
|
-
|
|
4940
|
+
// Determine which channels this message was sent to
|
|
4941
|
+
const messageChannels = message.channel ? [message.channel] : [];
|
|
4942
|
+
// Construct expected channel path (without env prefix)
|
|
4943
|
+
const myPath = this.apiOptions.path || [];
|
|
4944
|
+
const myPathString = myPath.join('/');
|
|
4945
|
+
// Check if any of the message channels CONTAIN our path
|
|
4946
|
+
const isWsUpdateChannel = messageChannels.some((ch) => {
|
|
4947
|
+
// Strip SYS- prefix if present for comparison
|
|
4948
|
+
const cleanChannel = ch.replace('SYS-', '');
|
|
4949
|
+
// Check if channel contains our path (or starts with it)
|
|
4950
|
+
const matches = cleanChannel === myPathString ||
|
|
4951
|
+
cleanChannel.startsWith(myPathString + '/') ||
|
|
4952
|
+
cleanChannel.includes(myPathString);
|
|
4953
|
+
console.log(`🔍 Channel check: ${ch} contains ${myPathString}? ${matches}`);
|
|
4954
|
+
return matches;
|
|
4955
|
+
});
|
|
4956
|
+
// If it's the expected channel, trigger fetchRecord like stateManagerMessage
|
|
4957
|
+
if (isWsUpdateChannel && message.content?.path) {
|
|
4958
|
+
console.log('🔍 Message received on expected channel (path match):', myPathString);
|
|
4959
|
+
console.log('📄 Content:', message.content);
|
|
4960
|
+
console.log('📥 Fetching record for channel:', myPathString);
|
|
4961
|
+
const path = message.content.path;
|
|
4962
|
+
const method = message.content.method || 'UPDATE';
|
|
4963
|
+
this.userAction.next({ sessionId: message.sessionId, content: message.content });
|
|
4964
|
+
this.fetchRecord(RequestOptions.adapt({ path }), method);
|
|
4965
|
+
}
|
|
4966
|
+
else if (message.content) {
|
|
4967
|
+
// Handle message content directly
|
|
4968
|
+
console.log('📄 Processing message content:', message.content);
|
|
3618
4969
|
this.appendMessages(ChannelMessage.adapt({
|
|
3619
4970
|
sessionId: message.sessionId,
|
|
3620
4971
|
content: message.content,
|
|
3621
4972
|
}));
|
|
3622
4973
|
}
|
|
4974
|
+
else {
|
|
4975
|
+
console.log('⚠️ Message does not contain data.content.path, skipping fetchRecord');
|
|
4976
|
+
}
|
|
4977
|
+
// Keep existing functionality for backward compatibility
|
|
4978
|
+
if (message.data?.content && !isWsUpdateChannel) {
|
|
4979
|
+
this.appendMessages(ChannelMessage.adapt({
|
|
4980
|
+
sessionId: message.data.sessionId,
|
|
4981
|
+
content: message.data.content,
|
|
4982
|
+
}));
|
|
4983
|
+
}
|
|
3623
4984
|
break;
|
|
3624
4985
|
case 'usersInChannel':
|
|
3625
4986
|
console.log(`👥 Users in channel "${message.channel}":`, message.data.users);
|
|
@@ -3835,11 +5196,21 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3835
5196
|
// FETCH RECORD
|
|
3836
5197
|
this.fetchRecord = (options, method) => this.effect(() => of(RequestOptions.adapt(options)).pipe(tap(() => console.log('🔄 fetchRecord effect triggered with path:', options?.path, 'method:', method)), switchMap((options) => {
|
|
3837
5198
|
this.streamedResponse = [];
|
|
5199
|
+
// Temporarily update apiOptions.path with the path from the WebSocket message
|
|
5200
|
+
// This ensures the request goes to the correct endpoint
|
|
5201
|
+
const originalPath = this.apiOptions.path;
|
|
5202
|
+
if (options?.path && Array.isArray(options.path)) {
|
|
5203
|
+
this.apiOptions.path = options.path;
|
|
5204
|
+
console.log('🔧 Temporarily set apiOptions.path to:', options.path);
|
|
5205
|
+
}
|
|
3838
5206
|
const requestOptions = this.updateRequestOptions(options?.headers);
|
|
3839
|
-
console.log('🌐 Making GET request to path:',
|
|
3840
|
-
return this.httpManagerService.getRequest(requestOptions
|
|
5207
|
+
console.log('🌐 Making GET request to path:', this.apiOptions.path);
|
|
5208
|
+
return this.httpManagerService.getRequest(requestOptions)
|
|
3841
5209
|
.pipe(tap((data) => {
|
|
3842
5210
|
console.log('📦 fetchRecord received data:', data);
|
|
5211
|
+
// Restore original path after request completes
|
|
5212
|
+
this.apiOptions.path = originalPath;
|
|
5213
|
+
console.log('🔧 Restored apiOptions.path to:', originalPath);
|
|
3843
5214
|
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
|
|
3844
5215
|
const id = options.path?.length ? options.path[options.path.length - 1] : null;
|
|
3845
5216
|
if (method === 'DELETE') {
|
|
@@ -3861,8 +5232,16 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3861
5232
|
return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, id);
|
|
3862
5233
|
if (method === 'UPDATE' && data)
|
|
3863
5234
|
return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
|
|
3864
|
-
if (method === 'CREATE' && data)
|
|
3865
|
-
|
|
5235
|
+
if (method === 'CREATE' && data) {
|
|
5236
|
+
// Validate that data has a valid id before saving to IndexedDB
|
|
5237
|
+
if (data && (data.id !== undefined && data.id !== null && data.id !== '')) {
|
|
5238
|
+
console.log('💾 Saving to IndexedDB:', { table: this.databaseOptions.table, id: data.id, data });
|
|
5239
|
+
return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
|
|
5240
|
+
}
|
|
5241
|
+
else {
|
|
5242
|
+
console.warn('⚠️ Skipping IndexedDB save: data.id is invalid', data);
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
3866
5245
|
}
|
|
3867
5246
|
return of(data);
|
|
3868
5247
|
}));
|
|
@@ -3875,8 +5254,8 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3875
5254
|
.pipe(tap((data) => {
|
|
3876
5255
|
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
|
|
3877
5256
|
this.addData$(data);
|
|
3878
|
-
if
|
|
3879
|
-
|
|
5257
|
+
// Always call wsCommunication - it will queue if not connected
|
|
5258
|
+
this.wsCommunication('CREATE', [...options?.path || [], data.id]);
|
|
3880
5259
|
}), concatMap((data) => {
|
|
3881
5260
|
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
3882
5261
|
return this.dbManagerService.createTableRecord(this.databaseOptions.table, data);
|
|
@@ -3892,8 +5271,8 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3892
5271
|
.pipe(tap((data) => {
|
|
3893
5272
|
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
|
|
3894
5273
|
this.updateData$(data);
|
|
3895
|
-
if
|
|
3896
|
-
|
|
5274
|
+
// Always call wsCommunication - it will queue if not connected
|
|
5275
|
+
this.wsCommunication('UPDATE', [...options?.path || []]);
|
|
3897
5276
|
}), concatMap((data) => {
|
|
3898
5277
|
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
3899
5278
|
return this.dbManagerService.updateTableRecord(this.databaseOptions.table, data);
|
|
@@ -3909,8 +5288,8 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3909
5288
|
.pipe(tap((data) => {
|
|
3910
5289
|
data = (!data) ? (this.dataType === DataType.ARRAY) ? [] : {} : data;
|
|
3911
5290
|
this.deleteData$(data);
|
|
3912
|
-
if
|
|
3913
|
-
|
|
5291
|
+
// Always call wsCommunication - it will queue if not connected
|
|
5292
|
+
this.wsCommunication('DELETE', [...options?.path || []]);
|
|
3914
5293
|
}), concatMap((data) => {
|
|
3915
5294
|
if (this.hasDatabase && this.databaseOptions?.table && data?.id) {
|
|
3916
5295
|
return this.dbManagerService.deleteTableRecord(this.databaseOptions.table, data.id);
|
|
@@ -3969,23 +5348,34 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
3969
5348
|
return of([]);
|
|
3970
5349
|
}));
|
|
3971
5350
|
})));
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
this.
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
5351
|
+
try {
|
|
5352
|
+
this.databaseOptions = database;
|
|
5353
|
+
this.maxRetries = this.apiOptions.ws?.retry?.times || 3;
|
|
5354
|
+
this.retryDelay = (this.apiOptions.ws?.retry?.delay && this.apiOptions.ws.retry.delay * 1000) || 5 * 1000;
|
|
5355
|
+
// Start next retry countdown at 0 to avoid showing 5000 pre-connection
|
|
5356
|
+
this.wsNextRetry = new BehaviorSubject(0);
|
|
5357
|
+
this.wsNextRetry$ = this.wsNextRetry.asObservable();
|
|
5358
|
+
this.setApiRequestOptions(apiOptions, dataType, database);
|
|
5359
|
+
if (this.databaseOptions && this.databaseOptions.table) {
|
|
5360
|
+
this.localStorageManagerService.createStore({
|
|
5361
|
+
name: this.databaseOptions.table,
|
|
5362
|
+
data: { ...this.databaseOptions, ...{ expires: this.utils.expires(this.databaseOptions.expiresIn) } },
|
|
5363
|
+
options: SettingOptions.adapt({
|
|
5364
|
+
storage: StorageType.GLOBAL,
|
|
5365
|
+
encrypted: false,
|
|
5366
|
+
})
|
|
5367
|
+
});
|
|
5368
|
+
this.initDBStorage();
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
catch (error) {
|
|
5372
|
+
console.error('Error initializing HTTPManagerStateService:', error);
|
|
5373
|
+
// Initialize with safe defaults
|
|
5374
|
+
this.databaseOptions = undefined;
|
|
5375
|
+
this.maxRetries = 3;
|
|
5376
|
+
this.retryDelay = 5000;
|
|
5377
|
+
this.wsNextRetry = new BehaviorSubject(0);
|
|
5378
|
+
this.wsNextRetry$ = this.wsNextRetry.asObservable();
|
|
3989
5379
|
}
|
|
3990
5380
|
}
|
|
3991
5381
|
/**
|
|
@@ -4042,8 +5432,13 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4042
5432
|
console.error('WSOptions invalid: wsServer is missing or empty');
|
|
4043
5433
|
return;
|
|
4044
5434
|
}
|
|
5435
|
+
// Clean up previous subscription to prevent duplicate handlers
|
|
5436
|
+
if (this.connectionStatusSubscription) {
|
|
5437
|
+
this.connectionStatusSubscription.unsubscribe();
|
|
5438
|
+
this.connectionStatusSubscription = undefined;
|
|
5439
|
+
}
|
|
4045
5440
|
// Setup connection status monitoring (internal subscription to drive retry counters)
|
|
4046
|
-
this.setupConnectionStatus().subscribe();
|
|
5441
|
+
this.connectionStatusSubscription = this.setupConnectionStatus().subscribe();
|
|
4047
5442
|
// Make initial connection attempt
|
|
4048
5443
|
console.log('🔄 Initial WebSocket connection attempt...');
|
|
4049
5444
|
this.httpManagerService.connect(this.apiOptions.ws, this.apiOptions.ws.jwtToken || '');
|
|
@@ -4175,10 +5570,46 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4175
5570
|
}
|
|
4176
5571
|
// WEBSOCKET COMMUNICATION (STATE MANAGER)
|
|
4177
5572
|
wsCommunication(method, path) {
|
|
4178
|
-
if (this.
|
|
5573
|
+
if (!this.apiOptions.ws) {
|
|
5574
|
+
console.warn('wsCommunication called but no WebSocket options configured');
|
|
5575
|
+
return;
|
|
5576
|
+
}
|
|
5577
|
+
// If connected, send immediately (check singleton WebSocketManagerService)
|
|
5578
|
+
if (WebSocketManagerService.isConnected()) {
|
|
5579
|
+
this.sendWsCommunication(method, path);
|
|
5580
|
+
}
|
|
5581
|
+
else {
|
|
5582
|
+
// Queue the message to be sent when connection is established
|
|
5583
|
+
console.log(`⏳ Queuing WS message (not connected): ${method} ${path ? JSON.stringify(path) : ''}`);
|
|
5584
|
+
HTTPManagerStateService.wsCommunicationQueue.push({ method, path });
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
/**
|
|
5588
|
+
* Actually send the WebSocket message (called when connected or from queue)
|
|
5589
|
+
*/
|
|
5590
|
+
sendWsCommunication(method, path) {
|
|
5591
|
+
if (this.apiOptions.ws) {
|
|
4179
5592
|
const wsServer = this.apiOptions.ws.id;
|
|
5593
|
+
// Guard: Don't send if channel is empty
|
|
5594
|
+
if (!wsServer || wsServer === '') {
|
|
5595
|
+
console.error('❌ Cannot send WS message: Channel ID is empty!');
|
|
5596
|
+
return;
|
|
5597
|
+
}
|
|
5598
|
+
// DEBUG: Log what we're sending
|
|
5599
|
+
console.log('🔍 [DEBUG] sendWsCommunication called:', {
|
|
5600
|
+
wsServer,
|
|
5601
|
+
wsServerType: typeof wsServer,
|
|
5602
|
+
wsServerEmpty: wsServer === '' || wsServer === null || wsServer === undefined,
|
|
5603
|
+
method,
|
|
5604
|
+
path,
|
|
5605
|
+
user: this.apiOptions.ws.user,
|
|
5606
|
+
fullPayload: { method, path, user: this.apiOptions.ws.user }
|
|
5607
|
+
});
|
|
4180
5608
|
this.httpManagerService.sendMessageInChannel(wsServer, { method, path, user: this.apiOptions.ws.user });
|
|
4181
5609
|
}
|
|
5610
|
+
else {
|
|
5611
|
+
console.error('❌ [DEBUG] sendWsCommunication: apiOptions.ws is undefined!');
|
|
5612
|
+
}
|
|
4182
5613
|
}
|
|
4183
5614
|
/**
|
|
4184
5615
|
* Send a message to channel(s)
|
|
@@ -4190,19 +5621,18 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4190
5621
|
const user = this.user.value;
|
|
4191
5622
|
const messageInfo = ChannelMessage.adapt({ ...message, fromUser: user });
|
|
4192
5623
|
console.log('📤 wsMessaging called with channels:', channels);
|
|
4193
|
-
if (
|
|
5624
|
+
if (WebSocketManagerService.isConnected() && this.apiOptions.ws) {
|
|
4194
5625
|
// If specific channels provided, send to each channel
|
|
4195
5626
|
// Channels are passed as-is - caller is responsible for including the correct prefix
|
|
4196
5627
|
if (channels && channels.length > 0) {
|
|
4197
5628
|
console.log(`📤 Sending to ${channels.length} channel(s):`, channels);
|
|
4198
|
-
channels
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
});
|
|
5629
|
+
// Send single message with all channels (more efficient)
|
|
5630
|
+
const user = this.user.value;
|
|
5631
|
+
const messageData = {
|
|
5632
|
+
sessionId: user,
|
|
5633
|
+
content: messageInfo
|
|
5634
|
+
};
|
|
5635
|
+
this.httpManagerService.sendChannelMessageToChannels(channels, messageData);
|
|
4206
5636
|
}
|
|
4207
5637
|
else {
|
|
4208
5638
|
// Fallback to the primary WS channel (already prefixed with SYS-)
|
|
@@ -4217,7 +5647,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4217
5647
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4218
5648
|
*/
|
|
4219
5649
|
subscribeToMessageChannel(channel) {
|
|
4220
|
-
if (
|
|
5650
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4221
5651
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
|
|
4222
5652
|
this.httpManagerService.subscribeToChannel(prefixedChannel);
|
|
4223
5653
|
console.log(`💬 Subscribed to message channel: ${prefixedChannel}`);
|
|
@@ -4231,7 +5661,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4231
5661
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4232
5662
|
*/
|
|
4233
5663
|
unsubscribeFromMessageChannel(channel) {
|
|
4234
|
-
if (
|
|
5664
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4235
5665
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.MESSAGE);
|
|
4236
5666
|
this.httpManagerService.unsubscribeFromChannel(prefixedChannel);
|
|
4237
5667
|
console.log(`💬 Unsubscribed from message channel: ${prefixedChannel}`);
|
|
@@ -4247,8 +5677,11 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4247
5677
|
* Use subscribeToMessageChannel() for MES- prefixed channels
|
|
4248
5678
|
*/
|
|
4249
5679
|
subscribeToChannel(channel) {
|
|
4250
|
-
if (
|
|
4251
|
-
|
|
5680
|
+
if (WebSocketManagerService.isConnected()) {
|
|
5681
|
+
// Get current user data to send with subscription
|
|
5682
|
+
const currentUser = this.user.value;
|
|
5683
|
+
console.log('👤 Subscribing with user:', currentUser);
|
|
5684
|
+
this.httpManagerService.subscribeToChannel(channel, currentUser);
|
|
4252
5685
|
}
|
|
4253
5686
|
else {
|
|
4254
5687
|
console.warn('Cannot subscribe: WebSocket not connected.');
|
|
@@ -4258,8 +5691,11 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4258
5691
|
* Subscribe to multiple channels at once
|
|
4259
5692
|
*/
|
|
4260
5693
|
subscribeToChannels(channels) {
|
|
4261
|
-
if (
|
|
4262
|
-
|
|
5694
|
+
if (WebSocketManagerService.isConnected()) {
|
|
5695
|
+
// Get current user data to send with subscription
|
|
5696
|
+
const currentUser = this.user.value;
|
|
5697
|
+
console.log('👤 Subscribing to', channels.length, 'channels with user:', currentUser);
|
|
5698
|
+
this.httpManagerService.subscribeToChannels(channels, currentUser);
|
|
4263
5699
|
}
|
|
4264
5700
|
else {
|
|
4265
5701
|
console.warn('Cannot subscribe: WebSocket not connected.');
|
|
@@ -4269,7 +5705,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4269
5705
|
* Unsubscribe from a channel
|
|
4270
5706
|
*/
|
|
4271
5707
|
unsubscribeFromChannel(channel) {
|
|
4272
|
-
if (
|
|
5708
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4273
5709
|
this.httpManagerService.unsubscribeFromChannel(channel);
|
|
4274
5710
|
}
|
|
4275
5711
|
else {
|
|
@@ -4292,7 +5728,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4292
5728
|
* Create a new channel on the server
|
|
4293
5729
|
*/
|
|
4294
5730
|
createChannel(channel) {
|
|
4295
|
-
if (
|
|
5731
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4296
5732
|
this.httpManagerService.createChannel(channel);
|
|
4297
5733
|
}
|
|
4298
5734
|
else {
|
|
@@ -4303,7 +5739,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4303
5739
|
* Delete a channel from the server
|
|
4304
5740
|
*/
|
|
4305
5741
|
deleteChannel(channel) {
|
|
4306
|
-
if (
|
|
5742
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4307
5743
|
this.httpManagerService.deleteChannel(channel);
|
|
4308
5744
|
}
|
|
4309
5745
|
else {
|
|
@@ -4314,7 +5750,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4314
5750
|
* Request list of all channels from server
|
|
4315
5751
|
*/
|
|
4316
5752
|
getAllChannels() {
|
|
4317
|
-
if (
|
|
5753
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4318
5754
|
this.httpManagerService.getAllChannels();
|
|
4319
5755
|
}
|
|
4320
5756
|
else {
|
|
@@ -4325,7 +5761,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4325
5761
|
* Get users in a specific channel
|
|
4326
5762
|
*/
|
|
4327
5763
|
getUsersInChannel(channel) {
|
|
4328
|
-
if (
|
|
5764
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4329
5765
|
this.httpManagerService.getUsersInChannel(channel);
|
|
4330
5766
|
}
|
|
4331
5767
|
else {
|
|
@@ -4339,7 +5775,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4339
5775
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4340
5776
|
*/
|
|
4341
5777
|
createNotificationChannel(channel) {
|
|
4342
|
-
if (
|
|
5778
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4343
5779
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
4344
5780
|
this.httpManagerService.createNotificationChannel(prefixedChannel);
|
|
4345
5781
|
console.log(`📢 Creating notification channel: ${prefixedChannel}`);
|
|
@@ -4352,7 +5788,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4352
5788
|
* Request list of all notification channels from server (in-memory)
|
|
4353
5789
|
*/
|
|
4354
5790
|
getNotificationChannels() {
|
|
4355
|
-
if (
|
|
5791
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4356
5792
|
this.httpManagerService.getNotificationChannels();
|
|
4357
5793
|
}
|
|
4358
5794
|
else {
|
|
@@ -4364,7 +5800,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4364
5800
|
* Returns unique channels that have notifications posted today
|
|
4365
5801
|
*/
|
|
4366
5802
|
getTodaysNotificationChannels() {
|
|
4367
|
-
if (
|
|
5803
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4368
5804
|
this.httpManagerService.getTodaysNotificationChannels();
|
|
4369
5805
|
}
|
|
4370
5806
|
else {
|
|
@@ -4376,7 +5812,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4376
5812
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4377
5813
|
*/
|
|
4378
5814
|
subscribeToNotificationChannel(channel, options, user) {
|
|
4379
|
-
if (
|
|
5815
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4380
5816
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
4381
5817
|
this.httpManagerService.subscribeToNotificationChannel(prefixedChannel, options, user);
|
|
4382
5818
|
console.log(`📢 Subscribing to notification channel: ${prefixedChannel}`);
|
|
@@ -4390,7 +5826,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4390
5826
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4391
5827
|
*/
|
|
4392
5828
|
unsubscribeFromNotificationChannel(channel) {
|
|
4393
|
-
if (
|
|
5829
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4394
5830
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
4395
5831
|
this.httpManagerService.unsubscribeFromNotificationChannel(prefixedChannel);
|
|
4396
5832
|
console.log(`📢 Unsubscribing from notification channel: ${prefixedChannel}`);
|
|
@@ -4404,7 +5840,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
4404
5840
|
* @param channel - Base channel name (MES- prefix added automatically)
|
|
4405
5841
|
*/
|
|
4406
5842
|
sendNotification(channel, content) {
|
|
4407
|
-
if (
|
|
5843
|
+
if (WebSocketManagerService.isConnected()) {
|
|
4408
5844
|
const prefixedChannel = this.prefixChannel(channel, ChannelType.NOTIFICATION);
|
|
4409
5845
|
this.httpManagerService.sendNotification(prefixedChannel, content);
|
|
4410
5846
|
console.log(`📢 Sending notification to channel: ${prefixedChannel}`);
|
|
@@ -5823,17 +7259,25 @@ class StateServiceDemo extends HTTPManagerStateService {
|
|
|
5823
7259
|
}
|
|
5824
7260
|
/**
|
|
5825
7261
|
* Initialize WebSocket connection with server configuration
|
|
7262
|
+
* @param server - Backend server URL
|
|
7263
|
+
* @param wsServer - WebSocket server URL
|
|
7264
|
+
* @param jwtToken - JWT authentication token
|
|
7265
|
+
* @param user - User information
|
|
7266
|
+
* @param path - Path for constructing channel name (e.g., ['ai','tests'])
|
|
5826
7267
|
*/
|
|
5827
|
-
updateConnection(server, wsServer, jwtToken, user) {
|
|
7268
|
+
updateConnection(server, wsServer, jwtToken, user, path = ['ai', 'tests']) {
|
|
7269
|
+
// Construct channel name from path: ['ai','tests'] → 'ai/tests'
|
|
7270
|
+
const channelId = path.join('/');
|
|
5828
7271
|
this.setApiRequestOptions({
|
|
5829
7272
|
server,
|
|
7273
|
+
path, // Set the path for HTTP requests
|
|
5830
7274
|
retry: RetryOptions.adapt({
|
|
5831
7275
|
times: 3,
|
|
5832
7276
|
delay: 1,
|
|
5833
7277
|
}),
|
|
5834
7278
|
adapter: OIDCClient.adapt,
|
|
5835
7279
|
ws: {
|
|
5836
|
-
id: 'USERS123'
|
|
7280
|
+
id: channelId, // Use path-based channel ID instead of hardcoded 'USERS123'
|
|
5837
7281
|
wsServer,
|
|
5838
7282
|
jwtToken,
|
|
5839
7283
|
user,
|
|
@@ -6158,7 +7602,7 @@ class StateDataRequestService extends HTTPManagerStateService {
|
|
|
6158
7602
|
this.nextRetry$ = this.wsNextRetry$;
|
|
6159
7603
|
this.path = ['ai', 'tests'];
|
|
6160
7604
|
}
|
|
6161
|
-
updateConnection(server, wsServer, jwtToken, user, path = []
|
|
7605
|
+
updateConnection(server, wsServer, jwtToken, user, path = []) {
|
|
6162
7606
|
this.path = path;
|
|
6163
7607
|
this.setApiRequestOptions({
|
|
6164
7608
|
server,
|
|
@@ -6168,7 +7612,7 @@ class StateDataRequestService extends HTTPManagerStateService {
|
|
|
6168
7612
|
}),
|
|
6169
7613
|
adapter: OIDCClient.adapt,
|
|
6170
7614
|
ws: {
|
|
6171
|
-
id:
|
|
7615
|
+
id: this.path.join('/'),
|
|
6172
7616
|
wsServer,
|
|
6173
7617
|
jwtToken,
|
|
6174
7618
|
user, // general info about user
|
|
@@ -6219,7 +7663,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
6219
7663
|
class WsDataControlComponent {
|
|
6220
7664
|
constructor() {
|
|
6221
7665
|
this.path = ['ai', 'tests'];
|
|
6222
|
-
this.wsChannel = '';
|
|
6223
7666
|
this.stateDataRequestService = inject(StateDataRequestService);
|
|
6224
7667
|
this.user$ = this.stateDataRequestService.user$;
|
|
6225
7668
|
this.users$ = this.stateDataRequestService.userList$;
|
|
@@ -6231,7 +7674,7 @@ class WsDataControlComponent {
|
|
|
6231
7674
|
};
|
|
6232
7675
|
}
|
|
6233
7676
|
ngOnInit() {
|
|
6234
|
-
this.stateDataRequestService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path
|
|
7677
|
+
this.stateDataRequestService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
|
|
6235
7678
|
this.stateDataRequestService.getData();
|
|
6236
7679
|
}
|
|
6237
7680
|
onGetData() {
|
|
@@ -6256,11 +7699,11 @@ class WsDataControlComponent {
|
|
|
6256
7699
|
this.stateDataRequestService.deleteData(lastRec);
|
|
6257
7700
|
}
|
|
6258
7701
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsDataControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6259
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsDataControlComponent, selector: "app-ws-data-control", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path"
|
|
7702
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsDataControlComponent, selector: "app-ws-data-control", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path" }, ngImport: i0, template: "@if ((data$ | async); as data) {\n <div style=\"margin: 1rem;\">\n @if ((users$ |async); as users) {\n <div>\n @if (users.length > 0) {\n <h3 style=\"margin: 0;\">Connected Users</h3>\n } @else {\n <h3 style=\"margin: 0;\">No Users</h3>\n }\n <mat-chip-set>\n @for (user of users; track $index) {\n <mat-chip\n [class.user-chip--primary]=\"isUser(user, (user$ | async))\"\n [style.color]=\"isUser(user, (user$ | async)) ? '#fff' : null\"\n [disableRipple]=\"true\"\n >\n {{ user.name || user.ldap || user.id || 'Anonymous' }}\n </mat-chip>\n }\n </mat-chip-set>\n </div>\n }\n <div style=\"margin-top: 1rem; margin-bottom: 1rem;\">\n <mat-divider></mat-divider>\n </div>\n\n <div class=\"box\" style=\"margin-bottom: 1rem;\" *ngIf=\"(userAction$ | async) as userAction\">\n {{ userAction?.content?.user?.name }} has {{ userAction?.content?.method }}\n </div>\n\n <h3 style=\"margin: 0;\">Data Actions</h3>\n <div style=\"display: flex; gap: 1rem; margin-bottom: 1rem;\">\n <button mat-stroked-button (click)=\"onGetData()\">Get Data</button>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button color=\"accent\" (click)=\"onUpdateData(data)\">Update Data</button>\n <button mat-stroked-button color=\"warn\" (click)=\"onRemoveData(data)\">Remove Data</button>\n <button mat-stroked-button color=\"primary\" (click)=\"onAddData()\">Add Data</button>\n </div>\n @if (data.length > 0) {\n <div>\n <table mat-table [dataSource]=\"data\" style=\"border: 1px solid grey;\">\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef> ID </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.id}} </td>\n </ng-container>\n <ng-container matColumnDef=\"spiffe\">\n <th mat-header-cell *matHeaderCellDef> Spiffe </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.spiffe}} </td>\n </ng-container>\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef> Name </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.first_name}} {{element.last_name}}</td>\n </ng-container>\n <ng-container matColumnDef=\"email\">\n <th mat-header-cell *matHeaderCellDef> Email </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.email}} </td>\n </ng-container>\n <tr mat-header-row *matHeaderRowDef=\"['id', 'spiffe', 'name', 'email']\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: ['id', 'spiffe', 'name', 'email'];\"></tr>\n </table>\n <div style=\"border: 1px solid grey; padding: .5rem; border-top: none;\">\n <h3 style=\"margin: 0;\">Total Records {{ data.length }}</h3>\n </div>\n </div>\n } @else {\n <div style=\"margin-top: 1rem; font-style: italic;\">\n No Data Available\n </div>\n }\n </div>\n}\n\n", styles: [".user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}.user-chip--primary :is(.mdc-evolution-chip__text-label,.mdc-evolution-chip__action,.mdc-evolution-chip__cell,.mat-mdc-chip-action-label){color:#fff!important}.user-chip--primary,.user-chip--primary *{color:#fff!important}:host ::ng-deep .user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}:host ::ng-deep .user-chip--primary .mdc-evolution-chip__text-label,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__action,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__cell,:host ::ng-deep .user-chip--primary .mat-mdc-chip-action-label,:host ::ng-deep .user-chip--primary *{color:#fff!important}.box{padding:.5rem;border:1px solid rgb(174,174,13);background-color:#ececaf}\n"], dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$1.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "component", type: i3$1.MatChipSet, selector: "mat-chip-set", inputs: ["disabled", "role", "tabIndex"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
|
|
6260
7703
|
}
|
|
6261
7704
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsDataControlComponent, decorators: [{
|
|
6262
7705
|
type: Component,
|
|
6263
|
-
args: [{ selector: 'app-ws-data-control', standalone: false, template: "@if ((data$ | async); as data) {\n <div style=\"margin: 1rem;\">\n @if ((users$ |async); as users) {\n <div>\n @if (users.length > 0) {\n <h3 style=\"margin: 0;\">Connected Users</h3>\n } @else {\n <h3 style=\"margin: 0;\">No Users</h3>\n }\n <mat-chip-set>\n @for (user of users; track
|
|
7706
|
+
args: [{ selector: 'app-ws-data-control', standalone: false, template: "@if ((data$ | async); as data) {\n <div style=\"margin: 1rem;\">\n @if ((users$ |async); as users) {\n <div>\n @if (users.length > 0) {\n <h3 style=\"margin: 0;\">Connected Users</h3>\n } @else {\n <h3 style=\"margin: 0;\">No Users</h3>\n }\n <mat-chip-set>\n @for (user of users; track $index) {\n <mat-chip\n [class.user-chip--primary]=\"isUser(user, (user$ | async))\"\n [style.color]=\"isUser(user, (user$ | async)) ? '#fff' : null\"\n [disableRipple]=\"true\"\n >\n {{ user.name || user.ldap || user.id || 'Anonymous' }}\n </mat-chip>\n }\n </mat-chip-set>\n </div>\n }\n <div style=\"margin-top: 1rem; margin-bottom: 1rem;\">\n <mat-divider></mat-divider>\n </div>\n\n <div class=\"box\" style=\"margin-bottom: 1rem;\" *ngIf=\"(userAction$ | async) as userAction\">\n {{ userAction?.content?.user?.name }} has {{ userAction?.content?.method }}\n </div>\n\n <h3 style=\"margin: 0;\">Data Actions</h3>\n <div style=\"display: flex; gap: 1rem; margin-bottom: 1rem;\">\n <button mat-stroked-button (click)=\"onGetData()\">Get Data</button>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button color=\"accent\" (click)=\"onUpdateData(data)\">Update Data</button>\n <button mat-stroked-button color=\"warn\" (click)=\"onRemoveData(data)\">Remove Data</button>\n <button mat-stroked-button color=\"primary\" (click)=\"onAddData()\">Add Data</button>\n </div>\n @if (data.length > 0) {\n <div>\n <table mat-table [dataSource]=\"data\" style=\"border: 1px solid grey;\">\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef> ID </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.id}} </td>\n </ng-container>\n <ng-container matColumnDef=\"spiffe\">\n <th mat-header-cell *matHeaderCellDef> Spiffe </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.spiffe}} </td>\n </ng-container>\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef> Name </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.first_name}} {{element.last_name}}</td>\n </ng-container>\n <ng-container matColumnDef=\"email\">\n <th mat-header-cell *matHeaderCellDef> Email </th>\n <td mat-cell *matCellDef=\"let element\"> {{element.email}} </td>\n </ng-container>\n <tr mat-header-row *matHeaderRowDef=\"['id', 'spiffe', 'name', 'email']\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: ['id', 'spiffe', 'name', 'email'];\"></tr>\n </table>\n <div style=\"border: 1px solid grey; padding: .5rem; border-top: none;\">\n <h3 style=\"margin: 0;\">Total Records {{ data.length }}</h3>\n </div>\n </div>\n } @else {\n <div style=\"margin-top: 1rem; font-style: italic;\">\n No Data Available\n </div>\n }\n </div>\n}\n\n", styles: [".user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}.user-chip--primary :is(.mdc-evolution-chip__text-label,.mdc-evolution-chip__action,.mdc-evolution-chip__cell,.mat-mdc-chip-action-label){color:#fff!important}.user-chip--primary,.user-chip--primary *{color:#fff!important}:host ::ng-deep .user-chip--primary{background-color:var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5))!important;color:#fff!important;--mdc-evolution-chip-container-color: var(--mdc-theme-primary, var(--md-sys-color-primary, #3f51b5));--mdc-evolution-chip-label-text-color: #fff}:host ::ng-deep .user-chip--primary .mdc-evolution-chip__text-label,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__action,:host ::ng-deep .user-chip--primary .mdc-evolution-chip__cell,:host ::ng-deep .user-chip--primary .mat-mdc-chip-action-label,:host ::ng-deep .user-chip--primary *{color:#fff!important}.box{padding:.5rem;border:1px solid rgb(174,174,13);background-color:#ececaf}\n"] }]
|
|
6264
7707
|
}], propDecorators: { server: [{
|
|
6265
7708
|
type: Input
|
|
6266
7709
|
}], wsServer: [{
|
|
@@ -6271,12 +7714,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
6271
7714
|
type: Input
|
|
6272
7715
|
}], path: [{
|
|
6273
7716
|
type: Input
|
|
6274
|
-
}], wsChannel: [{
|
|
6275
|
-
type: Input
|
|
6276
7717
|
}] } });
|
|
6277
7718
|
|
|
6278
7719
|
class WsMessagingComponent {
|
|
6279
7720
|
constructor() {
|
|
7721
|
+
this.path = ['ai', 'tests']; // Default path for channel name
|
|
6280
7722
|
this.destroy$ = new Subject();
|
|
6281
7723
|
this.fb = inject(FormBuilder);
|
|
6282
7724
|
this.messageService = inject(MessageServiceDemo);
|
|
@@ -6321,11 +7763,14 @@ class WsMessagingComponent {
|
|
|
6321
7763
|
return this.messages.get('content');
|
|
6322
7764
|
}
|
|
6323
7765
|
ngOnInit() {
|
|
6324
|
-
this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user);
|
|
7766
|
+
this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
|
|
6325
7767
|
// Only trigger once when connection becomes true
|
|
6326
7768
|
this.connectionStatus$.pipe(filter$1(status => status === true), take$1(1), takeUntil$1(this.destroy$)).subscribe(() => {
|
|
6327
|
-
//
|
|
6328
|
-
|
|
7769
|
+
// Wait a moment for subscription to be processed, then fetch channels
|
|
7770
|
+
setTimeout(() => {
|
|
7771
|
+
console.log('📋 Fetching channels after connection...');
|
|
7772
|
+
this.messageService.getAllChannels();
|
|
7773
|
+
}, 500); // 500ms delay to ensure subscription is processed
|
|
6329
7774
|
});
|
|
6330
7775
|
// Subscribe to latest messages and show toast notification
|
|
6331
7776
|
this.latestCommunicationMessages$.pipe(filter$1(message => !!message), takeUntil$1(this.destroy$)).subscribe((message) => {
|
|
@@ -6429,11 +7874,11 @@ class WsMessagingComponent {
|
|
|
6429
7874
|
this.content.reset();
|
|
6430
7875
|
}
|
|
6431
7876
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsMessagingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6432
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsMessagingComponent, selector: "app-ws-messaging", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user" }, ngImport: i0, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"], dependencies: [{ kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "directive", type: i5.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i3$1.MatChipListbox, selector: "mat-chip-listbox", inputs: ["multiple", "aria-orientation", "selectable", "compareWith", "required", "hideSingleSelectionIndicator", "value"], outputs: ["change"] }, { kind: "component", type: i3$1.MatChipOption, selector: "mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]", inputs: ["selectable", "selected"], outputs: ["selectionChange"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
|
|
7877
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: WsMessagingComponent, selector: "app-ws-messaging", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path" }, ngImport: i0, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel; let i = $index) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel; let i = $index) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"], dependencies: [{ kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "directive", type: i5.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i3$1.MatChipListbox, selector: "mat-chip-listbox", inputs: ["multiple", "aria-orientation", "selectable", "compareWith", "required", "hideSingleSelectionIndicator", "value"], outputs: ["change"] }, { kind: "component", type: i3$1.MatChipOption, selector: "mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]", inputs: ["selectable", "selected"], outputs: ["selectionChange"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
|
|
6433
7878
|
}
|
|
6434
7879
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: WsMessagingComponent, decorators: [{
|
|
6435
7880
|
type: Component,
|
|
6436
|
-
args: [{ selector: 'app-ws-messaging', standalone: false, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"] }]
|
|
7881
|
+
args: [{ selector: 'app-ws-messaging', standalone: false, template: "\n @if ((data$ | async); as data) {\n @if ((user$ | async); as user) {\n <div style=\"display: flex; gap: 1rem; flex-direction: column; margin-top: 1rem;\">\n\n <!-- Channel Creation Section -->\n <div style=\"padding: 1rem; background: #e3f2fd; border-radius: 4px;\">\n <strong>Create New Channel</strong>\n <div style=\"display: flex; flex-direction: column; margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Channel Name</mat-label>\n <input matInput [formControl]=\"newChannelName\" placeholder=\"Enter channel name\"\n (keydown.enter)=\"onCreateChannel()\">\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onCreateChannel()\"\n [disabled]=\"newChannelName.invalid\">\n Create Channel\n </button>\n </div>\n </div>\n </div>\n\n <!-- Messaging Section - only show when subscribed to channels -->\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n @if (subscribedChannels.length > 0) {\n <div style=\"padding: 1rem; background: #fff3e0; border-radius: 4px;\" [formGroup]=\"messages\">\n <strong>Send Message</strong>\n\n <!-- Channel Selection with Checkboxes -->\n <div style=\"margin-top: 0.5rem;\">\n <mat-form-field appearance=\"outline\" style=\"width: 100%;\">\n <mat-label>Select Channels</mat-label>\n <mat-select formControlName=\"selectedChannels\" multiple>\n <mat-select-trigger>\n @if (selectedChannels.value?.length === 1) {\n {{ selectedChannels.value[0] }}\n } @else if (selectedChannels.value?.length > 1) {\n {{ selectedChannels.value[0] }} (+{{ selectedChannels.value.length - 1 }} {{ selectedChannels.value.length === 2 ? 'other' : 'others' }})\n }\n </mat-select-trigger>\n @for (channel of subscribedChannels; track channel; let i = $index) {\n <mat-option [value]=\"channel\">{{ channel }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Message Input -->\n <div style=\"display: flex; flex-direction: column;\">\n <mat-form-field style=\"width: 100%;\" appearance=\"outline\">\n <mat-label>Message</mat-label>\n <textarea\n matInput placeholder=\"Type your message...\"\n formControlName=\"content\"\n (keydown.enter)=\"onSendMessage(user); $event.preventDefault()\"\n ></textarea>\n </mat-form-field>\n <div style=\"display: flex; justify-content: flex-end; margin-top: -0.5rem;\">\n <button mat-raised-button color=\"primary\" (click)=\"onSendMessage(user)\"\n [disabled]=\"messages.invalid\">\n Send\n </button>\n </div>\n </div>\n </div>\n }\n }\n\n </div>\n }\n }\n\n <!-- Channel List with Subscription Controls using Chips -->\n @if ((channels$ | async); as channels) {\n @if ((subscribedChannels$ | async); as subscribedChannels) {\n <div style=\"padding: 1rem; background: #f5f5f5; border-radius: 4px; margin-top: 1rem;\">\n <strong>Subscribe to Channel(s)</strong>\n <div style=\"margin-top: 0.5rem;\">\n @if (channels.length === 0) {\n <div style=\"color: #666; font-style: italic;\">No channels available. Create one above.</div>\n } @else {\n <mat-chip-listbox aria-label=\"Channel selection\" [multiple]=\"true\">\n @for (channel of channels; track channel; let i = $index) {\n <mat-chip-option\n [selected]=\"isSubscribed(channel, subscribedChannels)\"\n (click)=\"onChipClick(channel, subscribedChannels)\">\n {{ channel }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n }\n </div>\n </div>\n }\n }\n", styles: ["button[mat-raised-button],button[mat-stroked-button]{min-width:120px}mat-chip-option{min-width:120px;justify-content:center;border-radius:4px!important;padding:.25rem!important}\n"] }]
|
|
6437
7882
|
}], propDecorators: { server: [{
|
|
6438
7883
|
type: Input
|
|
6439
7884
|
}], wsServer: [{
|
|
@@ -6442,6 +7887,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
6442
7887
|
type: Input
|
|
6443
7888
|
}], user: [{
|
|
6444
7889
|
type: Input
|
|
7890
|
+
}], path: [{
|
|
7891
|
+
type: Input
|
|
6445
7892
|
}] } });
|
|
6446
7893
|
|
|
6447
7894
|
class WsNotificationsComponent {
|
|
@@ -6668,7 +8115,6 @@ class RequestManagerWsDemoComponent {
|
|
|
6668
8115
|
this.stateService = inject(StateServiceDemo);
|
|
6669
8116
|
this.fb = inject(FormBuilder);
|
|
6670
8117
|
this.path = ['ai', 'tests'];
|
|
6671
|
-
this.wsChannel = '';
|
|
6672
8118
|
this.user$ = this.stateService.user$;
|
|
6673
8119
|
this.attempts$ = this.stateService.wsRetryAttempts$;
|
|
6674
8120
|
this.nextRetry$ = this.stateService.wsNextRetry$;
|
|
@@ -6677,14 +8123,14 @@ class RequestManagerWsDemoComponent {
|
|
|
6677
8123
|
this.isPending$ = this.stateService.isPending$;
|
|
6678
8124
|
}
|
|
6679
8125
|
ngOnInit() {
|
|
6680
|
-
this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user);
|
|
8126
|
+
this.stateService.updateConnection(this.server, this.wsServer, this.jwtToken, this.user, this.path);
|
|
6681
8127
|
}
|
|
6682
8128
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerWsDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6683
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerWsDemoComponent, selector: "app-request-manager-ws-demo", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path"
|
|
8129
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerWsDemoComponent, selector: "app-request-manager-ws-demo", inputs: { server: "server", wsServer: "wsServer", jwtToken: "jwtToken", user: "user", path: "path" }, ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2 style=\"display: flex;\">\n <span style=\"flex:1\">HTTP Request State Manager - Websockets</span>\n @if ((connectionStatus$ | async); as connected) {\n <span>\n WS -\n <span style=\"color: green;\">Connected</span>\n </span>\n } @else {\n <span style=\"color: red;\">Disconnected {{ attempts$ | async }} - {{ nextRetry$ |async }}</span>\n }\n </h2>\n\n <div>\n\n @if ((user$ | async); as userInfo) {\n <div>\n <mat-toolbar>\n <div style=\"display: flex; flex:1\">\n <div style=\"flex:1\">{{ userInfo.name }}</div>\n <div>({{ userInfo.ldap }})</div>\n </div>\n </mat-toolbar>\n </div>\n }\n\n @if ((isPending$ | async)) {\n <div>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </div>\n }\n\n <mat-tab-group animationDuration=\"0ms\" [selectedIndex]=\"1\">\n\n <mat-tab label=\"WS - Data Control\">\n <!-- DATA CONTROL -->\n <app-ws-data-control\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-data-control>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Messaging\">\n <!-- MESSAGING -->\n <app-ws-messaging\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-messaging>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Notifications\">\n <!-- WS - Notifications -->\n <app-ws-notifications\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n ></app-ws-notifications>\n </mat-tab>\n\n <mat-tab label=\"WS - Chats\" [disabled]=\"true\">\n <!-- WS - Chats -->\n <app-ws-chats></app-ws-chats>\n </mat-tab>\n\n <mat-tab label=\"WS - AI Messaging\" [disabled]=\"true\">\n <!-- WS - AI Messaging -->\n <app-ws-ai-messaging></app-ws-ai-messaging>\n </mat-tab>\n\n </mat-tab-group>\n</div>\n\n</div>\n\n", styles: [""], dependencies: [{ kind: "component", type: i1$2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i1$2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: i10.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i3$2.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "component", type: WsDataControlComponent, selector: "app-ws-data-control", inputs: ["server", "wsServer", "jwtToken", "user", "path"] }, { kind: "component", type: WsMessagingComponent, selector: "app-ws-messaging", inputs: ["server", "wsServer", "jwtToken", "user", "path"] }, { kind: "component", type: WsNotificationsComponent, selector: "app-ws-notifications", inputs: ["server", "wsServer", "jwtToken", "user"] }, { kind: "component", type: WsAiMessagingComponent, selector: "app-ws-ai-messaging" }, { kind: "component", type: WsChatsComponent, selector: "app-ws-chats" }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
|
|
6684
8130
|
}
|
|
6685
8131
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerWsDemoComponent, decorators: [{
|
|
6686
8132
|
type: Component,
|
|
6687
|
-
args: [{ selector: 'app-request-manager-ws-demo', standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2 style=\"display: flex;\">\n <span style=\"flex:1\">HTTP Request State Manager - Websockets</span>\n @if ((connectionStatus$ | async); as connected) {\n <span>\n WS -\n <span style=\"color: green;\">Connected</span>\n </span>\n } @else {\n <span style=\"color: red;\">Disconnected {{ attempts$ | async }} - {{ nextRetry$ |async }}</span>\n }\n </h2>\n\n <div>\n\n @if ((user$ | async); as userInfo) {\n <div>\n <mat-toolbar>\n <div style=\"display: flex; flex:1\">\n <div style=\"flex:1\">{{ userInfo.name }}</div>\n <div>({{ userInfo.ldap }})</div>\n </div>\n </mat-toolbar>\n </div>\n }\n\n @if ((isPending$ | async)) {\n <div>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </div>\n }\n\n <mat-tab-group animationDuration=\"0ms\" [selectedIndex]=\"1\">\n\n <mat-tab label=\"WS - Data Control\">\n <!-- DATA CONTROL -->\n <app-ws-data-control\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n
|
|
8133
|
+
args: [{ selector: 'app-request-manager-ws-demo', standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2 style=\"display: flex;\">\n <span style=\"flex:1\">HTTP Request State Manager - Websockets</span>\n @if ((connectionStatus$ | async); as connected) {\n <span>\n WS -\n <span style=\"color: green;\">Connected</span>\n </span>\n } @else {\n <span style=\"color: red;\">Disconnected {{ attempts$ | async }} - {{ nextRetry$ |async }}</span>\n }\n </h2>\n\n <div>\n\n @if ((user$ | async); as userInfo) {\n <div>\n <mat-toolbar>\n <div style=\"display: flex; flex:1\">\n <div style=\"flex:1\">{{ userInfo.name }}</div>\n <div>({{ userInfo.ldap }})</div>\n </div>\n </mat-toolbar>\n </div>\n }\n\n @if ((isPending$ | async)) {\n <div>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </div>\n }\n\n <mat-tab-group animationDuration=\"0ms\" [selectedIndex]=\"1\">\n\n <mat-tab label=\"WS - Data Control\">\n <!-- DATA CONTROL -->\n <app-ws-data-control\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-data-control>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Messaging\">\n <!-- MESSAGING -->\n <app-ws-messaging\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-ws-messaging>\n\n </mat-tab>\n\n <mat-tab label=\"WS - Notifications\">\n <!-- WS - Notifications -->\n <app-ws-notifications\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n ></app-ws-notifications>\n </mat-tab>\n\n <mat-tab label=\"WS - Chats\" [disabled]=\"true\">\n <!-- WS - Chats -->\n <app-ws-chats></app-ws-chats>\n </mat-tab>\n\n <mat-tab label=\"WS - AI Messaging\" [disabled]=\"true\">\n <!-- WS - AI Messaging -->\n <app-ws-ai-messaging></app-ws-ai-messaging>\n </mat-tab>\n\n </mat-tab-group>\n</div>\n\n</div>\n\n" }]
|
|
6688
8134
|
}], propDecorators: { server: [{
|
|
6689
8135
|
type: Input
|
|
6690
8136
|
}], wsServer: [{
|
|
@@ -6695,8 +8141,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
6695
8141
|
type: Input
|
|
6696
8142
|
}], path: [{
|
|
6697
8143
|
type: Input
|
|
6698
|
-
}], wsChannel: [{
|
|
6699
|
-
type: Input
|
|
6700
8144
|
}] } });
|
|
6701
8145
|
|
|
6702
8146
|
class Settings {
|
|
@@ -6917,7 +8361,6 @@ class HttpRequestServicesDemoComponent {
|
|
|
6917
8361
|
this.jwtToken = '';
|
|
6918
8362
|
this.server = 'http:';
|
|
6919
8363
|
this.path = ['ai', 'tests'];
|
|
6920
|
-
this.wsChannel = '';
|
|
6921
8364
|
this.requestTypes = [
|
|
6922
8365
|
{ name: "Http Service", value: 'http_service' },
|
|
6923
8366
|
// { name: "Http Signals Service", value: 'http_signals_service', new: true },
|
|
@@ -6938,11 +8381,11 @@ class HttpRequestServicesDemoComponent {
|
|
|
6938
8381
|
this.selectedService = this.requestTypes[type].value;
|
|
6939
8382
|
}
|
|
6940
8383
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HttpRequestServicesDemoComponent, deps: [{ token: CONFIG_SETTINGS_TOKEN }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6941
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: HttpRequestServicesDemoComponent, selector: "app-http-request-services-demo", inputs: { wsServer: "wsServer", jwtToken: "jwtToken", server: "server", user: "user", path: "path",
|
|
8384
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: HttpRequestServicesDemoComponent, selector: "app-http-request-services-demo", inputs: { wsServer: "wsServer", jwtToken: "jwtToken", server: "server", user: "user", path: "path", adapter: "adapter", mapper: "mapper" }, ngImport: i0, template: "<mat-toolbar style=\"display:flex\">\n <div>Http Request Manager Services</div>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button [matMenuTriggerFor]=\"menu\">Services</button>\n <mat-menu #menu=\"matMenu\">\n @for (type of requestTypes; track type; let i = $index) {\n @if (type?.divider) {\n <div\n style=\"margin-top: .5rem; margin-bottom: .5rem;\"\n >\n <mat-divider></mat-divider>\n </div>\n }\n <button\n mat-menu-item\n (click)=\"onSelected(i)\"\n [disabled]=\"type.disabled\"\n >\n {{ type.name }}\n </button>\n }\n\n </mat-menu>\n</mat-toolbar>\n\n<span>\n @switch (selectedService) {\n @case ('http_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'http_signals_service'\">\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-signals-manager-demo></app-request-signals-manager-demo>\n </p> -->\n @case ('http_state_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-state-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-state-demo>\n </p>\n }\n @case ('http_state_service_ws') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-ws-demo\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-request-manager-ws-demo>\n </p>\n }\n @case ('database_service') {\n <p>\n <app-database-data-demo></app-database-data-demo>\n </p>\n }\n @case ('local_storage_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-demo></app-local-storage-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'local_storage_signals_service'\">\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-signals-demo></app-local-storage-signals-demo>\n</p> -->\n@case ('store_state_manager') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-store-state-manager-demo></app-store-state-manager-demo>\n </p>\n}\n@default {\n <p>\n Other\n </p>\n}\n}\n</span>\n\n<ng-template #HTTP_OPTIONS>\n @if (injectionOptions?.httpRequestOptions) {\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - HTTP Options</h3>\n {{ injectionOptions?.httpRequestOptions| json }}\n </div>\n }\n</ng-template>\n\n<ng-template #LOCAL_OPTIONS>\n @if (injectionOptions?.LocalStorageOptions) {\n <ng-container class=\"box\">\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - LocalStorage Options</h3>\n {{ injectionOptions?.LocalStorageOptions| json }}\n </div>\n </ng-container>\n }\n</ng-template>\n\n\n", styles: [".box{padding:1rem;background-color:#f5f5f5;border:thin gray solid;margin-top:1rem}\n"], dependencies: [{ kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "component", type: i3$2.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "component", type: RequestManagerStateDemoComponent, selector: "app-request-manager-state-demo", inputs: ["server", "adapter", "mapper"] }, { kind: "component", type: RequestManagerDemoComponent, selector: "app-request-manager-demo", inputs: ["server", "adapter", "mapper"] }, { kind: "component", type: LocalStorageDemoComponent, selector: "app-local-storage-demo" }, { kind: "component", type: RequestManagerWsDemoComponent, selector: "app-request-manager-ws-demo", inputs: ["server", "wsServer", "jwtToken", "user", "path"] }, { kind: "component", type: StoreStateManagerDemoComponent, selector: "app-store-state-manager-demo" }, { kind: "component", type: DatabaseDataDemoComponent, selector: "app-database-data-demo" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }] }); }
|
|
6942
8385
|
}
|
|
6943
8386
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HttpRequestServicesDemoComponent, decorators: [{
|
|
6944
8387
|
type: Component,
|
|
6945
|
-
args: [{ selector: 'app-http-request-services-demo', standalone: false, template: "<mat-toolbar style=\"display:flex\">\n <div>Http Request Manager Services</div>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button [matMenuTriggerFor]=\"menu\">Services</button>\n <mat-menu #menu=\"matMenu\">\n @for (type of requestTypes; track type; let i = $index) {\n @if (type?.divider) {\n <div\n style=\"margin-top: .5rem; margin-bottom: .5rem;\"\n >\n <mat-divider></mat-divider>\n </div>\n }\n <button\n mat-menu-item\n (click)=\"onSelected(i)\"\n [disabled]=\"type.disabled\"\n >\n {{ type.name }}\n </button>\n }\n\n </mat-menu>\n</mat-toolbar>\n\n<span>\n @switch (selectedService) {\n @case ('http_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'http_signals_service'\">\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-signals-manager-demo></app-request-signals-manager-demo>\n </p> -->\n @case ('http_state_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-state-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-state-demo>\n </p>\n }\n @case ('http_state_service_ws') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-ws-demo\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [
|
|
8388
|
+
args: [{ selector: 'app-http-request-services-demo', standalone: false, template: "<mat-toolbar style=\"display:flex\">\n <div>Http Request Manager Services</div>\n <div style=\"flex:1\"></div>\n <button mat-stroked-button [matMenuTriggerFor]=\"menu\">Services</button>\n <mat-menu #menu=\"matMenu\">\n @for (type of requestTypes; track type; let i = $index) {\n @if (type?.divider) {\n <div\n style=\"margin-top: .5rem; margin-bottom: .5rem;\"\n >\n <mat-divider></mat-divider>\n </div>\n }\n <button\n mat-menu-item\n (click)=\"onSelected(i)\"\n [disabled]=\"type.disabled\"\n >\n {{ type.name }}\n </button>\n }\n\n </mat-menu>\n</mat-toolbar>\n\n<span>\n @switch (selectedService) {\n @case ('http_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'http_signals_service'\">\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-signals-manager-demo></app-request-signals-manager-demo>\n </p> -->\n @case ('http_state_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-state-demo\n [server]=\"server\"\n [adapter]=\"adapter\"\n [mapper]=\"mapper\"\n ></app-request-manager-state-demo>\n </p>\n }\n @case ('http_state_service_ws') {\n <p>\n <ng-container *ngTemplateOutlet=\"HTTP_OPTIONS\"></ng-container>\n <app-request-manager-ws-demo\n [server]=\"server\"\n [wsServer]=\"wsServer\"\n [jwtToken]=\"jwtToken\"\n [user]=\"user\"\n [path]=\"path\"\n ></app-request-manager-ws-demo>\n </p>\n }\n @case ('database_service') {\n <p>\n <app-database-data-demo></app-database-data-demo>\n </p>\n }\n @case ('local_storage_service') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-demo></app-local-storage-demo>\n </p>\n }\n <!-- <p *ngSwitchCase=\"'local_storage_signals_service'\">\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-local-storage-signals-demo></app-local-storage-signals-demo>\n</p> -->\n@case ('store_state_manager') {\n <p>\n <ng-container *ngTemplateOutlet=\"LOCAL_OPTIONS\"></ng-container>\n <app-store-state-manager-demo></app-store-state-manager-demo>\n </p>\n}\n@default {\n <p>\n Other\n </p>\n}\n}\n</span>\n\n<ng-template #HTTP_OPTIONS>\n @if (injectionOptions?.httpRequestOptions) {\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - HTTP Options</h3>\n {{ injectionOptions?.httpRequestOptions| json }}\n </div>\n }\n</ng-template>\n\n<ng-template #LOCAL_OPTIONS>\n @if (injectionOptions?.LocalStorageOptions) {\n <ng-container class=\"box\">\n <div class=\"box\">\n <h3 style=\"font-weight: bold;\">Injection Token Detected - LocalStorage Options</h3>\n {{ injectionOptions?.LocalStorageOptions| json }}\n </div>\n </ng-container>\n }\n</ng-template>\n\n\n", styles: [".box{padding:1rem;background-color:#f5f5f5;border:thin gray solid;margin-top:1rem}\n"] }]
|
|
6946
8389
|
}], ctorParameters: () => [{ type: ConfigOptions, decorators: [{
|
|
6947
8390
|
type: Inject,
|
|
6948
8391
|
args: [CONFIG_SETTINGS_TOKEN]
|
|
@@ -6956,8 +8399,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
6956
8399
|
type: Input
|
|
6957
8400
|
}], path: [{
|
|
6958
8401
|
type: Input
|
|
6959
|
-
}], wsChannel: [{
|
|
6960
|
-
type: Input
|
|
6961
8402
|
}], adapter: [{
|
|
6962
8403
|
type: Input
|
|
6963
8404
|
}], mapper: [{
|
|
@@ -7629,5 +9070,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
7629
9070
|
* Generated bundle index. Do not edit.
|
|
7630
9071
|
*/
|
|
7631
9072
|
|
|
7632
|
-
export { ApiRequest, AppService, AsymmetricalEncryptionService, CONFIG_SETTINGS_TOKEN, ChannelInfo, ChannelType, CommunicationType, ConfigHTTPOptions, ConfigOptions, DataType, DatabaseDataDemoComponent, DatabaseManagerService, DatabaseStorage, DbService, ErrorDisplaySettings, GlobalStoreOptions, HTTPManagerService, HTTPManagerSignalsService, HTTPManagerStateService, HeadersService, HttpRequestManagerModule, HttpRequestServicesDemoComponent, LocalStorageDemoComponent, LocalStorageManagerService, LocalStorageOptions, LocalStorageSignalsManagerService, PathQueryService, Random, RandomHSLColor, RandomHexColor, RandomNumber, RandomNumbers, RandomNumbersUnique, RandomPaletteColor, RandomSignature, RandomStr, RandomVisibleColor, RequestErrorInterceptor, RequestHeadersInterceptor, RequestManagerDemoComponent, RequestManagerStateDemoComponent, RequestOptions, RequestService, RequestSignalsService, RetryOptions, SettingOptions, StateStorageOptions, StorageData, StorageOption, StorageType, StoreStateManagerService, StreamType, SymmetricalEncryptionService, TableSchemaDef, UUID, UUID_STR, UserData, UtilsService, WSOptions, WSUser, WebsocketService, WithCredentialsInterceptor, countdown, createChannelName, delayedRetry, requestPolling, requestStreaming, streamAI, streamAuto, streamEvents, streamJSON, streamNDJSON };
|
|
9073
|
+
export { ApiRequest, AppService, AsymmetricalEncryptionService, CONFIG_SETTINGS_TOKEN, ChannelInfo, ChannelType, CommunicationType, ConfigHTTPOptions, ConfigOptions, DataType, DatabaseDataDemoComponent, DatabaseManagerService, DatabaseStorage, DbService, ErrorDisplaySettings, GlobalStoreOptions, HTTPManagerService, HTTPManagerSignalsService, HTTPManagerStateService, HeadersService, HttpRequestManagerModule, HttpRequestServicesDemoComponent, LocalStorageDemoComponent, LocalStorageManagerService, LocalStorageOptions, LocalStorageSignalsManagerService, PathQueryService, Random, RandomHSLColor, RandomHexColor, RandomNumber, RandomNumbers, RandomNumbersUnique, RandomPaletteColor, RandomSignature, RandomStr, RandomVisibleColor, RequestErrorInterceptor, RequestHeadersInterceptor, RequestManagerDemoComponent, RequestManagerStateDemoComponent, RequestOptions, RequestService, RequestSignalsService, RetryOptions, SettingOptions, StateStorageOptions, StorageData, StorageOption, StorageType, StoreStateManagerService, StreamType, SymmetricalEncryptionService, TableSchemaDef, UUID, UUID_STR, UserData, UtilsService, WSOptions, WSUser, WebSocketManagerService, WebsocketService, WithCredentialsInterceptor, countdown, createChannelName, delayedRetry, requestPolling, requestStreaming, streamAI, streamAuto, streamEvents, streamJSON, streamNDJSON };
|
|
7633
9074
|
//# sourceMappingURL=http-request-manager.mjs.map
|