@manonero/chat-client-sdk 1.0.0-beta.3 → 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 +196 -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 +6 -5
- package/dist/http/ProxyApi.d.ts.map +1 -1
- package/dist/http/ProxyApi.js +15 -11
- 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;
|
|
@@ -964,6 +1077,20 @@ const patched = await client.proxy.patch<Order>('trading', 'api/orders/123', {
|
|
|
964
1077
|
// DELETE /api/proxy/trading/api/orders/123
|
|
965
1078
|
await client.proxy.delete('trading', 'api/orders/123');
|
|
966
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
|
+
|
|
967
1094
|
// Request tùy chỉnh method (hỗ trợ GET, POST, PUT, PATCH, DELETE)
|
|
968
1095
|
const result = await client.proxy.request<unknown>('myservice', 'api/resource/123', {
|
|
969
1096
|
method: 'DELETE',
|
|
@@ -1060,6 +1187,12 @@ await client.realtime.chat.leaveConversation('conv-id');
|
|
|
1060
1187
|
```
|
|
1061
1188
|
|
|
1062
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.
|
|
1063
1196
|
|
|
1064
1197
|
### Gửi tin nhắn qua SignalR
|
|
1065
1198
|
|
|
@@ -1138,6 +1271,7 @@ client.realtime.chat.off('messageReceived', handler);
|
|
|
1138
1271
|
| `messageUpdated` | `MessageUpdatedDto` | Tin nhắn được chỉnh sửa |
|
|
1139
1272
|
| `messageDeleted` | `MessageDeletedDto` | Tin nhắn bị xóa |
|
|
1140
1273
|
| `messageRecovered` | `ChatMessageDto` | Tin nhắn được khôi phục |
|
|
1274
|
+
| `messageThumbnailsReady` | `MessageThumbnailsReadyDto` | Server đã sinh xong thumbnail cho ảnh/video — patch theo `blockIndex` |
|
|
1141
1275
|
| `reactionAdded` | `ReactionAddedDto` | Thêm reaction |
|
|
1142
1276
|
| `reactionRemoved` | `ReactionRemovedDto` | Xóa reaction |
|
|
1143
1277
|
| `typingStarted` | `TypingDto` | Người dùng đang gõ |
|
|
@@ -1211,6 +1345,36 @@ interface ReadReceiptUpdatedDto {
|
|
|
1211
1345
|
lastReadMessageId: string;
|
|
1212
1346
|
readAt: string; // ISO 8601
|
|
1213
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
|
+
});
|
|
1214
1378
|
```
|
|
1215
1379
|
|
|
1216
1380
|
### Bot streaming
|
|
@@ -1288,7 +1452,7 @@ await client.realtime.notifications.resubscribePresence();
|
|
|
1288
1452
|
### Theo dõi trạng thái online (Presence)
|
|
1289
1453
|
|
|
1290
1454
|
```ts
|
|
1291
|
-
// Đă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
|
|
1292
1456
|
await client.realtime.notifications.subscribeToPresence(['user-1', 'user-2', 'user-3']);
|
|
1293
1457
|
|
|
1294
1458
|
// Server gửi PresenceState ngay lập tức sau khi subscribe
|
|
@@ -1303,7 +1467,7 @@ client.realtime.notifications.on('presenceChanged', (dto) => {
|
|
|
1303
1467
|
console.log(dto.participantId, 'is now', dto.isOnline ? 'online' : 'offline');
|
|
1304
1468
|
});
|
|
1305
1469
|
|
|
1306
|
-
// Hủy theo dõi — cũng
|
|
1470
|
+
// Hủy theo dõi — cũng tự chia nhỏ thành các batch 200
|
|
1307
1471
|
await client.realtime.notifications.unsubscribeFromPresence(['user-1']);
|
|
1308
1472
|
```
|
|
1309
1473
|
|
|
@@ -1352,11 +1516,11 @@ interface PresenceChangedDto {
|
|
|
1352
1516
|
interface NewMessageNotificationDto {
|
|
1353
1517
|
conversationId: string;
|
|
1354
1518
|
conversationType: ConversationType;
|
|
1355
|
-
conversationName?: string;
|
|
1519
|
+
conversationName?: string | null;
|
|
1356
1520
|
messageId: string;
|
|
1357
1521
|
senderId: string;
|
|
1358
1522
|
senderName: string;
|
|
1359
|
-
senderAvatar?: MediaReference;
|
|
1523
|
+
senderAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
1360
1524
|
contentPreview: string;
|
|
1361
1525
|
sentAt: string; // ISO 8601
|
|
1362
1526
|
}
|
|
@@ -1381,15 +1545,15 @@ interface UnreadCountChangedDto {
|
|
|
1381
1545
|
interface ConversationCreatedDto {
|
|
1382
1546
|
conversationId: string;
|
|
1383
1547
|
type: ConversationType;
|
|
1384
|
-
name?: string;
|
|
1385
|
-
avatar?: MediaReference;
|
|
1548
|
+
name?: string | null;
|
|
1549
|
+
avatar?: MediaReference | null;
|
|
1386
1550
|
participantCount: number;
|
|
1387
1551
|
}
|
|
1388
1552
|
|
|
1389
1553
|
interface ConversationUpdatedDto {
|
|
1390
1554
|
conversationId: string;
|
|
1391
1555
|
type: ConversationType;
|
|
1392
|
-
name?: string;
|
|
1556
|
+
name?: string | null;
|
|
1393
1557
|
avatar?: MediaReference | null; // null = avatar đã bị xóa
|
|
1394
1558
|
participantCount: number;
|
|
1395
1559
|
}
|
|
@@ -1398,7 +1562,7 @@ interface ParticipantJoinedDto {
|
|
|
1398
1562
|
conversationId: string;
|
|
1399
1563
|
participantId: string;
|
|
1400
1564
|
participantName: string;
|
|
1401
|
-
participantAvatar?: MediaReference;
|
|
1565
|
+
participantAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
1402
1566
|
changedAt: string; // ISO 8601
|
|
1403
1567
|
}
|
|
1404
1568
|
|
|
@@ -1407,7 +1571,7 @@ interface ParticipantLeftDto {
|
|
|
1407
1571
|
conversationId: string;
|
|
1408
1572
|
participantId: string;
|
|
1409
1573
|
participantName: string;
|
|
1410
|
-
participantAvatar?: MediaReference;
|
|
1574
|
+
participantAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
1411
1575
|
changedAt: string; // ISO 8601
|
|
1412
1576
|
}
|
|
1413
1577
|
|
|
@@ -1465,7 +1629,7 @@ const manager = new ReconnectionManager({
|
|
|
1465
1629
|
// Gọi API refresh token của ứng dụng
|
|
1466
1630
|
const newToken = await refreshToken();
|
|
1467
1631
|
if (newToken) {
|
|
1468
|
-
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
|
|
1469
1633
|
}
|
|
1470
1634
|
return newToken; // Trả về null để hủy reconnect
|
|
1471
1635
|
},
|
|
@@ -1479,7 +1643,9 @@ manager.stop();
|
|
|
1479
1643
|
|
|
1480
1644
|
**Chiến lược backoff:** 3 lần thử với delay 2s → 5s → 10s.
|
|
1481
1645
|
|
|
1482
|
-
**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.
|
|
1483
1649
|
|
|
1484
1650
|
---
|
|
1485
1651
|
|
|
@@ -1693,7 +1859,7 @@ type ButtonStyle = 'Default' | 'Primary' | 'Danger';
|
|
|
1693
1859
|
interface ActionButton {
|
|
1694
1860
|
label: string;
|
|
1695
1861
|
action: ButtonAction;
|
|
1696
|
-
value
|
|
1862
|
+
value?: string | null;
|
|
1697
1863
|
style: ButtonStyle;
|
|
1698
1864
|
}
|
|
1699
1865
|
|
|
@@ -1711,7 +1877,7 @@ type ChoiceMode = 'Single' | 'Multiple';
|
|
|
1711
1877
|
interface ChoiceOption {
|
|
1712
1878
|
label: string;
|
|
1713
1879
|
value: string;
|
|
1714
|
-
selected
|
|
1880
|
+
selected?: boolean;
|
|
1715
1881
|
}
|
|
1716
1882
|
|
|
1717
1883
|
interface ChoiceBlock {
|
|
@@ -1792,7 +1958,7 @@ interface ChatMessageDto {
|
|
|
1792
1958
|
conversationId: string;
|
|
1793
1959
|
senderId: string;
|
|
1794
1960
|
senderName: string; // Có ở SignalR, không có ở REST
|
|
1795
|
-
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
|
|
1796
1962
|
senderType: SenderType; // Có ở cả REST và SignalR
|
|
1797
1963
|
blocks: Block[];
|
|
1798
1964
|
plainTextContent: string | null; // Tên khác với REST (plainTextIndex)
|
|
@@ -1824,14 +1990,14 @@ type SystemEventType =
|
|
|
1824
1990
|
| 'AvatarChanged' // Đổi avatar
|
|
1825
1991
|
| 'MemberAdded' // Thêm thành viên
|
|
1826
1992
|
| 'MemberRemoved' // Xóa thành viên
|
|
1827
|
-
| 'MemberLeft'
|
|
1828
|
-
| 'Custom';
|
|
1993
|
+
| 'MemberLeft'; // Thành viên tự rời
|
|
1829
1994
|
|
|
1830
1995
|
interface SystemEventInfo {
|
|
1831
1996
|
type: SystemEventType;
|
|
1832
|
-
actorId
|
|
1997
|
+
actorId?: string | null; // null/undefined khi không có actor (vd. MemberLeft tự rời)
|
|
1833
1998
|
targetIds?: string[];
|
|
1834
|
-
|
|
1999
|
+
// Tất cả giá trị đều là string (vd. ConversationRenamed → { newName: "Team Beta" })
|
|
2000
|
+
metadata?: Record<string, string>;
|
|
1835
2001
|
}
|
|
1836
2002
|
|
|
1837
2003
|
// SignalR (ChatHub) version — kế thừa SystemEventInfo, thêm display names
|
|
@@ -1847,7 +2013,8 @@ interface SystemEventDto extends SystemEventInfo {
|
|
|
1847
2013
|
interface HubErrorDto {
|
|
1848
2014
|
code: string;
|
|
1849
2015
|
message: string;
|
|
1850
|
-
|
|
2016
|
+
// Server có thể gửi `null` rõ ràng, hoặc bỏ field hoàn toàn
|
|
2017
|
+
details?: Record<string, unknown> | null;
|
|
1851
2018
|
}
|
|
1852
2019
|
```
|
|
1853
2020
|
|
|
@@ -1860,8 +2027,8 @@ interface StreamStartedDto {
|
|
|
1860
2027
|
conversationId: string;
|
|
1861
2028
|
senderId: string;
|
|
1862
2029
|
senderName: string;
|
|
1863
|
-
senderAvatar?: MediaReference;
|
|
1864
|
-
replyToMessageId?: string;
|
|
2030
|
+
senderAvatar?: MediaReference | null; // Có thể null khi server chưa sẵn signed URL
|
|
2031
|
+
replyToMessageId?: string | null;
|
|
1865
2032
|
startedAt: string; // ISO 8601
|
|
1866
2033
|
}
|
|
1867
2034
|
|
|
@@ -2124,7 +2291,9 @@ const cursor = Number(result.nextCursor); // BUG!
|
|
|
2124
2291
|
|
|
2125
2292
|
- `setToken()` hoạt động ngay cho **HTTP requests**.
|
|
2126
2293
|
- SignalR đọc token khi **connect()**, không phải mid-session.
|
|
2127
|
-
- Để
|
|
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.
|
|
2128
2297
|
|
|
2129
2298
|
### REST vs SignalR — field name khác nhau
|
|
2130
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":"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"}
|
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([
|