@manonero/chat-client-sdk 1.0.0-beta.2 → 1.0.0-beta.4
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.
- package/README.md +204 -27
- package/dist/ChatClient.d.ts +67 -8
- package/dist/ChatClient.d.ts.map +1 -1
- package/dist/ChatClient.js +82 -9
- package/dist/ChatClient.js.map +1 -1
- package/dist/http/ConversationApi.d.ts +18 -1
- package/dist/http/ConversationApi.d.ts.map +1 -1
- package/dist/http/ConversationApi.js +23 -0
- package/dist/http/ConversationApi.js.map +1 -1
- package/dist/http/HttpClient.d.ts +18 -1
- package/dist/http/HttpClient.d.ts.map +1 -1
- package/dist/http/HttpClient.js +64 -13
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/http/MessageApi.d.ts.map +1 -1
- package/dist/http/MessageApi.js +13 -2
- package/dist/http/MessageApi.js.map +1 -1
- package/dist/http/ProxyApi.d.ts +14 -3
- package/dist/http/ProxyApi.d.ts.map +1 -1
- package/dist/http/ProxyApi.js +25 -7
- package/dist/http/ProxyApi.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/realtime/ChatHubClient.d.ts +58 -1
- package/dist/realtime/ChatHubClient.d.ts.map +1 -1
- package/dist/realtime/ChatHubClient.js +202 -15
- package/dist/realtime/ChatHubClient.js.map +1 -1
- package/dist/realtime/NotificationHubClient.d.ts +40 -0
- package/dist/realtime/NotificationHubClient.d.ts.map +1 -1
- package/dist/realtime/NotificationHubClient.js +131 -29
- package/dist/realtime/NotificationHubClient.js.map +1 -1
- package/dist/realtime/ReconnectionManager.d.ts +2 -1
- package/dist/realtime/ReconnectionManager.d.ts.map +1 -1
- package/dist/realtime/ReconnectionManager.js +13 -3
- package/dist/realtime/ReconnectionManager.js.map +1 -1
- package/dist/types/block.d.ts +17 -2
- package/dist/types/block.d.ts.map +1 -1
- package/dist/types/block.js +30 -0
- package/dist/types/block.js.map +1 -1
- package/dist/types/chat-events.d.ts +23 -3
- package/dist/types/chat-events.d.ts.map +1 -1
- package/dist/types/conversation.d.ts +52 -3
- package/dist/types/conversation.d.ts.map +1 -1
- package/dist/types/message.d.ts +5 -6
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/notification-events.d.ts +10 -7
- package/dist/types/notification-events.d.ts.map +1 -1
- package/dist/types/signalr.d.ts +2 -1
- package/dist/types/signalr.d.ts.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -171,7 +171,12 @@ const client = new ChatClient({
|
|
|
171
171
|
});
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
-
> **Ưu tiên token
|
|
174
|
+
> **Ưu tiên token (resolve mỗi request / connect):**
|
|
175
|
+
> 1. Nếu `tokenProvider` được cấu hình **VÀ** trả về string non-empty → dùng giá trị đó.
|
|
176
|
+
> 2. Ngược lại → dùng token nội bộ (`token` constructor / `setToken()` / auto-set sau login).
|
|
177
|
+
> 3. Ngược lại → `null` (không gửi `Authorization`).
|
|
178
|
+
>
|
|
179
|
+
> Provider luôn được hỏi trước nên các hệ thống refresh-token rotation tiếp tục hoạt động kể cả sau khi `setToken()` đã được gọi. Dùng `clearToken()` nếu muốn xoá token nội bộ và trả quyền hoàn toàn về provider.
|
|
175
180
|
|
|
176
181
|
### Cấu hình SignalR log level
|
|
177
182
|
|
|
@@ -205,6 +210,7 @@ new ChatClient(options: ChatClientOptions)
|
|
|
205
210
|
| `baseUrl` | `string` | ✅ | URL gốc của server, ví dụ `"https://chat-api.example.com"` |
|
|
206
211
|
| `token` | `string` | ❌ | JWT token ban đầu (nếu đã đăng nhập) |
|
|
207
212
|
| `tokenProvider` | `() => string \| null` | ❌ | Hàm lấy token từ bên ngoài |
|
|
213
|
+
| `requestTimeoutMs` | `number \| null` | ❌ | Timeout HTTP / upload tính bằng ms. Mặc định `30000`. Truyền `null` để tắt (hữu ích khi upload file lớn). |
|
|
208
214
|
| `signalrOptions.logLevel` | `LogLevel` | ❌ | Mức log cho SignalR (mặc định: `Warning`) |
|
|
209
215
|
|
|
210
216
|
### Thuộc tính
|
|
@@ -225,20 +231,52 @@ new ChatClient(options: ChatClientOptions)
|
|
|
225
231
|
|
|
226
232
|
### Phương thức
|
|
227
233
|
|
|
228
|
-
#### `setToken(token: string): void`
|
|
234
|
+
#### `setToken(token: string | null): void`
|
|
229
235
|
|
|
230
|
-
Cập nhật JWT token
|
|
236
|
+
Cập nhật JWT token nội bộ. Có hiệu lực **ngay** với mọi HTTP request tiếp theo. Truyền `null` để xoá token nội bộ và `currentUser` (tương đương `clearToken()`).
|
|
231
237
|
|
|
232
|
-
> ⚠️ Các kết nối SignalR
|
|
238
|
+
> ⚠️ Các kết nối SignalR **đang hoạt động** vẫn dùng token cũ cho đến lúc reconnect. Gọi `refreshConnections()` để áp token mới ngay cho hub đang Connected.
|
|
239
|
+
|
|
240
|
+
> ℹ️ Nếu `tokenProvider` đã được cấu hình và vẫn trả về giá trị, provider tiếp tục được ưu tiên — `setToken()` chỉ ảnh hưởng khi provider trả `null`/empty.
|
|
233
241
|
|
|
234
242
|
```ts
|
|
235
243
|
client.setToken('new-jwt-token-here');
|
|
244
|
+
await client.refreshConnections(); // tuỳ chọn — chỉ khi muốn áp ngay cho SignalR
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### `clearToken(): void`
|
|
248
|
+
|
|
249
|
+
Xoá token nội bộ và `currentUser`. Không động tới `tokenProvider` (provider tiếp tục hoạt động bình thường) và không ngắt SignalR. Dùng `logout()` nếu muốn ngắt cả hub.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
client.clearToken();
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### `refreshConnections(): Promise<void>`
|
|
256
|
+
|
|
257
|
+
`disconnect()` + `connect()` lại từng hub đang ở trạng thái `Connected`. Hub đang Disconnected/Reconnecting được giữ nguyên. Dùng để áp token mới (sau `setToken()`) cho phiên SignalR đang chạy.
|
|
258
|
+
|
|
259
|
+
> ⚠️ `disconnect()` xoá local tracking (`joinedConversations`, `subscribedPresenceIds`). Sau khi gọi `refreshConnections()`, ứng dụng phải tự `joinConversation` / `subscribeToPresence` lại — hoặc dùng `ReconnectionManager` để khôi phục trong suốt cho mọi loại disconnect.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
client.setToken(newToken);
|
|
263
|
+
await client.refreshConnections();
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### `logout(): Promise<void>`
|
|
267
|
+
|
|
268
|
+
Xoá token + `currentUser` rồi `disconnect()` cả hai hub. Chỉ clear local state — không gọi server (server không có endpoint logout). Nếu `tokenProvider` được cấu hình và vẫn trả token, request kế tiếp vẫn có thể auth lại.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
await client.logout();
|
|
236
272
|
```
|
|
237
273
|
|
|
238
274
|
#### `disconnect(): Promise<void>`
|
|
239
275
|
|
|
240
276
|
Ngắt kết nối tất cả các SignalR hub (ChatHub + NotificationHub) đồng thời.
|
|
241
277
|
|
|
278
|
+
> Cả hai hub set cờ `intentionallyClosed` trước khi tear down nên `ReconnectionManager` (nếu được attach) sẽ **bỏ qua** lần `disconnected` này thay vì auto-reconnect ngược ý người dùng.
|
|
279
|
+
|
|
242
280
|
```ts
|
|
243
281
|
await client.disconnect();
|
|
244
282
|
```
|
|
@@ -512,6 +550,39 @@ await client.conversations.markAsRead('conv-id', 'last-message-id');
|
|
|
512
550
|
await client.conversations.markAsUnread('conv-id');
|
|
513
551
|
```
|
|
514
552
|
|
|
553
|
+
### `getMedia(conversationId, params?): Promise<CursorPaginatedResult<ConversationMediaItemDto>>`
|
|
554
|
+
|
|
555
|
+
Liệt kê toàn bộ media (image/video/audio/file) và link (LinkPreview hoặc external URL) đã từng xuất hiện trong cuộc hội thoại — sắp xếp **newest-first**, cursor pagination.
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
// Mặc định: kind = 'all', limit = 50
|
|
559
|
+
const page1 = await client.conversations.getMedia('conv-id');
|
|
560
|
+
|
|
561
|
+
// Chỉ file/ảnh nội bộ
|
|
562
|
+
const attachments = await client.conversations.getMedia('conv-id', {
|
|
563
|
+
kind: 'attachment',
|
|
564
|
+
limit: 100,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Chỉ link / LinkPreview
|
|
568
|
+
const links = await client.conversations.getMedia('conv-id', { kind: 'link' });
|
|
569
|
+
|
|
570
|
+
// Trang tiếp theo
|
|
571
|
+
if (page1.hasMore) {
|
|
572
|
+
const page2 = await client.conversations.getMedia('conv-id', {
|
|
573
|
+
cursor: page1.nextCursor!,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
| Param | Type | Default | Mô tả |
|
|
579
|
+
|-------|------|---------|-------|
|
|
580
|
+
| `kind` | `'all' \| 'attachment' \| 'link'` | `'all'` | Lọc theo loại item |
|
|
581
|
+
| `limit` | number | 50 | 1..100 |
|
|
582
|
+
| `cursor` | string | — | `nextCursor` từ trang trước (opaque) |
|
|
583
|
+
|
|
584
|
+
> **Lưu ý:** Người dùng chỉ thấy media của các message từ thời điểm họ join cuộc hội thoại trở đi. Server trả `403` nếu caller không phải participant.
|
|
585
|
+
|
|
515
586
|
### Kiểu dữ liệu Conversation
|
|
516
587
|
|
|
517
588
|
```ts
|
|
@@ -555,6 +626,49 @@ interface ConversationListItemDto {
|
|
|
555
626
|
unreadCount: number;
|
|
556
627
|
participantCount: number;
|
|
557
628
|
}
|
|
629
|
+
|
|
630
|
+
// Media listing (getMedia)
|
|
631
|
+
type ConversationMediaKindFilter = 'all' | 'attachment' | 'link';
|
|
632
|
+
type ConversationMediaKind = 'Attachment' | 'Link';
|
|
633
|
+
type ConversationMediaBlockType = 'Image' | 'Video' | 'Audio' | 'File' | 'LinkPreview';
|
|
634
|
+
|
|
635
|
+
interface ConversationMediaItemDto {
|
|
636
|
+
id: string; // Composite "{messageId}#{blockIndex:D2}"
|
|
637
|
+
conversationId: string;
|
|
638
|
+
messageId: string;
|
|
639
|
+
blockIndex: number; // 0-based vị trí block trong message
|
|
640
|
+
senderId: string;
|
|
641
|
+
senderType: string; // 'User' | 'Bot' | 'System'
|
|
642
|
+
kind: ConversationMediaKind;
|
|
643
|
+
blockType: ConversationMediaBlockType;
|
|
644
|
+
createdAt: string; // ISO 8601 — thời điểm message được tạo
|
|
645
|
+
|
|
646
|
+
storageKey?: string | null; // set khi kind === 'Attachment'
|
|
647
|
+
url?: string | null; // set khi kind === 'Link'
|
|
648
|
+
|
|
649
|
+
// File / audio / video
|
|
650
|
+
fileName?: string | null;
|
|
651
|
+
mimeType?: string | null;
|
|
652
|
+
fileSizeBytes?: number | null;
|
|
653
|
+
|
|
654
|
+
// Image / video / audio
|
|
655
|
+
width?: number | null;
|
|
656
|
+
height?: number | null;
|
|
657
|
+
durationSeconds?: number | null;
|
|
658
|
+
caption?: string | null;
|
|
659
|
+
altText?: string | null;
|
|
660
|
+
|
|
661
|
+
// Image / video thumbnail
|
|
662
|
+
thumbnailStorageKey?: string | null;
|
|
663
|
+
thumbnailUrl?: string | null;
|
|
664
|
+
|
|
665
|
+
// LinkPreview metadata
|
|
666
|
+
linkTitle?: string | null;
|
|
667
|
+
linkDescription?: string | null;
|
|
668
|
+
linkSiteName?: string | null;
|
|
669
|
+
linkImageStorageKey?: string | null;
|
|
670
|
+
linkImageUrl?: string | null;
|
|
671
|
+
}
|
|
558
672
|
```
|
|
559
673
|
|
|
560
674
|
---
|
|
@@ -665,7 +779,6 @@ interface MessageDto {
|
|
|
665
779
|
conversationId: string;
|
|
666
780
|
senderId: string;
|
|
667
781
|
senderType: SenderType;
|
|
668
|
-
timestamp: string; // ISO 8601 — thời điểm gửi
|
|
669
782
|
blocks: Block[]; // Nội dung tin nhắn
|
|
670
783
|
plainTextIndex: string | null;
|
|
671
784
|
replyToMessageId: string | null;
|
|
@@ -956,6 +1069,28 @@ const updated = await client.proxy.put<Order>('trading', 'api/orders/123', {
|
|
|
956
1069
|
quantity: 200,
|
|
957
1070
|
});
|
|
958
1071
|
|
|
1072
|
+
// PATCH /api/proxy/trading/api/orders/123
|
|
1073
|
+
const patched = await client.proxy.patch<Order>('trading', 'api/orders/123', {
|
|
1074
|
+
status: 'cancelled',
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// DELETE /api/proxy/trading/api/orders/123
|
|
1078
|
+
await client.proxy.delete('trading', 'api/orders/123');
|
|
1079
|
+
|
|
1080
|
+
// Truyền custom headers — mỗi shortcut đều nhận `headers?` ở tham số cuối
|
|
1081
|
+
const dataWithHeader = await client.proxy.get<StockData>(
|
|
1082
|
+
'trading',
|
|
1083
|
+
'api/stocks/VN30',
|
|
1084
|
+
{ 'X-Trace-Id': 'abc-123' },
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
const orderWithHeader = await client.proxy.post<Order>(
|
|
1088
|
+
'trading',
|
|
1089
|
+
'api/orders',
|
|
1090
|
+
{ symbol: 'VNM', quantity: 100 },
|
|
1091
|
+
{ 'Idempotency-Key': 'order-uuid' },
|
|
1092
|
+
);
|
|
1093
|
+
|
|
959
1094
|
// Request tùy chỉnh method (hỗ trợ GET, POST, PUT, PATCH, DELETE)
|
|
960
1095
|
const result = await client.proxy.request<unknown>('myservice', 'api/resource/123', {
|
|
961
1096
|
method: 'DELETE',
|
|
@@ -1052,6 +1187,12 @@ await client.realtime.chat.leaveConversation('conv-id');
|
|
|
1052
1187
|
```
|
|
1053
1188
|
|
|
1054
1189
|
> SDK theo dõi `joinedConversations` và tự động re-join sau khi reconnect.
|
|
1190
|
+
>
|
|
1191
|
+
> **Lưu ý về FORBIDDEN/UNAUTHORIZED:** Theo spec, server có thể chấp nhận lời gọi `joinConversation` nhưng từ chối quyền truy cập sau đó qua event `Error` (vd. `FORBIDDEN — You are not a participant`). SDK sẽ tự động loại conversationId đó khỏi `joinedConversations` để tránh re-join sai sau reconnect, dựa trên:
|
|
1192
|
+
> 1. `error.details.conversationId` nếu server cung cấp (chính xác nhất), hoặc
|
|
1193
|
+
> 2. Fallback: message khớp `/not a participant/i` **và** chỉ có đúng 1 lời gọi `joinConversation` đang trong cửa sổ ~2s.
|
|
1194
|
+
>
|
|
1195
|
+
> Vẫn nên đăng ký listener `error` để hiển thị thông báo cho user.
|
|
1055
1196
|
|
|
1056
1197
|
### Gửi tin nhắn qua SignalR
|
|
1057
1198
|
|
|
@@ -1130,6 +1271,7 @@ client.realtime.chat.off('messageReceived', handler);
|
|
|
1130
1271
|
| `messageUpdated` | `MessageUpdatedDto` | Tin nhắn được chỉnh sửa |
|
|
1131
1272
|
| `messageDeleted` | `MessageDeletedDto` | Tin nhắn bị xóa |
|
|
1132
1273
|
| `messageRecovered` | `ChatMessageDto` | Tin nhắn được khôi phục |
|
|
1274
|
+
| `messageThumbnailsReady` | `MessageThumbnailsReadyDto` | Server đã sinh xong thumbnail cho ảnh/video — patch theo `blockIndex` |
|
|
1133
1275
|
| `reactionAdded` | `ReactionAddedDto` | Thêm reaction |
|
|
1134
1276
|
| `reactionRemoved` | `ReactionRemovedDto` | Xóa reaction |
|
|
1135
1277
|
| `typingStarted` | `TypingDto` | Người dùng đang gõ |
|
|
@@ -1203,6 +1345,36 @@ interface ReadReceiptUpdatedDto {
|
|
|
1203
1345
|
lastReadMessageId: string;
|
|
1204
1346
|
readAt: string; // ISO 8601
|
|
1205
1347
|
}
|
|
1348
|
+
|
|
1349
|
+
// Thumbnail ready cho một block media trong message
|
|
1350
|
+
interface MessageThumbnailDto {
|
|
1351
|
+
blockIndex: number; // Vị trí block trong mảng blocks của message
|
|
1352
|
+
thumbnail: MediaReference;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Server đã sinh xong thumbnail cho ảnh/video (xử lý bất đồng bộ).
|
|
1356
|
+
// Event này gửi SAU messageReceived/streamCompleted.
|
|
1357
|
+
interface MessageThumbnailsReadyDto {
|
|
1358
|
+
messageId: string;
|
|
1359
|
+
conversationId: string;
|
|
1360
|
+
thumbnails: MessageThumbnailDto[];
|
|
1361
|
+
}
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
### Cập nhật thumbnail bất đồng bộ
|
|
1365
|
+
|
|
1366
|
+
Khi gửi tin nhắn có ảnh/video, server xử lý thumbnail trong nền và phát event `messageThumbnailsReady` sau khi hoàn tất. Client nên patch block theo `blockIndex` thay vì re-fetch toàn bộ message.
|
|
1367
|
+
|
|
1368
|
+
```ts
|
|
1369
|
+
client.realtime.chat.on('messageThumbnailsReady', (dto) => {
|
|
1370
|
+
// dto.thumbnails: [{ blockIndex: 0, thumbnail: { storageKey, url } }, ...]
|
|
1371
|
+
for (const item of dto.thumbnails) {
|
|
1372
|
+
const block = messageStore.get(dto.messageId)?.blocks[item.blockIndex];
|
|
1373
|
+
if (block && (block.$type === 'image' || block.$type === 'video')) {
|
|
1374
|
+
block.thumbnail = item.thumbnail;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1206
1378
|
```
|
|
1207
1379
|
|
|
1208
1380
|
### Bot streaming
|
|
@@ -1280,7 +1452,7 @@ await client.realtime.notifications.resubscribePresence();
|
|
|
1280
1452
|
### Theo dõi trạng thái online (Presence)
|
|
1281
1453
|
|
|
1282
1454
|
```ts
|
|
1283
|
-
// Đăng ký theo dõi
|
|
1455
|
+
// Đăng ký theo dõi presence — SDK tự chia nhỏ thành các batch 200 nếu danh sách dài hơn
|
|
1284
1456
|
await client.realtime.notifications.subscribeToPresence(['user-1', 'user-2', 'user-3']);
|
|
1285
1457
|
|
|
1286
1458
|
// Server gửi PresenceState ngay lập tức sau khi subscribe
|
|
@@ -1295,7 +1467,7 @@ client.realtime.notifications.on('presenceChanged', (dto) => {
|
|
|
1295
1467
|
console.log(dto.participantId, 'is now', dto.isOnline ? 'online' : 'offline');
|
|
1296
1468
|
});
|
|
1297
1469
|
|
|
1298
|
-
// Hủy theo dõi — cũng
|
|
1470
|
+
// Hủy theo dõi — cũng tự chia nhỏ thành các batch 200
|
|
1299
1471
|
await client.realtime.notifications.unsubscribeFromPresence(['user-1']);
|
|
1300
1472
|
```
|
|
1301
1473
|
|
|
@@ -1344,11 +1516,11 @@ interface PresenceChangedDto {
|
|
|
1344
1516
|
interface NewMessageNotificationDto {
|
|
1345
1517
|
conversationId: string;
|
|
1346
1518
|
conversationType: ConversationType;
|
|
1347
|
-
conversationName?: string;
|
|
1519
|
+
conversationName?: string | null;
|
|
1348
1520
|
messageId: string;
|
|
1349
1521
|
senderId: string;
|
|
1350
1522
|
senderName: string;
|
|
1351
|
-
senderAvatar?: MediaReference;
|
|
1523
|
+
senderAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
1352
1524
|
contentPreview: string;
|
|
1353
1525
|
sentAt: string; // ISO 8601
|
|
1354
1526
|
}
|
|
@@ -1373,15 +1545,15 @@ interface UnreadCountChangedDto {
|
|
|
1373
1545
|
interface ConversationCreatedDto {
|
|
1374
1546
|
conversationId: string;
|
|
1375
1547
|
type: ConversationType;
|
|
1376
|
-
name?: string;
|
|
1377
|
-
avatar?: MediaReference;
|
|
1548
|
+
name?: string | null;
|
|
1549
|
+
avatar?: MediaReference | null;
|
|
1378
1550
|
participantCount: number;
|
|
1379
1551
|
}
|
|
1380
1552
|
|
|
1381
1553
|
interface ConversationUpdatedDto {
|
|
1382
1554
|
conversationId: string;
|
|
1383
1555
|
type: ConversationType;
|
|
1384
|
-
name?: string;
|
|
1556
|
+
name?: string | null;
|
|
1385
1557
|
avatar?: MediaReference | null; // null = avatar đã bị xóa
|
|
1386
1558
|
participantCount: number;
|
|
1387
1559
|
}
|
|
@@ -1390,7 +1562,7 @@ interface ParticipantJoinedDto {
|
|
|
1390
1562
|
conversationId: string;
|
|
1391
1563
|
participantId: string;
|
|
1392
1564
|
participantName: string;
|
|
1393
|
-
participantAvatar?: MediaReference;
|
|
1565
|
+
participantAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
1394
1566
|
changedAt: string; // ISO 8601
|
|
1395
1567
|
}
|
|
1396
1568
|
|
|
@@ -1399,7 +1571,7 @@ interface ParticipantLeftDto {
|
|
|
1399
1571
|
conversationId: string;
|
|
1400
1572
|
participantId: string;
|
|
1401
1573
|
participantName: string;
|
|
1402
|
-
participantAvatar?: MediaReference;
|
|
1574
|
+
participantAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
1403
1575
|
changedAt: string; // ISO 8601
|
|
1404
1576
|
}
|
|
1405
1577
|
|
|
@@ -1457,7 +1629,7 @@ const manager = new ReconnectionManager({
|
|
|
1457
1629
|
// Gọi API refresh token của ứng dụng
|
|
1458
1630
|
const newToken = await refreshToken();
|
|
1459
1631
|
if (newToken) {
|
|
1460
|
-
client.setToken(newToken); // Cập nhật token vào SDK
|
|
1632
|
+
client.setToken(newToken); // Cập nhật token vào SDK; lần connect() kế tiếp sẽ dùng giá trị mới
|
|
1461
1633
|
}
|
|
1462
1634
|
return newToken; // Trả về null để hủy reconnect
|
|
1463
1635
|
},
|
|
@@ -1471,7 +1643,9 @@ manager.stop();
|
|
|
1471
1643
|
|
|
1472
1644
|
**Chiến lược backoff:** 3 lần thử với delay 2s → 5s → 10s.
|
|
1473
1645
|
|
|
1474
|
-
**Phát hiện token hết hạn:**
|
|
1646
|
+
**Phát hiện token hết hạn:** heuristic — match (case-insensitive) các keyword `401`, `unauthorized`, `expired`, `invalid_token`, `invalid token` trong message của error.
|
|
1647
|
+
|
|
1648
|
+
**Bỏ qua close chủ động:** Cả `ChatHubClient` và `NotificationHubClient` expose getter `intentionallyClosed`. Trước khi tear down trong `disconnect()` (kể cả khi gọi qua `client.disconnect()` / `client.logout()`), cờ này được set `true`. Manager kiểm tra cờ ngay đầu handler và **bỏ qua** lần `disconnected` đó — không tự reconnect ngược ý người dùng. Cờ được reset về `false` ở đầu lần `connect()` kế tiếp.
|
|
1475
1649
|
|
|
1476
1650
|
---
|
|
1477
1651
|
|
|
@@ -1685,7 +1859,7 @@ type ButtonStyle = 'Default' | 'Primary' | 'Danger';
|
|
|
1685
1859
|
interface ActionButton {
|
|
1686
1860
|
label: string;
|
|
1687
1861
|
action: ButtonAction;
|
|
1688
|
-
value
|
|
1862
|
+
value?: string | null;
|
|
1689
1863
|
style: ButtonStyle;
|
|
1690
1864
|
}
|
|
1691
1865
|
|
|
@@ -1703,7 +1877,7 @@ type ChoiceMode = 'Single' | 'Multiple';
|
|
|
1703
1877
|
interface ChoiceOption {
|
|
1704
1878
|
label: string;
|
|
1705
1879
|
value: string;
|
|
1706
|
-
selected
|
|
1880
|
+
selected?: boolean;
|
|
1707
1881
|
}
|
|
1708
1882
|
|
|
1709
1883
|
interface ChoiceBlock {
|
|
@@ -1784,7 +1958,7 @@ interface ChatMessageDto {
|
|
|
1784
1958
|
conversationId: string;
|
|
1785
1959
|
senderId: string;
|
|
1786
1960
|
senderName: string; // Có ở SignalR, không có ở REST
|
|
1787
|
-
senderAvatar?: MediaReference; // Có ở SignalR, không có ở REST
|
|
1961
|
+
senderAvatar?: MediaReference | null; // Có ở SignalR, không có ở REST. Có thể null khi server chưa sẵn signed URL
|
|
1788
1962
|
senderType: SenderType; // Có ở cả REST và SignalR
|
|
1789
1963
|
blocks: Block[];
|
|
1790
1964
|
plainTextContent: string | null; // Tên khác với REST (plainTextIndex)
|
|
@@ -1816,14 +1990,14 @@ type SystemEventType =
|
|
|
1816
1990
|
| 'AvatarChanged' // Đổi avatar
|
|
1817
1991
|
| 'MemberAdded' // Thêm thành viên
|
|
1818
1992
|
| 'MemberRemoved' // Xóa thành viên
|
|
1819
|
-
| 'MemberLeft'
|
|
1820
|
-
| 'Custom';
|
|
1993
|
+
| 'MemberLeft'; // Thành viên tự rời
|
|
1821
1994
|
|
|
1822
1995
|
interface SystemEventInfo {
|
|
1823
1996
|
type: SystemEventType;
|
|
1824
|
-
actorId
|
|
1997
|
+
actorId?: string | null; // null/undefined khi không có actor (vd. MemberLeft tự rời)
|
|
1825
1998
|
targetIds?: string[];
|
|
1826
|
-
|
|
1999
|
+
// Tất cả giá trị đều là string (vd. ConversationRenamed → { newName: "Team Beta" })
|
|
2000
|
+
metadata?: Record<string, string>;
|
|
1827
2001
|
}
|
|
1828
2002
|
|
|
1829
2003
|
// SignalR (ChatHub) version — kế thừa SystemEventInfo, thêm display names
|
|
@@ -1839,7 +2013,8 @@ interface SystemEventDto extends SystemEventInfo {
|
|
|
1839
2013
|
interface HubErrorDto {
|
|
1840
2014
|
code: string;
|
|
1841
2015
|
message: string;
|
|
1842
|
-
|
|
2016
|
+
// Server có thể gửi `null` rõ ràng, hoặc bỏ field hoàn toàn
|
|
2017
|
+
details?: Record<string, unknown> | null;
|
|
1843
2018
|
}
|
|
1844
2019
|
```
|
|
1845
2020
|
|
|
@@ -1852,8 +2027,8 @@ interface StreamStartedDto {
|
|
|
1852
2027
|
conversationId: string;
|
|
1853
2028
|
senderId: string;
|
|
1854
2029
|
senderName: string;
|
|
1855
|
-
senderAvatar?: MediaReference;
|
|
1856
|
-
replyToMessageId?: string;
|
|
2030
|
+
senderAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
2031
|
+
replyToMessageId?: string | null;
|
|
1857
2032
|
startedAt: string; // ISO 8601
|
|
1858
2033
|
}
|
|
1859
2034
|
|
|
@@ -2116,7 +2291,9 @@ const cursor = Number(result.nextCursor); // BUG!
|
|
|
2116
2291
|
|
|
2117
2292
|
- `setToken()` hoạt động ngay cho **HTTP requests**.
|
|
2118
2293
|
- SignalR đọc token khi **connect()**, không phải mid-session.
|
|
2119
|
-
- Để
|
|
2294
|
+
- Để áp token mới ngay cho hub đang Connected: `setToken(newToken)` → `await refreshConnections()`.
|
|
2295
|
+
- Sau `refreshConnections()` cần tự `joinConversation()` / `subscribeToPresence()` lại (tracking bị clear khi disconnect). Hoặc dùng `ReconnectionManager` để tự động khôi phục.
|
|
2296
|
+
- Khi `tokenProvider` được cấu hình, provider luôn được hỏi trước. `setToken()` chỉ là fallback cho trường hợp provider trả `null`/empty.
|
|
2120
2297
|
|
|
2121
2298
|
### REST vs SignalR — field name khác nhau
|
|
2122
2299
|
|
package/dist/ChatClient.d.ts
CHANGED
|
@@ -24,10 +24,25 @@ export interface ChatClientOptions {
|
|
|
24
24
|
token?: string;
|
|
25
25
|
/**
|
|
26
26
|
* Optional external token provider function.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
*
|
|
28
|
+
* Resolution order on every request / connect:
|
|
29
|
+
* 1. If `tokenProvider` is set AND returns a non-empty string, that value
|
|
30
|
+
* is used (so external rotation systems are honored on each call).
|
|
31
|
+
* 2. Otherwise the internal token (constructor `token`, `setToken()`, or
|
|
32
|
+
* auto-set after a successful login) is used.
|
|
33
|
+
* 3. Otherwise null (no Authorization header sent).
|
|
34
|
+
*
|
|
35
|
+
* Note: this is reversed from the previous beta — previously a one-time
|
|
36
|
+
* setToken() would shadow the provider permanently. Now the provider always
|
|
37
|
+
* wins when it returns a value. Use `clearToken()` if you want to make sure
|
|
38
|
+
* the provider takes over after using setToken().
|
|
29
39
|
*/
|
|
30
40
|
tokenProvider?: () => string | null;
|
|
41
|
+
/**
|
|
42
|
+
* Per-request HTTP timeout in milliseconds. Default 30000 (30s).
|
|
43
|
+
* Pass `null` to disable the timeout — useful when uploading large files.
|
|
44
|
+
*/
|
|
45
|
+
requestTimeoutMs?: number | null;
|
|
31
46
|
/** Options for SignalR hub connections */
|
|
32
47
|
signalrOptions?: ChatClientSignalrOptions;
|
|
33
48
|
}
|
|
@@ -67,8 +82,13 @@ export interface ChatClientRealtime {
|
|
|
67
82
|
*/
|
|
68
83
|
export declare class ChatClient {
|
|
69
84
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
85
|
+
* Internally held JWT token (set via constructor `token`, `setToken()`, or
|
|
86
|
+
* after a successful login).
|
|
87
|
+
*
|
|
88
|
+
* Acts as the fallback when no `tokenProvider` is configured, or when the
|
|
89
|
+
* configured `tokenProvider` returns null. The external `tokenProvider`
|
|
90
|
+
* always wins when it returns a non-empty value — see
|
|
91
|
+
* {@link ChatClientOptions.tokenProvider}.
|
|
72
92
|
*/
|
|
73
93
|
private _token;
|
|
74
94
|
/**
|
|
@@ -101,16 +121,55 @@ export declare class ChatClient {
|
|
|
101
121
|
/**
|
|
102
122
|
* Update the JWT token.
|
|
103
123
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
124
|
+
* Effect on requests:
|
|
125
|
+
* - HTTP: takes effect on the very next request.
|
|
126
|
+
* - SignalR: existing active connections keep using the token they were
|
|
127
|
+
* opened with. To apply the new token immediately to active hubs, call
|
|
128
|
+
* `refreshConnections()` after `setToken()`.
|
|
129
|
+
*
|
|
130
|
+
* Pass `null` to clear the internally held token (same as `clearToken()`):
|
|
131
|
+
* both `_token` and `currentUser` are cleared so the SDK no longer reports
|
|
132
|
+
* stale identity. If a `tokenProvider` is configured and still returns a
|
|
133
|
+
* value, it will continue to be used regardless of this call.
|
|
134
|
+
*/
|
|
135
|
+
setToken(token: string | null): void;
|
|
136
|
+
/**
|
|
137
|
+
* Clear the internally held JWT token and authenticated user.
|
|
138
|
+
*
|
|
139
|
+
* After this, the configured `tokenProvider` (if any) becomes the only
|
|
140
|
+
* source of tokens. Use `logout()` instead when you also want to disconnect
|
|
141
|
+
* SignalR hubs.
|
|
107
142
|
*/
|
|
108
|
-
|
|
143
|
+
clearToken(): void;
|
|
144
|
+
/**
|
|
145
|
+
* Disconnect any active SignalR hub and reconnect it with the latest token.
|
|
146
|
+
*
|
|
147
|
+
* Useful right after `setToken()` to propagate the new token to live hubs.
|
|
148
|
+
* Hubs that are not currently connected are left untouched.
|
|
149
|
+
*
|
|
150
|
+
* Re-joins of conversations / re-subscriptions of presence are NOT performed
|
|
151
|
+
* automatically here — `disconnect()` clears that local tracking by design.
|
|
152
|
+
* The application is responsible for re-issuing those after a manual refresh
|
|
153
|
+
* (or for using `ReconnectionManager` for transparent recovery).
|
|
154
|
+
*/
|
|
155
|
+
refreshConnections(): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Log the current user out: clear the internal token + currentUser and
|
|
158
|
+
* disconnect both SignalR hubs.
|
|
159
|
+
*
|
|
160
|
+
* Note: there is no server-side logout endpoint — this only clears local
|
|
161
|
+
* SDK state. If a `tokenProvider` is configured and still returns a token,
|
|
162
|
+
* subsequent requests can authenticate again with that token.
|
|
163
|
+
*/
|
|
164
|
+
logout(): Promise<void>;
|
|
109
165
|
/**
|
|
110
166
|
* Disconnect all SignalR hubs (ChatHub + NotificationHub).
|
|
111
167
|
*
|
|
112
168
|
* Resolves after both hubs have finished disconnecting. Individual hub
|
|
113
169
|
* disconnect failures do not prevent the other hub from disconnecting.
|
|
170
|
+
*
|
|
171
|
+
* Both hubs set their `intentionallyClosed` flag, so a `ReconnectionManager`
|
|
172
|
+
* attached to them will skip auto-reconnect for the resulting close.
|
|
114
173
|
*/
|
|
115
174
|
disconnect(): Promise<void>;
|
|
116
175
|
/**
|
package/dist/ChatClient.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatClient.d.ts","sourceRoot":"","sources":["../src/ChatClient.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"ChatClient.d.ts","sourceRoot":"","sources":["../src/ChatClient.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,OAAO,KAAK,EAAgB,YAAY,EAAgB,MAAM,iBAAiB,CAAC;AAoDhF,MAAM,WAAW,wBAAwB;IACvC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;;OAcG;IACH,aAAa,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACpC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,0CAA0C;IAC1C,cAAc,CAAC,EAAE,wBAAwB,CAAC;CAC3C;AAMD,MAAM,WAAW,kBAAkB;IACjC,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC;CAC/C;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,UAAU;IAKrB;;;;;;;;OAQG;IACH,OAAO,CAAC,MAAM,CAAgB;IAE9B;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAsB;IAM7D,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAEvB,6BAA6B;IAC7B,QAAQ,CAAC,YAAY,EAAE,cAAc,CAAC;IAEtC,8BAA8B;IAC9B,QAAQ,CAAC,aAAa,EAAE,eAAe,CAAC;IAExC,yBAAyB;IACzB,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAE9B,6BAA6B;IAC7B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IAExB,qBAAqB;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,4BAA4B;IAC5B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IAEzB,6BAA6B;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAM3B,0BAA0B;IAC1B,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IAMtC,OAAO,CAAC,YAAY,CAA6B;IAEjD,+EAA+E;IAC/E,IAAI,WAAW,IAAI,YAAY,GAAG,IAAI,CAErC;gBAMW,OAAO,EAAE,iBAAiB;IA+DtC;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAOpC;;;;;;OAMG;IACH,UAAU,IAAI,IAAI;IAKlB;;;;;;;;;;OAUG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBzC;;;;;;;OAOG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAK7B;;;;;;;;OAQG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAWjC;;;OAGG;IACH,OAAO,CAAC,eAAe;CAIxB"}
|
package/dist/ChatClient.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ChatClient.ts — Main SDK facade
|
|
2
|
-
import { LogLevel } from '@microsoft/signalr';
|
|
2
|
+
import { HubConnectionState, LogLevel } from '@microsoft/signalr';
|
|
3
3
|
import { HttpClient } from './http/HttpClient.js';
|
|
4
4
|
import { AuthApi } from './http/AuthApi.js';
|
|
5
5
|
import { ParticipantApi } from './http/ParticipantApi.js';
|
|
@@ -94,16 +94,26 @@ export class ChatClient {
|
|
|
94
94
|
// ---------------------------------------------------------------------------
|
|
95
95
|
this._currentUser = null;
|
|
96
96
|
this._token = options.token ?? null;
|
|
97
|
-
// Build the combined token provider
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
97
|
+
// Build the combined token provider — see ChatClientOptions.tokenProvider
|
|
98
|
+
// for the rationale. The external provider takes priority when it returns
|
|
99
|
+
// a non-empty value so refresh-token rotation systems keep working even
|
|
100
|
+
// after setToken() has been called.
|
|
101
101
|
const externalProvider = options.tokenProvider;
|
|
102
|
-
this._internalTokenProvider = () =>
|
|
102
|
+
this._internalTokenProvider = () => {
|
|
103
|
+
if (externalProvider) {
|
|
104
|
+
const fromProvider = externalProvider();
|
|
105
|
+
if (fromProvider)
|
|
106
|
+
return fromProvider;
|
|
107
|
+
}
|
|
108
|
+
return this._token;
|
|
109
|
+
};
|
|
103
110
|
// Shared HTTP client used by all REST API modules
|
|
104
111
|
const http = new HttpClient({
|
|
105
112
|
baseUrl: options.baseUrl,
|
|
106
113
|
tokenProvider: this._internalTokenProvider,
|
|
114
|
+
...(options.requestTimeoutMs !== undefined
|
|
115
|
+
? { requestTimeoutMs: options.requestTimeoutMs }
|
|
116
|
+
: {}),
|
|
107
117
|
});
|
|
108
118
|
// Instantiate REST API modules
|
|
109
119
|
this.participants = new ParticipantApi(http);
|
|
@@ -139,18 +149,81 @@ export class ChatClient {
|
|
|
139
149
|
/**
|
|
140
150
|
* Update the JWT token.
|
|
141
151
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
152
|
+
* Effect on requests:
|
|
153
|
+
* - HTTP: takes effect on the very next request.
|
|
154
|
+
* - SignalR: existing active connections keep using the token they were
|
|
155
|
+
* opened with. To apply the new token immediately to active hubs, call
|
|
156
|
+
* `refreshConnections()` after `setToken()`.
|
|
157
|
+
*
|
|
158
|
+
* Pass `null` to clear the internally held token (same as `clearToken()`):
|
|
159
|
+
* both `_token` and `currentUser` are cleared so the SDK no longer reports
|
|
160
|
+
* stale identity. If a `tokenProvider` is configured and still returns a
|
|
161
|
+
* value, it will continue to be used regardless of this call.
|
|
145
162
|
*/
|
|
146
163
|
setToken(token) {
|
|
147
164
|
this._token = token;
|
|
165
|
+
if (token === null) {
|
|
166
|
+
this._currentUser = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Clear the internally held JWT token and authenticated user.
|
|
171
|
+
*
|
|
172
|
+
* After this, the configured `tokenProvider` (if any) becomes the only
|
|
173
|
+
* source of tokens. Use `logout()` instead when you also want to disconnect
|
|
174
|
+
* SignalR hubs.
|
|
175
|
+
*/
|
|
176
|
+
clearToken() {
|
|
177
|
+
this._token = null;
|
|
178
|
+
this._currentUser = null;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Disconnect any active SignalR hub and reconnect it with the latest token.
|
|
182
|
+
*
|
|
183
|
+
* Useful right after `setToken()` to propagate the new token to live hubs.
|
|
184
|
+
* Hubs that are not currently connected are left untouched.
|
|
185
|
+
*
|
|
186
|
+
* Re-joins of conversations / re-subscriptions of presence are NOT performed
|
|
187
|
+
* automatically here — `disconnect()` clears that local tracking by design.
|
|
188
|
+
* The application is responsible for re-issuing those after a manual refresh
|
|
189
|
+
* (or for using `ReconnectionManager` for transparent recovery).
|
|
190
|
+
*/
|
|
191
|
+
async refreshConnections() {
|
|
192
|
+
const tasks = [];
|
|
193
|
+
if (this.realtime.chat.state === HubConnectionState.Connected) {
|
|
194
|
+
tasks.push((async () => {
|
|
195
|
+
await this.realtime.chat.disconnect();
|
|
196
|
+
await this.realtime.chat.connect();
|
|
197
|
+
})());
|
|
198
|
+
}
|
|
199
|
+
if (this.realtime.notifications.state === HubConnectionState.Connected) {
|
|
200
|
+
tasks.push((async () => {
|
|
201
|
+
await this.realtime.notifications.disconnect();
|
|
202
|
+
await this.realtime.notifications.connect();
|
|
203
|
+
})());
|
|
204
|
+
}
|
|
205
|
+
await Promise.all(tasks);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Log the current user out: clear the internal token + currentUser and
|
|
209
|
+
* disconnect both SignalR hubs.
|
|
210
|
+
*
|
|
211
|
+
* Note: there is no server-side logout endpoint — this only clears local
|
|
212
|
+
* SDK state. If a `tokenProvider` is configured and still returns a token,
|
|
213
|
+
* subsequent requests can authenticate again with that token.
|
|
214
|
+
*/
|
|
215
|
+
async logout() {
|
|
216
|
+
this.clearToken();
|
|
217
|
+
await this.disconnect();
|
|
148
218
|
}
|
|
149
219
|
/**
|
|
150
220
|
* Disconnect all SignalR hubs (ChatHub + NotificationHub).
|
|
151
221
|
*
|
|
152
222
|
* Resolves after both hubs have finished disconnecting. Individual hub
|
|
153
223
|
* disconnect failures do not prevent the other hub from disconnecting.
|
|
224
|
+
*
|
|
225
|
+
* Both hubs set their `intentionallyClosed` flag, so a `ReconnectionManager`
|
|
226
|
+
* attached to them will skip auto-reconnect for the resulting close.
|
|
154
227
|
*/
|
|
155
228
|
async disconnect() {
|
|
156
229
|
await Promise.allSettled([
|