@manonero/chat-client-sdk 1.0.0-beta.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.
- package/README.md +2343 -0
- package/dist/ChatClient.d.ts +122 -0
- package/dist/ChatClient.d.ts.map +1 -0
- package/dist/ChatClient.js +173 -0
- package/dist/ChatClient.js.map +1 -0
- package/dist/errors/ChatApiError.d.ts +22 -0
- package/dist/errors/ChatApiError.d.ts.map +1 -0
- package/dist/errors/ChatApiError.js +50 -0
- package/dist/errors/ChatApiError.js.map +1 -0
- package/dist/http/AuthApi.d.ts +26 -0
- package/dist/http/AuthApi.d.ts.map +1 -0
- package/dist/http/AuthApi.js +35 -0
- package/dist/http/AuthApi.js.map +1 -0
- package/dist/http/BotApi.d.ts +50 -0
- package/dist/http/BotApi.d.ts.map +1 -0
- package/dist/http/BotApi.js +67 -0
- package/dist/http/BotApi.js.map +1 -0
- package/dist/http/ConversationApi.d.ts +81 -0
- package/dist/http/ConversationApi.d.ts.map +1 -0
- package/dist/http/ConversationApi.js +115 -0
- package/dist/http/ConversationApi.js.map +1 -0
- package/dist/http/FileApi.d.ts +48 -0
- package/dist/http/FileApi.d.ts.map +1 -0
- package/dist/http/FileApi.js +68 -0
- package/dist/http/FileApi.js.map +1 -0
- package/dist/http/HealthApi.d.ts +26 -0
- package/dist/http/HealthApi.d.ts.map +1 -0
- package/dist/http/HealthApi.js +31 -0
- package/dist/http/HealthApi.js.map +1 -0
- package/dist/http/HttpClient.d.ts +51 -0
- package/dist/http/HttpClient.d.ts.map +1 -0
- package/dist/http/HttpClient.js +182 -0
- package/dist/http/HttpClient.js.map +1 -0
- package/dist/http/MessageApi.d.ts +67 -0
- package/dist/http/MessageApi.d.ts.map +1 -0
- package/dist/http/MessageApi.js +91 -0
- package/dist/http/MessageApi.js.map +1 -0
- package/dist/http/ParticipantApi.d.ts +28 -0
- package/dist/http/ParticipantApi.d.ts.map +1 -0
- package/dist/http/ParticipantApi.js +37 -0
- package/dist/http/ParticipantApi.js.map +1 -0
- package/dist/http/ProxyApi.d.ts +41 -0
- package/dist/http/ProxyApi.d.ts.map +1 -0
- package/dist/http/ProxyApi.js +57 -0
- package/dist/http/ProxyApi.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/realtime/ChatHubClient.d.ts +87 -0
- package/dist/realtime/ChatHubClient.d.ts.map +1 -0
- package/dist/realtime/ChatHubClient.js +182 -0
- package/dist/realtime/ChatHubClient.js.map +1 -0
- package/dist/realtime/NotificationHubClient.d.ts +73 -0
- package/dist/realtime/NotificationHubClient.d.ts.map +1 -0
- package/dist/realtime/NotificationHubClient.js +162 -0
- package/dist/realtime/NotificationHubClient.js.map +1 -0
- package/dist/realtime/ReconnectionManager.d.ts +65 -0
- package/dist/realtime/ReconnectionManager.d.ts.map +1 -0
- package/dist/realtime/ReconnectionManager.js +118 -0
- package/dist/realtime/ReconnectionManager.js.map +1 -0
- package/dist/types/auth.d.ts +28 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +3 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/block.d.ts +148 -0
- package/dist/types/block.d.ts.map +1 -0
- package/dist/types/block.js +47 -0
- package/dist/types/block.js.map +1 -0
- package/dist/types/bot.d.ts +48 -0
- package/dist/types/bot.d.ts.map +1 -0
- package/dist/types/bot.js +3 -0
- package/dist/types/bot.js.map +1 -0
- package/dist/types/chat-events.d.ts +153 -0
- package/dist/types/chat-events.d.ts.map +1 -0
- package/dist/types/chat-events.js +3 -0
- package/dist/types/chat-events.js.map +1 -0
- package/dist/types/common.d.ts +57 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +3 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/conversation.d.ts +70 -0
- package/dist/types/conversation.d.ts.map +1 -0
- package/dist/types/conversation.js +3 -0
- package/dist/types/conversation.js.map +1 -0
- package/dist/types/file.d.ts +31 -0
- package/dist/types/file.d.ts.map +1 -0
- package/dist/types/file.js +3 -0
- package/dist/types/file.js.map +1 -0
- package/dist/types/message.d.ts +83 -0
- package/dist/types/message.d.ts.map +1 -0
- package/dist/types/message.js +3 -0
- package/dist/types/message.js.map +1 -0
- package/dist/types/notification-events.d.ts +97 -0
- package/dist/types/notification-events.d.ts.map +1 -0
- package/dist/types/notification-events.js +3 -0
- package/dist/types/notification-events.js.map +1 -0
- package/dist/types/participant.d.ts +25 -0
- package/dist/types/participant.d.ts.map +1 -0
- package/dist/types/participant.js +3 -0
- package/dist/types/participant.js.map +1 -0
- package/dist/types/signalr.d.ts +87 -0
- package/dist/types/signalr.d.ts.map +1 -0
- package/dist/types/signalr.js +3 -0
- package/dist/types/signalr.js.map +1 -0
- package/dist/utils/TypedEventEmitter.d.ts +32 -0
- package/dist/utils/TypedEventEmitter.d.ts.map +1 -0
- package/dist/utils/TypedEventEmitter.js +60 -0
- package/dist/utils/TypedEventEmitter.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// realtime/ChatHubClient.ts — Manages ChatHub SignalR connection
|
|
2
|
+
import { HubConnection, HubConnectionBuilder, HubConnectionState, LogLevel, } from '@microsoft/signalr';
|
|
3
|
+
import { TypedEventEmitter as EventEmitter } from '../utils/TypedEventEmitter.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// ChatHubClient
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Typed client for the ChatHub (`/hubs/chat`).
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities:
|
|
11
|
+
* - Manages HubConnection lifecycle (connect / disconnect / auto-reconnect)
|
|
12
|
+
* - Exposes typed Hub methods (Client → Server)
|
|
13
|
+
* - Exposes typed event subscription (Server → Client)
|
|
14
|
+
* - Tracks joined conversations so ReconnectionManager can re-join on reconnect
|
|
15
|
+
*/
|
|
16
|
+
export class ChatHubClient {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.connection = null;
|
|
19
|
+
/** Set of conversationIds that have been joined in the current session */
|
|
20
|
+
this.joinedConversations = new Set();
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.emitter = new EventEmitter();
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Connection state
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
get state() {
|
|
28
|
+
return this.connection?.state ?? HubConnectionState.Disconnected;
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Connection management
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/** Build a new HubConnection using the latest token from tokenProvider */
|
|
34
|
+
buildConnection() {
|
|
35
|
+
const { hubUrl, tokenProvider, logLevel = LogLevel.Warning } = this.options;
|
|
36
|
+
return new HubConnectionBuilder()
|
|
37
|
+
.withUrl(hubUrl, {
|
|
38
|
+
accessTokenFactory: () => tokenProvider() ?? '',
|
|
39
|
+
})
|
|
40
|
+
.withAutomaticReconnect()
|
|
41
|
+
.configureLogging(logLevel)
|
|
42
|
+
.build();
|
|
43
|
+
}
|
|
44
|
+
/** Attach SignalR server→client event handlers to the connection */
|
|
45
|
+
attachHandlers(conn) {
|
|
46
|
+
// Message events
|
|
47
|
+
conn.on('MessageReceived', (msg) => this.emitter.emit('messageReceived', msg));
|
|
48
|
+
conn.on('MessageUpdated', (dto) => this.emitter.emit('messageUpdated', dto));
|
|
49
|
+
conn.on('MessageDeleted', (dto) => this.emitter.emit('messageDeleted', dto));
|
|
50
|
+
conn.on('MessageRecovered', (msg) => this.emitter.emit('messageRecovered', msg));
|
|
51
|
+
// Reaction events
|
|
52
|
+
conn.on('ReactionAdded', (dto) => this.emitter.emit('reactionAdded', dto));
|
|
53
|
+
conn.on('ReactionRemoved', (dto) => this.emitter.emit('reactionRemoved', dto));
|
|
54
|
+
// Typing events
|
|
55
|
+
conn.on('TypingStarted', (dto) => this.emitter.emit('typingStarted', dto));
|
|
56
|
+
conn.on('TypingStopped', (dto) => this.emitter.emit('typingStopped', dto));
|
|
57
|
+
// Read receipt
|
|
58
|
+
conn.on('ReadReceiptUpdated', (dto) => this.emitter.emit('readReceiptUpdated', dto));
|
|
59
|
+
// Streaming events
|
|
60
|
+
conn.on('StreamStarted', (dto) => this.emitter.emit('streamStarted', dto));
|
|
61
|
+
conn.on('StreamStatusUpdated', (dto) => this.emitter.emit('streamStatusUpdated', dto));
|
|
62
|
+
conn.on('StreamChunkReceived', (dto) => this.emitter.emit('streamChunkReceived', dto));
|
|
63
|
+
conn.on('StreamCompleted', (dto) => this.emitter.emit('streamCompleted', dto));
|
|
64
|
+
conn.on('StreamAborted', (dto) => this.emitter.emit('streamAborted', dto));
|
|
65
|
+
// Hub errors (server→client)
|
|
66
|
+
conn.on('Error', (error) => this.emitter.emit('error', error));
|
|
67
|
+
// Connection lifecycle
|
|
68
|
+
conn.onreconnecting((err) => this.emitter.emit('reconnecting', err));
|
|
69
|
+
conn.onreconnected((connectionId) => {
|
|
70
|
+
// Re-join all previously joined conversations after reconnect
|
|
71
|
+
this.rejoinConversations().catch(() => {
|
|
72
|
+
// Best-effort — errors will be surfaced via error events
|
|
73
|
+
});
|
|
74
|
+
this.emitter.emit('reconnected', connectionId);
|
|
75
|
+
});
|
|
76
|
+
conn.onclose((err) => this.emitter.emit('disconnected', err));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Connect to ChatHub. If already connected, resolves immediately.
|
|
80
|
+
* Builds a fresh connection using the current token.
|
|
81
|
+
*/
|
|
82
|
+
async connect() {
|
|
83
|
+
if (this.connection && this.connection.state === HubConnectionState.Connected) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Disconnect existing stale connection cleanly
|
|
87
|
+
if (this.connection) {
|
|
88
|
+
await this.safeStop();
|
|
89
|
+
}
|
|
90
|
+
const conn = this.buildConnection();
|
|
91
|
+
this.attachHandlers(conn);
|
|
92
|
+
this.connection = conn;
|
|
93
|
+
await conn.start();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Disconnect from ChatHub and clear joined conversation tracking.
|
|
97
|
+
*/
|
|
98
|
+
async disconnect() {
|
|
99
|
+
this.joinedConversations.clear();
|
|
100
|
+
await this.safeStop();
|
|
101
|
+
this.connection = null;
|
|
102
|
+
}
|
|
103
|
+
async safeStop() {
|
|
104
|
+
try {
|
|
105
|
+
await this.connection?.stop();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Ignore errors during stop
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Re-join all tracked conversations. Called internally after reconnect.
|
|
113
|
+
* Called by ReconnectionManager as well.
|
|
114
|
+
*/
|
|
115
|
+
async rejoinConversations() {
|
|
116
|
+
const ids = Array.from(this.joinedConversations);
|
|
117
|
+
for (const id of ids) {
|
|
118
|
+
try {
|
|
119
|
+
await this.invokeHub('JoinConversation', id);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Best-effort
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Hub Methods (Client → Server)
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
async invokeHub(method, ...args) {
|
|
130
|
+
if (!this.connection || this.connection.state !== HubConnectionState.Connected) {
|
|
131
|
+
throw new Error(`ChatHub is not connected (state: ${this.connection?.state ?? 'null'})`);
|
|
132
|
+
}
|
|
133
|
+
return this.connection.invoke(method, ...args);
|
|
134
|
+
}
|
|
135
|
+
async joinConversation(conversationId) {
|
|
136
|
+
await this.invokeHub('JoinConversation', conversationId);
|
|
137
|
+
this.joinedConversations.add(conversationId);
|
|
138
|
+
}
|
|
139
|
+
async leaveConversation(conversationId) {
|
|
140
|
+
await this.invokeHub('LeaveConversation', conversationId);
|
|
141
|
+
this.joinedConversations.delete(conversationId);
|
|
142
|
+
}
|
|
143
|
+
async startTyping(conversationId) {
|
|
144
|
+
await this.invokeHub('StartTyping', conversationId);
|
|
145
|
+
}
|
|
146
|
+
async stopTyping(conversationId) {
|
|
147
|
+
await this.invokeHub('StopTyping', conversationId);
|
|
148
|
+
}
|
|
149
|
+
async markAsRead(conversationId, messageId) {
|
|
150
|
+
return this.invokeHub('MarkAsRead', conversationId, messageId);
|
|
151
|
+
}
|
|
152
|
+
async sendMessage(request) {
|
|
153
|
+
return this.invokeHub('SendMessage', request);
|
|
154
|
+
}
|
|
155
|
+
async editMessage(request) {
|
|
156
|
+
return this.invokeHub('EditMessage', request);
|
|
157
|
+
}
|
|
158
|
+
/** Server expects an object { messageId }, not a bare string */
|
|
159
|
+
async deleteMessage(request) {
|
|
160
|
+
return this.invokeHub('DeleteMessage', request);
|
|
161
|
+
}
|
|
162
|
+
/** Server expects an object { messageId }, not a bare string */
|
|
163
|
+
async recoverMessage(request) {
|
|
164
|
+
return this.invokeHub('RecoverMessage', request);
|
|
165
|
+
}
|
|
166
|
+
async addReaction(request) {
|
|
167
|
+
return this.invokeHub('AddReaction', request);
|
|
168
|
+
}
|
|
169
|
+
async removeReaction(request) {
|
|
170
|
+
return this.invokeHub('RemoveReaction', request);
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Event subscription (Server → Client)
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
on(event, handler) {
|
|
176
|
+
return this.emitter.on(event, handler);
|
|
177
|
+
}
|
|
178
|
+
off(event, handler) {
|
|
179
|
+
this.emitter.off(event, handler);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=ChatHubClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatHubClient.js","sourceRoot":"","sources":["../../src/realtime/ChatHubClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { HubConnectionState, LogLevel } from '@microsoft/signalr';
|
|
2
|
+
import type { Unsubscribe } from '../utils/TypedEventEmitter.js';
|
|
3
|
+
import type { PresenceStateItem, PresenceChangedDto, NewMessageNotificationDto, MentionedNotificationDto, UnreadCountChangedDto, ConversationCreatedDto, ConversationUpdatedDto, ParticipantJoinedDto, ParticipantLeftDto, ConversationPinnedDto, ConversationUnpinnedDto, ReadStatusChangedDto } from '../types/notification-events.js';
|
|
4
|
+
import type { HubErrorDto } from '../types/signalr.js';
|
|
5
|
+
export interface NotificationHubEventMap {
|
|
6
|
+
presenceState: PresenceStateItem[];
|
|
7
|
+
presenceChanged: PresenceChangedDto;
|
|
8
|
+
newMessageNotification: NewMessageNotificationDto;
|
|
9
|
+
mentionedNotification: MentionedNotificationDto;
|
|
10
|
+
unreadCountChanged: UnreadCountChangedDto;
|
|
11
|
+
conversationCreated: ConversationCreatedDto;
|
|
12
|
+
conversationUpdated: ConversationUpdatedDto;
|
|
13
|
+
participantJoined: ParticipantJoinedDto;
|
|
14
|
+
participantLeft: ParticipantLeftDto;
|
|
15
|
+
conversationPinned: ConversationPinnedDto;
|
|
16
|
+
conversationUnpinned: ConversationUnpinnedDto;
|
|
17
|
+
readStatusChanged: ReadStatusChangedDto;
|
|
18
|
+
error: HubErrorDto;
|
|
19
|
+
reconnecting: Error | undefined;
|
|
20
|
+
reconnected: string | undefined;
|
|
21
|
+
disconnected: Error | undefined;
|
|
22
|
+
}
|
|
23
|
+
export interface NotificationHubClientOptions {
|
|
24
|
+
/** Full URL of the NotificationHub endpoint, e.g. "https://api.example.com/hubs/notifications" */
|
|
25
|
+
hubUrl: string;
|
|
26
|
+
/** Returns the current JWT token to use as access_token query param */
|
|
27
|
+
tokenProvider: () => string | null;
|
|
28
|
+
/** Minimum log level for SignalR internal logging (default: Warning) */
|
|
29
|
+
logLevel?: LogLevel;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Typed client for the NotificationHub (`/hubs/notifications`).
|
|
33
|
+
*
|
|
34
|
+
* Responsibilities:
|
|
35
|
+
* - Manages HubConnection lifecycle (connect / disconnect / auto-reconnect)
|
|
36
|
+
* - Exposes typed Hub methods: subscribeToPresence / unsubscribeFromPresence
|
|
37
|
+
* - Exposes typed event subscription (Server → Client)
|
|
38
|
+
* - Tracks subscribed presence participants for re-subscribe on reconnect
|
|
39
|
+
*/
|
|
40
|
+
export declare class NotificationHubClient {
|
|
41
|
+
private readonly options;
|
|
42
|
+
private connection;
|
|
43
|
+
private readonly emitter;
|
|
44
|
+
/** Set of participantIds currently subscribed to presence updates */
|
|
45
|
+
readonly subscribedPresenceIds: Set<string>;
|
|
46
|
+
constructor(options: NotificationHubClientOptions);
|
|
47
|
+
get state(): HubConnectionState;
|
|
48
|
+
/** Build a new HubConnection using the latest token from tokenProvider */
|
|
49
|
+
private buildConnection;
|
|
50
|
+
/** Attach SignalR server→client event handlers to the connection */
|
|
51
|
+
private attachHandlers;
|
|
52
|
+
/**
|
|
53
|
+
* Connect to NotificationHub. If already connected, resolves immediately.
|
|
54
|
+
* Connecting automatically marks the user as online on the server.
|
|
55
|
+
*/
|
|
56
|
+
connect(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Disconnect from NotificationHub. Automatically marks user as offline (server-side).
|
|
59
|
+
*/
|
|
60
|
+
disconnect(): Promise<void>;
|
|
61
|
+
private safeStop;
|
|
62
|
+
/**
|
|
63
|
+
* Re-subscribe to all tracked presence participant IDs.
|
|
64
|
+
* Called internally after reconnect. Also callable by ReconnectionManager.
|
|
65
|
+
*/
|
|
66
|
+
resubscribePresence(): Promise<void>;
|
|
67
|
+
private invokeHub;
|
|
68
|
+
subscribeToPresence(participantIds: string[]): Promise<void>;
|
|
69
|
+
unsubscribeFromPresence(participantIds: string[]): Promise<void>;
|
|
70
|
+
on<K extends keyof NotificationHubEventMap>(event: K, handler: (payload: NotificationHubEventMap[K]) => void): Unsubscribe;
|
|
71
|
+
off<K extends keyof NotificationHubEventMap>(event: K, handler: (payload: NotificationHubEventMap[K]) => void): void;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=NotificationHubClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotificationHubClient.d.ts","sourceRoot":"","sources":["../../src/realtime/NotificationHubClient.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,kBAAkB,EAClB,QAAQ,EACT,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAqB,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAEpF,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,EACrB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAMvD,MAAM,WAAW,uBAAuB;IAEtC,aAAa,EAAE,iBAAiB,EAAE,CAAC;IACnC,eAAe,EAAE,kBAAkB,CAAC;IAGpC,sBAAsB,EAAE,yBAAyB,CAAC;IAClD,qBAAqB,EAAE,wBAAwB,CAAC;IAChD,kBAAkB,EAAE,qBAAqB,CAAC;IAG1C,mBAAmB,EAAE,sBAAsB,CAAC;IAC5C,mBAAmB,EAAE,sBAAsB,CAAC;IAC5C,iBAAiB,EAAE,oBAAoB,CAAC;IACxC,eAAe,EAAE,kBAAkB,CAAC;IACpC,kBAAkB,EAAE,qBAAqB,CAAC;IAC1C,oBAAoB,EAAE,uBAAuB,CAAC;IAC9C,iBAAiB,EAAE,oBAAoB,CAAC;IAGxC,KAAK,EAAE,WAAW,CAAC;IAGnB,YAAY,EAAE,KAAK,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,YAAY,EAAE,KAAK,GAAG,SAAS,CAAC;CACjC;AAMD,MAAM,WAAW,4BAA4B;IAC3C,kGAAkG;IAClG,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,aAAa,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAMD;;;;;;;;GAQG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6C;IAErE,qEAAqE;IACrE,QAAQ,CAAC,qBAAqB,cAAqB;gBAEvC,OAAO,EAAE,4BAA4B;IASjD,IAAI,KAAK,IAAI,kBAAkB,CAE9B;IAMD,0EAA0E;IAC1E,OAAO,CAAC,eAAe;IAWvB,oEAAoE;IACpE,OAAO,CAAC,cAAc;IA4CtB;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB9B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAMnB,QAAQ;IAQtB;;;OAGG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;YAiB5B,SAAS;IASjB,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5D,uBAAuB,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IActE,EAAE,CAAC,CAAC,SAAS,MAAM,uBAAuB,EACxC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC,CAAC,KAAK,IAAI,GACrD,WAAW;IAId,GAAG,CAAC,CAAC,SAAS,MAAM,uBAAuB,EACzC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC,CAAC,KAAK,IAAI,GACrD,IAAI;CAGR"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// realtime/NotificationHubClient.ts — Manages NotificationHub SignalR connection
|
|
2
|
+
import { HubConnection, HubConnectionBuilder, HubConnectionState, LogLevel, } from '@microsoft/signalr';
|
|
3
|
+
import { TypedEventEmitter as EventEmitter } from '../utils/TypedEventEmitter.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// NotificationHubClient
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Typed client for the NotificationHub (`/hubs/notifications`).
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities:
|
|
11
|
+
* - Manages HubConnection lifecycle (connect / disconnect / auto-reconnect)
|
|
12
|
+
* - Exposes typed Hub methods: subscribeToPresence / unsubscribeFromPresence
|
|
13
|
+
* - Exposes typed event subscription (Server → Client)
|
|
14
|
+
* - Tracks subscribed presence participants for re-subscribe on reconnect
|
|
15
|
+
*/
|
|
16
|
+
export class NotificationHubClient {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.connection = null;
|
|
19
|
+
/** Set of participantIds currently subscribed to presence updates */
|
|
20
|
+
this.subscribedPresenceIds = new Set();
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.emitter = new EventEmitter();
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Connection state
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
get state() {
|
|
28
|
+
return this.connection?.state ?? HubConnectionState.Disconnected;
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Connection management
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/** Build a new HubConnection using the latest token from tokenProvider */
|
|
34
|
+
buildConnection() {
|
|
35
|
+
const { hubUrl, tokenProvider, logLevel = LogLevel.Warning } = this.options;
|
|
36
|
+
return new HubConnectionBuilder()
|
|
37
|
+
.withUrl(hubUrl, {
|
|
38
|
+
accessTokenFactory: () => tokenProvider() ?? '',
|
|
39
|
+
})
|
|
40
|
+
.withAutomaticReconnect()
|
|
41
|
+
.configureLogging(logLevel)
|
|
42
|
+
.build();
|
|
43
|
+
}
|
|
44
|
+
/** Attach SignalR server→client event handlers to the connection */
|
|
45
|
+
attachHandlers(conn) {
|
|
46
|
+
// Presence events
|
|
47
|
+
conn.on('PresenceState', (states) => this.emitter.emit('presenceState', states));
|
|
48
|
+
conn.on('PresenceChanged', (dto) => this.emitter.emit('presenceChanged', dto));
|
|
49
|
+
// Message notifications
|
|
50
|
+
conn.on('NewMessageNotification', (dto) => this.emitter.emit('newMessageNotification', dto));
|
|
51
|
+
conn.on('MentionedNotification', (dto) => this.emitter.emit('mentionedNotification', dto));
|
|
52
|
+
conn.on('UnreadCountChanged', (dto) => this.emitter.emit('unreadCountChanged', dto));
|
|
53
|
+
// Conversation events
|
|
54
|
+
conn.on('ConversationCreated', (dto) => this.emitter.emit('conversationCreated', dto));
|
|
55
|
+
conn.on('ConversationUpdated', (dto) => this.emitter.emit('conversationUpdated', dto));
|
|
56
|
+
conn.on('ParticipantJoined', (dto) => this.emitter.emit('participantJoined', dto));
|
|
57
|
+
conn.on('ParticipantLeft', (dto) => this.emitter.emit('participantLeft', dto));
|
|
58
|
+
conn.on('ConversationPinned', (dto) => this.emitter.emit('conversationPinned', dto));
|
|
59
|
+
conn.on('ConversationUnpinned', (dto) => this.emitter.emit('conversationUnpinned', dto));
|
|
60
|
+
conn.on('ReadStatusChanged', (dto) => this.emitter.emit('readStatusChanged', dto));
|
|
61
|
+
// Hub errors (server→client)
|
|
62
|
+
conn.on('Error', (error) => this.emitter.emit('error', error));
|
|
63
|
+
// Connection lifecycle
|
|
64
|
+
conn.onreconnecting((err) => this.emitter.emit('reconnecting', err));
|
|
65
|
+
conn.onreconnected((connectionId) => {
|
|
66
|
+
// Re-subscribe presence for all tracked participants
|
|
67
|
+
this.resubscribePresence().catch(() => {
|
|
68
|
+
// Best-effort
|
|
69
|
+
});
|
|
70
|
+
this.emitter.emit('reconnected', connectionId);
|
|
71
|
+
});
|
|
72
|
+
conn.onclose((err) => this.emitter.emit('disconnected', err));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Connect to NotificationHub. If already connected, resolves immediately.
|
|
76
|
+
* Connecting automatically marks the user as online on the server.
|
|
77
|
+
*/
|
|
78
|
+
async connect() {
|
|
79
|
+
if (this.connection && this.connection.state === HubConnectionState.Connected) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Disconnect existing stale connection cleanly
|
|
83
|
+
if (this.connection) {
|
|
84
|
+
await this.safeStop();
|
|
85
|
+
}
|
|
86
|
+
const conn = this.buildConnection();
|
|
87
|
+
this.attachHandlers(conn);
|
|
88
|
+
this.connection = conn;
|
|
89
|
+
await conn.start();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Disconnect from NotificationHub. Automatically marks user as offline (server-side).
|
|
93
|
+
*/
|
|
94
|
+
async disconnect() {
|
|
95
|
+
this.subscribedPresenceIds.clear();
|
|
96
|
+
await this.safeStop();
|
|
97
|
+
this.connection = null;
|
|
98
|
+
}
|
|
99
|
+
async safeStop() {
|
|
100
|
+
try {
|
|
101
|
+
await this.connection?.stop();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Ignore errors during stop
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Re-subscribe to all tracked presence participant IDs.
|
|
109
|
+
* Called internally after reconnect. Also callable by ReconnectionManager.
|
|
110
|
+
*/
|
|
111
|
+
async resubscribePresence() {
|
|
112
|
+
const ids = Array.from(this.subscribedPresenceIds);
|
|
113
|
+
if (ids.length === 0)
|
|
114
|
+
return;
|
|
115
|
+
// Batch into chunks of 200 to respect the server limit
|
|
116
|
+
for (let i = 0; i < ids.length; i += 200) {
|
|
117
|
+
try {
|
|
118
|
+
await this.invokeHub('SubscribeToPresence', ids.slice(i, i + 200));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Best-effort
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Hub Methods (Client → Server)
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
async invokeHub(method, ...args) {
|
|
129
|
+
if (!this.connection || this.connection.state !== HubConnectionState.Connected) {
|
|
130
|
+
throw new Error(`NotificationHub is not connected (state: ${this.connection?.state ?? 'null'})`);
|
|
131
|
+
}
|
|
132
|
+
return this.connection.invoke(method, ...args);
|
|
133
|
+
}
|
|
134
|
+
async subscribeToPresence(participantIds) {
|
|
135
|
+
if (participantIds.length > 200) {
|
|
136
|
+
throw new Error('Cannot subscribe to more than 200 participants at once');
|
|
137
|
+
}
|
|
138
|
+
await this.invokeHub('SubscribeToPresence', participantIds);
|
|
139
|
+
for (const id of participantIds) {
|
|
140
|
+
this.subscribedPresenceIds.add(id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async unsubscribeFromPresence(participantIds) {
|
|
144
|
+
if (participantIds.length > 200) {
|
|
145
|
+
throw new Error('Cannot unsubscribe from more than 200 participants at once');
|
|
146
|
+
}
|
|
147
|
+
await this.invokeHub('UnsubscribeFromPresence', participantIds);
|
|
148
|
+
for (const id of participantIds) {
|
|
149
|
+
this.subscribedPresenceIds.delete(id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Event subscription (Server → Client)
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
on(event, handler) {
|
|
156
|
+
return this.emitter.on(event, handler);
|
|
157
|
+
}
|
|
158
|
+
off(event, handler) {
|
|
159
|
+
this.emitter.off(event, handler);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=NotificationHubClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotificationHubClient.js","sourceRoot":"","sources":["../../src/realtime/NotificationHubClient.ts"],"names":[],"mappings":"AAAA,iFAAiF;AAEjF,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,kBAAkB,EAClB,QAAQ,GACT,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,iBAAiB,IAAI,YAAY,EAAE,MAAM,+BAA+B,CAAC;AA8DlF,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E;;;;;;;;GAQG;AACH,MAAM,OAAO,qBAAqB;IAQhC,YAAY,OAAqC;QANzC,eAAU,GAAyB,IAAI,CAAC;QAGhD,qEAAqE;QAC5D,0BAAqB,GAAG,IAAI,GAAG,EAAU,CAAC;QAGjD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,IAAI,YAAY,EAA2B,CAAC;IAC7D,CAAC;IAED,8EAA8E;IAC9E,mBAAmB;IACnB,8EAA8E;IAE9E,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,kBAAkB,CAAC,YAAY,CAAC;IACnE,CAAC;IAED,8EAA8E;IAC9E,wBAAwB;IACxB,8EAA8E;IAE9E,0EAA0E;IAClE,eAAe;QACrB,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,GAAG,QAAQ,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5E,OAAO,IAAI,oBAAoB,EAAE;aAC9B,OAAO,CAAC,MAAM,EAAE;YACf,kBAAkB,EAAE,GAAG,EAAE,CAAC,aAAa,EAAE,IAAI,EAAE;SAChD,CAAC;aACD,sBAAsB,EAAE;aACxB,gBAAgB,CAAC,QAAQ,CAAC;aAC1B,KAAK,EAAE,CAAC;IACb,CAAC;IAED,oEAAoE;IAC5D,cAAc,CAAC,IAAmB;QACxC,kBAAkB;QAClB,IAAI,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,MAA2B,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;QACtG,IAAI,CAAC,EAAE,CAAC,iBAAiB,EAAE,CAAC,GAAuB,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC,CAAC;QAEnG,wBAAwB;QACxB,IAAI,CAAC,EAAE,CAAC,wBAAwB,EAAE,CAAC,GAA8B,EAAE,EAAE,CACnE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAC,CAAC;QACpD,IAAI,CAAC,EAAE,CAAC,uBAAuB,EAAE,CAAC,GAA6B,EAAE,EAAE,CACjE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC,CAAC;QACnD,IAAI,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,GAA0B,EAAE,EAAE,CAC3D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAC,CAAC;QAEhD,sBAAsB;QACtB,IAAI,CAAC,EAAE,CAAC,qBAAqB,EAAE,CAAC,GAA2B,EAAE,EAAE,CAC7D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC,CAAC;QACjD,IAAI,CAAC,EAAE,CAAC,qBAAqB,EAAE,CAAC,GAA2B,EAAE,EAAE,CAC7D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC,CAAC;QACjD,IAAI,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,GAAyB,EAAE,EAAE,CACzD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC,CAAC;QAC/C,IAAI,CAAC,EAAE,CAAC,iBAAiB,EAAE,CAAC,GAAuB,EAAE,EAAE,CACrD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC,CAAC;QAC7C,IAAI,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,GAA0B,EAAE,EAAE,CAC3D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAC,CAAC;QAChD,IAAI,CAAC,EAAE,CAAC,sBAAsB,EAAE,CAAC,GAA4B,EAAE,EAAE,CAC/D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC,CAAC;QAClD,IAAI,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,GAAyB,EAAE,EAAE,CACzD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC,CAAC;QAE/C,6BAA6B;QAC7B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAkB,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;QAE5E,uBAAuB;QACvB,IAAI,CAAC,cAAc,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC;QACrE,IAAI,CAAC,aAAa,CAAC,CAAC,YAAY,EAAE,EAAE;YAClC,qDAAqD;YACrD,IAAI,CAAC,mBAAmB,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;gBACpC,cAAc;YAChB,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC;IAChE,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,KAAK,kBAAkB,CAAC,SAAS,EAAE,CAAC;YAC9E,OAAO;QACT,CAAC;QAED,+CAA+C;QAC/C,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxB,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QACpC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,UAAU;QACd,IAAI,CAAC,qBAAqB,CAAC,KAAK,EAAE,CAAC;QACnC,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;IACzB,CAAC;IAEO,KAAK,CAAC,QAAQ;QACpB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,4BAA4B;QAC9B,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,mBAAmB;QACvB,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;QACnD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC7B,uDAAuD;QACvD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC;YACzC,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,SAAS,CAAC,qBAAqB,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;YACrE,CAAC;YAAC,MAAM,CAAC;gBACP,cAAc;YAChB,CAAC;QACH,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,gCAAgC;IAChC,8EAA8E;IAEtE,KAAK,CAAC,SAAS,CAAW,MAAc,EAAE,GAAG,IAAe;QAClE,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,KAAK,kBAAkB,CAAC,SAAS,EAAE,CAAC;YAC/E,MAAM,IAAI,KAAK,CACb,4CAA4C,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,MAAM,GAAG,CAChF,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,CAAI,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,cAAwB;QAChD,IAAI,cAAc,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC5E,CAAC;QACD,MAAM,IAAI,CAAC,SAAS,CAAC,qBAAqB,EAAE,cAAc,CAAC,CAAC;QAC5D,KAAK,MAAM,EAAE,IAAI,cAAc,EAAE,CAAC;YAChC,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,uBAAuB,CAAC,cAAwB;QACpD,IAAI,cAAc,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;QAChF,CAAC;QACD,MAAM,IAAI,CAAC,SAAS,CAAC,yBAAyB,EAAE,cAAc,CAAC,CAAC;QAChE,KAAK,MAAM,EAAE,IAAI,cAAc,EAAE,CAAC;YAChC,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,uCAAuC;IACvC,8EAA8E;IAE9E,EAAE,CACA,KAAQ,EACR,OAAsD;QAEtD,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IAED,GAAG,CACD,KAAQ,EACR,OAAsD;QAEtD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC;CACF"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ChatHubClient } from './ChatHubClient.js';
|
|
2
|
+
import type { NotificationHubClient } from './NotificationHubClient.js';
|
|
3
|
+
export interface ReconnectionManagerOptions {
|
|
4
|
+
/** ChatHub client to manage */
|
|
5
|
+
chatHub: ChatHubClient;
|
|
6
|
+
/** NotificationHub client to manage */
|
|
7
|
+
notificationHub: NotificationHubClient;
|
|
8
|
+
/**
|
|
9
|
+
* Called when reconnect is rejected with 401 (token likely expired).
|
|
10
|
+
* Should return a fresh token, or null to cancel reconnect attempts.
|
|
11
|
+
*
|
|
12
|
+
* The returned token should be committed to the application's token store
|
|
13
|
+
* so that the shared tokenProvider reflects the new value immediately —
|
|
14
|
+
* the SDK will then pick it up on the next connect() call.
|
|
15
|
+
*/
|
|
16
|
+
onTokenExpired?: () => Promise<string | null>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* ReconnectionManager coordinates reconnection for both ChatHub and NotificationHub.
|
|
20
|
+
*
|
|
21
|
+
* Responsibilities:
|
|
22
|
+
* 1. Listens for `disconnected` events from both hubs and attempts to reconnect.
|
|
23
|
+
* 2. After ChatHub reconnects, re-joins all previously joined conversations.
|
|
24
|
+
* 3. After NotificationHub reconnects, re-subscribes to all previously tracked presences.
|
|
25
|
+
* 4. On 401-style reconnect failures, calls `onTokenExpired` to obtain a fresh token,
|
|
26
|
+
* then retries the connection once.
|
|
27
|
+
*
|
|
28
|
+
* Note on token propagation:
|
|
29
|
+
* - SignalR tokens are provided at connect-time via accessTokenFactory.
|
|
30
|
+
* - Both hub clients call tokenProvider() on every new connect(), so updating
|
|
31
|
+
* the token in the provider is sufficient before calling connect() again.
|
|
32
|
+
* - Existing active connections cannot receive a new token mid-session.
|
|
33
|
+
*/
|
|
34
|
+
export declare class ReconnectionManager {
|
|
35
|
+
private readonly chatHub;
|
|
36
|
+
private readonly notificationHub;
|
|
37
|
+
private readonly onTokenExpired?;
|
|
38
|
+
private unsubscribeChatDisconnected?;
|
|
39
|
+
private unsubscribeNotificationDisconnected?;
|
|
40
|
+
/** Whether the manager is actively managing reconnections */
|
|
41
|
+
private active;
|
|
42
|
+
constructor(options: ReconnectionManagerOptions);
|
|
43
|
+
/**
|
|
44
|
+
* Start managing reconnections. Attaches `disconnected` listeners to both hubs.
|
|
45
|
+
* Safe to call multiple times — subsequent calls are no-ops.
|
|
46
|
+
*/
|
|
47
|
+
start(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Stop managing reconnections and detach all listeners.
|
|
50
|
+
*/
|
|
51
|
+
stop(): void;
|
|
52
|
+
private handleChatDisconnected;
|
|
53
|
+
private handleNotificationDisconnected;
|
|
54
|
+
/**
|
|
55
|
+
* Determine if the disconnect error is likely due to token expiry (HTTP 401).
|
|
56
|
+
* SignalR surfaces this as an error message containing "401" or "Unauthorized".
|
|
57
|
+
*/
|
|
58
|
+
private isTokenExpiredError;
|
|
59
|
+
/**
|
|
60
|
+
* Attempt to reconnect a hub with exponential backoff (max 3 retries).
|
|
61
|
+
* After a successful connect, calls `afterConnect` to restore subscriptions.
|
|
62
|
+
*/
|
|
63
|
+
private reconnectHub;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=ReconnectionManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReconnectionManager.d.ts","sourceRoot":"","sources":["../../src/realtime/ReconnectionManager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAExE,MAAM,WAAW,0BAA0B;IACzC,+BAA+B;IAC/B,OAAO,EAAE,aAAa,CAAC;IACvB,uCAAuC;IACvC,eAAe,EAAE,qBAAqB,CAAC;IACvC;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAwB;IACxD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAA+B;IAE/D,OAAO,CAAC,2BAA2B,CAAC,CAAa;IACjD,OAAO,CAAC,mCAAmC,CAAC,CAAa;IAEzD,6DAA6D;IAC7D,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,0BAA0B;IAM/C;;;OAGG;IACH,KAAK,IAAI,IAAI;IAeb;;OAEG;IACH,IAAI,IAAI,IAAI;YAYE,sBAAsB;YAiBtB,8BAA8B;IAmB5C;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAM3B;;;OAGG;YACW,YAAY;CA2B3B"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// realtime/ReconnectionManager.ts — Coordinates auto-reconnect for both hubs
|
|
2
|
+
import { HubConnectionState } from '@microsoft/signalr';
|
|
3
|
+
/**
|
|
4
|
+
* ReconnectionManager coordinates reconnection for both ChatHub and NotificationHub.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* 1. Listens for `disconnected` events from both hubs and attempts to reconnect.
|
|
8
|
+
* 2. After ChatHub reconnects, re-joins all previously joined conversations.
|
|
9
|
+
* 3. After NotificationHub reconnects, re-subscribes to all previously tracked presences.
|
|
10
|
+
* 4. On 401-style reconnect failures, calls `onTokenExpired` to obtain a fresh token,
|
|
11
|
+
* then retries the connection once.
|
|
12
|
+
*
|
|
13
|
+
* Note on token propagation:
|
|
14
|
+
* - SignalR tokens are provided at connect-time via accessTokenFactory.
|
|
15
|
+
* - Both hub clients call tokenProvider() on every new connect(), so updating
|
|
16
|
+
* the token in the provider is sufficient before calling connect() again.
|
|
17
|
+
* - Existing active connections cannot receive a new token mid-session.
|
|
18
|
+
*/
|
|
19
|
+
export class ReconnectionManager {
|
|
20
|
+
constructor(options) {
|
|
21
|
+
/** Whether the manager is actively managing reconnections */
|
|
22
|
+
this.active = false;
|
|
23
|
+
this.chatHub = options.chatHub;
|
|
24
|
+
this.notificationHub = options.notificationHub;
|
|
25
|
+
this.onTokenExpired = options.onTokenExpired;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start managing reconnections. Attaches `disconnected` listeners to both hubs.
|
|
29
|
+
* Safe to call multiple times — subsequent calls are no-ops.
|
|
30
|
+
*/
|
|
31
|
+
start() {
|
|
32
|
+
if (this.active)
|
|
33
|
+
return;
|
|
34
|
+
this.active = true;
|
|
35
|
+
this.unsubscribeChatDisconnected = this.chatHub.on('disconnected', (err) => void this.handleChatDisconnected(err));
|
|
36
|
+
this.unsubscribeNotificationDisconnected = this.notificationHub.on('disconnected', (err) => void this.handleNotificationDisconnected(err));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Stop managing reconnections and detach all listeners.
|
|
40
|
+
*/
|
|
41
|
+
stop() {
|
|
42
|
+
this.active = false;
|
|
43
|
+
this.unsubscribeChatDisconnected?.();
|
|
44
|
+
this.unsubscribeNotificationDisconnected?.();
|
|
45
|
+
this.unsubscribeChatDisconnected = undefined;
|
|
46
|
+
this.unsubscribeNotificationDisconnected = undefined;
|
|
47
|
+
}
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Disconnect handlers
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
async handleChatDisconnected(err) {
|
|
52
|
+
if (!this.active)
|
|
53
|
+
return;
|
|
54
|
+
// withAutomaticReconnect already tried — this is a terminal disconnect.
|
|
55
|
+
// Attempt one more manual reconnect after optional token refresh.
|
|
56
|
+
if (this.isTokenExpiredError(err)) {
|
|
57
|
+
const newToken = await this.onTokenExpired?.();
|
|
58
|
+
if (!newToken)
|
|
59
|
+
return; // Client chose to abandon reconnect
|
|
60
|
+
}
|
|
61
|
+
await this.reconnectHub(() => this.chatHub.state, () => this.chatHub.connect(), () => this.chatHub.rejoinConversations());
|
|
62
|
+
}
|
|
63
|
+
async handleNotificationDisconnected(err) {
|
|
64
|
+
if (!this.active)
|
|
65
|
+
return;
|
|
66
|
+
if (this.isTokenExpiredError(err)) {
|
|
67
|
+
const newToken = await this.onTokenExpired?.();
|
|
68
|
+
if (!newToken)
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await this.reconnectHub(() => this.notificationHub.state, () => this.notificationHub.connect(), () => this.notificationHub.resubscribePresence());
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Helpers
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/**
|
|
77
|
+
* Determine if the disconnect error is likely due to token expiry (HTTP 401).
|
|
78
|
+
* SignalR surfaces this as an error message containing "401" or "Unauthorized".
|
|
79
|
+
*/
|
|
80
|
+
isTokenExpiredError(err) {
|
|
81
|
+
if (!err)
|
|
82
|
+
return false;
|
|
83
|
+
const msg = err.message ?? '';
|
|
84
|
+
return msg.includes('401') || msg.toLowerCase().includes('unauthorized');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Attempt to reconnect a hub with exponential backoff (max 3 retries).
|
|
88
|
+
* After a successful connect, calls `afterConnect` to restore subscriptions.
|
|
89
|
+
*/
|
|
90
|
+
async reconnectHub(getState, connect, afterConnect) {
|
|
91
|
+
if (getState() === HubConnectionState.Connected)
|
|
92
|
+
return;
|
|
93
|
+
const delays = [2000, 5000, 10000]; // ms — 2s, 5s, 10s
|
|
94
|
+
for (const delay of delays) {
|
|
95
|
+
if (!this.active)
|
|
96
|
+
return;
|
|
97
|
+
await sleep(delay);
|
|
98
|
+
if (getState() === HubConnectionState.Connected)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
await connect();
|
|
102
|
+
// Successful connect — restore state
|
|
103
|
+
await afterConnect().catch(() => {
|
|
104
|
+
// Best-effort: don't fail reconnect if re-subscribe fails
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Retry after next delay
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// All retries exhausted — emit is handled by the hub's onclose handler
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function sleep(ms) {
|
|
116
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=ReconnectionManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReconnectionManager.js","sourceRoot":"","sources":["../../src/realtime/ReconnectionManager.ts"],"names":[],"mappings":"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"}
|