@manonero/chat-client-sdk 0.0.1-beta.1
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 +2453 -0
- package/dist/ChatClient.d.ts +181 -0
- package/dist/ChatClient.d.ts.map +1 -0
- package/dist/ChatClient.js +246 -0
- package/dist/ChatClient.js.map +1 -0
- package/dist/errors/ChatApiError.d.ts +45 -0
- package/dist/errors/ChatApiError.d.ts.map +1 -0
- package/dist/errors/ChatApiError.js +69 -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 +43 -0
- package/dist/http/BotApi.d.ts.map +1 -0
- package/dist/http/BotApi.js +60 -0
- package/dist/http/BotApi.js.map +1 -0
- package/dist/http/ConversationApi.d.ts +92 -0
- package/dist/http/ConversationApi.d.ts.map +1 -0
- package/dist/http/ConversationApi.js +128 -0
- package/dist/http/ConversationApi.js.map +1 -0
- package/dist/http/FileApi.d.ts +60 -0
- package/dist/http/FileApi.d.ts.map +1 -0
- package/dist/http/FileApi.js +91 -0
- package/dist/http/FileApi.js.map +1 -0
- package/dist/http/HealthApi.d.ts +28 -0
- package/dist/http/HealthApi.d.ts.map +1 -0
- package/dist/http/HealthApi.js +34 -0
- package/dist/http/HealthApi.js.map +1 -0
- package/dist/http/HttpClient.d.ts +69 -0
- package/dist/http/HttpClient.d.ts.map +1 -0
- package/dist/http/HttpClient.js +244 -0
- package/dist/http/HttpClient.js.map +1 -0
- package/dist/http/MessageApi.d.ts +69 -0
- package/dist/http/MessageApi.d.ts.map +1 -0
- package/dist/http/MessageApi.js +104 -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 +59 -0
- package/dist/http/ProxyApi.d.ts.map +1 -0
- package/dist/http/ProxyApi.js +86 -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 +143 -0
- package/dist/realtime/ChatHubClient.d.ts.map +1 -0
- package/dist/realtime/ChatHubClient.js +365 -0
- package/dist/realtime/ChatHubClient.js.map +1 -0
- package/dist/realtime/NotificationHubClient.d.ts +89 -0
- package/dist/realtime/NotificationHubClient.d.ts.map +1 -0
- package/dist/realtime/NotificationHubClient.js +191 -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 +129 -0
- package/dist/realtime/ReconnectionManager.js.map +1 -0
- package/dist/types/auth.d.ts +30 -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 +163 -0
- package/dist/types/block.d.ts.map +1 -0
- package/dist/types/block.js +77 -0
- package/dist/types/block.js.map +1 -0
- package/dist/types/bot.d.ts +42 -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 +191 -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 +64 -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 +106 -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 +84 -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 +89 -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 +22 -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 +84 -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 +42 -0
|
@@ -0,0 +1,365 @@
|
|
|
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
|
+
import { normalizeBlocksForSend } from '../types/block.js';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// ChatHubClient
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/**
|
|
9
|
+
* Typed client for the ChatHub (`/hubs/chat`).
|
|
10
|
+
*
|
|
11
|
+
* Responsibilities:
|
|
12
|
+
* - Manages HubConnection lifecycle (connect / disconnect / auto-reconnect)
|
|
13
|
+
* - Exposes typed Hub methods (Client → Server)
|
|
14
|
+
* - Exposes typed event subscription (Server → Client)
|
|
15
|
+
* - Tracks joined conversations so ReconnectionManager can re-join on reconnect
|
|
16
|
+
*/
|
|
17
|
+
export class ChatHubClient {
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.connection = null;
|
|
20
|
+
/** In-flight connect Promise, used to dedupe concurrent connect() calls. */
|
|
21
|
+
this.connectingPromise = null;
|
|
22
|
+
/**
|
|
23
|
+
* True after a user-initiated disconnect() until the next connect() begins.
|
|
24
|
+
* ReconnectionManager uses this to skip auto-reconnect when the close was
|
|
25
|
+
* intentional rather than caused by a transport failure.
|
|
26
|
+
*/
|
|
27
|
+
this._intentionallyClosed = false;
|
|
28
|
+
/** Set of conversationIds that have been joined in the current session */
|
|
29
|
+
this.joinedConversations = new Set();
|
|
30
|
+
/**
|
|
31
|
+
* Tracks join attempts that resolved successfully but may still be revoked by
|
|
32
|
+
* a follow-up server `Error` event (FORBIDDEN/UNAUTHORIZED). Entries auto-expire
|
|
33
|
+
* after PENDING_JOIN_TTL_MS to bound memory.
|
|
34
|
+
*/
|
|
35
|
+
this.pendingJoinAttempts = new Map();
|
|
36
|
+
this.options = options;
|
|
37
|
+
this.emitter = new EventEmitter();
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Connection state
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
get state() {
|
|
43
|
+
return this.connection?.state ?? HubConnectionState.Disconnected;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* True after a user-initiated disconnect() and until the next connect() starts.
|
|
47
|
+
* ReconnectionManager honors this to avoid reconnecting after an intentional close.
|
|
48
|
+
*/
|
|
49
|
+
get intentionallyClosed() {
|
|
50
|
+
return this._intentionallyClosed;
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Connection management
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
/** Build a new HubConnection using the latest token from tokenProvider */
|
|
56
|
+
buildConnection() {
|
|
57
|
+
const { hubUrl, tokenProvider, logLevel = LogLevel.Warning } = this.options;
|
|
58
|
+
return new HubConnectionBuilder()
|
|
59
|
+
.withUrl(hubUrl, {
|
|
60
|
+
// Throw rather than return '' so SignalR fails the request loudly with
|
|
61
|
+
// a useful error instead of sending an empty `Authorization: Bearer ` header.
|
|
62
|
+
accessTokenFactory: () => {
|
|
63
|
+
const t = tokenProvider();
|
|
64
|
+
if (!t) {
|
|
65
|
+
throw new Error('ChatHub: no authentication token available');
|
|
66
|
+
}
|
|
67
|
+
return t;
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
.withAutomaticReconnect()
|
|
71
|
+
.configureLogging(logLevel)
|
|
72
|
+
.build();
|
|
73
|
+
}
|
|
74
|
+
/** Attach SignalR server→client event handlers to the connection */
|
|
75
|
+
attachHandlers(conn) {
|
|
76
|
+
// Message events
|
|
77
|
+
conn.on('MessageReceived', (msg) => this.emitter.emit('messageReceived', msg));
|
|
78
|
+
conn.on('MessageUpdated', (dto) => this.emitter.emit('messageUpdated', dto));
|
|
79
|
+
conn.on('MessageDeleted', (dto) => this.emitter.emit('messageDeleted', dto));
|
|
80
|
+
conn.on('MessageRecovered', (msg) => this.emitter.emit('messageRecovered', msg));
|
|
81
|
+
conn.on('MessageThumbnailsReady', (dto) => this.emitter.emit('messageThumbnailsReady', dto));
|
|
82
|
+
// Reaction events
|
|
83
|
+
conn.on('ReactionAdded', (dto) => this.emitter.emit('reactionAdded', dto));
|
|
84
|
+
conn.on('ReactionRemoved', (dto) => this.emitter.emit('reactionRemoved', dto));
|
|
85
|
+
// Typing events
|
|
86
|
+
conn.on('TypingStarted', (dto) => this.emitter.emit('typingStarted', dto));
|
|
87
|
+
conn.on('TypingStopped', (dto) => this.emitter.emit('typingStopped', dto));
|
|
88
|
+
// Streaming events
|
|
89
|
+
conn.on('StreamStarted', (dto) => this.emitter.emit('streamStarted', dto));
|
|
90
|
+
conn.on('StreamStatusUpdated', (dto) => this.emitter.emit('streamStatusUpdated', dto));
|
|
91
|
+
conn.on('StreamChunkReceived', (dto) => this.emitter.emit('streamChunkReceived', dto));
|
|
92
|
+
conn.on('StreamChunkBatchReceived', (dto) => this.emitter.emit('streamChunkBatchReceived', dto));
|
|
93
|
+
conn.on('StreamCompleted', (dto) => this.emitter.emit('streamCompleted', dto));
|
|
94
|
+
conn.on('StreamAborted', (dto) => this.emitter.emit('streamAborted', dto));
|
|
95
|
+
// Hub errors (server→client) — also revert pending joins on FORBIDDEN/UNAUTHORIZED
|
|
96
|
+
conn.on('Error', (error) => {
|
|
97
|
+
this.handlePotentialJoinFailure(error);
|
|
98
|
+
this.emitter.emit('error', error);
|
|
99
|
+
});
|
|
100
|
+
// Connection lifecycle
|
|
101
|
+
conn.onreconnecting((err) => this.emitter.emit('reconnecting', err));
|
|
102
|
+
conn.onreconnected((connectionId) => {
|
|
103
|
+
// Re-join all previously joined conversations after reconnect
|
|
104
|
+
this.rejoinConversations().catch(() => {
|
|
105
|
+
// Best-effort — errors will be surfaced via error events
|
|
106
|
+
});
|
|
107
|
+
this.emitter.emit('reconnected', connectionId);
|
|
108
|
+
});
|
|
109
|
+
conn.onclose((err) => this.emitter.emit('disconnected', err));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Connect to ChatHub. If already connected, resolves immediately.
|
|
113
|
+
* Concurrent calls share a single in-flight Promise — building two
|
|
114
|
+
* connections at once would leak handlers.
|
|
115
|
+
* Builds a fresh connection using the current token.
|
|
116
|
+
*
|
|
117
|
+
* Throws synchronously when no authentication token is available so the
|
|
118
|
+
* caller gets a clear error rather than a malformed Authorization header.
|
|
119
|
+
*/
|
|
120
|
+
async connect() {
|
|
121
|
+
if (this.connection && this.connection.state === HubConnectionState.Connected) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (this.connectingPromise) {
|
|
125
|
+
return this.connectingPromise;
|
|
126
|
+
}
|
|
127
|
+
if (!this.options.tokenProvider()) {
|
|
128
|
+
throw new Error('ChatHubClient.connect(): no authentication token available. ' +
|
|
129
|
+
'Provide one via ChatClient.setToken() or the tokenProvider option.');
|
|
130
|
+
}
|
|
131
|
+
// The user is explicitly opening a connection — clear any prior
|
|
132
|
+
// intentional-close flag so ReconnectionManager will manage future drops.
|
|
133
|
+
this._intentionallyClosed = false;
|
|
134
|
+
const promise = (async () => {
|
|
135
|
+
try {
|
|
136
|
+
// Disconnect existing stale connection cleanly
|
|
137
|
+
if (this.connection) {
|
|
138
|
+
await this.safeStop();
|
|
139
|
+
}
|
|
140
|
+
const conn = this.buildConnection();
|
|
141
|
+
this.attachHandlers(conn);
|
|
142
|
+
this.connection = conn;
|
|
143
|
+
await conn.start();
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
this.connectingPromise = null;
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
149
|
+
this.connectingPromise = promise;
|
|
150
|
+
return promise;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Disconnect from ChatHub and clear joined conversation tracking.
|
|
154
|
+
* If a connect is in flight, it is awaited first so we always tear down a
|
|
155
|
+
* fully-formed connection (not a half-built one).
|
|
156
|
+
*
|
|
157
|
+
* Sets `intentionallyClosed` so ReconnectionManager skips its reconnect
|
|
158
|
+
* attempt for the resulting `disconnected` event.
|
|
159
|
+
*/
|
|
160
|
+
async disconnect() {
|
|
161
|
+
// Mark intentional BEFORE awaiting the in-flight connect so any
|
|
162
|
+
// disconnected events that fire during teardown are not auto-reconnected.
|
|
163
|
+
this._intentionallyClosed = true;
|
|
164
|
+
if (this.connectingPromise) {
|
|
165
|
+
try {
|
|
166
|
+
await this.connectingPromise;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// The connect failed — there's nothing established to tear down,
|
|
170
|
+
// but still proceed to clear local state below.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
this.joinedConversations.clear();
|
|
174
|
+
this.clearPendingJoinAttempts();
|
|
175
|
+
await this.safeStop();
|
|
176
|
+
this.connection = null;
|
|
177
|
+
}
|
|
178
|
+
/** Detach all server→client method handlers registered via attachHandlers(). */
|
|
179
|
+
detachHandlers(conn) {
|
|
180
|
+
for (const name of ChatHubClient.HUB_EVENT_NAMES) {
|
|
181
|
+
conn.off(name);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async safeStop() {
|
|
185
|
+
if (!this.connection)
|
|
186
|
+
return;
|
|
187
|
+
// Detach handlers first so any late events on this connection are dropped.
|
|
188
|
+
this.detachHandlers(this.connection);
|
|
189
|
+
try {
|
|
190
|
+
await this.connection.stop();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Ignore errors during stop
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Re-join all tracked conversations. Called internally after reconnect.
|
|
198
|
+
* Called by ReconnectionManager as well.
|
|
199
|
+
*
|
|
200
|
+
* If a rejoin fails with an access-denied style error (e.g. user was kicked
|
|
201
|
+
* between disconnect and reconnect), the conversation is purged from
|
|
202
|
+
* `joinedConversations` so we do not retry it forever on every reconnect.
|
|
203
|
+
* Other errors keep the entry — the next reconnect will try again.
|
|
204
|
+
*/
|
|
205
|
+
async rejoinConversations() {
|
|
206
|
+
const ids = Array.from(this.joinedConversations);
|
|
207
|
+
for (const id of ids) {
|
|
208
|
+
try {
|
|
209
|
+
await this.invokeHub('JoinConversation', id);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
if (this.isAccessDeniedError(err)) {
|
|
213
|
+
this.joinedConversations.delete(id);
|
|
214
|
+
}
|
|
215
|
+
// Other errors: keep tracking so a later reconnect retries.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
isAccessDeniedError(err) {
|
|
220
|
+
if (!(err instanceof Error))
|
|
221
|
+
return false;
|
|
222
|
+
const msg = err.message.toLowerCase();
|
|
223
|
+
return (msg.includes('forbidden') ||
|
|
224
|
+
msg.includes('not a participant') ||
|
|
225
|
+
msg.includes('unauthorized') ||
|
|
226
|
+
msg.includes('access denied'));
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Hub Methods (Client → Server)
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
async invokeHub(method, ...args) {
|
|
232
|
+
if (!this.connection || this.connection.state !== HubConnectionState.Connected) {
|
|
233
|
+
throw new Error(`ChatHub is not connected (state: ${this.connection?.state ?? 'null'})`);
|
|
234
|
+
}
|
|
235
|
+
return this.connection.invoke(method, ...args);
|
|
236
|
+
}
|
|
237
|
+
async joinConversation(conversationId) {
|
|
238
|
+
await this.invokeHub('JoinConversation', conversationId);
|
|
239
|
+
this.joinedConversations.add(conversationId);
|
|
240
|
+
// Track as pending so a follow-up FORBIDDEN/UNAUTHORIZED Error event can revert it.
|
|
241
|
+
this.markJoinPending(conversationId);
|
|
242
|
+
}
|
|
243
|
+
async leaveConversation(conversationId) {
|
|
244
|
+
await this.invokeHub('LeaveConversation', conversationId);
|
|
245
|
+
this.joinedConversations.delete(conversationId);
|
|
246
|
+
}
|
|
247
|
+
async startTyping(conversationId) {
|
|
248
|
+
await this.invokeHub('StartTyping', conversationId);
|
|
249
|
+
}
|
|
250
|
+
async stopTyping(conversationId) {
|
|
251
|
+
await this.invokeHub('StopTyping', conversationId);
|
|
252
|
+
}
|
|
253
|
+
async sendMessage(request) {
|
|
254
|
+
// Spec §6: `$type` must be the first property in each block when sent to
|
|
255
|
+
// the server, otherwise the polymorphic deserializer rejects the payload.
|
|
256
|
+
const normalized = {
|
|
257
|
+
...request,
|
|
258
|
+
blocks: normalizeBlocksForSend(request.blocks),
|
|
259
|
+
};
|
|
260
|
+
return this.invokeHub('SendMessage', normalized);
|
|
261
|
+
}
|
|
262
|
+
async editMessage(request) {
|
|
263
|
+
const normalized = {
|
|
264
|
+
...request,
|
|
265
|
+
newBlocks: normalizeBlocksForSend(request.newBlocks),
|
|
266
|
+
};
|
|
267
|
+
return this.invokeHub('EditMessage', normalized);
|
|
268
|
+
}
|
|
269
|
+
/** Server expects an object { messageId }, not a bare string */
|
|
270
|
+
async deleteMessage(request) {
|
|
271
|
+
return this.invokeHub('DeleteMessage', request);
|
|
272
|
+
}
|
|
273
|
+
/** Server expects an object { messageId }, not a bare string */
|
|
274
|
+
async recoverMessage(request) {
|
|
275
|
+
return this.invokeHub('RecoverMessage', request);
|
|
276
|
+
}
|
|
277
|
+
async addReaction(request) {
|
|
278
|
+
return this.invokeHub('AddReaction', request);
|
|
279
|
+
}
|
|
280
|
+
async removeReaction(request) {
|
|
281
|
+
return this.invokeHub('RemoveReaction', request);
|
|
282
|
+
}
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Event subscription (Server → Client)
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
on(event, handler) {
|
|
287
|
+
return this.emitter.on(event, handler);
|
|
288
|
+
}
|
|
289
|
+
off(event, handler) {
|
|
290
|
+
this.emitter.off(event, handler);
|
|
291
|
+
}
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Pending join cleanup — guards against the spec edge case where the server
|
|
294
|
+
// accepts the JoinConversation invoke but later rejects via an `Error` event
|
|
295
|
+
// (e.g. FORBIDDEN — user is not a participant). Without this, the conversation
|
|
296
|
+
// would stay in `joinedConversations` and be wrongly auto-rejoined on reconnect.
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
markJoinPending(conversationId) {
|
|
299
|
+
const existing = this.pendingJoinAttempts.get(conversationId);
|
|
300
|
+
if (existing)
|
|
301
|
+
clearTimeout(existing);
|
|
302
|
+
const timer = setTimeout(() => this.pendingJoinAttempts.delete(conversationId), ChatHubClient.PENDING_JOIN_TTL_MS);
|
|
303
|
+
this.pendingJoinAttempts.set(conversationId, timer);
|
|
304
|
+
}
|
|
305
|
+
clearPendingJoinAttempts() {
|
|
306
|
+
for (const timer of this.pendingJoinAttempts.values())
|
|
307
|
+
clearTimeout(timer);
|
|
308
|
+
this.pendingJoinAttempts.clear();
|
|
309
|
+
}
|
|
310
|
+
revertJoin(conversationId) {
|
|
311
|
+
this.joinedConversations.delete(conversationId);
|
|
312
|
+
const timer = this.pendingJoinAttempts.get(conversationId);
|
|
313
|
+
if (timer)
|
|
314
|
+
clearTimeout(timer);
|
|
315
|
+
this.pendingJoinAttempts.delete(conversationId);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* If a server-emitted Error indicates a join was rejected, remove the
|
|
319
|
+
* conversation from `joinedConversations` to prevent auto-rejoin on reconnect.
|
|
320
|
+
*
|
|
321
|
+
* Strategy:
|
|
322
|
+
* 1. If `error.details.conversationId` is provided AND matches a pending
|
|
323
|
+
* join attempt → revert that one (most reliable signal).
|
|
324
|
+
* 2. Else, if the message matches the spec's documented FORBIDDEN text
|
|
325
|
+
* ("not a participant") AND there is exactly one pending join → revert it.
|
|
326
|
+
* 3. Otherwise do nothing — we cannot safely correlate the error to a join.
|
|
327
|
+
*/
|
|
328
|
+
handlePotentialJoinFailure(error) {
|
|
329
|
+
if (error.code !== 'FORBIDDEN' && error.code !== 'UNAUTHORIZED')
|
|
330
|
+
return;
|
|
331
|
+
const detailsConvId = error.details && typeof error.details === 'object'
|
|
332
|
+
? error.details.conversationId
|
|
333
|
+
: undefined;
|
|
334
|
+
if (typeof detailsConvId === 'string' && this.pendingJoinAttempts.has(detailsConvId)) {
|
|
335
|
+
this.revertJoin(detailsConvId);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (/not a participant/i.test(error.message) && this.pendingJoinAttempts.size === 1) {
|
|
339
|
+
const onlyId = this.pendingJoinAttempts.keys().next().value;
|
|
340
|
+
if (onlyId)
|
|
341
|
+
this.revertJoin(onlyId);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
ChatHubClient.PENDING_JOIN_TTL_MS = 2000;
|
|
346
|
+
/** Server→client method names registered via conn.on() — used for cleanup. */
|
|
347
|
+
ChatHubClient.HUB_EVENT_NAMES = [
|
|
348
|
+
'MessageReceived',
|
|
349
|
+
'MessageUpdated',
|
|
350
|
+
'MessageDeleted',
|
|
351
|
+
'MessageRecovered',
|
|
352
|
+
'MessageThumbnailsReady',
|
|
353
|
+
'ReactionAdded',
|
|
354
|
+
'ReactionRemoved',
|
|
355
|
+
'TypingStarted',
|
|
356
|
+
'TypingStopped',
|
|
357
|
+
'StreamStarted',
|
|
358
|
+
'StreamStatusUpdated',
|
|
359
|
+
'StreamChunkReceived',
|
|
360
|
+
'StreamChunkBatchReceived',
|
|
361
|
+
'StreamCompleted',
|
|
362
|
+
'StreamAborted',
|
|
363
|
+
'Error',
|
|
364
|
+
];
|
|
365
|
+
//# 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,89 @@
|
|
|
1
|
+
import { HubConnectionState, LogLevel } from '@microsoft/signalr';
|
|
2
|
+
import type { Unsubscribe } from '../utils/TypedEventEmitter.js';
|
|
3
|
+
import type { NewMessageNotificationDto, MentionedNotificationDto, ConversationCreatedDto, ConversationUpdatedDto, ParticipantJoinedDto, ParticipantLeftDto, ConversationPinnedDto, ConversationUnpinnedDto } from '../types/notification-events.js';
|
|
4
|
+
export interface NotificationHubEventMap {
|
|
5
|
+
newMessageNotification: NewMessageNotificationDto;
|
|
6
|
+
mentionedNotification: MentionedNotificationDto;
|
|
7
|
+
conversationCreated: ConversationCreatedDto;
|
|
8
|
+
conversationUpdated: ConversationUpdatedDto;
|
|
9
|
+
participantJoined: ParticipantJoinedDto;
|
|
10
|
+
participantLeft: ParticipantLeftDto;
|
|
11
|
+
conversationPinned: ConversationPinnedDto;
|
|
12
|
+
conversationUnpinned: ConversationUnpinnedDto;
|
|
13
|
+
reconnecting: Error | undefined;
|
|
14
|
+
reconnected: string | undefined;
|
|
15
|
+
disconnected: Error | undefined;
|
|
16
|
+
}
|
|
17
|
+
export interface NotificationHubClientOptions {
|
|
18
|
+
/** Full URL of the NotificationHub endpoint, e.g. "https://api.example.com/hubs/notifications" */
|
|
19
|
+
hubUrl: string;
|
|
20
|
+
/** Returns the current JWT token to use as access_token query param */
|
|
21
|
+
tokenProvider: () => string | null;
|
|
22
|
+
/** Minimum log level for SignalR internal logging (default: Warning) */
|
|
23
|
+
logLevel?: LogLevel;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Typed client for the NotificationHub (`/hubs/notifications`).
|
|
27
|
+
*
|
|
28
|
+
* Per spec §4: NotificationHub has **no client-callable hub methods** —
|
|
29
|
+
* after connecting, the server automatically pushes events to the user's
|
|
30
|
+
* personal group (`participant:{userId}`). There is also no `Error` event
|
|
31
|
+
* on this hub: invalid/expired JWT is rejected at the gateway (HTTP 401)
|
|
32
|
+
* and a missing `sub` claim aborts the connection — both silent on the
|
|
33
|
+
* wire. Register the `Error` listener on ChatHub instead.
|
|
34
|
+
*
|
|
35
|
+
* Responsibilities:
|
|
36
|
+
* - Manages HubConnection lifecycle (connect / disconnect / auto-reconnect)
|
|
37
|
+
* - Exposes typed event subscription (Server → Client)
|
|
38
|
+
*/
|
|
39
|
+
export declare class NotificationHubClient {
|
|
40
|
+
private readonly options;
|
|
41
|
+
private connection;
|
|
42
|
+
private readonly emitter;
|
|
43
|
+
/** In-flight connect Promise, used to dedupe concurrent connect() calls. */
|
|
44
|
+
private connectingPromise;
|
|
45
|
+
/**
|
|
46
|
+
* True after a user-initiated disconnect() until the next connect() begins.
|
|
47
|
+
* ReconnectionManager uses this to skip auto-reconnect when the close was
|
|
48
|
+
* intentional rather than caused by a transport failure.
|
|
49
|
+
*/
|
|
50
|
+
private _intentionallyClosed;
|
|
51
|
+
/** Server→client method names registered via conn.on() — used for cleanup. */
|
|
52
|
+
private static readonly HUB_EVENT_NAMES;
|
|
53
|
+
constructor(options: NotificationHubClientOptions);
|
|
54
|
+
get state(): HubConnectionState;
|
|
55
|
+
/**
|
|
56
|
+
* True after a user-initiated disconnect() and until the next connect() starts.
|
|
57
|
+
* ReconnectionManager honors this to avoid reconnecting after an intentional close.
|
|
58
|
+
*/
|
|
59
|
+
get intentionallyClosed(): boolean;
|
|
60
|
+
/** Build a new HubConnection using the latest token from tokenProvider */
|
|
61
|
+
private buildConnection;
|
|
62
|
+
/** Attach SignalR server→client event handlers to the connection */
|
|
63
|
+
private attachHandlers;
|
|
64
|
+
/**
|
|
65
|
+
* Connect to NotificationHub. If already connected, resolves immediately.
|
|
66
|
+
* Concurrent calls share a single in-flight Promise — building two
|
|
67
|
+
* connections at once would leak handlers.
|
|
68
|
+
* Builds a fresh connection using the current token.
|
|
69
|
+
*
|
|
70
|
+
* Throws synchronously when no authentication token is available so the
|
|
71
|
+
* caller gets a clear error rather than a malformed Authorization header.
|
|
72
|
+
*/
|
|
73
|
+
connect(): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Disconnect from NotificationHub.
|
|
76
|
+
* If a connect is in flight, it is awaited first so we always tear down a
|
|
77
|
+
* fully-formed connection (not a half-built one).
|
|
78
|
+
*
|
|
79
|
+
* Sets `intentionallyClosed` so ReconnectionManager skips its reconnect
|
|
80
|
+
* attempt for the resulting `disconnected` event.
|
|
81
|
+
*/
|
|
82
|
+
disconnect(): Promise<void>;
|
|
83
|
+
/** Detach all server→client method handlers registered via attachHandlers(). */
|
|
84
|
+
private detachHandlers;
|
|
85
|
+
private safeStop;
|
|
86
|
+
on<K extends keyof NotificationHubEventMap>(event: K, handler: (payload: NotificationHubEventMap[K]) => void): Unsubscribe;
|
|
87
|
+
off<K extends keyof NotificationHubEventMap>(event: K, handler: (payload: NotificationHubEventMap[K]) => void): void;
|
|
88
|
+
}
|
|
89
|
+
//# 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,yBAAyB,EACzB,wBAAwB,EACxB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,uBAAuB,EACxB,MAAM,iCAAiC,CAAC;AAMzC,MAAM,WAAW,uBAAuB;IAEtC,sBAAsB,EAAE,yBAAyB,CAAC;IAClD,qBAAqB,EAAE,wBAAwB,CAAC;IAGhD,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;IAG9C,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;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6C;IAErE,4EAA4E;IAC5E,OAAO,CAAC,iBAAiB,CAA8B;IAEvD;;;;OAIG;IACH,OAAO,CAAC,oBAAoB,CAAS;IAErC,8EAA8E;IAC9E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAS5B;gBAEC,OAAO,EAAE,4BAA4B;IASjD,IAAI,KAAK,IAAI,kBAAkB,CAE9B;IAED;;;OAGG;IACH,IAAI,mBAAmB,IAAI,OAAO,CAEjC;IAMD,0EAA0E;IAC1E,OAAO,CAAC,eAAe;IAmBvB,oEAAoE;IACpE,OAAO,CAAC,cAAc;IA2BtB;;;;;;;;OAQG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAuC9B;;;;;;;OAOG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBjC,gFAAgF;IAChF,OAAO,CAAC,cAAc;YAMR,QAAQ;IAetB,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,191 @@
|
|
|
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
|
+
* Per spec §4: NotificationHub has **no client-callable hub methods** —
|
|
11
|
+
* after connecting, the server automatically pushes events to the user's
|
|
12
|
+
* personal group (`participant:{userId}`). There is also no `Error` event
|
|
13
|
+
* on this hub: invalid/expired JWT is rejected at the gateway (HTTP 401)
|
|
14
|
+
* and a missing `sub` claim aborts the connection — both silent on the
|
|
15
|
+
* wire. Register the `Error` listener on ChatHub instead.
|
|
16
|
+
*
|
|
17
|
+
* Responsibilities:
|
|
18
|
+
* - Manages HubConnection lifecycle (connect / disconnect / auto-reconnect)
|
|
19
|
+
* - Exposes typed event subscription (Server → Client)
|
|
20
|
+
*/
|
|
21
|
+
export class NotificationHubClient {
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.connection = null;
|
|
24
|
+
/** In-flight connect Promise, used to dedupe concurrent connect() calls. */
|
|
25
|
+
this.connectingPromise = null;
|
|
26
|
+
/**
|
|
27
|
+
* True after a user-initiated disconnect() until the next connect() begins.
|
|
28
|
+
* ReconnectionManager uses this to skip auto-reconnect when the close was
|
|
29
|
+
* intentional rather than caused by a transport failure.
|
|
30
|
+
*/
|
|
31
|
+
this._intentionallyClosed = false;
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.emitter = new EventEmitter();
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Connection state
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
get state() {
|
|
39
|
+
return this.connection?.state ?? HubConnectionState.Disconnected;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* True after a user-initiated disconnect() and until the next connect() starts.
|
|
43
|
+
* ReconnectionManager honors this to avoid reconnecting after an intentional close.
|
|
44
|
+
*/
|
|
45
|
+
get intentionallyClosed() {
|
|
46
|
+
return this._intentionallyClosed;
|
|
47
|
+
}
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Connection management
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
/** Build a new HubConnection using the latest token from tokenProvider */
|
|
52
|
+
buildConnection() {
|
|
53
|
+
const { hubUrl, tokenProvider, logLevel = LogLevel.Warning } = this.options;
|
|
54
|
+
return new HubConnectionBuilder()
|
|
55
|
+
.withUrl(hubUrl, {
|
|
56
|
+
// Throw rather than return '' so SignalR fails the request loudly with
|
|
57
|
+
// a useful error instead of sending an empty `Authorization: Bearer ` header.
|
|
58
|
+
accessTokenFactory: () => {
|
|
59
|
+
const t = tokenProvider();
|
|
60
|
+
if (!t) {
|
|
61
|
+
throw new Error('NotificationHub: no authentication token available');
|
|
62
|
+
}
|
|
63
|
+
return t;
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
.withAutomaticReconnect()
|
|
67
|
+
.configureLogging(logLevel)
|
|
68
|
+
.build();
|
|
69
|
+
}
|
|
70
|
+
/** Attach SignalR server→client event handlers to the connection */
|
|
71
|
+
attachHandlers(conn) {
|
|
72
|
+
// Message notifications
|
|
73
|
+
conn.on('NewMessageNotification', (dto) => this.emitter.emit('newMessageNotification', dto));
|
|
74
|
+
conn.on('MentionedNotification', (dto) => this.emitter.emit('mentionedNotification', dto));
|
|
75
|
+
// Conversation events
|
|
76
|
+
conn.on('ConversationCreated', (dto) => this.emitter.emit('conversationCreated', dto));
|
|
77
|
+
conn.on('ConversationUpdated', (dto) => this.emitter.emit('conversationUpdated', dto));
|
|
78
|
+
conn.on('ParticipantJoined', (dto) => this.emitter.emit('participantJoined', dto));
|
|
79
|
+
conn.on('ParticipantLeft', (dto) => this.emitter.emit('participantLeft', dto));
|
|
80
|
+
conn.on('ConversationPinned', (dto) => this.emitter.emit('conversationPinned', dto));
|
|
81
|
+
conn.on('ConversationUnpinned', (dto) => this.emitter.emit('conversationUnpinned', dto));
|
|
82
|
+
// Connection lifecycle
|
|
83
|
+
conn.onreconnecting((err) => this.emitter.emit('reconnecting', err));
|
|
84
|
+
conn.onreconnected((connectionId) => this.emitter.emit('reconnected', connectionId));
|
|
85
|
+
conn.onclose((err) => this.emitter.emit('disconnected', err));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Connect to NotificationHub. If already connected, resolves immediately.
|
|
89
|
+
* Concurrent calls share a single in-flight Promise — building two
|
|
90
|
+
* connections at once would leak handlers.
|
|
91
|
+
* Builds a fresh connection using the current token.
|
|
92
|
+
*
|
|
93
|
+
* Throws synchronously when no authentication token is available so the
|
|
94
|
+
* caller gets a clear error rather than a malformed Authorization header.
|
|
95
|
+
*/
|
|
96
|
+
async connect() {
|
|
97
|
+
if (this.connection && this.connection.state === HubConnectionState.Connected) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (this.connectingPromise) {
|
|
101
|
+
return this.connectingPromise;
|
|
102
|
+
}
|
|
103
|
+
if (!this.options.tokenProvider()) {
|
|
104
|
+
throw new Error('NotificationHubClient.connect(): no authentication token available. ' +
|
|
105
|
+
'Provide one via ChatClient.setToken() or the tokenProvider option.');
|
|
106
|
+
}
|
|
107
|
+
// The user is explicitly opening a connection — clear any prior
|
|
108
|
+
// intentional-close flag so ReconnectionManager will manage future drops.
|
|
109
|
+
this._intentionallyClosed = false;
|
|
110
|
+
const promise = (async () => {
|
|
111
|
+
try {
|
|
112
|
+
// Disconnect existing stale connection cleanly
|
|
113
|
+
if (this.connection) {
|
|
114
|
+
await this.safeStop();
|
|
115
|
+
}
|
|
116
|
+
const conn = this.buildConnection();
|
|
117
|
+
this.attachHandlers(conn);
|
|
118
|
+
this.connection = conn;
|
|
119
|
+
await conn.start();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
this.connectingPromise = null;
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
this.connectingPromise = promise;
|
|
126
|
+
return promise;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Disconnect from NotificationHub.
|
|
130
|
+
* If a connect is in flight, it is awaited first so we always tear down a
|
|
131
|
+
* fully-formed connection (not a half-built one).
|
|
132
|
+
*
|
|
133
|
+
* Sets `intentionallyClosed` so ReconnectionManager skips its reconnect
|
|
134
|
+
* attempt for the resulting `disconnected` event.
|
|
135
|
+
*/
|
|
136
|
+
async disconnect() {
|
|
137
|
+
// Mark intentional BEFORE awaiting the in-flight connect so any
|
|
138
|
+
// disconnected events that fire during teardown are not auto-reconnected.
|
|
139
|
+
this._intentionallyClosed = true;
|
|
140
|
+
if (this.connectingPromise) {
|
|
141
|
+
try {
|
|
142
|
+
await this.connectingPromise;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// The connect failed — there's nothing established to tear down,
|
|
146
|
+
// but still proceed to clear local state below.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
await this.safeStop();
|
|
150
|
+
this.connection = null;
|
|
151
|
+
}
|
|
152
|
+
/** Detach all server→client method handlers registered via attachHandlers(). */
|
|
153
|
+
detachHandlers(conn) {
|
|
154
|
+
for (const name of NotificationHubClient.HUB_EVENT_NAMES) {
|
|
155
|
+
conn.off(name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async safeStop() {
|
|
159
|
+
if (!this.connection)
|
|
160
|
+
return;
|
|
161
|
+
// Detach handlers first so any late events on this connection are dropped.
|
|
162
|
+
this.detachHandlers(this.connection);
|
|
163
|
+
try {
|
|
164
|
+
await this.connection.stop();
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Ignore errors during stop
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Event subscription (Server → Client)
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
on(event, handler) {
|
|
174
|
+
return this.emitter.on(event, handler);
|
|
175
|
+
}
|
|
176
|
+
off(event, handler) {
|
|
177
|
+
this.emitter.off(event, handler);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Server→client method names registered via conn.on() — used for cleanup. */
|
|
181
|
+
NotificationHubClient.HUB_EVENT_NAMES = [
|
|
182
|
+
'NewMessageNotification',
|
|
183
|
+
'MentionedNotification',
|
|
184
|
+
'ConversationCreated',
|
|
185
|
+
'ConversationUpdated',
|
|
186
|
+
'ParticipantJoined',
|
|
187
|
+
'ParticipantLeft',
|
|
188
|
+
'ConversationPinned',
|
|
189
|
+
'ConversationUnpinned',
|
|
190
|
+
];
|
|
191
|
+
//# sourceMappingURL=NotificationHubClient.js.map
|